mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: allow lock topic
This commit is contained in:
1 parent
931e96a6b4
commit
c305e57860
11 files changed
+49
-18
No files matched your search
@@ -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,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",
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
Reference in new issue
Block a user