Skip to content

Commit f95d7ec

Browse files
authored
Allow Per-task allowed languages (#1486)
1 parent 6ea6839 commit f95d7ec

File tree

11 files changed

+106
-13
lines changed

11 files changed

+106
-13
lines changed

cms/db/task.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
77
# Copyright © 2012-2018 Luca Wehrstedt <[email protected]>
88
# Copyright © 2013 Bernard Blackham <[email protected]>
9+
# Copyright © 2025 Pasit Sangprachathanarak <[email protected]>
910
#
1011
# This program is free software: you can redistribute it and/or modify
1112
# it under the terms of the GNU Affero General Public License as
@@ -116,6 +117,12 @@ class Task(Base):
116117
nullable=False,
117118
default=[])
118119

120+
# The list of names of programming languages allowed for this task.
121+
# If null, all contest languages are allowed.
122+
allowed_languages: list[str] | None = Column(
123+
ARRAY(String), nullable=True, default=None
124+
)
125+
119126
# The parameters that control task-tokens follow. Note that their
120127
# effect during the contest depends on the interaction with the
121128
# parameters that control contest-tokens, defined on the Contest.
@@ -272,6 +279,21 @@ class Task(Base):
272279
passive_deletes=True,
273280
back_populates="task")
274281

282+
def get_allowed_languages(self) -> list[str] | None:
283+
"""Get the list of allowed languages for this task.
284+
285+
If the task has specific allowed languages configured, return those.
286+
Otherwise, return the contest's allowed languages.
287+
288+
return: list of allowed language names, or None if no contest is set
289+
"""
290+
# If task has specific language restrictions, use those
291+
if self.allowed_languages is not None:
292+
return self.allowed_languages
293+
294+
# Otherwise, use contest language restrictions
295+
return self.contest.languages if self.contest else None
296+
275297

276298
class Statement(Base):
277299
"""Class to store a translation of the task statement.

cms/server/admin/handlers/task.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Copyright © 2014 Artem Iglikov <[email protected]>
99
# Copyright © 2014 Fabian Gundlach <[email protected]>
1010
# Copyright © 2016 Myungwoo Chun <[email protected]>
11+
# Copyright © 2025 Pasit Sangprachathanarak <[email protected]>
1112
#
1213
# This program is free software: you can redistribute it and/or modify
1314
# it under the terms of the GNU Affero General Public License as
@@ -147,6 +148,14 @@ def post(self, task_id):
147148
self.get_submission_format(attrs)
148149
self.get_string(attrs, "feedback_level")
149150

151+
# Process allowed languages
152+
selected_languages = self.get_arguments("allowed_languages")
153+
if not selected_languages:
154+
# No languages selected means allow all contest languages (NULL)
155+
attrs["allowed_languages"] = None
156+
else:
157+
attrs["allowed_languages"] = selected_languages
158+
150159
self.get_string(attrs, "token_mode")
151160
self.get_int(attrs, "token_max_number")
152161
self.get_timedelta_sec(attrs, "token_min_interval")

cms/server/admin/static/aws_style.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,15 @@ table td.wrapping-options label {
382382
margin-right: 15px;
383383
}
384384

385+
.language-item label {
386+
display: block;
387+
white-space: nowrap;
388+
cursor: pointer;
389+
}
390+
391+
.language-item input[type="checkbox"] {
392+
margin-right: 6px;
393+
}
385394
table td.partial::after {
386395
content: "*";
387396
}

cms/server/admin/templates/task.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,19 @@ <h2 id="title_task_configuration" class="toggling_on">Task configuration</h2>
130130
<input type="text" name="submission_format" value="{{ task.submission_format|join(", ") }}"/>
131131
</td>
132132
</tr>
133+
<tr>
134+
<td>
135+
<span class="info" title="Programming languages that contestants can use to solve this task.
136+
If none are selected, all contest languages are allowed.
137+
Otherwise, only the selected languages (which must be a subset of contest languages) are allowed."></span>
138+
Allowed programming languages
139+
</td>
140+
<td class="wrapping-options">
141+
{% for lang in LANGUAGES %}
142+
<label><input type="checkbox" name="allowed_languages" value="{{ lang.name }}" {{ "checked" if task.allowed_languages is none or lang.name in (task.allowed_languages or []) else "" }}>{{ lang.name }}</label>
143+
{% endfor %}
144+
</td>
145+
</tr>
133146
<tr>
134147
<td>
135148
<span class="info" title="With 'restricted' contestants only see limited technical information about each testcase; with 'full' they see more details, but malicious contestants might use this data to get some information about the test data."></span>

cms/server/contest/static/cws_utils.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,17 +338,19 @@ CMS.CWSUtils.prototype.switch_lang = function() {
338338
location.reload();
339339
};
340340

341-
CMS.CWSUtils.filter_languages = function(options, inputs) {
341+
CMS.CWSUtils.filter_languages = function (options, inputs, languages) {
342+
languages = languages || LANGUAGES;
343+
342344
var exts = [];
343345
for (var i = 0; i < inputs.length; i++) {
344346
exts.push('.' + inputs[i].value.match(/[^.]*$/)[0]);
345347
}
346348
// Find all languages that should be enabled.
347349
var enabled = {};
348350
var anyEnabled = false;
349-
for (var lang in LANGUAGES) {
351+
for (var lang in languages) {
350352
for (i = 0; i < exts.length; i++) {
351-
if (LANGUAGES[lang][exts[i]]) {
353+
if (languages[lang][exts[i]]) {
352354
enabled[lang] = true;
353355
anyEnabled = true;
354356
break;

cms/server/contest/submission/workflow.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# Copyright © 2015-2016 William Di Luigi <[email protected]>
1212
# Copyright © 2016 Myungwoo Chun <[email protected]>
1313
# Copyright © 2016 Amir Keivan Mohtashami <[email protected]>
14+
# Copyright © 2025 Pasit Sangprachathanarak <[email protected]>
1415
#
1516
# This program is free software: you can redistribute it and/or modify
1617
# it under the terms of the GNU Affero General Public License as
@@ -195,8 +196,11 @@ def accept_submission(
195196

196197
try:
197198
files, language = match_files_and_language(
198-
received_files, language_name, required_codenames,
199-
contest.languages)
199+
received_files,
200+
language_name,
201+
required_codenames,
202+
task.get_allowed_languages(),
203+
)
200204
except InvalidFilesOrLanguage as err:
201205
logger.info(f'Submission rejected: {err}')
202206
raise UnacceptableSubmission(
@@ -398,8 +402,11 @@ def accept_user_test(
398402

399403
try:
400404
files, language = match_files_and_language(
401-
received_files, language_name, required_codenames,
402-
contest.languages)
405+
received_files,
406+
language_name,
407+
required_codenames,
408+
task.get_allowed_languages(),
409+
)
403410
except InvalidFilesOrLanguage as err:
404411
logger.info(f'Test rejected: {err}')
405412
raise UnacceptableUserTest(

cms/server/contest/templates/overview.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,9 @@ <h2>{% trans %}Task overview{% endtrans %}</h2>
205205
</tr>
206206
</thead>
207207
<tbody>
208-
{% set extensions = "[%s]"|format(contest.languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %}
209208
{% for t_iter in contest.tasks %}
209+
{% set task_allowed_languages = t_iter.get_allowed_languages() %}
210+
{% set extensions = "[%s]"|format(task_allowed_languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %}
210211
<tr>
211212
<th>{{ t_iter.name }}</th>
212213
<td>{{ t_iter.title }}</td>

cms/server/contest/templates/task_description.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ <h2>{% trans %}Some details{% endtrans %}</h2>
118118
{% endif %}
119119
{% set compilation_commands = task_type.get_compilation_commands(task.submission_format) %}
120120
{% if compilation_commands is not none %}
121-
{% set compilation_commands = compilation_commands|dictselect("in", contest.languages, by="key") %}
121+
{% set allowed_languages = task.get_allowed_languages() %}
122+
{% set compilation_commands = compilation_commands|dictselect("in", allowed_languages, by="key") %}
122123
<tr>
123124
<th rowspan="{{ compilation_commands|length }}">{% trans %}Compilation commands{% endtrans %}</th>
124125
{% for l, c in compilation_commands|dictsort(by="key") %}

cms/server/contest/templates/task_submissions.html

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66

77
{% set score_type = get_score_type(dataset=task.active_dataset) %}
88

9+
{% block js_init %}
10+
// Define TASK_LANGUAGES for task-specific language filtering
11+
var TASK_LANGUAGES = {
12+
{% for lang in task.get_allowed_languages() or [] %}
13+
'{{ lang }}': {
14+
{% for extension in (lang|to_language).source_extensions %}
15+
'{{ extension }}': true,
16+
{% endfor %}
17+
},
18+
{% endfor %}
19+
};
20+
{% endblock js_init %}
21+
922
{# Whether tokens are allowed on this contest. #}
1023
{% set can_use_tokens_in_contest =
1124
tokens_contest != TOKEN_MODE_DISABLED
@@ -254,15 +267,15 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a solution{% endtrans %}</h2>
254267
<input type="file" class="input-xlarge"
255268
id="input{{ loop.index0 }}" name="{{ filename }}"
256269
onchange="CMS.CWSUtils.filter_languages($(this).parents('form').find('select[name=language] option'),
257-
$(this).parents('form').find('input[type=file]'))"/>
270+
$(this).parents('form').find('input[type=file]'), TASK_LANGUAGES)"/>
258271
</div>
259272
</div>
260273
{% endfor %}
261274
{% if task.submission_format|any("endswith", ".%l") %}
262275
<div class="control-group">
263276
<div class="controls">
264277
<select name="language">
265-
{% for lang in contest.languages %}
278+
{% for lang in task.get_allowed_languages() or [] %}
266279
<option value="{{ lang }}">{{ lang }}</option>
267280
{% endfor %}
268281
</select>

cms/server/contest/templates/test_interface.html

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
{% set page = "test_interface" %}
44

5+
{% block js_init %}
6+
// Define TASK_LANGUAGES for task-specific language filtering
7+
var TASK_LANGUAGES = {
8+
{% for lang in task.get_allowed_languages() or [] %}
9+
'{{ lang }}': {
10+
{% for extension in (lang|to_language).source_extensions %}
11+
'{{ extension }}': true,
12+
{% endfor %}
13+
},
14+
{% endfor %}
15+
};
16+
{% endblock js_init %}
17+
518
{% block additional_js %}
619
$(document).on("click", ".user_test_list tbody tr td.status .details", function (event) {
720
var $this = $(this);
@@ -105,7 +118,7 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a test{% endtrans %}</h2>
105118
<input type="file" class="input-xlarge"
106119
id="input{{ loop.index0 }}" name="{{ filename }}"
107120
onchange="CMS.CWSUtils.filter_languages($(this).parents('form').find('select[name=language] option'),
108-
$(this).parents('form').find('input[type=file]').not('#input_file'))"/>
121+
$(this).parents('form').find('input[type=file]').not('#input_file'), TASK_LANGUAGES)"/>
109122
</div>
110123
</div>
111124
{% endfor %}
@@ -118,7 +131,7 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a test{% endtrans %}</h2>
118131
<div class="control-group">
119132
<div class="controls">
120133
<select name="language">
121-
{% for lang in contest.languages %}
134+
{% for lang in task.get_allowed_languages() or [] %}
122135
<option value="{{ lang }}">{{ lang }}</option>
123136
{% endfor %}
124137
</select>

0 commit comments

Comments
 (0)