Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic restriction lifting #25

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions cms/db/contest.py
Original file line number Diff line number Diff line change
@@ -219,6 +219,9 @@ class Contest(Base):
CheckConstraint("min_user_test_interval > '0 seconds'"),
nullable=True)

# Time after which the minimum interval restriction for submissions is lifted
restricted_time = Column(Interval, nullable=True)

# The scores for this contest will be rounded to this number of
# decimal places.
score_precision = Column(
1 change: 1 addition & 0 deletions cms/server/admin/handlers/contest.py
Original file line number Diff line number Diff line change
@@ -121,6 +121,7 @@ def post(self, contest_id):
self.get_int(attrs, "max_user_test_number")
self.get_timedelta_sec(attrs, "min_submission_interval")
self.get_timedelta_sec(attrs, "min_user_test_interval")
self.get_timedelta_sec(attrs, "restricted_time")

self.get_string(attrs, "timezone", empty=None)
self.get_int(attrs, "score_precision")
9 changes: 9 additions & 0 deletions cms/server/admin/templates/contest.html
Original file line number Diff line number Diff line change
@@ -251,6 +251,15 @@ <h1>Contest configuration</h1>
</td>
<td><input type="text" name="min_user_test_interval" value="{{ contest.min_user_test_interval.total_seconds()|int if contest.min_user_test_interval is not none else "" }}"></td>
</tr>
<tr>
<td>
<span class="info" title="The time (in seconds) after which the minimum interval restriction for submissions is lifted.
Leave empty to keep forever.
Give a positive value to specify the time from the contest start, give a negative value to specify the time before the contest end."></span>
Lift minimum submission interval restriction after
</td>
<td><input type="text" name="restricted_time" value="{{ contest.restricted_time.total_seconds()| int if contest.restricted_time is not none else "" }}"></td>
</tr>
</table>
<input type="submit"
value="Update"
5 changes: 3 additions & 2 deletions cms/server/contest/handlers/contest.py
Original file line number Diff line number Diff line change
@@ -195,14 +195,15 @@ def render_params(self):
group.analysis_stop if group.analysis_enabled
else None,
group.per_user_time, participation.starting_time,
self.contest.restricted_time,
participation.delay_time, participation.extra_time)

ret["actual_phase"], ret["current_phase_begin"], \
ret["current_phase_end"], ret["valid_phase_begin"], \
ret["valid_phase_end"] = res

if ret["actual_phase"] == 0:
ret["phase"] = 0
if ret["actual_phase"] == 0 or ret["actual_phase"] == .5:
ret["phase"] = ret["actual_phase"]

# set the timezone used to format timestamps
ret["timezone"] = get_timezone(participation.user, self.contest)
4 changes: 2 additions & 2 deletions cms/server/contest/handlers/main.py
Original file line number Diff line number Diff line change
@@ -382,7 +382,7 @@ class PrintingHandler(ContestHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def get(self):
participation = self.current_user
@@ -404,7 +404,7 @@ def get(self):
**self.r_params)

@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def post(self):
try:
6 changes: 3 additions & 3 deletions cms/server/contest/handlers/task.py
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ class TaskDescriptionHandler(ContestHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name):
task = self.get_task(task_name)
@@ -64,7 +64,7 @@ class TaskStatementViewHandler(FileHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name, lang_code):
task = self.get_task(task_name)
@@ -90,7 +90,7 @@ class TaskAttachmentViewHandler(FileHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name, filename):
task = self.get_task(task_name)
16 changes: 8 additions & 8 deletions cms/server/contest/handlers/tasksubmission.py
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ class SubmitHandler(ContestHandler):
"""

@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def post(self, task_name):
task = self.get_task(task_name)
@@ -78,15 +78,15 @@ def post(self, task_name):

# Only set the official bit when the user can compete and we are not in
# analysis mode.
official = self.r_params["actual_phase"] == 0
official = self.r_params["actual_phase"] == 0 or self.r_params["actual_phase"] == 0.5

query_args = dict()

try:
submission = accept_submission(
self.sql_session, self.service.file_cacher, self.current_user,
task, self.timestamp, self.request.files,
self.get_argument("language", None), official)
self.get_argument("language", None), official, self.r_params["actual_phase"] == 0)
self.sql_session.commit()
except UnacceptableSubmission as e:
logger.info("Sent error: `%s' - `%s'", e.subject, e.formatted_text)
@@ -112,7 +112,7 @@ class TaskSubmissionsHandler(ContestHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name):
participation = self.current_user
@@ -246,7 +246,7 @@ def add_task_score(self, participation, task, data):
task.score_precision, translation=self.translation)

@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name, submission_num):
task = self.get_task(task_name)
@@ -311,7 +311,7 @@ class SubmissionDetailsHandler(ContestHandler):
refresh_cookie = False

@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name, submission_num):
task = self.get_task(task_name)
@@ -355,7 +355,7 @@ class SubmissionFileHandler(FileHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0, 3)
@actual_phase_required(0, 0.5, 3)
@multi_contest
def get(self, task_name, submission_num, filename):
if not self.contest.submissions_download_allowed:
@@ -401,7 +401,7 @@ class UseTokenHandler(ContestHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def post(self, task_name, submission_num):
task = self.get_task(task_name)
12 changes: 6 additions & 6 deletions cms/server/contest/handlers/taskusertest.py
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ class UserTestInterfaceHandler(ContestHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def get(self):
participation = self.current_user
@@ -117,7 +117,7 @@ class UserTestHandler(ContestHandler):
refresh_cookie = False

@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def post(self, task_name):
if not self.r_params["testing_enabled"]:
@@ -163,7 +163,7 @@ class UserTestStatusHandler(ContestHandler):
refresh_cookie = False

@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def get(self, task_name, user_test_num):
if not self.r_params["testing_enabled"]:
@@ -218,7 +218,7 @@ class UserTestDetailsHandler(ContestHandler):
refresh_cookie = False

@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def get(self, task_name, user_test_num):
if not self.r_params["testing_enabled"]:
@@ -243,7 +243,7 @@ class UserTestIOHandler(FileHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def get(self, task_name, user_test_num, io):
if not self.r_params["testing_enabled"]:
@@ -277,7 +277,7 @@ class UserTestFileHandler(FileHandler):

"""
@tornado_web.authenticated
@actual_phase_required(0)
@actual_phase_required(0, 0.5)
@multi_contest
def get(self, task_name, user_test_num, filename):
if not self.r_params["testing_enabled"]:
16 changes: 12 additions & 4 deletions cms/server/contest/phase_management.py
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@

def compute_actual_phase(timestamp, contest_start, contest_stop,
analysis_start, analysis_stop, per_user_time,
starting_time, delay_time, extra_time):
starting_time, restricted_time, delay_time, extra_time):
"""Determine the current phase and when the active phase is.

The "actual phase" of the contest for a certain user is the status
@@ -43,7 +43,9 @@ def compute_actual_phase(timestamp, contest_start, contest_stop,
already started, its per-user time frame hasn't yet (this
usually means the user still has to click on the "start!"
button in USACO-like contests);
* 0: the user can compete;
* 0: the user can compete;
* 0.5:the user can still compete and the interval restriction between
submissions is lifted
* +1: the user cannot compete because, even if the contest hasn't
stopped yet, its per-user time frame already has (again, this
should normally happen only in USACO-like contests);
@@ -134,8 +136,14 @@ def compute_actual_phase(timestamp, contest_start, contest_stop,

if actual_start <= timestamp <= actual_stop:
actual_phase = 0
current_phase_begin = actual_start
current_phase_end = actual_stop
if restricted_time is not None:
if restricted_time > timedelta():
lift_time = min(actual_stop, actual_start + restricted_time)
else:
lift_time = max(actual_start, actual_stop + restricted_time)
actual_phase = 0 if timestamp <= lift_time else 0.5
current_phase_begin = actual_start if actual_phase == 0 else lift_time
current_phase_end = actual_stop if actual_phase == .5 or restricted_time is None else lift_time
elif contest_start <= timestamp < actual_start:
# This also includes a funny corner case: the user's start
# is known but is in the future (the admin either set it
51 changes: 35 additions & 16 deletions cms/server/contest/submission/workflow.py
Original file line number Diff line number Diff line change
@@ -35,9 +35,12 @@
from .check import check_max_number, check_min_interval
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, \
store_local_copy

from .utils import (
fetch_file_digests_from_previous_submission,
StorageFailed,
store_local_copy,
)
from ..phase_management import compute_actual_phase

logger = logging.getLogger(__name__)

@@ -62,7 +65,7 @@ def formatted_text(self):


def accept_submission(sql_session, file_cacher, participation, task, timestamp,
tornado_files, language_name, official):
tornado_files, language_name, official, check_interval_restriction):
"""Process a contestant's request to submit a submission.

Parse and validate the data that a contestant sent for a submission
@@ -106,20 +109,36 @@ def accept_submission(sql_session, file_cacher, participation, task, timestamp,
participation, task=task):
raise UnacceptableSubmission(
N_("Too many submissions!"),
N_("You have reached the maximum limit of "
"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):
N_(
"You have reached the maximum limit of "
"at most %d submissions on this task."
),
task.max_submission_number,
)

if check_interval_restriction and 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):
N_(
"Among all tasks, you can submit again "
"after %d seconds from last submission."
),
contest.min_submission_interval.total_seconds(),
)

if check_interval_restriction and 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 "
4 changes: 2 additions & 2 deletions cms/server/contest/templates/contest.html
Original file line number Diff line number Diff line change
@@ -184,7 +184,7 @@ <h3 id="countdown_box">
<span id="unread_count" class="label label-warning no_unread"></span>
</a>
</li>
{% if actual_phase == 0 or actual_phase == 3 or participation.unrestricted %}
{% if actual_phase == 0 or actual_phase == 0.5 or actual_phase == 3 or participation.unrestricted %}
{% for t_iter in contest.tasks %}
<li class="nav-header">
{{ t_iter.name }}
@@ -201,7 +201,7 @@ <h3 id="countdown_box">
<li{% if page == "documentation" %} class="active"{% endif %}>
<a href="{{ contest_url("documentation") }}">{% trans %}Documentation{% endtrans %}</a>
</li>
{% if actual_phase == 0 or participation.unrestricted %}{# FIXME maybe >= 0? #}
{% if actual_phase == 0 or actual_phase == 0.5 or participation.unrestricted %}{# FIXME maybe >= 0? #}
{% if testing_enabled %}
<li{% if page == "testing" %} class="active"{% endif %}>
<a href="{{ contest_url("testing") }}">{% trans %}Testing{% endtrans %}</a>
6 changes: 3 additions & 3 deletions cms/server/contest/templates/macro/submission.html
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@
<col class="files"/>
{% set num_cols = num_cols + 1 %}
{% endif %}
{% if can_use_tokens and actual_phase == 0 %}
{% if can_use_tokens and actual_phase == 0 or actual_phase == 0.5%}
<col class="token"/>
{% set num_cols = num_cols + 1 %}
{% endif %}
@@ -78,7 +78,7 @@
{% if submissions_download_allowed %}
<th class="files">{% trans %}Files{% endtrans %}</th>
{% endif %}
{% if can_use_tokens and actual_phase == 0 %}
{% if can_use_tokens and (actual_phase == 0 or actual_phase == 0.5) %}
<th class="token">{% trans %}Token{% endtrans %}</th>
{% endif %}
</tr>
@@ -255,7 +255,7 @@
{% endif %}
</td>
{% endif %}
{% if can_use_tokens and actual_phase == 0 %}
{% if can_use_tokens and (actual_phase == 0 or actual_phase == 0.5) %}
<td class="token">
{% if s.token is not none %}
<a class="btn disabled">{% trans %}Played{% endtrans %}</a>
4 changes: 2 additions & 2 deletions cms/server/contest/templates/overview.html
Original file line number Diff line number Diff line change
@@ -146,7 +146,7 @@ <h2>{% trans %}General information{% endtrans %}</h2>
{% 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 %}
{% elif actual_phase == 0 %}
{% elif actual_phase == 0 or actual_phase == 0.5 %}
{% 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 %}
{% elif actual_phase == +1 %}
@@ -179,7 +179,7 @@ <h2>{% trans %}General information{% endtrans %}</h2>



{% if actual_phase == 0 or actual_phase == 3%}
{% if actual_phase == 0 or actual_phase == 0.5 or actual_phase == 3%}
<h2>{% trans %}Task overview{% endtrans %}</h2>

<table class="table table-bordered table-striped">
6 changes: 3 additions & 3 deletions cms/server/contest/templates/task_submissions.html
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
{# Whether the user has a token to play (maybe after waiting some time). #}
{% set can_play_token =
can_use_tokens
and actual_phase == 0
and (actual_phase == 0 or actual_phase == 0.5)
and (tokens_info[0] > 0 or tokens_info[0] == -1) %}
{# Whether the user can play a token right now (only meaningful when
can_play_token = true). #}
@@ -316,7 +316,7 @@ <h2 style="margin-bottom: 10px">{% trans %}Submit a solution{% endtrans %}</h2>

<h2 style="margin: 40px 0 10px">{% trans %}Previous submissions{% endtrans %}</h2>

{% if can_use_tokens_in_contest and actual_phase == 0 %}
{% if can_use_tokens_in_contest and (actual_phase == 0 or actual_phase == 0.5) %}
<div style="padding-bottom:10px">
{% if score_type.feedback() in ["full", "partial", "no"] %}
{% elif not can_use_tokens %}
@@ -343,7 +343,7 @@ <h2 style="margin: 40px 0 10px">{% trans %}Previous submissions{% endtrans %}</h
{% endif %}
{% else %}
{% trans %}Right now, you do not have tokens available for this task.{% endtrans %}
{% if actual_phase == 0 and tokens_info[1] is not none %}
{% if (actual_phase == 0 or actual_phase == 0.5) and tokens_info[1] is not none %}
{% trans gen_time=tokens_info[1]|format_datetime_smart %}
You will receive a new token at {{ gen_time }}.
{% endtrans %}
20 changes: 18 additions & 2 deletions cmscontrib/gerpythonformat/ContestConfig.py
Original file line number Diff line number Diff line change
@@ -162,7 +162,8 @@ def __init__(self, rules, name, ignore_latex=False, verbose_latex=False,
# Default submission limits
self.submission_limits(None, None)
self.user_test_limits(None, None)

self.lift_restrictions_after(None)

# a standard tokenwise comparator (specified here so that it has to be
# compiled at most once per contest)
shutil.copy(os.path.join(self._get_ready_dir(), "tokens.cpp"),
@@ -534,6 +535,20 @@ def test_user(self, u):
"""
self._mytestuser = u

@exported_function
def lift_restrictions_after(self, restricted_time):
"""
Set the time before the interval restriction on submissions is lifted
This refers to both restrictions set by contest and task. After lifting of restrictions
a participation is moved to phase 0.5
restricted_time (timedelta): if None restrictions are always active
if 0 restrictions are never active
if <0 restrictions are active until end + restricted_time
if >0 restrictions are active until start + restricted_time
"""
self.restricted_time = restricted_time

def short_path(self, f):
"""
Return a (possibly) shorter name for a file (which can be relative
@@ -566,7 +581,8 @@ def _makecontest(self):
cdb.min_submission_interval = self.min_submission_interval
cdb.max_user_test_number = self.max_user_test_number
cdb.min_user_test_interval = self.min_user_test_interval

cdb.restricted_time = self.restricted_time

self.usersdb = {}
self.participationsdb = {}

1 change: 1 addition & 0 deletions cmscontrib/loaders/italy_yaml.py
Original file line number Diff line number Diff line change
@@ -216,6 +216,7 @@ def get_contest(self):
load(conf, args, "max_user_test_number")
load(conf, args, "min_submission_interval", conv=make_timedelta)
load(conf, args, "min_user_test_interval", conv=make_timedelta)
load(conf, args, "restricted_time", conv=make__timedelta)

tasks = load(conf, None, ["tasks", "problemi"])
participations = load(conf, None, ["users", "utenti"])
1 change: 1 addition & 0 deletions cmscontrib/loaders/tps.py
Original file line number Diff line number Diff line change
@@ -205,6 +205,7 @@ def get_task(self, get_statement=True):

args['min_submission_interval'] = make_timedelta(60)
args['min_user_test_interval'] = make_timedelta(60)
args['restricted_time'] = make_timedelta(-60*15)

task = Task(**args)

5 changes: 5 additions & 0 deletions cmscontrib/updaters/update_45.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
begin;

alter table contests add restricted_time varchar;

rollback; -- change this to: commit;
184 changes: 104 additions & 80 deletions cmstestsuite/unit_tests/server/contest/phase_management_test.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/Configuring a contest.rst
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ The limits can be set both for individual tasks and for the whole contest. A sub

Each of these fields can be left unset to prevent the corresponding limitation from being enforced.

You can set ``restricted_time`` to have the minimum interval restriction on submissions lifted after a certain time. If the field is left unset the restriction is never lifted, if it has a value of zero it is never active. If it is positive the restriction is lifted this much time in seconds after the contest starts, and if it is negative it is lifted this much time in seconds (as an absolute value) before the contest ends.`

Feedback to contestants
=======================