Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c43c195
Fix whitespace in consecutive {% trans %} blocks
andreyv Nov 4, 2018
633959a
HTML fixes
andreyv Nov 4, 2018
2df497e
Fix potential bug in removing task from contest
stefano-maggiolo Nov 4, 2018
c3726ef
Fix encoding issues in cmscommon/terminal.py
lucach Nov 13, 2018
78bd5dd
Fix bug caused by shadowing of the admin render param
stefano-maggiolo Dec 2, 2018
89fae0c
Fix reporting of used memory
andreyv Dec 3, 2018
ce56775
Fix loading in task score not going away after a compilation failed
stefano-maggiolo Nov 27, 2018
7419970
Fix score-dependant rendering in CWS when rounding happens
stefano-maggiolo Nov 24, 2018
3816f0d
Regenerate cms.pot file
lw Feb 18, 2019
786f455
Translate Yes/No messages in the Official column of the submissions row
andreyv Nov 25, 2018
8026966
Fix grammar in delete_sandbox() warning
andreyv Dec 12, 2018
c927117
Store hashed password in cookie to avoid re-hashing at every request
lw Nov 27, 2018
e7461b7
Use contest id to map contests for CWS
brianbbsu Mar 14, 2019
e251b98
Match only integer contest id
brianbbsu Mar 14, 2019
555e2cc
Not adding contest id in relative path
brianbbsu Mar 16, 2019
a9354ce
Add option to view task statement in new tab
brianbbsu Apr 13, 2019
2fc67c8
Make proxy service sweep new ranking change every 5 sec
brianbbsu Apr 17, 2019
d61f288
Added C++14
brianbbsu Nov 27, 2019
dc8624a
Added C++14 to setup.py
brianbbsu Nov 27, 2019
a465f2d
Merge remote-tracking branch 'origin/v1.4-cpp14' into v1.4
brianbbsu Sep 4, 2020
168d8eb
Merge remote-tracking branch 'bb/v1.4' into ytp2022
OmeletWithoutEgg Jul 21, 2022
450e261
Merge pull request #2 from tw-ytp/master
tw-ytp Aug 8, 2023
5819187
Revert "fix: sort subtasks using index"
tw-ytp Aug 8, 2023
46c2f70
Merge pull request #3 from YTPxAPCSCamp/revert-2-master
tw-ytp Aug 8, 2023
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
15 changes: 10 additions & 5 deletions cms/grading/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ def compare(a, b):

# Computing global scores (for ranking).

def task_score(participation, task, public=False, only_tokened=False):
def task_score(participation, task,
public=False, only_tokened=False, rounded=False):
"""Return the score of a contest's user on a task.

participation (Participation): the user and contest for which to
Expand All @@ -122,6 +123,7 @@ def task_score(participation, task, public=False, only_tokened=False):
at the results of tokened submissions (that is, the score that the user
would obtain if all non-tokened submissions scored 0.0, or equivalently
had not been scored yet).
rounded (bool): if True, round the score to the task's score_precision.

return ((float, bool)): the score of user on task, and True if not
all submissions of the participation in the task have been scored.
Expand Down Expand Up @@ -169,13 +171,16 @@ def task_score(participation, task, public=False, only_tokened=False):
score_details_tokened.append((score, score_details, s.tokened()))

if task.score_mode == SCORE_MODE_MAX:
return _task_score_max(score_details_tokened), partial
if task.score_mode == SCORE_MODE_MAX_SUBTASK:
return _task_score_max_subtask(score_details_tokened), partial
score = _task_score_max(score_details_tokened)
elif task.score_mode == SCORE_MODE_MAX_SUBTASK:
score = _task_score_max_subtask(score_details_tokened)
elif task.score_mode == SCORE_MODE_MAX_TOKENED_LAST:
return _task_score_max_tokened_last(score_details_tokened), partial
score = _task_score_max_tokened_last(score_details_tokened)
else:
raise ValueError("Unknown score mode '%s'" % task.score_mode)
if rounded:
score = round(score, task.score_precision)
return score, partial


def _task_score_max_tokened_last(score_details_tokened):
Expand Down
2 changes: 1 addition & 1 deletion cms/grading/tasktypes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def delete_sandbox(sandbox, success=True, keep_sandbox=False):
"""
# If the job was not successful, we keep the sandbox around.
if not success:
logger.warning("Sandbox %s kept around because job did not succeeded.",
logger.warning("Sandbox %s kept around because job did not succeed.",
sandbox.get_root_path())

delete = success and not config.keep_sandbox and not keep_sandbox
Expand Down
11 changes: 7 additions & 4 deletions cms/locale/cms.pot
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Translations template for Contest Management System.
# Copyright (C) 2018 CMS development group
# Copyright (C) 2019 CMS development group
# This file is distributed under the same license as the Contest Management
# System project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Contest Management System 1.4.dev0\n"
"Project-Id-Version: Contest Management System 1.4rc1\n"
"Report-Msgid-Bugs-To: contestms@googlegroups.com\n"
"POT-Creation-Date: 2018-10-01 09:06+0100\n"
"POT-Creation-Date: 2019-02-18 21:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -317,6 +317,9 @@ msgstr ""
msgid "Evaluated"
msgstr ""

msgid "status"
msgstr ""

msgid "Token request received"
msgstr ""

Expand Down
3 changes: 3 additions & 0 deletions cms/locale/zh_TW/LC_MESSAGES/cms.po
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,9 @@ msgstr "本題沒有敘述"
msgid "Download task statement"
msgstr "下載題目敘述"

msgid "View task statement"
msgstr "觀看題目敘述"

msgid ""
"The statement for this task is available in multiple versions, in different "
"languages."
Expand Down
3 changes: 1 addition & 2 deletions cms/server/admin/handlers/contestranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ def get(self, contest_id, format="online"):
total_score = 0.0
partial = False
for task in self.contest.tasks:
t_score, t_partial = task_score(p, task)
t_score = round(t_score, task.score_precision)
t_score, t_partial = task_score(p, task, rounded=True)
p.scores.append((t_score, t_partial))
total_score += t_score
partial = partial or t_partial
Expand Down
3 changes: 3 additions & 0 deletions cms/server/admin/handlers/contesttask.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,16 @@ def post(self, contest_id):
# Unassign the task to the contest.
task.contest = None
task.num = None # not strictly necessary
self.sql_session.flush()

# Decrease by 1 the num of every subsequent task.
for t in self.sql_session.query(Task)\
.filter(Task.contest == self.contest)\
.filter(Task.num > task_num)\
.order_by(Task.num)\
.all():
t.num -= 1
self.sql_session.flush()

elif operation == self.MOVE_UP:
task2 = self.sql_session.query(Task)\
Expand Down
54 changes: 28 additions & 26 deletions cms/server/contest/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,35 +52,18 @@
logger = logging.getLogger(__name__)


def safe_validate_password(participation, password):
"""Check that the password is correct for the authentication.

Validate the given password against the participation (using either
the global or the contest-specific password that is stored in the
database), and guard against a misconfiguration.
def get_password(participation):
"""Return the password the participation can log in with.

participation (Participation): a participation.
password (str): a password provided by someone trying to log in
claiming to be the given participation.

return (bool): whether the password matches the expected one.
return (str): the password that is on record for them.

"""
if participation.password is None:
correct_password = participation.user.password
return participation.user.password
else:
correct_password = participation.password

try:
password_valid = validate_password(correct_password, password)
except ValueError as e:
# This is either a programming or a configuration error.
logger.warning(
"Invalid password stored in database for user %s in contest %s: "
"%s", participation.user.username, participation.contest.name, e)
return False

return password_valid
return participation.password


def validate_login(
Expand Down Expand Up @@ -132,7 +115,18 @@ def log_failed_attempt(msg, *args):
log_failed_attempt("user not registered to contest")
return None, None

if not safe_validate_password(participation, password):
correct_password = get_password(participation)

try:
password_valid = validate_password(correct_password, password)
except ValueError as e:
# This is either a programming or a configuration error.
logger.warning(
"Invalid password stored in database for user %s in contest %s: "
"%s", participation.user.username, participation.contest.name, e)
return None, None

if not password_valid:
log_failed_attempt("wrong password")
return None, None

Expand All @@ -149,8 +143,10 @@ def log_failed_attempt(msg, *args):
"contest %s, at %s", ip_address, username, contest.name,
timestamp)

# If hashing is used, the cookie stores the hashed password so that
# the expensive bcrypt call doesn't need to be done at every request.
return (participation,
json.dumps([username, password, make_timestamp(timestamp)])
json.dumps([username, correct_password, make_timestamp(timestamp)])
.encode("utf-8"))


Expand Down Expand Up @@ -349,14 +345,20 @@ def log_failed_attempt(msg, *args):
log_failed_attempt("user not registered to contest")
return None, None

if not safe_validate_password(participation, password):
correct_password = get_password(participation)

# We compare hashed password because it would be too expensive to
# re-hash the user-provided plaintext password at every request.
if password != correct_password:
log_failed_attempt("wrong password")
return None, None

logger.info("Successful cookie authentication as user %r, on contest %s, "
"returning from %s, at %s", username, contest.name, last_update,
timestamp)

# We store the hashed password (if hashing is used) so that the
# expensive bcrypt hashing doesn't need to be done at every request.
return (participation,
json.dumps([username, password, make_timestamp(timestamp)])
json.dumps([username, correct_password, make_timestamp(timestamp)])
.encode("utf-8"))
4 changes: 3 additions & 1 deletion cms/server/contest/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def format_token_rules(tokens, t_type=None, translation=DEFAULT_TRANSLATION):
return result


def get_score_class(score, max_score):
def get_score_class(score, max_score, score_precision):
"""Return a CSS class to visually represent the score/max_score

score (float): the score of the submission.
Expand All @@ -143,6 +143,8 @@ def get_score_class(score, max_score):
return (unicode): class name

"""
score = round(score, score_precision)
max_score = round(max_score, score_precision)
if score <= 0:
return "score_0"
elif score >= max_score:
Expand Down
14 changes: 9 additions & 5 deletions cms/server/contest/handlers/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

from cms import config, TOKEN_MODE_MIXED
from cms.db import Contest, Submission, Task, UserTest
from cms.server import FileHandlerMixin
from cms.server import FileHandlerMixin, Url
from cms.locale import filter_language_codes
from cms.server.contest.authentication import authenticate_request
from cmscommon.datetime import get_timezone
Expand Down Expand Up @@ -88,7 +88,11 @@ def prepare(self):
super(ContestHandler, self).prepare()

if self.is_multi_contest():
self.contest_url = self.url[self.contest.name]
if self.url.url_root.count('..') > 1:
self.contest_url = Url(self.url.url_root[3:])
else:
self.contest_url = Url(self.url.url_root[1:])
self.contest_url.url_root += '/'
else:
self.contest_url = self.url

Expand All @@ -106,14 +110,14 @@ def choose_contest(self):
if self.is_multi_contest():
# Choose the contest found in the path argument
# see: https://github.com/tornadoweb/tornado/issues/1673
contest_name = self.path_args[0]
contest_id = self.path_args[0]

# Select the correct contest or return an error
self.contest = self.sql_session.query(Contest)\
.filter(Contest.name == contest_name).first()
.filter(Contest.id == contest_id).first()
if self.contest is None:
self.contest = Contest(
name=contest_name, description=contest_name)
name=contest_id, description=contest_id)
# render_params in this class assumes the contest is loaded,
# so we cannot call it without a fully defined contest. Luckily
# the one from the base class is enough to display a 404 page.
Expand Down
5 changes: 4 additions & 1 deletion cms/server/contest/handlers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ def get(self, task_name, lang_code):
else:
filename = "%s.pdf" % task.name

self.fetch(statement, "application/pdf", filename)
if self.request.arguments.has_key('view'):
self.fetch(statement, "application/pdf", filename, view=True)
else:
self.fetch(statement, "application/pdf", filename)


class TaskAttachmentViewHandler(FileHandler):
Expand Down
57 changes: 31 additions & 26 deletions cms/server/contest/handlers/tasksubmission.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ def get(self, task_name):
.all()

public_score, is_public_score_partial = task_score(
participation, task, public=True)
participation, task, public=True, rounded=True)
tokened_score, is_tokened_score_partial = task_score(
participation, task, only_tokened=True)
participation, task, only_tokened=True, rounded=True)
# These two should be the same, anyway.
is_score_partial = is_public_score_partial or is_tokened_score_partial

Expand Down Expand Up @@ -182,6 +182,14 @@ def get(self, task_name):

class SubmissionStatusHandler(ContestHandler):

STATUS_TEXT = {
SubmissionResult.COMPILING: N_("Compiling..."),
SubmissionResult.COMPILATION_FAILED: N_("Compilation failed"),
SubmissionResult.EVALUATING: N_("Evaluating..."),
SubmissionResult.SCORING: N_("Scoring..."),
SubmissionResult.SCORED: N_("Evaluated"),
}

refresh_cookie = False

def add_task_score(self, participation, task, data):
Expand All @@ -205,9 +213,9 @@ def add_task_score(self, participation, task, data):
.options(joinedload(Submission.results))\
.all()
data["task_public_score"], public_score_is_partial = \
task_score(participation, task, public=True)
task_score(participation, task, public=True, rounded=True)
data["task_tokened_score"], tokened_score_is_partial = \
task_score(participation, task, only_tokened=True)
task_score(participation, task, only_tokened=True, rounded=True)
# These two should be the same, anyway.
data["task_score_is_partial"] = \
public_score_is_partial or tokened_score_is_partial
Expand Down Expand Up @@ -242,39 +250,36 @@ def get(self, task_name, submission_num):
else:
data["status"] = sr.get_status()

if data["status"] == SubmissionResult.COMPILING:
data["status_text"] = self._("Compiling...")
elif data["status"] == SubmissionResult.COMPILATION_FAILED:
data["status_text"] = self._("Compilation failed")
elif data["status"] == SubmissionResult.EVALUATING:
data["status_text"] = self._("Evaluating...")
elif data["status"] == SubmissionResult.SCORING:
data["status_text"] = self._("Scoring...")
elif data["status"] == SubmissionResult.SCORED:
data["status_text"] = self._("Evaluated")
data["status_text"] = self._(self.STATUS_TEXT[data["status"]])

# For terminal statuses we add the scores information to the payload.
if data["status"] == SubmissionResult.COMPILATION_FAILED \
or data["status"] == SubmissionResult.SCORED:
self.add_task_score(submission.participation, task, data)

score_type = task.active_dataset.score_type_object
if score_type.max_public_score > 0:
data["max_public_score"] = \
round(score_type.max_public_score, task.score_precision)
data["public_score"] = \
round(sr.public_score, task.score_precision)
data["public_score_message"] = score_type.format_score(
sr.public_score, score_type.max_public_score,
sr.public_score_details, task.score_precision,
translation=self.translation)
if data["status"] == SubmissionResult.SCORED:
data["public_score"] = \
round(sr.public_score, task.score_precision)
data["public_score_message"] = score_type.format_score(
sr.public_score, score_type.max_public_score,
sr.public_score_details, task.score_precision,
translation=self.translation)
if score_type.max_public_score < score_type.max_score \
and (submission.token is not None
or self.r_params["actual_phase"] == 3):
data["max_score"] = \
round(score_type.max_score, task.score_precision)
data["score"] = \
round(sr.score, task.score_precision)
data["score_message"] = score_type.format_score(
sr.score, score_type.max_score,
sr.score_details, task.score_precision,
translation=self.translation)
if data["status"] == SubmissionResult.SCORED:
data["score"] = \
round(sr.score, task.score_precision)
data["score_message"] = score_type.format_score(
sr.score, score_type.max_score,
sr.score_details, task.score_precision,
translation=self.translation)

self.write(data)

Expand Down
6 changes: 3 additions & 3 deletions cms/server/contest/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ def __init__(self, shard, contest_id=None):
self.contest_id = contest_id

if self.contest_id is None:
HANDLERS.append((r"", MainHandler))
handlers = [(r'/', ContestListHandler)]
HANDLERS.append((r"/", MainHandler))
handlers = []
for h in HANDLERS:
handlers.append((r'/([^/]+)' + h[0],) + h[1:])
handlers.append((r'/(\d+)' + h[0],) + h[1:])
else:
HANDLERS.append((r"/", MainHandler))
handlers = HANDLERS
Expand Down
Loading