Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 22 additions & 0 deletions cms/db/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# Copyright © 2010-2012 Matteo Boscariol <boscarim@hotmail.com>
# Copyright © 2012-2018 Luca Wehrstedt <luca.wehrstedt@gmail.com>
# Copyright © 2013 Bernard Blackham <bernard@largestprime.net>
# Copyright © 2025 Pasit Sangprachathanarak <ouipingpasit@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -116,6 +117,12 @@ class Task(Base):
nullable=False,
default=[])

# The list of names of programming languages allowed for this task.
# If null, all contest languages are allowed.
allowed_languages: list[str] | None = Column(
ARRAY(String), nullable=True, default=None
)

# The parameters that control task-tokens follow. Note that their
# effect during the contest depends on the interaction with the
# parameters that control contest-tokens, defined on the Contest.
Expand Down Expand Up @@ -272,6 +279,21 @@ class Task(Base):
passive_deletes=True,
back_populates="task")

def get_allowed_languages(self) -> list[str] | None:
"""Get the list of allowed languages for this task.

If the task has specific allowed languages configured, return those.
Otherwise, return the contest's allowed languages.

return: list of allowed language names, or None if no contest is set
"""
# If task has specific language restrictions, use those
if self.allowed_languages is not None:
return self.allowed_languages

# Otherwise, use contest language restrictions
return self.contest.languages if self.contest else None


class Statement(Base):
"""Class to store a translation of the task statement.
Expand Down
9 changes: 9 additions & 0 deletions cms/server/admin/handlers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Copyright © 2014 Artem Iglikov <artem.iglikov@gmail.com>
# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com>
# Copyright © 2016 Myungwoo Chun <mc.tamaki@gmail.com>
# Copyright © 2025 Pasit Sangprachathanarak <ouipingpasit@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -147,6 +148,14 @@ def post(self, task_id):
self.get_submission_format(attrs)
self.get_string(attrs, "feedback_level")

# Process allowed languages
selected_languages = self.get_arguments("allowed_languages")
if not selected_languages:
# No languages selected means allow all contest languages (NULL)
attrs["allowed_languages"] = None
else:
attrs["allowed_languages"] = selected_languages

self.get_string(attrs, "token_mode")
self.get_int(attrs, "token_max_number")
self.get_timedelta_sec(attrs, "token_min_interval")
Expand Down
9 changes: 9 additions & 0 deletions cms/server/admin/static/aws_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@ table td.wrapping-options label {
margin-right: 15px;
}

.language-item label {
display: block;
white-space: nowrap;
cursor: pointer;
}

.language-item input[type="checkbox"] {
margin-right: 6px;
}
table td.partial::after {
content: "*";
}
Expand Down
13 changes: 13 additions & 0 deletions cms/server/admin/templates/task.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ <h2 id="title_task_configuration" class="toggling_on">Task configuration</h2>
<input type="text" name="submission_format" value="{{ task.submission_format|join(", ") }}"/>
</td>
</tr>
<tr>
<td>
<span class="info" title="Programming languages that contestants can use to solve this task.
If none are selected, all contest languages are allowed.
Otherwise, only the selected languages (which must be a subset of contest languages) are allowed."></span>
Allowed programming languages
</td>
<td class="wrapping-options">
{% for lang in LANGUAGES %}
<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>
{% endfor %}
</td>
</tr>
<tr>
<td>
<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>
Expand Down
8 changes: 5 additions & 3 deletions cms/server/contest/static/cws_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,17 +338,19 @@ CMS.CWSUtils.prototype.switch_lang = function() {
location.reload();
};

CMS.CWSUtils.filter_languages = function(options, inputs) {
CMS.CWSUtils.filter_languages = function (options, inputs, languages) {
languages = languages || LANGUAGES;

var exts = [];
for (var i = 0; i < inputs.length; i++) {
exts.push('.' + inputs[i].value.match(/[^.]*$/)[0]);
}
// Find all languages that should be enabled.
var enabled = {};
var anyEnabled = false;
for (var lang in LANGUAGES) {
for (var lang in languages) {
for (i = 0; i < exts.length; i++) {
if (LANGUAGES[lang][exts[i]]) {
if (languages[lang][exts[i]]) {
enabled[lang] = true;
anyEnabled = true;
break;
Expand Down
15 changes: 11 additions & 4 deletions cms/server/contest/submission/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# Copyright © 2015-2016 William Di Luigi <williamdiluigi@gmail.com>
# Copyright © 2016 Myungwoo Chun <mc.tamaki@gmail.com>
# Copyright © 2016 Amir Keivan Mohtashami <akmohtashami97@gmail.com>
# Copyright © 2025 Pasit Sangprachathanarak <ouipingpasit@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -195,8 +196,11 @@ def accept_submission(

try:
files, language = match_files_and_language(
received_files, language_name, required_codenames,
contest.languages)
received_files,
language_name,
required_codenames,
task.get_allowed_languages(),
)
except InvalidFilesOrLanguage as err:
logger.info(f'Submission rejected: {err}')
raise UnacceptableSubmission(
Expand Down Expand Up @@ -398,8 +402,11 @@ def accept_user_test(

try:
files, language = match_files_and_language(
received_files, language_name, required_codenames,
contest.languages)
received_files,
language_name,
required_codenames,
task.get_allowed_languages(),
)
except InvalidFilesOrLanguage as err:
logger.info(f'Test rejected: {err}')
raise UnacceptableUserTest(
Expand Down
3 changes: 2 additions & 1 deletion cms/server/contest/templates/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ <h2>{% trans %}Task overview{% endtrans %}</h2>
</tr>
</thead>
<tbody>
{% set extensions = "[%s]"|format(contest.languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %}
{% for t_iter in contest.tasks %}
{% set task_allowed_languages = t_iter.get_allowed_languages() %}
{% set extensions = "[%s]"|format(task_allowed_languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %}
<tr>
<th>{{ t_iter.name }}</th>
<td>{{ t_iter.title }}</td>
Expand Down
3 changes: 2 additions & 1 deletion cms/server/contest/templates/task_description.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ <h2>{% trans %}Some details{% endtrans %}</h2>
{% endif %}
{% set compilation_commands = task_type.get_compilation_commands(task.submission_format) %}
{% if compilation_commands is not none %}
{% set compilation_commands = compilation_commands|dictselect("in", contest.languages, by="key") %}
{% set allowed_languages = task.get_allowed_languages() %}
{% set compilation_commands = compilation_commands|dictselect("in", allowed_languages, by="key") %}
<tr>
<th rowspan="{{ compilation_commands|length }}">{% trans %}Compilation commands{% endtrans %}</th>
{% for l, c in compilation_commands|dictsort(by="key") %}
Expand Down
17 changes: 15 additions & 2 deletions cms/server/contest/templates/task_submissions.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

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

{% block js_init %}
// Define TASK_LANGUAGES for task-specific language filtering
var TASK_LANGUAGES = {
{% for lang in task.get_allowed_languages() or [] %}
'{{ lang }}': {
{% for extension in (lang|to_language).source_extensions %}
'{{ extension }}': true,
{% endfor %}
},
{% endfor %}
};
{% endblock js_init %}

{# Whether tokens are allowed on this contest. #}
{% set can_use_tokens_in_contest =
tokens_contest != TOKEN_MODE_DISABLED
Expand Down Expand Up @@ -254,15 +267,15 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a solution{% endtrans %}</h2>
<input type="file" class="input-xlarge"
id="input{{ loop.index0 }}" name="{{ filename }}"
onchange="CMS.CWSUtils.filter_languages($(this).parents('form').find('select[name=language] option'),
$(this).parents('form').find('input[type=file]'))"/>
$(this).parents('form').find('input[type=file]'), TASK_LANGUAGES)"/>
</div>
</div>
{% endfor %}
{% if task.submission_format|any("endswith", ".%l") %}
<div class="control-group">
<div class="controls">
<select name="language">
{% for lang in contest.languages %}
{% for lang in task.get_allowed_languages() or [] %}
<option value="{{ lang }}">{{ lang }}</option>
{% endfor %}
</select>
Expand Down
17 changes: 15 additions & 2 deletions cms/server/contest/templates/test_interface.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

{% set page = "test_interface" %}

{% block js_init %}
Comment thread
pxsit marked this conversation as resolved.
// Define TASK_LANGUAGES for task-specific language filtering
var TASK_LANGUAGES = {
{% for lang in task.get_allowed_languages() or [] %}
'{{ lang }}': {
{% for extension in (lang|to_language).source_extensions %}
'{{ extension }}': true,
{% endfor %}
},
{% endfor %}
};
{% endblock js_init %}

{% block additional_js %}
$(document).on("click", ".user_test_list tbody tr td.status .details", function (event) {
var $this = $(this);
Expand Down Expand Up @@ -105,7 +118,7 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a test{% endtrans %}</h2>
<input type="file" class="input-xlarge"
id="input{{ loop.index0 }}" name="{{ filename }}"
onchange="CMS.CWSUtils.filter_languages($(this).parents('form').find('select[name=language] option'),
$(this).parents('form').find('input[type=file]').not('#input_file'))"/>
$(this).parents('form').find('input[type=file]').not('#input_file'), TASK_LANGUAGES)"/>
</div>
</div>
{% endfor %}
Expand All @@ -118,7 +131,7 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a test{% endtrans %}</h2>
<div class="control-group">
<div class="controls">
<select name="language">
{% for lang in contest.languages %}
{% for lang in task.get_allowed_languages() or [] %}
<option value="{{ lang }}">{{ lang }}</option>
{% endfor %}
</select>
Expand Down
3 changes: 3 additions & 0 deletions cmscontrib/updaters/update_from_1.5.sql
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ ALTER TABLE user_test_results ADD COLUMN evaluation_sandbox_digests VARCHAR[];
UPDATE user_test_results SET evaluation_sandbox_paths = string_to_array(evaluation_sandbox, ':');
ALTER TABLE user_test_results DROP COLUMN evaluation_sandbox;

-- https://github.com/cms-dev/cms/pull/1486
ALTER TABLE public.tasks ADD COLUMN allowed_languages varchar[];

COMMIT;