ui: problem_edit tools

This commit is contained in:
undefined committed 2021-04-30 01:34:52 +08:00
1 parent 1910a57dbb
commit 3664df4202
10 files changed
+187 -58

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "2.23.29", "version": "2.23.30",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "dist/loader.js", "main": "dist/loader.js",
"typings": "dist/loader.d.ts", "typings": "dist/loader.d.ts",
+47 -34
View File
@@ -55,7 +55,8 @@ export class ProblemHandler extends Handler {
export class ProblemMainHandler extends ProblemHandler { export class ProblemMainHandler extends ProblemHandler {
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
@param('q', Types.Content, true) @param('q', Types.Content, true)
async get(domainId: string, page = 1, q = '') { @param('category', Types.Name, true, null, parseCategory)
async get(domainId: string, page = 1, q = '', category = []) {
this.response.template = 'problem_main.html'; this.response.template = 'problem_main.html';
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
const query: FilterQuery<Pdoc> = {}; const query: FilterQuery<Pdoc> = {};
@@ -66,9 +67,17 @@ export class ProblemMainHandler extends ProblemHandler {
]; ];
const search = global.Hydro.lib.problemSearch; const search = global.Hydro.lib.problemSearch;
let sort: number[]; let sort: number[];
if (q) { if (category.length) {
query.$and = [];
for (const tag of category) query.$and.push({ tag });
}
if (q) category.push(q);
if (category.length) {
this.extraTitleContent = category.join('+');
path[1][1] = 'problem_main'; path[1][1] = 'problem_main';
path.push([`${this.translate('Keyword')}: ${q}`, null, null, true]); path.push([category.join('+'), null, null, true]);
}
if (q) {
if (search) { if (search) {
const result = await search(domainId, q); const result = await search(domainId, q);
query.docId = { $in: result }; query.docId = { $in: result };
@@ -94,7 +103,7 @@ export class ProblemMainHandler extends ProblemHandler {
); );
} }
this.response.body = { this.response.body = {
path, page, pcount, ppcount, pdocs, psdict, category: q, path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'),
}; };
} }
@@ -109,37 +118,41 @@ export class ProblemMainHandler extends ProblemHandler {
await problem.setStar(domainId, pid, this.user._id, false); await problem.setStar(domainId, pid, this.user._id, false);
this.back({ star: false }); this.back({ star: false });
} }
}
export class ProblemCategoryHandler extends ProblemHandler { @param('pids', Types.NumericArray)
@param('page', Types.PositiveInt, true) async postDelete(domainId: string, pids: number[]) {
@param('category', Types.Name, null, parseCategory) for (const pid of pids) {
async get(domainId: string, page = 1, category: string[]) { // eslint-disable-next-line no-await-in-loop
this.response.template = 'problem_main.html'; const pdoc = await problem.get(domainId, pid);
const q: any = { $and: [] }; if (pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
for (const tag of category) q.$and.push({ tag }); // eslint-disable-next-line no-await-in-loop
let psdict = {}; await problem.del(domainId, pid);
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
await bus.serial('problem/list', q, this);
const [pdocs, ppcount, pcount] = await paginate(
problem.getMulti(domainId, q).sort({ pid: 1, docId: 1 }),
page,
system.get('pagination.problem'),
);
if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) {
psdict = await problem.getListStatus(
domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId),
);
} }
const path = [ this.back();
['Hydro', 'homepage'], }
['problem_main', 'problem_main'],
[category, null, null, true], @param('pids', Types.NumericArray)
]; async postHide(domainId: string, pids: number[]) {
this.response.body = { for (const pid of pids) {
path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'), // eslint-disable-next-line no-await-in-loop
}; const pdoc = await problem.get(domainId, pid);
this.extraTitleContent = category.join('+'); if (pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
// eslint-disable-next-line no-await-in-loop
await problem.edit(domainId, pid, { hidden: true });
}
this.back();
}
@param('pids', Types.NumericArray)
async postUnhide(domainId: string, pids: number[]) {
for (const pid of pids) {
// eslint-disable-next-line no-await-in-loop
const pdoc = await problem.get(domainId, pid);
if (pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
// eslint-disable-next-line no-await-in-loop
await problem.edit(domainId, pid, { hidden: false });
}
this.back();
} }
} }
@@ -599,7 +612,7 @@ export class ProblemPrefixListHandler extends Handler {
export async function apply() { export async function apply() {
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_category', '/p/category/:category', ProblemCategoryHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_category', '/p/category/:category', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM); Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
+1 -1
View File
@@ -567,7 +567,7 @@ export class Handler extends HandlerCommon {
} }
async putResponse() { async putResponse() {
if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition); if (this.response.disposition) this.ctx.set('Content-Disposition', encodeURIComponent(this.response.disposition));
if (!this.response.body) return; if (!this.response.body) return;
if (this.response.redirect && !this.request.json) { if (this.response.redirect && !this.request.json) {
this.ctx.response.type = 'application/octet-stream'; this.ctx.response.type = 'application/octet-stream';
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.6.21", "version": "4.6.22",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -1,9 +1,15 @@
import _ from 'lodash'; import _ from 'lodash';
import { NamedPage } from 'vj/misc/Page'; import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import { ConfirmDialog } from 'vj/components/dialog';
import Dropdown from 'vj/components/dropdown/Dropdown'; import Dropdown from 'vj/components/dropdown/Dropdown';
import pjax from 'vj/utils/pjax'; import pjax from 'vj/utils/pjax';
import substitute from 'vj/utils/substitute'; import substitute from 'vj/utils/substitute';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
const categories = {}; const categories = {};
const dirtyCategories = []; const dirtyCategories = [];
@@ -56,6 +62,8 @@ async function updateSelection(sendRequest = true) {
.join('+'), // build a beautiful URL .join('+'), // build a beautiful URL
}); });
} }
const q = $('[name="q"]').val();
if (q) url += `?q=${q}`;
pjax.request({ url }); pjax.request({ url });
} }
} }
@@ -160,9 +168,58 @@ function parseCategorySelection() {
updateSelection(false); updateSelection(false);
} }
function ensureAndGetSelectedPids() {
const pids = _.map(
$(`tbody [data-checkbox-group="problem"]:checked`),
(ch) => $(ch).closest('tr').attr('data-pid'),
);
if (pids.length === 0) {
Notification.error(i18n('Please select at least one problem to perform this operation.'));
return null;
}
return pids;
}
async function handleOperation(operation) {
const pids = ensureAndGetSelectedPids();
if (pids === null) return;
if (operation === 'delete') {
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n('Confirm to delete the selected problems?')}</p>
</div>`,
}).open();
if (action !== 'yes') return;
}
try {
await request.post('', { operation, pids });
Notification.success(i18n(`Selected problems have been ${operation}d.`));
await delay(2000);
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
const page = new NamedPage(['problem_main', 'problem_category'], () => { const page = new NamedPage(['problem_main', 'problem_category'], () => {
const doc = document.documentElement;
doc.className = doc.className + ' display-mode';
buildCategoryFilter(); buildCategoryFilter();
parseCategorySelection(); parseCategorySelection();
$('[name="leave-edit-mode"]').on('click', () => {
doc.className = doc.className.replace(' edit-mode', ' display-mode');
});
$('[name="enter-edit-mode"]').on('click', () => {
doc.className = doc.className.replace(' display-mode', ' edit-mode');
});
$('[name="remove_selected_problems"]').on('click', () => handleOperation('delete'));
$('[name="hide_selected_problems"]').on('click', () => handleOperation('hide'));
$('[name="unhide_selected_problems"]').on('click', () => handleOperation('unhide'));
$('#search').on('click', (ev) => {
ev.preventDefault();
updateSelection();
});
}); });
export default page; export default page;
@@ -1,8 +1,14 @@
.page--problem_main .page--problem_main
.page--problem_category .page--problem_category
.page--problem_settings .page--problem_settings
.col--checkbox
width: rem(60px)
.col--pid
width: rem(80px)
.col--status .col--status
width: rem(150px) width: rem(140px)
position: relative position: relative
.col--submit_n, .col--ac_rate, .col--difficulty .col--submit_n, .col--ac_rate, .col--difficulty
@@ -97,3 +103,10 @@
margin-left: 0 margin-left: 0
margin-right: 0 margin-right: 0
margin-top: rem(floor($menu-drop-triangle-size * -1.414)) margin-top: rem(floor($menu-drop-triangle-size * -1.414))
.display-mode .display-mode-hide
display: none
.edit-mode .edit-mode-hide
display: none
@@ -1,5 +1,5 @@
{% macro render_status_td(rdoc, rid_key='_id') %} {% macro render_status_td(rdoc, rid_key='_id', class='') %}
<td class="col--status record-status--border {{ model.builtin.STATUS_CODES[rdoc.status] }}"> <td class="col--status record-status--border {{ class }} {{ model.builtin.STATUS_CODES[rdoc.status] }}">
<div class="col--status__text"> <div class="col--status__text">
<span class="icon record-status--icon {{ model.builtin.STATUS_CODES[rdoc.status] }}"></span> <span class="icon record-status--icon {{ model.builtin.STATUS_CODES[rdoc.status] }}"></span>
<a href="{{ url('record_detail', rid=rdoc[rid_key]) }}" class="record-status--text {{ model.builtin.STATUS_CODES[rdoc.status] }}"> <a href="{{ url('record_detail', rid=rdoc[rid_key]) }}" class="record-status--text {{ model.builtin.STATUS_CODES[rdoc.status] }}">
@@ -8,8 +8,12 @@
{% else %} {% else %}
<table class="data-table"> <table class="data-table">
<colgroup> <colgroup>
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
<col class="col--checkbox display-mode-hide">
<col class="col--pid display-mode-hide">
{% endif %}
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
<col class="col--status"> <col class="col--status edit-mode-hide">
{% endif %} {% endif %}
<col class="col--name"> <col class="col--name">
<col class="col--submit_n"> <col class="col--submit_n">
@@ -18,8 +22,16 @@
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
<th class="col--checkbox display-mode-hide">
<label class="compact checkbox">
<input type="checkbox" name="select_all" data-checkbox-toggle="problem">
</label>
</th>
<th class="col--pid display-mode-hide">{{ _('ID') }}</th>
{% endif %}
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
<th class="col--status record-status--border">{{ _('Status') }}</th> <th class="col--status record-status--border edit-mode-hide">{{ _('Status') }}</th>
{% endif %} {% endif %}
<th class="col--name">{{ _('Problem') }}</th> <th class="col--name">{{ _('Problem') }}</th>
<th class="col--submit_n">{{ _('Submit') }}</th> <th class="col--submit_n">{{ _('Submit') }}</th>
@@ -28,17 +40,24 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{%- for pdoc in pdocs -%} {%- for pdoc in pdocs -%}
{% set psdoc = psdict[pdoc['docId']] %} {% set psdoc = psdict[pdoc['docId']] %}
<tr> <tr data-pid="{{ pdoc.docId }}">
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
{% if psdoc['rid'] %} <td class="col--checkbox display-mode-hide">
{{ record.render_status_td(psdoc, rid_key='rid') }} <label class="compact checkbox">
{% else %} <input type="checkbox" data-checkbox-group="problem" data-checkbox-range>
<td class="col--status record-status--border"> </label>
</td> </td>
{% endif %} <td class="col--pid display-mode-hide">{{ pdoc.docId }}</td>
{% endif %} {% endif %}
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
{% if psdoc['rid'] %}
{{ record.render_status_td(psdoc, rid_key='rid', class="edit-mode-hide") }}
{% else %}
<td class="col--status record-status--border edit-mode-hide"></td>
{% endif %}
{% endif %}
<td class="col--name col--problem-name"> <td class="col--name col--problem-name">
{{ problem.render_problem_title(pdoc) }} {{ problem.render_problem_title(pdoc) }}
</td> </td>
@@ -60,7 +60,7 @@
<div class="section__body"> <div class="section__body">
<button class="rounded button" name="download_selected_testdata">{{ _('Download Selected') }}</button> <button class="rounded button" name="download_selected_testdata">{{ _('Download Selected') }}</button>
<button class="rounded button" name="remove_selected_testdata">{{ _('Remove Selected') }}</button> <button class="rounded button" name="remove_selected_testdata">{{ _('Remove Selected') }}</button>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -129,5 +129,4 @@
{% include "partials/problem_sidebar.html" %} {% include "partials/problem_sidebar.html" %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -16,14 +16,42 @@
</div> </div>
</div> </div>
<div class="medium-3 columns"> <div class="medium-3 columns">
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
<div class="section side">
<ol class="menu">
<a href="javascript:;" class="menu__link edit-mode-hide" name="enter-edit-mode">
<span class="icon icon-edit"></span>
{{ _('Enter Edit Mode') }}
</a>
<a href="javascript:;" class="menu__link display-mode-hide" name="leave-edit-mode">
<span class="icon icon-edit"></span>
{{ _('Leave Edit Mode') }}
</a>
{% if pdocs.length %}
<a href="javascript:;" class="menu__link display-mode-hide" name="hide_selected_problems">
<span class="icon icon-close"></span>
{{ _('Hide Selected') }}
</a>
<a href="javascript:;" class="menu__link display-mode-hide" name="unhide_selected_problems">
<span class="icon icon-check"></span>
{{ _('Unhide Selected') }}
</a>
<a href="javascript:;" class="menu__link display-mode-hide" name="remove_selected_problems">
<span class="icon icon-delete"></span>
{{ _('Remove Selected') }}
</a>
{% endif %}
</ol>
</div>
{% endif %}
<div class="section side"> <div class="section side">
<div class="section__header"> <div class="section__header edit-mode-hide">
<h1 class="section__title">{{ _('Categories') }}</h1> <h1 class="section__title">{{ _('Categories') }}</h1>
</div> </div>
<div class="section__body"> <div class="section__body">
{% include "partials/category.html" %} {% include "partials/category.html" %}
</div> </div>
<div class="section__body"> <div class="section__body edit-mode-hide">
{% include "partials/problem_lucky.html" %} {% include "partials/problem_lucky.html" %}
</div> </div>
</div> </div>
@@ -36,7 +64,7 @@
<label> <label>
<input name="q" type="text" class="textbox" value="{{ category }}" placeholder="1001"> <input name="q" type="text" class="textbox" value="{{ category }}" placeholder="1001">
</label> </label>
<button type="submit" class="primary button">{{ _('Search') }}</button> <button type="submit" id="search" class="primary button">{{ _('Search') }}</button>
</div> </div>
</form> </form>
</div> </div>