NDOJ/judge/admin/problem.py

488 lines
16 KiB
Python
Raw Permalink Normal View History

2020-01-21 06:35:58 +00:00
from operator import attrgetter
from django import forms
from django.contrib import admin, messages
from django.db import transaction, IntegrityError
2022-04-04 22:13:23 +00:00
from django.db.models import Q, Avg, Count
2022-04-04 22:04:40 +00:00
from django.db.models.aggregates import StdDev
2022-06-20 15:16:58 +00:00
from django.forms import ModelForm, TextInput
2020-01-21 06:35:58 +00:00
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext
2022-06-12 07:57:46 +00:00
from django_ace import AceWidget
2022-09-02 23:03:54 +00:00
from django.utils import timezone
from django.core.exceptions import ValidationError
2022-06-12 07:57:46 +00:00
2020-01-21 06:35:58 +00:00
from reversion.admin import VersionAdmin
2022-04-26 03:00:15 +00:00
from reversion_compare.admin import CompareVersionAdmin
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
from judge.models import (
LanguageLimit,
2022-06-12 07:57:46 +00:00
LanguageTemplate,
2022-05-14 17:57:27 +00:00
Problem,
ProblemTranslation,
Profile,
Solution,
2022-09-04 17:31:49 +00:00
Notification,
2022-05-14 17:57:27 +00:00
)
2023-10-10 22:38:48 +00:00
from judge.models.notification import make_notification
2022-05-14 17:57:27 +00:00
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget,
AdminSelect2Widget,
CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget,
)
2023-10-12 13:56:53 +00:00
from judge.utils.problems import user_editable_ids, user_tester_ids
2020-01-21 06:35:58 +00:00
2022-08-31 05:23:23 +00:00
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
2020-01-21 06:35:58 +00:00
class ProblemForm(ModelForm):
2022-05-14 17:57:27 +00:00
change_message = forms.CharField(
max_length=256, label="Edit reason", required=False
)
2022-08-31 05:23:23 +00:00
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS)
2020-01-21 06:35:58 +00:00
def __init__(self, *args, **kwargs):
super(ProblemForm, self).__init__(*args, **kwargs)
2022-05-14 17:57:27 +00:00
self.fields["authors"].widget.can_add_related = False
self.fields["curators"].widget.can_add_related = False
self.fields["testers"].widget.can_add_related = False
self.fields["banned_users"].widget.can_add_related = False
self.fields["change_message"].widget.attrs.update(
{
"placeholder": gettext("Describe the changes you made (optional)"),
}
)
2020-01-21 06:35:58 +00:00
def clean_code(self):
code = self.cleaned_data.get("code")
if self.instance.pk:
return code
if Problem.objects.filter(code=code).exists():
raise ValidationError(_("A problem with this code already exists."))
return code
2022-08-31 05:23:23 +00:00
def clean(self):
memory_unit = self.cleaned_data.get("memory_unit", "KB")
if memory_unit == "MB":
self.cleaned_data["memory_limit"] *= 1024
2022-09-02 23:03:54 +00:00
date = self.cleaned_data.get("date")
if not date or date > timezone.now():
self.cleaned_data["date"] = timezone.now()
2022-08-31 05:23:23 +00:00
return self.cleaned_data
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", attrs={"style": "width: 100%"}
),
"curators": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"testers": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"banned_users": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2", attrs={"style": "width: 100%"}
),
"types": AdminSelect2MultipleWidget,
"group": AdminSelect2Widget,
2022-06-26 05:07:34 +00:00
"memory_limit": TextInput(attrs={"size": "20"}),
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("problem_preview")
)
2020-01-21 06:35:58 +00:00
class ProblemCreatorListFilter(admin.SimpleListFilter):
2022-05-14 17:57:27 +00:00
title = parameter_name = "creator"
2020-01-21 06:35:58 +00:00
def lookups(self, request, model_admin):
2022-05-14 17:57:27 +00:00
queryset = Profile.objects.exclude(authored_problems=None).values_list(
"user__username", flat=True
)
2020-01-21 06:35:58 +00:00
return [(name, name) for name in queryset]
def queryset(self, request, queryset):
if self.value() is None:
return queryset
return queryset.filter(authors__user__username=self.value())
class LanguageLimitInlineForm(ModelForm):
2022-08-31 05:23:23 +00:00
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS, label=_("Memory unit"))
2020-01-21 06:35:58 +00:00
class Meta:
2022-08-31 05:23:23 +00:00
widgets = {
"language": AdminSelect2Widget,
"memory_limit": TextInput(attrs={"size": "10"}),
}
def clean(self):
if not self.cleaned_data.get("language"):
self.cleaned_data["DELETE"] = True
if (
self.cleaned_data.get("memory_limit")
and self.cleaned_data.get("memory_unit") == "MB"
):
self.cleaned_data["memory_limit"] *= 1024
return self.cleaned_data
2020-01-21 06:35:58 +00:00
class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit
2022-08-31 05:23:23 +00:00
fields = ("language", "time_limit", "memory_limit", "memory_unit")
2020-01-21 06:35:58 +00:00
form = LanguageLimitInlineForm
extra = 0
2020-01-21 06:35:58 +00:00
2022-06-12 07:57:46 +00:00
class LanguageTemplateInlineForm(ModelForm):
class Meta:
widgets = {
"language": AdminSelect2Widget,
"source": AceWidget(width="600px", height="200px", toolbar=False),
}
class LanguageTemplateInline(admin.TabularInline):
model = LanguageTemplate
fields = ("language", "source")
form = LanguageTemplateInlineForm
extra = 0
2022-06-12 07:57:46 +00:00
2020-01-21 06:35:58 +00:00
class ProblemSolutionForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
2022-05-14 17:57:27 +00:00
self.fields["authors"].widget.can_add_related = False
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", 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["content"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("solution_preview")
)
2020-01-21 06:35:58 +00:00
class ProblemSolutionInline(admin.StackedInline):
model = Solution
2022-05-14 17:57:27 +00:00
fields = ("is_public", "publish_on", "authors", "content")
2020-01-21 06:35:58 +00:00
form = ProblemSolutionForm
extra = 0
class ProblemTranslationForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
2022-05-14 17:57:27 +00:00
widgets = {
"description": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("problem_preview")
)
}
2020-01-21 06:35:58 +00:00
class ProblemTranslationInline(admin.StackedInline):
model = ProblemTranslation
2022-05-14 17:57:27 +00:00
fields = ("language", "name", "description")
2020-01-21 06:35:58 +00:00
form = ProblemTranslationForm
extra = 0
2022-04-26 03:00:15 +00:00
class ProblemAdmin(CompareVersionAdmin):
2020-01-21 06:35:58 +00:00
fieldsets = (
2022-05-14 17:57:27 +00:00
(
None,
{
"fields": (
"code",
"name",
"is_public",
2022-09-04 17:31:49 +00:00
"organizations",
2022-05-14 17:57:27 +00:00
"date",
"authors",
"curators",
"testers",
"description",
2022-08-31 03:50:08 +00:00
"pdf_description",
2022-05-14 17:57:27 +00:00
"license",
),
},
),
(
_("Social Media"),
{"classes": ("collapse",), "fields": ("og_image", "summary")},
),
(_("Taxonomy"), {"fields": ("types", "group")}),
(_("Points"), {"fields": (("points", "partial"), "short_circuit")}),
2022-08-31 05:23:23 +00:00
(_("Limits"), {"fields": ("time_limit", ("memory_limit", "memory_unit"))}),
2022-05-14 17:57:27 +00:00
(_("Language"), {"fields": ("allowed_languages",)}),
(_("Justice"), {"fields": ("banned_users",)}),
(_("History"), {"fields": ("change_message",)}),
)
list_display = [
"code",
"name",
"show_authors",
2022-09-02 23:03:54 +00:00
"date",
2022-05-14 17:57:27 +00:00
"points",
"is_public",
"show_public",
]
2022-09-02 23:03:54 +00:00
ordering = ["-date"]
2022-05-14 17:57:27 +00:00
search_fields = (
"code",
"name",
"authors__user__username",
"curators__user__username",
2020-01-21 06:35:58 +00:00
)
2022-05-14 17:57:27 +00:00
inlines = [
LanguageLimitInline,
2022-06-12 07:57:46 +00:00
LanguageTemplateInline,
2022-05-14 17:57:27 +00:00
ProblemSolutionInline,
ProblemTranslationInline,
]
2020-01-21 06:35:58 +00:00
list_max_show_all = 1000
actions_on_top = True
actions_on_bottom = True
2022-05-14 17:57:27 +00:00
list_filter = ("is_public", ProblemCreatorListFilter)
2020-01-21 06:35:58 +00:00
form = ProblemForm
2022-05-14 17:57:27 +00:00
date_hierarchy = "date"
2020-01-21 06:35:58 +00:00
def get_actions(self, request):
actions = super(ProblemAdmin, self).get_actions(request)
2022-05-14 17:57:27 +00:00
if request.user.has_perm("judge.change_public_visibility"):
func, name, desc = self.get_action("make_public")
2020-01-21 06:35:58 +00:00
actions[name] = (func, name, desc)
2022-05-14 17:57:27 +00:00
func, name, desc = self.get_action("make_private")
2020-01-21 06:35:58 +00:00
actions[name] = (func, name, desc)
return actions
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.change_public_visibility"):
fields += ("is_public",)
2020-01-21 06:35:58 +00:00
return fields
def show_authors(self, obj):
2022-05-14 17:57:27 +00:00
return ", ".join(map(attrgetter("user.username"), obj.authors.all()))
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
show_authors.short_description = _("Authors")
2020-01-21 06:35:58 +00:00
def show_public(self, obj):
2022-05-14 17:57:27 +00:00
return format_html(
'<a href="{1}">{0}</a>', gettext("View on site"), obj.get_absolute_url()
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
show_public.short_description = ""
2020-01-21 06:35:58 +00:00
def _rescore(self, request, problem_id):
from judge.tasks import rescore_problem
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
transaction.on_commit(rescore_problem.s(problem_id).delay)
def make_public(self, request, queryset):
count = queryset.update(is_public=True)
2022-05-14 17:57:27 +00:00
for problem_id in queryset.values_list("id", flat=True):
2020-01-21 06:35:58 +00:00
self._rescore(request, problem_id)
2022-05-14 17:57:27 +00:00
self.message_user(
request,
ungettext(
"%d problem successfully marked as public.",
"%d problems successfully marked as public.",
count,
)
% count,
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
make_public.short_description = _("Mark problems as public")
2020-01-21 06:35:58 +00:00
def make_private(self, request, queryset):
count = queryset.update(is_public=False)
2022-05-14 17:57:27 +00:00
for problem_id in queryset.values_list("id", flat=True):
2020-01-21 06:35:58 +00:00
self._rescore(request, problem_id)
2022-05-14 17:57:27 +00:00
self.message_user(
request,
ungettext(
"%d problem successfully marked as private.",
"%d problems successfully marked as private.",
count,
)
% count,
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
make_private.short_description = _("Mark problems as private")
2020-01-21 06:35:58 +00:00
def get_queryset(self, request):
2022-05-14 17:57:27 +00:00
queryset = Problem.objects.prefetch_related("authors__user")
if request.user.has_perm("judge.edit_all_problem"):
2020-01-21 06:35:58 +00:00
return queryset
access = Q()
2022-05-14 17:57:27 +00:00
if request.user.has_perm("judge.edit_public_problem"):
2020-01-21 06:35:58 +00:00
access |= Q(is_public=True)
2022-05-14 17:57:27 +00:00
if request.user.has_perm("judge.edit_own_problem"):
access |= Q(authors__id=request.profile.id) | Q(
curators__id=request.profile.id
)
2022-11-03 06:47:20 +00:00
return queryset.filter(access).distinct() if access else queryset.none()
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 request.user.has_perm("judge.edit_all_problem") or obj is None:
2020-01-21 06:35:58 +00:00
return True
2022-05-14 17:57:27 +00:00
if request.user.has_perm("judge.edit_public_problem") and obj.is_public:
2020-01-21 06:35:58 +00:00
return True
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.edit_own_problem"):
2020-01-21 06:35:58 +00:00
return False
return obj.is_editor(request.profile)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
2022-05-14 17:57:27 +00:00
if db_field.name == "allowed_languages":
kwargs["widget"] = CheckboxSelectMultipleWithSelectAll()
return super(ProblemAdmin, self).formfield_for_manytomany(
db_field, request, **kwargs
)
2020-01-21 06:35:58 +00:00
def get_form(self, *args, **kwargs):
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
2022-05-14 17:57:27 +00:00
form.base_fields["authors"].queryset = Profile.objects.all()
2020-01-21 06:35:58 +00:00
return form
def save_model(self, request, obj, form, change):
2022-09-04 17:31:49 +00:00
form.changed_data.remove("memory_unit")
2020-05-02 18:43:50 +00:00
super().save_model(request, obj, form, change)
2022-05-14 17:57:27 +00:00
if form.changed_data and any(
f in form.changed_data for f in ("is_public", "points", "partial")
):
2020-01-21 06:35:58 +00:00
self._rescore(request, obj.id)
2022-07-20 08:06:15 +00:00
def save_related(self, request, form, formsets, change):
2023-10-12 13:56:53 +00:00
editors = set()
testers = set()
if "curators" in form.changed_data or "authors" in form.changed_data:
editors = set(form.instance.editor_ids)
if "testers" in form.changed_data:
testers = set(form.instance.tester_ids)
2022-07-20 08:06:15 +00:00
super().save_related(request, form, formsets, change)
obj = form.instance
obj.curators.add(request.profile)
2023-10-12 13:56:53 +00:00
if "curators" in form.changed_data or "authors" in form.changed_data:
del obj.editor_ids
editors = editors.union(set(obj.editor_ids))
if "testers" in form.changed_data:
del obj.tester_ids
testers = testers.union(set(obj.tester_ids))
for editor in editors:
user_editable_ids.dirty(editor)
for tester in testers:
user_tester_ids.dirty(tester)
2022-09-04 17:31:49 +00:00
# Create notification
if "is_public" in form.changed_data or "organizations" in form.changed_data:
2022-09-04 17:31:49 +00:00
users = set(obj.authors.all())
2022-09-06 02:01:41 +00:00
users = users.union(users, set(obj.curators.all()))
orgs = []
2022-09-04 17:31:49 +00:00
if obj.organizations.count() > 0:
for org in obj.organizations.all():
users = users.union(users, set(org.admins.all()))
orgs.append(org.name)
2022-09-04 17:31:49 +00:00
else:
admins = Profile.objects.filter(user__is_superuser=True).all()
users = users.union(users, admins)
link = reverse_lazy("admin:judge_problem_change", args=(obj.id,))
2022-09-04 17:32:10 +00:00
html = f'<a href="{link}">{obj.name}</a>'
category = "Problem public: " + str(obj.is_public)
if orgs:
category += " (" + ", ".join(orgs) + ")"
2023-10-11 01:49:23 +00:00
make_notification(users, category, html, request.profile)
2022-07-20 08:06:15 +00:00
2020-01-21 06:35:58 +00:00
def construct_change_message(self, request, form, *args, **kwargs):
2022-05-14 17:57:27 +00:00
if form.cleaned_data.get("change_message"):
return form.cleaned_data["change_message"]
return super(ProblemAdmin, self).construct_change_message(
request, form, *args, **kwargs
)
2022-03-10 05:38:29 +00:00
class ProblemPointsVoteAdmin(admin.ModelAdmin):
2022-05-14 17:57:27 +00:00
list_display = (
"vote_points",
"voter",
"voter_rating",
"voter_point",
"problem_name",
"problem_code",
"problem_points",
)
search_fields = ("voter__user__username", "problem__code", "problem__name")
readonly_fields = (
"voter",
"problem",
"problem_code",
"problem_points",
"voter_rating",
"voter_point",
)
2022-03-10 05:38:29 +00:00
def has_change_permission(self, request, obj=None):
if obj is None:
2022-05-14 17:57:27 +00:00
return request.user.has_perm("judge.edit_own_problem")
2022-03-10 05:38:29 +00:00
return obj.problem.is_editable_by(request.user)
2022-04-04 22:43:56 +00:00
def lookup_allowed(self, key, value):
return True
2022-03-10 17:27:52 +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"
2022-03-10 17:27:52 +00:00
def problem_points(self, obj):
return obj.problem.points
2022-05-14 17:57:27 +00:00
problem_points.short_description = _("Points")
problem_points.admin_order_field = "problem__points"
2022-03-10 17:37:18 +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"
2022-03-10 17:27:52 +00:00
2022-03-31 06:28:40 +00:00
def voter_rating(self, obj):
return obj.voter.rating
2022-05-14 17:57:27 +00:00
voter_rating.short_description = _("Voter rating")
voter_rating.admin_order_field = "voter__rating"
2022-03-31 06:28:40 +00:00
def voter_point(self, obj):
return round(obj.voter.performance_points)
2022-05-14 17:57:27 +00:00
voter_point.short_description = _("Voter point")
voter_point.admin_order_field = "voter__performance_points"
2022-03-31 06:28:40 +00:00
2022-03-10 17:37:18 +00:00
def vote_points(self, obj):
return obj.points
2022-05-14 17:57:27 +00:00
vote_points.short_description = _("Vote")