diff --git a/cms/grading/scoring.py b/cms/grading/scoring.py index 15ed04421..6969e79e0 100644 --- a/cms/grading/scoring.py +++ b/cms/grading/scoring.py @@ -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 @@ -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. @@ -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): diff --git a/cms/grading/tasktypes/util.py b/cms/grading/tasktypes/util.py index c343dbff7..61037b4e4 100644 --- a/cms/grading/tasktypes/util.py +++ b/cms/grading/tasktypes/util.py @@ -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 diff --git a/cms/locale/cms.pot b/cms/locale/cms.pot index b202436d2..1a21a9029 100644 --- a/cms/locale/cms.pot +++ b/cms/locale/cms.pot @@ -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 , 2018. +# FIRST AUTHOR , 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 \n" "Language-Team: LANGUAGE \n" @@ -317,6 +317,9 @@ msgstr "" msgid "Evaluated" msgstr "" +msgid "status" +msgstr "" + msgid "Token request received" msgstr "" diff --git a/cms/locale/zh_TW/LC_MESSAGES/cms.po b/cms/locale/zh_TW/LC_MESSAGES/cms.po index 168d113d7..7817b15bf 100644 --- a/cms/locale/zh_TW/LC_MESSAGES/cms.po +++ b/cms/locale/zh_TW/LC_MESSAGES/cms.po @@ -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." diff --git a/cms/server/admin/handlers/contestranking.py b/cms/server/admin/handlers/contestranking.py index 6ea250add..ba42f622f 100644 --- a/cms/server/admin/handlers/contestranking.py +++ b/cms/server/admin/handlers/contestranking.py @@ -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 diff --git a/cms/server/admin/handlers/contesttask.py b/cms/server/admin/handlers/contesttask.py index 50f3a603d..bfd70c425 100644 --- a/cms/server/admin/handlers/contesttask.py +++ b/cms/server/admin/handlers/contesttask.py @@ -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)\ diff --git a/cms/server/contest/authentication.py b/cms/server/contest/authentication.py index 326b7d47e..5f0e9e3ce 100644 --- a/cms/server/contest/authentication.py +++ b/cms/server/contest/authentication.py @@ -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( @@ -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 @@ -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")) @@ -349,7 +345,11 @@ 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 @@ -357,6 +357,8 @@ def log_failed_attempt(msg, *args): "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")) diff --git a/cms/server/contest/formatting.py b/cms/server/contest/formatting.py index ca0c6ffcb..4ba1be0c7 100644 --- a/cms/server/contest/formatting.py +++ b/cms/server/contest/formatting.py @@ -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. @@ -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: diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py index ae334c58e..6b38ca038 100644 --- a/cms/server/contest/handlers/contest.py +++ b/cms/server/contest/handlers/contest.py @@ -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 @@ -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 @@ -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. diff --git a/cms/server/contest/handlers/task.py b/cms/server/contest/handlers/task.py index 7ba75c808..076b3ed69 100644 --- a/cms/server/contest/handlers/task.py +++ b/cms/server/contest/handlers/task.py @@ -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): diff --git a/cms/server/contest/handlers/tasksubmission.py b/cms/server/contest/handlers/tasksubmission.py index 5bfffc48b..07b57fdbc 100644 --- a/cms/server/contest/handlers/tasksubmission.py +++ b/cms/server/contest/handlers/tasksubmission.py @@ -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 @@ -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): @@ -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 @@ -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) diff --git a/cms/server/contest/server.py b/cms/server/contest/server.py index 9e5f0c8b8..fb3a085f1 100644 --- a/cms/server/contest/server.py +++ b/cms/server/contest/server.py @@ -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 diff --git a/cms/server/contest/static/cws_style.css b/cms/server/contest/static/cws_style.css index fba5321b3..e8e52fc02 100644 --- a/cms/server/contest/static/cws_style.css +++ b/cms/server/contest/static/cws_style.css @@ -269,6 +269,10 @@ div.login_box { text-align: center; } +.statement.one_statement a { + margin: 0 15px; +} + @media (max-width: 767px) { .task_description .statement.many_statements .main_statements { margin-bottom: 15px; @@ -996,6 +1000,13 @@ td.token_rules p:last-child { width: 100px; } +/** Printing interface */ + +#printjob_list tbody tr td.no_printjobs { + font-style: italic; + text-align: center !important; +} + /* Contest selection page */ .contest-list { diff --git a/cms/server/contest/templates/contest_list.html b/cms/server/contest/templates/contest_list.html index 4a097a94e..b41c22b3c 100644 --- a/cms/server/contest/templates/contest_list.html +++ b/cms/server/contest/templates/contest_list.html @@ -8,7 +8,7 @@

{% trans %}Choose a contest{% endtrans %}

diff --git a/cms/server/contest/templates/overview.html b/cms/server/contest/templates/overview.html index 527fab45a..d3fc9f2dc 100644 --- a/cms/server/contest/templates/overview.html +++ b/cms/server/contest/templates/overview.html @@ -82,17 +82,17 @@

{% trans %}General information{% 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 %} + {%+ 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 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 %} + {%+ 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 %}

@@ -102,7 +102,7 @@

{% trans %}General information{% 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 %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %}

{% else %}

@@ -113,7 +113,7 @@

{% trans %}General information{% 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 %} + {%+ trans %}Your score for each task will be the maximum among the tokened submissions and the last one.{% endtrans %}

{% endif %} {% endif %} @@ -142,23 +142,23 @@

{% trans %}General information{% 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 %} + {%+ 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 %} + {%+ 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 %} + {%+ 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 %} + {%+ 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 %} - {% trans %}There's nothing you can do now.{% endtrans %} + {%+ trans %}There's nothing you can do now.{% endtrans %} {% endif %}

@@ -166,7 +166,7 @@

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

{{ xsrf_form_html|safe }} - +
{% endif %} diff --git a/cms/server/contest/templates/printing.html b/cms/server/contest/templates/printing.html index 2eb986c50..80f7498df 100644 --- a/cms/server/contest/templates/printing.html +++ b/cms/server/contest/templates/printing.html @@ -89,4 +89,6 @@

{% trans %}Previous print jobs{% endtrans %}

+ + {% endblock core %} diff --git a/cms/server/contest/templates/submission_row.html b/cms/server/contest/templates/submission_row.html index b45fa2b68..a431547b4 100644 --- a/cms/server/contest/templates/submission_row.html +++ b/cms/server/contest/templates/submission_row.html @@ -25,7 +25,7 @@ {% if score_type is defined and score_type.max_public_score > 0 %} {% if status == SubmissionResult.SCORED %} - + {{ score_type.format_score(sr.public_score, score_type.max_public_score, sr.public_score_details, task.score_precision, translation=translation) }} {% else %} @@ -36,7 +36,7 @@ {% endif %} {% if score_type is defined and score_type.max_public_score < score_type.max_score %} {% if status == SubmissionResult.SCORED and (s.token is not none or actual_phase == 3) %} - + {{ score_type.format_score(sr.score, score_type.max_score, sr.score_details, task.score_precision, translation=translation) }} {% else %} @@ -48,9 +48,9 @@ {% if actual_phase >= +3 %} {% if s.official %} - Yes + {% trans %}Yes{% endtrans %} {% else %} - No + {% trans %}No{% endtrans %} {% endif %} {% endif %} diff --git a/cms/server/contest/templates/task_description.html b/cms/server/contest/templates/task_description.html index 761e54ecf..68371a432 100644 --- a/cms/server/contest/templates/task_description.html +++ b/cms/server/contest/templates/task_description.html @@ -25,6 +25,7 @@

{% trans %}Statement{% endtrans %}

@@ -34,8 +35,8 @@

{% trans %}Statement{% endtrans %}

{% trans %}The statement for this task is available in multiple versions, in different languages.{% endtrans %} - {% trans %}You can see (and download) all of them using the list on the right.{% endtrans %} - {% trans %}Some suggested translations follow.{% endtrans %} + {%+ trans %}You can see (and download) all of them using the list on the right.{% endtrans %} + {%+ trans %}Some suggested translations follow.{% endtrans %}

{% for statement in itervalues(task.statements)|sort(attribute="language") %} {% if statement.language in task.primary_statements %} @@ -152,7 +153,7 @@

{% trans %}Some details{% endtrans %}

{% trans %}Remember that to see the detailed result of a submission you need to use both a contest-token and a task-token.{% endtrans %} - {% trans type_pl=_("contest-tokens"), contest_root=contest_url() %}You can find the rules for the {{ type_pl }} on the contest overview page.{% endtrans %} + {%+ trans type_pl=_("contest-tokens"), contest_root=contest_url() %}You can find the rules for the {{ type_pl }} on the contest overview page.{% endtrans %}

{% endif %} diff --git a/cms/server/contest/templates/task_submissions.html b/cms/server/contest/templates/task_submissions.html index 655b64aea..bf850411e 100644 --- a/cms/server/contest/templates/task_submissions.html +++ b/cms/server/contest/templates/task_submissions.html @@ -33,6 +33,11 @@ modal.modal("show"); }); +function is_status_terminal (status) { + return status == {{ SubmissionResult.COMPILATION_FAILED }} + || status == {{ SubmissionResult.SCORED }}; +}; + function get_score_class (score, max_score) { if (score <= 0) { return "score_0"; @@ -85,15 +90,15 @@ var row = $("#submission_list tbody tr[data-submission=\"" + submission_id + "\"]"); row.attr("data-status", data["status"]); row.children("td.status").text(data["status_text"]); - if (data["status"] != {{ SubmissionResult.COMPILATION_FAILED }} - && data["status"] != {{ SubmissionResult.SCORED }}) { + var terminal_status = is_status_terminal(data["status"]); + if (!terminal_status) { row.children("td.status").append( $("")); } else { row.children("td.status").append( $("{% trans %}details{% endtrans %}")); } - if (data["status"] == {{ SubmissionResult.SCORED }}) { + if (terminal_status) { update_score( row.children("td.public_score"), $("#task_score_public"), data["public_score"], data["public_score_message"], @@ -106,7 +111,7 @@ data["task_tokened_score"], data["task_tokened_score_message"], data["task_score_is_partial"], data["max_score"]); {% endif %} - } else if (data["status"] != {{ SubmissionResult.COMPILATION_FAILED }}) { + } else { schedule_update_scores(submission_id); } }; @@ -157,7 +162,7 @@

{% trans name=task.title, short_name=task.name %}{{ name }} ({{ short_name } {% if score_type.max_public_score > 0 %} {# Show the public score (alone, if everything is public or tokens are disabled, or together with the tokened score). #}
+ class="{{ "span6" if two_task_scores else "span12" }} well well-small task_score {{ get_score_class(public_score, score_type.max_public_score, task.score_precision) }}"> {% if score_type.max_public_score == score_type.max_score %} {% trans %}Score:{% endtrans %} @@ -178,7 +183,7 @@

{% trans name=task.title, short_name=task.name %}{{ name }} ({{ short_name } {% if score_type.max_public_score < score_type.max_score %} {# Show the tokened score (alone if everything is non-public, or together with the public score). #}
+ class="{{ "span6" if two_task_scores else "span12" }} well well-small task_score {{ get_score_class(tokened_score, score_type.max_score, task.score_precision) if can_use_tokens else "undefined" }}"> {% if can_use_tokens %} {% trans %}Score of tokened submissions:{% endtrans %} diff --git a/cms/server/file_middleware.py b/cms/server/file_middleware.py index 4f2e9abd8..f65d03cef 100644 --- a/cms/server/file_middleware.py +++ b/cms/server/file_middleware.py @@ -55,6 +55,7 @@ class FileServerMiddleware(object): DIGEST_HEADER = "X-CMS-File-Digest" FILENAME_HEADER = "X-CMS-File-Filename" + STATEMENT_VIEW_HEADER = "X-CMS-Statement_View" def __init__(self, file_cacher, app): """Create an instance. @@ -94,6 +95,7 @@ def wsgi_app(self, environ, start_response): digest = original_response.headers.pop(self.DIGEST_HEADER) filename = original_response.headers.pop(self.FILENAME_HEADER, None) mimetype = original_response.mimetype + statement_view = original_response.headers.pop(self.STATEMENT_VIEW_HEADER, None) try: fobj = self.file_cacher.get_file(digest) @@ -110,8 +112,12 @@ def wsgi_app(self, environ, start_response): response.status_code = 200 response.mimetype = mimetype if filename is not None: - response.headers.add( - "Content-Disposition", "attachment", filename=filename) + if not statement_view: + response.headers.add( + "Content-Disposition", "attachment", filename=filename) + else: + response.headers.add( + "Content-Disposition", "inline", filename=filename) response.set_etag(digest) response.cache_control.max_age = SECONDS_IN_A_YEAR response.cache_control.private = True diff --git a/cms/server/util.py b/cms/server/util.py index 82f9511cf..17e380b2c 100644 --- a/cms/server/util.py +++ b/cms/server/util.py @@ -74,7 +74,7 @@ class FileHandlerMixin(RequestHandler): """ - def fetch(self, digest, content_type, filename): + def fetch(self, digest, content_type, filename, view=False): """Serve the file with the given digest. This will just add the headers required to trigger @@ -88,6 +88,8 @@ def fetch(self, digest, content_type, filename): self.set_header(FileServerMiddleware.DIGEST_HEADER, digest) self.set_header(FileServerMiddleware.FILENAME_HEADER, filename) self.set_header("Content-Type", content_type) + if view: + self.set_header(FileServerMiddleware.STATEMENT_VIEW_HEADER, "TRUE") self.finish() diff --git a/cms/service/ProxyService.py b/cms/service/ProxyService.py index 1a3d91cab..a24a5c758 100644 --- a/cms/service/ProxyService.py +++ b/cms/service/ProxyService.py @@ -274,7 +274,8 @@ def __init__(self, shard, contest_id): # example. self.initialize() - self.start_sweeper(347.0) + sweeper_timeout = 5.0 # was 347.0 + self.start_sweeper(sweeper_timeout) def _missing_operations(self): """Return a generator of data to be sent to the rankings.. diff --git a/cms/service/ResourceService.py b/cms/service/ResourceService.py index b06dd7b3d..7c6d52a14 100644 --- a/cms/service/ResourceService.py +++ b/cms/service/ResourceService.py @@ -343,13 +343,11 @@ def percent_from_delta(v): data["cpu"]["num_cpu"] = psutil.cpu_count() self._prev_cpu_times = cpu_times - # Memory. The following relations hold (I think... I only - # verified them experimentally on a swap-less system): - # * vmem.free == vmem.available - vmem.cached - vmem.buffers - # * vmem.total == vmem.used + vmem.free - # That means that cache & buffers are counted both in .used - # and in .available. We want to partition the memory into - # types that sum up to vmem.total. + # Memory. The following equality should hold, as per psutil >= 4.4: + # vmem.total = vmem.used + vmem.buffers + vmem.cached + vmem.free + # Although psutil documentation describes the "used" field as + # platform-dependent, on Linux specifically it matches the output + # of the free(1) utility. vmem = psutil.virtual_memory() swap = psutil.swap_memory() data["memory"] = { @@ -357,7 +355,7 @@ def percent_from_delta(v): "ram_available": vmem.free / B_TO_MB, "ram_cached": vmem.cached / B_TO_MB, "ram_buffers": vmem.buffers / B_TO_MB, - "ram_used": (vmem.used - vmem.cached - vmem.buffers) / B_TO_MB, + "ram_used": vmem.used / B_TO_MB, "swap_total": swap.total / B_TO_MB, "swap_available": swap.free / B_TO_MB, "swap_used": swap.used / B_TO_MB, diff --git a/cmscommon/terminal.py b/cmscommon/terminal.py index a4d913e81..c608f86f0 100644 --- a/cmscommon/terminal.py +++ b/cmscommon/terminal.py @@ -3,6 +3,7 @@ # Contest Management System - http://cms-dev.github.io/ # Copyright © 2015 Luca Versari +# Copyright © 2018 Luca Chiodini # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -119,17 +120,18 @@ def move_cursor(direction, amount=1, stream=sys.stdout, erase=False): """ if stream.isatty(): if direction == directions.UP: - print(curses.tparm(curses.tigetstr("cuu"), amount), + print(curses.tparm(curses.tigetstr("cuu"), amount).decode('ascii'), file=stream, end='') elif direction == directions.DOWN: - print(curses.tparm(curses.tigetstr("cud"), amount), + print(curses.tparm(curses.tigetstr("cud"), amount).decode('ascii'), file=stream, end='') elif direction == directions.LEFT: - print(curses.tparm(curses.tigetstr("cub"), amount), + print(curses.tparm(curses.tigetstr("cub"), amount).decode('ascii'), file=stream, end='') elif direction == directions.RIGHT: - print(curses.tparm(curses.tigetstr("cuf"), amount), + print(curses.tparm(curses.tigetstr("cuf"), amount).decode('ascii'), file=stream, end='') if erase: - print(curses.tparm(curses.tigetstr("el")), file=stream, end='') + print(curses.tparm(curses.tigetstr("el")).decode('ascii'), + file=stream, end='') stream.flush() diff --git a/cmscontrib/loaders/tps.py b/cmscontrib/loaders/tps.py index 6ce992e71..ba5dc55a9 100644 --- a/cmscontrib/loaders/tps.py +++ b/cmscontrib/loaders/tps.py @@ -393,8 +393,7 @@ def get_file_list(files_dir, prefix, except_files): add_optional_name = data['add_optional_name'] if 'add_optional_name' in data else False - subtasks = sorted(subtasks_data['subtasks'].items(), key=lambda subtask: subtask[1]['index']) - for subtask, subtask_data in subtasks: + for subtask, subtask_data in subtasks_data['subtasks'].items(): subtask_no += 1 score = int(subtask_data["score"]) if use_mapping: diff --git a/cmstestsuite/unit_tests/grading/scoring_test.py b/cmstestsuite/unit_tests/grading/scoring_test.py index a16db54b8..bfd8678c7 100755 --- a/cmstestsuite/unit_tests/grading/scoring_test.py +++ b/cmstestsuite/unit_tests/grading/scoring_test.py @@ -46,7 +46,8 @@ class TaskScoreMixin(DatabaseMixin): def setUp(self): super(TaskScoreMixin, self).setUp() self.participation = self.add_participation() - self.task = self.add_task(contest=self.participation.contest) + self.task = self.add_task(contest=self.participation.contest, + score_precision=2) dataset = self.add_dataset(task=self.task) self.task.active_dataset = dataset self.timestamp = make_datetime() @@ -54,9 +55,10 @@ def setUp(self): def at(self, timestamp): return self.timestamp + timedelta(seconds=timestamp) - def call(self, public=False, only_tokened=False): + def call(self, public=False, only_tokened=False, rounded=False): return task_score(self.participation, self.task, - public=public, only_tokened=only_tokened) + public=public, only_tokened=only_tokened, + rounded=rounded) def add_result(self, timestamp, score, tokened=False, score_details=None, public_score=None, public_score_details=None): @@ -375,6 +377,18 @@ def test_only_tokened(self): self.session.flush() self.assertEqual(self.call(only_tokened=True), (44.4, False)) + def test_unrounded(self): + self.add_result(self.at(1), 44.44444, tokened=False) + self.add_result(self.at(2), 44.44443, tokened=False) + self.session.flush() + self.assertEqual(self.call(), (44.44444, False)) + + def test_rounded(self): + self.add_result(self.at(1), 44.44444, tokened=False) + self.add_result(self.at(2), 44.44443, tokened=False) + self.session.flush() + self.assertEqual(self.call(rounded=True), (44.44, False)) + if __name__ == "__main__": unittest.main() diff --git a/cmstestsuite/unit_tests/server/contest/authentication_test.py b/cmstestsuite/unit_tests/server/contest/authentication_test.py index d1e0bd689..88d7344f0 100755 --- a/cmstestsuite/unit_tests/server/contest/authentication_test.py +++ b/cmstestsuite/unit_tests/server/contest/authentication_test.py @@ -261,14 +261,7 @@ def test_cookie_contains_password(self): # Cookies are of no use if one cannot login by password. self.contest.allow_password_authentication = False self.assertFailure() - - # The cookie works with all methods as it holds the plaintext password. self.contest.allow_password_authentication = True - self.user.password = hash_password("mypass", method="bcrypt") - self.assertSuccessAndCookieRefreshed() - - self.user.password = hash_password("mypass", method="plaintext") - self.assertSuccessAndCookieRefreshed() # Cookies contain the password, which is validated every time. self.user.password = build_password("newpass")