mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
+nginx.conf; update scratchpad
This commit is contained in:
1 parent
6ad41dc2ae
commit
2d5eef8d08
28 files changed
+206
-230
No files matched your search
@@ -7,3 +7,5 @@ web端运行脚本似乎异常
|
||||
举报功能
|
||||
SETTINGS_PRIVACY.allowRegisteredUsers
|
||||
model.setting.ui.name
|
||||
pdoc.secretConfig?
|
||||
在线IDE 更换颜色主题
|
||||
@@ -0,0 +1,9 @@
|
||||
listen:
|
||||
port: 8080
|
||||
hosts:
|
||||
"*":
|
||||
paths:
|
||||
/:
|
||||
proxy.reverse.url: "http://[unix:/home/hydro/hydro.sock]"
|
||||
proxy.preserve-host: ON
|
||||
proxy.websocket: ON
|
||||
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
root /tmp/hydro/public;
|
||||
|
||||
location / {
|
||||
try_files $uri @proxy_to_app;
|
||||
}
|
||||
|
||||
location @proxy_to_app {
|
||||
proxy_pass http://unix:/home/hydro/hydro.sock;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -2,7 +2,7 @@
|
||||
const { isClass } = require('./utils');
|
||||
|
||||
interface IHydroError {
|
||||
new(...args: any[]): HydroError
|
||||
new(...args: any[]): HydroError
|
||||
}
|
||||
|
||||
const Err = (name: string, ...info: Array<IHydroError | Function | string | number>) => {
|
||||
@@ -29,19 +29,19 @@ const Err = (name: string, ...info: Array<IHydroError | Function | string | numb
|
||||
};
|
||||
|
||||
export class HydroError extends Error {
|
||||
params: any[];
|
||||
params: any[];
|
||||
|
||||
code: number;
|
||||
code: number;
|
||||
|
||||
constructor(...params: any[]) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
constructor(...params: any[]) {
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
msg() {
|
||||
return 'HydroError';
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
msg() {
|
||||
return 'HydroError';
|
||||
}
|
||||
}
|
||||
|
||||
export const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400);
|
||||
@@ -85,7 +85,7 @@ export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenEr
|
||||
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
|
||||
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
|
||||
export const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.');
|
||||
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {1} not found.');
|
||||
export const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {0} not found.');
|
||||
export const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.');
|
||||
export const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.');
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as system from '../model/system';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as bus from '../service/bus';
|
||||
|
||||
const ContestHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
@@ -281,7 +282,11 @@ class ContestProblemSubmitHandler extends ContestProblemHandler {
|
||||
lang,
|
||||
code,
|
||||
}, true);
|
||||
await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId);
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
if (!this.canShowRecord(this.tdoc)) {
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PenaltyRules } from '../interface';
|
||||
import {
|
||||
Route, Handler, Types, param,
|
||||
} from '../service/server';
|
||||
import * as bus from '../service/bus';
|
||||
import { PERM } from '../model/builtin';
|
||||
import * as system from '../model/system';
|
||||
import * as user from '../model/user';
|
||||
@@ -175,7 +176,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
@param('code', Types.String)
|
||||
@param('lang', Types.String)
|
||||
async post(domainId: string, tid: ObjectID, pid: number, code: string, lang: string) {
|
||||
this.limitRate('add_record', 60, 100);
|
||||
this.limitRate('add_record', 3600, 100);
|
||||
const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK);
|
||||
if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
|
||||
@@ -183,8 +184,12 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
const rid = await record.add(domainId, {
|
||||
pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK,
|
||||
}, true);
|
||||
await contest.updateStatus(domainId, tid, this.user._id,
|
||||
rid, pid, false, 0, document.TYPE_HOMEWORK);
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
contest.updateStatus(domainId, tid, this.user._id,
|
||||
rid, pid, false, 0, document.TYPE_HOMEWORK),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { tid, rid };
|
||||
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
|
||||
else this.response.redirect = this.url('homework_detail', { tid });
|
||||
|
||||
@@ -140,6 +140,7 @@ class UiSettingsHandler extends Handler {
|
||||
header_background_2x,
|
||||
});
|
||||
this.response.type = 'text/css';
|
||||
this.ctx.set('nolog', '1');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-10
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
NoProblemError, ProblemDataNotFoundError, BadRequestError,
|
||||
SolutionNotFoundError,
|
||||
ProblemNotFoundError,
|
||||
} from '../error';
|
||||
import { Pdoc, User, Rdoc } from '../interface';
|
||||
|
||||
@@ -220,7 +221,11 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
const rid = await record.add(domainId, {
|
||||
uid: this.user._id, lang, code, pid: this.pdoc.docId,
|
||||
}, true);
|
||||
await domain.incUserInDomain(domainId, this.user._id, 'nSubmit');
|
||||
const [rdoc] = await Promise.all([
|
||||
record.get(domainId, rid),
|
||||
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
|
||||
]);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { rid };
|
||||
this.response.redirect = this.url('record_detail', { rid });
|
||||
}
|
||||
@@ -229,13 +234,14 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
|
||||
class ProblemPretestHandler extends ProblemDetailHandler {
|
||||
@param('lang', Types.String)
|
||||
@param('code', Types.String)
|
||||
@param('input', Types.String)
|
||||
async post(domainId: string, lang: string, code: string, input: string) {
|
||||
this.limitRate('add_record', 60, 100);
|
||||
@param('input', Types.String, true)
|
||||
async post(domainId: string, lang: string, code: string, input: string = '') {
|
||||
this.limitRate('add_record', 3600, 100);
|
||||
const rid = await record.add(domainId, {
|
||||
uid: this.user._id, lang, code, pid: this.pdoc.docId, input,
|
||||
}, true);
|
||||
await record.judge(domainId, rid);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
bus.publish('record_change', rdoc);
|
||||
this.response.body = { rid };
|
||||
}
|
||||
}
|
||||
@@ -247,7 +253,9 @@ class ProblemPretestConnectionHandler extends ConnectionHandler {
|
||||
|
||||
@param('pid', Types.String)
|
||||
async prepare(domainId: string, pid: string) {
|
||||
this.pid = pid;
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
|
||||
this.pid = pdoc.docId.toString();
|
||||
this.domainId = domainId;
|
||||
bus.subscribe(['record_change'], this, 'onRecordChange');
|
||||
}
|
||||
@@ -310,11 +318,11 @@ class ProblemSettingsHandler extends ProblemManageHandler {
|
||||
|
||||
@param('pid', Types.String, null, parsePid)
|
||||
@param('hidden', Types.Boolean)
|
||||
@param('category', Types.String, null, parseCategory)
|
||||
@param('tag', Types.String, null, parseCategory)
|
||||
@param('category', Types.String, true, null, parseCategory)
|
||||
@param('tag', Types.String, true, null, parseCategory)
|
||||
async postSetting(
|
||||
domainId: string, pid: string | number, hidden = false,
|
||||
category: string[], tag: string[],
|
||||
category: string[] = [], tag: string[] = [],
|
||||
) {
|
||||
const pdoc = await problem.get(domainId, pid);
|
||||
await problem.edit(domainId, pdoc.docId, { hidden, category, tag });
|
||||
@@ -554,7 +562,7 @@ export async function apply() {
|
||||
Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
||||
Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
||||
Route('problem_create', '/problem/create', ProblemCreateHandler);
|
||||
Connection('problem_pretest_conn', '/p/:pid/pretest-conn', ProblemPretestConnectionHandler);
|
||||
Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.problem = apply;
|
||||
+1
-1
@@ -42,7 +42,7 @@ export interface User extends Dictionary<any> {
|
||||
priv: number,
|
||||
regat: Date,
|
||||
loginat: Date,
|
||||
perm: bigint,
|
||||
perm: () => bigint,
|
||||
role: string,
|
||||
regip: () => string,
|
||||
loginip: () => string,
|
||||
|
||||
@@ -503,14 +503,6 @@ export const CATEGORIES = {
|
||||
],
|
||||
};
|
||||
|
||||
export const FOOTER_EXTRA_HTMLS = [];
|
||||
|
||||
export const VIEW_LANGS = [
|
||||
{ code: 'zh_CN', name: '简体中文' },
|
||||
{ code: 'zh_TW', name: '正體中文' },
|
||||
{ code: 'en', name: 'English' },
|
||||
];
|
||||
|
||||
export const LANG_TEXTS = {
|
||||
c: 'C',
|
||||
cc: 'C++',
|
||||
@@ -623,8 +615,6 @@ global.Hydro.model.builtin = {
|
||||
BUILTIN_ROLES,
|
||||
DEFAULT_NODES,
|
||||
CATEGORIES,
|
||||
VIEW_LANGS,
|
||||
FOOTER_EXTRA_HTMLS,
|
||||
LANG_TEXTS,
|
||||
LANG_HIGHLIGHT_ID,
|
||||
STATUS,
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function add(
|
||||
domainId: string, pid: string = null, title: string, content: string, owner: number,
|
||||
tag: string[] = [], category: string[] = [], data: ObjectID = null, hidden = false,
|
||||
) {
|
||||
const d = await domain.inc(domainId, 'pid_counter', 1);
|
||||
const d = await domain.inc(domainId, 'pidCounter', 1);
|
||||
if (!pid) pid = d.pidCounter.toString();
|
||||
return await document.add(
|
||||
domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null,
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface JudgeTask {
|
||||
type?: string,
|
||||
}
|
||||
|
||||
export async function add(domainId: string, data: RdocBase, addTask: boolean) {
|
||||
export async function add(domainId: string, data: RdocBase, addTask: boolean): Promise<ObjectID> {
|
||||
_.defaults(data, {
|
||||
status: STATUS.STATUS_WAITING,
|
||||
type: 'judge',
|
||||
|
||||
+11
-9
@@ -13,7 +13,7 @@ for (const country of countries) {
|
||||
for (const t of tz) tzs.add(t);
|
||||
}
|
||||
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]) as [string, string][];
|
||||
const langRange: Dictionary<string> = {};
|
||||
export const langRange: Dictionary<string> = {};
|
||||
|
||||
for (const lang in global.Hydro.locales) {
|
||||
langRange[lang] = global.Hydro.locales[lang].__langname;
|
||||
@@ -78,8 +78,8 @@ export const SystemSetting = (...settings: _Setting[]) => {
|
||||
PreferenceSetting(
|
||||
Setting('setting_display', 'viewLang', langRange, 'zh_CN', 'select', 'UI Language'),
|
||||
Setting('setting_display', 'timeZone', timezones, 'Asia/Shanghai', 'select', 'Timezone'),
|
||||
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, null, 'select', 'Default Code Language'),
|
||||
Setting('setting_usage', 'codeTemplate', null, null, 'textarea', 'Default Code Template',
|
||||
Setting('setting_usage', 'codeLang', builtin.LANG_TEXTS, 'c', 'select', 'Default Code Language'),
|
||||
Setting('setting_usage', 'codeTemplate', null, '', 'textarea', 'Default Code Template',
|
||||
'If left blank, the built-in template of the corresponding language will be used.'),
|
||||
);
|
||||
|
||||
@@ -87,7 +87,7 @@ AccountSetting(
|
||||
Setting('setting_info', 'gravatar', null, null, 'text', 'Gravatar Email',
|
||||
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'),
|
||||
Setting('setting_info', 'qq', null, null, 'text', 'QQ'),
|
||||
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, null, 'select', 'Gender'),
|
||||
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, builtin.USER_GENDER_OTHER, 'select', 'Gender'),
|
||||
Setting('setting_info', 'bio', null, null, 'markdown', 'Bio'),
|
||||
Setting('setting_customize', 'backgroundImage', null,
|
||||
'/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image',
|
||||
@@ -99,11 +99,11 @@ DomainSetting(
|
||||
Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'),
|
||||
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
|
||||
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
|
||||
Setting('storage', 'pid_counter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'pidCounter', null, 0, 'number', 'Problem ID Counter', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
);
|
||||
|
||||
SystemSetting(
|
||||
@@ -126,6 +126,7 @@ SystemSetting(
|
||||
Setting('setting_session', 'session.saved_expire_seconds', null, 3600 * 24 * 30, 'number', 'Saved session expire seconds'),
|
||||
Setting('setting_session', 'session.unsaved_expire_seconds', null, 3600 * 3, 'number', 'Unsaved session expire seconds'),
|
||||
Setting('setting_ui', 'ui.name', null, 'Hydro', 'text', 'Site Name'),
|
||||
Setting('setting_ui', 'ui.footer_extra_html', null, '', 'textarea', 'Footer Extra HTML'),
|
||||
Setting('setting_ui', 'ui.header_logo', null, '/components/header/header-logo.png', 'text', 'Header Logo'),
|
||||
Setting('setting_ui', 'ui.header_logo_2x', null, '/components/header/header-logo@2x.png', 'text', 'Header Logo@2x'),
|
||||
Setting('setting_ui', 'ui.header_background', null, '/components/header/header-background.png', 'text', 'Header Background'),
|
||||
@@ -168,6 +169,7 @@ global.Hydro.postInit.push(
|
||||
);
|
||||
|
||||
global.Hydro.model.setting = {
|
||||
langRange,
|
||||
Setting,
|
||||
PreferenceSetting,
|
||||
AccountSetting,
|
||||
|
||||
+3
-3
@@ -43,7 +43,7 @@ export class User implements _User {
|
||||
|
||||
loginat: Date;
|
||||
|
||||
perm: bigint;
|
||||
perm: () => bigint;
|
||||
|
||||
role: string;
|
||||
|
||||
@@ -67,7 +67,7 @@ export class User implements _User {
|
||||
this.regip = () => udoc.regip;
|
||||
this.loginat = udoc.loginat;
|
||||
this.loginip = () => udoc.loginip;
|
||||
this.perm = dudoc.perm;
|
||||
this.perm = () => dudoc.perm;
|
||||
this.role = dudoc.role || 'default';
|
||||
|
||||
for (const key in setting.SETTINGS_BY_KEY) {
|
||||
@@ -86,7 +86,7 @@ export class User implements _User {
|
||||
}
|
||||
|
||||
hasPerm(p: bigint) {
|
||||
return (this.perm & p) === p;
|
||||
return (this.perm() & p) === p;
|
||||
}
|
||||
|
||||
hasPriv(p: number) {
|
||||
|
||||
+21
-22
@@ -12,7 +12,7 @@ import Body from 'koa-body';
|
||||
import Router from 'koa-router';
|
||||
import cache from 'koa-static-cache';
|
||||
import sockjs from 'sockjs';
|
||||
import serialize from 'serialize-javascript';
|
||||
import serialize, { SerializeJSOptions } from 'serialize-javascript';
|
||||
import parse from 'yargs-parser';
|
||||
import { lrucache } from '../utils';
|
||||
import { User, DomainDoc } from '../interface';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
NotFoundError,
|
||||
} from '../error';
|
||||
import { render } from '../lib/template';
|
||||
import hash from '../lib/hash.hydro';
|
||||
import * as misc from '../lib/misc';
|
||||
import * as user from '../model/user';
|
||||
import * as domain from '../model/domain';
|
||||
@@ -30,10 +31,8 @@ import * as system from '../model/system';
|
||||
import * as blacklist from '../model/blacklist';
|
||||
import * as token from '../model/token';
|
||||
import * as opcount from '../model/opcount';
|
||||
import hash from '../lib/hash.hydro';
|
||||
|
||||
const argv = parse(process.argv.slice(2));
|
||||
let enableLog = true;
|
||||
const app = new Koa();
|
||||
let server;
|
||||
const router = new Router();
|
||||
@@ -43,14 +42,6 @@ interface H {
|
||||
[key: string]: any,
|
||||
}
|
||||
|
||||
interface IHandler {
|
||||
// @ts-ignore
|
||||
new(ctx: Koa.Context): Handler;
|
||||
}
|
||||
interface IConnectionHandler {
|
||||
// @ts-ignore
|
||||
new(conn: sockjs.Connection): ConnectionHandler;
|
||||
}
|
||||
type MethodDecorator = (target: any, name: string, obj: any) => any;
|
||||
type Converter = (value: any) => any;
|
||||
type Validator = (value: any) => boolean;
|
||||
@@ -268,7 +259,6 @@ export class Handler {
|
||||
}
|
||||
|
||||
async renderHTML(name: string, context: any): Promise<string> {
|
||||
if (enableLog) console.time(name);
|
||||
const UserContext = {
|
||||
...this.user,
|
||||
gravatar: misc.gravatar(this.user.gravatar || '', 128),
|
||||
@@ -281,7 +271,6 @@ export class Handler {
|
||||
_: this.translate.bind(this),
|
||||
...context,
|
||||
});
|
||||
if (enableLog) console.timeEnd(name);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -406,6 +395,7 @@ export class Handler {
|
||||
),
|
||||
]);
|
||||
this.csrfToken = this.getCsrfToken(this.session._id || String.random(32));
|
||||
this.UIContext.csrfToken = this.csrfToken;
|
||||
}
|
||||
|
||||
async finish() {
|
||||
@@ -421,12 +411,23 @@ export class Handler {
|
||||
}
|
||||
|
||||
async renderBody() {
|
||||
if (this.response.redirect) {
|
||||
this.response.body = this.response.body || {};
|
||||
this.response.body.url = this.response.redirect;
|
||||
}
|
||||
if (this.response.type) return;
|
||||
if (this.request.json || this.response.redirect || !this.response.template) {
|
||||
this.response.body = serialize(this.response.body);
|
||||
if (
|
||||
this.request.json || this.response.redirect
|
||||
|| this.request.query.noTemplate || !this.response.template) {
|
||||
try {
|
||||
this.response.body = JSON.stringify(this.response.body);
|
||||
} catch (e) {
|
||||
const opt: SerializeJSOptions = { ignoreFunction: true };
|
||||
if (this.request.query.noTemplate) opt.space = 2;
|
||||
this.response.body = serialize(this.response.body, opt);
|
||||
}
|
||||
this.response.type = 'application/json';
|
||||
} else if (this.response.body || this.response.template) {
|
||||
if (this.request.query.noTemplate || this.request.json) return;
|
||||
const templateName = this.request.query.template || this.response.template;
|
||||
if (templateName) {
|
||||
this.response.body = this.response.body || {};
|
||||
@@ -442,10 +443,6 @@ export class Handler {
|
||||
this.ctx.response.status = 302;
|
||||
this.ctx.redirect(this.response.redirect);
|
||||
} else {
|
||||
if (this.response.redirect) {
|
||||
this.response.body = this.response.body || {};
|
||||
this.response.body.url = this.response.redirect;
|
||||
}
|
||||
if (this.response.body != null) {
|
||||
this.ctx.response.body = this.response.body;
|
||||
this.ctx.response.status = this.response.status || 200;
|
||||
@@ -757,8 +754,10 @@ export function Middleware(middleware: Koa.Middleware) {
|
||||
export async function start() {
|
||||
const [disableLog, port] = await system.getMany(['server.log', 'server.port']);
|
||||
if (!disableLog) {
|
||||
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
|
||||
} else enableLog = false;
|
||||
app.use(morgan(':method :url :status :res[content-length] - :response-time ms', {
|
||||
skip: (req, res) => res.hasHeader('nolog'),
|
||||
}));
|
||||
}
|
||||
app.use(router.routes()).use(router.allowedMethods());
|
||||
Route('notfound_handler', '*', Handler);
|
||||
server.listen(argv.port || port);
|
||||
|
||||
+1
-1
@@ -234,7 +234,7 @@ Domain {0} is bulit-in and cannot be modified.: 域 {0} 为内置,不可修改
|
||||
Domain {0} not found.: 域 {0} 不存在。
|
||||
Domain ID cannot be changed once the domain is created.: 在创建后不能更改 ID。
|
||||
Domain ID: 域 ID
|
||||
domain_dashboard: 域概况
|
||||
domain_dashboard: 管理域
|
||||
domain_discussion: 讨论节点
|
||||
domain_edit: 编辑域资料
|
||||
domain_join: 加入域
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.8.1",
|
||||
"version": "2.8.2",
|
||||
"main": "hydro/loader.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||
"author": "masnn",
|
||||
@@ -79,10 +79,10 @@
|
||||
"build": "node build --development",
|
||||
"build:development": "node build --development",
|
||||
"build:production": "node build --production",
|
||||
"build:all": "cd ui && yarn && yarn build:production && cd .. && yarn --ignore-optional && yarn build:production",
|
||||
"build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build:production",
|
||||
"start": "ts-node hydro/loader.ts",
|
||||
"pkg": "pkg .",
|
||||
"lint": "eslint build hydro module tool --ext ts,js --fix",
|
||||
"lint": "eslint build hydro tool --ext ts,js --fix",
|
||||
"snyk-protect": "snyk protect",
|
||||
"prepare": "yarn run snyk-protect"
|
||||
},
|
||||
@@ -95,4 +95,4 @@
|
||||
]
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,8 @@
|
||||
<li class="footer__extra-link-item" data-dropdown-target="#menu-footer-lang">
|
||||
<span><span class="icon icon-global"></span> Language <span class="icon icon-expand_less"></span></span>
|
||||
<ol class="dropdown-target menu" id="menu-footer-lang">
|
||||
{% for item in model.builtin.VIEW_LANGS %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('switch_language', lang=item.code) }}">{{ item.name }}</a></li>
|
||||
{% for k, v in model.setting.langRange %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('switch_language', lang=k) }}">{{ v }}</a></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</li>
|
||||
@@ -62,6 +62,7 @@
|
||||
</div>
|
||||
<div class="footer__extra-right">
|
||||
<ol class="clearfix">
|
||||
<!-- TODO -->
|
||||
{% for html in model.builtin.FOOTER_EXTRA_HTMLS %}
|
||||
<li class="footer__extra-link-item">{{ html|safe }}</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
var Context = {{ {
|
||||
problemId: pdoc.pid,
|
||||
postSubmitUrl: url('problem_submit', pid=pdoc.pid),
|
||||
postPretestUrl: url('problem_pretest', pid=pdoc.pid),
|
||||
getSubmissionsUrl: url('problem_submit', pid=pdoc.pid),
|
||||
getRecordDetailUrl: url('record_detail', rid='{rid}'),
|
||||
codeLang: handler.user.codeLang,
|
||||
@@ -28,13 +29,13 @@
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
{% if tdoc %}
|
||||
{% if tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST %}
|
||||
{% if tdoc['docType'] == model.document.TYPE_CONTEST %}
|
||||
{% if model.contest.isDone(tdoc) %}
|
||||
<blockquote class="note">
|
||||
<p>{{ _('You cannot submit for this problem because the contest is ended. You can click "Open in Problem Set" to view this problem in normal mode.') }}</p>
|
||||
</blockquote>
|
||||
{% endif %}{# handler.isDone(tdoc) #}
|
||||
{% endif %}{# tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST #}
|
||||
{% endif %}{# tdoc['docType'] == model.document.TYPE_CONTEST #}
|
||||
{% endif %}{# tdoc #}
|
||||
{{ pdoc['content']|markdown|safe }}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% extends 'layout/simple.html' %}
|
||||
{% block body %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% if (typeof(pdoc.data) == 'string') %}
|
||||
<p><b>{{ _('Current dataset comes from') }} <a href="{{ pdoc.data.split('from:')[1] }}" target="_blank">{{ pdoc.data.split('from:')[1] }}</a></b></p>
|
||||
{% else %}
|
||||
<p>{{ _('Current dataset: {0}').format((pdoc['data'] or _('(None)')).toString()) }}</p>
|
||||
<p>{{ _('MD5') }}: {{ md5|default(_('(None)'))) }}</p>
|
||||
{% endif %}
|
||||
<p>{{ _('New dataset') }}:
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
<input type="file" name="file">
|
||||
<input type="submit" value="{{ _('Upload') }}" class="rounded primary button">
|
||||
</p>
|
||||
{% if (typeof(pdoc.data) == 'string') %}
|
||||
<p><b>{{ _('Current dataset comes from') }} <a href="{{ pdoc.data.split('from:')[1] }}" target="_blank">{{ pdoc.data.split('from:')[1] }}</a></b></p>
|
||||
{% else %}
|
||||
<p>{{ _('Current dataset: {0}').format((pdoc['data'] or _('(None)')).toString()) }}</p>
|
||||
<p>{{ _('MD5') }}: {{ md5|default(_('(None)')) }}</p>
|
||||
{% endif %}
|
||||
<p>{{ _('New dataset') }}:
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
<input type="file" name="file">
|
||||
<input type="submit" value="{{ _('Upload') }}" class="rounded primary button">
|
||||
</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -11,6 +11,7 @@ const IGNORE_MISSING = [
|
||||
];
|
||||
const RE_TEMPLATE = /_\(['"]([\s\S])*?['"]\)/gmi;
|
||||
const RE_UI = /i18n\(['"]([\s\S])*?['"](,.*?)?\)/gmi;
|
||||
const RE_SETTING = /Setting\(['"]([\s\S])*?['"](,[^()]*?)\)/gmi;
|
||||
const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
|
||||
const LOCALE_ROOT = path.resolve(__dirname, '..', 'locales');
|
||||
const UI_ROOT = path.resolve(__dirname, '..', 'ui');
|
||||
@@ -27,6 +28,8 @@ function _(str, format) {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const i18n = _;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Setting = _;
|
||||
|
||||
function scanTemplate(folder, relative = 'templates') {
|
||||
const files = fs.readdirSync(folder);
|
||||
@@ -65,7 +68,7 @@ function scanUi(folder, relative = 'ui') {
|
||||
eval(substr);
|
||||
} catch (e) {
|
||||
if (e.message.endsWith('is not defined')) {
|
||||
global[e.message.split(' ')[0]] = undefined;
|
||||
global[e.message.split(' ')[0]] = () => { };
|
||||
try {
|
||||
eval(substr);
|
||||
} catch (err) {
|
||||
@@ -78,8 +81,27 @@ function scanUi(folder, relative = 'ui') {
|
||||
}
|
||||
}
|
||||
}
|
||||
function scanSetting() {
|
||||
const file = path.resolve(__dirname, '..', 'hydro', 'model', 'setting.ts');
|
||||
const content = fs.readFileSync(file).toString();
|
||||
content.replace(RE_SETTING, (substr) => {
|
||||
try {
|
||||
eval(substr);
|
||||
} catch (e) {
|
||||
if (e.message.endsWith('is not defined')) {
|
||||
global[e.message.split(' ')[0]] = () => { };
|
||||
try {
|
||||
eval(substr);
|
||||
} catch (err) {
|
||||
console.error('Cannot parse: ', substr, ' in setting');
|
||||
}
|
||||
} else console.error('Cannot parse: ', substr, ' in setting');
|
||||
}
|
||||
});
|
||||
}
|
||||
scanTemplate(TEMPLATE_ROOT);
|
||||
scanUi(UI_ROOT);
|
||||
scanSetting();
|
||||
if (!name) {
|
||||
for (const locale of locales) {
|
||||
if (!IGNORE_CHECK.includes(locale.split('.')[0])) {
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Tabs, { TabPane } from 'rc-tabs';
|
||||
import TabContent from 'rc-tabs/lib/TabContent';
|
||||
import ScrollableInkTabBar from 'rc-tabs/lib/ScrollableInkTabBar';
|
||||
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import Icon from 'vj/components/react/IconComponent';
|
||||
import Panel from './PanelComponent';
|
||||
import PanelButton from './PanelButtonComponent';
|
||||
import ScratchpadPretestTabPane from './ScratchpadPretestTabPaneContainer';
|
||||
import DataInput from './DataInputComponent';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
current: state.pretest.current,
|
||||
tabs: state.pretest.tabs,
|
||||
meta: state.pretest.meta,
|
||||
input: state.pretest.input,
|
||||
output: state.pretest.output,
|
||||
rid: state.pretest.rid,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleClickClose() {
|
||||
handleDataChange(type, value) {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_UI_SET_VISIBILITY',
|
||||
type: 'SCRATCHPAD_PRETEST_DATA_CHANGE',
|
||||
payload: {
|
||||
uiElement: 'pretest',
|
||||
visibility: false,
|
||||
type,
|
||||
value,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -41,32 +37,18 @@ export default class ScratchpadPretestContainer extends React.PureComponent {
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<Tabs
|
||||
className="scratchpad__panel-tab flex-col flex-fill"
|
||||
activeKey={this.props.current}
|
||||
onChange={(tabId) => this.props.handleSwitchData(tabId)}
|
||||
animation="slide-horizontal"
|
||||
renderTabBar={() => (
|
||||
<ScrollableInkTabBar
|
||||
extraContent={(
|
||||
<span>
|
||||
<PanelButton
|
||||
onClick={() => this.props.handleClickClose()}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</PanelButton>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
renderTabContent={() => <TabContent />}
|
||||
>
|
||||
{this.props.tabs.map((tabId) => (
|
||||
<TabPane tab={this.props.meta[tabId].title} key={tabId}>
|
||||
<ScratchpadPretestTabPane id={tabId} />
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className="flex-row flex-fill">
|
||||
<DataInput
|
||||
title={i18n('Input')}
|
||||
value={this.props.input}
|
||||
onChange={(v) => this.props.handleDataChange('input', v)}
|
||||
/>
|
||||
<DataInput
|
||||
title={i18n('Output')}
|
||||
value={this.props.output}
|
||||
onChange={(v) => this.props.handleDataChange('output', v)}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import DataInput from './DataInputComponent';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
data: state.pretest.data,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleDataChange(id, type, value) {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_PRETEST_DATA_CHANGE',
|
||||
payload: {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
|
||||
...dispatchProps,
|
||||
id: ownProps.id,
|
||||
input: stateProps.data[ownProps.id] ? stateProps.data[ownProps.id].input : '',
|
||||
output: stateProps.data[ownProps.id] ? stateProps.data[ownProps.id].output : '',
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps, mergeProps)
|
||||
export default class ScratchpadPretestTabPaneContainer extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="flex-row flex-fill">
|
||||
<DataInput
|
||||
title={i18n('Sample Input')}
|
||||
value={this.props.input}
|
||||
onChange={(v) => this.props.handleDataChange(this.props.id, 'input', v)}
|
||||
/>
|
||||
<DataInput
|
||||
title={i18n('Sample Output')}
|
||||
value={this.props.output}
|
||||
onChange={(v) => this.props.handleDataChange(this.props.id, 'output', v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ const mapStateToProps = (state) => ({
|
||||
pretestVisible: state.ui.pretest.visible,
|
||||
recordsVisible: state.ui.records.visible,
|
||||
isPosting: state.ui.isPosting,
|
||||
isRunning: state.pretest.isRunning,
|
||||
editorLang: state.editor.lang,
|
||||
});
|
||||
|
||||
@@ -36,15 +37,11 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
postPretest(context) {
|
||||
const state = context.store.getState();
|
||||
const { pretest } = state;
|
||||
const testCases = pretest.tabs;
|
||||
const inputs = testCases.map((tabId) => pretest.data[tabId].input);
|
||||
const outputs = testCases.map((tabId) => pretest.data[tabId].output);
|
||||
const { input } = state.pretest;
|
||||
const req = request.post(Context.postPretestUrl, {
|
||||
lang: state.editor.lang,
|
||||
code: state.editor.code,
|
||||
data_input: inputs,
|
||||
data_output: outputs,
|
||||
input,
|
||||
});
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_POST_PRETEST',
|
||||
@@ -74,7 +71,7 @@ export default class ScratchpadToolbarContainer extends React.PureComponent {
|
||||
return (
|
||||
<Toolbar>
|
||||
<ToolbarButton
|
||||
disabled={this.props.isPosting || !this.props.pretestValid}
|
||||
disabled={this.props.isPosting || this.props.isRunning}
|
||||
className="scratchpad__toolbar__pretest"
|
||||
onClick={() => this.props.postPretest(this.context)}
|
||||
data-global-hotkey="f9"
|
||||
@@ -113,7 +110,7 @@ export default class ScratchpadToolbarContainer extends React.PureComponent {
|
||||
</ToolbarItem>
|
||||
<ToolbarSplit />
|
||||
<ToolbarButton
|
||||
activated
|
||||
activated={this.props.pretestVisible}
|
||||
onClick={() => this.props.togglePanel('pretest')}
|
||||
data-global-hotkey="alt+p"
|
||||
data-tooltip={`${i18n('Toggle Pretest Panel')} (Alt+P)`}
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const initialId = uuid();
|
||||
|
||||
export default function reducer(state = {
|
||||
counter: 1,
|
||||
current: initialId,
|
||||
tabs: [initialId],
|
||||
meta: {
|
||||
[initialId]: {
|
||||
id: initialId,
|
||||
title: '#1',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
[initialId]: {
|
||||
id: initialId,
|
||||
title: '#1',
|
||||
input: '',
|
||||
output: '',
|
||||
},
|
||||
},
|
||||
input: '',
|
||||
output: '',
|
||||
rid: null,
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case 'SCRATCHPAD_PRETEST_DATA_CHANGE': {
|
||||
const { id, type, value } = action.payload;
|
||||
if (action.type === 'SCRATCHPAD_PRETEST_DATA_CHANGE') {
|
||||
const { type, value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
data: {
|
||||
...state.data,
|
||||
[id]: {
|
||||
...state.data[id],
|
||||
[type]: value,
|
||||
},
|
||||
},
|
||||
[type]: value,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
if (action.type === 'SCRATCHPAD_RECORDS_PUSH') {
|
||||
const { rdoc } = action.payload;
|
||||
if (rdoc._id === state.rid) {
|
||||
return {
|
||||
...state,
|
||||
output: (rdoc.stdout || '') + (rdoc.stderr || ''),
|
||||
rid: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (action.type === 'SCRATCHPAD_POST_PRETEST_FULFILLED') {
|
||||
return {
|
||||
...state,
|
||||
rid: action.payload.rid,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -15,11 +15,6 @@ export default function reducer(state = {
|
||||
}
|
||||
case 'SCRATCHPAD_RECORDS_PUSH': {
|
||||
const { rdoc } = action.payload;
|
||||
if (rdoc.uid !== UserContext._id
|
||||
|| rdoc.pid !== Context.problemId
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
rows: [rdoc._id, ...state.rows],
|
||||
|
||||
@@ -167,7 +167,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
React, render, unmountComponentAtNode, Provider, store,
|
||||
} = await loadReactRedux(ScratchpadReducer);
|
||||
|
||||
const sock = new SockJs(Context.socketUrl);
|
||||
const sock = new SockJs(`/conn/pretest?token=${UiContext.token}&pid=${Context.problemId}`);
|
||||
sock.onmessage = (message) => {
|
||||
const msg = JSON.parse(message.data);
|
||||
store.dispatch({
|
||||
|
||||
@@ -22,12 +22,12 @@ clone 本项目到一个空文件夹,并进行编译:
|
||||
git clone https://github.com/hydro-dev/Hydro.git
|
||||
cd Hydro
|
||||
yarn build:all
|
||||
yarn start
|
||||
node .build/app.js
|
||||
```
|
||||
|
||||
注意:单个Hydro实例需要约100MB的运行内存,但在载入时可能需要较多内存(取决于安装的模块数量与大小)
|
||||
|
||||
Hydro 会自行初始化并监听 8888 端口。请用浏览器访问并进行相应配置。
|
||||
数据库配置完成后,会自动创建 Root 用户。(账号 `root` ,密码 `rootroot`),请及时修改。
|
||||
数据库配置完成后,会自动创建 Root 用户。(账号 `Root` ,密码 `rootroot`),请及时修改。
|
||||
|
||||
之后的进阶配置可在 管理 面板进行。
|
||||
Reference in new issue
Block a user