2020-01-21 06:35:58 +00:00
|
|
|
from functools import partial
|
|
|
|
from operator import itemgetter
|
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
from django.conf.urls import url
|
|
|
|
from django.contrib import admin, messages
|
|
|
|
from django.core.cache import cache
|
|
|
|
from django.core.exceptions import PermissionDenied
|
|
|
|
from django.db.models import Q
|
|
|
|
from django.http import HttpResponseRedirect
|
|
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
from django.utils.html import format_html
|
|
|
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
|
|
|
|
|
|
|
from django_ace import AceWidget
|
2022-05-14 17:57:27 +00:00
|
|
|
from judge.models import (
|
|
|
|
ContestParticipation,
|
|
|
|
ContestProblem,
|
|
|
|
ContestSubmission,
|
|
|
|
Profile,
|
|
|
|
Submission,
|
|
|
|
SubmissionSource,
|
|
|
|
SubmissionTestCase,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
from judge.utils.raw_sql import use_straight_join
|
|
|
|
|
|
|
|
|
|
|
|
class SubmissionStatusFilter(admin.SimpleListFilter):
|
2022-05-14 17:57:27 +00:00
|
|
|
parameter_name = title = "status"
|
|
|
|
__lookups = (
|
|
|
|
("None", _("None")),
|
|
|
|
("NotDone", _("Not done")),
|
|
|
|
("EX", _("Exceptional")),
|
|
|
|
) + Submission.STATUS
|
2020-01-21 06:35:58 +00:00
|
|
|
__handles = set(map(itemgetter(0), Submission.STATUS))
|
|
|
|
|
|
|
|
def lookups(self, request, model_admin):
|
|
|
|
return self.__lookups
|
|
|
|
|
|
|
|
def queryset(self, request, queryset):
|
2022-05-14 17:57:27 +00:00
|
|
|
if self.value() == "None":
|
2020-01-21 06:35:58 +00:00
|
|
|
return queryset.filter(status=None)
|
2022-05-14 17:57:27 +00:00
|
|
|
elif self.value() == "NotDone":
|
|
|
|
return queryset.exclude(status__in=["D", "IE", "CE", "AB"])
|
|
|
|
elif self.value() == "EX":
|
|
|
|
return queryset.exclude(status__in=["D", "CE", "G", "AB"])
|
2020-01-21 06:35:58 +00:00
|
|
|
elif self.value() in self.__handles:
|
|
|
|
return queryset.filter(status=self.value())
|
|
|
|
|
|
|
|
|
|
|
|
class SubmissionResultFilter(admin.SimpleListFilter):
|
2022-05-14 17:57:27 +00:00
|
|
|
parameter_name = title = "result"
|
|
|
|
__lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT
|
2020-01-21 06:35:58 +00:00
|
|
|
__handles = set(map(itemgetter(0), Submission.RESULT))
|
|
|
|
|
|
|
|
def lookups(self, request, model_admin):
|
|
|
|
return self.__lookups
|
|
|
|
|
|
|
|
def queryset(self, request, queryset):
|
2022-05-14 17:57:27 +00:00
|
|
|
if self.value() == "None":
|
2020-01-21 06:35:58 +00:00
|
|
|
return queryset.filter(result=None)
|
2022-05-14 17:57:27 +00:00
|
|
|
elif self.value() == "BAD":
|
|
|
|
return queryset.exclude(result="AC")
|
2020-01-21 06:35:58 +00:00
|
|
|
elif self.value() in self.__handles:
|
|
|
|
return queryset.filter(result=self.value())
|
|
|
|
|
|
|
|
|
|
|
|
class SubmissionTestCaseInline(admin.TabularInline):
|
2022-05-14 17:57:27 +00:00
|
|
|
fields = ("case", "batch", "status", "time", "memory", "points", "total")
|
|
|
|
readonly_fields = ("case", "batch", "total")
|
2020-01-21 06:35:58 +00:00
|
|
|
model = SubmissionTestCase
|
|
|
|
can_delete = False
|
|
|
|
max_num = 0
|
|
|
|
|
|
|
|
|
|
|
|
class ContestSubmissionInline(admin.StackedInline):
|
2022-05-14 17:57:27 +00:00
|
|
|
fields = ("problem", "participation", "points")
|
2020-01-21 06:35:58 +00:00
|
|
|
model = ContestSubmission
|
|
|
|
|
|
|
|
def get_formset(self, request, obj=None, **kwargs):
|
2022-05-14 17:57:27 +00:00
|
|
|
kwargs["formfield_callback"] = partial(
|
|
|
|
self.formfield_for_dbfield, request=request, obj=obj
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
|
|
|
|
|
|
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
2022-05-14 17:57:27 +00:00
|
|
|
submission = kwargs.pop("obj", None)
|
2020-01-21 06:35:58 +00:00
|
|
|
label = None
|
|
|
|
if submission:
|
2022-05-14 17:57:27 +00:00
|
|
|
if db_field.name == "participation":
|
|
|
|
kwargs["queryset"] = ContestParticipation.objects.filter(
|
|
|
|
user=submission.user, contest__problems=submission.problem
|
|
|
|
).only("id", "contest__name")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def label(obj):
|
|
|
|
return obj.contest.name
|
2022-05-14 17:57:27 +00:00
|
|
|
|
|
|
|
elif db_field.name == "problem":
|
|
|
|
kwargs["queryset"] = ContestProblem.objects.filter(
|
|
|
|
problem=submission.problem
|
|
|
|
).only("id", "problem__name", "contest__name")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def label(obj):
|
2022-05-14 17:57:27 +00:00
|
|
|
return pgettext("contest problem", "%(problem)s in %(contest)s") % {
|
|
|
|
"problem": obj.problem.name,
|
|
|
|
"contest": obj.contest.name,
|
2020-01-21 06:35:58 +00:00
|
|
|
}
|
2022-05-14 17:57:27 +00:00
|
|
|
|
|
|
|
field = super(ContestSubmissionInline, self).formfield_for_dbfield(
|
|
|
|
db_field, **kwargs
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
if label is not None:
|
|
|
|
field.label_from_instance = label
|
|
|
|
return field
|
|
|
|
|
|
|
|
|
|
|
|
class SubmissionSourceInline(admin.StackedInline):
|
2022-05-14 17:57:27 +00:00
|
|
|
fields = ("source",)
|
2020-01-21 06:35:58 +00:00
|
|
|
model = SubmissionSource
|
|
|
|
can_delete = False
|
|
|
|
extra = 0
|
|
|
|
|
|
|
|
def get_formset(self, request, obj=None, **kwargs):
|
2022-05-14 17:57:27 +00:00
|
|
|
kwargs.setdefault("widgets", {})["source"] = AceWidget(
|
|
|
|
mode=obj and obj.language.ace, theme=request.profile.ace_theme
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return super().get_formset(request, obj, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
class SubmissionAdmin(admin.ModelAdmin):
|
2022-05-14 17:57:27 +00:00
|
|
|
readonly_fields = ("user", "problem", "date", "judged_date")
|
|
|
|
fields = (
|
|
|
|
"user",
|
|
|
|
"problem",
|
|
|
|
"date",
|
|
|
|
"judged_date",
|
|
|
|
"time",
|
|
|
|
"memory",
|
|
|
|
"points",
|
|
|
|
"language",
|
|
|
|
"status",
|
|
|
|
"result",
|
|
|
|
"case_points",
|
|
|
|
"case_total",
|
|
|
|
"judged_on",
|
|
|
|
"error",
|
|
|
|
)
|
|
|
|
actions = ("judge", "recalculate_score")
|
|
|
|
list_display = (
|
|
|
|
"id",
|
|
|
|
"problem_code",
|
|
|
|
"problem_name",
|
|
|
|
"user_column",
|
|
|
|
"execution_time",
|
|
|
|
"pretty_memory",
|
|
|
|
"points",
|
|
|
|
"language_column",
|
|
|
|
"status",
|
|
|
|
"result",
|
|
|
|
"judge_column",
|
|
|
|
)
|
|
|
|
list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter)
|
|
|
|
search_fields = ("problem__code", "problem__name", "user__user__username")
|
2020-01-21 06:35:58 +00:00
|
|
|
actions_on_top = True
|
|
|
|
actions_on_bottom = True
|
2022-05-14 17:57:27 +00:00
|
|
|
inlines = [
|
|
|
|
SubmissionSourceInline,
|
|
|
|
SubmissionTestCaseInline,
|
|
|
|
ContestSubmissionInline,
|
|
|
|
]
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_queryset(self, request):
|
2022-05-14 17:57:27 +00:00
|
|
|
queryset = Submission.objects.select_related(
|
|
|
|
"problem", "user__user", "language"
|
|
|
|
).only(
|
|
|
|
"problem__code",
|
|
|
|
"problem__name",
|
|
|
|
"user__user__username",
|
|
|
|
"language__name",
|
|
|
|
"time",
|
|
|
|
"memory",
|
|
|
|
"points",
|
|
|
|
"status",
|
|
|
|
"result",
|
2020-01-21 06:35:58 +00:00
|
|
|
)
|
|
|
|
use_straight_join(queryset)
|
2022-05-14 17:57:27 +00:00
|
|
|
if not request.user.has_perm("judge.edit_all_problem"):
|
2020-01-21 06:35:58 +00:00
|
|
|
id = request.profile.id
|
2022-05-14 17:57:27 +00:00
|
|
|
queryset = queryset.filter(
|
|
|
|
Q(problem__authors__id=id) | Q(problem__curators__id=id)
|
|
|
|
).distinct()
|
2020-01-21 06:35:58 +00:00
|
|
|
return queryset
|
|
|
|
|
|
|
|
def has_add_permission(self, request):
|
|
|
|
return False
|
|
|
|
|
|
|
|
def lookup_allowed(self, key, value):
|
2022-05-14 17:57:27 +00:00
|
|
|
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
|
|
|
|
"problem__code",
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
2023-05-08 02:37:22 +00:00
|
|
|
def save_model(self, request, obj, form, change):
|
|
|
|
super().save_model(request, obj, form, change)
|
|
|
|
if "case_points" in form.changed_data or "case_total" in form.changed_data:
|
|
|
|
obj.update_contest()
|
|
|
|
|
2020-01-21 06:35:58 +00:00
|
|
|
def judge(self, request, queryset):
|
2022-05-14 17:57:27 +00:00
|
|
|
if not request.user.has_perm(
|
|
|
|
"judge.rejudge_submission"
|
|
|
|
) or not request.user.has_perm("judge.edit_own_problem"):
|
|
|
|
self.message_user(
|
|
|
|
request,
|
|
|
|
gettext("You do not have the permission to rejudge submissions."),
|
|
|
|
level=messages.ERROR,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return
|
2022-05-14 17:57:27 +00:00
|
|
|
queryset = queryset.order_by("id")
|
|
|
|
if (
|
|
|
|
not request.user.has_perm("judge.rejudge_submission_lot")
|
|
|
|
and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT
|
|
|
|
):
|
|
|
|
self.message_user(
|
|
|
|
request,
|
|
|
|
gettext(
|
|
|
|
"You do not have the permission to rejudge THAT many submissions."
|
|
|
|
),
|
|
|
|
level=messages.ERROR,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return
|
2022-05-14 17:57:27 +00:00
|
|
|
if not request.user.has_perm("judge.edit_all_problem"):
|
2020-01-21 06:35:58 +00:00
|
|
|
id = request.profile.id
|
2022-05-14 17:57:27 +00:00
|
|
|
queryset = queryset.filter(
|
|
|
|
Q(problem__authors__id=id) | Q(problem__curators__id=id)
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
judged = len(queryset)
|
|
|
|
for model in queryset:
|
|
|
|
model.judge(rejudge=True, batch_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.",
|
|
|
|
judged,
|
|
|
|
)
|
|
|
|
% judged,
|
|
|
|
)
|
|
|
|
|
|
|
|
judge.short_description = _("Rejudge the selected submissions")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def recalculate_score(self, request, queryset):
|
2022-05-14 17:57:27 +00:00
|
|
|
if not request.user.has_perm("judge.rejudge_submission"):
|
|
|
|
self.message_user(
|
|
|
|
request,
|
|
|
|
gettext("You do not have the permission to rejudge submissions."),
|
|
|
|
level=messages.ERROR,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return
|
2022-05-14 17:57:27 +00:00
|
|
|
submissions = list(
|
|
|
|
queryset.defer(None)
|
|
|
|
.select_related(None)
|
|
|
|
.select_related("problem")
|
|
|
|
.only(
|
|
|
|
"points",
|
|
|
|
"case_points",
|
|
|
|
"case_total",
|
|
|
|
"problem__partial",
|
|
|
|
"problem__points",
|
|
|
|
)
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
for submission in submissions:
|
2022-05-14 17:57:27 +00:00
|
|
|
submission.points = round(
|
|
|
|
submission.case_points
|
|
|
|
/ submission.case_total
|
|
|
|
* submission.problem.points
|
|
|
|
if submission.case_total
|
|
|
|
else 0,
|
|
|
|
1,
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
not submission.problem.partial
|
|
|
|
and submission.points < submission.problem.points
|
|
|
|
):
|
2020-01-21 06:35:58 +00:00
|
|
|
submission.points = 0
|
|
|
|
submission.save()
|
|
|
|
submission.update_contest()
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
for profile in Profile.objects.filter(
|
|
|
|
id__in=queryset.values_list("user_id", flat=True).distinct()
|
|
|
|
):
|
2020-01-21 06:35:58 +00:00
|
|
|
profile.calculate_points()
|
2022-05-14 17:57:27 +00:00
|
|
|
cache.delete("user_complete:%d" % profile.id)
|
|
|
|
cache.delete("user_attempted:%d" % profile.id)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
for participation in ContestParticipation.objects.filter(
|
2022-05-14 17:57:27 +00:00
|
|
|
id__in=queryset.values_list("contest__participation_id")
|
|
|
|
).prefetch_related("contest"):
|
2020-01-21 06:35:58 +00:00
|
|
|
participation.recompute_results()
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
self.message_user(
|
|
|
|
request,
|
|
|
|
ungettext(
|
|
|
|
"%d submission were successfully rescored.",
|
|
|
|
"%d submissions were successfully rescored.",
|
|
|
|
len(submissions),
|
|
|
|
)
|
|
|
|
% len(submissions),
|
|
|
|
)
|
|
|
|
|
|
|
|
recalculate_score.short_description = _("Rescore the selected submissions")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def problem_code(self, obj):
|
|
|
|
return obj.problem.code
|
2022-05-14 17:57:27 +00:00
|
|
|
|
|
|
|
problem_code.short_description = _("Problem code")
|
|
|
|
problem_code.admin_order_field = "problem__code"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def problem_name(self, obj):
|
|
|
|
return obj.problem.name
|
2022-05-14 17:57:27 +00:00
|
|
|
|
|
|
|
problem_name.short_description = _("Problem name")
|
|
|
|
problem_name.admin_order_field = "problem__name"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def user_column(self, obj):
|
|
|
|
return obj.user.user.username
|
2022-05-14 17:57:27 +00:00
|
|
|
|
|
|
|
user_column.admin_order_field = "user__user__username"
|
|
|
|
user_column.short_description = _("User")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def execution_time(self, obj):
|
2022-05-14 17:57:27 +00:00
|
|
|
return round(obj.time, 2) if obj.time is not None else "None"
|
|
|
|
|
|
|
|
execution_time.short_description = _("Time")
|
|
|
|
execution_time.admin_order_field = "time"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def pretty_memory(self, obj):
|
|
|
|
memory = obj.memory
|
|
|
|
if memory is None:
|
2022-05-14 17:57:27 +00:00
|
|
|
return gettext("None")
|
2020-01-21 06:35:58 +00:00
|
|
|
if memory < 1000:
|
2022-05-14 17:57:27 +00:00
|
|
|
return gettext("%d KB") % memory
|
2020-01-21 06:35:58 +00:00
|
|
|
else:
|
2022-05-14 17:57:27 +00:00
|
|
|
return gettext("%.2f MB") % (memory / 1024)
|
|
|
|
|
|
|
|
pretty_memory.admin_order_field = "memory"
|
|
|
|
pretty_memory.short_description = _("Memory")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def language_column(self, obj):
|
|
|
|
return obj.language.name
|
2022-05-14 17:57:27 +00:00
|
|
|
|
|
|
|
language_column.admin_order_field = "language__name"
|
|
|
|
language_column.short_description = _("Language")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def judge_column(self, obj):
|
2022-05-14 17:57:27 +00:00
|
|
|
return format_html(
|
|
|
|
'<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />',
|
|
|
|
obj.id,
|
|
|
|
)
|
|
|
|
|
|
|
|
judge_column.short_description = ""
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_urls(self):
|
|
|
|
return [
|
2022-05-14 17:57:27 +00:00
|
|
|
url(r"^(\d+)/judge/$", self.judge_view, name="judge_submission_rejudge"),
|
2020-01-21 06:35:58 +00:00
|
|
|
] + super(SubmissionAdmin, self).get_urls()
|
|
|
|
|
|
|
|
def judge_view(self, request, id):
|
2022-05-14 17:57:27 +00:00
|
|
|
if not request.user.has_perm(
|
|
|
|
"judge.rejudge_submission"
|
|
|
|
) or not request.user.has_perm("judge.edit_own_problem"):
|
2020-01-21 06:35:58 +00:00
|
|
|
raise PermissionDenied()
|
|
|
|
submission = get_object_or_404(Submission, id=id)
|
2022-05-14 17:57:27 +00:00
|
|
|
if not request.user.has_perm(
|
|
|
|
"judge.edit_all_problem"
|
|
|
|
) and not submission.problem.is_editor(request.profile):
|
2020-01-21 06:35:58 +00:00
|
|
|
raise PermissionDenied()
|
|
|
|
submission.judge(rejudge=True)
|
2022-05-14 17:57:27 +00:00
|
|
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|