Skip to content

Commit 871e9c4

Browse files
aguss787veluca93
authored andcommitted
feat: add config for removing submission rate limit
1 parent 2faa419 commit 871e9c4

8 files changed

Lines changed: 168 additions & 19 deletions

File tree

cms/db/contest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
if typing.TYPE_CHECKING:
4343
from . import Task, Participation
4444

45+
4546
class Contest(Base):
4647
"""Class to store a contest (which is a single day of a
4748
programming competition).
@@ -253,6 +254,10 @@ class Contest(Base):
253254
Interval,
254255
CheckConstraint("min_submission_interval > '0 seconds'"),
255256
nullable=True)
257+
min_submission_interval_grace_period: timedelta | None = Column(
258+
Interval,
259+
CheckConstraint("min_submission_interval_grace_period > '0 seconds'"),
260+
nullable=True)
256261
min_user_test_interval: timedelta | None = Column(
257262
Interval,
258263
CheckConstraint("min_user_test_interval > '0 seconds'"),

cms/server/admin/handlers/contest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def post(self, contest_id: str):
114114
self.get_int(attrs, "max_submission_number")
115115
self.get_int(attrs, "max_user_test_number")
116116
self.get_timedelta_sec(attrs, "min_submission_interval")
117+
self.get_timedelta_sec(attrs, "min_submission_interval_grace_period")
117118
self.get_timedelta_sec(attrs, "min_user_test_interval")
118119

119120
self.get_datetime(attrs, "start")

cms/server/admin/templates/contest.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ <h1>Contest configuration</h1>
262262
</td>
263263
<td><input type="text" name="min_submission_interval" value="{{ contest.min_submission_interval.total_seconds()|int if contest.min_submission_interval is not none else "" }}"></td>
264264
</tr>
265+
<tr>
266+
<td>
267+
<span class="info" title="The amount of time (in seconds) until the end of participation in which the minimum interval between submissions is not enforced."></span>
268+
Minimum submission interval grace period
269+
</td>
270+
<td><input type="text" name="min_submission_interval_grace_period" value="{{ contest.min_submission_interval_grace_period.total_seconds()|int if contest.min_submission_interval_grace_period is not none else "" }}"></td>
271+
</tr>
265272
<tr>
266273
<td>
267274
<span class="info" title="The minimum amount of time (in seconds) that a contestant needs to wait after a user test before being able to send another.

cms/server/contest/submission/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""
2828

2929
from .check import get_submission_count, check_max_number, \
30-
get_latest_submission, check_min_interval
30+
get_latest_submission, check_min_interval, is_last_minutes
3131
from .file_matching import InvalidFilesOrLanguage, match_files_and_language
3232
from .file_retrieval import ReceivedFile, InvalidArchive, \
3333
extract_files_from_archive, extract_files_from_tornado
@@ -40,7 +40,7 @@
4040
__all__ = [
4141
# check.py
4242
"get_submission_count", "check_max_number", "get_latest_submission",
43-
"check_min_interval",
43+
"check_min_interval", "is_last_minutes",
4444
# file_retrieval.py
4545
"ReceivedFile", "InvalidArchive", "extract_files_from_archive",
4646
"extract_files_from_tornado",

cms/server/contest/submission/check.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
exported as they may be of general interest.
3434
3535
"""
36-
3736
from datetime import datetime, timedelta
3837
from sqlalchemy import desc, func
3938
from sqlalchemy.orm import Query
@@ -207,3 +206,26 @@ def check_min_interval(
207206
sql_session, participation, contest=contest, task=task, cls=cls)
208207
return (submission is None
209208
or timestamp - submission.timestamp >= min_interval)
209+
210+
211+
def is_last_minutes(timestamp: datetime, participation: Participation):
212+
"""
213+
timestamp: the current timestamp.
214+
participation: the participation to be checked.
215+
216+
return: whether the participation is in its last minutes of contest.
217+
"""
218+
219+
if participation.unrestricted \
220+
or participation.contest.min_submission_interval_grace_period is None:
221+
return False
222+
223+
if participation.contest.per_user_time is None:
224+
end_time = participation.contest.stop
225+
else:
226+
end_time = participation.starting_time + participation.contest.per_user_time
227+
228+
end_time += participation.delay_time + participation.extra_time
229+
time_left = end_time - timestamp
230+
return time_left <= \
231+
participation.contest.min_submission_interval_grace_period

cms/server/contest/submission/workflow.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
)
4848
from cms.db.filecacher import FileCacher
4949
from cmscommon.datetime import make_timestamp
50-
from .check import check_max_number, check_min_interval
50+
from .check import check_max_number, check_min_interval, is_last_minutes
5151
from .file_matching import InvalidFilesOrLanguage, match_files_and_language
5252
from .file_retrieval import InvalidArchive, extract_files_from_tornado
5353
from .utils import fetch_file_digests_from_previous_submission, StorageFailed, \
@@ -134,21 +134,22 @@ def accept_submission(
134134
"at most %d submissions on this task."),
135135
task.max_submission_number)
136136

137-
if not check_min_interval(sql_session, contest.min_submission_interval,
138-
timestamp, participation, contest=contest):
139-
raise UnacceptableSubmission(
140-
N_("Submissions too frequent!"),
141-
N_("Among all tasks, you can submit again "
142-
"after %d seconds from last submission."),
143-
contest.min_submission_interval.total_seconds())
137+
if not is_last_minutes(timestamp, participation):
138+
if not check_min_interval(sql_session, contest.min_submission_interval,
139+
timestamp, participation, contest=contest):
140+
raise UnacceptableSubmission(
141+
N_("Submissions too frequent!"),
142+
N_("Among all tasks, you can submit again "
143+
"after %d seconds from last submission."),
144+
contest.min_submission_interval.total_seconds())
144145

145-
if not check_min_interval(sql_session, task.min_submission_interval,
146-
timestamp, participation, task=task):
147-
raise UnacceptableSubmission(
148-
N_("Submissions too frequent!"),
149-
N_("For this task, you can submit again "
150-
"after %d seconds from last submission."),
151-
task.min_submission_interval.total_seconds())
146+
if not check_min_interval(sql_session, task.min_submission_interval,
147+
timestamp, participation, task=task):
148+
raise UnacceptableSubmission(
149+
N_("Submissions too frequent!"),
150+
N_("For this task, you can submit again "
151+
"after %d seconds from last submission."),
152+
task.min_submission_interval.total_seconds())
152153

153154
# Process the data we received and ensure it's valid.
154155

cmstestsuite/unit_tests/server/contest/submission/check_test.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from cms.db import UserTest, Submission
2727
from cms.server.contest.submission import get_submission_count, \
28-
check_max_number, get_latest_submission, check_min_interval
28+
check_max_number, get_latest_submission, check_min_interval, is_last_minutes
2929
from cmscommon.datetime import make_datetime
3030

3131

@@ -399,5 +399,86 @@ def test_limit_unrestricted(self):
399399
self.get_latest_submission.assert_not_called()
400400

401401

402+
class TestIsLastMinutes(DatabaseMixin, unittest.TestCase):
403+
404+
def setUp(self):
405+
super().setUp()
406+
self.contest = self.add_contest()
407+
self.task = self.add_task(contest=self.contest)
408+
self.participation = self.add_participation(unrestricted=False,
409+
contest=self.contest)
410+
411+
self.timestamp = make_datetime()
412+
413+
def test_unconfigured_min_submission_interval_grace_period(self):
414+
self.setup_contest_with_no_user_time()
415+
416+
self.assertFalse(
417+
is_last_minutes(self.timestamp, self.participation))
418+
419+
def test_no_per_user_time_and_last_minutes(self):
420+
self.setup_contest_with_no_user_time()
421+
self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
422+
423+
self.assertTrue(
424+
is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
425+
426+
def test_no_per_user_time_and_not_last_minutes(self):
427+
self.setup_contest_with_no_user_time()
428+
self.contest.min_submission_interval_grace_period = timedelta(minutes=10)
429+
430+
self.assertFalse(
431+
is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
432+
433+
def test_per_user_time_and_last_minutes(self):
434+
self.participation.contest.per_user_time = timedelta(hours=5)
435+
self.participation.contest.start = self.timestamp - timedelta(hours=10)
436+
self.participation.contest.stop = self.timestamp
437+
self.participation.starting_time = self.timestamp - timedelta(hours=5)
438+
self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
439+
440+
self.assertTrue(
441+
is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
442+
443+
def test_per_user_time_and_not_last_minutes(self):
444+
self.participation.contest.per_user_time = timedelta(hours=5)
445+
self.participation.contest.start = self.timestamp - timedelta(hours=10)
446+
self.participation.contest.stop = self.timestamp
447+
self.participation.starting_time = self.timestamp - timedelta(hours=5)
448+
self.contest.min_submission_interval_grace_period = timedelta(minutes=10)
449+
450+
self.assertFalse(
451+
is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
452+
453+
def test_consider_extra_time(self):
454+
self.setup_contest_with_no_user_time()
455+
456+
self.participation.extra_time = timedelta(seconds=1)
457+
self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
458+
459+
self.assertFalse(
460+
is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
461+
462+
def test_consider_delay(self):
463+
self.setup_contest_with_no_user_time()
464+
465+
self.participation.delay_time = timedelta(seconds=1)
466+
self.contest.min_submission_interval_grace_period = timedelta(minutes=15)
467+
468+
self.assertFalse(
469+
is_last_minutes(self.timestamp - timedelta(minutes=15), self.participation))
470+
471+
def test_unrestricted_participation(self):
472+
self.setup_contest_with_no_user_time()
473+
self.participation.unrestricted = True
474+
475+
self.assertFalse(is_last_minutes(self.timestamp, self.participation))
476+
477+
def setup_contest_with_no_user_time(self):
478+
self.participation.contest.per_user_time = None
479+
self.participation.contest.start = self.timestamp - timedelta(hours=5)
480+
self.participation.contest.stop = self.timestamp
481+
482+
402483
if __name__ == "__main__":
403484
unittest.main()

cmstestsuite/unit_tests/server/contest/submission/workflow_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ def setUp(self):
108108
self.addCleanup(patcher.stop)
109109
self.check_min_interval.return_value = True
110110

111+
patcher = patch(
112+
"cms.server.contest.submission.workflow.is_last_minutes")
113+
self.is_last_minutes = patcher.start()
114+
self.addCleanup(patcher.stop)
115+
self.is_last_minutes.return_value = False
116+
111117
patcher = patch(
112118
"cms.server.contest.submission.workflow.extract_files_from_tornado")
113119
self.extract_files_from_tornado = patcher.start()
@@ -251,6 +257,19 @@ def test_failure_due_to_min_interval_on_contest(self):
251257
self.session, min_interval, self.timestamp, self.participation,
252258
contest=self.contest)
253259

260+
def test_success_with_min_interval_on_contest_in_last_minutes(self):
261+
min_interval = timedelta(seconds=unique_long_id())
262+
self.contest.min_submission_interval = min_interval
263+
# False only when we ask for contest.
264+
self.check_min_interval.side_effect = \
265+
lambda *args, **kwargs: "contest" not in kwargs
266+
self.is_last_minutes.return_value = True
267+
268+
self.call()
269+
270+
self.is_last_minutes.assert_called_with(
271+
self.timestamp, self.participation)
272+
254273
def test_failure_due_to_min_interval_on_task(self):
255274
min_interval = timedelta(seconds=unique_long_id())
256275
self.task.min_submission_interval = min_interval
@@ -266,6 +285,19 @@ def test_failure_due_to_min_interval_on_task(self):
266285
self.session, min_interval, self.timestamp, self.participation,
267286
task=self.task)
268287

288+
def test_success_with_min_interval_on_task_in_last_minutes(self):
289+
min_interval = timedelta(seconds=unique_long_id())
290+
self.task.min_submission_interval = min_interval
291+
# False only when we ask for task.
292+
self.check_min_interval.side_effect = \
293+
lambda *args, **kwargs: "task" not in kwargs
294+
self.is_last_minutes.return_value = True
295+
296+
self.call()
297+
298+
self.is_last_minutes.assert_called_with(
299+
self.timestamp, self.participation)
300+
269301
def test_failure_due_to_extract_files_from_tornado(self):
270302
self.extract_files_from_tornado.side_effect = InvalidArchive
271303

0 commit comments

Comments
 (0)