diff --git a/oioioi/base/admin.py b/oioioi/base/admin.py index a515a311a..10a824157 100644 --- a/oioioi/base/admin.py +++ b/oioioi/base/admin.py @@ -15,6 +15,7 @@ from django.utils.html import escape from django.utils.text import capfirst from django.utils.translation import gettext_lazy as _ +from django import forms from oioioi.base.forms import OioioiUserChangeForm, OioioiUserCreationForm from oioioi.base.menu import MenuRegistry, side_pane_menus_registry @@ -356,14 +357,20 @@ class OioioiUserAdmin(UserAdmin, ObjectWithMixins, metaclass=ModelAdminMeta): fieldsets = ( (None, {'fields': ('username', 'password')}), (_("Personal info"), {'fields': ('first_name', 'last_name', 'email')}), - (_("Permissions"), {'fields': ('is_active', 'is_superuser', 'groups')}), + (_("Permissions"), {'fields': ('is_active', 'is_superuser', 'user_permissions', 'groups')}), (_("Important dates"), {'fields': ('last_login', 'date_joined')}), ) list_filter = ['is_superuser', 'is_active'] list_display = ['username', 'email', 'first_name', 'last_name', 'is_active'] - filter_horizontal = () + filter_horizontal = ('user_permissions',) actions = ['activate_user'] + # Overriding the formfield_for_manytomany method to ensure we render the field as checkboxes + def formfield_for_manytomany(self, db_field, request=None, **kwargs): + if db_field.name == 'user_permissions': + kwargs['widget'] = forms.CheckboxSelectMultiple() + return super().formfield_for_manytomany(db_field, request, **kwargs) + def activate_user(self, request, qs): qs.update(is_active=True) diff --git a/oioioi/base/forms.py b/oioioi/base/forms.py index ce937c622..a512b84e8 100644 --- a/oioioi/base/forms.py +++ b/oioioi/base/forms.py @@ -2,6 +2,7 @@ import bleach from collections import OrderedDict +from django.contrib.auth.models import Permission from captcha.fields import CaptchaField, CaptchaTextInput from django import forms from django.conf import settings @@ -269,6 +270,7 @@ def __init__(self, *args, **kwargs): super(OioioiUserChangeForm, self).__init__(*args, **kwargs) adjust_username_field(self) adjust_name_fields(self) + self.fields['user_permissions'].queryset = Permission.objects.filter(codename='can_modify_tags') class OioioiPasswordResetForm(PasswordResetForm): diff --git a/oioioi/problems/admin.py b/oioioi/problems/admin.py index a34ea3c91..3023ef1f7 100644 --- a/oioioi/problems/admin.py +++ b/oioioi/problems/admin.py @@ -55,7 +55,7 @@ ProblemSite, ProblemStatement, ) -from oioioi.problems.utils import can_add_problems, can_admin_problem +from oioioi.problems.utils import can_add_problems, can_admin_problem, can_modify_tags logger = logging.getLogger(__name__) @@ -274,10 +274,28 @@ def _update_queryset_if_problems(db_field, **kwargs): 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, @@ -344,7 +362,7 @@ def formfield_for_manytomany(self, db_field, request, **kwargs): return super(OriginInfoValueAdmin, self).formfield_for_manytomany( db_field, request, **kwargs ) - + admin.site.register(OriginInfoValue, OriginInfoValueAdmin) @@ -354,6 +372,7 @@ def formfield_for_manytomany(self, db_field, request, **kwargs): 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 @@ -381,6 +400,7 @@ def formfield_for_manytomany(self, db_field, request, **kwargs): 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 @@ -404,6 +424,10 @@ def formfield_for_manytomany(self, db_field, request, **kwargs): class ProblemAdmin(admin.ModelAdmin): + tag_inlines = ( + DifficultyTagInline, + AlgorithmTagInline, + ) inlines = ( DifficultyTagInline, AlgorithmTagInline, @@ -437,10 +461,12 @@ 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) + return can_modify_tags(request, obj) def has_delete_permission(self, request, obj=None): - return self.has_change_permission(request, obj) + 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: @@ -482,7 +508,7 @@ def get_queryset(self, request): combined = request.user.problem_set.all() if request.user.is_superuser: return queryset - if request.user.has_perm('problems.problems_db_admin'): + 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) @@ -503,14 +529,26 @@ def get_readonly_fields(self, request, obj=None): 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.inlines]) + set([getattr(inline, 'category', None) for inline in self.get_inlines(request, problem)]) ) - extra_context['no_category'] = NO_CATEGORY + 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): diff --git a/oioioi/problems/migrations/0034_alter_problem_options.py b/oioioi/problems/migrations/0034_alter_problem_options.py new file mode 100644 index 000000000..432e012d0 --- /dev/null +++ b/oioioi/problems/migrations/0034_alter_problem_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2025-01-20 16:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('problems', '0033_populate_aggregated_tag_proposals'), + ] + + operations = [ + migrations.AlterModelOptions( + name='problem', + options={'permissions': (('can_modify_tags', 'Can modify tags'), ('problems_db_admin', 'Can administer the problems database'), ('problem_admin', 'Can administer the problem')), 'verbose_name': 'problem', 'verbose_name_plural': 'problems'}, + ), + ] diff --git a/oioioi/problems/models.py b/oioioi/problems/models.py index c67b29a5c..9b1dcd868 100644 --- a/oioioi/problems/models.py +++ b/oioioi/problems/models.py @@ -156,6 +156,7 @@ class Meta(object): verbose_name = _("problem") verbose_name_plural = _("problems") permissions = ( + ('can_modify_tags', _("Can modify tags")), ('problems_db_admin', _("Can administer the problems database")), ('problem_admin', _("Can administer the problem")), ) diff --git a/oioioi/problems/problem_site.py b/oioioi/problems/problem_site.py index 18fad2859..9e9be53e8 100644 --- a/oioioi/problems/problem_site.py +++ b/oioioi/problems/problem_site.py @@ -37,6 +37,7 @@ ) from oioioi.problems.problem_sources import UploadedPackageSource from oioioi.problems.utils import ( + can_modify_tags, can_admin_problem, generate_add_to_contest_metadata, generate_model_solutions_context, @@ -251,6 +252,23 @@ def problem_site_settings(request, problem): ) model_solutions = generate_model_solutions_context(request, problem_instance) extra_actions = problem.controller.get_extra_problem_site_actions(problem) + + return TemplateResponse( + request, + 'problems/settings.html', + { + 'site_key': problem.problemsite.url_key, + 'problem': problem, + 'administered_recent_contests': administered_recent_contests, + 'package': package if package and package.package_file else None, + 'model_solutions': model_solutions, + 'can_admin_problem': can_admin_problem(request, problem), + 'extra_actions': extra_actions, + }, + ) + +@problem_site_tab(_("Tags"), key='tags', order=600, condition=can_modify_tags) +def problem_site_tags(request, problem): algorithm_tag_proposals = ( AggregatedAlgorithmTagProposal.objects.all().filter(problem=problem).order_by('-amount')[:25] ) @@ -260,17 +278,13 @@ def problem_site_settings(request, problem): return TemplateResponse( request, - 'problems/settings.html', + 'problems/tags.html', { 'site_key': problem.problemsite.url_key, 'problem': problem, - 'administered_recent_contests': administered_recent_contests, - 'package': package if package and package.package_file else None, - 'model_solutions': model_solutions, 'algorithm_tag_proposals': algorithm_tag_proposals, 'difficulty_tag_proposals': difficulty_tag_proposals, 'can_admin_problem': can_admin_problem(request, problem), - 'extra_actions': extra_actions, }, ) diff --git a/oioioi/problems/templates/problems/settings.html b/oioioi/problems/templates/problems/settings.html index c4558d007..53717d9b8 100644 --- a/oioioi/problems/templates/problems/settings.html +++ b/oioioi/problems/templates/problems/settings.html @@ -8,11 +8,6 @@ {% include 'problems/ingredients/add-to-contest-panel.html' %} - {# Tags panel #} -