Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
bb17b1a
Add a Score counter to task overview
pxsit Jul 23, 2025
6653f15
Merge branch 'main' into add-counter
pxsit Jul 23, 2025
3d62b5d
Merge branch 'main' into add-counter
pxsit Jul 23, 2025
6b52ff7
Merge branch 'main' into add-counter
pxsit Jul 28, 2025
8428046
Merge branch 'main' into add-counter
pxsit Aug 1, 2025
4ae18e9
Restore Formatting, Make it toggleable
pxsit Aug 1, 2025
cbe5f8e
Fix color not displaying
pxsit Aug 1, 2025
8055bf6
Fix
pxsit Aug 3, 2025
fc50113
Fix the Fix
pxsit Aug 4, 2025
251c8b5
Fix the Fix that Fix the Fix
pxsit Aug 4, 2025
527bdea
Merge branch 'main' into add-counter
pxsit Aug 5, 2025
95cc100
Update cws_style.css
pxsit Aug 5, 2025
e60f328
Merge branch 'main' into add-counter
pxsit Aug 5, 2025
13c491e
Add credit for original owner?
pxsit Aug 5, 2025
95349b0
Merge branch 'main' into add-counter
pxsit Aug 6, 2025
28c1ae1
Merge branch 'main' into add-counter
pxsit Aug 6, 2025
e0840f6
Update update_from_1.5.sql
pxsit Aug 6, 2025
a958b48
Merge branch 'main' into add-counter
pxsit Aug 8, 2025
7a3b7b7
Merge branch 'main' into add-counter
pxsit Aug 11, 2025
fa4e7ca
Merge branch 'main' into add-counter
pxsit Aug 15, 2025
4b0bd83
Merge branch 'main' into add-counter
pxsit Aug 16, 2025
d12ba11
Merge branch 'cms-dev:main' into add-counter
pxsit Sep 21, 2025
85d0eb8
Merge branch 'cms-dev:main' into add-counter
pxsit Sep 30, 2025
3181514
Merge branch 'cms-dev:main' into add-counter
pxsit Oct 8, 2025
003a7f9
Merge branch 'cms-dev:main' into add-counter
pxsit Oct 23, 2025
7aac12b
Remove unused PrintJob import from main.py
pxsit Nov 17, 2025
1134711
Merge branch 'main' into add-counter
pxsit Nov 17, 2025
6411ec3
Merge branch 'main' into add-counter
pxsit Jan 1, 2026
44822d6
Optimize contest data loading
pxsit Jan 13, 2026
98a11b7
Merge branch 'main' into add-counter
pxsit Feb 17, 2026
b84d5f8
Add sidebar task scores for contests
pxsit Apr 5, 2026
56a4f96
Update update_from_1.5.sql
pxsit Apr 5, 2026
e4a1295
Show tokened task scores in sidebar
pxsit Apr 5, 2026
7760b48
Enable task scores in sidebar by default
pxsit Apr 5, 2026
b2e8b21
Unify task score computation into _compute_task_scores
pxsit Apr 5, 2026
7271662
Show full scores in analysis mode and minor fixes
pxsit Apr 5, 2026
2ff708e
Reuse preloaded participation and show N/A scores
pxsit Apr 5, 2026
f36ca31
Update contest.html
pxsit Apr 5, 2026
ba352bb
Update contest.py
pxsit Apr 5, 2026
1b0b0a1
Added an option to display task scores in the contest sidebar
pxsit Apr 5, 2026
000e7e8
Merge branch 'main' into add-counter
pxsit Apr 5, 2026
ef00a6a
Merge branch 'main' into add-counter
pxsit Apr 6, 2026
2a975c9
Merge branch 'main' into add-counter
pxsit Apr 8, 2026
ea5ce65
Fix bugs & Optimize
pxsit Apr 8, 2026
9f0157b
Update contest.html
pxsit Apr 8, 2026
44e6a95
Fix bugs
pxsit Apr 8, 2026
888daa9
Update contest.py
pxsit Apr 8, 2026
41b7ad0
Merge branch 'main' into add-counter
pxsit Apr 9, 2026
f1a4ec0
Merge branch 'main' into add-counter
pxsit Apr 9, 2026
a6d8a6e
Merge branch 'main' into add-counter
pxsit Apr 20, 2026
d3a3808
Merge upstream e0e4f8b7 commit
pxsit May 16, 2026
2690129
Fix template get_score_class calls to use new 2-arg signature
pxsit May 16, 2026
bc3ba42
Merge branch 'main' into add-counter
pxsit May 16, 2026
c8428aa
Merge branch 'cms-dev:main' into add-counter
pxsit Jun 5, 2026
a01df8c
Merge branch 'main' into add-counter
pxsit Sep 21, 2026
e045499
Fix small bugs
pxsit Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cms/db/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ 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 show task scores in the sidebar task list.
show_task_scores_in_sidebar: bool = Column(Boolean, nullable=False, default=True)

# Whether to prevent hidden participations to log in.
block_hidden_participations: bool = Column(
Boolean,
Expand Down
3 changes: 3 additions & 0 deletions cms/server/admin/handlers/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# Copyright © 2026 Tobias Lenz <t_lenz94@web.de>
# Copyright © 2026 Chuyang Wang <mail@chuyang-wang.de>
# Copyright © 2026 Jonathan Baumann <Jonathan.Baumann@edu.ruhr-uni-bochum.de>
# Copyright © 2026 Pasit Sangprachathanarak <ouipingpasit@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -106,6 +107,8 @@ 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, "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")
Expand Down
18 changes: 18 additions & 0 deletions cms/server/admin/templates/contest.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ <h1>Contest configuration</h1>
<input type="checkbox" id="allow_unofficial_submission_before_analysis_mode" name="allow_unofficial_submission_before_analysis_mode" {{ "checked" if contest.allow_unofficial_submission_before_analysis_mode else "" }}/>
</td>
</tr>
<tr>
<td>
<span class="info" title="Whether to show task scores in the overview page for contestants."></span>
<label for="show_task_scores_in_overview">Show task scores in overview page</label>
</td>
<td>
<input type="checkbox" id="show_task_scores_in_overview" name="show_task_scores_in_overview" {{ "checked" if contest.show_task_scores_in_overview else "" }}/>
</td>
</tr>
<tr>
<td>
<span class="info" title="Whether to show task scores next to task names in the sidebar for contestants."></span>
<label for="show_task_scores_in_sidebar">Show task scores in sidebar</label>
</td>
<td>
<input type="checkbox" id="show_task_scores_in_sidebar" name="show_task_scores_in_sidebar" {{ "checked" if contest.show_task_scores_in_sidebar else "" }}/>
</td>
</tr>
<tr>
<td>
<span class="info" title="Timezone of the server, used to display start, end times and the current server time to contestants.
Expand Down
76 changes: 75 additions & 1 deletion cms/server/contest/handlers/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@
collections.MutableMapping = collections.abc.MutableMapping

import tornado.web
from sqlalchemy.orm import joinedload, selectinload

from cms import config, TOKEN_MODE_MIXED
from cms.db import Contest, Submission, Task, UserTest
from cms.db import Contest, Dataset, 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
Expand Down Expand Up @@ -193,6 +195,78 @@ 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.user),
joinedload(Participation.contest)
.joinedload(Contest.tasks)
.joinedload(Task.active_dataset)
.selectinload(Dataset.testcases),
selectinload(Participation.submissions).joinedload(Submission.token),
selectinload(Participation.submissions).joinedload(Submission.results),
)
.first()
)

def _compute_task_scores(
self,
participation: Participation,
*,
actual_phase: int,
) -> dict[int, tuple[float, float, str]]:
"""Compute per-task scores for UI task lists.

By default, this 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]] = {}
tokened_task_ids = {
s.task_id for s in participation.submissions if s.official and s.tokened()
}

for task in participation.contest.tasks:
if task.active_dataset is None:
continue
score_type = task.active_dataset.score_type_object

has_tokened_submission = task.id in tokened_task_ids
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=actual_phase != 3)
max_score_value = score_type.max_score
else:
max_score_value = score_type.max_public_score
if max_score_value <= 0:
continue
score_value, _ = task_score(participation, task, public=True)

score_message = score_type.format_score(
score_value, max_score_value, None, translation=self.translation)
task_scores[task.id] = (score_value, max_score_value, score_message)

return task_scores

@functools.cached_property
def task_scores(self) -> dict[int, tuple[float, float, str]]:
"""Load scores only when a template displays them, once per request."""
participation = self._load_participation_for_scores(self.current_user)
if participation is None:
return {}
return self._compute_task_scores(
participation, actual_phase=self.r_params["actual_phase"])

def render_params(self):
ret = super().render_params()

Expand Down
2 changes: 2 additions & 0 deletions cms/server/contest/handlers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com>
# Copyright © 2015-2018 William Di Luigi <williamdiluigi@gmail.com>
# Copyright © 2021 Grace Hawkins <amoomajid99@gmail.com>
# Copyright © 2025 Pasit Sangprachathanarak <ouipingpasit@gmail.com>
# Copyright © 2025 kk@cscmu-cnx
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down
14 changes: 10 additions & 4 deletions cms/server/contest/handlers/tasksubmission.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def get(self, task_name):
public_score, is_public_score_partial = task_score(
participation, task, public=True)
tokened_score, is_tokened_score_partial = task_score(
participation, task, only_tokened=True)
participation, task, only_tokened=self.r_params["actual_phase"] != 3)
# These two should be the same, anyway.
is_score_partial = is_public_score_partial or is_tokened_score_partial

Expand Down Expand Up @@ -207,10 +207,12 @@ def add_task_score(self, participation: Participation, task: Task, data: dict):
task: task for which we want the score.
data: where to put the data; all fields will start with "task",
followed by "public" if referring to the public scores, or
"tokened" if referring to the total score (always limited to
tokened submissions); for both public and tokened, the fields are:
"tokened" if referring to the total score (limited to tokened
submissions during contest, full score in analysis mode); for both
public and tokened, the fields are:
"score" and "score_message"; in addition we have
"task_is_score_partial" as partial info is the same for both.
"task_use_tokened_score" selects the full score for task badges.

"""
# Just to preload all information required to compute the task score.
Expand All @@ -223,7 +225,11 @@ def add_task_score(self, participation: Participation, task: Task, data: dict):
data["task_public_score"], public_score_is_partial = \
task_score(participation, task, public=True)
data["task_tokened_score"], tokened_score_is_partial = \
task_score(participation, task, only_tokened=True)
task_score(participation, task,
only_tokened=self.r_params["actual_phase"] != 3)
data["task_use_tokened_score"] = self.r_params["actual_phase"] == 3 or any(
s.official and s.task_id == task.id and s.tokened()
for s in participation.submissions)
# These two should be the same, anyway.
data["task_score_is_partial"] = \
public_score_is_partial or tokened_score_is_partial
Expand Down
33 changes: 27 additions & 6 deletions cms/server/contest/static/cws_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,21 @@ 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;
}
.nav-list .nav-header .task_score_badge.undefined {
color: #888;
background-color: transparent;
}
/*** Submit a solution */

#submit_solution {
Expand Down Expand Up @@ -559,27 +574,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 {
Comment thread
pxsit marked this conversation as resolved.
Comment thread
pxsit marked this conversation as resolved.
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);
}

Expand Down
16 changes: 14 additions & 2 deletions cms/server/contest/templates/contest.html
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,21 @@ <h3 id="countdown_box">
</a>
</li>
{% if actual_phase >= 0 or participation.unrestricted %}
{% if contest.show_task_scores_in_sidebar %}
{% set sidebar_task_scores = handler.task_scores %}
{% endif %}
{% for t_iter in contest.tasks %}
<li class="nav-header">
{{ t_iter.name }}
<li class="nav-header" data-task-name="{{ t_iter.name }}">
<span>{{ t_iter.name }}</span>
{% if contest.show_task_scores_in_sidebar %}
{% if sidebar_task_scores is defined and t_iter.id in sidebar_task_scores %}
<span class="task_score_badge task_score {{ get_score_class(sidebar_task_scores[t_iter.id][0], sidebar_task_scores[t_iter.id][1]) }}">
{{ sidebar_task_scores[t_iter.id][2] }}
</span>
{% else %}
<span class="task_score_badge task_score undefined">{% trans %}N/A{% endtrans %}</span>
{% endif %}
{% endif %}
</li>
<li{% if page == "task_description" and task == t_iter %} class="active"{% endif %}>
<a href="{{ contest_url("tasks", t_iter.name, "description") }}">{% trans %}Statement{% endtrans %}</a>
Expand Down
21 changes: 17 additions & 4 deletions cms/server/contest/templates/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,12 @@ <h2>{% trans %}General information{% endtrans %}</h2>


{% if actual_phase >= 0 or participation.unrestricted %}
{% if contest.show_task_scores_in_overview %}
{% set task_scores = handler.task_scores %}
{% endif %}
<h2>{% trans %}Task overview{% endtrans %}</h2>

<table class="table table-bordered table-striped">
<table class="main_task_list table table-bordered table-striped">
<!-- <colgroup>
<col class="task"/>
<col class="time_limit"/>
Expand All @@ -193,6 +196,9 @@ <h2>{% trans %}Task overview{% endtrans %}</h2>
</colgroup> -->
<thead>
<tr>
{% if contest.show_task_scores_in_overview and task_scores is defined%}
<th>{% trans %}Score{% endtrans %}</th>
{% endif %}
<th>{% trans %}Task{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Time limit{% endtrans %}</th>
Expand All @@ -209,23 +215,30 @@ <h2>{% trans %}Task overview{% endtrans %}</h2>
{% set task_allowed_languages = t_iter.get_allowed_languages() %}
{% set extensions = "[%s]"|format(task_allowed_languages|map("to_language")|map(attribute="source_extension")|unique|join("|")) %}
<tr>
{% if contest.show_task_scores_in_overview and task_scores is defined %}
{% if t_iter.id in task_scores %}
<td class="public_score {{ get_score_class(task_scores[t_iter.id][0], task_scores[t_iter.id][1]) }}">{{ task_scores[t_iter.id][2] }}</td>
{% else %}
<td class="public_score undefined">{% trans %}N/A{% endtrans %}</td>
{% endif %}
{% endif %}
<th>{{ t_iter.name }}</th>
<td>{{ t_iter.title }}</td>
<td>
{% if t_iter.active_dataset.time_limit is not none %}
{% if t_iter.active_dataset is not none and 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 %}
</td>
<td>
{% if t_iter.active_dataset.memory_limit is not none %}
{% if t_iter.active_dataset is not none and t_iter.active_dataset.memory_limit is not none %}
{{ t_iter.active_dataset.memory_limit|format_size }}
{% else %}
{% trans %}N/A{% endtrans %}
{% endif %}
</td>
<td>{{ get_task_type(dataset=t_iter.active_dataset).name }}</td>
<td>{% if t_iter.active_dataset is not none %}{{ get_task_type(dataset=t_iter.active_dataset).name }}{% else %}{% trans %}N/A{% endtrans %}{% endif %}</td>
<td>{{ t_iter.submission_format|map("replace", ".%l", extensions)|join(" ") }}</td>
{% if tokens_contest != TOKEN_MODE_DISABLED and tokens_tasks != TOKEN_MODE_DISABLED %}
<td>
Expand Down
Loading
Loading