mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: allow user to manually end contest early (#1147)
This commit is contained in:
1 parent
d8a5716a22
commit
125c4b2388
9 files changed
+99
-40
No files matched your search
@@ -93,14 +93,17 @@ export class ContestDetailBaseHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.tdoc.duration && this.tsdoc?.startAt) {
|
if (this.tdoc.duration && this.tsdoc?.startAt) {
|
||||||
const endAt = moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate();
|
this.tsdoc.endAt = moment.min([
|
||||||
this.tsdoc.endAt = endAt < this.tdoc.endAt ? endAt : this.tdoc.endAt;
|
moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours'),
|
||||||
|
moment(this.tdoc.endAt),
|
||||||
|
...(this.tsdoc.endAt ? [moment(this.tsdoc.endAt)] : []),
|
||||||
|
]).toDate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tsdocAsPublic() {
|
tsdocAsPublic() {
|
||||||
if (!this.tsdoc) return null;
|
if (!this.tsdoc) return null;
|
||||||
return pick(this.tsdoc, ['attend', 'subscribe', 'startAt', ...(this.tdoc.duration ? ['endAt'] : [])]);
|
return pick(this.tsdoc, ['attend', 'subscribe', 'startAt', ...(this.tdoc.duration || this.tsdoc.endAt ? ['endAt'] : [])]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@param('tid', Types.ObjectId, true)
|
@param('tid', Types.ObjectId, true)
|
||||||
@@ -173,7 +176,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
|||||||
@param('code', Types.String, true)
|
@param('code', Types.String, true)
|
||||||
async postAttend(domainId: string, tid: ObjectId, code = '') {
|
async postAttend(domainId: string, tid: ObjectId, code = '') {
|
||||||
this.checkPerm(PERM.PERM_ATTEND_CONTEST);
|
this.checkPerm(PERM.PERM_ATTEND_CONTEST);
|
||||||
if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(tid);
|
if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
|
||||||
if (this.tdoc._code && code !== this.tdoc._code) throw new InvalidTokenError('Contest Invitation', code);
|
if (this.tdoc._code && code !== this.tdoc._code) throw new InvalidTokenError('Contest Invitation', code);
|
||||||
await contest.attend(domainId, tid, this.user._id, { subscribe: 1 });
|
await contest.attend(domainId, tid, this.user._id, { subscribe: 1 });
|
||||||
this.back();
|
this.back();
|
||||||
@@ -186,6 +189,15 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
|
|||||||
await contest.setStatus(domainId, tid, this.user._id, { subscribe: subscribe ? 1 : 0 });
|
await contest.setStatus(domainId, tid, this.user._id, { subscribe: subscribe ? 1 : 0 });
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@param('tid', Types.ObjectId)
|
||||||
|
async postEarlyEnd(domainId: string, tid: ObjectId) {
|
||||||
|
if (this.tdoc.rule === 'homework') throw new ContestNotFoundError(domainId, tid);
|
||||||
|
if (!this.tsdoc?.attend) throw new ContestNotAttendedError(domainId, tid);
|
||||||
|
if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(domainId, tid);
|
||||||
|
await contest.setStatus(domainId, tid, this.user._id, { endAt: new Date() });
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContestPrintHandler extends ContestDetailBaseHandler {
|
export class ContestPrintHandler extends ContestDetailBaseHandler {
|
||||||
@@ -698,10 +710,16 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
|
|||||||
@param('tid', Types.ObjectId)
|
@param('tid', Types.ObjectId)
|
||||||
async get(domainId: string, tid: ObjectId) {
|
async get(domainId: string, tid: ObjectId) {
|
||||||
const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).project({
|
const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).project({
|
||||||
uid: 1, attend: 1, startAt: 1, unrank: 1,
|
uid: 1, attend: 1, startAt: 1, unrank: 1, endAt: 1,
|
||||||
}).toArray();
|
}).toArray();
|
||||||
for (const tsdoc of tsdocs) {
|
for (const tsdoc of tsdocs) {
|
||||||
tsdoc.endAt = (this.tdoc.duration && tsdoc.startAt) ? moment(tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate() : null;
|
if (this.tdoc.duration && tsdoc.startAt) {
|
||||||
|
tsdoc.endAt = moment.min([
|
||||||
|
moment(tsdoc.startAt).add(this.tdoc.duration, 'hours'),
|
||||||
|
moment(this.tdoc.endAt),
|
||||||
|
...(tsdoc.endAt ? [moment(tsdoc.endAt)] : []),
|
||||||
|
]).toDate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const udict = await user.getListForRender(
|
const udict = await user.getListForRender(
|
||||||
domainId, [this.tdoc.owner, ...tsdocs.map((i) => i.uid)],
|
domainId, [this.tdoc.owner, ...tsdocs.map((i) => i.uid)],
|
||||||
@@ -728,6 +746,20 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
|
|||||||
await contest.setStatus(domainId, tid, uid, { unrank: !tsdoc.unrank });
|
await contest.setStatus(domainId, tid, uid, { unrank: !tsdoc.unrank });
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@param('tid', Types.ObjectId)
|
||||||
|
@param('uid', Types.PositiveInt)
|
||||||
|
async postResume(domainId: string, tid: ObjectId, uid: number) {
|
||||||
|
const tsdoc = await contest.getStatus(domainId, tid, uid);
|
||||||
|
if (!tsdoc?.attend) throw new ContestNotAttendedError(uid);
|
||||||
|
if (this.tdoc.endAt <= new Date()) throw new ContestNotLiveError(domainId, tid);
|
||||||
|
if (this.tdoc.duration && tsdoc.startAt) {
|
||||||
|
const durationEnd = moment(tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate();
|
||||||
|
if (durationEnd <= new Date()) throw new ContestNotLiveError(domainId, tid);
|
||||||
|
}
|
||||||
|
await contest.setStatus(domainId, tid, uid, null, { endAt: '' });
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContestBalloonHandler extends ContestManagementBaseHandler {
|
export class ContestBalloonHandler extends ContestManagementBaseHandler {
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ export function isNotStarted(tdoc: Tdoc) {
|
|||||||
|
|
||||||
export function isOngoing(tdoc: Tdoc, tsdoc?: any) {
|
export function isOngoing(tdoc: Tdoc, tsdoc?: any) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
if (tsdoc?.endAt && tsdoc.endAt <= now) return false;
|
||||||
if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return false;
|
if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return false;
|
||||||
return (tdoc.beginAt <= now && now < tdoc.endAt);
|
return (tdoc.beginAt <= now && now < tdoc.endAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDone(tdoc: Tdoc, tsdoc?: any) {
|
export function isDone(tdoc: Tdoc, tsdoc?: any) {
|
||||||
if (tdoc.endAt <= new Date()) return true;
|
if (tdoc.endAt <= new Date()) return true;
|
||||||
|
if (tsdoc?.endAt && tsdoc.endAt <= new Date()) return true;
|
||||||
if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return true;
|
if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -940,8 +942,8 @@ export function getMultiStatus(domainId: string, query: any) {
|
|||||||
return document.getMultiStatus(domainId, document.TYPE_CONTEST, query);
|
return document.getMultiStatus(domainId, document.TYPE_CONTEST, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setStatus(domainId: string, tid: ObjectId, uid: number, $set: any) {
|
export function setStatus(domainId: string, tid: ObjectId, uid: number, $set?: any, $unset?: any) {
|
||||||
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set);
|
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set, $unset);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function count(domainId: string, query: any) {
|
export function count(domainId: string, query: any) {
|
||||||
|
|||||||
@@ -295,11 +295,16 @@ export function getMultiStatusWithoutDomain<K extends keyof DocStatusType>(
|
|||||||
|
|
||||||
export async function setStatus<K extends keyof DocStatusType>(
|
export async function setStatus<K extends keyof DocStatusType>(
|
||||||
domainId: string, docType: K, docId: DocStatusType[K]['docId'], uid: number,
|
domainId: string, docType: K, docId: DocStatusType[K]['docId'], uid: number,
|
||||||
args: UpdateFilter<DocStatusType[K]>['$set'], returnDocument: 'before' | 'after' = 'after',
|
$set: UpdateFilter<DocStatusType[K]>['$set'] | null,
|
||||||
|
$unset?: UpdateFilter<DocStatusType[K]>['$unset'] | null,
|
||||||
|
returnDocument: 'before' | 'after' = 'after',
|
||||||
): Promise<DocStatusType[K]> {
|
): Promise<DocStatusType[K]> {
|
||||||
|
const op: UpdateFilter<DocStatusType[K]> = {};
|
||||||
|
if ($set && Object.keys($set).length) op.$set = $set;
|
||||||
|
if ($unset && Object.keys($unset).length) op.$unset = $unset;
|
||||||
return await collStatus.findOneAndUpdate(
|
return await collStatus.findOneAndUpdate(
|
||||||
{ domainId, docType, docId, uid },
|
{ domainId, docType, docId, uid },
|
||||||
{ $set: args },
|
op,
|
||||||
{
|
{
|
||||||
upsert: true,
|
upsert: true,
|
||||||
returnDocument,
|
returnDocument,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class SolutionModel {
|
|||||||
if (!doc) throw new SolutionNotFoundError(domainId, psid);
|
if (!doc) throw new SolutionNotFoundError(domainId, psid);
|
||||||
const before = await document.setStatus(
|
const before = await document.setStatus(
|
||||||
domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid,
|
domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid,
|
||||||
{ vote: value }, 'before',
|
{ vote: value }, null, 'before',
|
||||||
);
|
);
|
||||||
let inc = value;
|
let inc = value;
|
||||||
if (before?.vote) inc -= before.vote;
|
if (before?.vote) inc -= before.vote;
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ Confirm deleting the selected roles?: 您确定删除所选角色吗?
|
|||||||
Confirm deleting this comment? Its replies will be deleted as well.: 确认删除这个评论吗?回复会被同时删除。
|
Confirm deleting this comment? Its replies will be deleted as well.: 确认删除这个评论吗?回复会被同时删除。
|
||||||
Confirm deleting this domain? This action cannot be undone.: '确认删除此域?此操作无法撤销。'
|
Confirm deleting this domain? This action cannot be undone.: '确认删除此域?此操作无法撤销。'
|
||||||
Confirm deleting this reply?: 确认删除这个回复吗?
|
Confirm deleting this reply?: 确认删除这个回复吗?
|
||||||
|
Confirm end contest early? You will not be able to submit after this.: 确认提前结束比赛吗?之后您将无法提交代码。
|
||||||
Confirm rejudge this problem?: 确定要重测这道题吗?
|
Confirm rejudge this problem?: 确定要重测这道题吗?
|
||||||
Confirm removing the selected users?: 您确定将所选用户移除吗?
|
Confirm removing the selected users?: 您确定将所选用户移除吗?
|
||||||
Confirm to delete the file?: 确认删除此文件吗?
|
Confirm to delete the file?: 确认删除此文件吗?
|
||||||
@@ -209,6 +210,7 @@ Content copied to clipboard!: 内容已复制到剪贴板!
|
|||||||
content: 内容
|
content: 内容
|
||||||
Content: 内容
|
Content: 内容
|
||||||
Contest Maintainer: 比赛管理员
|
Contest Maintainer: 比赛管理员
|
||||||
|
Contest resumed.: 比赛已恢复。
|
||||||
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
||||||
Contest Scoreboard: 比赛成绩表
|
Contest Scoreboard: 比赛成绩表
|
||||||
Contest Settings: 比赛设置
|
Contest Settings: 比赛设置
|
||||||
@@ -376,6 +378,7 @@ Email Visibility: Email 可见性
|
|||||||
Email: 电子邮件
|
Email: 电子邮件
|
||||||
Enabled: 开启
|
Enabled: 开启
|
||||||
End at: 结束于
|
End at: 结束于
|
||||||
|
End Contest Early: 提前结束比赛
|
||||||
End Date: 结束日期
|
End Date: 结束日期
|
||||||
End Time: 结束时间
|
End Time: 结束时间
|
||||||
Enroll Training: 参加训练
|
Enroll Training: 参加训练
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ contestTimer.appendTo(document.body);
|
|||||||
|
|
||||||
export default new NamedPage(['contest_detail', 'contest_problemlist', 'contest_detail_problem', 'contest_scoreboard'], () => {
|
export default new NamedPage(['contest_detail', 'contest_problemlist', 'contest_detail_problem', 'contest_scoreboard'], () => {
|
||||||
const beginAt = new Date((UiContext.tdoc.duration && UiContext.tsdoc?.startAt) || UiContext.tdoc.beginAt).getTime();
|
const beginAt = new Date((UiContext.tdoc.duration && UiContext.tsdoc?.startAt) || UiContext.tdoc.beginAt).getTime();
|
||||||
const endAt = new Date((UiContext.tdoc.duration && UiContext.tsdoc?.endAt) || UiContext.tdoc.endAt).getTime();
|
const endAt = new Date(UiContext.tsdoc?.endAt || UiContext.tdoc.endAt).getTime();
|
||||||
NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 });
|
NProgress.configure({ trickle: false, showSpinner: false, minimum: 0 });
|
||||||
function updateProgress() {
|
function updateProgress() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|||||||
@@ -52,46 +52,50 @@ const page = new NamedPage('contest_user', () => {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handlePostRequest(params, successMessage) {
|
||||||
|
try {
|
||||||
|
const res = await request.post('', params);
|
||||||
|
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
||||||
|
else {
|
||||||
|
Notification.success(successMessage);
|
||||||
|
pjax.request({ push: false });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notification.error([error.message, ...error.params].join(' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleClickAddUser() {
|
async function handleClickAddUser() {
|
||||||
const action = await addUserDialog.clear().open();
|
const action = await addUserDialog.clear().open();
|
||||||
if (action !== 'ok') return;
|
if (action !== 'ok') return;
|
||||||
const unrank = addUserDialog.$dom.find('[name="unrank"]').prop('checked');
|
const unrank = addUserDialog.$dom.find('[name="unrank"]').prop('checked');
|
||||||
const uids = userSelect.value();
|
const uids = userSelect.value();
|
||||||
try {
|
await handlePostRequest({
|
||||||
const res = await request.post('', {
|
operation: 'add_user',
|
||||||
operation: 'add_user',
|
uids: uids.join(','),
|
||||||
uids: uids.join(','),
|
unrank,
|
||||||
unrank,
|
}, i18n('User added.'));
|
||||||
});
|
|
||||||
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
|
||||||
else {
|
|
||||||
Notification.success(i18n('User added.'));
|
|
||||||
pjax.request({ push: false });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Notification.error([error.message, ...error.params].join(' '));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEditRank(ev) {
|
async function handleEditRank(ev) {
|
||||||
const uid = $(ev.target).data('uid');
|
const uid = $(ev.target).data('uid');
|
||||||
try {
|
await handlePostRequest({
|
||||||
const res = await request.post('', {
|
operation: 'rank',
|
||||||
operation: 'rank',
|
uid,
|
||||||
uid,
|
}, i18n('Ranking status updated.'));
|
||||||
});
|
}
|
||||||
if (res.url && res.url !== window.location.href) window.location.href = res.url;
|
|
||||||
else {
|
async function handleResume(ev) {
|
||||||
Notification.success(i18n('Ranking status updated.'));
|
const uid = $(ev.target).data('uid');
|
||||||
pjax.request({ push: false });
|
await handlePostRequest({
|
||||||
}
|
operation: 'resume',
|
||||||
} catch (error) {
|
uid,
|
||||||
Notification.error([error.message, ...error.params].join(' '));
|
}, i18n('Contest resumed.'));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('[name="add_user"]').on('click', () => handleClickAddUser());
|
$('[name="add_user"]').on('click', () => handleClickAddUser());
|
||||||
$(document).on('click', '[name="edit_rank"]', handleEditRank);
|
$(document).on('click', '[name="edit_rank"]', handleEditRank);
|
||||||
|
$(document).on('click', '[name="resume_contest"]', handleResume);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default page;
|
export default page;
|
||||||
@@ -68,6 +68,16 @@
|
|||||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_problemlist', tid=tdoc.docId) }}">
|
<li class="menu__item"><a class="menu__link" href="{{ url('contest_problemlist', tid=tdoc.docId) }}">
|
||||||
<span class="icon icon-unordered_list"></span> {{ _('Problem List') }}
|
<span class="icon icon-unordered_list"></span> {{ _('Problem List') }}
|
||||||
</a></li>
|
</a></li>
|
||||||
|
{% if page_name == 'contest_detail' and tsdoc.attend and model.contest.isOngoing(tdoc, tsdoc) and tdoc.rule !== 'homework' %}
|
||||||
|
<li class="menu__item">
|
||||||
|
<form action="{{ url('contest_detail', tid=tdoc.docId) }}" method="POST" onsubmit='return confirm({{ _("Confirm end contest early? You will not be able to submit after this.")|json|safe }});'>
|
||||||
|
<input type="hidden" name="operation" value="early_end">
|
||||||
|
<button class="menu__link" type="submit">
|
||||||
|
<span class="icon icon-delete"></span> {{ _('End Contest Early') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}{# not attend and not done #}
|
{% endif %}{# not attend and not done #}
|
||||||
{% if model.contest.canShowScoreboard.call(handler, tdoc, False) %}
|
{% if model.contest.canShowScoreboard.call(handler, tdoc, False) %}
|
||||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_scoreboard', tid=tdoc.docId) }}">
|
<li class="menu__item"><a class="menu__link" href="{{ url('contest_scoreboard', tid=tdoc.docId) }}">
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
<td class="col--rank">{{ _('UnRank') if tsdoc.unrank else _('Rank') }}</td>
|
<td class="col--rank">{{ _('UnRank') if tsdoc.unrank else _('Rank') }}</td>
|
||||||
<td class="col--actions">
|
<td class="col--actions">
|
||||||
<a name="edit_rank" data-uid="{{ tsdoc.uid }}">{{ _('Rank') if tsdoc.unrank else _('UnRank') }}</a>
|
<a name="edit_rank" data-uid="{{ tsdoc.uid }}">{{ _('Rank') if tsdoc.unrank else _('UnRank') }}</a>
|
||||||
|
{% if tsdoc.endAt and tsdoc.endAt < tdoc.endAt and tdoc.endAt.getTime() > Date.now() and (not tdoc.duration or not tsdoc.startAt or tsdoc.startAt.getTime() + tdoc.duration * 3600000 > Date.now()) %}
|
||||||
|
| <a name="resume_contest" data-uid="{{ tsdoc.uid }}">{{ _('Resume') }}</a>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</tbody>
|
</tbody>
|
||||||
Reference in new issue
Block a user