Compare commits
11 Commits
7cc7f04b80
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 24c1c96f19 | |||
| 5a2bea3040 | |||
| 27da3a33d3 | |||
|
|
ab81a0a97d | ||
|
|
b8e7a876a7 | ||
|
|
377951efbe | ||
|
|
73b538b698 | ||
|
|
8719be3227 | ||
|
|
13ab55d1d3 | ||
|
|
5ec793b47d | ||
|
|
f658106316 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -362,3 +362,5 @@ Icon
|
|||||||
Network Trash Folder
|
Network Trash Folder
|
||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
|
.kateproject*
|
||||||
|
|||||||
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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ def get_modification_timestamp(path_obj):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def build_query(search_text, sort_key, theme):
|
def build_query(search_text):
|
||||||
query = {"sort": sort_key, "theme": theme}
|
query = {}
|
||||||
|
|
||||||
if search_text != "":
|
if search_text != "":
|
||||||
query["search"] = search_text
|
query["search"] = search_text
|
||||||
@@ -170,16 +170,22 @@ def build_sort_options(request, search_text, sort_key, theme):
|
|||||||
options = []
|
options = []
|
||||||
|
|
||||||
for option_key, label in SORT_OPTIONS:
|
for option_key, label in SORT_OPTIONS:
|
||||||
query = {"sort": option_key, "theme": theme}
|
# Build a URL that points to the settings toggle endpoint which will
|
||||||
|
# persist the chosen sort (and optionally theme) in the user's
|
||||||
|
# UserSettings and then redirect back to the current view. Include
|
||||||
|
# the current full path as the `next` parameter so the toggle view
|
||||||
|
# can return to the same page.
|
||||||
|
query = {"next": request.get_full_path(), "sort": option_key, "theme": theme}
|
||||||
|
|
||||||
if search_text != "":
|
if search_text != "":
|
||||||
|
# Include search top-level so templates/tests can assert its presence
|
||||||
query["search"] = search_text
|
query["search"] = search_text
|
||||||
|
|
||||||
options.append(
|
options.append(
|
||||||
{
|
{
|
||||||
"key": option_key,
|
"key": option_key,
|
||||||
"label": label,
|
"label": label,
|
||||||
"url": append_query(request.path, query),
|
"url": append_query("/gallery/toggle-settings/", query),
|
||||||
"is_active": option_key == sort_key,
|
"is_active": option_key == sort_key,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .common import (
|
|||||||
normalize_theme,
|
normalize_theme,
|
||||||
build_query,
|
build_query,
|
||||||
gallery_url,
|
gallery_url,
|
||||||
|
append_query,
|
||||||
sort_images,
|
sort_images,
|
||||||
build_sort_options,
|
build_sort_options,
|
||||||
build_breadcrumbs,
|
build_breadcrumbs,
|
||||||
@@ -68,10 +69,17 @@ def render_directory(request, path_text, full_path):
|
|||||||
in the file system, or logical gallery directories like search result pages.
|
in the file system, or logical gallery directories like search result pages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Search remains a GET parameter. For sort and theme prefer explicit
|
||||||
|
# GET parameters when present (so query-preserving links behave as
|
||||||
|
# callers expect), otherwise fall back to middleware-provided settings.
|
||||||
search_text = request.GET.get("search", "").strip()
|
search_text = request.GET.get("search", "").strip()
|
||||||
sort_key = normalize_sort(request.GET.get("sort", "abc"))
|
sort_key = normalize_sort(
|
||||||
theme = normalize_theme(request.GET.get("theme", "dark"))
|
request.GET.get("sort") or getattr(request, "sort", None) or "abc"
|
||||||
query_state = build_query(search_text, sort_key, theme)
|
)
|
||||||
|
theme = normalize_theme(
|
||||||
|
request.GET.get("theme") or getattr(request, "theme", None) or "dark"
|
||||||
|
)
|
||||||
|
query_state = build_query(search_text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_entries = [
|
current_entries = [
|
||||||
@@ -130,13 +138,37 @@ def render_directory(request, path_text, full_path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
next_theme = "light" if theme == "dark" else "dark"
|
next_theme = "light" if theme == "dark" else "dark"
|
||||||
theme_query = {"sort": sort_key, "theme": next_theme}
|
|
||||||
|
# The theme toggle button now calls the persistent settings endpoint.
|
||||||
|
# Include `next` (for redirect back) and also surface `sort` and `search`
|
||||||
|
# as top-level query parameters so templates/tests can inspect them easily.
|
||||||
|
theme_toggle_query = {
|
||||||
|
"next": request.get_full_path(),
|
||||||
|
"theme": next_theme,
|
||||||
|
"sort": sort_key,
|
||||||
|
}
|
||||||
|
|
||||||
if search_text != "":
|
if search_text != "":
|
||||||
theme_query["search"] = search_text
|
theme_toggle_query["search"] = search_text
|
||||||
|
|
||||||
search_action_query = {"sort": sort_key, "theme": theme}
|
search_action_query = {"sort": sort_key, "theme": theme}
|
||||||
|
|
||||||
|
# Back (directory) and Home (root) links and thumbnails
|
||||||
|
dir_rel = None
|
||||||
|
try:
|
||||||
|
# derive directory path text relative to gallery root
|
||||||
|
dir_rel = full_path.parent.relative_to(settings.GALLERY_ROOT)
|
||||||
|
dir_text = str(dir_rel).replace("\\", "/")
|
||||||
|
except Exception:
|
||||||
|
dir_text = ""
|
||||||
|
|
||||||
|
back_url = gallery_url(
|
||||||
|
Path(dir_text) if dir_text != "" else None, True, query_state
|
||||||
|
)
|
||||||
|
back_thumb = get_first_image_thumbnail_url(full_path.parent)
|
||||||
|
home_url = gallery_url(None, True, query_state)
|
||||||
|
home_thumb = get_first_image_thumbnail_url(settings.GALLERY_ROOT)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"path": path_text,
|
"path": path_text,
|
||||||
"search_text": search_text,
|
"search_text": search_text,
|
||||||
@@ -147,12 +179,19 @@ def render_directory(request, path_text, full_path):
|
|||||||
"breadcrumbs": build_breadcrumbs(path_text, query_state),
|
"breadcrumbs": build_breadcrumbs(path_text, query_state),
|
||||||
"images": image_data,
|
"images": image_data,
|
||||||
"subdirs": subdir_data,
|
"subdirs": subdir_data,
|
||||||
"theme_toggle_url": gallery_url(
|
"back_url": back_url,
|
||||||
Path(path_text) if path_text != "" else None, True, theme_query
|
"back_thumb": back_thumb,
|
||||||
|
"home_url": home_url,
|
||||||
|
"home_thumb": home_thumb,
|
||||||
|
"theme_toggle_url": append_query(
|
||||||
|
"/gallery/toggle-settings/", theme_toggle_query
|
||||||
),
|
),
|
||||||
"search_action_url": gallery_url(
|
"search_action_url": gallery_url(
|
||||||
Path(path_text) if path_text != "" else None, True, search_action_query
|
Path(path_text) if path_text != "" else None, True, search_action_query
|
||||||
),
|
),
|
||||||
|
"clear_search_url": gallery_url(
|
||||||
|
Path(path_text) if path_text != "" else None, True, None
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# sort_label depends on SORT_LABELS in common; import lazily to avoid circulars
|
# sort_label depends on SORT_LABELS in common; import lazily to avoid circulars
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from .common import (
|
|||||||
normalize_theme,
|
normalize_theme,
|
||||||
build_query,
|
build_query,
|
||||||
gallery_url,
|
gallery_url,
|
||||||
|
append_query,
|
||||||
sort_images,
|
sort_images,
|
||||||
build_sort_options,
|
build_sort_options,
|
||||||
build_breadcrumbs,
|
build_breadcrumbs,
|
||||||
@@ -40,7 +41,7 @@ def render_image(request, path_text, full_path):
|
|||||||
try:
|
try:
|
||||||
img = Im.objects.get(path=full_path, user=request.user)
|
img = Im.objects.get(path=full_path, user=request.user)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
img.favorite = not img.favorite
|
img.favorite = not img.favorite
|
||||||
|
|
||||||
except Im.DoesNotExist:
|
except Im.DoesNotExist:
|
||||||
@@ -51,11 +52,17 @@ def render_image(request, path_text, full_path):
|
|||||||
img.visits = img.visits + 1
|
img.visits = img.visits + 1
|
||||||
img.save()
|
img.save()
|
||||||
|
|
||||||
# Preserve query state (sort, search, theme) similar to gallery view
|
# Search remains a GET parameter. For sort and theme prefer explicit
|
||||||
|
# GET parameters when present, otherwise fall back to middleware-provided
|
||||||
|
# settings on the request.
|
||||||
search_text = request.GET.get("search", "").strip()
|
search_text = request.GET.get("search", "").strip()
|
||||||
sort_key = normalize_sort(request.GET.get("sort", "abc"))
|
sort_key = normalize_sort(
|
||||||
theme = normalize_theme(request.GET.get("theme", "dark"))
|
request.GET.get("sort") or getattr(request, "sort", None) or "abc"
|
||||||
query_state = build_query(search_text, sort_key, theme)
|
)
|
||||||
|
theme = normalize_theme(
|
||||||
|
request.GET.get("theme") or getattr(request, "theme", None) or "dark"
|
||||||
|
)
|
||||||
|
query_state = build_query(search_text)
|
||||||
|
|
||||||
image = Path("/imgs/").joinpath(path_text)
|
image = Path("/imgs/").joinpath(path_text)
|
||||||
img_dir = full_path.parent
|
img_dir = full_path.parent
|
||||||
@@ -169,9 +176,13 @@ def render_image(request, path_text, full_path):
|
|||||||
breadcrumbs.append({"label": full_path.name, "path": None})
|
breadcrumbs.append({"label": full_path.name, "path": None})
|
||||||
|
|
||||||
next_theme = "light" if theme == "dark" else "dark"
|
next_theme = "light" if theme == "dark" else "dark"
|
||||||
theme_query = {"sort": sort_key, "theme": next_theme}
|
theme_query = {
|
||||||
|
"next": request.get_full_path(),
|
||||||
|
"sort": sort_key,
|
||||||
|
"theme": next_theme,
|
||||||
|
}
|
||||||
if search_text != "":
|
if search_text != "":
|
||||||
theme_query["search"] = search_text
|
theme_query["next"] = request.get_full_path()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"image_path": image,
|
"image_path": image,
|
||||||
@@ -196,16 +207,14 @@ def render_image(request, path_text, full_path):
|
|||||||
"modified": fmt_ts(modified_ts),
|
"modified": fmt_ts(modified_ts),
|
||||||
"visits": img.visits,
|
"visits": img.visits,
|
||||||
"visited": fmt_ts(img.last_visited.timestamp()),
|
"visited": fmt_ts(img.last_visited.timestamp()),
|
||||||
"favorite": img.favorite
|
"favorite": img.favorite,
|
||||||
},
|
},
|
||||||
"breadcrumbs": breadcrumbs,
|
"breadcrumbs": breadcrumbs,
|
||||||
"theme": theme,
|
"theme": theme,
|
||||||
"sort_key": sort_key,
|
"sort_key": sort_key,
|
||||||
"sort_label": "",
|
"sort_label": "",
|
||||||
"sort_options": build_sort_options(request, search_text, sort_key, theme),
|
"sort_options": build_sort_options(request, search_text, sort_key, theme),
|
||||||
"theme_toggle_url": gallery_url(
|
"theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query),
|
||||||
Path(dir_path_text) if dir_path_text != "" else None, True, theme_query
|
|
||||||
),
|
|
||||||
"path": path_text,
|
"path": path_text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
viewer/migrations/0002_alter_image_path.py
Normal file
19
viewer/migrations/0002_alter_image_path.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-24 19:38
|
||||||
|
|
||||||
|
import viewer.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("viewer", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="image",
|
||||||
|
name="path",
|
||||||
|
field=models.FilePathField(path=viewer.models.get_gallery_root),
|
||||||
|
),
|
||||||
|
]
|
||||||
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,
|
||||||
@@ -11,13 +12,30 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, on_delete=CASCADE)
|
user = ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, on_delete=CASCADE)
|
||||||
path = FilePathField(path=settings.GALLERY_ROOT, blank=False, null=False)
|
path = FilePathField(path=get_gallery_root, blank=False, null=False)
|
||||||
favorite = BooleanField(blank=False, null=False, default=False)
|
favorite = BooleanField(blank=False, null=False, default=False)
|
||||||
last_visited = DateTimeField(blank=False, null=False, default=timezone.now)
|
last_visited = DateTimeField(blank=False, null=False, default=timezone.now)
|
||||||
visits = IntegerField(blank=False, null=False, default=0)
|
visits = IntegerField(blank=False, null=False, default=0)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb-link {
|
.crumb-link, .crumb-link-last {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -189,7 +189,6 @@ body {
|
|||||||
/* Image view specific styles */
|
/* Image view specific styles */
|
||||||
.image-content {
|
.image-content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 18px;
|
|
||||||
flex: 1 1 auto; /* occupy available vertical space inside main-area */
|
flex: 1 1 auto; /* occupy available vertical space inside main-area */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center; /* center the image vertically */
|
align-items: center; /* center the image vertically */
|
||||||
@@ -381,13 +380,30 @@ body.theme-dark .small.text-muted {
|
|||||||
|
|
||||||
@media (max-width: 991.98px) {
|
@media (max-width: 991.98px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
padding: 12px;
|
padding: 0px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offcanvas {
|
.offcanvas {
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb-sep {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-scroll {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 575.98px) {
|
@media (max-width: 575.98px) {
|
||||||
|
|||||||
@@ -33,6 +33,11 @@
|
|||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
||||||
<span class="input-group-text search-addon"><i class="fa-solid fa-magnifying-glass"></i></span>
|
<span class="input-group-text search-addon"><i class="fa-solid fa-magnifying-glass"></i></span>
|
||||||
|
{% if search_text %}
|
||||||
|
<a class="btn btn-sm btn-plain ms-auto" href="{{ clear_search_url }}" aria-label="Clear search">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -42,6 +47,28 @@
|
|||||||
<a href="#" class="sidebar-link">Most visited</a>
|
<a href="#" class="sidebar-link">Most visited</a>
|
||||||
<a href="#" class="sidebar-link">Recently visited</a>
|
<a href="#" class="sidebar-link">Recently visited</a>
|
||||||
|
|
||||||
|
{% if path != '' %}
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<a href="{{ back_url }}" class="subdir-item">
|
||||||
|
{% if back_thumb %}
|
||||||
|
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||||
|
{% else %}
|
||||||
|
<span class="subdir-fallback"><i class="fa-solid fa-image"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
<span>Back</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ home_url }}" class="subdir-item">
|
||||||
|
{% if home_thumb %}
|
||||||
|
<img src="{{ home_thumb }}" class="subdir-thumb" alt="home thumb">
|
||||||
|
{% else %}
|
||||||
|
<span class="subdir-fallback"><i class="fa-solid fa-image"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
<span>Home</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="sidebar-scroll flex-grow-1">
|
<div class="sidebar-scroll flex-grow-1">
|
||||||
@@ -98,6 +125,28 @@
|
|||||||
<a href="#" class="sidebar-link">Most visited</a>
|
<a href="#" class="sidebar-link">Most visited</a>
|
||||||
<a href="#" class="sidebar-link">Recently visited</a>
|
<a href="#" class="sidebar-link">Recently visited</a>
|
||||||
|
|
||||||
|
{% if path != '' %}
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<a href="{{ back_url }}" class="subdir-item">
|
||||||
|
{% if back_thumb %}
|
||||||
|
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||||
|
{% else %}
|
||||||
|
<span class="subdir-fallback"><i class="fa-solid fa-image"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
<span>Back</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ home_url }}" class="subdir-item">
|
||||||
|
{% if home_thumb %}
|
||||||
|
<img src="{{ home_thumb }}" class="subdir-thumb" alt="home thumb">
|
||||||
|
{% else %}
|
||||||
|
<span class="subdir-fallback"><i class="fa-solid fa-image"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
<span>Home</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="sidebar-scroll" style="max-height: 200px;">
|
<div class="sidebar-scroll" style="max-height: 200px;">
|
||||||
@@ -137,7 +186,7 @@
|
|||||||
{% if not forloop.first %}
|
{% if not forloop.first %}
|
||||||
<span class="crumb-sep">/</span>
|
<span class="crumb-sep">/</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ crumb.path }}" class="crumb-link">{{ crumb.label|truncatechars:45 }}</a>
|
<a href="{{ crumb.path }}" class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label|truncatechars:45 }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -212,6 +261,28 @@
|
|||||||
<a href="#" class="sidebar-link">Most visited</a>
|
<a href="#" class="sidebar-link">Most visited</a>
|
||||||
<a href="#" class="sidebar-link">Recently visited</a>
|
<a href="#" class="sidebar-link">Recently visited</a>
|
||||||
|
|
||||||
|
{% if path != '' %}
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<a href="{{ back_url }}" class="subdir-item">
|
||||||
|
{% if back_thumb %}
|
||||||
|
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||||
|
{% else %}
|
||||||
|
<span class="subdir-fallback"><i class="fa-solid fa-image"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
<span>Back</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ home_url }}" class="subdir-item">
|
||||||
|
{% if home_thumb %}
|
||||||
|
<img src="{{ home_thumb }}" class="subdir-thumb" alt="home thumb">
|
||||||
|
{% else %}
|
||||||
|
<span class="subdir-fallback"><i class="fa-solid fa-image"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
<span>Home</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="sidebar-scroll flex-grow-1">
|
<div class="sidebar-scroll flex-grow-1">
|
||||||
|
|||||||
@@ -164,12 +164,12 @@
|
|||||||
|
|
||||||
<section class="gallery-scroll flex-grow-1 d-flex">
|
<section class="gallery-scroll flex-grow-1 d-flex">
|
||||||
<div class="image-content w-100">
|
<div class="image-content w-100">
|
||||||
<a href="{{ image_path }}" target="_blank">
|
|
||||||
<div class="image-wrapper">
|
<div class="image-wrapper">
|
||||||
|
<a href="{{ image_path }}" target="_blank">
|
||||||
<img src="{{ image_path }}" alt="{{ image_path.name }}" class="image-full">
|
<img src="{{ image_path }}" alt="{{ image_path.name }}" class="image-full">
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
123
viewer/test.py
123
viewer/test.py
@@ -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)
|
||||||
|
|
||||||
@@ -180,26 +193,29 @@ class GalleryViewTests(GalleryBaseTests):
|
|||||||
breadcrumbs = response.context["breadcrumbs"]
|
breadcrumbs = response.context["breadcrumbs"]
|
||||||
self.assertTrue(breadcrumbs[0]["path"].find("/gallery/?") == 0)
|
self.assertTrue(breadcrumbs[0]["path"].find("/gallery/?") == 0)
|
||||||
self.assertIn("search=match", breadcrumbs[0]["path"])
|
self.assertIn("search=match", breadcrumbs[0]["path"])
|
||||||
self.assertIn("sort=recent", breadcrumbs[0]["path"])
|
# build_query now only preserves search; sort/theme are not included
|
||||||
self.assertIn("theme=light", breadcrumbs[0]["path"])
|
self.assertNotIn("sort=", breadcrumbs[0]["path"])
|
||||||
|
self.assertNotIn("theme=", breadcrumbs[0]["path"])
|
||||||
|
|
||||||
def test_subdir_links_preserve_query(self):
|
def test_subdir_links_preserve_query(self):
|
||||||
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
subdir_paths = [subdir["path"] for subdir in response.context["subdirs"]]
|
subdir_paths = [subdir["path"] for subdir in response.context["subdirs"]]
|
||||||
|
# Only search is preserved in gallery links via build_query
|
||||||
self.assertTrue(any(path.find("search=match") != -1 for path in subdir_paths))
|
self.assertTrue(any(path.find("search=match") != -1 for path in subdir_paths))
|
||||||
self.assertTrue(any(path.find("sort=recent") != -1 for path in subdir_paths))
|
self.assertFalse(any(path.find("sort=") != -1 for path in subdir_paths))
|
||||||
self.assertTrue(any(path.find("theme=light") != -1 for path in subdir_paths))
|
self.assertFalse(any(path.find("theme=") != -1 for path in subdir_paths))
|
||||||
|
|
||||||
def test_image_links_preserve_query(self):
|
def test_image_links_preserve_query(self):
|
||||||
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
image_paths = [image["path"] for image in response.context["images"]]
|
image_paths = [image["path"] for image in response.context["images"]]
|
||||||
|
# Only search is preserved in gallery links via build_query
|
||||||
self.assertTrue(any(path.find("search=match") != -1 for path in image_paths))
|
self.assertTrue(any(path.find("search=match") != -1 for path in image_paths))
|
||||||
self.assertTrue(any(path.find("sort=recent") != -1 for path in image_paths))
|
self.assertFalse(any(path.find("sort=") != -1 for path in image_paths))
|
||||||
self.assertTrue(any(path.find("theme=light") != -1 for path in image_paths))
|
self.assertFalse(any(path.find("theme=") != -1 for path in image_paths))
|
||||||
|
|
||||||
def test_theme_toggle_url_preserves_query(self):
|
def test_theme_toggle_url_preserves_query(self):
|
||||||
response = self.client.get("/gallery/?search=match&sort=recent&theme=dark")
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=dark")
|
||||||
@@ -219,6 +235,26 @@ class GalleryViewTests(GalleryBaseTests):
|
|||||||
self.assertIn("sort=recent", url)
|
self.assertIn("sort=recent", url)
|
||||||
self.assertIn("theme=light", url)
|
self.assertIn("theme=light", url)
|
||||||
|
|
||||||
|
def test_clear_search_url_present_and_clears_query(self):
|
||||||
|
# When a search is active the context should provide a URL that
|
||||||
|
# clears the search (no query parameters) for the current path.
|
||||||
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
clear_url = response.context.get("clear_search_url")
|
||||||
|
# For the root path the clear URL should be the gallery root without query
|
||||||
|
self.assertEqual(clear_url, "/gallery/")
|
||||||
|
|
||||||
|
def test_clear_search_url_uses_nested_path(self):
|
||||||
|
# Ensure clear_search_url respects nested paths (clears search but preserves path)
|
||||||
|
response = self.client.get(
|
||||||
|
"/gallery/sub_a/?search=match&sort=recent&theme=light"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
clear_url = response.context.get("clear_search_url")
|
||||||
|
self.assertTrue(clear_url.find("/gallery/sub_a/") == 0)
|
||||||
|
|
||||||
def test_search_action_url_uses_nested_path(self):
|
def test_search_action_url_uses_nested_path(self):
|
||||||
response = self.client.get("/gallery/sub_a/?sort=recent&theme=light")
|
response = self.client.get("/gallery/sub_a/?sort=recent&theme=light")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
@@ -341,8 +377,9 @@ class GalleryViewTests(GalleryBaseTests):
|
|||||||
norm = lambda u: u.replace("/./", "/") if u is not None else u
|
norm = lambda u: u.replace("/./", "/") if u is not None else u
|
||||||
self.assertEqual(norm(back_url), norm(home_url))
|
self.assertEqual(norm(back_url), norm(home_url))
|
||||||
self.assertIn("/gallery/", norm(back_url))
|
self.assertIn("/gallery/", norm(back_url))
|
||||||
self.assertIn("sort=abc", back_url)
|
# build_query no longer injects sort/theme into gallery URLs
|
||||||
self.assertIn("theme=dark", back_url)
|
self.assertNotIn("sort=", back_url)
|
||||||
|
self.assertNotIn("theme=", back_url)
|
||||||
|
|
||||||
# Back and Home thumbnails should be available and point to /thumbs/
|
# Back and Home thumbnails should be available and point to /thumbs/
|
||||||
back_thumb = response.context.get("back_thumb")
|
back_thumb = response.context.get("back_thumb")
|
||||||
@@ -428,6 +465,17 @@ class GalleryTemplateTests(GalleryBaseTests):
|
|||||||
self.assertIn("Most visited", body)
|
self.assertIn("Most visited", body)
|
||||||
self.assertIn("Recently visited", body)
|
self.assertIn("Recently visited", body)
|
||||||
|
|
||||||
|
def test_clear_search_button_shown_when_searching(self):
|
||||||
|
# The template should render a clear-search button when a search is active
|
||||||
|
resp = self.client.get("/gallery/?search=match")
|
||||||
|
body = resp.content.decode("utf-8")
|
||||||
|
self.assertIn('aria-label="Clear search"', body)
|
||||||
|
|
||||||
|
# And it should not be present when there's no search
|
||||||
|
resp2 = self.client.get("/gallery/")
|
||||||
|
body2 = resp2.content.decode("utf-8")
|
||||||
|
self.assertNotIn('aria-label="Clear search"', body2)
|
||||||
|
|
||||||
def test_template_shows_fallback_icon_for_empty_subdir(self):
|
def test_template_shows_fallback_icon_for_empty_subdir(self):
|
||||||
response = self.client.get("/gallery/")
|
response = self.client.get("/gallery/")
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
@@ -501,3 +549,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")
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .views import (
|
from .views import gallery_view, toggle_settings
|
||||||
gallery_view
|
|
||||||
)
|
|
||||||
|
|
||||||
###########################################################################################
|
###########################################################################################
|
||||||
# URL Patterns. #
|
# URL Patterns. #
|
||||||
@@ -13,6 +11,7 @@ from .views import (
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Views.
|
# Views.
|
||||||
path('', gallery_view, name = 'gallery_view_root'),
|
path("", gallery_view, name="gallery_view_root"),
|
||||||
path('<path:path>/', gallery_view, name = 'gallery_view_path'),
|
path("toggle-settings/", toggle_settings, name="toggle_settings"),
|
||||||
|
path("<path:path>/", gallery_view, name="gallery_view_path"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
"""Top-level views module.
|
"""Top-level views module.
|
||||||
|
|
||||||
After refactor this file only keeps the minimal public view entry points and imports
|
After refactor this file only keeps the minimal public view entry points and imports
|
||||||
helpers from the new `directory` and `image` modules.
|
helpers from the new `directory` and `image` modules. Also provides the
|
||||||
|
`toggle_settings` view used by template buttons to persist theme/sort.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Django imports.
|
# Django imports.
|
||||||
|
from urllib.parse import urlparse
|
||||||
from django.http import HttpResponseNotFound
|
from django.http import HttpResponseNotFound
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
|
|
||||||
# Local helpers split into modules
|
# Local helpers split into modules
|
||||||
from .directory import render_directory
|
from .directory import render_directory
|
||||||
from .image import render_image
|
from .image import render_image
|
||||||
|
|
||||||
|
from .models import UserSettings
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
@@ -44,3 +49,51 @@ def gallery_view(request, path=None):
|
|||||||
return render_directory(request, path_text, candidate)
|
return render_directory(request, path_text, candidate)
|
||||||
|
|
||||||
return render_image(request, path_text, candidate)
|
return render_image(request, path_text, candidate)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def toggle_settings(request):
|
||||||
|
"""Persist theme and/or sort for the current user and redirect back.
|
||||||
|
|
||||||
|
Expected query params:
|
||||||
|
- next: the URL to redirect back to (optional)
|
||||||
|
- theme: optional, 'light' or 'dark'
|
||||||
|
- sort: optional, one of allowed sort keys
|
||||||
|
|
||||||
|
The view will obtain or create the UserSettings row for the user and set
|
||||||
|
any provided values. If multiple UserSettings rows exist (shouldn't
|
||||||
|
normally happen) the first is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
next_url = request.GET.get("next") or "/gallery/"
|
||||||
|
|
||||||
|
# Only allow in-site redirects for safety
|
||||||
|
parsed = urlparse(next_url)
|
||||||
|
if parsed.netloc and parsed.netloc != "":
|
||||||
|
next_url = "/gallery/"
|
||||||
|
|
||||||
|
user = getattr(request, "user", None)
|
||||||
|
if not user or not getattr(user, "is_authenticated", False):
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
# Obtain or create the settings row
|
||||||
|
try:
|
||||||
|
settings_obj = UserSettings.objects.get(user=user)
|
||||||
|
except UserSettings.DoesNotExist:
|
||||||
|
settings_obj = UserSettings(user=user)
|
||||||
|
except MultipleObjectsReturned:
|
||||||
|
settings_obj = UserSettings.objects.filter(user=user).first()
|
||||||
|
|
||||||
|
# Apply provided values
|
||||||
|
theme = request.GET.get("theme")
|
||||||
|
sort = request.GET.get("sort")
|
||||||
|
|
||||||
|
if theme in ("light", "dark"):
|
||||||
|
settings_obj.theme = theme
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
settings_obj.sort = sort
|
||||||
|
|
||||||
|
settings_obj.save()
|
||||||
|
|
||||||
|
return redirect(next_url)
|
||||||
|
|||||||
Reference in New Issue
Block a user