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, make_request_condition
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 (
    ProblemInstance,
    ProblemStatementConfig,
    RankingVisibilityConfig, RegistrationAvailabilityConfig,
)
from oioioi.contests.utils import is_contest_admin, is_contest_basicadmin
from oioioi.problems.forms import (
    AlgorithmTagThroughForm,
    DifficultyTagThroughForm,
    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

[docs]logger = logging.getLogger(__name__)
[docs]class StatementConfigInline(admin.TabularInline):
[docs] model = ProblemStatementConfig
[docs] extra = 1
[docs] form = ProblemStatementConfigForm
[docs] category = _("Advanced")
[docs] def has_add_permission(self, request, obj=None): return is_contest_admin(request)
[docs] def has_change_permission(self, request, obj=None): return is_contest_admin(request)
[docs] def has_delete_permission(self, request, obj=None): return is_contest_admin(request)
[docs]class StatementConfigAdminMixin(object): """Adds :class:`~oioioi.contests.models.ProblemStatementConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super(StatementConfigAdminMixin, self).__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (StatementConfigInline,)
ContestAdmin.mix_in(StatementConfigAdminMixin)
[docs]class RankingVisibilityConfigInline(admin.TabularInline):
[docs] model = RankingVisibilityConfig
[docs] extra = 1
[docs] form = RankingVisibilityConfigForm
[docs] category = _("Advanced")
[docs] def has_add_permission(self, request, obj=None): return is_contest_admin(request)
[docs] def has_change_permission(self, request, obj=None): return is_contest_admin(request)
[docs] def has_delete_permission(self, request, obj=None): return is_contest_admin(request)
[docs]class RankingVisibilityConfigAdminMixin(object): """Adds :class:`~oioioi.contests.models.RankingVisibilityConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super(RankingVisibilityConfigAdminMixin, self).__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (RankingVisibilityConfigInline,)
ContestAdmin.mix_in(RankingVisibilityConfigAdminMixin)
[docs]class RegistrationAvailabilityConfigInline(admin.TabularInline):
[docs] model = RegistrationAvailabilityConfig
[docs] extra = 1
[docs] form = RegistrationAvailabilityConfigForm
[docs] category = _("Advanced")
[docs] def has_add_permission(self, request, obj=None): return is_contest_admin(request)
[docs] def has_change_permission(self, request, obj=None): return is_contest_admin(request)
[docs] def has_delete_permission(self, request, obj=None): return is_contest_admin(request)
[docs]class RegistrationAvailabilityConfigAdminMixin(object): """Adds :class:`~oioioi.contests.models.OpenRegistrationConfig` to an admin panel. """ def __init__(self, *args, **kwargs): super(RegistrationAvailabilityConfigAdminMixin, self).__init__(*args, **kwargs) self.inlines = tuple(self.inlines) + (RegistrationAvailabilityConfigInline,)
ContestAdmin.mix_in(RegistrationAvailabilityConfigAdminMixin)
[docs]class NameInline(admin.StackedInline):
[docs] model = ProblemName
[docs] can_delete = False
[docs] formset = ProblemNameInlineFormSet
[docs] fields = ['name', 'language']
[docs] category = NO_CATEGORY
[docs] def has_add_permission(self, request, obj=None): return True
[docs] def has_change_permission(self, request, obj=None): return True
[docs] def has_delete_permission(self, request, obj=None): return True
[docs]class StatementInline(admin.TabularInline):
[docs] model = ProblemStatement
[docs] can_delete = False
[docs] readonly_fields = ['language', 'content_link']
[docs] fields = readonly_fields
[docs] category = NO_CATEGORY
[docs] def has_add_permission(self, request, obj=None): return False
[docs] def has_change_permission(self, request, obj=None): return True
[docs] def has_delete_permission(self, request, obj=None): return False
[docs] content_link.short_description = _("Content file")
[docs]class AttachmentInline(admin.TabularInline):
[docs] model = ProblemAttachment
[docs] extra = 0
[docs] readonly_fields = ['content_link']
[docs] category = NO_CATEGORY
[docs] content_link.short_description = _("Content file")
[docs]class ProblemInstanceInline(admin.StackedInline):
[docs] model = ProblemInstance
[docs] can_delete = False
[docs] fields = []
[docs] inline_classes = ('collapse open',)
[docs] category = _("Advanced")
[docs] def has_add_permission(self, request, obj=None): return False
[docs] def has_change_permission(self, request, obj=None): return False
[docs] def has_delete_permission(self, request, obj=None): return False
[docs]class ProblemSiteInline(admin.StackedInline):
[docs] model = ProblemSite
[docs] form = ProblemSiteForm
[docs] category = NO_CATEGORY
[docs] def has_add_permission(self, request, obj=None): return True
[docs] def has_change_permission(self, request, obj=None): return True
[docs] def has_delete_permission(self, request, obj=None): return False
[docs]def tag_inline( model, form, verbose_name, verbose_name_plural, extra=0, category=_("Tags"), has_permission_func=lambda self, request, obj=None: True, ): 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
[docs]def _update_queryset_if_problems(db_field, **kwargs): if db_field.name == 'problems': kwargs['queryset'] = Problem.objects.filter( visibility=Problem.VISIBILITY_PUBLIC )
[docs]class BaseTagLocalizationInline(admin.StackedInline):
[docs] formset = LocalizationFormset
[docs]class BaseTagAdmin(admin.ModelAdmin):
[docs] filter_horizontal = ('problems',)
@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, )
[docs]class OriginTagInline(admin.StackedInline): pass
[docs]class OriginTagLocalizationInline(BaseTagLocalizationInline):
[docs] model = OriginTagLocalization
[docs]class OriginTagAdmin(BaseTagAdmin):
[docs] inlines = (OriginTagLocalizationInline,)
[docs] def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super(OriginTagAdmin, self).formfield_for_manytomany( db_field, request, **kwargs )
admin.site.register(OriginTag, OriginTagAdmin)
[docs]class OriginInfoCategoryLocalizationInline(BaseTagLocalizationInline):
[docs] model = OriginInfoCategoryLocalization
[docs]class OriginInfoCategoryAdmin(admin.ModelAdmin):
[docs] 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, )
[docs]class OriginInfoValueInline(admin.StackedInline): pass
[docs]class OriginInfoValueLocalizationInline(BaseTagLocalizationInline):
[docs] model = OriginInfoValueLocalization
[docs]class OriginInfoValueAdmin(admin.ModelAdmin):
[docs] form = OriginInfoValueForm
[docs] inlines = (OriginInfoValueLocalizationInline,)
[docs] def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super(OriginInfoValueAdmin, self).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"), )
[docs]class DifficultyTagInline(admin.StackedInline): pass
[docs]class DifficultyTagLocalizationInline(BaseTagLocalizationInline):
[docs] model = DifficultyTagLocalization
[docs]class DifficultyTagAdmin(BaseTagAdmin):
[docs] inlines = (DifficultyTagLocalizationInline,)
[docs] def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super(DifficultyTagAdmin, self).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"), )
[docs]class AlgorithmTagInline(admin.StackedInline): pass
[docs]class AlgorithmTagLocalizationInline(BaseTagLocalizationInline):
[docs] model = AlgorithmTagLocalization
[docs]class AlgorithmTagAdmin(BaseTagAdmin):
[docs] inlines = (AlgorithmTagLocalizationInline,)
[docs] def formfield_for_manytomany(self, db_field, request, **kwargs): _update_queryset_if_problems(db_field, **kwargs) return super(AlgorithmTagAdmin, self).formfield_for_manytomany( db_field, request, **kwargs )
admin.site.register(AlgorithmTag, AlgorithmTagAdmin)
[docs]class ProblemAdmin(admin.ModelAdmin):
[docs] inlines = ( DifficultyTagInline, AlgorithmTagInline, OriginTagInline, OriginInfoValueInline, NameInline, StatementInline, AttachmentInline, ProblemInstanceInline, ProblemSiteInline, )
[docs] readonly_fields = [ 'author', 'legacy_name', 'short_name', 'controller_name', 'package_backend_name', 'main_problem_instance', 'ascii_name', ]
[docs] exclude = ['contest']
[docs] list_filter = ['short_name']
[docs] class Media(object):
[docs] js = ('problems/admin-origintag.js',)
[docs] def has_add_permission(self, request): return can_add_problems(request)
[docs] def has_change_permission(self, request, obj=None): if obj is None: return self.get_queryset(request).exists() else: return can_admin_problem(request, obj)
[docs] def has_delete_permission(self, request, obj=None): return self.has_change_permission(request, obj)
[docs] 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')
[docs] 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(ProblemAdmin, self).response_change(request, obj)
[docs] 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')
[docs] 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)
[docs] def get_queryset(self, request): queryset = super(ProblemAdmin, self).get_queryset(request) if request.user.is_anonymous: combined = queryset.none() else: combined = request.user.problem_set.all() if request.user.has_perm('problems.problems_db_admin'): combined |= queryset.filter(contest__isnull=True) if is_contest_basicadmin(request): combined |= queryset.filter(contest=request.contest) return combined
[docs] def delete_view(self, request, object_id, extra_context=None): obj = self.get_object(request, unquote(object_id)) response = super(ProblemAdmin, self).delete_view( request, object_id, extra_context ) if isinstance(response, HttpResponseRedirect): return self.redirect_to_list(request, obj) return response
[docs] 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
[docs] def change_view(self, request, object_id, form_url='', extra_context=None): extra_context = extra_context or {} extra_context['categories'] = sorted( set([getattr(inline, 'category', None) for inline in self.inlines]) ) extra_context['no_category'] = NO_CATEGORY return super(ProblemAdmin, self).change_view( request, object_id, form_url, extra_context=extra_context )
[docs]class BaseProblemAdmin(admin.MixinsAdmin):
[docs] default_model_admin = ProblemAdmin
[docs] def _mixins_for_instance(self, request, instance=None): if instance: return instance.controller.mixins_for_admin()
[docs] def reupload_view(self, request, object_id): model_admin = self._find_model_admin(request, object_id) return model_admin.reupload_view(request, object_id)
[docs] def download_view(self, request, object_id): model_admin = self._find_model_admin(request, object_id) return model_admin.download_view(request, object_id)
[docs] def get_urls(self): urls = super(BaseProblemAdmin, self).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) @make_request_condition
[docs]def pending_packages(request): return ProblemPackage.objects.filter(status__in=['?', 'ERR']).exists()
@make_request_condition
[docs]def pending_contest_packages(request): if not request.contest: return False return ProblemPackage.objects.filter( contest=request.contest, status__in=['?', 'ERR'] ).exists()
[docs]class ProblemPackageAdmin(admin.ModelAdmin):
[docs] list_display = [ 'contest', 'problem_name', 'colored_status', 'created_by', 'creation_date', 'package_info', ]
[docs] list_filter = ['status', 'problem_name', 'contest']
[docs] actions = ['delete_selected'] # This allows us to override the action
def __init__(self, *args, **kwargs): super(ProblemPackageAdmin, self).__init__(*args, **kwargs) self.list_display_links = None
[docs] def has_add_permission(self, request): return False
[docs] def has_change_permission(self, request, obj=None): if obj: return False return request.user.is_superuser
[docs] def has_view_permission(self, request, obj=None): return self.has_change_permission(request, obj)
[docs] def has_delete_permission(self, request, obj=None): if not request.user.is_superuser: return False return (not obj) or (obj.status != 'OK')
[docs] 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)
[docs] delete_selected.short_description = ( _("Delete selected %s") % ProblemPackage._meta.verbose_name_plural.title() )
[docs] def colored_status(self, instance): status_to_str = {'OK': 'ok', '?': 'in_prog', 'ERR': 'err'} package_status = status_to_str[instance.status] return format_html( u'<span class="submission-admin prob-pack--{}">{}</span>', package_status, instance.get_status_display(), )
colored_status.short_description = _("Status")
[docs] colored_status.admin_order_field = 'status'
[docs] 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")
[docs] def came_from(self): return reverse('oioioiadmin:problems_problempackage_changelist')
[docs] 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
[docs] 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
[docs] def get_list_display(self, request): items = super(ProblemPackageAdmin, self).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
[docs] def get_list_filter(self, request): items = super(ProblemPackageAdmin, self).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
admin.site.register(ProblemPackage, ProblemPackageAdmin) system_admin_menu_registry.register( 'problempackage_change', _("Problem packages"), lambda request: reverse('oioioiadmin:problems_problempackage_changelist'), condition=pending_packages, order=70, )
[docs]class ContestProblemPackage(ProblemPackage):
[docs] class Meta(object):
[docs] proxy = True
[docs] verbose_name = _("Contest Problem Package")
[docs]class ContestProblemPackageAdmin(ProblemPackageAdmin):
[docs] list_display = [ x for x in ProblemPackageAdmin.list_display if x not in ['contest', 'celery_task_id'] ]
[docs] list_filter = [x for x in ProblemPackageAdmin.list_filter if x != 'contest']
def __init__(self, *args, **kwargs): super(ContestProblemPackageAdmin, self).__init__(*args, **kwargs) self.list_display_links = None
[docs] def get_queryset(self, request): qs = super(ContestProblemPackageAdmin, self).get_queryset(request) return qs.filter( Q(contest=request.contest) | Q(problem__contest=request.contest) )
[docs] def has_change_permission(self, request, obj=None): if obj: return False return is_contest_basicadmin(request)
[docs] def has_delete_permission(self, request, obj=None): return False
[docs] def came_from(self): return reverse('oioioiadmin:problems_contestproblempackage_changelist')
[docs] def get_actions(self, request): actions = super(ContestProblemPackageAdmin, self).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) & pending_contest_packages), order=70, )
[docs]class MainProblemInstanceAdmin(admin.ModelAdmin):
[docs] fields = ('problem', 'short_name')
[docs] readonly_fields = ('problem',)
[docs] def has_add_permission(self, request): return False
[docs] 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)
[docs] def has_delete_permission(self, request, obj=None): return False
[docs] def response_change(self, request, obj): if '_continue' not in request.POST: return redirect('problem_site', obj.problem.problemsite.url_key) else: return super(MainProblemInstanceAdmin, self).response_change(request, obj)
admin.site.register(MainProblemInstance, MainProblemInstanceAdmin)