diff --git a/cms/db/__init__.py b/cms/db/__init__.py index 3344ac10c7..1e7ee98152 100644 --- a/cms/db/__init__.py +++ b/cms/db/__init__.py @@ -81,7 +81,7 @@ # Instantiate or import these objects. -version = 44 +version = 45 engine = create_engine(config.database, echo=config.database_debug, pool_timeout=60, pool_recycle=120) diff --git a/cms/db/submission.py b/cms/db/submission.py index d262789828..6dacba9b7f 100644 --- a/cms/db/submission.py +++ b/cms/db/submission.py @@ -27,6 +27,7 @@ """ from datetime import datetime +import random from sqlalchemy import Boolean from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import relationship @@ -46,6 +47,15 @@ class Submission(Base): """ __tablename__ = 'submissions' + __table_args__ = ( + UniqueConstraint("participation_id", "opaque_id", + name="participation_opaque_unique"), + ) + + # Opaque ID to be used to refer to this submission. + opaque_id: int = Column( + BigInteger, + nullable=False) # Auto increment primary key. id: int = Column( @@ -177,6 +187,25 @@ def tokened(self) -> bool: """ return self.token is not None + @classmethod + def generate_opaque_id(cls, session, participation_id): + randint_upper_bound = 2**63-1 + + opaque_id = random.randint(0, randint_upper_bound) + + # Note that in theory this may cause the transaction to fail by + # generating a non-actually-unique ID. This is however extremely + # unlikely (prob. ~num_parallel_submissions_per_contestant^2/2**63). + while (session + .query(Submission) + .filter(Submission.participation_id == participation_id) + .filter(Submission.opaque_id == opaque_id) + .first() + is not None): + opaque_id = random.randint(0, randint_upper_bound) + + return opaque_id + class File(Base): """Class to store information about one file submitted within a diff --git a/cms/server/contest/handlers/contest.py b/cms/server/contest/handlers/contest.py index 77e7f690b9..a79a53d0d7 100644 --- a/cms/server/contest/handlers/contest.py +++ b/cms/server/contest/handlers/contest.py @@ -250,7 +250,7 @@ def get_task(self, task_name: str) -> Task | None: .filter(Task.name == task_name) \ .one_or_none() - def get_submission(self, task: Task, submission_num: str) -> Submission | None: + def get_submission(self, task: Task, opaque_id: str | int) -> Submission | None: """Return the num-th contestant's submission on the given task. task: a task for the contest that is being served. @@ -265,8 +265,7 @@ def get_submission(self, task: Task, submission_num: str) -> Submission | None: return self.sql_session.query(Submission) \ .filter(Submission.participation == self.current_user) \ .filter(Submission.task == task) \ - .order_by(Submission.timestamp) \ - .offset(int(submission_num) - 1) \ + .filter(Submission.opaque_id == int(opaque_id)) \ .first() def get_user_test(self, task: Task, user_test_num: int) -> UserTest | None: diff --git a/cms/server/contest/handlers/tasksubmission.py b/cms/server/contest/handlers/tasksubmission.py index a05f68a116..c91e7eb75f 100644 --- a/cms/server/contest/handlers/tasksubmission.py +++ b/cms/server/contest/handlers/tasksubmission.py @@ -242,12 +242,12 @@ def add_task_score(self, participation: Participation, task: Task, data: dict): @tornado_web.authenticated @actual_phase_required(0, 1, 2, 3, 4) @multi_contest - def get(self, task_name, submission_num): + def get(self, task_name, opaque_id): task = self.get_task(task_name) if task is None: raise tornado_web.HTTPError(404) - submission = self.get_submission(task, submission_num) + submission = self.get_submission(task, opaque_id) if submission is None: raise tornado_web.HTTPError(404) @@ -284,7 +284,7 @@ def get(self, task_name, submission_num): round(score_type.max_score, task.score_precision) if data["status"] == SubmissionResult.SCORED \ and (submission.token is not None - or self.r_params["actual_phase"] == 3): + or self.r_params["actual_phase"] == 3): data["score"] = \ round(sr.score, task.score_precision) data["score_message"] = score_type.format_score( @@ -302,12 +302,12 @@ class SubmissionDetailsHandler(ContestHandler): @tornado_web.authenticated @actual_phase_required(0, 1, 2, 3, 4) @multi_contest - def get(self, task_name, submission_num): + def get(self, task_name, opaque_id): task = self.get_task(task_name) if task is None: raise tornado_web.HTTPError(404) - submission = self.get_submission(task, submission_num) + submission = self.get_submission(task, opaque_id) if submission is None: raise tornado_web.HTTPError(404) @@ -343,7 +343,7 @@ class SubmissionFileHandler(FileHandler): @tornado_web.authenticated @actual_phase_required(0, 1, 2, 3, 4) @multi_contest - def get(self, task_name, submission_num, filename): + def get(self, task_name, opaque_id, filename): if not self.contest.submissions_download_allowed: raise tornado_web.HTTPError(404) @@ -351,7 +351,7 @@ def get(self, task_name, submission_num, filename): if task is None: raise tornado_web.HTTPError(404) - submission = self.get_submission(task, submission_num) + submission = self.get_submission(task, opaque_id) if submission is None: raise tornado_web.HTTPError(404) @@ -389,12 +389,12 @@ class UseTokenHandler(ContestHandler): @tornado_web.authenticated @actual_phase_required(0) @multi_contest - def post(self, task_name, submission_num): + def post(self, task_name, opaque_id): task = self.get_task(task_name) if task is None: raise tornado_web.HTTPError(404) - submission = self.get_submission(task, submission_num) + submission = self.get_submission(task, opaque_id) if submission is None: raise tornado_web.HTTPError(404) diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py index 32922f5eed..4e14458d2c 100644 --- a/cms/server/contest/submission/workflow.py +++ b/cms/server/contest/submission/workflow.py @@ -221,10 +221,12 @@ def accept_submission( participation.user.username) # Use the filenames of the contestant as a default submission comment - received_filenames_joined = ",".join([file.filename for file in received_files]) + received_filenames_joined = ",".join( + [file.filename for file in received_files]) submission = Submission( timestamp=timestamp, + opaque_id=Submission.generate_opaque_id(sql_session, participation.id), language=language.name if language is not None else None, task=task, participation=participation, diff --git a/cms/server/contest/templates/macro/submission.html b/cms/server/contest/templates/macro/submission.html index 22706cbf9e..e7832ebc4e 100644 --- a/cms/server/contest/templates/macro/submission.html +++ b/cms/server/contest/templates/macro/submission.html @@ -95,7 +95,7 @@ xsrf_form_html, actual_phase, s, - submissions|length - loop.index0, + s.opaque_id, show_date, can_use_tokens, can_play_token, @@ -109,7 +109,7 @@ {%- endmacro %} {% macro row(url, contest_url, translation, xsrf_form_html, - actual_phase, s, s_num, show_date, + actual_phase, s, opaque_id, show_date, can_use_tokens, can_play_token, can_play_token_now, submissions_download_allowed) -%} {# @@ -121,8 +121,7 @@ xsrf_form_html (str): input element for the XSRF protection. actual_phase (int): phase of the contest. s (Submission): the submission to display. -s_num (int): 1-based position of the submission in the list of - submissions of the currently logged in participaiton on this task. +opaque_id (int): opaque id of the submission. show_date (bool): whether to display only the time or also the date. can_use_tokens (bool): whether tokens are allowed for this task. can_play_token (bool): if can_use_tokens is true, whether the user has a @@ -138,7 +137,7 @@ {% set score_type = get_score_type(dataset=task.active_dataset) %} {% set sr = s.get_result(task.active_dataset) or undefined %} {% set status = sr.get_status() if sr is defined else SubmissionResult.COMPILING %} -