diff --git a/build/index.js b/build/index.js index 1a9f218d3..b05876b68 100644 --- a/build/index.js +++ b/build/index.js @@ -2,7 +2,7 @@ const fs = require('fs'); const root = require('./root'); async function build(type) { - if (!['development', 'production'].includes(type)) throw new Error('Unknown type: ' + type); + if (!['development', 'production'].includes(type)) throw new Error(`Unknown type: ${type}`); try { fs.rmdirSync(root('.build')); } catch (e) { } diff --git a/hydro/development.js b/hydro/development.js index 722db7195..424bafae7 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -11,7 +11,7 @@ process.stdin.on('data', async (input) => { global.Hydro = { handler: {}, - template: {} + template: {}, }; require('./lib/i18n'); @@ -44,7 +44,7 @@ async function run() { for (const i in global.Hydro.handler) { await global.Hydro.handler[i].apply(); } - const notfound = require(`./handler/notfound`); + const notfound = require('./handler/notfound'); await notfound.apply(); server.start(); } diff --git a/hydro/error.js b/hydro/error.js index d140501c5..a9836bb17 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -225,6 +225,13 @@ class DiscussionNotFoundError extends NotFoundError { } } +class MessageNotFoundError extends NotFoundError { + constructor(mid) { + super('Message {0} not found.'); + this.params = [mid]; + } +} + global.Hydro.error = module.exports = { BadRequestError, BlacklistedError, @@ -258,6 +265,7 @@ global.Hydro.error = module.exports = { DocumentNotFoundError, DiscussionNotFoundError, RoleAlreadyExistError, + MessageNotFoundError, }; /* @@ -314,12 +322,6 @@ class DiscussionNodeAlreadyExistError(ForbiddenError): return 'Discussion node {1} already exists.' -class MessageNotFoundError(NotFoundError): - @property - def message(self): - return 'Message {0} not found.' - - class TrainingRequirementNotSatisfiedError(ForbiddenError): @property def message(self): diff --git a/hydro/handler/discussion.js b/hydro/handler/discussion.js index b3c2f19ab..504d75f72 100644 --- a/hydro/handler/discussion.js +++ b/hydro/handler/discussion.js @@ -25,8 +25,8 @@ class DiscussionHandler extends Handler { if (did) { this.ddoc = await discussion.get(did); if (!this.ddoc) throw new DiscussionNotFoundError(did); - type = this.ddoc.type; - docId = this.ddoc.parent; + type = this.ddoc.parentType; + docId = this.ddoc.parentId; if (drid) { this.drdoc = await discussion.getReply(drid, did); if (!this.drdoc) throw new DiscussionNotFoundError(drid); diff --git a/hydro/handler/home.js b/hydro/handler/home.js index fcdb3e12f..16e33ddbf 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -1,13 +1,15 @@ const { VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, - NotFoundError, + NotFoundError, MessageNotFoundError, } = require('../error'); const options = require('../options'); +const bus = require('../service/bus'); const { Route, Handler } = require('../service/server'); const md5 = require('../lib/md5'); const contest = require('../model/contest'); const user = require('../model/user'); const setting = require('../model/setting'); +const discussion = require('../model/discussion'); const token = require('../model/token'); const training = require('../model/training'); const { @@ -45,14 +47,13 @@ class HomeHandler extends Handler { } async discussion() { - // TODO(masnn) - // if (this.user.hasPerm(PERM_VIEW_DISCUSSION)) { - // const ddocs = await discussion.getMulti() - // .limit(DISCUSSIONS_ON_MAIN) - // .toArray(); - // const vndict = await discussion.getListVnodes(map(discussion.node_id, ddocs)); - // return [ddocs, vndict]; - // } + if (this.user.hasPerm(PERM_VIEW_DISCUSSION)) { + const ddocs = await discussion.getMulti() + .limit(DISCUSSIONS_ON_MAIN) + .toArray(); + const vndict = await discussion.getListVnodes(ddocs, this); + return [ddocs, vndict]; + } return [[], {}]; } @@ -164,13 +165,72 @@ class UserChangemailWithCodeHandler extends Handler { const udoc = await user.getByEmail(tdoc.mail); if (udoc) throw new UserAlreadyExistError(tdoc.mail); // TODO(twd2): Ensure mail is unique. - await user.set_mail(this.user._id, tdoc.mail); + await user.setEmail(this.user._id, tdoc.mail); await token.delete(code, token.TYPE_CHANGEMAIL); - this.response.body = {}; this.response.redirect = '/home/security'; } } +class HomeMessagesHandler extends Handler { + udoc(udict, key) { // eslint-disable-line class-methods-use-this + const udoc = udict[key]; + if (!udoc) return; + const gravatar_url = misc.gravatar_url(udoc.gravatar); + if (udoc.gravatar) udict[key] = { ...udoc, gravatar_url, gravatar: '' }; + } + + async prepare() { + this.checkPerm(PERM_LOGGEDIN); + } + + async get() { + // TODO(iceboy): projection, pagination. + const mdocs = await message.getMulti(this.user._id).sort('_id', -1).limit(50).toArray(); + const udict = await user.getList([ + ...mdocs.map((mdoc) => mdoc.from), + ...mdocs.map((mdoc) => mdoc.to), + ]); + // TODO(twd2): improve here: + for (const mdoc of mdocs) { + this.modify_udoc(udict, mdoc.from); + this.modify_udoc(udict, mdoc.to); + } + this.response.body = { messages: mdocs, udict }; + this.response.template = 'home_message.html'; + } + + async postSendMessage({ uid, content }) { + const udoc = await user.getById(uid); + const mdoc = await message.add(this.user._id, udoc._id, content); + // TODO(twd2): improve here: + // projection + mdoc.from = this.user; + this.modify_udoc(mdoc, 'from'); + mdoc.to = udoc; + this.modify_udoc(mdoc, 'to'); + if (this.user._id !== uid) { + await bus.publish(`user_message-${uid}`, { type: 'new', data: mdoc }); + } + this.back({ mdoc }); + } + + async postReplyMessage({ message_id, content }) { + const [mdoc, reply] = await message.addReply(message_id, this.user._id, content); + if (!mdoc) throw new MessageNotFoundError(message_id); + if (mdoc.from !== mdoc.to) { + const other = mdoc.from === this.user._id ? mdoc.from : mdoc.to; + await bus.publish(`user_message-${other}`, { type: 'reply', data: mdoc }); + } + mdoc.reply = [reply]; + this.back({ reply }); + } + + async postDeleteMessage({ message_id }) { + await message.delete(message_id, this.user._id); + this.back(); + } +} + async function apply() { Route('/', module.exports.HomeHandler); Route('/home/security', module.exports.HomeSecurityHandler); @@ -181,75 +241,8 @@ async function apply() { global.Hydro.handler.home = module.exports = { HomeHandler, HomeSecurityHandler, HomeSettingsHandler, UserChangemailWithCodeHandler, apply, }; + /* -@app.route('/home/messages', 'home_messages', global_route=True) -class HomeMessagesHandler(base.OperationHandler): - def modify_udoc(this, udict, key): - udoc = udict.get(key) - if not udoc: - return - gravatar_url = misc.gravatar_url(udoc.get('gravatar')) - if 'gravatar' in udoc and udoc['gravatar']: - udict[key] = {**udoc, - 'gravatar_url': gravatar_url, - 'gravatar': ''} - - @base.require_priv(builtin.PRIV_USER_PROFILE) - async def get(this): - # TODO(iceboy): projection, pagination. - mdocs = await message.get_multi(this.user['_id']).sort([('_id', -1)]).limit(50).to_list() - udict = await user.get_dict( - itertools.chain.from_iterable((mdoc['sender_uid'], mdoc['sendee_uid']) for mdoc in mdocs), - fields=user.PROJECTION_PUBLIC) - # TODO(twd2): improve here: - for mdoc in mdocs: - this.modify_udoc(udict, mdoc['sender_uid']) - this.modify_udoc(udict, mdoc['sendee_uid']) - this.json_or_render('home_messages.html', messages=mdocs, udict=udict) - - @base.require_priv(builtin.PRIV_USER_PROFILE) - @base.require_csrf_token - @base.sanitize - async def post_send_message(this, *, uid: int, content: str): - udoc = await user.get_by_uid(uid, user.PROJECTION_PUBLIC) - if not udoc: - raise error.UserNotFoundError(uid) - mdoc = await message.add(this.user['_id'], udoc['_id'], content) - # TODO(twd2): improve here: - # projection - sender_udoc = await user.get_by_uid(this.user['_id'], user.PROJECTION_PUBLIC) - mdoc['sender_udoc'] = sender_udoc - this.modify_udoc(mdoc, 'sender_udoc') - mdoc['sendee_udoc'] = udoc - this.modify_udoc(mdoc, 'sendee_udoc') - if this.user['_id'] != uid: - await bus.publish('message_received-' + str(uid), {'type': 'new', 'data': mdoc}) - this.json_or_redirect(this.url, mdoc=mdoc) - - @base.require_priv(builtin.PRIV_USER_PROFILE) - @base.require_csrf_token - @base.sanitize - async def post_reply_message(this, *, message_id: objectid.ObjectId, content: str): - mdoc, reply = await message.add_reply(message_id, this.user['_id'], content) - if not mdoc: - return error.MessageNotFoundError(message_id) - if mdoc['sender_uid'] != mdoc['sendee_uid']: - if mdoc['sender_uid'] == this.user['_id']: - other_uid = mdoc['sendee_uid'] - else: - other_uid = mdoc['sender_uid'] - mdoc['reply'] = [reply] - await bus.publish('message_received-' + str(other_uid), {'type': 'reply', 'data': mdoc}) - this.json_or_redirect(this.url, reply=reply) - - @base.require_priv(builtin.PRIV_USER_PROFILE) - @base.require_csrf_token - @base.sanitize - async def post_delete_message(this, *, message_id: objectid.ObjectId): - await message.delete(message_id, this.user['_id']) - this.back(); - - @app.connection_route('/home/messages-conn', 'home_messages-conn', global_route=True) class HomeMessagesConnection(base.Connection): @base.require_priv(builtin.PRIV_USER_PROFILE) diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 67b1bc17f..081f1ad38 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -90,7 +90,6 @@ class JudgeHandler extends Handler { } async get({ check = false }) { - this.response.body = {}; if (check) return; const tasks = []; let rid = await queue.get('judge', false); @@ -122,17 +121,16 @@ class JudgeHandler extends Handler { tasks.push(task); rid = await queue.get('judge', false); // eslint-disable-line no-await-in-loop } + this.response.body = { tasks }; } async postNext() { await next(this.request.body); - this.response.body = {}; } async postEnd() { this.request.body.judger = this.user._id; await end(this.request.body); - this.response.body = {}; } } class JudgeConnectionHandler extends ConnectionHandler { diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js index 346862228..fec11c00d 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.js @@ -34,7 +34,7 @@ class ManageEditHandler extends ManageHandler { await user.setById(0, { uname, unameLower, gravatar, bio, }); - this.back(); + this.response.redirect = '/manage/dashboard'; } } @@ -43,7 +43,7 @@ class ManageUserHandler extends ManageHandler { const uids = []; const rudocs = {}; const [udocs, roles, system] = await Promise.all([ - user.getMulti({ role: { $ne: 'default' } }).toArray(), + user.getMulti({ role: { $nin: ['default', 'guest'] } }).toArray(), user.getRoles(), user.getById(0), ]); @@ -61,7 +61,7 @@ class ManageUserHandler extends ManageHandler { } async postSetUser({ uid, role }) { - await user.setById(uid, { role }); + await user.setRole(uid, role); this.back(); } } diff --git a/hydro/handler/notfound.js b/hydro/handler/notfound.js index 07f3e24d2..b1467deb1 100644 --- a/hydro/handler/notfound.js +++ b/hydro/handler/notfound.js @@ -3,7 +3,7 @@ const { NotFoundError } = require('../error'); class NotFoundHandler extends Handler { async prepare() { - throw new NotFoundError(); + throw new NotFoundError(); // eslint-disable-line class-methods-use-this } } diff --git a/hydro/handler/user.js b/hydro/handler/user.js index d52a9235d..496a0da1f 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -25,7 +25,6 @@ class UserLoginHandler extends Handler { udoc.password = ''; this.session.uid = udoc._id; this.session.rememberme = rememberme; - this.response.body = {}; const referer = this.request.headers.referer || '/'; this.response.redirect = referer.endsWith('/login') ? '/' : referer; } @@ -42,7 +41,6 @@ class UserLogoutHandler extends Handler { async post() { this.session = { uid: 1 }; - this.response.body = {}; } } @@ -66,7 +64,6 @@ class UserRegisterHandler extends Handler { if (options.smtp.user) { const m = await this.renderHTML('user_register_mail', { url: `/register/${t}` }); await sendMail(mail, 'Sign Up', 'user_register_mail', m); - this.response.body = {}; this.response.template = 'user_register_mail_sent.html'; } else { this.response.redirect = `/register/${t[0]}`; @@ -98,7 +95,6 @@ class UserRegisterWithCodeHandler extends Handler { }); await token.delete(code, token.TYPE_REGISTRATION); this.session.uid = uid; - this.response.body = {}; this.response.redirect = '/'; } } @@ -123,7 +119,6 @@ class UserLostPassHandler extends Handler { ); const m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname }); await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m); - this.response.body = {}; this.response.template = 'user_lostpass_mail_sent.html'; } } @@ -174,7 +169,7 @@ class UserSearchHandler extends Handler { } for (const i in udocs) { if (udocs[i].gravatar) { - udocs[i].gravatar_url = misc.gravatar_url[udocs[i].gravatar]; + udocs[i].gravatar_url = misc.gravatar_url(udocs[i].gravatar); } } this.response.body = udocs; diff --git a/hydro/interface.js b/hydro/interface.js index 4da7a4168..30db5c4cf 100644 --- a/hydro/interface.js +++ b/hydro/interface.js @@ -15,6 +15,7 @@ exports.udoc = { regat: new Date(), regip: '0.0.0.0', gravatar: '', + viewLang: 'zh_CN', loginat: new Date(), loginip: '0.0.0.0', }; diff --git a/hydro/interface.ts b/hydro/interface.ts index 76236fcc2..b4c93821a 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -13,6 +13,10 @@ export interface Udoc { nSubmit: number nLike: number perm: string + viewLang: string + codeLang: string + codeTemplate: string + gravatar: string hasPerm: Function checkPassword: Function } diff --git a/hydro/lib/misc.js b/hydro/lib/misc.js index 1071f5c21..f0a59a1c6 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.js @@ -1,8 +1,9 @@ const md5 = require('./md5'); -exports.gravatar_url = function gravatarUrl(email, size) { +exports.gravatar_url = function gravatarUrl(email, size = 32) { return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`; }; + exports.datetime_span = function datetimeSpan(dt, relative = true, format = '%Y-%m-%d %H:%M:%S') { if (!dt) return 'DATETIME_SPAN_ERROR'; if (dt.generationTime) dt = new Date(dt.generationTime * 1000); @@ -13,6 +14,7 @@ exports.datetime_span = function datetimeSpan(dt, relative = true, format = '%Y- dt.format(format), ); }; + exports.paginate = function* paginate(page, numPages) { const radius = 2; let first; let last; @@ -33,6 +35,7 @@ exports.paginate = function* paginate(page, numPages) { if (page < numPages) yield ['next', page + 1]; yield ['last', numPages]; }; + exports.format_size = function formatSize(size, base = 1) { size *= base; const unit = 1024; diff --git a/hydro/model/discussion.js b/hydro/model/discussion.js index 6e487e482..59385ec6b 100644 --- a/hydro/model/discussion.js +++ b/hydro/model/discussion.js @@ -1,4 +1,5 @@ const { ObjectID } = require('bson'); +const problem = require('./problem'); const { DocumentNotFoundError } = require('../error'); const db = require('../service/db.js'); @@ -6,8 +7,8 @@ const coll = db.collection('discussion'); const collReply = db.collection('discussion.reply'); const collStatus = db.collection('discussion.status'); -function add(parentType, parentId, owner, title, content, ip = null, highlight = false) { - return coll.insertOne({ +async function add(parentType, parentId, owner, title, content, ip = null, highlight = false) { + let res = await coll.insertOne({ owner, title, content, @@ -18,6 +19,7 @@ function add(parentType, parentId, owner, title, content, ip = null, highlight = nReply: 0, updateAt: new Date(), }); + return res.insertedId; } function get(did) { @@ -128,6 +130,26 @@ function getStatus(did, uid) { return collStatus.findOne({ did, uid }); } +async function getVnode(ddoc, handler) { + if (ddoc.parentType === 'problem') { + const pdoc = await problem.getById(ddoc.parentId); + if (!pdoc) return null; + if (pdoc.hidden && handler) handler.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + return { ...pdoc, parentType: ddoc.parentType, parentId: ddoc.parentId }; + } if (ddoc.parentType === 'contest') { + const tdoc = await contest.get(ddoc.parentId); + return { ...tdoc, parentType: ddoc.parentType, parentId: ddoc.parentId }; + } return null; +} + +async function getListVnodes(ddocs, handler) { + const res = {}; + for (const ddoc of ddocs) { + res[ddoc._id] = await getVnode(ddoc, handler); + } + return res; +} + module.exports = { add, get, @@ -147,4 +169,6 @@ module.exports = { delTailReply, setStar, getStatus, + getVnode, + getListVnodes, }; diff --git a/hydro/model/user.js b/hydro/model/user.js index f28adb3fd..2ea86aa02 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -15,12 +15,13 @@ class USER { this.salt = user.salt; this.hash = user.hash; this.perm = user.perm; - this.lang = user.language || 'zh_CN'; + this.viewLang = user.language || 'zh_CN'; this.codeLang = user.codeLang || 'c'; this.codeTemplate = user.codeTemplate || ''; this.regat = user.regat; this.loginat = user.loginat; this.bio = user.bio || ''; + this.gravatar = user.gravatar || ''; this.nAccept = user.nAccept || 0; this.nSubmit = user.nSubmit || 0; } @@ -144,7 +145,7 @@ async function getPrefixList(prefix, limit = 50) { async function setRole(uid, role) { const udoc = await getById(uid); return await Promise.all([ - coll.findOneAndUpdate({ _id: uid }, { role }), + coll.findOneAndUpdate({ _id: uid }, { $set: { role } }), collRole.updateOne({ _id: udoc.role }, { $inc: { count: -1 } }), collRole.updateOne({ _id: role }, { $inc: { count: 1 } }), ]); diff --git a/hydro/options.js b/hydro/options.js index 89c164313..310979735 100644 --- a/hydro/options.js +++ b/hydro/options.js @@ -44,6 +44,7 @@ let options = { CONTEST_PER_PAGE: 20, TRAINING_PER_PAGE: 10, DISCUSSION_PER_PAGE: 50, + REPLY_PER_PAGE: 50, CONTESTS_ON_MAIN: 5, TRAININGS_ON_MAIN: 5, DISCUSSIONS_ON_MAIN: 20, diff --git a/hydro/service/server.js b/hydro/service/server.js index 8341ee157..57c5848d4 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -113,7 +113,6 @@ class Handler { back(body) { if (body) this.response.body = body; - else if (this.preferJson) this.response.body = {}; this.response.redirect = this.request.headers.referer || '/'; } @@ -129,6 +128,7 @@ class Handler { } async ___prepare() { + this.response.body = {}; this.now = new Date(); this._handler.sid = this.request.cookies.get('sid'); this._handler.save = this.request.cookies.get('save'); @@ -241,7 +241,7 @@ class Handler { } } -const check = ['tid', 'rid', 'did', 'drid', 'drrid', 'psid', 'psrid']; +const check = ['tid', 'rid', 'did', 'drid', 'drrid', 'psid', 'psrid', 'docId']; function Route(route, RouteHandler) { router.all(route, async (ctx) => { diff --git a/locales/zh_CN.yaml b/locales/zh_CN.yaml index 4dd71fb79..b82a7e147 100644 --- a/locales/zh_CN.yaml +++ b/locales/zh_CN.yaml @@ -401,8 +401,7 @@ Continue: 继续 Role: 角色 Select a role: 选择一个角色 Add User: 添加用户 -Add User to Domain: 添加用户到域 -Remove Selected User from Domain: 将所选用户移出域 +Remove Selected User: 将所选用户移出 Set Roles for Selected User: 设置所选用户的角色 Create Role: 创建角色 Role name can only contains numbers, letters and underscores.: 角色名称只能包含数字、字母和下划线。 @@ -511,8 +510,8 @@ You can create your own training plans and share with others.: 您可以创建 Set Role: 设置角色 Set Roles for selected users: 为所选用户设置角色 Please select at least one user to perform this operation.: 请选择至少一个用户来进行操作。 -Confirm removing the selected users from this domain?: 您确定将所选用户从此域中移除吗? -Their account will not be deleted and they will be with the default role when visiting this domain.: 他们的账号不会被删除,并且之后将以 default 角色访问此域。 +Confirm removing the selected users?: 您确定将所选用户移除吗? +Their account will not be deleted and they will be with the default role.: 他们的账号不会被删除,并且之后将以 default 角色访问。 Selected users have been removed from the domain.: 所选用户已从此域中移除。 Role has been updated to {0} for selected users.: 所选用户角色已更新为 {0}。 Role has been updated to {0}.: 角色已更新为 {0}。 diff --git a/locales/zh_TW.yaml b/locales/zh_TW.yaml index f6951e9a9..52e2d9ee2 100644 --- a/locales/zh_TW.yaml +++ b/locales/zh_TW.yaml @@ -387,8 +387,7 @@ User ID: 使用者 ID Role: 角色 Select a role: 選擇一個角色 Add User: 新增使用者 -Add User to Domain: 新增使用者到域 -Remove Selected User from Domain: 將所選使用者移出域 +Remove Selected User: 將所選使用者移出 Set Roles for Selected User: 設定所選使用者的角色 Create Role: 創建角色 Role name can only contains numbers, letters and underscores.: 角色名稱只能包含數字、字母和下劃線。 @@ -497,8 +496,8 @@ no_translation_warn:
這部分內容正在翻譯。 {% for k, v in args.options %} - {% endfor %} diff --git a/templates/discussion_create.html b/templates/discussion_create.html index 7d5994345..ec114d619 100644 --- a/templates/discussion_create.html +++ b/templates/discussion_create.html @@ -1,10 +1,11 @@ +{% set page_name = "discussion_create" %} {% extends "layout/basic.html" %} {% block content %}-{{ _('Create Discussion in {0}').format(title) }}
+{{ _('Create Discussion in {0}').format(vnode.title) }}