Skip to content

Commit b9196ef

Browse files
authored
Merge branch 'main' into css-fix
2 parents 1274dc7 + 9389eeb commit b9196ef

31 files changed

Lines changed: 227 additions & 92 deletions

cms/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"TOKEN_MODE_DISABLED", "TOKEN_MODE_FINITE", "TOKEN_MODE_INFINITE",
3434
"TOKEN_MODE_MIXED",
3535
"FEEDBACK_LEVEL_FULL", "FEEDBACK_LEVEL_RESTRICTED",
36+
"FEEDBACK_LEVEL_OI_RESTRICTED",
3637
# log
3738
# Nothing intended for external use, no need to advertise anything.
3839
# conf
@@ -68,6 +69,9 @@
6869
# Restricted set of information (no killing signal, time or memory, testcases
6970
# can be omitted).
7071
FEEDBACK_LEVEL_RESTRICTED = "restricted"
72+
# Restricted set of information in accordance with the Contest Rules of
73+
# the International Olympiad of Informatics
74+
FEEDBACK_LEVEL_OI_RESTRICTED = "oi_restricted"
7175

7276

7377
from .conf import Address, ServiceCoord, ConfigError, async_config, config

cms/db/contest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ class Contest(Base):
101101
nullable=False,
102102
default=True)
103103

104+
# Allow unofficial submission before analysis mode
105+
allow_unofficial_submission_before_analysis_mode = Column(
106+
Boolean,
107+
nullable=False,
108+
default=False)
109+
104110
# Whether to prevent hidden participations to log in.
105111
block_hidden_participations: bool = Column(
106112
Boolean,

cms/db/submission.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ class SubmissionResult(Base):
364364
JSONB,
365365
nullable=True)
366366

367+
# Time when the submission is scored for the first time
368+
scored_at = Column(
369+
DateTime,
370+
nullable=True)
371+
367372
# The same as the last two fields, but only showing information
368373
# visible to the user (assuming they did not use a token on this
369374
# submission).

cms/db/task.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
Interval, Enum, BigInteger
3737

3838
from cms import TOKEN_MODE_DISABLED, TOKEN_MODE_FINITE, TOKEN_MODE_INFINITE, \
39-
FEEDBACK_LEVEL_FULL, FEEDBACK_LEVEL_RESTRICTED
39+
FEEDBACK_LEVEL_FULL, FEEDBACK_LEVEL_RESTRICTED, FEEDBACK_LEVEL_OI_RESTRICTED
4040
from cmscommon.constants import \
4141
SCORE_MODE_MAX, SCORE_MODE_MAX_SUBTASK, SCORE_MODE_MAX_TOKENED_LAST
4242
from . import Codename, Filename, FilenameSchemaArray, Digest, Base, Contest
@@ -199,6 +199,7 @@ class Task(Base):
199199
# reverse engineer task data.
200200
feedback_level: str = Column(
201201
Enum(FEEDBACK_LEVEL_FULL, FEEDBACK_LEVEL_RESTRICTED,
202+
FEEDBACK_LEVEL_OI_RESTRICTED,
202203
name="feedback_level"),
203204
nullable=False,
204205
default=FEEDBACK_LEVEL_RESTRICTED)

cms/grading/scoretypes/abc.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,13 @@ class ScoreTypeGroup(ScoreTypeAlone):
274274
</thead>
275275
<tbody>
276276
{% for tc in st["testcases"] %}
277-
{% if "outcome" in tc
278-
and (feedback_level == FEEDBACK_LEVEL_FULL
279-
or tc["show_in_restricted_feedback"]) %}
277+
{% set show_tc = "outcome" in tc
278+
and ((feedback_level == FEEDBACK_LEVEL_FULL)
279+
or (feedback_level == FEEDBACK_LEVEL_RESTRICTED
280+
and tc["show_in_restricted_feedback"])
281+
or (feedback_level == FEEDBACK_LEVEL_OI_RESTRICTED
282+
and tc["show_in_oi_restricted_feedback"])) %}
283+
{% if show_tc %}
280284
{% if tc["outcome"] == "Correct" %}
281285
<tr class="correct">
282286
{% elif tc["outcome"] == "Not correct" %}
@@ -307,16 +311,18 @@ class ScoreTypeGroup(ScoreTypeAlone):
307311
{% endif %}
308312
</tr>
309313
{% else %}
314+
{% if feedback_level != FEEDBACK_LEVEL_OI_RESTRICTED %}
310315
<tr class="undefined">
311316
<td class="idx">{{ loop.index }}</td>
312-
{% if feedback_level == FEEDBACK_LEVEL_FULL %}
317+
{% if feedback_level == FEEDBACK_LEVEL_FULL %}
313318
<td colspan="4">
314-
{% else %}
319+
{% else %}
315320
<td colspan="2">
316-
{% endif %}
321+
{% endif %}
317322
{% trans %}N/A{% endtrans %}
318323
</td>
319324
</tr>
325+
{% endif %}
320326
{% endif %}
321327
{% endfor %}
322328
</tbody>
@@ -412,25 +418,33 @@ def compute_score(self, submission_result):
412418

413419
testcases = []
414420
public_testcases = []
415-
previous_tc_all_correct = True
421+
# In "Restricted" feedback mode:
422+
# show until the first testcase with a lowest score
423+
# In "OI Restricted" feedback mode:
424+
# show only the first testcase with a lowest score
425+
426+
tc_first_lowest_idx = None
427+
tc_first_lowest_score = None
416428
for tc_idx in target:
429+
tc_score = float(evaluations[tc_idx].outcome)
417430
tc_outcome = self.get_public_outcome(
418-
float(evaluations[tc_idx].outcome), parameter)
431+
tc_score, parameter)
419432

420433
testcases.append({
421434
"idx": tc_idx,
422435
"outcome": tc_outcome,
423436
"text": evaluations[tc_idx].text,
424437
"time": evaluations[tc_idx].execution_time,
425438
"memory": evaluations[tc_idx].execution_memory,
426-
"show_in_restricted_feedback": previous_tc_all_correct})
439+
"show_in_restricted_feedback": self.public_testcases[tc_idx],
440+
"show_in_oi_restricted_feedback": self.public_testcases[tc_idx]})
441+
427442
if self.public_testcases[tc_idx]:
428443
public_testcases.append(testcases[-1])
429-
# Only block restricted feedback if this is the first
430-
# *public* non-correct testcase, otherwise we might be
431-
# leaking info on private testcases.
432-
if tc_outcome != "Correct":
433-
previous_tc_all_correct = False
444+
if tc_first_lowest_score is None or \
445+
tc_score < tc_first_lowest_score:
446+
tc_first_lowest_idx = tc_idx
447+
tc_first_lowest_score = tc_score
434448
else:
435449
public_testcases.append({"idx": tc_idx})
436450

@@ -439,6 +453,15 @@ def compute_score(self, submission_result):
439453
parameter)
440454
st_score = st_score_fraction * parameter[0]
441455

456+
if tc_first_lowest_idx is not None and st_score_fraction < 1.0:
457+
for tc in testcases:
458+
if not self.public_testcases[tc["idx"]]:
459+
continue
460+
tc["show_in_restricted_feedback"] = (
461+
tc["idx"] <= tc_first_lowest_idx)
462+
tc["show_in_oi_restricted_feedback"] = (
463+
tc["idx"] == tc_first_lowest_idx)
464+
442465
score += st_score
443466
subtasks.append({
444467
"idx": st_idx + 1,

cms/grading/steps/stats.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,40 @@ def merge_execution_stats(
103103
if first_stats is None:
104104
return second_stats.copy()
105105

106+
Stat = typing.TypeVar('Stat', int, float)
107+
108+
def safe_sum(x: Stat | None, y: Stat | None) -> Stat | None:
109+
if x is None:
110+
return y
111+
elif y is None:
112+
return x
113+
else:
114+
return x + y
115+
116+
def safe_max(x: Stat | None, y: Stat | None) -> Stat | None:
117+
if x is None:
118+
return y
119+
elif y is None:
120+
return x
121+
else:
122+
return max(x, y)
123+
106124
ret = first_stats.copy()
107-
ret["execution_time"] += second_stats["execution_time"]
125+
ret["execution_time"] = safe_sum(ret["execution_time"],
126+
second_stats["execution_time"])
108127

109128
if concurrent:
110-
ret["execution_wall_clock_time"] = max(
129+
ret["execution_wall_clock_time"] = safe_max(
111130
ret["execution_wall_clock_time"],
112131
second_stats["execution_wall_clock_time"])
113-
ret["execution_memory"] += second_stats["execution_memory"]
132+
ret["execution_memory"] = safe_sum(ret["execution_memory"],
133+
second_stats["execution_memory"])
114134
else:
115-
ret["execution_wall_clock_time"] += \
116-
second_stats["execution_wall_clock_time"]
117-
ret["execution_memory"] = max(ret["execution_memory"],
118-
second_stats["execution_memory"])
135+
ret["execution_wall_clock_time"] = safe_sum(
136+
ret["execution_wall_clock_time"],
137+
second_stats["execution_wall_clock_time"])
138+
ret["execution_memory"] = safe_max(ret["execution_memory"],
139+
second_stats["execution_memory"])
119140

120141
if first_stats["exit_status"] == Sandbox.EXIT_OK:
121142
ret["exit_status"] = second_stats["exit_status"]

cms/grading/steps/trusted.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"""
3737

3838
import logging
39+
import re
3940

4041
from cms import config
4142
from cms.grading.Sandbox import Sandbox
@@ -55,24 +56,25 @@
5556
CHECKER_FILENAME = "checker"
5657

5758

58-
def _filter_ansi_escape(string: str) -> str:
59-
"""Filter out ANSI commands from the given string.
59+
def _sanitize_message(string: str) -> str:
60+
"""Sanitize a message read from manager output.
61+
62+
Percent signs are escaped (so they are not treated as formatting specifiers
63+
later). Control characters are rejected.
6064
6165
string: string to process.
6266
63-
return: string with ANSI commands stripped.
67+
return: sanitized string.
68+
69+
raise (ValueError): if invalid characters were found.
6470
6571
"""
66-
ansi_mode = False
67-
res = ''
68-
for char in string:
69-
if char == u'\033':
70-
ansi_mode = True
71-
if not ansi_mode:
72-
res += char
73-
if char == u'm':
74-
ansi_mode = False
75-
return res
72+
match = re.search('[\x00-\x08\x0a-\x1f\x7f-\xbf]', string)
73+
if match:
74+
ch = ord(match[0])
75+
raise ValueError(f'Invalid character in outcome: 0x{ch:02x}')
76+
77+
return string.replace('%', '%%')
7678

7779

7880
def extract_outcome_and_text(sandbox: Sandbox) -> tuple[float, list[str]]:
@@ -98,10 +100,13 @@ def extract_outcome_and_text(sandbox: Sandbox) -> tuple[float, list[str]]:
98100

99101
with sandbox.get_file_text(sandbox.stderr_file) as stderr_file:
100102
try:
101-
text = _filter_ansi_escape(stderr_file.readline().strip())
103+
text = _sanitize_message(stderr_file.readline().strip())
102104
except UnicodeDecodeError as error:
103105
logger.error("Manager stderr (text) is not valid UTF-8. %r", error)
104106
raise ValueError("Cannot decode the text.")
107+
except ValueError as error:
108+
logger.error("Manager stderr (text) is malformed. %r", error)
109+
raise error
105110

106111
try:
107112
outcome = float(outcome)

cms/log.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,16 @@ def process(self, msg: str, kwargs: dict):
393393

394394

395395
# Install a shell handler.
396-
shell_handler = StreamHandler(sys.stdout)
396+
shell_handler = StreamHandler(sys.stderr)
397397
shell_handler.setLevel(logging.INFO)
398-
shell_handler.setFormatter(CustomFormatter(has_color_support(sys.stdout)))
398+
shell_handler.setFormatter(CustomFormatter(has_color_support(sys.stderr)))
399399
root_logger.addHandler(shell_handler)
400400

401401

402402
def set_detailed_logs(detailed: bool):
403403
"""Set or unset the shell logs to detailed."""
404404
global shell_handler
405-
color = has_color_support(sys.stdout)
405+
color = has_color_support(sys.stderr)
406406
formatter = DetailedFormatter(color) \
407407
if detailed else CustomFormatter(color)
408408
shell_handler.setFormatter(formatter)

cms/server/admin/handlers/contest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def post(self, contest_id: str):
9696
self.get_bool(attrs, "submissions_download_allowed")
9797
self.get_bool(attrs, "allow_questions")
9898
self.get_bool(attrs, "allow_user_tests")
99+
self.get_bool(attrs, "allow_unofficial_submission_before_analysis_mode")
99100
self.get_bool(attrs, "block_hidden_participations")
100101
self.get_bool(attrs, "allow_password_authentication")
101102
self.get_bool(attrs, "allow_registration")

cms/server/admin/handlers/contestquestion.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@ class QuestionReplyHandler(BaseHandler):
7171
QUICK_ANSWERS = {
7272
"yes": "Yes",
7373
"no": "No",
74-
"answered": "Answered in task description",
75-
"invalid": "Invalid question",
76-
"nocomment": "No comment",
74+
"invalid": "Invalid Question (not a Yes/No Question)",
75+
"nocomment": "No Comment/Please refer to task statement",
7776
}
7877

7978
@require_permission(BaseHandler.PERMISSION_MESSAGING)
@@ -86,7 +85,8 @@ def post(self, contest_id, question_id):
8685
if self.contest is not question.participation.contest:
8786
raise tornado_web.HTTPError(404)
8887

89-
reply_subject_code: str = self.get_argument("reply_question_quick_answer", "")
88+
reply_subject_code: str = self.get_argument(
89+
"reply_question_quick_answer", "")
9090
question.reply_text = self.get_argument("reply_question_text", "")
9191

9292
# Ignore invalid answers

0 commit comments

Comments
 (0)