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
+179 -50

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "2.23.29",
"version": "2.23.30",
"bin": "bin/hydrooj.js",
"main": "dist/loader.js",
"typings": "dist/loader.d.ts",
+47 -34
View File
@@ -55,7 +55,8 @@ export class ProblemHandler extends Handler {
export class ProblemMainHandler extends ProblemHandler {
@param('page', Types.PositiveInt, 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';
// eslint-disable-next-line @typescript-eslint/no-shadow
const query: FilterQuery<Pdoc> = {};
@@ -66,9 +67,17 @@ export class ProblemMainHandler extends ProblemHandler {
];
const search = global.Hydro.lib.problemSearch;
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.push([`${this.translate('Keyword')}: ${q}`, null, null, true]);
path.push([category.join('+'), null, null, true]);
}
if (q) {
if (search) {
const result = await search(domainId, q);
query.docId = { $in: result };
@@ -94,7 +103,7 @@ export class ProblemMainHandler extends ProblemHandler {
);
}
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);
this.back({ star: false });
}
}
export class ProblemCategoryHandler extends ProblemHandler {
@param('page', Types.PositiveInt, true)
@param('category', Types.Name, null, parseCategory)
async get(domainId: string, page = 1, category: string[]) {
this.response.template = 'problem_main.html';
const q: any = { $and: [] };
for (const tag of category) q.$and.push({ tag });
let psdict = {};
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),
);
@param('pids', Types.NumericArray)
async postDelete(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.del(domainId, pid);
}
const path = [
['Hydro', 'homepage'],
['problem_main', 'problem_main'],
[category, null, null, true],
];
this.response.body = {
path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'),
};
this.extraTitleContent = category.join('+');
this.back();
}
@param('pids', Types.NumericArray)
async postHide(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: 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() {
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_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_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() {
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.redirect && !this.request.json) {
this.ctx.response.type = 'application/octet-stream';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.6.21",
"version": "4.6.22",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -1,9 +1,15 @@
import _ from 'lodash';
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 pjax from 'vj/utils/pjax';
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 dirtyCategories = [];
@@ -56,6 +62,8 @@ async function updateSelection(sendRequest = true) {
.join('+'), // build a beautiful URL
});
}
const q = $('[name="q"]').val();
if (q) url += `?q=${q}`;
pjax.request({ url });
}
}
@@ -160,9 +168,58 @@ function parseCategorySelection() {
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 doc = document.documentElement;
doc.className = doc.className + ' display-mode';
buildCategoryFilter();
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;
@@ -1,8 +1,14 @@
.page--problem_main
.page--problem_category
.page--problem_settings
.col--checkbox
width: rem(60px)
.col--pid
width: rem(80px)
.col--status
width: rem(150px)
width: rem(140px)
position: relative
.col--submit_n, .col--ac_rate, .col--difficulty
@@ -97,3 +103,10 @@
margin-left: 0
margin-right: 0
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') %}
<td class="col--status record-status--border {{ model.builtin.STATUS_CODES[rdoc.status] }}">
{% macro render_status_td(rdoc, rid_key='_id', class='') %}
<td class="col--status record-status--border {{ class }} {{ model.builtin.STATUS_CODES[rdoc.status] }}">
<div class="col--status__text">
<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] }}">
@@ -8,8 +8,12 @@
{% else %}
<table class="data-table">
<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) %}
<col class="col--status">
<col class="col--status edit-mode-hide">
{% endif %}
<col class="col--name">
<col class="col--submit_n">
@@ -18,8 +22,16 @@
</colgroup>
<thead>
<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) %}
<th class="col--status record-status--border">{{ _('Status') }}</th>
<th class="col--status record-status--border edit-mode-hide">{{ _('Status') }}</th>
{% endif %}
<th class="col--name">{{ _('Problem') }}</th>
<th class="col--submit_n">{{ _('Submit') }}</th>
@@ -30,13 +42,20 @@
<tbody>
{%- for pdoc in pdocs -%}
{% set psdoc = psdict[pdoc['docId']] %}
<tr>
<tr data-pid="{{ pdoc.docId }}">
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
<td class="col--checkbox display-mode-hide">
<label class="compact checkbox">
<input type="checkbox" data-checkbox-group="problem" data-checkbox-range>
</label>
</td>
<td class="col--pid display-mode-hide">{{ pdoc.docId }}</td>
{% endif %}
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
{% if psdoc['rid'] %}
{{ record.render_status_td(psdoc, rid_key='rid') }}
{{ record.render_status_td(psdoc, rid_key='rid', class="edit-mode-hide") }}
{% else %}
<td class="col--status record-status--border">
</td>
<td class="col--status record-status--border edit-mode-hide"></td>
{% endif %}
{% endif %}
<td class="col--name col--problem-name">
@@ -129,5 +129,4 @@
{% include "partials/problem_sidebar.html" %}
</div>
</div>
{% endblock %}
@@ -16,14 +16,42 @@
</div>
</div>
<div class="medium-3 columns">
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
<div class="section side">
<div class="section__header">
<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__header edit-mode-hide">
<h1 class="section__title">{{ _('Categories') }}</h1>
</div>
<div class="section__body">
{% include "partials/category.html" %}
</div>
<div class="section__body">
<div class="section__body edit-mode-hide">
{% include "partials/problem_lucky.html" %}
</div>
</div>
@@ -36,7 +64,7 @@
<label>
<input name="q" type="text" class="textbox" value="{{ category }}" placeholder="1001">
</label>
<button type="submit" class="primary button">{{ _('Search') }}</button>
<button type="submit" id="search" class="primary button">{{ _('Search') }}</button>
</div>
</form>
</div>