From 6b78ff3ab43b08cbab94cba42504771cf6d90875 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 28 Jul 2025 21:08:38 +0700 Subject: [PATCH 1/8] Commit --- cms/db/__init__.py | 2 +- cms/db/task.py | 4 ++ cms/server/admin/handlers/task.py | 3 ++ cms/server/admin/templates/task.html | 19 ++++++++ cms/server/contest/submission/workflow.py | 32 ++++++++++++-- .../contest/templates/task_submissions.html | 20 ++++++++- .../contest/templates/test_interface.html | 20 ++++++++- cmscontrib/updaters/update_47.py | 43 +++++++++++++++++++ cmscontrib/updaters/update_47.sql | 5 +++ 9 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 cmscontrib/updaters/update_47.py create mode 100644 cmscontrib/updaters/update_47.sql diff --git a/cms/db/__init__.py b/cms/db/__init__.py index 18f87364c8..26f0e5d164 100644 --- a/cms/db/__init__.py +++ b/cms/db/__init__.py @@ -81,7 +81,7 @@ # Instantiate or import these objects. -version = 46 +version = 47 engine = create_engine(config.database, echo=config.database_debug, pool_timeout=60, pool_recycle=120) diff --git a/cms/db/task.py b/cms/db/task.py index b27a49b8bd..2282b65570 100644 --- a/cms/db/task.py +++ b/cms/db/task.py @@ -116,6 +116,10 @@ class Task(Base): nullable=False, default=[]) + # The list of names of programming languages allowed for this task. + # If empty, all contest languages are allowed. + allowed_languages: list[str] = Column(ARRAY(String), nullable=False, default=[]) + # 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. diff --git a/cms/server/admin/handlers/task.py b/cms/server/admin/handlers/task.py index 5f44017a1b..1df82f092f 100644 --- a/cms/server/admin/handlers/task.py +++ b/cms/server/admin/handlers/task.py @@ -147,6 +147,9 @@ def post(self, task_id): self.get_submission_format(attrs) self.get_string(attrs, "feedback_level") + # Process allowed languages + attrs["allowed_languages"] = self.get_arguments("allowed_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/templates/task.html b/cms/server/admin/templates/task.html index 00c6759cb7..e73dc9b3ec 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -130,6 +130,25 @@

Task configuration

+ + + + Allowed programming languages + + + {% if task.contest %} + {% for lang in LANGUAGES %} + {% if lang.name in task.contest.languages %} + + {% endif %} + {% endfor %} + {% else %} + Task must be assigned to a contest to configure allowed languages. + {% endif %} + + diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py index 0a72542d7f..5d46ade90a 100644 --- a/cms/server/contest/submission/workflow.py +++ b/cms/server/contest/submission/workflow.py @@ -62,6 +62,24 @@ def N_(msgid): return msgid +def get_task_allowed_languages(task: Task) -> list[str] | None: + """Get the list of allowed languages for a task. + + If the task has specific allowed languages configured, return those. + Otherwise, return the contest's allowed languages. + + task: the task object + + return: list of allowed language names, or None if all languages are allowed + """ + # If task has specific language restrictions, use those + if task.allowed_languages: + return task.allowed_languages + + # Otherwise, use contest language restrictions + return task.contest.languages if task.contest else None + + class UnacceptableSubmission(Exception): def __init__(self, subject: str, text: str, text_params: object = None): @@ -193,8 +211,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, + get_task_allowed_languages(task), + ) except InvalidFilesOrLanguage as err: logger.info(f'Submission rejected: {err}') raise UnacceptableSubmission( @@ -387,8 +408,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, + get_task_allowed_languages(task), + ) except InvalidFilesOrLanguage as err: logger.info(f'Test rejected: {err}') raise UnacceptableUserTest( diff --git a/cms/server/contest/templates/task_submissions.html b/cms/server/contest/templates/task_submissions.html index 6105d17259..47c68d861b 100644 --- a/cms/server/contest/templates/task_submissions.html +++ b/cms/server/contest/templates/task_submissions.html @@ -6,6 +6,23 @@ {% set score_type = get_score_type(dataset=task.active_dataset) %} +{% block js_init %} +// Override LANGUAGES for task-specific language filtering +var TASK_LANGUAGES = { +{% set task_languages = task.allowed_languages if task.allowed_languages else contest.languages %} +{% for lang in task_languages %} + '{{ lang }}': { +{% for extension in (lang|to_language).source_extensions %} + '{{ extension }}': true, +{% endfor %} + }, +{% endfor %} +}; + +// Override the global LANGUAGES with task-specific ones for this page +LANGUAGES = TASK_LANGUAGES; +{% endblock js_init %} + {# Whether tokens are allowed on this contest. #} {% set can_use_tokens_in_contest = tokens_contest != TOKEN_MODE_DISABLED @@ -262,7 +279,8 @@

{% 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..317a5bea1a 100644 --- a/cms/server/contest/templates/test_interface.html +++ b/cms/server/contest/templates/test_interface.html @@ -2,6 +2,23 @@ {% set page = "test_interface" %} +{% block js_init %} +// Override LANGUAGES for task-specific language filtering +var TASK_LANGUAGES = { +{% set task_languages = task.allowed_languages if task.allowed_languages else contest.languages %} +{% for lang in task_languages %} + '{{ lang }}': { +{% for extension in (lang|to_language).source_extensions %} + '{{ extension }}': true, +{% endfor %} + }, +{% endfor %} +}; + +// Override the global LANGUAGES with task-specific ones for this page +LANGUAGES = TASK_LANGUAGES; +{% endblock js_init %} + {% block additional_js %} $(document).on("click", ".user_test_list tbody tr td.status .details", function (event) { var $this = $(this); @@ -118,7 +135,8 @@

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

diff --git a/cmscontrib/updaters/update_47.py b/cmscontrib/updaters/update_47.py new file mode 100644 index 0000000000..2f3b2dc1e6 --- /dev/null +++ b/cmscontrib/updaters/update_47.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2025 GitHub Copilot +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""A class to update a dump created by CMS. + +Used by DumpImporter and DumpUpdater. + +This updater adds the allowed_languages field to tasks to support +per-task language restrictions. + +""" + + +class Updater: + + def __init__(self, data): + assert data["_version"] == 46 + self.objs = data + + def run(self): + for k, v in self.objs.items(): + if k.startswith("_"): + continue + if v["_class"] == "Task": + # Add empty allowed_languages list (defaults to allowing all contest languages) + v["allowed_languages"] = [] + + return self.objs diff --git a/cmscontrib/updaters/update_47.sql b/cmscontrib/updaters/update_47.sql new file mode 100644 index 0000000000..d7e50a66ca --- /dev/null +++ b/cmscontrib/updaters/update_47.sql @@ -0,0 +1,5 @@ +begin; + +alter table tasks add allowed_languages varchar[] not null default '{}'; + +rollback; -- change this to: commit; From 5d2bd587d888e3eb6becfa6688ec4358a1bd6e4d Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 28 Jul 2025 21:20:45 +0700 Subject: [PATCH 2/8] Reverse --- cms/db/task.py | 6 +++++- cms/server/admin/handlers/task.py | 13 ++++++++++++- cms/server/admin/templates/task.html | 2 +- cms/server/contest/submission/workflow.py | 1 + 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cms/db/task.py b/cms/db/task.py index 2282b65570..7d8ab58d4c 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 @@ -118,7 +119,10 @@ class Task(Base): # The list of names of programming languages allowed for this task. # If empty, all contest languages are allowed. - allowed_languages: list[str] = Column(ARRAY(String), nullable=False, default=[]) + allowed_languages: list[str] = Column( + ARRAY(String), + nullable=False, + default=[]) # The parameters that control task-tokens follow. Note that their # effect during the contest depends on the interaction with the diff --git a/cms/server/admin/handlers/task.py b/cms/server/admin/handlers/task.py index 1df82f092f..866dad71fb 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 @@ -148,7 +149,17 @@ def post(self, task_id): self.get_string(attrs, "feedback_level") # Process allowed languages - attrs["allowed_languages"] = self.get_arguments("allowed_languages") + selected_languages = self.get_arguments("allowed_languages") + if task.contest: + # If all contest languages are selected, store as empty list (no restriction) + # Otherwise, store the specific selection + contest_languages = set(task.contest.languages) + if set(selected_languages) == contest_languages: + attrs["allowed_languages"] = [] + else: + attrs["allowed_languages"] = selected_languages + else: + attrs["allowed_languages"] = selected_languages self.get_string(attrs, "token_mode") self.get_int(attrs, "token_max_number") diff --git a/cms/server/admin/templates/task.html b/cms/server/admin/templates/task.html index e73dc9b3ec..1b3e5bf170 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -141,7 +141,7 @@

Task configuration

{% if task.contest %} {% for lang in LANGUAGES %} {% if lang.name in task.contest.languages %} - + {% endif %} {% endfor %} {% else %} diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py index 5d46ade90a..1619baf4df 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 From b308341f25354aab8b7595c27071601ac5bfcc66 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 28 Jul 2025 21:29:28 +0700 Subject: [PATCH 3/8] Fix Layout --- cms/server/admin/static/aws_style.css | 30 +++++++++++++++++++++++++++ cms/server/admin/templates/task.html | 22 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/cms/server/admin/static/aws_style.css b/cms/server/admin/static/aws_style.css index 2d4aaf1003..dbf6bec1ea 100644 --- a/cms/server/admin/static/aws_style.css +++ b/cms/server/admin/static/aws_style.css @@ -382,6 +382,36 @@ table td.wrapping-options label { margin-right: 15px; } +/* Language grid layout for better readability */ +table td.language-grid { + width: 100%; +} + +.language-grid-container { + width: 100%; + max-width: 600px; +} + +.language-row { + display: flex; + margin-bottom: 8px; +} + +.language-item { + flex: 1; + min-width: 120px; + padding-right: 10px; +} + +.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 1b3e5bf170..6fb5f3a427 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -137,13 +137,31 @@

Task configuration

Otherwise, only the selected languages (which must be a subset of contest languages) are allowed."> Allowed programming languages - + {% if task.contest %} + {% set contest_langs = [] %} {% for lang in LANGUAGES %} {% if lang.name in task.contest.languages %} - + {% set _ = contest_langs.append(lang.name) %} {% endif %} {% endfor %} + +
+ {% for i in range(0, contest_langs|length, 3) %} +
+ {% for j in range(3) %} + {% if i + j < contest_langs|length %} + {% set lang_name = contest_langs[i + j] %} +
+ +
+ {% else %} +
+ {% endif %} + {% endfor %} +
+ {% endfor %} +
{% else %} Task must be assigned to a contest to configure allowed languages. {% endif %} From be7144de9b70d5f91b3975011e99ba43f08ce364 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 28 Jul 2025 21:38:40 +0700 Subject: [PATCH 4/8] This was a bad idea --- cms/server/admin/static/aws_style.css | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cms/server/admin/static/aws_style.css b/cms/server/admin/static/aws_style.css index dbf6bec1ea..f40942014b 100644 --- a/cms/server/admin/static/aws_style.css +++ b/cms/server/admin/static/aws_style.css @@ -382,14 +382,9 @@ table td.wrapping-options label { margin-right: 15px; } -/* Language grid layout for better readability */ -table td.language-grid { - width: 100%; -} - .language-grid-container { width: 100%; - max-width: 600px; + max-width: 500px; } .language-row { From da93944f29ff780aff691b375f81e64cc346a461 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Fri, 1 Aug 2025 00:14:58 +0700 Subject: [PATCH 5/8] Fix a lot of things --- cms/db/__init__.py | 2 +- cms/db/task.py | 24 ++++++++--- cms/server/admin/handlers/task.py | 8 ++-- cms/server/admin/templates/task.html | 2 +- cms/server/contest/static/cws_utils.js | 8 ++-- cms/server/contest/submission/workflow.py | 22 +--------- .../contest/templates/task_submissions.html | 13 ++---- .../contest/templates/test_interface.html | 13 ++---- cmscontrib/updaters/update_47.py | 43 ------------------- cmscontrib/updaters/update_47.sql | 5 --- cmscontrib/updaters/update_from_1.5.sql | 3 ++ 11 files changed, 44 insertions(+), 99 deletions(-) delete mode 100644 cmscontrib/updaters/update_47.py delete mode 100644 cmscontrib/updaters/update_47.sql diff --git a/cms/db/__init__.py b/cms/db/__init__.py index 26f0e5d164..18f87364c8 100644 --- a/cms/db/__init__.py +++ b/cms/db/__init__.py @@ -81,7 +81,7 @@ # Instantiate or import these objects. -version = 47 +version = 46 engine = create_engine(config.database, echo=config.database_debug, pool_timeout=60, pool_recycle=120) diff --git a/cms/db/task.py b/cms/db/task.py index 7d8ab58d4c..3aa3dfd740 100644 --- a/cms/db/task.py +++ b/cms/db/task.py @@ -118,11 +118,10 @@ class Task(Base): default=[]) # The list of names of programming languages allowed for this task. - # If empty, all contest languages are allowed. - allowed_languages: list[str] = Column( - ARRAY(String), - nullable=False, - default=[]) + # 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 @@ -280,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 866dad71fb..b6b878bec3 100644 --- a/cms/server/admin/handlers/task.py +++ b/cms/server/admin/handlers/task.py @@ -151,15 +151,17 @@ def post(self, task_id): # Process allowed languages selected_languages = self.get_arguments("allowed_languages") if task.contest: - # If all contest languages are selected, store as empty list (no restriction) + # If all contest languages are selected, store as None (no restriction) # Otherwise, store the specific selection contest_languages = set(task.contest.languages) if set(selected_languages) == contest_languages: - attrs["allowed_languages"] = [] + attrs["allowed_languages"] = None else: attrs["allowed_languages"] = selected_languages else: - attrs["allowed_languages"] = selected_languages + attrs["allowed_languages"] = ( + selected_languages if selected_languages else None + ) self.get_string(attrs, "token_mode") self.get_int(attrs, "token_max_number") diff --git a/cms/server/admin/templates/task.html b/cms/server/admin/templates/task.html index 6fb5f3a427..0644f5b4ed 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -153,7 +153,7 @@

Task configuration

{% if i + j < contest_langs|length %} {% set lang_name = contest_langs[i + j] %}
- +
{% else %}
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 1619baf4df..6a082a0a57 100644 --- a/cms/server/contest/submission/workflow.py +++ b/cms/server/contest/submission/workflow.py @@ -63,24 +63,6 @@ def N_(msgid): return msgid -def get_task_allowed_languages(task: Task) -> list[str] | None: - """Get the list of allowed languages for a task. - - If the task has specific allowed languages configured, return those. - Otherwise, return the contest's allowed languages. - - task: the task object - - return: list of allowed language names, or None if all languages are allowed - """ - # If task has specific language restrictions, use those - if task.allowed_languages: - return task.allowed_languages - - # Otherwise, use contest language restrictions - return task.contest.languages if task.contest else None - - class UnacceptableSubmission(Exception): def __init__(self, subject: str, text: str, text_params: object = None): @@ -215,7 +197,7 @@ def accept_submission( received_files, language_name, required_codenames, - get_task_allowed_languages(task), + task.get_allowed_languages(), ) except InvalidFilesOrLanguage as err: logger.info(f'Submission rejected: {err}') @@ -412,7 +394,7 @@ def accept_user_test( received_files, language_name, required_codenames, - get_task_allowed_languages(task), + task.get_allowed_languages(), ) except InvalidFilesOrLanguage as err: logger.info(f'Test rejected: {err}') diff --git a/cms/server/contest/templates/task_submissions.html b/cms/server/contest/templates/task_submissions.html index 47c68d861b..be1a439f56 100644 --- a/cms/server/contest/templates/task_submissions.html +++ b/cms/server/contest/templates/task_submissions.html @@ -7,10 +7,9 @@ {% set score_type = get_score_type(dataset=task.active_dataset) %} {% block js_init %} -// Override LANGUAGES for task-specific language filtering +// Define TASK_LANGUAGES for task-specific language filtering var TASK_LANGUAGES = { -{% set task_languages = task.allowed_languages if task.allowed_languages else contest.languages %} -{% for lang in task_languages %} +{% for lang in task.get_allowed_languages() or [] %} '{{ lang }}': { {% for extension in (lang|to_language).source_extensions %} '{{ extension }}': true, @@ -18,9 +17,6 @@ }, {% endfor %} }; - -// Override the global LANGUAGES with task-specific ones for this page -LANGUAGES = TASK_LANGUAGES; {% endblock js_init %} {# Whether tokens are allowed on this contest. #} @@ -271,7 +267,7 @@

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

+ $(this).parents('form').find('input[type=file]'), TASK_LANGUAGES)"/>
{% endfor %} @@ -279,8 +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 317a5bea1a..49f49946f2 100644 --- a/cms/server/contest/templates/test_interface.html +++ b/cms/server/contest/templates/test_interface.html @@ -3,10 +3,9 @@ {% set page = "test_interface" %} {% block js_init %} -// Override LANGUAGES for task-specific language filtering +// Define TASK_LANGUAGES for task-specific language filtering var TASK_LANGUAGES = { -{% set task_languages = task.allowed_languages if task.allowed_languages else contest.languages %} -{% for lang in task_languages %} +{% for lang in task.get_allowed_languages() or [] %} '{{ lang }}': { {% for extension in (lang|to_language).source_extensions %} '{{ extension }}': true, @@ -14,9 +13,6 @@ }, {% endfor %} }; - -// Override the global LANGUAGES with task-specific ones for this page -LANGUAGES = TASK_LANGUAGES; {% endblock js_init %} {% block additional_js %} @@ -122,7 +118,7 @@

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

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

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

diff --git a/cmscontrib/updaters/update_47.py b/cmscontrib/updaters/update_47.py deleted file mode 100644 index 2f3b2dc1e6..0000000000 --- a/cmscontrib/updaters/update_47.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 - -# Contest Management System - http://cms-dev.github.io/ -# Copyright © 2025 GitHub Copilot -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""A class to update a dump created by CMS. - -Used by DumpImporter and DumpUpdater. - -This updater adds the allowed_languages field to tasks to support -per-task language restrictions. - -""" - - -class Updater: - - def __init__(self, data): - assert data["_version"] == 46 - self.objs = data - - def run(self): - for k, v in self.objs.items(): - if k.startswith("_"): - continue - if v["_class"] == "Task": - # Add empty allowed_languages list (defaults to allowing all contest languages) - v["allowed_languages"] = [] - - return self.objs diff --git a/cmscontrib/updaters/update_47.sql b/cmscontrib/updaters/update_47.sql deleted file mode 100644 index d7e50a66ca..0000000000 --- a/cmscontrib/updaters/update_47.sql +++ /dev/null @@ -1,5 +0,0 @@ -begin; - -alter table tasks add allowed_languages varchar[] not null default '{}'; - -rollback; -- change this to: commit; 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; From d05fb266d9939bbb2cf5679873b0295e29d4acce Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Fri, 1 Aug 2025 18:22:50 +0700 Subject: [PATCH 6/8] Fix --- cms/server/admin/static/aws_style.css | 16 -------------- cms/server/admin/templates/task.html | 22 ++----------------- cms/server/contest/templates/overview.html | 3 ++- .../contest/templates/task_description.html | 3 ++- 4 files changed, 6 insertions(+), 38 deletions(-) diff --git a/cms/server/admin/static/aws_style.css b/cms/server/admin/static/aws_style.css index f40942014b..cc56bd6700 100644 --- a/cms/server/admin/static/aws_style.css +++ b/cms/server/admin/static/aws_style.css @@ -382,22 +382,6 @@ table td.wrapping-options label { margin-right: 15px; } -.language-grid-container { - width: 100%; - max-width: 500px; -} - -.language-row { - display: flex; - margin-bottom: 8px; -} - -.language-item { - flex: 1; - min-width: 120px; - padding-right: 10px; -} - .language-item label { display: block; white-space: nowrap; diff --git a/cms/server/admin/templates/task.html b/cms/server/admin/templates/task.html index 0644f5b4ed..61d3ffb219 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -137,31 +137,13 @@

Task configuration

Otherwise, only the selected languages (which must be a subset of contest languages) are allowed."> Allowed programming languages - + {% if task.contest %} - {% set contest_langs = [] %} {% for lang in LANGUAGES %} {% if lang.name in task.contest.languages %} - {% set _ = contest_langs.append(lang.name) %} + {% endif %} {% endfor %} - -
- {% for i in range(0, contest_langs|length, 3) %} -
- {% for j in range(3) %} - {% if i + j < contest_langs|length %} - {% set lang_name = contest_langs[i + j] %} -
- -
- {% else %} -
- {% endif %} - {% endfor %} -
- {% endfor %} -
{% else %} Task must be assigned to a contest to configure allowed languages. {% endif %} 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") %} From 4cea60bde73a07b52c1c26c42495314d30bef50e Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Fri, 1 Aug 2025 18:40:07 +0700 Subject: [PATCH 7/8] Update --- cms/server/admin/handlers/contest.py | 22 ++++++++++++++++++++++ cms/server/admin/handlers/task.py | 15 ++++----------- cms/server/admin/templates/task.html | 14 ++++---------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cms/server/admin/handlers/contest.py b/cms/server/admin/handlers/contest.py index 1a4c8e8ea6..9153b025f0 100644 --- a/cms/server/admin/handlers/contest.py +++ b/cms/server/admin/handlers/contest.py @@ -93,6 +93,28 @@ def post(self, contest_id: str): attrs["languages"] = self.get_arguments("languages") + # Auto-enable newly added languages in all tasks + old_languages = set(contest.languages) + new_languages = set(attrs["languages"]) + newly_added_languages = new_languages - old_languages + newly_removed_languages = old_languages - new_languages + + if newly_added_languages or newly_removed_languages: + # Update all tasks in this contest that have allowed_languages set + for task in contest.tasks: + if task.allowed_languages is not None: + current_task_languages = set(task.allowed_languages) + + # Add newly enabled contest languages to task's allowed languages + if newly_added_languages: + current_task_languages |= newly_added_languages + + # Remove newly disabled contest languages from task's allowed languages + if newly_removed_languages: + current_task_languages -= newly_removed_languages + + task.allowed_languages = list(current_task_languages) + self.get_bool(attrs, "submissions_download_allowed") self.get_bool(attrs, "allow_questions") self.get_bool(attrs, "allow_user_tests") diff --git a/cms/server/admin/handlers/task.py b/cms/server/admin/handlers/task.py index b6b878bec3..77aed67613 100644 --- a/cms/server/admin/handlers/task.py +++ b/cms/server/admin/handlers/task.py @@ -150,18 +150,11 @@ def post(self, task_id): # Process allowed languages selected_languages = self.get_arguments("allowed_languages") - if task.contest: - # If all contest languages are selected, store as None (no restriction) - # Otherwise, store the specific selection - contest_languages = set(task.contest.languages) - if set(selected_languages) == contest_languages: - attrs["allowed_languages"] = None - else: - attrs["allowed_languages"] = selected_languages + if not selected_languages: + # No languages selected means allow all contest languages (NULL) + attrs["allowed_languages"] = None else: - attrs["allowed_languages"] = ( - selected_languages if selected_languages else None - ) + attrs["allowed_languages"] = selected_languages self.get_string(attrs, "token_mode") self.get_int(attrs, "token_max_number") diff --git a/cms/server/admin/templates/task.html b/cms/server/admin/templates/task.html index 61d3ffb219..0623a69849 100644 --- a/cms/server/admin/templates/task.html +++ b/cms/server/admin/templates/task.html @@ -137,16 +137,10 @@

Task configuration

Otherwise, only the selected languages (which must be a subset of contest languages) are allowed."> Allowed programming languages - - {% if task.contest %} - {% for lang in LANGUAGES %} - {% if lang.name in task.contest.languages %} - - {% endif %} - {% endfor %} - {% else %} - Task must be assigned to a contest to configure allowed languages. - {% endif %} + + {% for lang in LANGUAGES %} + + {% endfor %} From 8dba5daaea895218949a747578af2205f2a6014c Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Fri, 1 Aug 2025 19:11:17 +0700 Subject: [PATCH 8/8] Remove auto-sync --- cms/server/admin/handlers/contest.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/cms/server/admin/handlers/contest.py b/cms/server/admin/handlers/contest.py index 9153b025f0..1a4c8e8ea6 100644 --- a/cms/server/admin/handlers/contest.py +++ b/cms/server/admin/handlers/contest.py @@ -93,28 +93,6 @@ def post(self, contest_id: str): attrs["languages"] = self.get_arguments("languages") - # Auto-enable newly added languages in all tasks - old_languages = set(contest.languages) - new_languages = set(attrs["languages"]) - newly_added_languages = new_languages - old_languages - newly_removed_languages = old_languages - new_languages - - if newly_added_languages or newly_removed_languages: - # Update all tasks in this contest that have allowed_languages set - for task in contest.tasks: - if task.allowed_languages is not None: - current_task_languages = set(task.allowed_languages) - - # Add newly enabled contest languages to task's allowed languages - if newly_added_languages: - current_task_languages |= newly_added_languages - - # Remove newly disabled contest languages from task's allowed languages - if newly_removed_languages: - current_task_languages -= newly_removed_languages - - task.allowed_languages = list(current_task_languages) - self.get_bool(attrs, "submissions_download_allowed") self.get_bool(attrs, "allow_questions") self.get_bool(attrs, "allow_user_tests")