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 (
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, 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(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)
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(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)
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(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)
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=_("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
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):
filter_horizontal = ('problems',)
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(OriginTagAdmin, self).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(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"),
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(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"),
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(AlgorithmTagAdmin, self).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(object):
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(ProblemAdmin, self).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(ProblemAdmin, self).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(ProblemAdmin, self).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(
set([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(ProblemAdmin, self).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(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)
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(ProblemPackageAdmin, self).__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(
u'<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(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
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
def get_custom_list_select_related(self):
return super(ProblemPackageAdmin, self).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(object):
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(ContestProblemPackageAdmin, self).__init__(*args, **kwargs)
self.list_display_links = None
def get_queryset(self, request):
qs = super(ContestProblemPackageAdmin, self).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(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,
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(MainProblemInstanceAdmin, self).response_change(request, obj)
admin.site.register(MainProblemInstance, MainProblemInstanceAdmin)