tests: add UserSettings model and middleware tests; ensure tests create UserSettings defaults
This commit is contained in:
65
NibasaViewer/middleware.py
Normal file
65
NibasaViewer/middleware.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
|
|
||||||
|
from viewer.models import UserSettings
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsMiddleware:
|
||||||
|
"""Middleware that injects `theme` and `sort` into template response context.
|
||||||
|
|
||||||
|
On each request it looks for a UserSettings instance for the authenticated
|
||||||
|
user. The found values (or model defaults) are attached to the request and
|
||||||
|
then added to any TemplateResponse's context under the `theme` and `sort`
|
||||||
|
keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
# Populate request attributes with user settings or defaults.
|
||||||
|
request.theme = None
|
||||||
|
request.sort = None
|
||||||
|
|
||||||
|
user = getattr(request, "user", None)
|
||||||
|
if user and getattr(user, "is_authenticated", False):
|
||||||
|
# Use filter().first() to avoid exceptions if no settings exist.
|
||||||
|
try:
|
||||||
|
settings_obj = UserSettings.objects.get(user=user)
|
||||||
|
except UserSettings.DoesNotExist:
|
||||||
|
settings_obj = UserSettings(user=user)
|
||||||
|
settings_obj.save()
|
||||||
|
except MultipleObjectsReturned:
|
||||||
|
settings_obj = UserSettings.objects.filter(user=user).first()
|
||||||
|
finally:
|
||||||
|
request.theme = settings_obj.theme
|
||||||
|
request.sort = settings_obj.sort
|
||||||
|
|
||||||
|
# Fall back to the model defaults when not set from DB.
|
||||||
|
if request.theme is None:
|
||||||
|
request.theme = UserSettings._meta.get_field("theme").get_default()
|
||||||
|
if request.sort is None:
|
||||||
|
request.sort = UserSettings._meta.get_field("sort").get_default()
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_template_response(self, request, response):
|
||||||
|
"""Add `theme` and `sort` to TemplateResponse.context_data.
|
||||||
|
|
||||||
|
This method is only called for responses that implement
|
||||||
|
`render()` (TemplateResponse-like). We update or create
|
||||||
|
`context_data` so templates can access `theme` and `sort`.
|
||||||
|
"""
|
||||||
|
if not isinstance(response, TemplateResponse):
|
||||||
|
return response
|
||||||
|
|
||||||
|
ctx = getattr(response, "context_data", None)
|
||||||
|
if ctx is None:
|
||||||
|
response.context_data = {"theme": request.theme, "sort": request.sort}
|
||||||
|
else:
|
||||||
|
# Do not overwrite existing keys if templates or other processors set them
|
||||||
|
ctx.setdefault("theme", request.theme)
|
||||||
|
ctx.setdefault("sort", request.sort)
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -105,6 +105,7 @@ MIDDLEWARE = [
|
|||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"NibasaViewer.middleware.UserSettingsMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|||||||
39
viewer/migrations/0003_usersettings.py
Normal file
39
viewer/migrations/0003_usersettings.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-25 08:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("viewer", "0002_alter_image_path"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserSettings",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("theme", models.CharField(default="dark", max_length=5)),
|
||||||
|
("sort", models.CharField(default="abc", max_length=6)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,6 +2,7 @@ from django.utils import timezone
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Model,
|
Model,
|
||||||
|
CharField,
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateTimeField,
|
DateTimeField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
@@ -15,6 +16,19 @@ def get_gallery_root():
|
|||||||
return settings.GALLERY_ROOT
|
return settings.GALLERY_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettings(Model):
|
||||||
|
"""
|
||||||
|
User relations to a specific image file by path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, on_delete=CASCADE)
|
||||||
|
theme = CharField(max_length=5, blank=False, null=False, default='dark')
|
||||||
|
sort = CharField(max_length=6, blank=False, null=False, default='abc')
|
||||||
|
|
||||||
|
class meta:
|
||||||
|
ordering = ["pk"]
|
||||||
|
|
||||||
|
|
||||||
class Image(Model):
|
class Image(Model):
|
||||||
"""
|
"""
|
||||||
User relations to a specific image file by path.
|
User relations to a specific image file by path.
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ from PIL import Image
|
|||||||
from django.test import Client, RequestFactory, TestCase, override_settings
|
from django.test import Client, RequestFactory, TestCase, override_settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
|
||||||
# Project imports.
|
# Project imports.
|
||||||
from viewer.common import SORT_LABELS
|
from viewer.common import SORT_LABELS
|
||||||
from viewer.directory import do_recursive_search
|
from viewer.directory import do_recursive_search
|
||||||
from viewer.views import gallery_view
|
from viewer.views import gallery_view
|
||||||
from viewer.models import Image as Im
|
from viewer.models import Image as Im, UserSettings
|
||||||
|
from NibasaViewer.middleware import UserSettingsMiddleware
|
||||||
|
|
||||||
|
|
||||||
class GalleryBaseTests(TestCase):
|
class GalleryBaseTests(TestCase):
|
||||||
@@ -38,6 +41,10 @@ class GalleryBaseTests(TestCase):
|
|||||||
self.client = Client()
|
self.client = Client()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
# Ensure UserSettings exists for the test user so middleware-backed
|
||||||
|
# attributes (`theme`, `sort`) are available during view rendering
|
||||||
|
# even when tests call views directly via RequestFactory.
|
||||||
|
UserSettings.objects.get_or_create(user=self.user)
|
||||||
|
|
||||||
self._build_fixture_tree()
|
self._build_fixture_tree()
|
||||||
|
|
||||||
@@ -97,6 +104,12 @@ class GalleryViewTests(GalleryBaseTests):
|
|||||||
request = self.factory.get("/gallery/../")
|
request = self.factory.get("/gallery/../")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
|
# When calling the view directly we still need to allow any
|
||||||
|
# TemplateResponse processing to see the settings. Ensure the
|
||||||
|
# request has the attributes the middleware would provide.
|
||||||
|
request.theme = UserSettings._meta.get_field("theme").get_default()
|
||||||
|
request.sort = UserSettings._meta.get_field("sort").get_default()
|
||||||
|
|
||||||
response = gallery_view(request, "../")
|
response = gallery_view(request, "../")
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
@@ -501,3 +514,60 @@ class ImageModelTests(GalleryBaseTests):
|
|||||||
|
|
||||||
img.refresh_from_db()
|
img.refresh_from_db()
|
||||||
self.assertTrue(img.favorite)
|
self.assertTrue(img.favorite)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsModelTests(TestCase):
|
||||||
|
def test_usersettings_defaults(self):
|
||||||
|
user = User.objects.create_user("usertest", "u@example.com", "pw")
|
||||||
|
us = UserSettings.objects.create(user=user)
|
||||||
|
|
||||||
|
self.assertEqual(us.theme, "dark")
|
||||||
|
self.assertEqual(us.sort, "abc")
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsMiddlewareTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user("mwuser", "mw@example.com", "pw")
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_middleware_creates_settings_and_sets_request(self):
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = self.user
|
||||||
|
|
||||||
|
def get_response(req):
|
||||||
|
return HttpResponse("ok")
|
||||||
|
|
||||||
|
mw = UserSettingsMiddleware(get_response)
|
||||||
|
response = mw(request)
|
||||||
|
|
||||||
|
# UserSettings should have been created and request attrs populated
|
||||||
|
self.assertTrue(UserSettings.objects.filter(user=self.user).exists())
|
||||||
|
self.assertEqual(
|
||||||
|
request.theme, UserSettings._meta.get_field("theme").get_default()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
request.sort, UserSettings._meta.get_field("sort").get_default()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_process_template_response_injects_and_preserves(self):
|
||||||
|
# Create a UserSettings with non-defaults
|
||||||
|
UserSettings.objects.create(user=self.user, theme="light", sort="cba")
|
||||||
|
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = self.user
|
||||||
|
|
||||||
|
def get_response(req):
|
||||||
|
# Provide a TemplateResponse that already sets `theme` to ensure
|
||||||
|
# the middleware does not overwrite existing keys.
|
||||||
|
return TemplateResponse(
|
||||||
|
req, "viewer/gallery_view.html", {"theme": "override-theme"}
|
||||||
|
)
|
||||||
|
|
||||||
|
mw = UserSettingsMiddleware(get_response)
|
||||||
|
response = mw(request)
|
||||||
|
|
||||||
|
# process_template_response should set missing keys but preserve existing ones
|
||||||
|
resp = mw.process_template_response(request, response)
|
||||||
|
self.assertIsInstance(resp, TemplateResponse)
|
||||||
|
self.assertEqual(resp.context_data.get("theme"), "override-theme")
|
||||||
|
self.assertEqual(resp.context_data.get("sort"), "cba")
|
||||||
|
|||||||
Reference in New Issue
Block a user