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)