NDOJ/judge/admin/contest.py

529 lines
18 KiB
Python
Raw Normal View History

2020-01-21 06:35:58 +00:00
from django.conf.urls import url
from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.db import connection, transaction
from django.db.models import Q, TextField
2022-12-20 08:24:24 +00:00
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
2020-01-21 06:35:58 +00:00
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
2021-05-24 20:00:36 +00:00
from django.utils import timezone
2020-01-21 06:35:58 +00:00
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin
2020-01-21 06:35:58 +00:00
2021-05-24 20:00:36 +00:00
from django_ace import AceWidget
2020-01-21 06:35:58 +00:00
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest
2022-05-14 17:57:27 +00:00
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
AdminPagedownWidget,
AdminSelect2MultipleWidget,
AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.views.contests import recalculate_contest_summary_result
2020-01-21 06:35:58 +00:00
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@property
def is_hidden(self):
return False
class ContestTagForm(ModelForm):
contests = ModelMultipleChoiceField(
2022-05-14 17:57:27 +00:00
label=_("Included contests"),
2020-01-21 06:35:58 +00:00
queryset=Contest.objects.all(),
required=False,
2022-05-14 17:57:27 +00:00
widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"),
)
2020-01-21 06:35:58 +00:00
class ContestTagAdmin(admin.ModelAdmin):
2022-05-14 17:57:27 +00:00
fields = ("name", "color", "description", "contests")
list_display = ("name", "color")
2020-01-21 06:35:58 +00:00
actions_on_top = True
actions_on_bottom = True
form = ContestTagForm
if AdminPagedownWidget is not None:
formfield_overrides = {
2022-05-14 17:57:27 +00:00
TextField: {"widget": AdminPagedownWidget},
2020-01-21 06:35:58 +00:00
}
def save_model(self, request, obj, form, change):
super(ContestTagAdmin, self).save_model(request, obj, form, change)
2022-05-14 17:57:27 +00:00
obj.contests.set(form.cleaned_data["contests"])
2020-01-21 06:35:58 +00:00
def get_form(self, request, obj=None, **kwargs):
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
2022-05-14 17:57:27 +00:00
form.base_fields["contests"].initial = obj.contests.all()
2020-01-21 06:35:58 +00:00
return form
class ContestProblemInlineForm(ModelForm):
class Meta:
2022-12-20 08:24:24 +00:00
widgets = {
"problem": AdminHeavySelect2Widget(data_view="problem_select2"),
2023-01-02 23:22:45 +00:00
"hidden_subtasks": TextInput(attrs={"size": "3"}),
2022-12-20 08:24:24 +00:00
"points": TextInput(attrs={"size": "1"}),
"order": TextInput(attrs={"size": "1"}),
}
2020-01-21 06:35:58 +00:00
class ContestProblemInline(admin.TabularInline):
model = ContestProblem
2022-05-14 17:57:27 +00:00
verbose_name = _("Problem")
verbose_name_plural = "Problems"
fields = (
"problem",
"points",
"partial",
"is_pretested",
"max_submissions",
2023-01-02 23:22:45 +00:00
"hidden_subtasks",
"show_testcases",
2022-05-14 17:57:27 +00:00
"order",
"rejudge_column",
)
readonly_fields = ("rejudge_column",)
2020-01-21 06:35:58 +00:00
form = ContestProblemInlineForm
def rejudge_column(self, obj):
if obj.id is None:
2022-05-14 17:57:27 +00:00
return ""
return format_html(
'<a class="button rejudge-link" href="{}">Rejudge</a>',
reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)),
)
rejudge_column.short_description = ""
2020-01-21 06:35:58 +00:00
class ContestForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ContestForm, self).__init__(*args, **kwargs)
2022-05-14 17:57:27 +00:00
if "rate_exclude" in self.fields:
2020-01-21 06:35:58 +00:00
if self.instance and self.instance.id:
2022-05-14 17:57:27 +00:00
self.fields["rate_exclude"].queryset = Profile.objects.filter(
contest_history__contest=self.instance
).distinct()
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
self.fields["rate_exclude"].queryset = Profile.objects.none()
self.fields["banned_users"].widget.can_add_related = False
self.fields["view_contest_scoreboard"].widget.can_add_related = False
2020-01-21 06:35:58 +00:00
def clean(self):
cleaned_data = super(ContestForm, self).clean()
2022-05-14 17:57:27 +00:00
cleaned_data["banned_users"].filter(
current_contest__contest=self.instance
).update(current_contest=None)
2020-01-21 06:35:58 +00:00
class Meta:
widgets = {
2022-05-14 17:57:27 +00:00
"authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"curators": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"testers": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"private_contestants": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2"
),
"tags": AdminSelect2MultipleWidget,
"banned_users": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"view_contest_scoreboard": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
2020-01-21 06:35:58 +00:00
}
if HeavyPreviewAdminPageDownWidget is not None:
2022-05-14 17:57:27 +00:00
widgets["description"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("contest_preview")
)
2020-01-21 06:35:58 +00:00
class ContestAdmin(CompareVersionAdmin):
2020-01-21 06:35:58 +00:00
fieldsets = (
2022-05-14 17:57:27 +00:00
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
(
_("Settings"),
{
"fields": (
"is_visible",
"use_clarifications",
"hide_problem_tags",
2023-09-17 04:55:24 +00:00
"public_scoreboard",
2022-05-14 17:57:27 +00:00
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
2024-03-23 05:26:53 +00:00
"rate_limit",
2022-05-14 17:57:27 +00:00
)
},
),
2022-11-18 22:59:58 +00:00
(
_("Scheduling"),
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
),
2022-05-14 17:57:27 +00:00
(
_("Details"),
{
"fields": (
"description",
"og_image",
"logo_override_image",
"tags",
"summary",
)
},
),
(
_("Format"),
{"fields": ("format_name", "format_config", "problem_label_script")},
),
(
_("Rating"),
{
"fields": (
"is_rated",
"rate_all",
"rating_floor",
"rating_ceiling",
"rate_exclude",
)
},
),
(
_("Access"),
{
"fields": (
"access_code",
"private_contestants",
"organizations",
"view_contest_scoreboard",
)
},
),
(_("Justice"), {"fields": ("banned_users",)}),
)
list_display = (
"key",
"name",
"is_visible",
"is_rated",
"start_time",
"end_time",
"time_limit",
"user_count",
2020-01-21 06:35:58 +00:00
)
2022-05-14 17:57:27 +00:00
search_fields = ("key", "name")
2020-01-21 06:35:58 +00:00
inlines = [ContestProblemInline]
actions_on_top = True
actions_on_bottom = True
form = ContestForm
2022-05-14 17:57:27 +00:00
change_list_template = "admin/judge/contest/change_list.html"
filter_horizontal = ["rate_exclude"]
date_hierarchy = "start_time"
2020-01-21 06:35:58 +00:00
2020-12-28 02:27:03 +00:00
def get_actions(self, request):
actions = super(ContestAdmin, self).get_actions(request)
2022-05-14 17:57:27 +00:00
if request.user.has_perm(
"judge.change_contest_visibility"
) or request.user.has_perm("judge.create_private_contest"):
for action in ("make_visible", "make_hidden"):
2020-12-28 02:27:03 +00:00
actions[action] = self.get_action(action)
return actions
2020-01-21 06:35:58 +00:00
def get_queryset(self, request):
queryset = Contest.objects.all()
2022-05-14 17:57:27 +00:00
if request.user.has_perm("judge.edit_all_contest"):
2020-01-21 06:35:58 +00:00
return queryset
else:
2022-05-14 17:57:27 +00:00
return queryset.filter(
Q(authors=request.profile) | Q(curators=request.profile)
).distinct()
2020-01-21 06:35:58 +00:00
def get_readonly_fields(self, request, obj=None):
readonly = []
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.contest_rating"):
readonly += ["is_rated", "rate_all", "rate_exclude"]
if not request.user.has_perm("judge.contest_access_code"):
readonly += ["access_code"]
if not request.user.has_perm("judge.create_private_contest"):
readonly += [
"private_contestants",
"organizations",
]
if not request.user.has_perm("judge.change_contest_visibility"):
readonly += ["is_visible"]
if not request.user.has_perm("judge.contest_problem_label"):
readonly += ["problem_label_script"]
2020-01-21 06:35:58 +00:00
return readonly
2020-12-28 02:27:03 +00:00
def save_model(self, request, obj, form, change):
# `is_visible` will not appear in `cleaned_data` if user cannot edit it
2022-05-14 17:57:27 +00:00
if form.cleaned_data.get("is_visible") and not request.user.has_perm(
"judge.change_contest_visibility"
):
if (
2022-09-06 20:36:02 +00:00
not len(form.cleaned_data["organizations"]) > 0
and not len(form.cleaned_data["private_contestants"]) > 0
2022-05-14 17:57:27 +00:00
):
2020-12-28 02:27:03 +00:00
raise PermissionDenied
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.create_private_contest"):
2020-12-28 02:27:03 +00:00
raise PermissionDenied
super().save_model(request, obj, form, change)
# We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored
self._rescored = False
2022-05-14 17:57:27 +00:00
if form.changed_data and any(
2022-11-18 22:59:58 +00:00
f in form.changed_data
2023-01-04 21:12:41 +00:00
for f in (
"start_time",
"end_time",
"time_limit",
"format_config",
"format_name",
"freeze_after",
)
2022-05-14 17:57:27 +00:00
):
2020-12-28 02:27:03 +00:00
self._rescore(obj.key)
self._rescored = True
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model`
if not self._rescored and any(formset.has_changed() for formset in formsets):
2022-05-14 17:57:27 +00:00
self._rescore(form.cleaned_data["key"])
2022-05-28 08:54:12 +00:00
obj = form.instance
obj.is_organization_private = obj.organizations.count() > 0
obj.is_private = obj.private_contestants.count() > 0
obj.save()
2020-12-28 02:27:03 +00:00
2020-01-21 06:35:58 +00:00
def has_change_permission(self, request, obj=None):
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.edit_own_contest"):
2020-01-21 06:35:58 +00:00
return False
2021-05-24 20:00:36 +00:00
if obj is None:
2020-01-21 06:35:58 +00:00
return True
2021-05-24 20:00:36 +00:00
return obj.is_editable_by(request.user)
2020-01-21 06:35:58 +00:00
2020-12-28 02:27:03 +00:00
def _rescore(self, contest_key):
from judge.tasks import rescore_contest
2022-05-14 17:57:27 +00:00
2020-12-28 02:27:03 +00:00
transaction.on_commit(rescore_contest.s(contest_key).delay)
2020-01-21 06:35:58 +00:00
def make_visible(self, request, queryset):
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.change_contest_visibility"):
queryset = queryset.filter(
Q(is_private=True) | Q(is_organization_private=True)
)
2020-01-21 06:35:58 +00:00
count = queryset.update(is_visible=True)
2022-05-14 17:57:27 +00:00
self.message_user(
request,
ungettext(
"%d contest successfully marked as visible.",
"%d contests successfully marked as visible.",
count,
)
% count,
)
make_visible.short_description = _("Mark contests as visible")
2020-01-21 06:35:58 +00:00
def make_hidden(self, request, queryset):
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.change_contest_visibility"):
queryset = queryset.filter(
Q(is_private=True) | Q(is_organization_private=True)
)
2020-12-28 02:27:03 +00:00
count = queryset.update(is_visible=True)
2022-05-14 17:57:27 +00:00
self.message_user(
request,
ungettext(
"%d contest successfully marked as hidden.",
"%d contests successfully marked as hidden.",
count,
)
% count,
)
make_hidden.short_description = _("Mark contests as hidden")
2020-01-21 06:35:58 +00:00
def get_urls(self):
return [
2022-05-14 17:57:27 +00:00
url(r"^rate/all/$", self.rate_all_view, name="judge_contest_rate_all"),
url(r"^(\d+)/rate/$", self.rate_view, name="judge_contest_rate"),
url(
r"^(\d+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge"
),
2020-01-21 06:35:58 +00:00
] + super(ContestAdmin, self).get_urls()
def rejudge_view(self, request, contest_id, problem_id):
2022-05-14 17:57:27 +00:00
queryset = ContestSubmission.objects.filter(
problem_id=problem_id
).select_related("submission")
2020-01-21 06:35:58 +00:00
for model in queryset:
model.submission.judge(rejudge=True)
2022-05-14 17:57:27 +00:00
self.message_user(
request,
ungettext(
"%d submission was successfully scheduled for rejudging.",
"%d submissions were successfully scheduled for rejudging.",
len(queryset),
)
% len(queryset),
)
return HttpResponseRedirect(
reverse("admin:judge_contest_change", args=(contest_id,))
)
2020-01-21 06:35:58 +00:00
def rate_all_view(self, request):
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.contest_rating"):
2020-01-21 06:35:58 +00:00
raise PermissionDenied()
with transaction.atomic():
2021-05-24 20:00:36 +00:00
with connection.cursor() as cursor:
2022-05-14 17:57:27 +00:00
cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table)
2020-01-21 06:35:58 +00:00
Profile.objects.update(rating=None)
2022-05-14 17:57:27 +00:00
for contest in Contest.objects.filter(
is_rated=True, end_time__lte=timezone.now()
).order_by("end_time"):
2020-01-21 06:35:58 +00:00
rate_contest(contest)
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(reverse("admin:judge_contest_changelist"))
2020-01-21 06:35:58 +00:00
def rate_view(self, request, id):
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.contest_rating"):
2020-01-21 06:35:58 +00:00
raise PermissionDenied()
contest = get_object_or_404(Contest, id=id)
2021-05-24 20:00:36 +00:00
if not contest.is_rated or not contest.ended:
2020-01-21 06:35:58 +00:00
raise Http404()
with transaction.atomic():
contest.rate()
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist"))
)
2020-01-21 06:35:58 +00:00
2021-05-24 20:00:36 +00:00
def get_form(self, request, obj=None, **kwargs):
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
2022-05-14 17:57:27 +00:00
if "problem_label_script" in form.base_fields:
2021-05-24 20:00:36 +00:00
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
# on the model.
2022-05-14 17:57:27 +00:00
form.base_fields["problem_label_script"].widget = AceWidget(
"lua", request.profile.ace_theme
)
perms = ("edit_own_contest", "edit_all_contest")
form.base_fields["curators"].queryset = Profile.objects.filter(
Q(user__is_superuser=True)
| Q(user__groups__permissions__codename__in=perms)
| Q(user__user_permissions__codename__in=perms),
2020-01-21 06:35:58 +00:00
).distinct()
return form
class ContestParticipationForm(ModelForm):
class Meta:
widgets = {
2022-05-14 17:57:27 +00:00
"contest": AdminSelect2Widget(),
"user": AdminHeavySelect2Widget(data_view="profile_select2"),
2020-01-21 06:35:58 +00:00
}
class ContestParticipationAdmin(admin.ModelAdmin):
2022-05-14 17:57:27 +00:00
fields = ("contest", "user", "real_start", "virtual", "is_disqualified")
list_display = (
"contest",
"username",
"show_virtual",
"real_start",
"score",
"cumtime",
"tiebreaker",
)
actions = ["recalculate_results"]
2020-01-21 06:35:58 +00:00
actions_on_bottom = actions_on_top = True
2022-05-14 17:57:27 +00:00
search_fields = ("contest__key", "contest__name", "user__user__username")
2020-01-21 06:35:58 +00:00
form = ContestParticipationForm
2022-05-14 17:57:27 +00:00
date_hierarchy = "real_start"
2020-01-21 06:35:58 +00:00
def get_queryset(self, request):
2022-05-14 17:57:27 +00:00
return (
super(ContestParticipationAdmin, self)
.get_queryset(request)
.only(
"contest__name",
"contest__format_name",
"contest__format_config",
"user__user__username",
"real_start",
"score",
"cumtime",
"tiebreaker",
"virtual",
)
2020-01-21 06:35:58 +00:00
)
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
2022-05-14 17:57:27 +00:00
if form.changed_data and "is_disqualified" in form.changed_data:
2020-01-21 06:35:58 +00:00
obj.set_disqualified(obj.is_disqualified)
def recalculate_results(self, request, queryset):
count = 0
for participation in queryset:
participation.recompute_results()
count += 1
2022-05-14 17:57:27 +00:00
self.message_user(
request,
ungettext(
"%d participation recalculated.",
"%d participations recalculated.",
count,
)
% count,
)
recalculate_results.short_description = _("Recalculate results")
2020-01-21 06:35:58 +00:00
def username(self, obj):
return obj.user.username
2022-05-14 17:57:27 +00:00
username.short_description = _("username")
username.admin_order_field = "user__user__username"
2020-01-21 06:35:58 +00:00
def show_virtual(self, obj):
2022-05-14 17:57:27 +00:00
return obj.virtual or "-"
show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "virtual"
2023-10-06 08:54:37 +00:00
class ContestsSummaryForm(ModelForm):
class Meta:
widgets = {
"contests": AdminHeavySelect2MultipleWidget(
data_view="contest_select2", attrs={"style": "width: 100%"}
),
}
class ContestsSummaryAdmin(admin.ModelAdmin):
fields = ("key", "contests", "scores")
list_display = ("key",)
search_fields = ("key", "contests__key")
form = ContestsSummaryForm
def save_model(self, request, obj, form, change):
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
obj.refresh_from_db()
obj.results = recalculate_contest_summary_result(obj)
obj.save()