core: allow lock topic

This commit is contained in:
undefined committed 2022-02-12 19:21:35 +08:00
1 parent 931e96a6b4
commit c305e57860
11 files changed
+49 -18

No files matched your search

+3
View File
@@ -437,6 +437,7 @@ List View: 列表视图
Live: 正在进行 Live: 正在进行
Live...: 正在进行… Live...: 正在进行…
Location: 位置 Location: 位置
Lock Topic: 锁定主题
Login to Attend Contest: 登录后参加比赛 Login to Attend Contest: 登录后参加比赛
Login to Claim Homework: 登录后认领作业 Login to Claim Homework: 登录后认领作业
Login to Create a Discussion: 登录后创建讨论 Login to Create a Discussion: 登录后创建讨论
@@ -750,6 +751,7 @@ Terms of Service: 服务条款
Test data comes from: 测试数据来自 Test data comes from: 测试数据来自
Testdata Config: 测试数据配置 Testdata Config: 测试数据配置
Text: 文本 Text: 文本
The discussion is locked, you can not reply anymore.: 此讨论已经锁定,您不能再回复。
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。
The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。 The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。
The invitation code you provided is invalid.: 您提供的邀请码无效。 The invitation code you provided is invalid.: 您提供的邀请码无效。
@@ -800,6 +802,7 @@ Unhide Selected: 取消隐藏选中
Unique: 唯一 Unique: 唯一
Unknown field {0}.: 未知字段 {0}。 Unknown field {0}.: 未知字段 {0}。
Unknown: 未知 Unknown: 未知
Unlock Topic: 解锁主题
Unordered List: 无序列表 Unordered List: 无序列表
Unsaved session expire seconds: 未保存的会话的过期秒数 Unsaved session expire seconds: 未保存的会话的过期秒数
Unstar Topic: 取消收藏 Unstar Topic: 取消收藏
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "3.4.24", "version": "3.4.25",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "src/loader", "main": "src/loader",
"module": "src/loader", "module": "src/loader",
+1
View File
@@ -98,6 +98,7 @@ export const DomainJoinForbiddenError = Err('DomainJoinForbiddenError', Forbidde
export const DomainJoinAlreadyMemberError = Err('DomainJoinAlreadyMemberError', ForbiddenError, 'Failed to join the domain. You are already a member.'); export const DomainJoinAlreadyMemberError = Err('DomainJoinAlreadyMemberError', ForbiddenError, 'Failed to join the domain. You are already a member.');
export const InvalidJoinInvitationCodeError = Err('InvalidJoinInvitationCodeError', ForbiddenError, 'The invitation code you provided is invalid.'); export const InvalidJoinInvitationCodeError = Err('InvalidJoinInvitationCodeError', ForbiddenError, 'The invitation code you provided is invalid.');
export const CurrentPasswordError = Err('CurrentPasswordError', ForbiddenError, "Current password doesn't match."); export const CurrentPasswordError = Err('CurrentPasswordError', ForbiddenError, "Current password doesn't match.");
export const DiscussionLockedError = Err('DiscussionLockedError', ForbiddenError, 'The discussion is locked, you can not reply anymore.');
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.'); export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.'); export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
+17 -2
View File
@@ -1,6 +1,9 @@
import { isSafeInteger } from 'lodash'; import { isSafeInteger } from 'lodash';
import { ObjectID } from 'mongodb'; import { ObjectID } from 'mongodb';
import { DiscussionNotFoundError, DocumentNotFoundError, PermissionError } from '../error'; import {
DiscussionLockedError, DiscussionNotFoundError, DocumentNotFoundError,
PermissionError,
} from '../error';
import { DiscussionDoc, DiscussionReplyDoc, DiscussionTailReplyDoc } from '../interface'; import { DiscussionDoc, DiscussionReplyDoc, DiscussionTailReplyDoc } from '../interface';
import paginate from '../lib/paginate'; import paginate from '../lib/paginate';
import { PERM, PRIV } from '../model/builtin'; import { PERM, PRIV } from '../model/builtin';
@@ -207,6 +210,14 @@ class DiscussionDetailHandler extends DiscussionHandler {
this.checkPriv(PRIV.PRIV_USER_PROFILE); this.checkPriv(PRIV.PRIV_USER_PROFILE);
} }
@param('did', Types.ObjectID)
@param('lock', Types.Boolean)
async postSetLock(domainId: string, did: ObjectID, lock: boolean) {
if (!this.user.own(this.ddoc)) this.checkPerm(PERM.PERM_LOCK_DISCUSSION);
await discussion.edit(domainId, did, { lock });
this.back();
}
@param('type', Types.Range(['did', 'drid'])) @param('type', Types.Range(['did', 'drid']))
@param('id', Types.ObjectID) @param('id', Types.ObjectID)
@param('emoji', Types.Emoji) @param('emoji', Types.Emoji)
@@ -223,6 +234,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
@param('content', Types.Content) @param('content', Types.Content)
async postReply(domainId: string, did: ObjectID, content: string) { async postReply(domainId: string, did: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_DISCUSSION); this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
if (this.ddoc.lock) throw new DiscussionLockedError(domainId, did);
await this.limitRate('add_discussion', 3600, 60); await this.limitRate('add_discussion', 3600, 60);
const targets = new Set(Array.from(content.matchAll(/@\[\]\(\/user\/(\d+)\)/g)).map((i) => +i[1])); const targets = new Set(Array.from(content.matchAll(/@\[\]\(\/user\/(\d+)\)/g)).map((i) => +i[1]));
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i); const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
@@ -241,6 +253,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
@param('content', Types.Content) @param('content', Types.Content)
async postTailReply(domainId: string, drid: ObjectID, content: string) { async postTailReply(domainId: string, drid: ObjectID, content: string) {
this.checkPerm(PERM.PERM_REPLY_DISCUSSION); this.checkPerm(PERM.PERM_REPLY_DISCUSSION);
if (this.ddoc.lock) throw new DiscussionLockedError(domainId, this.ddoc.docId);
await this.limitRate('add_discussion', 3600, 60); await this.limitRate('add_discussion', 3600, 60);
const targets = new Set(Array.from(content.matchAll(/@\[\]\(\/user\/(\d+)\)/g)).map((i) => +i[1])); const targets = new Set(Array.from(content.matchAll(/@\[\]\(\/user\/(\d+)\)/g)).map((i) => +i[1]));
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i); const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
@@ -355,7 +368,9 @@ class DiscussionEditHandler extends DiscussionHandler {
if (!this.user.hasPerm(PERM.PERM_HIGHLIGHT_DISCUSSION)) highlight = this.ddoc.highlight; if (!this.user.hasPerm(PERM.PERM_HIGHLIGHT_DISCUSSION)) highlight = this.ddoc.highlight;
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin; if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
await Promise.all([ await Promise.all([
discussion.edit(domainId, did, title, content, highlight, pin), discussion.edit(domainId, did, {
title, content, highlight, pin,
}),
oplog.log(this, 'discussion.edit', this.ddoc), oplog.log(this, 'discussion.edit', this.ddoc),
]); ]);
this.response.body = { did }; this.response.body = { did };
+1
View File
@@ -435,6 +435,7 @@ declare module './model/discussion' {
react: Record<string, number>; react: Record<string, number>;
sort: number; sort: number;
lastRCount: number; lastRCount: number;
lock?: boolean;
} }
} }
+2
View File
@@ -58,6 +58,7 @@ export const PERM = {
PERM_DELETE_DISCUSSION_REPLY: 1n << 38n, PERM_DELETE_DISCUSSION_REPLY: 1n << 38n,
PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n, PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n,
PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n, PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n,
PERM_LOCK_DISCUSSION: 64n,
// Contest // Contest
PERM_VIEW_CONTEST: 1n << 41n, PERM_VIEW_CONTEST: 1n << 41n,
@@ -131,6 +132,7 @@ export const PERMS = [
Permission('perm_discussion', PERM.PERM_PIN_DISCUSSION, 'Pin discussions'), Permission('perm_discussion', PERM.PERM_PIN_DISCUSSION, 'Pin discussions'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION, 'Edit discussions'), Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION, 'Edit discussions'),
Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_SELF, 'Edit own discussions'), Permission('perm_discussion', PERM.PERM_EDIT_DISCUSSION_SELF, 'Edit own discussions'),
Permission('perm_discussion', PERM.PERM_LOCK_DISCUSSION, 'Lock discussions'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION, 'Delete discussions'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION, 'Delete discussions'),
Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_SELF, 'Delete own discussions'), Permission('perm_discussion', PERM.PERM_DELETE_DISCUSSION_SELF, 'Delete own discussions'),
Permission('perm_discussion', PERM.PERM_REPLY_DISCUSSION, 'Reply discussions'), Permission('perm_discussion', PERM.PERM_REPLY_DISCUSSION, 'Reply discussions'),
+3 -8
View File
@@ -57,6 +57,7 @@ export const PROJECTION_LIST: DiscussionDoc.Field[] = [
]; ];
export const PROJECTION_PUBLIC: DiscussionDoc.Field[] = [ export const PROJECTION_PUBLIC: DiscussionDoc.Field[] = [
...PROJECTION_LIST, 'content', 'history', 'react', 'maintainer', ...PROJECTION_LIST, 'content', 'history', 'react', 'maintainer',
'lock',
]; ];
export const typeDisplay = { export const typeDisplay = {
@@ -103,14 +104,8 @@ export async function get<T extends DiscussionDoc.Field>(
return await document.get(domainId, document.TYPE_DISCUSSION, did, projection); return await document.get(domainId, document.TYPE_DISCUSSION, did, projection);
} }
export function edit( export function edit(domainId: string, did: ObjectID, $set: Partial<DiscussionDoc>) {
domainId: string, did: ObjectID, return document.set(domainId, document.TYPE_DISCUSSION, did, $set);
title: string, content: string, highlight: boolean, pin: boolean,
): Promise<DiscussionDoc | null> {
const payload = {
title, content, highlight, pin,
};
return document.set(domainId, document.TYPE_DISCUSSION, did, payload);
} }
export function inc( export function inc(
+1 -1
View File
@@ -26,7 +26,7 @@ class MessageModel {
from, to, content, _id: res.insertedId, flag, from, to, content, _id: res.insertedId, flag,
}; };
if (from !== to) bus.broadcast('user/message', to, mdoc); if (from !== to) bus.broadcast('user/message', to, mdoc);
await user.inc(to, 'unreadMsg', 1); if (flag & MessageModel.FLAG_UNREAD) await user.inc(to, 'unreadMsg', 1);
return mdoc; return mdoc;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.31.13", "version": "4.31.14",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -34,7 +34,8 @@
reply_edit_text = 'Update', reply_edit_text = 'Update',
comment_delete_op = 'delete_reply', comment_delete_op = 'delete_reply',
reply_delete_op = 'delete_tail_reply', reply_delete_op = 'delete_tail_reply',
reactions = {} reactions = {},
ddoc = {}
) %} ) %}
<ul class="dczcomments__list view--discussion"> <ul class="dczcomments__list view--discussion">
{% if handler.user.hasPerm(comment_post_perm) %} {% if handler.user.hasPerm(comment_post_perm) %}
@@ -44,6 +45,9 @@
<img src="{{ avatarUrl(handler.user.avatar, 60) }}" width="60" height="60" class="medium user-profile-avatar"> <img src="{{ avatarUrl(handler.user.avatar, 60) }}" width="60" height="60" class="medium user-profile-avatar">
</div> </div>
<div class="media__body top"> <div class="media__body top">
{% if ddoc.lock %}
<textarea class="textbox" name="dczcomments__dummy-box" readonly placeholder="{{ _('The discussion is locked, you can not reply anymore.') }}"></textarea>
{% else %}
<div class="hasjs--hide"> <div class="hasjs--hide">
<form method="post" class="dczcomments__box" name="dczcomments__box__form" data-hotkey="ctrl+enter:submit,esc:vjCommentBoxCancel"> <form method="post" class="dczcomments__box" name="dczcomments__box__form" data-hotkey="ctrl+enter:submit,esc:vjCommentBoxCancel">
<div> <div>
@@ -73,6 +77,7 @@
</div> </div>
</li> </li>
</ul> </ul>
{% endif %}
</div> </div>
</div> </div>
</li> </li>
@@ -45,6 +45,16 @@
<span class="starred--show"><span class="icon icon-star"></span> {{ _('Unstar Topic') }}</span> <span class="starred--show"><span class="icon icon-star"></span> {{ _('Unstar Topic') }}</span>
</button> </button>
</form></li> </form></li>
{% endif %}
{% if handler.user.own(ddoc) or handler.user.hasPerm(perm.PERM_LOCK_DISCUSSION) %}
<li><form class="form--inline" method="post">
<input type="hidden" name="operation" value="set_lock">
{% if not ddoc.lock %}<input type="hidden" name="lock" value="1">{% endif %}
<button class="star{% if ddoc.lock %} activated{% endif %}" type="submit">
<span class="starred--hide">{{ _('Lock Topic') }}</span>
<span class="starred--show">{{ _('Unlock Topic') }}</span>
</button>
</form></li>
{% endif %} {% endif %}
<li><a href="{{ url('wiki_help', anchor='contact') }}"> <li><a href="{{ url('wiki_help', anchor='contact') }}">
<span class="icon icon-warning"></span> {{ _('Report') }} <span class="icon icon-warning"></span> {{ _('Report') }}
@@ -78,7 +88,8 @@
reply_delete_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION if handler.user.own(ddoc) else perm.PERM_DELETE_DISCUSSION_REPLY, reply_delete_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION if handler.user.own(ddoc) else perm.PERM_DELETE_DISCUSSION_REPLY,
reply_edit_self_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF, reply_edit_self_perm = perm.PERM_EDIT_DISCUSSION_REPLY_SELF,
reply_delete_self_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF, reply_delete_self_perm = perm.PERM_DELETE_DISCUSSION_REPLY_SELF,
reactions = reactions reactions = reactions,
ddoc = ddoc
) }} ) }}
{{ paginator.render(page, pcount) }} {{ paginator.render(page, pcount) }}
{% if drcount == 0 %} {% if drcount == 0 %}
@@ -97,9 +108,7 @@
<img src="{{ avatarUrl(udoc.avatar, 80) }}" width="80" height="80" class="medium user-profile-avatar"> <img src="{{ avatarUrl(udoc.avatar, 80) }}" width="80" height="80" class="medium user-profile-avatar">
</div> </div>
<div class="media__body"> <div class="media__body">
<p> <p>{{ user.render_inline(udoc, avatar=false, modbadge=false) }}</p>
{{ user.render_inline(udoc, avatar=false, modbadge=false) }}
</p>
</div> </div>
</div> </div>
</div> </div>