Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { stringify as toCSV } from 'csv-stringify/sync';
import { escapeRegExp, pick } from 'lodash';
import moment from 'moment-timezone';
import { ObjectId } from 'mongodb';
import {
Counter, diffArray, randomstring, sortFiles, Time, yaml,
} from '@hydrooj/utils/lib/utils';
import { Counter, diffArray, randomstring, sortFiles, Time, yaml } from '@hydrooj/utils/lib/utils';
import { Context, Service } from '../context';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
Expand All @@ -26,6 +24,7 @@ import ScheduleModel from '../model/schedule';
import storage from '../model/storage';
import * as system from '../model/system';
import user from '../model/user';
import * as setting from '../model/setting';
import {
Handler, param, post, Type, Types,
} from '../service/server';
Expand Down Expand Up @@ -279,13 +278,44 @@ export class ContestEditHandler extends Handler {
let ts = Date.now();
ts = ts - (ts % (15 * Time.minute)) + 15 * Time.minute;
const beginAt = moment(this.tdoc?.beginAt || new Date(ts)).tz(this.user.timeZone);

// key, label, selected
let langList = [] as [string, string, boolean][];
if (!Array.isArray(setting.SETTINGS_BY_KEY.codeLang.range)) {
Object.keys(setting.SETTINGS_BY_KEY.codeLang.range).forEach((key) => {
langList.push([key, setting.SETTINGS_BY_KEY.codeLang.range[key], true]);
});
} else {
langList = setting.SETTINGS_BY_KEY.codeLang.range.map((el) => [...el, true]);
}
let limitLangListString = '';
let isLimitLang = false;
if (Array.isArray(this.tdoc?.limitLangList)) {
isLimitLang = true;
const allowedLangSet = new Set<string>();
this.tdoc?.limitLangList.forEach((k) => {
allowedLangSet.add(k);
});
for (let i = 0; i < langList.length; i++) {
if (!allowedLangSet.has(langList[i][0])) {
langList[i][2] = false;
}
}
limitLangListString = this.tdoc?.limitLangList.join(',');
} else {
limitLangListString = langList.map((el) => el[0]).join(',');
}

this.response.body = {
rules,
tdoc: this.tdoc,
duration: tid ? -beginAt.diff(this.tdoc.endAt, 'hour', true) : 2,
pids: tid ? this.tdoc.pids.join(',') : '',
beginAt,
page_name: tid ? 'contest_edit' : 'contest_create',
langList,
isLimitLang,
limitLangListString,
};
}

Expand All @@ -305,11 +335,14 @@ export class ContestEditHandler extends Handler {
@param('contestDuration', Types.Float, true)
@param('maintainer', Types.NumericArray, true)
@param('allowViewCode', Types.Boolean)
@param('limitLang', Types.Boolean)
@param('limitLangList', Types.CommaSeperatedArray, true)
async postUpdate(
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false,
_code = '', autoHide = false, assign: string[] = [], lock: number = null,
contestDuration: number = null, maintainer: number[] = [], allowViewCode = false,
limitLang = false, limitLangList: string[] = [],
) {
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
Expand Down Expand Up @@ -350,7 +383,7 @@ export class ContestEditHandler extends Handler {
});
}
await contest.edit(domainId, tid, {
assign, _code, autoHide, lockAt, maintainer, allowViewCode,
assign, _code, autoHide, lockAt, maintainer, allowViewCode, limitLangList: limitLang ? limitLangList : null,
});
this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid });
Expand Down
17 changes: 14 additions & 3 deletions packages/hydrooj/src/handler/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ProblemNotFoundError, RecordNotFoundError, SolutionNotFoundError, ValidationError,
} from '../error';
import {
ProblemConfig,
ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User,
} from '../interface';
import { PERM, PRIV, STATUS } from '../model/builtin';
Expand Down Expand Up @@ -463,10 +464,18 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {

async get() {
this.response.template = 'problem_submit.html';
const langRange = (typeof this.pdoc.config === 'object' && this.pdoc.config.langs)
? Object.fromEntries(this.pdoc.config.langs.map((i) => [i, setting.langs[i]?.display || i]))
: setting.SETTINGS_BY_KEY.codeLang.range;
// const langRange = (typeof this.pdoc.config === 'object' && this.pdoc.config.langs)
// ? Object.fromEntries(this.pdoc.config.langs.map((i) => [i, setting.langs[i]?.display || i]))
// : setting.SETTINGS_BY_KEY.codeLang.range;

// problem_submit_page.tsx use pdoc.config.langs to get submitable langs
let submitableLangs = (this.pdoc.config as ProblemConfig).langs;
if (Array.isArray(this.tdoc?.limitLangList)) {
submitableLangs = Array.from(Set.intersection(submitableLangs, this.tdoc.limitLangList));
}
const langRange = Object.fromEntries(submitableLangs.map((i) => [i, setting.langs[i]?.display || i]));
this.response.body.langRange = langRange;
this.UiContext.submitableLangs = submitableLangs;
this.response.body.page_name = this.tdoc
? this.tdoc.rule === 'homework'
? 'homework_detail_problem_submit'
Expand All @@ -486,6 +495,8 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
lang = '_';
} else if ((config.langs && !config.langs.includes(lang)) || !setting.langs[lang] || setting.langs[lang].disabled) {
throw new ProblemNotAllowLanguageError();
} else if (this?.tdoc && Array.isArray(this.tdoc.limitLangList) && !this.tdoc.limitLangList.includes(lang)) {
throw new ProblemNotAllowLanguageError();
}
if (pretest) {
if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest as string;
Expand Down
3 changes: 3 additions & 0 deletions packages/hydrooj/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ export interface Tdoc extends Document {
balloon?: Record<number, string | { color: string, name: string }>;
score?: Record<number, number>;

// 允许使用的语言, undefined|null 则允许全部
limitLangList?: string[] | null;

/**
* In hours
* 在比赛有效时间内选择特定的 X 小时参加比赛(从首次打开比赛算起)
Expand Down
3 changes: 3 additions & 0 deletions packages/ui-default/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ Ledo: 乐多
Legacy mode: 兼容模式
Light: 亮色
Limit user to finish contest within N hours: 限制用户在N小时内完成比赛
Limit contest submission language: 限制比赛可提交的语言
Limitations: 限制
Links: 链接
List View: 列表视图
Expand Down Expand Up @@ -821,6 +822,8 @@ Section: 章节
Security: 安全
Select a node to create discussion.: 选择一个节点来发表讨论。
Select a role: 选择一个角色
Select All: 全选
Select None: 全不选
Select Category: 选择标签
Select User: 选择用户
Selected categories: 已选标签
Expand Down
29 changes: 29 additions & 0 deletions packages/ui-default/pages/contest_edit.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create',
$(`[data-perm="${type}"] input`).removeAttr('disabled');
$(`[data-perm="${type}"]`).show();
}).trigger('change');
$('[name=limitLang]').removeAttr('disabled').on('change', () => {
const checked = $('[name=limitLang]').is(':checked');
if (checked) {
$('#limitLangListBox').show();
$('#limitLang_btnBox').show();
} else {
$('#limitLangListBox').hide();
$('#limitLang_btnBox').hide();
}
}).trigger('change');
$('.limitLangListItem').each(function () {
$(this).removeAttr('disabled');
}).on('change', () => {
const selectedLang = [];
$('.limitLangListItem:checked').each(function () {
selectedLang.push($(this).val());
});
$('[name=limitLangList]').val(selectedLang.join(','));
}).trigger('change');
$('#limitLangBtn_selectAll').on('click', () => {
$('.limitLangListItem').each(function () {
$(this).attr('checked', 'true');
});
});
$('#limitLangBtn_selectNone').on('click', () => {
$('.limitLangListItem').each(function () {
$(this).removeAttr('checked');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix checkbox manipulation to use proper jQuery methods.

The current approach to setting checkbox states is incorrect and may not work consistently across browsers.

   $('#limitLangBtn_selectAll').on('click', () => {
     $('.limitLangListItem').each(function () {
-      $(this).attr('checked', 'true');
+      $(this).prop('checked', true);
     });
+    $('.limitLangListItem').trigger('change');
   });
   $('#limitLangBtn_selectNone').on('click', () => {
     $('.limitLangListItem').each(function () {
-      $(this).removeAttr('checked');
+      $(this).prop('checked', false);
     });
+    $('.limitLangListItem').trigger('change');
   });
🤖 Prompt for AI Agents
In packages/ui-default/pages/contest_edit.page.ts around lines 52 to 61, the
code incorrectly uses attr and removeAttr to set checkbox states, which is
unreliable. Replace attr('checked', 'true') with the jQuery prop method
prop('checked', true) to check the boxes, and replace removeAttr('checked') with
prop('checked', false) to uncheck them, ensuring consistent behavior across
browsers.

if (pagename.endsWith('edit')) {
let confirmed = false;
$(document).on('click', '[value="delete"]', (ev) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-default/pages/problem_submit.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const page = new NamedPage(['problem_submit', 'contest_detail_problem_submit', '
$('[name="lang"]').val('_');
return;
}
const availableLangs = getAvailableLangs(config.langs);
const availableLangs = getAvailableLangs(UiContext.submitableLangs);
const mainLangs = {};
const preferences = [UserContext.codeLang || ''];
for (const key in availableLangs) {
Expand Down
38 changes: 38 additions & 0 deletions packages/ui-default/templates/contest_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,44 @@ <h2 class="section__title">{{ _('Basic Info') }}</h2>
markdown:true
}) }}
</div>
<div class="section__body">
<div class="row">
{{ form.form_checkbox({
columns:9,
label:'Limit language',
name:'limitLang',
placeholder:_('Limit contest submission language'),
value:isLimitLang,
row:false,
disabled:true
}) }}
<div class="medium-3 columns" id="limitLang_btnBox" {% if not isLimitLang %}style="display:none;"{% endif %}>
<button type="button" class=" button" id="limitLangBtn_selectAll">
{{ _('Select All') }}
</button>
<button type="button" class=" button" id="limitLangBtn_selectNone">
{{ _('Select None') }}
</button>
</div>
</div>
<div id="limitLangListBox" {% if not isLimitLang %}style="display:none;"{% endif %}>
<div>
<p>对于有子类的语言,勾选时请详细至子分类!</p><br />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace hard-coded Chinese text with localized string.

The Chinese text should be replaced with a localized string to support internationalization.

-              <p>对于有子类的语言,勾选时请详细至子分类!</p><br />
+              <p>{{ _('For languages with subcategories, please select specific subcategories when checking!') }}</p><br />

You'll also need to add the corresponding translation to the localization files.

🤖 Prompt for AI Agents
In packages/ui-default/templates/contest_edit.html at line 104, replace the
hard-coded Chinese text inside the paragraph tag with a localized string
reference using the project's i18n method or syntax. Then, add the corresponding
translation for this string in the appropriate localization files to support
internationalization.

</div>
<div class="row">
{%- for k, v, enabled in langList -%}
<div class="medium-3 columns">
<div name="form_item_limitLangList" class="checkbox-container">
<label class="checkbox">
<input type="checkbox" {% if enabled %}checked{% endif %} class="checkbox limitLangListItem" value="{{k}}" disabled>{{v}}
</label>
</div>
</div>
{%- endfor -%}
</div>
</div>
<input type="hidden" name="limitLangList" value="{{limitLangListString}}">
</div>
<div style="padding-top: 0" class="section__header">
<h2 class="section__title">{{ _('Permission Control') }}</h2>
</div>
Expand Down