Add contest freeze
This commit is contained in:
parent
2c39774ff7
commit
a35ea0f6d5
13 changed files with 338 additions and 215 deletions
|
@ -158,7 +158,10 @@ class ContestAdmin(CompareVersionAdmin):
|
|||
)
|
||||
},
|
||||
),
|
||||
(_("Scheduling"), {"fields": ("start_time", "end_time", "time_limit")}),
|
||||
(
|
||||
_("Scheduling"),
|
||||
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
|
||||
),
|
||||
(
|
||||
_("Details"),
|
||||
{
|
||||
|
@ -274,7 +277,8 @@ class ContestAdmin(CompareVersionAdmin):
|
|||
# We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored
|
||||
self._rescored = False
|
||||
if form.changed_data and any(
|
||||
f in form.changed_data for f in ("format_config", "format_name")
|
||||
f in form.changed_data
|
||||
for f in ("format_config", "format_name", "freeze_after")
|
||||
):
|
||||
self._rescore(obj.key)
|
||||
self._rescored = True
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy
|
|||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.timezone import from_database_time, to_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
|
@ -54,6 +54,10 @@ class AtCoderContestFormat(DefaultContestFormat):
|
|||
points = 0
|
||||
format_data = {}
|
||||
|
||||
frozen_time = self.contest.end_time
|
||||
if self.contest.freeze_after:
|
||||
frozen_time = participation.start + self.contest.freeze_after
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
|
@ -66,9 +70,10 @@ class AtCoderContestFormat(DefaultContestFormat):
|
|||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
WHERE sub.date < %s
|
||||
GROUP BY cp.id
|
||||
""",
|
||||
(participation.id, participation.id),
|
||||
(participation.id, participation.id, to_database_time(frozen_time)),
|
||||
)
|
||||
|
||||
for score, time, prob in cursor.fetchall():
|
||||
|
@ -100,6 +105,7 @@ class AtCoderContestFormat(DefaultContestFormat):
|
|||
format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev}
|
||||
points += score
|
||||
|
||||
self.handle_frozen_state(participation, format_data)
|
||||
participation.cumtime = cumtime + penalty
|
||||
participation.score = points
|
||||
participation.tiebreaker = 0
|
||||
|
@ -114,7 +120,7 @@ class AtCoderContestFormat(DefaultContestFormat):
|
|||
'<small style="color:red"> ({penalty})</small>',
|
||||
penalty=floatformat(format_data["penalty"]),
|
||||
)
|
||||
if format_data["penalty"]
|
||||
if format_data.get("penalty")
|
||||
else ""
|
||||
)
|
||||
return format_html(
|
||||
|
@ -129,6 +135,7 @@ class AtCoderContestFormat(DefaultContestFormat):
|
|||
+ self.best_solution_state(
|
||||
format_data["points"], contest_problem.points
|
||||
)
|
||||
+ (" frozen" if format_data.get("frozen") else "")
|
||||
),
|
||||
url=reverse(
|
||||
"contest_user_submissions_ajax",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
class abstractclassmethod(classmethod):
|
||||
|
@ -95,3 +96,17 @@ class BaseContestFormat(metaclass=ABCMeta):
|
|||
if points == total:
|
||||
return "full-score"
|
||||
return "partial-score"
|
||||
|
||||
def handle_frozen_state(self, participation, format_data):
|
||||
if not self.contest.freeze_after:
|
||||
return
|
||||
queryset = participation.submissions.values("problem_id").annotate(
|
||||
time=Max("submission__date")
|
||||
)
|
||||
for result in queryset:
|
||||
problem = str(result["problem_id"])
|
||||
if format_data.get(problem):
|
||||
if result["time"] >= self.contest.freeze_after + participation.start:
|
||||
format_data[problem]["frozen"] = True
|
||||
else:
|
||||
format_data[problem] = {"time": 0, "points": 0, "frozen": True}
|
||||
|
|
|
@ -32,10 +32,19 @@ class DefaultContestFormat(BaseContestFormat):
|
|||
points = 0
|
||||
format_data = {}
|
||||
|
||||
for result in participation.submissions.values("problem_id").annotate(
|
||||
queryset = participation.submissions
|
||||
|
||||
if self.contest.freeze_after:
|
||||
queryset = queryset.filter(
|
||||
submission__date__lt=participation.start + self.contest.freeze_after
|
||||
)
|
||||
|
||||
queryset = queryset.values("problem_id").annotate(
|
||||
time=Max("submission__date"),
|
||||
points=Max("points"),
|
||||
):
|
||||
)
|
||||
|
||||
for result in queryset:
|
||||
dt = (result["time"] - participation.start).total_seconds()
|
||||
if result["points"]:
|
||||
cumtime += dt
|
||||
|
@ -45,6 +54,7 @@ class DefaultContestFormat(BaseContestFormat):
|
|||
}
|
||||
points += result["points"]
|
||||
|
||||
self.handle_frozen_state(participation, format_data)
|
||||
participation.cumtime = max(cumtime, 0)
|
||||
participation.score = points
|
||||
participation.tiebreaker = 0
|
||||
|
@ -66,6 +76,7 @@ class DefaultContestFormat(BaseContestFormat):
|
|||
+ self.best_solution_state(
|
||||
format_data["points"], contest_problem.points
|
||||
)
|
||||
+ (" frozen" if format_data.get("frozen") else "")
|
||||
),
|
||||
url=reverse(
|
||||
"contest_user_submissions_ajax",
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy
|
|||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.timezone import from_database_time, to_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
|
@ -60,6 +60,10 @@ class ECOOContestFormat(DefaultContestFormat):
|
|||
points = 0
|
||||
format_data = {}
|
||||
|
||||
frozen_time = self.contest.end_time
|
||||
if self.contest.freeze_after:
|
||||
frozen_time = participation.start + self.contest.freeze_after
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
|
@ -77,9 +81,15 @@ class ECOOContestFormat(DefaultContestFormat):
|
|||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
WHERE sub.date < %s
|
||||
GROUP BY cp.id
|
||||
""",
|
||||
(participation.id, participation.id, participation.id),
|
||||
(
|
||||
participation.id,
|
||||
participation.id,
|
||||
participation.id,
|
||||
to_database_time(frozen_time),
|
||||
),
|
||||
)
|
||||
|
||||
for score, time, prob, subs, max_score in cursor.fetchall():
|
||||
|
@ -105,6 +115,7 @@ class ECOOContestFormat(DefaultContestFormat):
|
|||
format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus}
|
||||
points += score
|
||||
|
||||
self.handle_frozen_state(participation, format_data)
|
||||
participation.cumtime = cumtime
|
||||
participation.score = points
|
||||
participation.tiebreaker = 0
|
||||
|
@ -134,6 +145,7 @@ class ECOOContestFormat(DefaultContestFormat):
|
|||
+ self.best_solution_state(
|
||||
format_data["points"], contest_problem.points
|
||||
)
|
||||
+ (" frozen" if format_data.get("frozen") else "")
|
||||
),
|
||||
url=reverse(
|
||||
"contest_user_submissions_ajax",
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy
|
|||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.timezone import from_database_time, to_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
|
@ -55,6 +55,10 @@ class ICPCContestFormat(DefaultContestFormat):
|
|||
score = 0
|
||||
format_data = {}
|
||||
|
||||
frozen_time = self.contest.end_time
|
||||
if self.contest.freeze_after:
|
||||
frozen_time = participation.start + self.contest.freeze_after
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
|
@ -67,9 +71,10 @@ class ICPCContestFormat(DefaultContestFormat):
|
|||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
WHERE sub.date < %s
|
||||
GROUP BY cp.id
|
||||
""",
|
||||
(participation.id, participation.id),
|
||||
(participation.id, participation.id, to_database_time(frozen_time)),
|
||||
)
|
||||
|
||||
for points, time, prob in cursor.fetchall():
|
||||
|
@ -102,6 +107,7 @@ class ICPCContestFormat(DefaultContestFormat):
|
|||
format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev}
|
||||
score += points
|
||||
|
||||
self.handle_frozen_state(participation, format_data)
|
||||
participation.cumtime = max(0, cumtime + penalty)
|
||||
participation.score = score
|
||||
participation.tiebreaker = last # field is sorted from least to greatest
|
||||
|
@ -116,7 +122,7 @@ class ICPCContestFormat(DefaultContestFormat):
|
|||
'<small style="color:red"> +{penalty}</small>',
|
||||
penalty=floatformat(format_data["penalty"]),
|
||||
)
|
||||
if format_data["penalty"]
|
||||
if format_data.get("penalty")
|
||||
else ""
|
||||
)
|
||||
return format_html(
|
||||
|
@ -131,6 +137,7 @@ class ICPCContestFormat(DefaultContestFormat):
|
|||
+ self.best_solution_state(
|
||||
format_data["points"], contest_problem.points
|
||||
)
|
||||
+ (" frozen" if format_data.get("frozen") else "")
|
||||
),
|
||||
url=reverse(
|
||||
"contest_user_submissions_ajax",
|
||||
|
|
|
@ -12,6 +12,7 @@ from judge.contest_format.default import DefaultContestFormat
|
|||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
from django.db.models import Min, OuterRef, Subquery
|
||||
|
||||
|
||||
@register_contest_format("ioi")
|
||||
|
@ -45,41 +46,42 @@ class IOIContestFormat(DefaultContestFormat):
|
|||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
score = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT MAX(cs.points) as `score`, (
|
||||
SELECT MIN(csub.date)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||
) AS `time`, cp.id AS `prob`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
""",
|
||||
(participation.id, participation.id),
|
||||
queryset = participation.submissions
|
||||
if self.contest.freeze_after:
|
||||
queryset = queryset.filter(
|
||||
submission__date__lt=participation.start + self.contest.freeze_after
|
||||
)
|
||||
|
||||
for score, time, prob in cursor.fetchall():
|
||||
if self.config["cumtime"]:
|
||||
dt = (
|
||||
from_database_time(time) - participation.start
|
||||
).total_seconds()
|
||||
if score:
|
||||
cumtime += dt
|
||||
else:
|
||||
dt = 0
|
||||
queryset = (
|
||||
queryset.values("problem_id")
|
||||
.filter(
|
||||
points=Subquery(
|
||||
queryset.filter(problem_id=OuterRef("problem_id"))
|
||||
.order_by("-points")
|
||||
.values("points")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(time=Min("submission__date"))
|
||||
.values_list("problem_id", "time", "points")
|
||||
)
|
||||
|
||||
format_data[str(prob)] = {"time": dt, "points": score}
|
||||
points += score
|
||||
for problem_id, time, points in queryset:
|
||||
if self.config["cumtime"]:
|
||||
dt = (time - participation.start).total_seconds()
|
||||
if points:
|
||||
cumtime += dt
|
||||
else:
|
||||
dt = 0
|
||||
|
||||
format_data[str(problem_id)] = {"points": points, "time": dt}
|
||||
score += points
|
||||
|
||||
self.handle_frozen_state(participation, format_data)
|
||||
participation.cumtime = max(cumtime, 0)
|
||||
participation.score = points
|
||||
participation.score = score
|
||||
participation.tiebreaker = 0
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
@ -99,6 +101,7 @@ class IOIContestFormat(DefaultContestFormat):
|
|||
+ self.best_solution_state(
|
||||
format_data["points"], contest_problem.points
|
||||
)
|
||||
+ (" frozen" if format_data.get("frozen") else "")
|
||||
),
|
||||
url=reverse(
|
||||
"contest_user_submissions_ajax",
|
||||
|
|
23
judge/migrations/0139_contest_freeze_after.py
Normal file
23
judge/migrations/0139_contest_freeze_after.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.16 on 2022-11-18 06:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("judge", "0138_bookmark_makebookmark"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="contest",
|
||||
name="freeze_after",
|
||||
field=models.DurationField(
|
||||
blank=True,
|
||||
help_text="Format hh:mm:ss. For example, if you want to freeze contest after 2 hours, enter 02:00:00",
|
||||
null=True,
|
||||
verbose_name="freeze after",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -128,6 +128,14 @@ class Contest(models.Model):
|
|||
"Format hh:mm:ss. For example, if you want a 2-hour contest, enter 02:00:00"
|
||||
),
|
||||
)
|
||||
freeze_after = models.DurationField(
|
||||
verbose_name=_("freeze after"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_(
|
||||
"Format hh:mm:ss. For example, if you want to freeze contest after 2 hours, enter 02:00:00"
|
||||
),
|
||||
)
|
||||
is_visible = models.BooleanField(
|
||||
verbose_name=_("publicly visible"),
|
||||
default=False,
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytz
|
|||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.timezone import make_aware, make_naive
|
||||
|
||||
|
||||
class TimezoneMiddleware(object):
|
||||
|
@ -25,3 +25,10 @@ def from_database_time(datetime):
|
|||
if tz is None:
|
||||
return datetime
|
||||
return make_aware(datetime, tz)
|
||||
|
||||
|
||||
def to_database_time(datetime):
|
||||
tz = connection.timezone
|
||||
if tz is None:
|
||||
return datetime
|
||||
return make_naive(datetime, tz)
|
||||
|
|
|
@ -301,6 +301,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
template_name = "submission/list.html"
|
||||
context_object_name = "submissions"
|
||||
first_page_href = None
|
||||
include_frozen = False
|
||||
|
||||
def get_result_data(self):
|
||||
result = self._get_result_data()
|
||||
|
@ -344,6 +345,10 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
queryset = queryset.filter(contest_object=self.contest)
|
||||
if not self.contest.can_see_full_scoreboard(self.request.user):
|
||||
queryset = queryset.filter(user=self.request.profile)
|
||||
if self.contest.freeze_after and not self.include_frozen:
|
||||
queryset = queryset.exclude(
|
||||
date__gte=self.contest.freeze_after + self.contest.start_time
|
||||
)
|
||||
else:
|
||||
queryset = queryset.select_related("contest_object").defer(
|
||||
"contest_object__description"
|
||||
|
@ -456,6 +461,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
self.selected_languages = set(request.GET.getlist("language"))
|
||||
self.selected_statuses = set(request.GET.getlist("status"))
|
||||
|
||||
if request.user.is_superuser:
|
||||
self.include_frozen = True
|
||||
|
||||
if "results" in request.GET:
|
||||
return JsonResponse(self.get_result_data())
|
||||
|
||||
|
@ -468,6 +476,8 @@ class UserMixin(object):
|
|||
raise ImproperlyConfigured("Must pass a user")
|
||||
self.profile = get_object_or_404(Profile, user__username=kwargs["user"])
|
||||
self.username = kwargs["user"]
|
||||
if self.profile == request.profile:
|
||||
self.include_frozen = True
|
||||
return super(UserMixin, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
@ -837,8 +847,8 @@ class UserContestSubmissionsAjax(UserContestSubmissions):
|
|||
s.contest_time = nice_repr(contest_time, "noday")
|
||||
else:
|
||||
s.contest_time = None
|
||||
points = floatformat(s.contest.points, -self.contest.points_precision)
|
||||
total = floatformat(contest_problem.points, -self.contest.points_precision)
|
||||
points = floatformat(s.contest.points, -self.contest.points_precision)
|
||||
s.display_point = f"{points} / {total}"
|
||||
filtered_submissions.append(s)
|
||||
context["submissions"] = filtered_submissions
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue