mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: problem_edit tools
This commit is contained in:
1 parent
1910a57dbb
commit
3664df4202
10 files changed
+179
-50
No files matched your search
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,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>
|
||||||
@@ -30,13 +42,20 @@
|
|||||||
<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.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 handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||||
{% if psdoc['rid'] %}
|
{% if psdoc['rid'] %}
|
||||||
{{ record.render_status_td(psdoc, rid_key='rid') }}
|
{{ record.render_status_td(psdoc, rid_key='rid', class="edit-mode-hide") }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="col--status record-status--border">
|
<td class="col--status record-status--border edit-mode-hide"></td>
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="col--name col--problem-name">
|
<td class="col--name col--problem-name">
|
||||||
|
|||||||
@@ -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">
|
<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>
|
<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>
|
||||||
|
|||||||
Reference in new issue
Block a user