diff --git a/cms/db/contest.py b/cms/db/contest.py
index 91dbc48b91..ff8454c5d5 100644
--- a/cms/db/contest.py
+++ b/cms/db/contest.py
@@ -293,6 +293,10 @@ class Contest(Base):
Interval,
CheckConstraint("min_submission_interval > '0 seconds'"),
nullable=True)
+ min_submission_interval_grace_period = Column(
+ Interval,
+ CheckConstraint("min_submission_interval_grace_period > '0 seconds'"),
+ nullable=True)
min_user_test_interval = Column(
Interval,
CheckConstraint("min_user_test_interval > '0 seconds'"),
diff --git a/cms/server/admin/handlers/contest.py b/cms/server/admin/handlers/contest.py
index 850bc8bc72..9861214c5e 100644
--- a/cms/server/admin/handlers/contest.py
+++ b/cms/server/admin/handlers/contest.py
@@ -135,6 +135,7 @@ def post(self, contest_id):
self.get_int(attrs, "max_submission_number")
self.get_int(attrs, "max_user_test_number")
self.get_timedelta_sec(attrs, "min_submission_interval")
+ self.get_timedelta_sec(attrs, "min_submission_interval_grace_period")
self.get_timedelta_sec(attrs, "min_user_test_interval")
self.get_datetime(attrs, "start")
diff --git a/cms/server/admin/templates/contest.html b/cms/server/admin/templates/contest.html
index 0e3c0cd1c1..a6005a0c7c 100644
--- a/cms/server/admin/templates/contest.html
+++ b/cms/server/admin/templates/contest.html
@@ -338,6 +338,13 @@
= min_interval)
+
+
+def is_last_minutes(timestamp: datetime, participation: Participation):
+ """
+ timestamp (datetime): the current timestamp.
+ participation (Participation): the participation to be checked.
+ delta (timedelta): length of the last time section to be checked.
+
+ return (bool): whether it is the last `delta` of the participation.
+ """
+
+ if participation.unrestricted \
+ or participation.contest.min_submission_interval_grace_period is None:
+ return False
+
+ if participation.contest.per_user_time is None:
+ end_time = participation.contest.stop
+ else:
+ end_time = participation.starting_time + participation.contest.per_user_time
+
+ end_time += participation.delay_time + participation.extra_time
+ time_left = end_time - timestamp
+ return time_left <= \
+ participation.contest.min_submission_interval_grace_period
diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py
index c451045011..b9e556b0fc 100644
--- a/cms/server/contest/submission/workflow.py
+++ b/cms/server/contest/submission/workflow.py
@@ -32,7 +32,7 @@
from cms import config
from cms.db import Submission, File, UserTestManager, UserTestFile, UserTest
from cmscommon.datetime import make_timestamp
-from .check import check_max_number, check_min_interval
+from .check import check_max_number, check_min_interval, is_last_minutes
from .file_matching import InvalidFilesOrLanguage, match_files_and_language
from .file_retrieval import InvalidArchive, extract_files_from_tornado
from .utils import fetch_file_digests_from_previous_submission, StorageFailed, \
@@ -110,21 +110,22 @@ def accept_submission(sql_session, file_cacher, participation, task, timestamp,
"at most %d submissions on this task."),
task.max_submission_number)
- if not check_min_interval(sql_session, contest.min_submission_interval,
- timestamp, participation, contest=contest):
- raise UnacceptableSubmission(
- N_("Submissions too frequent!"),
- N_("Among all tasks, you can submit again "
- "after %d seconds from last submission."),
- contest.min_submission_interval.total_seconds())
+ if not is_last_minutes(timestamp, participation):
+ if not check_min_interval(sql_session, contest.min_submission_interval,
+ timestamp, participation, contest=contest):
+ raise UnacceptableSubmission(
+ N_("Submissions too frequent!"),
+ N_("Among all tasks, you can submit again "
+ "after %d seconds from last submission."),
+ contest.min_submission_interval.total_seconds())
- if not check_min_interval(sql_session, task.min_submission_interval,
- timestamp, participation, task=task):
- raise UnacceptableSubmission(
- N_("Submissions too frequent!"),
- N_("For this task, you can submit again "
- "after %d seconds from last submission."),
- task.min_submission_interval.total_seconds())
+ if not check_min_interval(sql_session, task.min_submission_interval,
+ timestamp, participation, task=task):
+ raise UnacceptableSubmission(
+ N_("Submissions too frequent!"),
+ N_("For this task, you can submit again "
+ "after %d seconds from last submission."),
+ task.min_submission_interval.total_seconds())
# Process the data we received and ensure it's valid.
diff --git a/cmstestsuite/unit_tests/server/contest/submission/check_test.py b/cmstestsuite/unit_tests/server/contest/submission/check_test.py
index d1c8fa65dc..54cc6c5f4b 100755
--- a/cmstestsuite/unit_tests/server/contest/submission/check_test.py
+++ b/cmstestsuite/unit_tests/server/contest/submission/check_test.py
@@ -25,7 +25,7 @@
from cms.db import UserTest, Submission
from cms.server.contest.submission import get_submission_count, \
- check_max_number, get_latest_submission, check_min_interval
+ check_max_number, get_latest_submission, check_min_interval, is_last_minutes
from cmscommon.datetime import make_datetime
@@ -399,5 +399,86 @@ def test_limit_unrestricted(self):
self.get_latest_submission.assert_not_called()
+class TestIsLastMinutes(DatabaseMixin, unittest.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.contest = self.add_contest()
+ self.task = self.add_task(contest=self.contest)
+ self.participation = self.add_participation(unrestricted=False,
+ contest=self.contest)
+
+ self.timestamp = make_datetime()
+
+ def test_unconfigured_min_submission_interval_grace_period(self):
+ self.setup_contest_with_no_user_time()
+
+ self.assertFalse(
+ is_last_minutes(self.timestamp, self.participation))
+
+ def test_no_per_user_time_and_last_minutes(self):
+ self.setup_contest_with_no_user_time()
+ self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
+
+ self.assertTrue(
+ is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
+
+ def test_no_per_user_time_and_not_last_minutes(self):
+ self.setup_contest_with_no_user_time()
+ self.contest.min_submission_interval_grace_period = timedelta(minutes=10)
+
+ self.assertFalse(
+ is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
+
+ def test_per_user_time_and_last_minutes(self):
+ self.participation.contest.per_user_time = timedelta(hours=5)
+ self.participation.contest.start = self.timestamp - timedelta(hours=10)
+ self.participation.contest.stop = self.timestamp
+ self.participation.starting_time = self.timestamp - timedelta(hours=5)
+ self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
+
+ self.assertTrue(
+ is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
+
+ def test_per_user_time_and_not_last_minutes(self):
+ self.participation.contest.per_user_time = timedelta(hours=5)
+ self.participation.contest.start = self.timestamp - timedelta(hours=10)
+ self.participation.contest.stop = self.timestamp
+ self.participation.starting_time = self.timestamp - timedelta(hours=5)
+ self.contest.min_submission_interval_grace_period = timedelta(minutes=10)
+
+ self.assertFalse(
+ is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
+
+ def test_consider_extra_time(self):
+ self.setup_contest_with_no_user_time()
+
+ self.participation.extra_time = timedelta(seconds=1)
+ self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
+
+ self.assertFalse(
+ is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
+
+ def test_consider_delay(self):
+ self.setup_contest_with_no_user_time()
+
+ self.participation.delay_time = timedelta(seconds=1)
+ self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
+
+ self.assertFalse(
+ is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
+
+ def test_unrestricted_participation(self):
+ self.setup_contest_with_no_user_time()
+ self.participation.unrestricted = True
+
+ self.assertFalse(is_last_minutes(self.timestamp, self.participation))
+
+ def setup_contest_with_no_user_time(self):
+ self.participation.contest.per_user_time = None
+ self.participation.contest.start = self.timestamp - timedelta(hours=5)
+ self.participation.contest.stop = self.timestamp
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/cmstestsuite/unit_tests/server/contest/submission/workflow_test.py b/cmstestsuite/unit_tests/server/contest/submission/workflow_test.py
index ffb93ac017..6cc57d9137 100755
--- a/cmstestsuite/unit_tests/server/contest/submission/workflow_test.py
+++ b/cmstestsuite/unit_tests/server/contest/submission/workflow_test.py
@@ -108,6 +108,12 @@ def setUp(self):
self.addCleanup(patcher.stop)
self.check_min_interval.return_value = True
+ patcher = patch(
+ "cms.server.contest.submission.workflow.is_last_minutes")
+ self.is_last_minutes = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.is_last_minutes.return_value = False
+
patcher = patch(
"cms.server.contest.submission.workflow.extract_files_from_tornado")
self.extract_files_from_tornado = patcher.start()
@@ -251,6 +257,19 @@ def test_failure_due_to_min_interval_on_contest(self):
self.session, min_interval, self.timestamp, self.participation,
contest=self.contest)
+ def test_success_with_min_interval_on_contest_in_last_minutes(self):
+ min_interval = timedelta(seconds=unique_long_id())
+ self.contest.min_submission_interval = min_interval
+ # False only when we ask for contest.
+ self.check_min_interval.side_effect = \
+ lambda *args, **kwargs: "contest" not in kwargs
+ self.is_last_minutes.return_value = True
+
+ self.call()
+
+ self.is_last_minutes.assert_called_with(
+ self.timestamp, self.participation)
+
def test_failure_due_to_min_interval_on_task(self):
min_interval = timedelta(seconds=unique_long_id())
self.task.min_submission_interval = min_interval
@@ -266,6 +285,19 @@ def test_failure_due_to_min_interval_on_task(self):
self.session, min_interval, self.timestamp, self.participation,
task=self.task)
+ def test_success_with_min_interval_on_task_in_last_minutes(self):
+ min_interval = timedelta(seconds=unique_long_id())
+ self.task.min_submission_interval = min_interval
+ # False only when we ask for task.
+ self.check_min_interval.side_effect = \
+ lambda *args, **kwargs: "task" not in kwargs
+ self.is_last_minutes.return_value = True
+
+ self.call()
+
+ self.is_last_minutes.assert_called_with(
+ self.timestamp, self.participation)
+
def test_failure_due_to_extract_files_from_tornado(self):
self.extract_files_from_tornado.side_effect = InvalidArchive
|