diff --git a/packages/codemate-plugin/plugins/tags-entity/index.ts b/packages/codemate-plugin/plugins/tags-entity/index.ts new file mode 100644 index 0000000000..a7023513af --- /dev/null +++ b/packages/codemate-plugin/plugins/tags-entity/index.ts @@ -0,0 +1,63 @@ +import { Context, Handler, ObjectId, param, PRIV, Types } from 'hydrooj'; +import TagModel from './model'; + +class TagManageBaseHandler extends Handler { + async _prepare() { + this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); + } +} + +class TagAddHandler extends TagManageBaseHandler { + @param('name', Types.String) + @param('alias', Types.ArrayOf) + @param('description', Types.String) + async post(domainId: string, _name: string, alias: string[], _description: string) { + const name = JSON.parse(_name); + const description = JSON.parse(_description); + const docId = await TagModel.add(domainId, name, alias, description); + this.response.body = { docId }; + } +} + +class TagEditHandler extends TagManageBaseHandler { + @param('docId', Types.ObjectId) + @param('name', Types.String) + @param('alias', Types.ArrayOf) + @param('description', Types.String) + async post(domainId: string, docId: ObjectId, _name: string, alias: string[], _description: string) { + const name = JSON.parse(_name); + const description = JSON.parse(_description); + await TagModel.edit(domainId, docId, name, alias, description); + this.response.body = { docId }; + } +} + +class TagMainHandler extends Handler { + async get({ domainId }) { + const tags = await TagModel.getMulti(domainId); + this.response.body = { tags }; + } +} + +class TagDeleteHandler extends TagManageBaseHandler { + @param('docId', Types.ObjectId) + async get(domainId: string, docId: ObjectId) { + await TagModel.del(domainId, docId); + this.response.body = { success: true }; + } +} + +class TagGetHandler extends Handler { + @param('docId', Types.String) + async get(domainId: string, docId: ObjectId) { + this.response.body = { tagDoc: await TagModel.get(domainId, docId) }; + } +} + +export async function apply(ctx: Context) { + ctx.Route('tag_add', '/tag/add', TagAddHandler); + ctx.Route('tag_edit', '/tag/edit', TagEditHandler); + ctx.Route('tag_main', '/tag/list', TagMainHandler); + ctx.Route('tag_delete', '/tag/delete', TagDeleteHandler); + ctx.Route('tag_get', '/tag/get', TagGetHandler); +} diff --git a/packages/codemate-plugin/plugins/tags-entity/model.ts b/packages/codemate-plugin/plugins/tags-entity/model.ts new file mode 100644 index 0000000000..af3fee5def --- /dev/null +++ b/packages/codemate-plugin/plugins/tags-entity/model.ts @@ -0,0 +1,39 @@ +import { Document, DocumentModel, ObjectId, Projection } from 'hydrooj'; + +type stringDict = { [p: string]: string }; + +export interface TagDoc extends Document { + name: stringDict; + alias: string[]; + description: stringDict; +} + +export default class TagModel { + static async add(domainId: string, name: stringDict, alias: string[], description: stringDict): Promise { + return await DocumentModel.add(domainId, '', 1, DocumentModel.TYPE_TAGS, null, null, null, { + name, + alias, + description, + }); + } + + static async del(domainId: string, docId: ObjectId) { + await DocumentModel.deleteOne(domainId, DocumentModel.TYPE_TAGS, docId); + } + + static async edit(domainId: string, docId: ObjectId, name: stringDict, alias: string[], description: stringDict) { + await DocumentModel.set(domainId, DocumentModel.TYPE_TAGS, docId, { + name, + alias, + description, + }); + } + + static async get(domainId: string, docId: ObjectId) { + return await DocumentModel.get(domainId, DocumentModel.TYPE_TAGS, docId); + } + + static async getMulti(domainId: string, query?: Partial, projection?: Projection) { + return DocumentModel.getMulti(domainId, DocumentModel.TYPE_TAGS, query, projection); + } +} diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index a0a4b8482b..a496cf4ae1 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -101,15 +101,15 @@ export interface Udoc extends Record { /** * export const enum UserRole { - PRIMARY_SCHOOL_STUDENT = 0, // 小学生 - JUNIOR_MIDDLE_SCHOOL_STUDENT = 1, // 初中生 - SENIOR_MIDDLE_SCHOOL_STUDENT = 2, // 高中生 - ADULT = 10, // 一般成人 - COLLEGE_STUDENT = 11, // 大学生 - SCHOOL_TEACHER = 12, // 中小学教师 - INSTITUTE_TEACHER = 13, // 机构教师 - STUDENT_PARENT = 14, // 学生家长 -} + PRIMARY_SCHOOL_STUDENT = 0, // 小学生 + JUNIOR_MIDDLE_SCHOOL_STUDENT = 1, // 初中生 + SENIOR_MIDDLE_SCHOOL_STUDENT = 2, // 高中生 + ADULT = 10, // 一般成人 + COLLEGE_STUDENT = 11, // 大学生 + SCHOOL_TEACHER = 12, // 中小学教师 + INSTITUTE_TEACHER = 13, // 机构教师 + STUDENT_PARENT = 14, // 学生家长 + } */ export interface VUdoc { @@ -156,6 +156,7 @@ export interface BaseUser { displayName?: string; studentId?: string; } + export type BaseUserDict = Record; export interface FileInfo { @@ -244,18 +245,21 @@ export interface PlainContentNode { subType: 'html' | 'markdown'; text: string; } + export interface TextContentNode { type: 'Text'; subType: 'html' | 'markdown'; sectionTitle: string; text: string; } + export interface SampleContentNode { type: 'Sample'; text: string; sectionTitle: string; payload: [string, string]; } + // TODO drop contentNode support export type ContentNode = PlainContentNode | TextContentNode | SampleContentNode; export type Content = string | ContentNode[] | Record; @@ -279,7 +283,9 @@ declare module './model/problem' { content: string; nSubmit: number; nAccept: number; + /** @deprecated */ tag: string[]; + tags: ObjectId[]; data: FileInfo[]; additional_file: FileInfo[]; hidden?: boolean; @@ -384,6 +390,7 @@ export interface ScoreboardNode { style?: string; hover?: string; } + export type ScoreboardRow = ScoreboardNode[] & { raw?: any }; export type PenaltyRules = Dictionary; @@ -541,6 +548,7 @@ export interface TokenDoc { createAt: Date; updateAt: Date; expireAt: Date; + [key: string]: any; } @@ -647,6 +655,7 @@ export interface Task { type: string; subType?: string; priority: number; + [key: string]: any; } @@ -655,6 +664,7 @@ export interface Schedule { type: string; subType?: string; executeAfter: Date; + [key: string]: any; } @@ -795,6 +805,7 @@ export interface ProblemSearchResponse { total: number; countRelation: 'eq' | 'gte'; } + export interface ProblemSearchOptions { limit?: number; skip?: number; @@ -813,6 +824,7 @@ export interface Lib extends Record { } export type UIInjectableFields = 'ProblemAdd' | 'Notification' | 'Nav' | 'UserDropdown' | 'DomainManage' | 'ControlPanel'; + export interface UI { template: Record; nodes: Record; diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index 1e2362f144..00c73bdc91 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -1,6 +1,7 @@ /* eslint-disable object-curly-newline */ import assert from 'assert'; import { bulletin, plist } from 'codemate-plugin'; +import { TagDoc } from 'codemate-plugin/plugins/tags-entity/model'; import { Filter, FindCursor, ObjectId, OnlyFieldsOfType, PushOperator, UpdateFilter } from 'mongodb'; import { Context } from '../context'; import { Content, ContestClarificationDoc, DiscussionDoc, DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, Tdoc, TrainingDoc } from '../interface'; @@ -29,6 +30,7 @@ export const TYPE_TRAINING: 40 = 40; /** @deprecated use `TYPE_CONTEST` with rule `homework` instead. */ export const TYPE_HOMEWORK: 60 = 60; export const TYPE_BULLETIN: 80 = 80; +export const TYPE_TAGS: 90 = 90; export interface DocType { [TYPE_PROBLEM]: ProblemDoc; @@ -42,6 +44,7 @@ export interface DocType { [TYPE_TRAINING]: TrainingDoc; [TYPE_SYSTEM_PLIST]: plist.ProblemList; [TYPE_BULLETIN]: bulletin.BulletinDoc; + [TYPE_TAGS]: TagDoc; } export interface DocStatusType { @@ -550,4 +553,5 @@ global.Hydro.model.document = { TYPE_PROBLEM_SOLUTION, TYPE_TRAINING, TYPE_BULLETIN, + TYPE_TAGS, }; diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index f820ddbfbe..c3b492e098 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -22,9 +22,11 @@ import storage from './storage'; import user from './user'; export interface ProblemDoc extends Document {} + export type Field = keyof ProblemDoc; const logger = new Logger('problem'); + function sortable(source: string) { return source.replace(/(\d+)/g, (str) => (str.length >= 6 ? str : '0'.repeat(6 - str.length) + str)); } @@ -86,6 +88,7 @@ export class ProblemModel { nSubmit: 0, nAccept: 0, tag: [], + tags: [], data: [], additional_file: [], stats: {}, diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 4b9dea7a36..34ae178d4e 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-await-in-loop */ /* eslint-disable @typescript-eslint/naming-convention */ +import TagModel from 'codemate-plugin/plugins/tags-entity/model'; import yaml from 'js-yaml'; import { pick } from 'lodash'; import moment from 'moment-timezone'; @@ -226,6 +227,7 @@ const scripts: UpgradeScript[] = [ async function _51_52() { const mapping: Record = {}; const isStringPid = (i: string) => i.toString().includes(':'); + async function getProblem(domainId: string, target: string) { if (!target.toString().includes(':')) return await problem.get(domainId, target); const l = `${domainId}/${target}`; @@ -235,6 +237,7 @@ const scripts: UpgradeScript[] = [ mapping[l] = docId; return await problem.get(domainId, docId); } + const cursor = db.collection('document').find({ docType: document.TYPE_CONTEST }); for await (const doc of cursor) { const pids = []; @@ -297,9 +300,11 @@ const scripts: UpgradeScript[] = [ }, async function _54_55() { const bulk = db.collection('document').initializeUnorderedBulkOp(); + function sortable(source: string) { return source.replace(/(\d+)/g, (str) => (str.length >= 6 ? str : '0'.repeat(6 - str.length) + str)); } + await iterateAllProblem(['pid', '_id'], async (pdoc) => { bulk.find({ _id: pdoc._id }).updateOne({ $set: { sort: sortable(pdoc.pid || `P${pdoc.docId}`) } }); }); @@ -691,6 +696,21 @@ const scripts: UpgradeScript[] = [ } return true; }, + async function _91_92() { + // iterate all tags, and add a tag document according to it. + const tagMap = new Map(); + return await iterateAllProblem(['tag'], async (pdoc) => { + const _tags = []; + for (const tag of pdoc.tag) { + if (!tagMap.has(tag)) { + const docId = await TagModel.add(pdoc.domainId, { 'zh-cn': tag }, [], { 'zh-cn': tag }); + tagMap.set(tag, docId); + } + _tags.push(tagMap.get(tag)); + } + await problem.edit(pdoc.domainId, pdoc.docId, { tags: _tags }); + }); + }, ]; export default scripts;