diff --git a/cms/db/task.py b/cms/db/task.py index 388a4a56d4..c753efdce1 100644 --- a/cms/db/task.py +++ b/cms/db/task.py @@ -6,6 +6,7 @@ # Copyright © 2010-2012 Matteo Boscariol # Copyright © 2012-2018 Luca Wehrstedt # Copyright © 2013 Bernard Blackham +# Copyright © 2025 Pasit Sangprachathanarak # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -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. @@ -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. diff --git a/cms/server/admin/handlers/task.py b/cms/server/admin/handlers/task.py index 5f44017a1b..77aed67613 100644 --- a/cms/server/admin/handlers/task.py +++ b/cms/server/admin/handlers/task.py @@ -8,6 +8,7 @@ # Copyright © 2014 Artem Iglikov # Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> # Copyright © 2016 Myungwoo Chun +# Copyright © 2025 Pasit Sangprachathanarak # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -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") diff --git a/cms/server/admin/static/aws_style.css b/cms/server/admin/static/aws_style.css index 1dce90fcb3..cd442a3514 100644 --- a/cms/server/admin/static/aws_style.css +++ b/cms/server/admin/static/aws_style.css @@ -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: "*"; } diff --git a/cms/server/admin/templates/task.html b/cms/server/admin/templates/task.html index 00c6759cb7..0623a69849 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -130,6 +130,19 @@

Task configuration

+ + + + Allowed programming languages + + + {% for lang in LANGUAGES %} + + {% endfor %} + + diff --git a/cms/server/contest/static/cws_utils.js b/cms/server/contest/static/cws_utils.js index 23ab394f9c..aa86dc7161 100644 --- a/cms/server/contest/static/cws_utils.js +++ b/cms/server/contest/static/cws_utils.js @@ -338,7 +338,9 @@ 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]); @@ -346,9 +348,9 @@ CMS.CWSUtils.filter_languages = function(options, inputs) { // 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; diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py index 9210fbd80f..a421427eb8 100644 --- a/cms/server/contest/submission/workflow.py +++ b/cms/server/contest/submission/workflow.py @@ -11,6 +11,7 @@ # Copyright © 2015-2016 William Di Luigi # Copyright © 2016 Myungwoo Chun # Copyright © 2016 Amir Keivan Mohtashami +# Copyright © 2025 Pasit Sangprachathanarak # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -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( @@ -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( diff --git a/cms/server/contest/templates/overview.html b/cms/server/contest/templates/overview.html index 37fe202e3e..9d70120e13 100644 --- a/cms/server/contest/templates/overview.html +++ b/cms/server/contest/templates/overview.html @@ -205,8 +205,9 @@

{% trans %}Task overview{% endtrans %}

-{% 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("|")) %} {{ t_iter.name }} {{ t_iter.title }} diff --git a/cms/server/contest/templates/task_description.html b/cms/server/contest/templates/task_description.html index b560ab9bea..2948274a14 100644 --- a/cms/server/contest/templates/task_description.html +++ b/cms/server/contest/templates/task_description.html @@ -118,7 +118,8 @@

{% trans %}Some details{% endtrans %}

{% 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") %} {% trans %}Compilation commands{% endtrans %} {% for l, c in compilation_commands|dictsort(by="key") %} diff --git a/cms/server/contest/templates/task_submissions.html b/cms/server/contest/templates/task_submissions.html index 6105d17259..be1a439f56 100644 --- a/cms/server/contest/templates/task_submissions.html +++ b/cms/server/contest/templates/task_submissions.html @@ -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 @@ -254,7 +267,7 @@

{% trans %}Submit a solution{% endtrans %}

+ $(this).parents('form').find('input[type=file]'), TASK_LANGUAGES)"/> {% endfor %} @@ -262,7 +275,7 @@

{% trans %}Submit a solution{% endtrans %}

diff --git a/cms/server/contest/templates/test_interface.html b/cms/server/contest/templates/test_interface.html index 33e2d178b2..49f49946f2 100644 --- a/cms/server/contest/templates/test_interface.html +++ b/cms/server/contest/templates/test_interface.html @@ -2,6 +2,19 @@ {% set page = "test_interface" %} +{% 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 %} + {% block additional_js %} $(document).on("click", ".user_test_list tbody tr td.status .details", function (event) { var $this = $(this); @@ -105,7 +118,7 @@

{% trans %}Submit a test{% endtrans %}

+ $(this).parents('form').find('input[type=file]').not('#input_file'), TASK_LANGUAGES)"/>
{% endfor %} @@ -118,7 +131,7 @@

{% trans %}Submit a test{% endtrans %}

diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index c22b736c6a..a1ff578498 100644 --- a/cmscontrib/updaters/update_from_1.5.sql +++ b/cmscontrib/updaters/update_from_1.5.sql @@ -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;