Source code for oioioi.problems.admin

import logging
import urllib.parse

from django.contrib import messages
from django.contrib.admin.actions import delete_selected
from django.contrib.admin.utils import unquote
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import re_path, reverse
from django.utils.html import escape, format_html, mark_safe
from django.utils.translation import gettext_lazy as _

from oioioi.base import admin
from oioioi.base.admin import NO_CATEGORY, system_admin_menu_registry
from oioioi.base.permissions import is_superuser
from oioioi.base.utils import make_html_link, make_html_links
from oioioi.contests.admin import ContestAdmin, contest_site
from oioioi.contests.menu import contest_admin_menu_registry
from oioioi.contests.models import (
    LimitsVisibilityConfig,
    ProblemInstance,
    ProblemStatementConfig,
    RankingVisibilityConfig,
    RegistrationAvailabilityConfig,
)
from oioioi.contests.utils import is_contest_admin, is_contest_basicadmin
from oioioi.problems.forms import (
    AlgorithmTagThroughForm,
    DifficultyTagThroughForm,
    LimitsVisibilityConfigForm,
    LocalizationFormset,
    OriginInfoValueForm,
    OriginInfoValueThroughForm,
    OriginTagThroughForm,
    ProblemNameInlineFormSet,
    ProblemSiteForm,
    ProblemStatementConfigForm,
    RankingVisibilityConfigForm,
    RegistrationAvailabilityConfigForm,
)
from oioioi.problems.models import (
    AlgorithmTag,
    AlgorithmTagLocalization,
    DifficultyTag,
    DifficultyTagLocalization,
    MainProblemInstance,
    OriginInfoCategory,
    OriginInfoCategoryLocalization,
    OriginInfoValue,
    OriginInfoValueLocalization,
    OriginTag,
    OriginTagLocalization,
    Problem,
    ProblemAttachment,
    ProblemName,
    ProblemPackage,
    ProblemSite,
    ProblemStatement,
)
from oioioi.problems.utils import can_add_problems, can_admin_problem, can_modify_tags

logger = logging.getLogger(__name__)


class StatementConfigInline(admin.TabularInline):
    model = ProblemStatementConfig
    extra = 1
    form = ProblemStatementConfigForm
    category = _("Advanced")

    def has_add_permission(self, request, obj=None):
        return is_contest_admin(request)

    def has_change_permission(self, request, obj=None):
        return is_contest_admin(request)

    def has_delete_permission(self, request, obj=None):
        return is_contest_admin(request)


[docs] class StatementConfigAdminMixin: """Adds :class:`~oioioi.contests.models.ProblemStatementConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (StatementConfigInline,)
ContestAdmin.mix_in(StatementConfigAdminMixin) class RankingVisibilityConfigInline(admin.TabularInline): model = RankingVisibilityConfig extra = 1 form = RankingVisibilityConfigForm category = _("Advanced") def has_add_permission(self, request, obj=None): return is_contest_admin(request) def has_change_permission(self, request, obj=None): return is_contest_admin(request) def has_delete_permission(self, request, obj=None): return is_contest_admin(request) class RankingVisibilityConfigAdminMixin: """Adds :class:`~oioioi.contests.models.RankingVisibilityConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (RankingVisibilityConfigInline,) ContestAdmin.mix_in(RankingVisibilityConfigAdminMixin) class LimitsVisibilityConfigInline(admin.TabularInline): model = LimitsVisibilityConfig extra = 1 form = LimitsVisibilityConfigForm category = _("Advanced") def has_add_permission(self, request, obj=None): return is_contest_admin(request) def has_change_permission(self, request, obj=None): return is_contest_admin(request) def has_delete_permission(self, request, obj=None): return is_contest_admin(request) class LimitsVisibilityConfigAdminMixin: """Adds :class:`~oioioi.contests.models.LimitsVisibilityConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (LimitsVisibilityConfigInline,) ContestAdmin.mix_in(LimitsVisibilityConfigAdminMixin) class RegistrationAvailabilityConfigInline(admin.TabularInline): model = RegistrationAvailabilityConfig extra = 1 form = RegistrationAvailabilityConfigForm category = _("Advanced") def has_add_permission(self, request, obj=None): return is_contest_admin(request) def has_change_permission(self, request, obj=None): return is_contest_admin(request) def has_delete_permission(self, request, obj=None): return is_contest_admin(request) class RegistrationAvailabilityConfigAdminMixin: """Adds :class:`~oioioi.contests.models.OpenRegistrationConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (RegistrationAvailabilityConfigInline,) ContestAdmin.mix_in(RegistrationAvailabilityConfigAdminMixin) class NameInline(admin.StackedInline): model = ProblemName can_delete = False formset = ProblemNameInlineFormSet fields = ["name", "language"] category = NO_CATEGORY def has_add_permission(self, request, obj=None): return True def has_change_permission(self, request, obj=None): return True def has_delete_permission(self, request, obj=None): return True class StatementInline(admin.TabularInline): model = ProblemStatement can_delete = False readonly_fields = ["language", "content_link"] fields = readonly_fields category = NO_CATEGORY def has_add_permission(self, request, obj=None): return False def has_change_permission(self, request, obj=None): return True def has_delete_permission(self, request, obj=None): return False def content_link(self, instance): if instance.id is not None: href = reverse("show_statement", kwargs={"statement_id": str(instance.id)}) return make_html_link(href, instance.content.name) return None content_link.short_description = _("Content file") class AttachmentInline(admin.TabularInline): model = ProblemAttachment extra = 0 readonly_fields = ["content_link"] category = NO_CATEGORY def content_link(self, instance): if instance.id is not None: href = reverse("show_problem_attachment", kwargs={"attachment_id": str(instance.id)}) return make_html_link(href, instance.content.name) return None content_link.short_description = _("Content file") class ProblemInstanceInline(admin.StackedInline): model = ProblemInstance can_delete = False fields = [] inline_classes = ("collapse open",) category = _("Advanced") def has_add_permission(self, request, obj=None): return False def has_change_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): return False class ProblemSiteInline(admin.StackedInline): model = ProblemSite form = ProblemSiteForm category = NO_CATEGORY def has_add_permission(self, request, obj=None): return True def has_change_permission(self, request, obj=None): return True def has_delete_permission(self, request, obj=None): return False def tag_inline( model, form, verbose_name, verbose_name_plural, extra=0, category=None, has_permission_func=lambda self, request, obj=None: True, ): if category is None: category = _("Tags") def decorator(cls): cls.model = model cls.form = form cls.verbose_name = verbose_name cls.verbose_name_plural = verbose_name_plural cls.extra = extra cls.category = category cls.has_add_permission = has_permission_func cls.has_change_permission = has_permission_func cls.has_delete_permission = has_permission_func cls.has_view_permission = has_permission_func return cls return decorator def _update_queryset_if_problems(db_field, **kwargs): if db_field.name == "problems": kwargs["queryset"] = Problem.objects.filter(visibility=Problem.VISIBILITY_PUBLIC) class BaseTagLocalizationInline(admin.StackedInline): formset = LocalizationFormset def has_add_permission(self, request, obj=None): return can_modify_tags(request, obj) def has_change_permission(self, request, obj=None): return can_modify_tags(request, obj) def has_delete_permission(self, request, obj=None): return can_modify_tags(request, obj) class BaseTagAdmin(admin.ModelAdmin): def has_add_permission(self, request, obj=None): return can_modify_tags(request, obj) def has_change_permission(self, request, obj=None): return can_modify_tags(request, obj) def has_delete_permission(self, request, obj=None): return can_modify_tags(request, obj) @tag_inline( model=OriginTag.problems.through, form=OriginTagThroughForm, verbose_name=_("origin tag"), verbose_name_plural=_("origin tags"), has_permission_func=lambda self, request, obj=None: request.user.is_superuser, ) class OriginTagInline(admin.StackedInline): pass class OriginTagLocalizationInline(BaseTagLocalizationInline): model = OriginTagLocalization class OriginTagAdmin(BaseTagAdmin): inlines = (OriginTagLocalizationInline,) exclude = ["problems"] def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs) admin.site.register(OriginTag, OriginTagAdmin) class OriginInfoCategoryLocalizationInline(BaseTagLocalizationInline): model = OriginInfoCategoryLocalization class OriginInfoCategoryAdmin(admin.ModelAdmin): inlines = (OriginInfoCategoryLocalizationInline,) admin.site.register(OriginInfoCategory, OriginInfoCategoryAdmin) @tag_inline( model=OriginInfoValue.problems.through, form=OriginInfoValueThroughForm, verbose_name=_("origin information"), verbose_name_plural=_("additional origin information"), has_permission_func=lambda self, request, obj=None: request.user.is_superuser, ) class OriginInfoValueInline(admin.StackedInline): pass class OriginInfoValueLocalizationInline(BaseTagLocalizationInline): model = OriginInfoValueLocalization class OriginInfoValueAdmin(admin.ModelAdmin): form = OriginInfoValueForm inlines = (OriginInfoValueLocalizationInline,) exclude = ["problems"] def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs) admin.site.register(OriginInfoValue, OriginInfoValueAdmin) @tag_inline( model=DifficultyTag.problems.through, form=DifficultyTagThroughForm, verbose_name=_("Difficulty Tag"), verbose_name_plural=_("Difficulty Tags"), has_permission_func=lambda self, request, obj=None: can_modify_tags(request, obj), ) class DifficultyTagInline(admin.StackedInline): pass class DifficultyTagLocalizationInline(BaseTagLocalizationInline): model = DifficultyTagLocalization class DifficultyTagAdmin(BaseTagAdmin): inlines = (DifficultyTagLocalizationInline,) def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs) admin.site.register(DifficultyTag, DifficultyTagAdmin) @tag_inline( model=AlgorithmTag.problems.through, form=AlgorithmTagThroughForm, verbose_name=_("Algorithm Tag"), verbose_name_plural=_("Algorithm Tags"), has_permission_func=lambda self, request, obj=None: can_modify_tags(request, obj), ) class AlgorithmTagInline(admin.StackedInline): pass class AlgorithmTagLocalizationInline(BaseTagLocalizationInline): model = AlgorithmTagLocalization class AlgorithmTagAdmin(BaseTagAdmin): inlines = (AlgorithmTagLocalizationInline,) def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs) admin.site.register(AlgorithmTag, AlgorithmTagAdmin) class ProblemAdmin(admin.ModelAdmin): tag_inlines = ( DifficultyTagInline, AlgorithmTagInline, ) inlines = ( DifficultyTagInline, AlgorithmTagInline, OriginTagInline, OriginInfoValueInline, NameInline, StatementInline, AttachmentInline, ProblemInstanceInline, ProblemSiteInline, ) readonly_fields = [ "author", "legacy_name", "short_name", "controller_name", "package_backend_name", "main_problem_instance", "ascii_name", ] exclude = ["contest"] list_filter = ["short_name"] class Media: js = ("problems/admin-origintag.js",) def has_add_permission(self, request): return can_add_problems(request) def has_change_permission(self, request, obj=None): if obj is None: return self.get_queryset(request).exists() else: return can_modify_tags(request, obj) def has_delete_permission(self, request, obj=None): if obj is None: return self.get_queryset(request).exists() return can_admin_problem(request, obj) def redirect_to_list(self, request, problem): if problem.contest: return redirect("oioioiadmin:contests_probleminstance_changelist", contest_id=problem.contest.id) else: return redirect("problemset_all_problems") def response_change(self, request, obj): if "_continue" not in request.POST and obj.problemsite: return redirect("problem_site", obj.problemsite.url_key) else: return super().response_change(request, obj) def add_view(self, request, form_url="", extra_context=None): if request.contest: return redirect("add_or_update_problem", contest_id=request.contest.id) else: return redirect("add_or_update_problem") def download_view(self, request, object_id): problem = self.get_object(request, unquote(object_id)) if not problem: raise Http404 if not self.has_change_permission(request, problem): raise PermissionDenied try: return problem.package_backend.pack(problem) except NotImplementedError: self.message_user(request, _("Package not available for problem %s.") % (problem,)) return self.redirect_to_list(request, problem) def get_queryset(self, request): queryset = super().get_queryset(request) if request.user.is_anonymous: combined = queryset.none() else: combined = request.user.problem_set.all() if request.user.is_superuser: return queryset if request.user.has_perm("problems.problems_db_admin") or request.user.has_perm("problems.can_modify_tags"): combined |= queryset.filter(visibility=Problem.VISIBILITY_PUBLIC) if is_contest_basicadmin(request): combined |= queryset.filter(contest=request.contest) return combined def delete_view(self, request, object_id, extra_context=None): obj = self.get_object(request, unquote(object_id)) response = super().delete_view(request, object_id, extra_context) if isinstance(response, HttpResponseRedirect): return self.redirect_to_list(request, obj) return response def get_readonly_fields(self, request, obj=None): if not (request.user.is_superuser or is_contest_admin(request)): return ["visibility"] + self.readonly_fields return self.readonly_fields def change_view(self, request, object_id, form_url="", extra_context=None): problem = self.get_object(request, unquote(object_id)) extra_context = extra_context or {} extra_context["categories"] = sorted({getattr(inline, "category", None) for inline in self.get_inlines(request, problem)}) if problem is not None and can_admin_problem(request, problem): extra_context["no_category"] = NO_CATEGORY if request.user.has_perm("problems.problems_db_admin"): extra_context["no_category"] = NO_CATEGORY return super().change_view(request, object_id, form_url, extra_context=extra_context) def get_inlines(self, request, obj): if obj is not None and can_admin_problem(request, obj): return super().get_inlines(request, obj) elif can_modify_tags(request, obj): return self.tag_inlines else: return () class BaseProblemAdmin(admin.MixinsAdmin): default_model_admin = ProblemAdmin def _mixins_for_instance(self, request, instance=None): if instance: return instance.controller.mixins_for_admin() def reupload_view(self, request, object_id): model_admin = self._find_model_admin(request, object_id) return model_admin.reupload_view(request, object_id) def download_view(self, request, object_id): model_admin = self._find_model_admin(request, object_id) return model_admin.download_view(request, object_id) def get_urls(self): urls = super().get_urls() extra_urls = [ re_path( r"^(\d+)/download/$", self.download_view, name="problems_problem_download", ) ] return extra_urls + urls admin.site.register(Problem, BaseProblemAdmin) class ProblemPackageAdmin(admin.ModelAdmin): list_display = [ "contest", "problem_name", "colored_status", "created_by", "creation_date", "package_info", ] list_filter = ["status", "problem_name", "contest"] actions = ["delete_selected"] # This allows us to override the action def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.list_display_links = None def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): if obj: return False return request.user.is_superuser def has_view_permission(self, request, obj=None): return self.has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): if not request.user.is_superuser: return False return (not obj) or (obj.status != "OK") def delete_selected(self, request, queryset): # We use processed ProblemPackage instances to store orignal package # files. if queryset.filter(status="OK").exists(): messages.error(request, _("Cannot delete a processed Problem Package")) else: return delete_selected(self, request, queryset) delete_selected.short_description = _("Delete selected %s") % ProblemPackage._meta.verbose_name_plural.title() def colored_status(self, instance): status_to_str = {"OK": "ok", "?": "in_prog", "ERR": "err"} package_status = status_to_str[instance.status] return format_html( '<span class="submission-admin prob-pack--{}">{}</span>', package_status, instance.get_status_display(), ) colored_status.short_description = _("Status") colored_status.admin_order_field = "status" def package_info(self, instance): if instance.info: return mark_safe(escape(instance.info).replace("\n", "<br>")) else: return "-" package_info.short_description = _("Package information") def came_from(self): return reverse("oioioiadmin:problems_problempackage_changelist") def inline_actions(self, instance, contest): actions = [] if instance.package_file: package_download = reverse("download_package", kwargs={"package_id": str(instance.id)}) actions.append((package_download, _("Package download"))) if instance.status == "OK" and instance.problem: problem = instance.problem if (not problem.contest) or (problem.contest == contest): problem_view = ( reverse("oioioiadmin:problems_problem_change", args=(problem.id,)) + "?" + urllib.parse.urlencode({"came_from": self.came_from()}) ) actions.append((problem_view, _("Edit problem"))) if instance.status == "ERR" and instance.traceback: traceback_view = reverse("download_package_traceback", kwargs={"package_id": str(instance.id)}) actions.append((traceback_view, _("Error details"))) return actions def actions_field(self, contest): def inner(instance): inline_actions = self.inline_actions(instance, contest) return make_html_links(inline_actions) inner.short_description = _("Actions") return inner def get_list_display(self, request): items = super().get_list_display(request) + [self.actions_field(request.contest)] if not is_contest_admin(request): disallowed_items = ["created_by", "actions_field"] items = [item for item in items if item not in disallowed_items] return items def get_list_filter(self, request): items = super().get_list_filter(request) if not is_contest_admin(request): disallowed_items = ["created_by"] items = [item for item in items if item not in disallowed_items] return items def get_custom_list_select_related(self): return super().get_custom_list_select_related() + [ "problem", "problem__contest", ] admin.site.register(ProblemPackage, ProblemPackageAdmin) system_admin_menu_registry.register( "problempackage_change", _("Problem packages"), lambda request: reverse("oioioiadmin:problems_problempackage_changelist"), order=70, ) class ContestProblemPackage(ProblemPackage): class Meta: proxy = True verbose_name = _("Contest Problem Package") class ContestProblemPackageAdmin(ProblemPackageAdmin): list_display = [x for x in ProblemPackageAdmin.list_display if x not in ["contest", "celery_task_id"]] list_filter = [x for x in ProblemPackageAdmin.list_filter if x != "contest"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.list_display_links = None def get_queryset(self, request): qs = super().get_queryset(request) return qs.filter(Q(contest=request.contest) | Q(problem__contest=request.contest)) def has_change_permission(self, request, obj=None): if obj: return False return is_contest_basicadmin(request) def has_delete_permission(self, request, obj=None): return False def came_from(self): return reverse("oioioiadmin:problems_contestproblempackage_changelist") def get_actions(self, request): actions = super().get_actions(request) if "delete_selected" in actions: del actions["delete_selected"] return actions contest_site.contest_register(ContestProblemPackage, ContestProblemPackageAdmin) contest_admin_menu_registry.register( "problempackage_change", _("Problem packages"), lambda request: reverse("oioioiadmin:problems_contestproblempackage_changelist"), condition=~is_superuser, order=70, ) class MainProblemInstanceAdmin(admin.ModelAdmin): fields = ("problem", "short_name") readonly_fields = ("problem",) def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): if obj is None: return False problem = obj.problem if problem.main_problem_instance != obj: return False return can_admin_problem(request, problem) def has_delete_permission(self, request, obj=None): return False def response_change(self, request, obj): if "_continue" not in request.POST: return redirect("problem_site", obj.problem.problemsite.url_key) else: return super().response_change(request, obj) admin.site.register(MainProblemInstance, MainProblemInstanceAdmin)