+nginx.conf; update scratchpad

This commit is contained in:
undefined committed 2020-07-16 15:18:55 +08:00
1 parent 6ad41dc2ae
commit 2d5eef8d08
28 files changed
+206 -230

No files matched your search

+2
View File
@@ -7,3 +7,5 @@ web端运行脚本似乎异常
举报功能
SETTINGS_PRIVACY.allowRegisteredUsers
model.setting.ui.name
pdoc.secretConfig?
在线IDE 更换颜色主题
+9
View File
@@ -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
+17
View File
@@ -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
View File
@@ -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.');
+6 -1
View File
@@ -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 });
+8 -3
View File
@@ -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 });
+1
View File
@@ -140,6 +140,7 @@ class UiSettingsHandler extends Handler {
header_background_2x,
});
this.response.type = 'text/css';
this.ctx.set('nolog', '1');
}
}
+18 -10
View File
@@ -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
View File
@@ -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,
-10
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+3 -2
View File
@@ -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 %}
+3 -2
View File
@@ -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>
+11 -11
View File
@@ -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 %}
+23 -1
View File
@@ -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)`}
+22 -33
View File
@@ -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],
+1 -1
View File
@@ -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({
+2 -2
View File
@@ -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`),请及时修改。
之后的进阶配置可在 管理 面板进行。