From bb17b1aea0c8db8ef1bd3758b805c0e95ca7731b Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Wed, 23 Jul 2025 20:50:45 +0700 Subject: [PATCH 01/20] Add a Score counter to task overview Add a Score counter to task overview --- cms/server/contest/handlers/main.py | 48 ++- cms/server/contest/static/cws_style.css | 18 +- cms/server/contest/templates/overview.html | 452 +++++++++++---------- 3 files changed, 298 insertions(+), 220 deletions(-) diff --git a/cms/server/contest/handlers/main.py b/cms/server/contest/handlers/main.py index 4f34f81c16..98d3d0300c 100644 --- a/cms/server/contest/handlers/main.py +++ b/cms/server/contest/handlers/main.py @@ -10,6 +10,7 @@ # Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> # Copyright © 2015-2018 William Di Luigi # Copyright © 2021 Grace Hawkins +# 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 @@ -49,11 +50,13 @@ except ImportError: import tornado.web as tornado_web from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm import joinedload from cms import config -from cms.db import PrintJob, User, Participation, Team +from cms.db import Contest, PrintJob, User, Participation, Team from cms.grading.languagemanager import get_language from cms.grading.steps import COMPILATION_MESSAGES, EVALUATION_MESSAGES +from cms.grading.scoring import task_score from cms.server import multi_contest from cms.server.contest.authentication import validate_login from cms.server.contest.communication import get_communications @@ -79,8 +82,49 @@ class MainHandler(ContestHandler): """ @multi_contest def get(self): + self.r_params = self.render_params() self.render("overview.html", **self.r_params) + def render_params(self): + ret = super().render_params() + + if self.current_user is not None: + # This massive joined load gets all the information which we will need + participation = self.sql_session.query(Participation)\ + .filter(Participation.id == self.current_user.id)\ + .options( + joinedload('user'), + joinedload('contest'), + joinedload('submissions').joinedload('token'), + joinedload('submissions').joinedload('results'), + )\ + .first() + + self.contest = self.sql_session.query(Contest)\ + .filter(Contest.id == participation.contest.id)\ + .options( + joinedload('tasks') + .joinedload('active_dataset') + )\ + .first() + + ret["participation"] = participation + + # Compute public scores for all tasks + task_scores = {} + for task in self.contest.tasks: + score_type = task.active_dataset.score_type_object + max_public_score = round( + score_type.max_public_score, task.score_precision) + public_score, _ = task_score( + participation, task, public=True, rounded=True) + public_score = round(public_score, task.score_precision) + task_scores[task.id] = (public_score, + max_public_score, + score_type.format_score(public_score, score_type.max_public_score, None, task.score_precision, translation=self.translation)) + ret["task_scores"] = task_scores + + return ret class RegistrationHandler(ContestHandler): """Registration handler. @@ -384,7 +428,7 @@ def get(self): language_docs = [] if config.docs_path is not None: for language in languages: - ext = language.source_extensions[0][1:] # remove dot + ext = language.source_extensions[0][1:] # remove dot path = os.path.join(config.docs_path, ext) if os.path.exists(path): language_docs.append((language.name, ext)) diff --git a/cms/server/contest/static/cws_style.css b/cms/server/contest/static/cws_style.css index b5786b443f..5baef55d6a 100644 --- a/cms/server/contest/static/cws_style.css +++ b/cms/server/contest/static/cws_style.css @@ -559,27 +559,33 @@ td.token_rules p:last-child { color: #AAA; } -.submission_list td.public_score.score_0 { +.submission_list td.public_score.score_0, +.main_task_list td.public_score.score_0 { background-color: hsla(0, 100%, 50%, 0.4); } -.submission_list tr:hover td.public_score.score_0 { +.submission_list tr:hover td.public_score.score_0, +.main_task_list tr:hover td.public_score.score_0 { background-color: hsla(0, 100%, 50%, 0.5); } -.submission_list td.public_score.score_0_100 { +.submission_list td.public_score.score_0_100, +.main_task_list td.public_score.score_0_100 { background-color: hsla(60, 100%, 50%, 0.4); } -.submission_list tr:hover td.public_score.score_0_100 { +.submission_list tr:hover td.public_score.score_0_100, +.main_task_list tr:hover td.public_score.score_0_100 { background-color: hsla(60, 100%, 50%, 0.5); } -.submission_list td.public_score.score_100 { +.submission_list td.public_score.score_100, +.main_task_list td.public_score.score_100 { background-color: hsla(120, 100%, 50%, 0.4); } -.submission_list tr:hover td.public_score.score_100 { +.submission_list tr:hover td.public_score.score_100, +.main_task_list tr:hover td.public_score.score_100 { background-color: hsla(120, 100%, 50%, 0.5); } diff --git a/cms/server/contest/templates/overview.html b/cms/server/contest/templates/overview.html index 37fe202e3e..d2c31c81d6 100644 --- a/cms/server/contest/templates/overview.html +++ b/cms/server/contest/templates/overview.html @@ -2,188 +2,211 @@ {% set page = "overview" %} +{% block additional_js %} + +{% endblock additional_js %} + {% block core %}
- + -

{% trans %}General information{% endtrans %}

-
-
-

-{% if phase == -1 %} - {% trans %}The contest hasn't started yet.{% endtrans %} -

-

- {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, - stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} - The contest will start at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} -{% elif phase == 0 %} - {% trans %}The contest is currently running.{% endtrans %} -

-

- {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, - stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} - The contest started at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} -{% elif phase >= +1 %} - {% trans %}The contest has already ended.{% endtrans %} -

-

- {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, - stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} - The contest started at {{ start_time }} and ended at {{ stop_time }}. - {% endtrans %} -{% endif %} -

-{% if contest.analysis_enabled %} -

- {% if phase == +1 %} - {% trans %}The analysis mode hasn't started yet.{% endtrans %} -

-

- {% trans start_time=contest.analysis_start|format_datetime_smart, - stop_time=contest.analysis_stop|format_datetime_smart %} - The analysis mode will start at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} - {% elif phase == +2 %} - {% trans %}The analysis mode is currently running.{% endtrans %} -

-

- {% trans start_time=contest.analysis_start|format_datetime_smart, - stop_time=contest.analysis_stop|format_datetime_smart %} - The analysis mode started at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} - {% elif phase == +3 %} - {% trans %}The analysis mode has already ended.{% endtrans %} -

-

- {% trans start_time=contest.analysis_start|format_datetime_smart, - stop_time=contest.analysis_stop|format_datetime_smart %} - The analysis mode started at {{ start_time }} and ended at {{ stop_time }}. - {% endtrans %} - {% endif %} -

- -{% endif %} - - - -{% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} - {% if tokens_contest == TOKEN_MODE_INFINITE and tokens_tasks == TOKEN_MODE_INFINITE %} -

- {% trans %}You have an infinite number of tokens.{% endtrans %} -

- -

- {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} -

- {% elif tokens_contest == TOKEN_MODE_INFINITE %} -

- {% trans %}You have a distinct set of tokens for each task.{% endtrans %} - {%+ trans type_pl=_("tokens") %}You can find the rules for the {{ type_pl }} on each task's description page.{% endtrans %} -

- -

- {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} -

- {% elif tokens_tasks == TOKEN_MODE_INFINITE %} -

- {% trans %}You have a set of tokens shared among all tasks.{% endtrans %} - {{ contest|extract_token_params|format_token_rules }} -

- -

- {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} -

- {% else %} -

- {% trans %}You have two types of tokens: a set of contest-tokens shared among all tasks and a distinct set of task-tokens for each task.{% endtrans %} - {{ contest|extract_token_params|format_token_rules(t_type="contest") }} - {% trans type_pl=_("task-tokens") %}You can find the rules for the {{ type_pl }} on each task's description page.{% endtrans %} -

- -

- {% trans %}You can see the detailed result of a submission by using two tokens on it, one of each type.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} -

- {% endif %} -{% endif %} +

{% trans %}General information{% endtrans %}

+
+
+

+ {% if phase == -1 %} + {% trans %}The contest hasn't started yet.{% endtrans %} +

+

+ {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, + stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} + The contest will start at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} + {% elif phase == 0 %} + {% trans %}The contest is currently running.{% endtrans %} +

+

+ {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, + stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} + The contest started at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} + {% elif phase >= +1 %} + {% trans %}The contest has already ended.{% endtrans %} +

+

+ {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, + stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} + The contest started at {{ start_time }} and ended at {{ stop_time }}. + {% endtrans %} + {% endif %} +

+ {% if contest.analysis_enabled %} +

+ {% if phase == +1 %} + {% trans %}The analysis mode hasn't started yet.{% endtrans %} +

+

+ {% trans start_time=contest.analysis_start|format_datetime_smart, + stop_time=contest.analysis_stop|format_datetime_smart %} + The analysis mode will start at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} + {% elif phase == +2 %} + {% trans %}The analysis mode is currently running.{% endtrans %} +

+

+ {% trans start_time=contest.analysis_start|format_datetime_smart, + stop_time=contest.analysis_stop|format_datetime_smart %} + The analysis mode started at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} + {% elif phase == +3 %} + {% trans %}The analysis mode has already ended.{% endtrans %} +

+

+ {% trans start_time=contest.analysis_start|format_datetime_smart, + stop_time=contest.analysis_stop|format_datetime_smart %} + The analysis mode started at {{ start_time }} and ended at {{ stop_time }}. + {% endtrans %} + {% endif %} +

-{% if contest.max_submission_number is not none %} -

- {% trans submissions=contest.max_submission_number %}You can submit at most {{ submissions }} solutions during this contest.{% endtrans %} -

-{% endif %} + {% endif %} -{% if contest.max_user_test_number is not none %} -

- {% trans user_tests=contest.max_user_test_number %}You can submit at most {{ user_tests }} user tests during this contest.{% endtrans %} -

-{% endif %} -
-{% if contest.per_user_time is not none %} -
-
-

- {# TODO would be very nice to write something like "just for 3 consecutive hours"... #} - {% trans per_user_time=contest.per_user_time|format_timedelta %}Every user is allowed to compete (i.e. submit solutions) for a uninterrupted time frame of {{ per_user_time }}.{% endtrans %} -

- -

- {% if actual_phase == -2 %} - {% trans %}As soon as the contest starts you can choose to start your time frame.{% endtrans %} - {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the end of the contest, whatever comes first.{% endtrans %} - {% elif actual_phase == -1 %} - {% trans %}By clicking on the button below you can start your time frame.{% endtrans %} - {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the end of the contest, whatever comes first.{% endtrans %} - {% elif actual_phase == 0 %} - {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame at {{ start_time }}.{% endtrans %} - {%+ trans %}You can submit solutions until the end of the time frame or until the end of the contest, whatever comes first.{% endtrans %} - {% elif actual_phase == +1 %} - {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame at {{ start_time }} and you already finished it.{% endtrans %} - {%+ trans %}There's nothing you can do now.{% endtrans %} - {% elif actual_phase >= +2 %} - {% if participation.starting_time is none %} - {% trans %}You never started your time frame. Now it's too late.{% endtrans %} - {% else %} - {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame at {{ start_time }} and you already finished it.{% endtrans %} - {% endif %} - {% if actual_phase != +3 %} - {%+ trans %}There's nothing you can do now.{% endtrans %} - {% endif %} - {% endif %} + + {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} + {% if tokens_contest == TOKEN_MODE_INFINITE and tokens_tasks == TOKEN_MODE_INFINITE %} +

+ {% trans %}You have an infinite number of tokens.{% endtrans %}

- {% if actual_phase == -1 %} -
- {{ xsrf_form_html|safe }} - - -
- {% endif %} +

+ {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last + one.{% endtrans %} +

+ {% elif tokens_contest == TOKEN_MODE_INFINITE %} +

+ {% trans %}You have a distinct set of tokens for each task.{% endtrans %} + {%+ trans type_pl=_("tokens") %}You can find the rules for the {{ type_pl }} on each task's description + page.{% endtrans %} +

+ +

+ {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last + one.{% endtrans %} +

+ {% elif tokens_tasks == TOKEN_MODE_INFINITE %} +

+ {% trans %}You have a set of tokens shared among all tasks.{% endtrans %} + {{ contest|extract_token_params|format_token_rules }} +

+ +

+ {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last + one.{% endtrans %} +

+ {% else %} +

+ {% trans %}You have two types of tokens: a set of contest-tokens shared among all tasks and a + distinct set of task-tokens for each task.{% endtrans %} + {{ contest|extract_token_params|format_token_rules(t_type="contest") }} + {% trans type_pl=_("task-tokens") %}You can find the rules for the {{ type_pl }} on each task's + description page.{% endtrans %} +

+ +

+ {% trans %}You can see the detailed result of a submission by using two tokens on it, one of each + type.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last + one.{% endtrans %} +

+ {% endif %} + {% endif %} + + {% if contest.max_submission_number is not none %} +

+ {% trans submissions=contest.max_submission_number %}You can submit at most {{ submissions }} solutions + during this contest.{% endtrans %} +

+ {% endif %} + + {% if contest.max_user_test_number is not none %} +

+ {% trans user_tests=contest.max_user_test_number %}You can submit at most {{ user_tests }} user tests + during this contest.{% endtrans %} +

+ {% endif %}
+ {% if contest.per_user_time is not none %} +
+
+

+ {# TODO would be very nice to write something like "just for 3 consecutive hours"... #} + {% trans per_user_time=contest.per_user_time|format_timedelta %}Every user is allowed to compete + (i.e. submit solutions) for a uninterrupted time frame of {{ per_user_time }}.{% endtrans %} +

+ +

+ {% if actual_phase == -2 %} + {% trans %}As soon as the contest starts you can choose to start your time frame.{% endtrans %} + {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the + end of the contest, whatever comes first.{% endtrans %} + {% elif actual_phase == -1 %} + {% trans %}By clicking on the button below you can start your time frame.{% endtrans %} + {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the + end of the contest, whatever comes first.{% endtrans %} + {% elif actual_phase == 0 %} + {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame + at {{ start_time }}.{% endtrans %} + {%+ trans %}You can submit solutions until the end of the time frame or until the end of the + contest, whatever comes first.{% endtrans %} + {% elif actual_phase == +1 %} + {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame + at {{ start_time }} and you already finished it.{% endtrans %} + {%+ trans %}There's nothing you can do now.{% endtrans %} + {% elif actual_phase >= +2 %} + {% if participation.starting_time is none %} + {% trans %}You never started your time frame. Now it's too late.{% endtrans %} + {% else %} + {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame + at {{ start_time }} and you already finished it.{% endtrans %} + {% endif %} + {% if actual_phase != +3 %} + {%+ trans %}There's nothing you can do now.{% endtrans %} + {% endif %} + {% endif %} +

+ + {% if actual_phase == -1 %} +
+ {{ xsrf_form_html|safe }} + + +
+ {% endif %} + +
+
+ {% endif %}
-{% endif %} -
-{% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted or (0 <= actual_phase <= 3 and contest.allow_unofficial_submission_before_analysis_mode)%} -

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

+ {% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted or (0 <= actual_phase <= 3 and contest.allow_unofficial_submission_before_analysis_mode)%} +

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

- - - - - - - - - - -{% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} - -{% endif %} - - - -{% set extensions = "[%s]"|format(contest.languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %} -{% for t_iter in contest.tasks %} - - - - - - - - {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} - + + + + + + + + + + {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} + + {% endif %} + + + + {% set extensions = + "[%s]"|format(contest.languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %} + {% for t_iter in contest.tasks %} + + + + + + + + + {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} + + {% endif %} + + {% endfor %} + +
{% trans %}Task{% endtrans %}{% trans %}Name{% endtrans %}{% trans %}Time limit{% endtrans %}{% trans %}Memory limit{% endtrans %}{% trans %}Type{% endtrans %}{% trans %}Files{% endtrans %}{% trans %}Tokens{% endtrans %}
{{ t_iter.name }}{{ t_iter.title }} - {% if t_iter.active_dataset.time_limit is not none %} - {{ t_iter.active_dataset.time_limit|format_duration(length="long") }} - {% else %} - {% trans %}N/A{% endtrans %} - {% endif %} - - {% if t_iter.active_dataset.memory_limit is not none %} - {{ t_iter.active_dataset.memory_limit|format_size }} - {% else %} - {% trans %}N/A{% endtrans %} - {% endif %} - {{ get_task_type(dataset=t_iter.active_dataset).name }}{{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }} - {% if t_iter.token_mode == TOKEN_MODE_FINITE or t_iter.token_mode == TOKEN_MODE_INFINITE %} - {% trans %}Yes{% endtrans %} - {% else %} - {% trans %}No{% endtrans %} - {% endif %} -
{% trans %}Score{% endtrans %}{% trans %}Task{% endtrans %}{% trans %}Name{% endtrans %}{% trans %}Time limit{% endtrans %}{% trans %}Memory limit{% endtrans %}{% trans %}Type{% endtrans %}{% trans %}Files{% endtrans %}{% trans %}Tokens{% endtrans %}
+ {{ task_scores[t_iter.id][2] }}{{ t_iter.name }}{{ t_iter.title }} + {% if t_iter.active_dataset.time_limit is not none %} + {{ t_iter.active_dataset.time_limit|format_duration(length="long") }} + {% else %} + {% trans %}N/A{% endtrans %} + {% endif %} + + {% if t_iter.active_dataset.memory_limit is not none %} + {{ t_iter.active_dataset.memory_limit|format_size }} + {% else %} + {% trans %}N/A{% endtrans %} + {% endif %} + {{ get_task_type(dataset=t_iter.active_dataset).name }}{{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }} + {% if t_iter.token_mode == TOKEN_MODE_FINITE or t_iter.token_mode == TOKEN_MODE_INFINITE %} + {% trans %}Yes{% endtrans %} + {% else %} + {% trans %}No{% endtrans %} + {% endif %} +
{% endif %} - -{% endfor %} - - -{% endif %}
{% endblock core %} From 4ae18e9a95083ce13ea5d4dc28380aaf3609ca66 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Fri, 1 Aug 2025 17:22:53 +0700 Subject: [PATCH 02/20] Restore Formatting, Make it toggleable --- cms/db/contest.py | 3 + cms/server/admin/handlers/contest.py | 1 + cms/server/admin/templates/contest.html | 9 + cms/server/contest/handlers/main.py | 37 +- cms/server/contest/templates/overview.html | 458 ++++++++++----------- cmscontrib/updaters/update_from_1.5.sql | 3 + 6 files changed, 258 insertions(+), 253 deletions(-) diff --git a/cms/db/contest.py b/cms/db/contest.py index c245a3534f..d3c0c231d5 100644 --- a/cms/db/contest.py +++ b/cms/db/contest.py @@ -108,6 +108,9 @@ class Contest(Base): nullable=False, default=False) + # Whether to show task scores in the overview page + show_task_scores_in_overview: bool = Column(Boolean, nullable=False, default=True) + # Whether to prevent hidden participations to log in. block_hidden_participations: bool = Column( Boolean, diff --git a/cms/server/admin/handlers/contest.py b/cms/server/admin/handlers/contest.py index 1a4c8e8ea6..1038c6ff51 100644 --- a/cms/server/admin/handlers/contest.py +++ b/cms/server/admin/handlers/contest.py @@ -97,6 +97,7 @@ def post(self, contest_id: str): self.get_bool(attrs, "allow_questions") self.get_bool(attrs, "allow_user_tests") self.get_bool(attrs, "allow_unofficial_submission_before_analysis_mode") + self.get_bool(attrs, "show_task_scores_in_overview") self.get_bool(attrs, "block_hidden_participations") self.get_bool(attrs, "allow_password_authentication") self.get_bool(attrs, "allow_registration") diff --git a/cms/server/admin/templates/contest.html b/cms/server/admin/templates/contest.html index 43f57c0b60..e9cfbdfa58 100644 --- a/cms/server/admin/templates/contest.html +++ b/cms/server/admin/templates/contest.html @@ -90,6 +90,15 @@

Contest configuration

+ + + + + + + + +

Logging in

diff --git a/cms/server/contest/handlers/main.py b/cms/server/contest/handlers/main.py index df6c6f930d..dcbd2bf3bd 100644 --- a/cms/server/contest/handlers/main.py +++ b/cms/server/contest/handlers/main.py @@ -107,19 +107,30 @@ def render_params(self): ret["participation"] = participation - # Compute public scores for all tasks - task_scores = {} - for task in self.contest.tasks: - score_type = task.active_dataset.score_type_object - max_public_score = round( - score_type.max_public_score, task.score_precision) - public_score, _ = task_score( - participation, task, public=True, rounded=True) - public_score = round(public_score, task.score_precision) - task_scores[task.id] = (public_score, - max_public_score, - score_type.format_score(public_score, score_type.max_public_score, None, task.score_precision, translation=self.translation)) - ret["task_scores"] = task_scores + # Compute public scores for all tasks only if they will be shown + if self.contest.show_task_scores_in_overview: + task_scores = {} + for task in self.contest.tasks: + score_type = task.active_dataset.score_type_object + max_public_score = round( + score_type.max_public_score, task.score_precision + ) + public_score, _ = task_score( + participation, task, public=True, rounded=True + ) + public_score = round(public_score, task.score_precision) + task_scores[task.id] = ( + public_score, + max_public_score, + score_type.format_score( + public_score, + score_type.max_public_score, + None, + task.score_precision, + translation=self.translation, + ), + ) + ret["task_scores"] = task_scores return ret diff --git a/cms/server/contest/templates/overview.html b/cms/server/contest/templates/overview.html index d2c31c81d6..170658946e 100644 --- a/cms/server/contest/templates/overview.html +++ b/cms/server/contest/templates/overview.html @@ -2,211 +2,188 @@ {% set page = "overview" %} -{% block additional_js %} - -{% endblock additional_js %} - {% block core %}
- - -

{% trans %}General information{% endtrans %}

-
-
-

- {% if phase == -1 %} - {% trans %}The contest hasn't started yet.{% endtrans %} -

-

- {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, - stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} - The contest will start at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} - {% elif phase == 0 %} - {% trans %}The contest is currently running.{% endtrans %} -

-

- {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, - stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} - The contest started at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} - {% elif phase >= +1 %} - {% trans %}The contest has already ended.{% endtrans %} -

-

- {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, - stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} - The contest started at {{ start_time }} and ended at {{ stop_time }}. - {% endtrans %} - {% endif %} -

- {% if contest.analysis_enabled %} -

- {% if phase == +1 %} - {% trans %}The analysis mode hasn't started yet.{% endtrans %} -

-

- {% trans start_time=contest.analysis_start|format_datetime_smart, - stop_time=contest.analysis_stop|format_datetime_smart %} - The analysis mode will start at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} - {% elif phase == +2 %} - {% trans %}The analysis mode is currently running.{% endtrans %} -

-

- {% trans start_time=contest.analysis_start|format_datetime_smart, - stop_time=contest.analysis_stop|format_datetime_smart %} - The analysis mode started at {{ start_time }} and will end at {{ stop_time }}. - {% endtrans %} - {% elif phase == +3 %} - {% trans %}The analysis mode has already ended.{% endtrans %} -

-

- {% trans start_time=contest.analysis_start|format_datetime_smart, - stop_time=contest.analysis_stop|format_datetime_smart %} - The analysis mode started at {{ start_time }} and ended at {{ stop_time }}. - {% endtrans %} - {% endif %} -

- - {% endif %} - - - - {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} - {% if tokens_contest == TOKEN_MODE_INFINITE and tokens_tasks == TOKEN_MODE_INFINITE %} -

- {% trans %}You have an infinite number of tokens.{% endtrans %} -

- -

- {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last - one.{% endtrans %} -

- {% elif tokens_contest == TOKEN_MODE_INFINITE %} -

- {% trans %}You have a distinct set of tokens for each task.{% endtrans %} - {%+ trans type_pl=_("tokens") %}You can find the rules for the {{ type_pl }} on each task's description - page.{% endtrans %} -

- -

- {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last - one.{% endtrans %} -

- {% elif tokens_tasks == TOKEN_MODE_INFINITE %} -

- {% trans %}You have a set of tokens shared among all tasks.{% endtrans %} - {{ contest|extract_token_params|format_token_rules }} -

+ -

- {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last - one.{% endtrans %} -

- {% else %} -

- {% trans %}You have two types of tokens: a set of contest-tokens shared among all tasks and a - distinct set of task-tokens for each task.{% endtrans %} - {{ contest|extract_token_params|format_token_rules(t_type="contest") }} - {% trans type_pl=_("task-tokens") %}You can find the rules for the {{ type_pl }} on each task's - description page.{% endtrans %} -

+

{% trans %}General information{% endtrans %}

+
+
+

+{% if phase == -1 %} + {% trans %}The contest hasn't started yet.{% endtrans %} +

+

+ {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, + stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} + The contest will start at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} +{% elif phase == 0 %} + {% trans %}The contest is currently running.{% endtrans %} +

+

+ {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, + stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} + The contest started at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} +{% elif phase >= +1 %} + {% trans %}The contest has already ended.{% endtrans %} +

+

+ {% trans start_time=(contest.start + participation.delay_time)|format_datetime_smart, + stop_time=(contest.stop + participation.delay_time + participation.extra_time)|format_datetime_smart %} + The contest started at {{ start_time }} and ended at {{ stop_time }}. + {% endtrans %} +{% endif %} +

+{% if contest.analysis_enabled %} +

+ {% if phase == +1 %} + {% trans %}The analysis mode hasn't started yet.{% endtrans %} +

+

+ {% trans start_time=contest.analysis_start|format_datetime_smart, + stop_time=contest.analysis_stop|format_datetime_smart %} + The analysis mode will start at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} + {% elif phase == +2 %} + {% trans %}The analysis mode is currently running.{% endtrans %} +

+

+ {% trans start_time=contest.analysis_start|format_datetime_smart, + stop_time=contest.analysis_stop|format_datetime_smart %} + The analysis mode started at {{ start_time }} and will end at {{ stop_time }}. + {% endtrans %} + {% elif phase == +3 %} + {% trans %}The analysis mode has already ended.{% endtrans %} +

+

+ {% trans start_time=contest.analysis_start|format_datetime_smart, + stop_time=contest.analysis_stop|format_datetime_smart %} + The analysis mode started at {{ start_time }} and ended at {{ stop_time }}. + {% endtrans %} + {% endif %} +

+ +{% endif %} + + + +{% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} + {% if tokens_contest == TOKEN_MODE_INFINITE and tokens_tasks == TOKEN_MODE_INFINITE %} +

+ {% trans %}You have an infinite number of tokens.{% endtrans %} +

+ +

+ {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} +

+ {% elif tokens_contest == TOKEN_MODE_INFINITE %} +

+ {% trans %}You have a distinct set of tokens for each task.{% endtrans %} + {%+ trans type_pl=_("tokens") %}You can find the rules for the {{ type_pl }} on each task's description page.{% endtrans %} +

+ +

+ {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} +

+ {% elif tokens_tasks == TOKEN_MODE_INFINITE %} +

+ {% trans %}You have a set of tokens shared among all tasks.{% endtrans %} + {{ contest|extract_token_params|format_token_rules }} +

+ +

+ {% trans %}You can see the detailed result of a submission by using a token on it.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} +

+ {% else %} +

+ {% trans %}You have two types of tokens: a set of contest-tokens shared among all tasks and a distinct set of task-tokens for each task.{% endtrans %} + {{ contest|extract_token_params|format_token_rules(t_type="contest") }} + {% trans type_pl=_("task-tokens") %}You can find the rules for the {{ type_pl }} on each task's description page.{% endtrans %} +

+ +

+ {% trans %}You can see the detailed result of a submission by using two tokens on it, one of each type.{% endtrans %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %} +

+ {% endif %} +{% endif %} -

- {% trans %}You can see the detailed result of a submission by using two tokens on it, one of each - type.{% endtrans %} - {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last - one.{% endtrans %} -

- {% endif %} - {% endif %} +{% if contest.max_submission_number is not none %} +

+ {% trans submissions=contest.max_submission_number %}You can submit at most {{ submissions }} solutions during this contest.{% endtrans %} +

+{% endif %} - {% if contest.max_submission_number is not none %} -

- {% trans submissions=contest.max_submission_number %}You can submit at most {{ submissions }} solutions - during this contest.{% endtrans %} -

- {% endif %} +{% if contest.max_user_test_number is not none %} +

+ {% trans user_tests=contest.max_user_test_number %}You can submit at most {{ user_tests }} user tests during this contest.{% endtrans %} +

+{% endif %} - {% if contest.max_user_test_number is not none %} -

- {% trans user_tests=contest.max_user_test_number %}You can submit at most {{ user_tests }} user tests - during this contest.{% endtrans %} +

+{% if contest.per_user_time is not none %} +
+
+

+ {# TODO would be very nice to write something like "just for 3 consecutive hours"... #} + {% trans per_user_time=contest.per_user_time|format_timedelta %}Every user is allowed to compete (i.e. submit solutions) for a uninterrupted time frame of {{ per_user_time }}.{% endtrans %} +

+ +

+ {% if actual_phase == -2 %} + {% trans %}As soon as the contest starts you can choose to start your time frame.{% endtrans %} + {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the end of the contest, whatever comes first.{% endtrans %} + {% elif actual_phase == -1 %} + {% trans %}By clicking on the button below you can start your time frame.{% endtrans %} + {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the end of the contest, whatever comes first.{% endtrans %} + {% elif actual_phase == 0 %} + {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame at {{ start_time }}.{% endtrans %} + {%+ trans %}You can submit solutions until the end of the time frame or until the end of the contest, whatever comes first.{% endtrans %} + {% elif actual_phase == +1 %} + {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame at {{ start_time }} and you already finished it.{% endtrans %} + {%+ trans %}There's nothing you can do now.{% endtrans %} + {% elif actual_phase >= +2 %} + {% if participation.starting_time is none %} + {% trans %}You never started your time frame. Now it's too late.{% endtrans %} + {% else %} + {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame at {{ start_time }} and you already finished it.{% endtrans %} + {% endif %} + {% if actual_phase != +3 %} + {%+ trans %}There's nothing you can do now.{% endtrans %} + {% endif %} + {% endif %}

- {% endif %} - -
- {% if contest.per_user_time is not none %} -
-
-

- {# TODO would be very nice to write something like "just for 3 consecutive hours"... #} - {% trans per_user_time=contest.per_user_time|format_timedelta %}Every user is allowed to compete - (i.e. submit solutions) for a uninterrupted time frame of {{ per_user_time }}.{% endtrans %} -

-

- {% if actual_phase == -2 %} - {% trans %}As soon as the contest starts you can choose to start your time frame.{% endtrans %} - {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the - end of the contest, whatever comes first.{% endtrans %} - {% elif actual_phase == -1 %} - {% trans %}By clicking on the button below you can start your time frame.{% endtrans %} - {%+ trans %}Once you start, you can submit solutions until the end of the time frame or until the - end of the contest, whatever comes first.{% endtrans %} - {% elif actual_phase == 0 %} - {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame - at {{ start_time }}.{% endtrans %} - {%+ trans %}You can submit solutions until the end of the time frame or until the end of the - contest, whatever comes first.{% endtrans %} - {% elif actual_phase == +1 %} - {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame - at {{ start_time }} and you already finished it.{% endtrans %} - {%+ trans %}There's nothing you can do now.{% endtrans %} - {% elif actual_phase >= +2 %} - {% if participation.starting_time is none %} - {% trans %}You never started your time frame. Now it's too late.{% endtrans %} - {% else %} - {% trans start_time=participation.starting_time|format_datetime_smart %}You started your time frame - at {{ start_time }} and you already finished it.{% endtrans %} - {% endif %} - {% if actual_phase != +3 %} - {%+ trans %}There's nothing you can do now.{% endtrans %} - {% endif %} - {% endif %} -

- - {% if actual_phase == -1 %} -
- {{ xsrf_form_html|safe }} - - -
- {% endif %} + {% if actual_phase == -1 %} +
+ {{ xsrf_form_html|safe }} + + +
+ {% endif %} -
- {% endif %}
+{% endif %} +
- {% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted or (0 <= actual_phase <= 3 and contest.allow_unofficial_submission_before_analysis_mode)%} -

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

+{% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted or (0 <= actual_phase <= 3 and contest.allow_unofficial_submission_before_analysis_mode)%} +

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

- - - - - - - - - - - - {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} - - {% endif %} - - - - {% set extensions = - "[%s]"|format(contest.languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %} - {% for t_iter in contest.tasks %} - - - - - - - - - {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} - - {% endif %} - - {% endfor %} - -
{% trans %}Score{% endtrans %}{% trans %}Task{% endtrans %}{% trans %}Name{% endtrans %}{% trans %}Time limit{% endtrans %}{% trans %}Memory limit{% endtrans %}{% trans %}Type{% endtrans %}{% trans %}Files{% endtrans %}{% trans %}Tokens{% endtrans %}
- {{ task_scores[t_iter.id][2] }}{{ t_iter.name }}{{ t_iter.title }} - {% if t_iter.active_dataset.time_limit is not none %} - {{ t_iter.active_dataset.time_limit|format_duration(length="long") }} - {% else %} - {% trans %}N/A{% endtrans %} - {% endif %} - - {% if t_iter.active_dataset.memory_limit is not none %} - {{ t_iter.active_dataset.memory_limit|format_size }} - {% else %} - {% trans %}N/A{% endtrans %} - {% endif %} - {{ get_task_type(dataset=t_iter.active_dataset).name }}{{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }} - {% if t_iter.token_mode == TOKEN_MODE_FINITE or t_iter.token_mode == TOKEN_MODE_INFINITE %} - {% trans %}Yes{% endtrans %} - {% else %} - {% trans %}No{% endtrans %} - {% endif %} -
+ + +{% if contest.show_task_scores_in_overview %} + {% trans %}Score{% endtrans %} +{% endif %} + {% trans %}Task{% endtrans %} + {% trans %}Name{% endtrans %} + {% trans %}Time limit{% endtrans %} + {% trans %}Memory limit{% endtrans %} + {% trans %}Type{% endtrans %} + {% trans %}Files{% endtrans %} +{% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} + {% trans %}Tokens{% endtrans %} +{% endif %} + + + +{% set extensions = "[%s]"|format(contest.languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %} +{% for t_iter in contest.tasks %} + +{% if contest.show_task_scores_in_overview and task_scores is defined %} + {{ task_scores[t_iter.id][2] }} +{% endif %} + {{ t_iter.name }} + {{ t_iter.title }} + + {% if t_iter.active_dataset.time_limit is not none %} + {{ t_iter.active_dataset.time_limit|format_duration(length="long") }} + {% else %} + {% trans %}N/A{% endtrans %} + {% endif %} + + + {% if t_iter.active_dataset.memory_limit is not none %} + {{ t_iter.active_dataset.memory_limit|format_size }} + {% else %} + {% trans %}N/A{% endtrans %} + {% endif %} + + {{ get_task_type(dataset=t_iter.active_dataset).name }} + {{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }} + {% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %} + + {% if t_iter.token_mode == TOKEN_MODE_FINITE or t_iter.token_mode == TOKEN_MODE_INFINITE %} + {% trans %}Yes{% endtrans %} + {% else %} + {% trans %}No{% endtrans %} + {% endif %} + {% endif %} + +{% endfor %} + + +{% endif %}
{% endblock core %} diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index c22b736c6a..10ddf0b4ce 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/1476 +ALTER TABLE contests ADD COLUMN show_task_scores_in_overview boolean NOT NULL DEFAULT true; + COMMIT; From cbe5f8ea3a9ea4cce6a2bc88acd56299bbe9d128 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Fri, 1 Aug 2025 17:37:57 +0700 Subject: [PATCH 03/20] Fix color not displaying --- cms/server/contest/templates/overview.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/server/contest/templates/overview.html b/cms/server/contest/templates/overview.html index 170658946e..850683da08 100644 --- a/cms/server/contest/templates/overview.html +++ b/cms/server/contest/templates/overview.html @@ -182,7 +182,7 @@

{% trans %}General information{% endtrans %}

{% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted or (0 <= actual_phase <= 3 and contest.allow_unofficial_submission_before_analysis_mode)%}

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

- +
-{% if contest.show_task_scores_in_overview %} +{% if contest.show_task_scores_in_overview and task_scores is defined%} {% endif %} From b84d5f8c6e2babadf606e90091dd1dbcdeb41bca Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 6 Apr 2026 01:03:58 +0800 Subject: [PATCH 12/20] Add sidebar task scores for contests Introduce an option to display per-task public scores in the contest sidebar. Adds a new DB column (show_task_scores_in_sidebar) and admin checkbox to toggle it, includes CSS badge styling and template changes to render the score next to each task. Backend changes add helpers to load participations with the relations required for scoring and to compute formatted public task scores (only computed when needed and hiding tasks with zero public max in the sidebar). Includes SQL updater to migrate existing databases. --- cms/db/contest.py | 3 + cms/server/admin/handlers/contest.py | 1 + cms/server/admin/templates/contest.html | 9 +++ cms/server/contest/handlers/contest.py | 68 +++++++++++++++++++++++ cms/server/contest/handlers/main.py | 51 ++++------------- cms/server/contest/static/cws_style.css | 11 ++++ cms/server/contest/templates/contest.html | 7 ++- cmscontrib/updaters/update_from_1.5.sql | 4 ++ 8 files changed, 112 insertions(+), 42 deletions(-) diff --git a/cms/db/contest.py b/cms/db/contest.py index e470e31e8b..c38c43bfb7 100644 --- a/cms/db/contest.py +++ b/cms/db/contest.py @@ -111,6 +111,9 @@ class Contest(Base): # Whether to show task scores in the overview page show_task_scores_in_overview: bool = Column(Boolean, nullable=False, default=True) + # Whether to show task scores in the sidebar task list. + show_task_scores_in_sidebar: bool = Column(Boolean, nullable=False, default=False) + # Whether to prevent hidden participations to log in. block_hidden_participations: bool = Column( Boolean, diff --git a/cms/server/admin/handlers/contest.py b/cms/server/admin/handlers/contest.py index 1038c6ff51..d09b01ce33 100644 --- a/cms/server/admin/handlers/contest.py +++ b/cms/server/admin/handlers/contest.py @@ -98,6 +98,7 @@ def post(self, contest_id: str): self.get_bool(attrs, "allow_user_tests") self.get_bool(attrs, "allow_unofficial_submission_before_analysis_mode") self.get_bool(attrs, "show_task_scores_in_overview") + self.get_bool(attrs, "show_task_scores_in_sidebar") self.get_bool(attrs, "block_hidden_participations") self.get_bool(attrs, "allow_password_authentication") self.get_bool(attrs, "allow_registration") diff --git a/cms/server/admin/templates/contest.html b/cms/server/admin/templates/contest.html index e9cfbdfa58..0d49bf1401 100644 --- a/cms/server/admin/templates/contest.html +++ b/cms/server/admin/templates/contest.html @@ -99,6 +99,15 @@

Contest configuration

+ + + + diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py index 7ea473d2c9..88389449c6 100644 --- a/cms/server/contest/handlers/contest.py +++ b/cms/server/contest/handlers/contest.py @@ -48,9 +48,11 @@ collections.MutableMapping = collections.abc.MutableMapping import tornado.web +from sqlalchemy.orm import joinedload from cms import config, TOKEN_MODE_MIXED from cms.db import Contest, Submission, Task, UserTest +from cms.grading.scoring import task_score from cms.locale import filter_language_codes from cms.server import FileHandlerMixin from cms.server.contest.authentication import authenticate_request @@ -193,6 +195,56 @@ def get_current_user(self) -> Participation | None: self.impersonated_by_admin = impersonated return participation + def _load_participation_for_scores( + self, participation: Participation + ) -> Participation | None: + """Load participation with relationships needed for task score computation.""" + return ( + self.sql_session.query(Participation) + .filter(Participation.id == participation.id) + .options( + joinedload(Participation.contest) + .joinedload(Contest.tasks) + .joinedload(Task.active_dataset), + joinedload(Participation.submissions).joinedload(Submission.token), + joinedload(Participation.submissions).joinedload(Submission.results), + ) + .first() + ) + + def _compute_public_task_scores( + self, + participation: Participation, + *, + hide_zero_max_public: bool, + ) -> dict[int, tuple[float, float, str]]: + """Compute per-task public scores for the given participation.""" + task_scores: dict[int, tuple[float, float, str]] = {} + for task in participation.contest.tasks: + score_type = task.active_dataset.score_type_object + max_public_score = round( + score_type.max_public_score, task.score_precision + ) + + if hide_zero_max_public and max_public_score <= 0: + continue + + public_score, _ = task_score( + participation, task, public=True, rounded=True + ) + task_scores[task.id] = ( + public_score, + max_public_score, + score_type.format_score( + public_score, + score_type.max_public_score, + None, + task.score_precision, + translation=self.translation, + ), + ) + return task_scores + def render_params(self): ret = super().render_params() @@ -230,6 +282,22 @@ def render_params(self): # set the timezone used to format timestamps ret["timezone"] = get_timezone(participation.user, self.contest) + if self.contest.show_task_scores_in_sidebar and ( + ret["actual_phase"] >= 0 or participation.unrestricted + ): + loaded_participation = self._load_participation_for_scores(participation) + if loaded_participation is not None: + # Keep references synchronized with the fully loaded objects. + participation = loaded_participation + self.contest = participation.contest + ret["contest"] = self.contest + ret["participation"] = participation + ret["user"] = participation.user + ret["sidebar_task_scores"] = self._compute_public_task_scores( + participation, + hide_zero_max_public=True, + ) + # some information about token configuration ret["tokens_contest"] = self.contest.token_mode diff --git a/cms/server/contest/handlers/main.py b/cms/server/contest/handlers/main.py index ba4e2d272d..a76d78f073 100644 --- a/cms/server/contest/handlers/main.py +++ b/cms/server/contest/handlers/main.py @@ -48,13 +48,11 @@ import tornado.web from sqlalchemy.orm.exc import NoResultFound -from sqlalchemy.orm import joinedload from cms import config -from cms.db import User, Participation, Team, Submission, Task +from cms.db import User, Participation, Team from cms.grading.languagemanager import get_language from cms.grading.steps import COMPILATION_MESSAGES, EVALUATION_MESSAGES -from cms.grading.scoring import task_score from cms.server import multi_contest from cms.server.contest.authentication import validate_login from cms.server.contest.communication import get_communications @@ -84,51 +82,22 @@ def render_params(self): ret = super().render_params() if self.current_user is not None: - # This massive joined load gets all the information which we will need - participation = ( - self.sql_session.query(Participation) - .filter(Participation.id == self.current_user.id) - .options( - joinedload(Participation.user), - joinedload(Participation.contest) - .joinedload(Contest.tasks) - .joinedload(Task.active_dataset), - joinedload(Participation.submissions).joinedload(Submission.token), - joinedload(Participation.submissions).joinedload( - Submission.results - ), - ) - .first() - ) + participation = self._load_participation_for_scores(self.current_user) + if participation is None: + return ret self.contest = participation.contest - # Ensure the template sees this fully-loaded version + # Ensure the template sees this fully-loaded version. ret["contest"] = self.contest ret["participation"] = participation + ret["user"] = participation.user # Compute public scores for all tasks only if they will be shown if self.contest.show_task_scores_in_overview: - task_scores = {} - for task in self.contest.tasks: - score_type = task.active_dataset.score_type_object - max_public_score = round( - score_type.max_public_score, task.score_precision - ) - public_score, _ = task_score( - participation, task, public=True, rounded=True - ) - task_scores[task.id] = ( - public_score, - max_public_score, - score_type.format_score( - public_score, - score_type.max_public_score, - None, - task.score_precision, - translation=self.translation, - ), - ) - ret["task_scores"] = task_scores + ret["task_scores"] = self._compute_public_task_scores( + participation, + hide_zero_max_public=False, + ) return ret diff --git a/cms/server/contest/static/cws_style.css b/cms/server/contest/static/cws_style.css index 4bfefa4dfb..24d107cc76 100644 --- a/cms/server/contest/static/cws_style.css +++ b/cms/server/contest/static/cws_style.css @@ -462,6 +462,17 @@ td.token_rules p:last-child { background-color: hsla(120, 100%, 50%, 0.4); } +.nav-list .nav-header .task_score_badge { + float: right; + margin-right: 5px; + padding: 1px 6px; + border-radius: 4px; + font-size: 10px; + line-height: 14px; + font-weight: bold; + color: #333; + text-transform: none; +} /*** Submit a solution */ #submit_solution { diff --git a/cms/server/contest/templates/contest.html b/cms/server/contest/templates/contest.html index ba514771ba..5193fb5a0c 100644 --- a/cms/server/contest/templates/contest.html +++ b/cms/server/contest/templates/contest.html @@ -180,7 +180,12 @@

{% if actual_phase >= 0 or participation.unrestricted %} {% for t_iter in contest.tasks %} {% trans %}Statement{% endtrans %} diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index 4a55396bde..5f7e701bde 100644 --- a/cmscontrib/updaters/update_from_1.5.sql +++ b/cmscontrib/updaters/update_from_1.5.sql @@ -52,4 +52,8 @@ ALTER TABLE public.tasks ADD COLUMN allowed_languages varchar[]; -- https://github.com/cms-dev/cms/pull/1583 DROP TABLE public.printjobs; +-- Sidebar task score toggle +ALTER TABLE contests ADD COLUMN show_task_scores_in_sidebar boolean NOT NULL DEFAULT false; +ALTER TABLE contests ALTER COLUMN show_task_scores_in_sidebar DROP DEFAULT; + COMMIT; From 56a4f9667c4a4f47c303e9cc7290a09daa103b2b Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 6 Apr 2026 01:04:52 +0800 Subject: [PATCH 13/20] Update update_from_1.5.sql --- cmscontrib/updaters/update_from_1.5.sql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index 5f7e701bde..604a6a437d 100644 --- a/cmscontrib/updaters/update_from_1.5.sql +++ b/cmscontrib/updaters/update_from_1.5.sql @@ -44,7 +44,9 @@ ALTER TABLE user_test_results DROP COLUMN evaluation_sandbox; -- https://github.com/cms-dev/cms/pull/1476 ALTER TABLE contests ADD COLUMN show_task_scores_in_overview boolean NOT NULL DEFAULT true; +ALTER TABLE contests ADD COLUMN show_task_scores_in_sidebar boolean NOT NULL DEFAULT true; ALTER TABLE contests ALTER COLUMN show_task_scores_in_overview DROP DEFAULT; +ALTER TABLE contests ALTER COLUMN show_task_scores_in_sidebar DROP DEFAULT; -- https://github.com/cms-dev/cms/pull/1486 ALTER TABLE public.tasks ADD COLUMN allowed_languages varchar[]; @@ -52,8 +54,5 @@ ALTER TABLE public.tasks ADD COLUMN allowed_languages varchar[]; -- https://github.com/cms-dev/cms/pull/1583 DROP TABLE public.printjobs; --- Sidebar task score toggle -ALTER TABLE contests ADD COLUMN show_task_scores_in_sidebar boolean NOT NULL DEFAULT false; -ALTER TABLE contests ALTER COLUMN show_task_scores_in_sidebar DROP DEFAULT; COMMIT; From e4a12953dcd4d9fa22737250f31f7bc7173f113c Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 6 Apr 2026 01:21:57 +0800 Subject: [PATCH 14/20] Show tokened task scores in sidebar Add logic to display tokened/total task scores in the sidebar when a token has been played on a task or when in analysis phase. Introduce _compute_sidebar_task_scores in the contest handler and switch render_params to use it (passing actual_phase) so sidebar values use tokened scores where appropriate while still hiding tasks with no public score. Templates updated: add data-task-name attribute to task nav headers and add JS to initialize sidebar state, update sidebar badges dynamically (sidebar_use_tokened_score, update_sidebar_task_score) and switch displayed score to tokened totals when submissions reveal full scores. This enables correct initial rendering and real-time updates of sidebar task scores based on token usage and scoring visibility. --- cms/server/contest/handlers/contest.py | 67 ++++++++++++++++++- cms/server/contest/templates/contest.html | 2 +- .../contest/templates/task_submissions.html | 43 ++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py index 88389449c6..eb46301596 100644 --- a/cms/server/contest/handlers/contest.py +++ b/cms/server/contest/handlers/contest.py @@ -245,6 +245,69 @@ def _compute_public_task_scores( ) return task_scores + def _compute_sidebar_task_scores( + self, + participation: Participation, + *, + actual_phase: int, + ) -> dict[int, tuple[float, float, str]]: + """Compute per-task scores for the sidebar. + + By default the sidebar shows public scores. If a token has been played + on a task (or we're in analysis mode), it shows the tokened/total score + for that task instead. + """ + task_scores: dict[int, tuple[float, float, str]] = {} + + for task in participation.contest.tasks: + score_type = task.active_dataset.score_type_object + + has_tokened_submission = any( + s.official and s.task_id == task.id and s.tokened() + for s in participation.submissions + ) + show_tokened_total = ( + score_type.max_public_score < score_type.max_score + and (has_tokened_submission or actual_phase == 3) + ) + + if show_tokened_total: + score_value, _ = task_score( + participation, task, only_tokened=True, rounded=True + ) + max_score_value = round(score_type.max_score, task.score_precision) + score_message = score_type.format_score( + score_value, + score_type.max_score, + None, + task.score_precision, + translation=self.translation, + ) + else: + max_public_score = round( + score_type.max_public_score, task.score_precision + ) + + # Do not show a sidebar score if there is no public score. + if max_public_score <= 0: + continue + + score_value, _ = task_score( + participation, task, public=True, rounded=True + ) + max_score_value = max_public_score + score_message = score_type.format_score( + score_value, + score_type.max_public_score, + None, + task.score_precision, + translation=self.translation, + ) + + task_scores[task.id] = (score_value, max_score_value, score_message) + + return task_scores + def render_params(self): ret = super().render_params() @@ -293,9 +356,9 @@ def render_params(self): ret["contest"] = self.contest ret["participation"] = participation ret["user"] = participation.user - ret["sidebar_task_scores"] = self._compute_public_task_scores( + ret["sidebar_task_scores"] = self._compute_sidebar_task_scores( participation, - hide_zero_max_public=True, + actual_phase=ret["actual_phase"], ) # some information about token configuration diff --git a/cms/server/contest/templates/contest.html b/cms/server/contest/templates/contest.html index 5193fb5a0c..566f48adfb 100644 --- a/cms/server/contest/templates/contest.html +++ b/cms/server/contest/templates/contest.html @@ -179,7 +179,7 @@

{% if actual_phase >= 0 or participation.unrestricted %} {% for t_iter in contest.tasks %} - diff --git a/cms/server/contest/templates/task_submissions.html b/cms/server/contest/templates/task_submissions.html index 82ede7ef07..913d84fef4 100644 --- a/cms/server/contest/templates/task_submissions.html +++ b/cms/server/contest/templates/task_submissions.html @@ -132,9 +132,10 @@ return; } - badge.removeClass('score_0 score_0_100 score_100'); + badge.removeClass('undefined score_0 score_0_100 score_100'); badge.addClass(get_score_class(task_score, max_score)); badge.text(task_score_message); + badge.show(); }; update_scores = function (submission_id, data) { From f36ca31ee2b24aed1990acf03b057583bcf1be2a Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 6 Apr 2026 02:28:57 +0800 Subject: [PATCH 19/20] Update contest.html --- cms/server/contest/templates/contest.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/server/contest/templates/contest.html b/cms/server/contest/templates/contest.html index 3da6804d3b..9604fb51b7 100644 --- a/cms/server/contest/templates/contest.html +++ b/cms/server/contest/templates/contest.html @@ -187,7 +187,7 @@

{{ sidebar_task_scores[t_iter.id][2] }} {% else %} - {% trans %}N/A{% endtrans %} + 0 / 0 {% endif %} {% endif %} From ba352bb4536c5705dd19c8c4f623b8988743c091 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Mon, 6 Apr 2026 02:31:38 +0800 Subject: [PATCH 20/20] Update contest.py --- cms/server/contest/handlers/contest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py index 41949790b1..bdadc02abc 100644 --- a/cms/server/contest/handlers/contest.py +++ b/cms/server/contest/handlers/contest.py @@ -48,7 +48,7 @@ collections.MutableMapping = collections.abc.MutableMapping import tornado.web -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, selectinload from cms import config, TOKEN_MODE_MIXED from cms.db import Contest, Submission, Task, UserTest @@ -207,8 +207,8 @@ def _load_participation_for_scores( joinedload(Participation.contest) .joinedload(Contest.tasks) .joinedload(Task.active_dataset), - joinedload(Participation.submissions).joinedload(Submission.token), - joinedload(Participation.submissions).joinedload(Submission.results), + selectinload(Participation.submissions).joinedload(Submission.token), + selectinload(Participation.submissions).joinedload(Submission.results), ) .first() )

{% trans %}Score{% endtrans %}{% trans %}Task{% endtrans %}
+ + + + +

Logging in