mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: contest: add print (#1036)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
1c7b2275ff
commit
720eb6b7e0
13 files changed
+419
-6
No files matched your search
@@ -10,6 +10,7 @@ contest_detail: Contest Detail
|
||||
contest_edit: Contest Edit
|
||||
contest_main: Contest
|
||||
contest_manage: Contest Management
|
||||
contest_print: Print
|
||||
contest_problemlist: Problem List
|
||||
contest_scoreboard: Scoreboard
|
||||
contest_user: Attendee Manage
|
||||
|
||||
@@ -32,13 +32,13 @@ __langname: 简体中文
|
||||
'Join {0}': '加入 {0}'
|
||||
'No': 否
|
||||
'Note: Problem title may not be hidden.': 注意:题目标题可能不会被隐藏。
|
||||
'Or upload a file:': 或者上传一个文件:
|
||||
'Original score: {0}': '原得分: {0}'
|
||||
'Role {1} already exists in domain {0}.': '域 {0} 中已存在角色 {1}。'
|
||||
'Solved {0} problems, RP: {1} (No. {2})': '解决了 {0} 道题目,RP: {1} (No. {2})'
|
||||
'The value `{1}` of {0} already exists.': '{0} 的值 `{1}` 已经存在。'
|
||||
'Yes': 是
|
||||
"Effects only when Difficulty is not 'Use algorithm calculated'.": 仅当难度不为“使用算法计算”时才起效。
|
||||
'Or upload a file:': 或者上传一个文件:
|
||||
"Split by ',' '.'": 由“, ”或“,”分隔。
|
||||
"What's file?": 什么是文件?
|
||||
(All Problems): (所有题目)
|
||||
@@ -167,6 +167,7 @@ contest_detail: 比赛详情
|
||||
contest_edit: 编辑比赛
|
||||
contest_main: 比赛
|
||||
contest_manage: 比赛管理
|
||||
contest_print: 打印
|
||||
contest_problemlist: 题目列表
|
||||
contest_scoreboard: 比赛成绩表
|
||||
contest_user: 参赛者管理
|
||||
@@ -592,6 +593,7 @@ Pretest Your Code: 使用自定义数据测试
|
||||
Pretest: 自测
|
||||
Pretest?: 自测?
|
||||
Preview: 预览
|
||||
Print: 打印
|
||||
Privacy: 隐私
|
||||
Problem {1} not found.: 题目 {1} 不存在。
|
||||
Problem Categories: 题目标签
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Readable } from 'stream';
|
||||
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
|
||||
import { stringify as toCSV } from 'csv-stringify/sync';
|
||||
import { readFile } from 'fs-extra';
|
||||
import { escapeRegExp, pick } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import { ObjectId } from 'mongodb';
|
||||
@@ -182,6 +183,77 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export class ContestPrintHandler extends ContestDetailBaseHandler {
|
||||
@param('tid', Types.ObjectId)
|
||||
async prepare({ domainId }, tid: ObjectId) {
|
||||
if (!this.tdoc?.allowPrint) throw new NotFoundError();
|
||||
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST) && !this.tsdoc?.attend) {
|
||||
throw new ContestNotAttendedError(domainId, tid);
|
||||
}
|
||||
}
|
||||
|
||||
async get() {
|
||||
this.response.body = { tdoc: this.tdoc };
|
||||
this.response.template = 'contest_print.html';
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
@param('title', Types.Title, true)
|
||||
@param('content', Types.Content, true)
|
||||
async postPrint(domainId: string, tid: ObjectId, title = '', content = '') {
|
||||
if (!this.tsdoc?.attend) throw new ContestNotAttendedError(domainId, tid);
|
||||
if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(domainId, tid);
|
||||
await this.limitRate('add_print', 3600, 60);
|
||||
if (this.request.files?.file) {
|
||||
const file = this.request.files.file;
|
||||
if (file.size > 1024 * 1024) throw new ValidationError('file');
|
||||
content = await readFile(file.filepath, 'utf-8');
|
||||
title ||= file.originalFilename || 'file';
|
||||
}
|
||||
if (!content) throw new ValidationError('content');
|
||||
await contest.addPrintTask(domainId, tid, this.user._id, title, content);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
async postGetPrintTask(domainId: string, tid: ObjectId) {
|
||||
const isContestAdmin = this.user.own(this.tdoc) || this.user.hasPerm(PERM.PERM_EDIT_CONTEST);
|
||||
const tasks = await contest.getMultiPrintTask(domainId, tid, isContestAdmin ? {} : { owner: this.user._id })
|
||||
.project({ _id: 1, title: 1, owner: 1, status: 1 }).sort({ _id: 1 }).toArray();
|
||||
const uids = Array.from(new Set(tasks.map((i) => i.owner)));
|
||||
const udict = await user.getListForRender(
|
||||
domainId, uids,
|
||||
this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME)
|
||||
? ['displayName', 'school', 'studentId']
|
||||
: [],
|
||||
);
|
||||
this.response.body = { tasks, udict };
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
async postAllocatePrintTask(domainId: string, tid: ObjectId) {
|
||||
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) {
|
||||
throw new PermissionError(PERM.PERM_EDIT_CONTEST);
|
||||
}
|
||||
const task = await contest.allocatePrintTask(domainId, tid);
|
||||
const udoc = task ? await user.getById(domainId, task.owner) : null;
|
||||
this.response.body = { task, udoc };
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectId)
|
||||
@param('taskId', Types.ObjectId)
|
||||
@param('status', Types.Range(['printed', 'pending']))
|
||||
async postUpdatePrintTask(domainId: string, tid: ObjectId, taskId: ObjectId, status: 'printed' | 'pending') {
|
||||
if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) {
|
||||
throw new PermissionError(PERM.PERM_EDIT_CONTEST);
|
||||
}
|
||||
await contest.updatePrintTask(domainId, tid, taskId, {
|
||||
status: status === 'printed' ? contest.PrintTaskStatus.printed : contest.PrintTaskStatus.pending,
|
||||
});
|
||||
this.response.body = { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export class ContestProblemListHandler extends ContestDetailBaseHandler {
|
||||
@param('tid', Types.ObjectId)
|
||||
async get(domainId: string, tid: ObjectId) {
|
||||
@@ -305,12 +377,13 @@ export class ContestEditHandler extends Handler {
|
||||
@param('contestDuration', Types.Float, true)
|
||||
@param('maintainer', Types.NumericArray, true)
|
||||
@param('allowViewCode', Types.Boolean)
|
||||
@param('allowPrint', Types.Boolean)
|
||||
@param('langs', Types.CommaSeperatedArray, true)
|
||||
async postUpdate(
|
||||
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number,
|
||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||
_code = '', autoHide = false, assign: string[] = [], lock: number = null,
|
||||
contestDuration: number = null, maintainer: number[] = [], allowViewCode = false, langs: string[] = [],
|
||||
contestDuration: number = null, maintainer: number[] = [], allowViewCode = false, allowPrint = false, langs: string[] = [],
|
||||
) {
|
||||
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
||||
@@ -351,7 +424,7 @@ export class ContestEditHandler extends Handler {
|
||||
});
|
||||
}
|
||||
await contest.edit(domainId, tid, {
|
||||
assign, _code, autoHide, lockAt, maintainer, allowViewCode, langs,
|
||||
assign, _code, autoHide, lockAt, maintainer, allowViewCode, allowPrint, langs,
|
||||
});
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
@@ -734,6 +807,7 @@ export async function apply(ctx: Context) {
|
||||
ctx.Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
|
||||
ctx.Route('contest_problemlist', '/contest/:tid/problems', ContestProblemListHandler, PERM.PERM_VIEW_CONTEST);
|
||||
ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
|
||||
ctx.Route('contest_print', '/contest/:tid/print', ContestPrintHandler, PERM.PERM_VIEW_CONTEST);
|
||||
ctx.Route('contest_manage', '/contest/:tid/management', ContestManagementHandler);
|
||||
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
|
||||
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
FileInfo, RecordJudgeInfo, RecordPayload,
|
||||
} from '@hydrooj/common/types';
|
||||
import type { Context } from './context';
|
||||
import type { PrintTaskStatus } from './model/contest';
|
||||
import type { DocStatusType } from './model/document';
|
||||
import type { OauthMap } from './model/oauth';
|
||||
import type { ProblemDoc } from './model/problem';
|
||||
@@ -264,6 +265,7 @@ export interface Tdoc extends Document {
|
||||
assign?: string[];
|
||||
files?: FileInfo[];
|
||||
allowViewCode?: boolean;
|
||||
allowPrint?: boolean;
|
||||
|
||||
// For contest
|
||||
lockAt?: Date;
|
||||
@@ -383,6 +385,16 @@ export interface ContestClarificationDoc extends Document {
|
||||
reply: DiscussionTailReplyDoc[];
|
||||
}
|
||||
|
||||
export interface ContestPrintDoc extends Document {
|
||||
docType: document['TYPE_CONTEST_PRINT'];
|
||||
docId: ObjectId;
|
||||
parentType: document['TYPE_CONTEST'];
|
||||
parentId: ObjectId;
|
||||
title: string;
|
||||
content: string;
|
||||
status: PrintTaskStatus;
|
||||
}
|
||||
|
||||
export interface TokenDoc {
|
||||
_id: string;
|
||||
tokenType: number;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ContestScoreboardHiddenError, ValidationError,
|
||||
} from '../error';
|
||||
import {
|
||||
BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
||||
BaseUserDict, ContestPrintDoc, ContestRule, ContestRules, ProblemDict, RecordDoc,
|
||||
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
|
||||
} from '../interface';
|
||||
import bus from '../service/bus';
|
||||
@@ -20,6 +20,13 @@ import * as document from './document';
|
||||
import problem from './problem';
|
||||
import user, { User } from './user';
|
||||
|
||||
export enum PrintTaskStatus {
|
||||
pending = 'pending',
|
||||
printing = 'printing',
|
||||
printed = 'printed',
|
||||
failed = 'failed',
|
||||
}
|
||||
|
||||
interface AcmJournal {
|
||||
rid: ObjectId;
|
||||
pid: number;
|
||||
@@ -1047,8 +1054,41 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
|
||||
? 'Live...'
|
||||
: 'Done');
|
||||
|
||||
export function addPrintTask(domainId: string, tid: ObjectId, uid: number, name: string, content: string) {
|
||||
return document.add(domainId, content, uid, document.TYPE_CONTEST_PRINT, null, document.TYPE_CONTEST, tid, {
|
||||
title: name,
|
||||
status: PrintTaskStatus.pending,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePrintTask(domainId: string, tid: ObjectId, taskId: ObjectId, $set: Partial<ContestPrintDoc>) {
|
||||
const res = await document.coll.updateOne({
|
||||
domainId, docType: document.TYPE_CONTEST_PRINT,
|
||||
docId: taskId, parentType: document.TYPE_CONTEST, parentId: tid,
|
||||
}, { $set });
|
||||
return !!res.modifiedCount;
|
||||
}
|
||||
|
||||
export function allocatePrintTask(domainId: string, tid: ObjectId) {
|
||||
return document.coll.findOneAndUpdate({
|
||||
domainId, docType: document.TYPE_CONTEST_PRINT,
|
||||
parentType: document.TYPE_CONTEST, parentId: tid,
|
||||
status: PrintTaskStatus.pending,
|
||||
}, {
|
||||
$set: {
|
||||
status: PrintTaskStatus.printing,
|
||||
},
|
||||
}, { returnDocument: 'after' });
|
||||
}
|
||||
|
||||
export function getMultiPrintTask(domainId: string, tid: ObjectId, query = {}) {
|
||||
return document.getMulti(domainId, document.TYPE_CONTEST_PRINT, { parentType: document.TYPE_CONTEST, parentId: tid, ...query })
|
||||
.sort({ _id: 1 });
|
||||
}
|
||||
|
||||
global.Hydro.model.contest = {
|
||||
RULES,
|
||||
PrintTaskStatus,
|
||||
buildContestRule,
|
||||
add,
|
||||
getListStatus,
|
||||
@@ -1089,4 +1129,8 @@ global.Hydro.model.contest = {
|
||||
isExtended,
|
||||
applyProjection,
|
||||
statusText,
|
||||
addPrintTask,
|
||||
updatePrintTask,
|
||||
allocatePrintTask,
|
||||
getMultiPrintTask,
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'mongodb';
|
||||
import { Context } from '../context';
|
||||
import {
|
||||
Content, ContestClarificationDoc, DiscussionDoc,
|
||||
Content, ContestClarificationDoc, ContestPrintDoc, DiscussionDoc,
|
||||
DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc,
|
||||
Tdoc, TrainingDoc,
|
||||
} from '../interface';
|
||||
@@ -27,6 +27,7 @@ export const TYPE_DISCUSSION = 21 as const;
|
||||
export const TYPE_DISCUSSION_REPLY = 22 as const;
|
||||
export const TYPE_CONTEST = 30 as const;
|
||||
export const TYPE_CONTEST_CLARIFICATION = 31 as const;
|
||||
export const TYPE_CONTEST_PRINT = 32 as const;
|
||||
export const TYPE_TRAINING = 40 as const;
|
||||
|
||||
export interface DocType {
|
||||
@@ -37,6 +38,7 @@ export interface DocType {
|
||||
[TYPE_DISCUSSION]: DiscussionDoc;
|
||||
[TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc;
|
||||
[TYPE_CONTEST]: Tdoc;
|
||||
[TYPE_CONTEST_PRINT]: ContestPrintDoc;
|
||||
[TYPE_CONTEST_CLARIFICATION]: ContestClarificationDoc;
|
||||
[TYPE_TRAINING]: TrainingDoc;
|
||||
}
|
||||
@@ -480,6 +482,7 @@ global.Hydro.model.document = {
|
||||
|
||||
TYPE_CONTEST,
|
||||
TYPE_CONTEST_CLARIFICATION,
|
||||
TYPE_CONTEST_PRINT,
|
||||
TYPE_DISCUSSION,
|
||||
TYPE_DISCUSSION_NODE,
|
||||
TYPE_DISCUSSION_REPLY,
|
||||
|
||||
@@ -162,7 +162,9 @@ export class User {
|
||||
serialize(h) {
|
||||
if (!this._isPrivate) {
|
||||
const fields = ['_id', 'uname', 'mail', 'perm', 'role', 'priv', 'regat', 'loginat', 'tfa', 'authn'];
|
||||
if (h?.user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME)) fields.push('displayName');
|
||||
if (h?.user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME)) {
|
||||
fields.push('displayName', 'school', 'studentId');
|
||||
}
|
||||
return pick(this, fields);
|
||||
}
|
||||
return JSON.stringify(this, serializer(true, h));
|
||||
|
||||
@@ -688,6 +688,7 @@ Pretest Data: 自测数据
|
||||
Pretest Your Code: 使用自定义数据测试
|
||||
Pretest: 自测
|
||||
Pretest?: 自测?
|
||||
Print New File: 打印新文件
|
||||
Privacy: 隐私
|
||||
Privilege: 权限
|
||||
Problem {1} not found.: 题目 {1} 不存在。
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000"><path fill="currentColor" d="M800 426h-39c-4.3 0-7.8 3.5-7.8 7.8v39c0 4.3 3.5 7.8 7.8 7.8h39c4.3 0 7.8-3.5 7.8-7.8v-39c0-4.3-3.5-7.8-7.8-7.8zm31-102H715V117c0-4.3-3.5-7.8-7.8-7.8H293c-4.3 0-7.8 3.5-7.8 7.8v207H168c-43.2 0-78.1 35-78.1 78.1v320c0 17.3 14 31.3 31.3 31.3h164v129c0 4.3 3.5 7.8 7.8 7.8h414c4.3 0 7.8-3.5 7.8-7.8V754h164c17.3 0 31.3-14 31.3-31.3V402c0-43.2-35-78.1-78.1-78.1zM352 176h293v148H352zm293 648H352V555h293zm195-137H715V488H293v199H156V402c0-6.4 5.3-11.7 11.7-11.7h664c6.4 0 11.7 5.3 11.7 11.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 616 B |
@@ -0,0 +1,233 @@
|
||||
import $ from 'jquery';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import highlighter from 'vj/components/highlighter/prismjs';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { delay, i18n, mongoId, request, tpl } from 'vj/utils';
|
||||
|
||||
interface PrintTask {
|
||||
_id: string;
|
||||
owner: number;
|
||||
title: string;
|
||||
createAt: string;
|
||||
uid: number;
|
||||
content: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const apis = {
|
||||
getPrintTask: () => request.post('', { operation: 'get_print_task' }),
|
||||
updatePrintTask: (taskId: string, status: string) => request.post('', { operation: 'update_print_task', taskId, status }),
|
||||
allocatePrintTask: () => request.post('', { operation: 'allocate_print_task' }),
|
||||
} as const;
|
||||
|
||||
const callApi = async <T extends keyof typeof apis>(api: T, ...args: Parameters<typeof apis[T]> & any[]) => {
|
||||
try {
|
||||
return (await apis[api].call(apis[api], ...args)) as Awaited<ReturnType<typeof apis[T]>>;
|
||||
} catch (error) {
|
||||
console.error(`Failed to call API ${api}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function inlineStyles(element: HTMLElement) {
|
||||
const style = window.getComputedStyle(element);
|
||||
element.style.color = style.color;
|
||||
element.style.backgroundColor = style.backgroundColor;
|
||||
element.style.fontWeight = style.fontWeight;
|
||||
element.style.fontStyle = style.fontStyle;
|
||||
element.style.textDecoration = style.textDecoration;
|
||||
element.style.fontFamily = style.fontFamily;
|
||||
element.style.fontSize = style.fontSize;
|
||||
element.style.lineHeight = style.lineHeight;
|
||||
element.style.whiteSpace = style.whiteSpace;
|
||||
element.style.wordSpacing = style.wordSpacing;
|
||||
element.style.wordBreak = style.wordBreak;
|
||||
element.style.tabSize = style.tabSize;
|
||||
element.style.hyphens = style.hyphens;
|
||||
Array.from(element.children).forEach((child) => inlineStyles(child as HTMLElement));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
const PrintKiosk = ({ isAdmin }: { isAdmin: boolean }) => {
|
||||
const [printTasks, setPrintTasks] = useState<PrintTask[]>([]);
|
||||
const [isKioskActive, setIsKioskActive] = useState(false);
|
||||
const [udict, setUdict] = useState<Record<number, any>>({});
|
||||
|
||||
const pollPrintTasks = async () => {
|
||||
const response = await callApi('getPrintTask');
|
||||
setPrintTasks(response.tasks || []);
|
||||
setUdict(response.udict || {});
|
||||
};
|
||||
|
||||
const printTask = async (task, udoc) => {
|
||||
const printWindow = window.open('', '_blank', 'width=800,height=600,popup=1');
|
||||
if (!printWindow) return;
|
||||
|
||||
// Limit line count to prevent printing too many pages
|
||||
const finalContent = [];
|
||||
let cnt = 0;
|
||||
for (const line of task.content.split('\n')) {
|
||||
cnt += Math.ceil(line.length / 100);
|
||||
if (cnt > 300) break;
|
||||
finalContent.push(line);
|
||||
}
|
||||
|
||||
const parts = task.title.split('.');
|
||||
const ext = parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'plaintext';
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.display = 'none';
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'content';
|
||||
const code = document.createElement('code');
|
||||
code.className = `language-${ext}`;
|
||||
code.textContent = finalContent.join('\n');
|
||||
pre.appendChild(code);
|
||||
tempDiv.appendChild(pre);
|
||||
document.body.appendChild(tempDiv);
|
||||
highlighter.highlightBlocks($(tempDiv));
|
||||
inlineStyles(pre);
|
||||
const highlightedContent = pre.outerHTML;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
const header = tpl(<div className="header">
|
||||
<p>
|
||||
[{udoc.uname}] {udoc.school || ''} {udoc.displayName || ''}
|
||||
<span style={{ float: 'right' }}>{new Date(mongoId(task._id).timestamp * 1000).toLocaleString()}</span>
|
||||
<br />
|
||||
Filename: {task.title}
|
||||
<span style={{ float: 'right' }}>By Hydro</span>
|
||||
</p>
|
||||
</div>);
|
||||
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Print Page</title>
|
||||
<style>
|
||||
body { font-family: monospace; margin: 10px; font-size: 14px; line-height: 1.2; }
|
||||
.header { border-bottom: 1px solid #ccc; }
|
||||
pre { margin: 0; margin-block: 0; white-space: pre-wrap !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
${highlightedContent}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
// printWindow.close();
|
||||
callApi('updatePrintTask', task._id, 'printed').then(() => pollPrintTasks());
|
||||
}, 500);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
pollPrintTasks();
|
||||
let active = true;
|
||||
if (isKioskActive) {
|
||||
(async () => {
|
||||
while (active) { // eslint-disable-line no-unmodified-loop-condition
|
||||
const task = await callApi('allocatePrintTask');
|
||||
if (!task?.task) await delay(5000);
|
||||
else {
|
||||
await printTask(task.task, task.udoc);
|
||||
await pollPrintTasks();
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [isKioskActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const cb = () => setIsKioskActive(true);
|
||||
$(document).on('click', '[name="enable_print_kiosk"]', cb);
|
||||
return () => {
|
||||
$(document).off('click', '[name="enable_print_kiosk"]', cb);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div className="print-kiosk">
|
||||
{isKioskActive && <p style={{ textAlign: 'center' }}>Print Kiosk is enabled</p>}
|
||||
|
||||
<div className="table-container">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Title</th>
|
||||
<th>Time</th>
|
||||
<th>Status</th>
|
||||
{isAdmin && <th>Action</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{printTasks.length === 0
|
||||
&& <tr><td colSpan={isAdmin ? 5 : 4}><p style={{ textAlign: 'center' }}>No pending print tasks.</p></td></tr>}
|
||||
{printTasks.map((task) => (
|
||||
<tr key={task._id}>
|
||||
<td><a href={`/user/${task.owner}`}>{udict[task.owner]?.uname}</a></td>
|
||||
<td>{task.title}</td>
|
||||
<td>{new Date(mongoId(task._id).timestamp * 1000).toLocaleString()}</td>
|
||||
<td>{task.status}</td>
|
||||
{isAdmin && <td>
|
||||
<button
|
||||
onClick={() => callApi('updatePrintTask', task._id, 'pending').then(() => pollPrintTasks())}
|
||||
className="typo-a"
|
||||
>
|
||||
Re-Print
|
||||
</button>
|
||||
</td>}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const page = new NamedPage('contest_print', () => {
|
||||
const element = document.createElement('input');
|
||||
element.type = 'file';
|
||||
element.name = 'file';
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.onchange = async (ev: Event) => {
|
||||
const file = (ev.target as HTMLInputElement).files[0];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('operation', 'print');
|
||||
const language = file.name.split('.').pop()?.toLowerCase();
|
||||
const dialog = new ConfirmDialog({
|
||||
$body: tpl(<div className="typo">
|
||||
<h3>{i18n('Are you sure to print this file?')}<span style={{ float: 'right' }}>{i18n('Filename: {0}', file.name)}</span></h3>
|
||||
<div style={{ maxHeight: '60vh', overflow: 'scroll' }}>
|
||||
<pre><code className={`language-${language}`}>{await file.text()}</code></pre>
|
||||
</div>
|
||||
</div>),
|
||||
});
|
||||
const action = await dialog.open();
|
||||
if (action !== 'yes') return;
|
||||
request.postFile('', formData).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
$(document).on('click', '[name="add_print_task"]', () => {
|
||||
element.click();
|
||||
});
|
||||
const container = document.getElementById('printKioskContainer');
|
||||
if (!container) return;
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<PrintKiosk isAdmin={!!container.dataset.isAdmin} />);
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -166,6 +166,14 @@
|
||||
value:tdoc.allowViewCode|default(true),
|
||||
row:false
|
||||
}) }}
|
||||
{{ form.form_checkbox({
|
||||
columns:4,
|
||||
label:'Allow Print',
|
||||
name:'allowPrint',
|
||||
placeholder:_('Allow user to print during contest'),
|
||||
value:tdoc.allowPrint|default(false),
|
||||
row:false
|
||||
}) }}
|
||||
<div style="display: none" class="contest-rule-settings contest-rule--acm contest-rule--ioi">
|
||||
{{ form.form_text({
|
||||
columns:4,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% import "components/contest.html" as contest with context %}
|
||||
{% import "components/form.html" as form with context %}
|
||||
{% block content %}
|
||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||
{{ set(UiContext, 'tsdoc', tsdoc) }}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Print Request') }} - {{ tdoc.title }}</h1>
|
||||
<div class="section__tools">
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
||||
<button class="primary rounded button" name="enable_print_kiosk">{{ _('Enable Print Kiosk') }}</button>
|
||||
{% endif %}
|
||||
<button class="primary rounded button" name="add_print_task">{{ _('Print New File') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section__body no-padding" id="printKioskContainer" {% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}data-is-admin="true" {% endif %}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% set owner_udoc = udict[tdoc.owner] %}
|
||||
{% include "partials/contest_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -78,6 +78,11 @@
|
||||
<span class="icon icon-statistics"></span> {{ _('Scoreboard (Hidden)') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if tdoc.allowPrint and (tsdoc.attend or handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST)) %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_print', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-print"></span> {{ _('Print') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_edit', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit Contest') }}
|
||||
|
||||
Reference in new issue
Block a user