Skip to content

Commit

Permalink
Add permission to change tags (#445)
Browse files Browse the repository at this point in the history
* first commit

* fix permissions

* fix admin panel not saving

* small fixes

* fix origin info

* change permission name

* fix permissions for contest admins

* fix permissions for contest admins

* fix for problems without contests

* add test

* resolve conflicts

* small fix

* test tags tab

* squash migrations

* remove redundant migrations
  • Loading branch information
Atanazyy authored Feb 26, 2025
1 parent 8153ee4 commit 71d1997
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 39 deletions.
11 changes: 9 additions & 2 deletions oioioi/base/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions oioioi/base/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
52 changes: 45 additions & 7 deletions oioioi/problems/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -404,6 +424,10 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):


class ProblemAdmin(admin.ModelAdmin):
tag_inlines = (
DifficultyTagInline,
AlgorithmTagInline,
)
inlines = (
DifficultyTagInline,
AlgorithmTagInline,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
17 changes: 17 additions & 0 deletions oioioi/problems/migrations/0034_alter_problem_options.py
Original file line number Diff line number Diff line change
@@ -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'},
),
]
1 change: 1 addition & 0 deletions oioioi/problems/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
)
Expand Down
24 changes: 19 additions & 5 deletions oioioi/problems/problem_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
)
Expand All @@ -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,
},
)

Expand Down
23 changes: 0 additions & 23 deletions oioioi/problems/templates/problems/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
{% include 'problems/ingredients/add-to-contest-panel.html' %}
</div>

{# Tags panel #}
<div class="settings-group col-lg-5">
{% include 'problems/ingredients/tags-panel.html' %}
</div>

{# Management buttons #}
<div class="settings-group col-lg-3">
{% include 'problems/ingredients/action-btn-panel.html' %}
Expand All @@ -34,15 +29,6 @@

</div>

<div class="row settings-row d-none d-md-flex d-lg-none">

{# Tags panel #}
<div class="settings-group col-md-12">
{% include 'problems/ingredients/tags-panel.html' %}
</div>

</div>

<div class="row settings-row d-md-none">

{# Management buttons #}
Expand All @@ -61,15 +47,6 @@

</div>

<div class="row settings-row d-md-none">

{# Tags panel #}
<div class="settings-group col-sm-12">
{% include 'problems/ingredients/tags-panel.html' %}
</div>

</div>

<div class="row settings-row">

{# Model solutions panel #}
Expand Down
29 changes: 29 additions & 0 deletions oioioi/problems/templates/problems/tags.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% load i18n %}


<div class="row settings-row d-none d-lg-flex">

{# Tags panel #}
<div class="settings-group col-lg-5">
{% include 'problems/ingredients/tags-panel.html' %}
</div>

</div>

<div class="row settings-row d-none d-md-flex d-lg-none">

{# Tags panel #}
<div class="settings-group col-md-12">
{% include 'problems/ingredients/tags-panel.html' %}
</div>

</div>

<div class="row settings-row d-md-none">

{# Tags panel #}
<div class="settings-group col-sm-12">
{% include 'problems/ingredients/tags-panel.html' %}
</div>

</div>
37 changes: 35 additions & 2 deletions oioioi/problems/tests/test_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,14 +429,47 @@ def test_settings_tab(self):
self.assertContains(response, 'Settings')
response = self.client.get(url)
self.assertContains(response, 'Add to contest')
self.assertContains(response, 'Current tags')
self.assertContains(response, 'Edit problem')
self.assertContains(response, 'Edit tests')
self.assertContains(response, 'Reupload problem')
self.assertContains(response, 'Model solutions')
self.assertContains(response, 'Medium')

@override_settings(LANGUAGE_CODE='en')
def test_tags_tab_admin(self):
problemsite_url = self._get_site_urls()['statement']
url = reverse('problem_site', kwargs={'site_key': '123'}) + '?key=tags'

response = self.client.get(problemsite_url)
self.assertNotContains(response, 'Tags')

self.assertTrue(self.client.login(username='test_admin'))
response = self.client.get(problemsite_url)
self.assertContains(response, 'Tags')
response = self.client.get(url)
self.assertContains(response, 'Current tags')
self.assertContains(response, 'dp')
self.assertContains(response, 'lcis')

@override_settings(LANGUAGE_CODE='en')
def test_tags_tab_user_with_permission(self):
problemsite_url = self._get_site_urls()['statement']
url = reverse('problem_site', kwargs={'site_key': '123'}) + '?key=tags'

response = self.client.get(problemsite_url)
self.assertNotContains(response, 'Tags')

user = User.objects.get(username='test_user')
permission = Permission.objects.get(codename='can_modify_tags')
user.user_permissions.add(permission)

self.assertTrue(self.client.login(username='test_user'))
response = self.client.get(problemsite_url)
self.assertContains(response, 'Tags')
response = self.client.get(url)
self.assertContains(response, 'Current tags')
self.assertContains(response, 'dp')
self.assertContains(response, 'lcis')
self.assertContains(response, 'Medium')

def test_statement_replacement(self):
url = (
Expand Down
12 changes: 12 additions & 0 deletions oioioi/problems/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ def can_admin_problem(request, problem):
return False


def can_modify_tags(request, problem):
"""Checks if the user can add tags to the problem.
The user can modify tags if user can admin problem or user has can_modify_tags permission
"""
if request.user.has_perm('problems.can_modify_tags'):
return True
if problem is None:
return False
return can_admin_problem(request, problem)


def can_admin_instance_of_problem(request, problem):
"""Checks if the user has admin permission in a ProblemInstace
of the given Problem.
Expand Down

0 comments on commit 71d1997

Please sign in to comment.