Skip to content

Commit

Permalink
Content moderator user preferences admin view (#3914)
Browse files Browse the repository at this point in the history
* Add UserPreferences model

* Add string name for model, and create prefs on user creation

* Add user preferences to separate section in admin UI

* Use a custom form for only changing blur images in Admin UI

* Add tests for the admin form

* Add moderator user and content moderator group creation to init

* Use more idiomatic Django MVC logic

* Jump directly into the single user preferences entry from list view

* Always return a bound object

Co-authored-by: sarayourfriend <[email protected]>

* Linting

* Add UserPreferences insertion data migration

* Use settings.AUTH_USER_MODEL instead of model directly

* Update migration

* Update permissions for UserPreferences admin models

Co-authored-by: Dhruv Bhanushali <[email protected]>

* Re-add change permissions

This is required for has_view_permissions: The default implementation returns True if the user has either the “change” or “view” permission.
https://docs.djangoproject.com/en/5.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.has_view_permission

---------

Co-authored-by: sarayourfriend <[email protected]>
Co-authored-by: Dhruv Bhanushali <[email protected]>
  • Loading branch information
3 people committed Apr 15, 2024
1 parent 4524098 commit effd06c
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 10 deletions.
44 changes: 43 additions & 1 deletion api/api/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
from django.contrib import admin
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.models import Group, User
from django.http.response import HttpResponseRedirect
from django.urls import reverse

from oauth2_provider.models import AccessToken

from api.admin.forms import UserPreferencesAdminForm
from api.admin.site import openverse_admin
from api.models import PENDING, Audio, AudioReport, ContentProvider, Image, ImageReport
from api.models import (
PENDING,
Audio,
AudioReport,
ContentProvider,
Image,
ImageReport,
UserPreferences,
)
from api.models.media import AbstractDeletedMedia, AbstractSensitiveMedia
from api.models.oauth import ThrottledApplication

Expand Down Expand Up @@ -127,3 +138,34 @@ class AccessTokenAdmin(admin.ModelAdmin):
"created",
"updated",
)


@admin.register(UserPreferences)
class IndividualUserPreferencesAdmin(admin.ModelAdmin):
"""
Model admin for showing user preferences. This should only ever show the
currently logged-in user's preferences
"""

verbose_name_plural = "My Preferences"
verbose_name = "My Preferences"
form = UserPreferencesAdminForm

def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(user=request.user)

def has_change_permission(self, request, obj=None):
return True

def has_add_permission(*args, **kwargs):
return False

def has_delete_permission(*args, **kwargs):
return False

def changelist_view(self, request, extra_context=None):
obj = self.get_queryset(request).first()
return HttpResponseRedirect(
reverse("admin:api_userpreferences_change", args=[obj.id])
)
19 changes: 19 additions & 0 deletions api/api/admin/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django import forms

from api.models import UserPreferences


class UserPreferencesAdminForm(forms.ModelForm):
blur_images = forms.BooleanField(initial=True, required=False)

class Meta:
model = UserPreferences
fields = ["blur_images"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial["blur_images"] = self.instance.blur_images

def save(self, commit=True):
self.instance.blur_images = self.cleaned_data.get("blur_images")
return super().save(commit=commit)
16 changes: 16 additions & 0 deletions api/api/admin/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ def key(entry):
}
app_list.insert(0, media_app)

# Move user preferences to its own section
for model in api_app["models"][:]:
if model["object_name"] == "UserPreferences":
model["name"] = "My Preferences"
api_app["models"].remove(model)
app_list.insert(
-1,
{
"name": "User Preferences",
"app_label": "preferences",
"app_url": "/admin/api/userpreferences/",
"has_module_perms": True,
"models": [model],
},
)

return app_list


Expand Down
34 changes: 34 additions & 0 deletions api/api/migrations/0059_userpreferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.11 on 2024-04-10 01:53

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


def create_user_preferences(apps, schema_editor):
UserPreferences = apps.get_model("api", "UserPreferences")
User = apps.get_model(*settings.AUTH_USER_MODEL.split("."))
for user in User.objects.all():
if not hasattr(user, "userpreferences"):
UserPreferences.objects.create(user=user)
user.userpreferences.save()


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0058_moderation_decision'),
]

operations = [
migrations.CreateModel(
name='UserPreferences',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('preferences', models.JSONField(default=dict)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(create_user_preferences)
]
1 change: 1 addition & 0 deletions api/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
PENDING,
)
from api.models.models import ContentProvider, Tag
from api.models.moderation import UserPreferences
from api.models.oauth import (
OAuth2Registration,
OAuth2Verification,
Expand Down
38 changes: 38 additions & 0 deletions api/api/models/moderation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver


class UserPreferences(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
preferences = models.JSONField(default=dict)

def __str__(self):
return f"{self.user.username}'s preferences"

@property
def moderator(self):
if "moderator" not in self.preferences:
self.preferences["moderator"] = {}

return self.preferences["moderator"]

@moderator.setter
def moderator(self, value):
self.preferences["moderator"] = value

@property
def blur_images(self):
return self.moderator.get("blur_images", True)

@blur_images.setter
def blur_images(self, value):
self.moderator |= {"blur_images": value}


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
UserPreferences.objects.create(user=instance)
instance.userpreferences.save()
62 changes: 62 additions & 0 deletions api/test/unit/admin/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest

from api.admin.forms import UserPreferencesAdminForm
from api.models import UserPreferences


@pytest.mark.django_db
@pytest.mark.parametrize(
"initial_preferences, blur_images_value, expected_preferences",
[
({}, True, {"moderator": {"blur_images": True}}),
({}, False, {"moderator": {"blur_images": False}}),
(
{"moderator": {"blur_images": False}},
True,
{"moderator": {"blur_images": True}},
),
(
{"moderator": {"blur_images": False}},
False,
{"moderator": {"blur_images": False}},
),
(
{"moderator": {"blur_images": True}},
True,
{"moderator": {"blur_images": True}},
),
(
{"moderator": {"blur_images": False}},
False,
{"moderator": {"blur_images": False}},
),
(
{"some_other_setting": 123},
False,
{"some_other_setting": 123, "moderator": {"blur_images": False}},
),
(
{"moderator": {"some_other_setting": 123}},
False,
{"moderator": {"some_other_setting": 123, "blur_images": False}},
),
],
)
def test_user_preferences_form(
initial_preferences, blur_images_value, expected_preferences, django_user_model
):
user = django_user_model.objects.create(username="foobar", password="fake")
preferences = user.userpreferences
# Check that new user has no preferences
assert preferences.preferences == {}
# Set the initial user preferences
preferences.preferences = initial_preferences
preferences.save()

form_data = {"blur_images": blur_images_value}
form = UserPreferencesAdminForm(data=form_data, instance=preferences)
assert form.is_valid(), "Form should be valid"
instance = form.save()
# Retrieve the instance from the database to ensure it's saved correctly
saved_instance = UserPreferences.objects.get(id=instance.id)
assert saved_instance.preferences == expected_preferences
39 changes: 30 additions & 9 deletions load_sample_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,37 @@ just dc exec -T "$WEB_SERVICE_NAME" python3 manage.py migrate --noinput
# Create a superuser and a user for integration testing
echo "
from django.contrib.auth.models import User
usernames = ['continuous_integration', 'deploy']
usernames = ['continuous_integration', 'deploy', 'moderator']
for username in usernames:
if User.objects.filter(username=username).exists():
print(f'User {username} already exists')
continue
if username == 'deploy':
user = User.objects.create_superuser(username, f'{username}@example.com', 'deploy')
else:
user = User.objects.create_user(username, f'{username}@example.com', 'deploy')
user.save()
if User.objects.filter(username=username).exists():
print(f'User {username} already exists')
continue
if username == 'deploy':
user = User.objects.create_superuser(username, f'{username}@example.com', 'deploy')
else:
is_staff = username == 'moderator'
user = User.objects.create_user(username, f'{username}@example.com', 'deploy', is_staff=is_staff)
user.save()
" | just dc exec -T "$WEB_SERVICE_NAME" python3 manage.py shell

# Create the Content Moderator group and add the moderator to it
# Credit: https://stackoverflow.com/a/53733693
echo "
from django.contrib.auth.models import User, Group, Permission
perms_to_add = ['view', 'add', 'change']
models_to_affect = ['audio report', 'image report', 'sensitive audio', 'sensitive image']
mod_group, created = Group.objects.get_or_create(name='Content Moderators')
if created:
print('Setting up Content Moderators group')
for model in models_to_affect:
for perm in perms_to_add:
name = f'Can {perm} {model}'
print(f'Adding permission to moderators group: {name}')
model_add_perm = Permission.objects.get(name=name)
mod_group.permissions.add(model_add_perm)
mod_group.save()
mod_group.user_set.add(User.objects.get(username='moderator'))
" | just dc exec -T "$WEB_SERVICE_NAME" python3 manage.py shell

# Load content providers
Expand Down

0 comments on commit effd06c

Please sign in to comment.