Compare commits
6 Commits
24c1c96f19
...
21a3ab40c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 21a3ab40c5 | |||
| 0e48a5d9bd | |||
| 701845dceb | |||
| 97aa42661a | |||
| 5f1035a252 | |||
| 532690a329 |
BIN
viewer/.views.py.kate-swp
Normal file
BIN
viewer/.views.py.kate-swp
Normal file
Binary file not shown.
@@ -64,15 +64,15 @@ def append_query(url, query_dict):
|
||||
return url + "?" + urlencode(query_dict)
|
||||
|
||||
|
||||
def gallery_url(path_obj=None, is_dir=False, query_dict=None):
|
||||
def gallery_url(path_obj=None, is_dir=False, query_dict=None, special=None):
|
||||
if query_dict is None:
|
||||
query_dict = {}
|
||||
|
||||
if path_obj is None:
|
||||
base_url = "/gallery/"
|
||||
base_url = f"/gallery/{special}/" if special is not None else "/gallery/"
|
||||
else:
|
||||
path_text = str(path_obj).replace("\\", "/")
|
||||
base_url = "/gallery/" + path_text
|
||||
base_url = (f"/gallery/{special}/" if special is not None else "/gallery/") + path_text
|
||||
|
||||
if is_dir and not base_url.endswith("/"):
|
||||
base_url += "/"
|
||||
|
||||
@@ -24,6 +24,7 @@ from .common import (
|
||||
is_image_file,
|
||||
make_thumbnail,
|
||||
)
|
||||
from .specials import get_special_paths, special_name
|
||||
|
||||
|
||||
def do_recursive_search(start_path, query):
|
||||
@@ -63,7 +64,7 @@ def do_recursive_search(start_path, query):
|
||||
return subdirs, images
|
||||
|
||||
|
||||
def render_directory(request, path_text, full_path):
|
||||
def render_directory(request, path_text, full_path, special=None):
|
||||
"""
|
||||
Renders the gallery view related to directories, be it the contents of an actual directory
|
||||
in the file system, or logical gallery directories like search result pages.
|
||||
@@ -81,6 +82,17 @@ def render_directory(request, path_text, full_path):
|
||||
)
|
||||
query_state = build_query(search_text)
|
||||
|
||||
# If rendering a logical special gallery, obtain the image list from the
|
||||
# Image model and skip filesystem subdir enumeration.
|
||||
if special is not None:
|
||||
# build images using the helper that returns Path objects
|
||||
images = get_special_paths(request.user, special)
|
||||
# Respect the client's sort_key only if the special is favorites
|
||||
# otherwise the ordering comes from the DB query (most-visited/recent).
|
||||
if special == "favorites":
|
||||
images = sort_images(images, sort_key)
|
||||
current_subdirs = []
|
||||
else:
|
||||
try:
|
||||
current_entries = [
|
||||
entry for entry in full_path.iterdir() if not entry.is_symlink()
|
||||
@@ -110,7 +122,7 @@ def render_directory(request, path_text, full_path):
|
||||
image_data = []
|
||||
for image in images:
|
||||
rel_path = image.relative_to(settings.GALLERY_ROOT)
|
||||
image_url = gallery_url(rel_path, False, query_state)
|
||||
image_url = gallery_url(rel_path, False, query_state, special)
|
||||
|
||||
thumbnail = None
|
||||
try:
|
||||
@@ -162,6 +174,11 @@ def render_directory(request, path_text, full_path):
|
||||
except Exception:
|
||||
dir_text = ""
|
||||
|
||||
# For special galleries hide the Back control but still present Home
|
||||
if special is not None:
|
||||
back_url = None
|
||||
back_thumb = None
|
||||
else:
|
||||
back_url = gallery_url(
|
||||
Path(dir_text) if dir_text != "" else None, True, query_state
|
||||
)
|
||||
@@ -194,6 +211,29 @@ def render_directory(request, path_text, full_path):
|
||||
),
|
||||
}
|
||||
|
||||
# When rendering a special gallery adjust breadcrumbs and hide search
|
||||
# and back controls. Use `is_special` and `special_name` in the template.
|
||||
if special is not None:
|
||||
context["is_special"] = True
|
||||
context["special_name"] = special_name(special)
|
||||
# expose which special is active so templates can highlight it
|
||||
context["active_special"] = special
|
||||
# Override breadcrumbs for special galleries to be a single label
|
||||
# and make it link to the special gallery root so templates can show
|
||||
# it as an active, clickable breadcrumb.
|
||||
from .specials import special_root_url
|
||||
|
||||
context["breadcrumbs"] = [
|
||||
{
|
||||
"label": context["special_name"],
|
||||
"path": special_root_url(special, query_state),
|
||||
}
|
||||
]
|
||||
# Hide the search box (templates use `is_special` to decide)
|
||||
context["search_text"] = ""
|
||||
context["search_action_url"] = ""
|
||||
context["clear_search_url"] = ""
|
||||
|
||||
# sort_label depends on SORT_LABELS in common; import lazily to avoid circulars
|
||||
from .common import SORT_LABELS
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import datetime
|
||||
|
||||
# Django imports.
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -31,9 +31,10 @@ from .common import (
|
||||
is_image_file,
|
||||
make_thumbnail,
|
||||
)
|
||||
from .specials import get_special_paths, special_name
|
||||
|
||||
|
||||
def render_image(request, path_text, full_path):
|
||||
def render_image(request, path_text, full_path, special=None):
|
||||
"""
|
||||
Renders the view corresponding to an image file.
|
||||
"""
|
||||
@@ -67,6 +68,13 @@ def render_image(request, path_text, full_path):
|
||||
image = Path("/imgs/").joinpath(path_text)
|
||||
img_dir = full_path.parent
|
||||
|
||||
if special is not None:
|
||||
# Use the special gallery ordering for prev/next navigation
|
||||
images_sorted = get_special_paths(request.user, special)
|
||||
# If favorites, respect client sort; otherwise keep DB ordering
|
||||
if special == "favorites":
|
||||
images_sorted = sort_images(images_sorted, sort_key)
|
||||
else:
|
||||
try:
|
||||
entries = [
|
||||
entry
|
||||
@@ -79,7 +87,7 @@ def render_image(request, path_text, full_path):
|
||||
# Sort siblings according to requested sort mode
|
||||
images_sorted = sort_images(entries, sort_key)
|
||||
|
||||
# Find index of current image
|
||||
# Find index of current image within the resolved list
|
||||
try:
|
||||
index = next(i for i, p in enumerate(images_sorted) if p.name == full_path.name)
|
||||
except StopIteration:
|
||||
@@ -107,10 +115,21 @@ def render_image(request, path_text, full_path):
|
||||
next_url = None
|
||||
if prev_path is not None:
|
||||
rel = prev_path.relative_to(settings.GALLERY_ROOT)
|
||||
# When viewing from a special gallery, route prev/next through the
|
||||
# special gallery URL so subsequent navigation preserves the special
|
||||
# gallery context. Otherwise use the normal gallery URL for the file.
|
||||
if special is not None:
|
||||
rel_text = str(rel).replace("\\", "/")
|
||||
prev_url = append_query(f"/gallery/{special}/{rel_text}/", query_state)
|
||||
else:
|
||||
prev_url = gallery_url(rel, False, query_state)
|
||||
|
||||
if next_path is not None:
|
||||
rel = next_path.relative_to(settings.GALLERY_ROOT)
|
||||
if special is not None:
|
||||
rel_text = str(rel).replace("\\", "/")
|
||||
next_url = append_query(f"/gallery/{special}/{rel_text}/", query_state)
|
||||
else:
|
||||
next_url = gallery_url(rel, False, query_state)
|
||||
|
||||
# Back (directory) and Home (root) links and thumbnails
|
||||
@@ -122,6 +141,11 @@ def render_image(request, path_text, full_path):
|
||||
except Exception:
|
||||
dir_text = ""
|
||||
|
||||
# For special galleries hide the Back control but still present Home
|
||||
if special is not None:
|
||||
back_url = None
|
||||
back_thumb = None
|
||||
else:
|
||||
back_url = gallery_url(
|
||||
Path(dir_text) if dir_text != "" else None, True, query_state
|
||||
)
|
||||
@@ -169,7 +193,20 @@ def render_image(request, path_text, full_path):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Breadcrumbs: include directory breadcrumbs then append file name as label-only
|
||||
# Breadcrumbs
|
||||
if special is not None:
|
||||
# SPECIAL NAME / IMAGE
|
||||
from .specials import special_root_url
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
"label": special_name(special),
|
||||
"path": special_root_url(special, query_state),
|
||||
},
|
||||
{"label": full_path.name, "path": None},
|
||||
]
|
||||
else:
|
||||
# include directory breadcrumbs then append file name as label-only
|
||||
# build_breadcrumbs expects a path_text representing directories only
|
||||
dir_path_text = dir_text if dir_text != "" else ""
|
||||
breadcrumbs = build_breadcrumbs(dir_path_text, query_state)
|
||||
@@ -216,6 +253,7 @@ def render_image(request, path_text, full_path):
|
||||
"sort_options": build_sort_options(request, search_text, sort_key, theme),
|
||||
"theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query),
|
||||
"path": path_text,
|
||||
"is_special": special is not None,
|
||||
}
|
||||
|
||||
from .common import SORT_LABELS
|
||||
|
||||
71
viewer/specials.py
Normal file
71
viewer/specials.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Helpers for assembling "special" galleries (favorites, most-visited, recent).
|
||||
These functions return filesystem Path objects suitable for use by the
|
||||
existing directory and image renderers.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .models import Image as ImModel
|
||||
|
||||
|
||||
SPECIALS = {
|
||||
"favorites": "Favorites",
|
||||
"most-visited": "Most Visited",
|
||||
"recent": "Recent",
|
||||
}
|
||||
|
||||
|
||||
def special_name(key):
|
||||
return SPECIALS.get(key, key)
|
||||
|
||||
|
||||
def special_root_url(key, query_dict=None):
|
||||
# Build a URL for the special gallery root (e.g. /gallery/favorites/)
|
||||
base = f"/gallery/{key}/"
|
||||
if not query_dict:
|
||||
return base
|
||||
from urllib.parse import urlencode
|
||||
|
||||
return base + "?" + urlencode(query_dict)
|
||||
|
||||
|
||||
def get_special_paths(user, key, limit=100):
|
||||
"""Return a list of pathlib.Path objects for the requested special gallery.
|
||||
|
||||
Only include files that actually exist on disk. The returned list is
|
||||
ordered according to the special gallery semantics.
|
||||
"""
|
||||
|
||||
qs = ImModel.objects.filter(user=user)
|
||||
|
||||
if key == "favorites":
|
||||
qs = qs.filter(favorite=True).order_by("-last_visited")
|
||||
elif key == "most-visited":
|
||||
qs = qs.order_by("-visits")
|
||||
elif key == "recent":
|
||||
qs = qs.order_by("-last_visited")
|
||||
else:
|
||||
return []
|
||||
|
||||
if key in ("most-visited", "recent"):
|
||||
qs = qs[:limit]
|
||||
|
||||
paths = []
|
||||
for row in qs:
|
||||
try:
|
||||
p = Path(row.path)
|
||||
# ensure the stored path is inside GALLERY_ROOT for safety
|
||||
try:
|
||||
p.relative_to(settings.GALLERY_ROOT)
|
||||
except Exception:
|
||||
# skip paths outside the gallery root
|
||||
continue
|
||||
if p.exists() and p.is_file():
|
||||
paths.append(p)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return paths
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<hr>
|
||||
|
||||
{% if not is_special %}
|
||||
<form action="{{ search_action_url }}" method="get">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
||||
@@ -40,16 +41,19 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link {% if active_special == 'favorites' %}active-sort{% endif %}">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link {% if active_special == 'most-visited' %}active-sort{% endif %}">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link {% if active_special == 'recent' %}active-sort{% endif %}">Recently visited</a>
|
||||
|
||||
{% if path != '' or is_special %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% if path != '' %}
|
||||
<hr>
|
||||
|
||||
<a href="{{ back_url }}" class="subdir-item">
|
||||
{% if back_thumb %}
|
||||
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||
@@ -58,7 +62,9 @@
|
||||
{% endif %}
|
||||
<span>Back</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if path != '' or is_special %}
|
||||
<a href="{{ home_url }}" class="subdir-item">
|
||||
{% if home_thumb %}
|
||||
<img src="{{ home_thumb }}" class="subdir-thumb" alt="home thumb">
|
||||
@@ -112,22 +118,26 @@
|
||||
|
||||
<hr>
|
||||
|
||||
{% if not is_special %}
|
||||
<form action="{{ search_action_url }}" method="get">
|
||||
<div class="input-group input-group-sm">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link {% if active_special == 'favorites' %}active-sort{% endif %}">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link {% if active_special == 'most-visited' %}active-sort{% endif %}">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link {% if active_special == 'recent' %}active-sort{% endif %}">Recently visited</a>
|
||||
|
||||
{% if path != '' or is_special %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% if path != '' %}
|
||||
<hr>
|
||||
|
||||
<a href="{{ back_url }}" class="subdir-item">
|
||||
{% if back_thumb %}
|
||||
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||
@@ -136,7 +146,9 @@
|
||||
{% endif %}
|
||||
<span>Back</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if path != '' or is_special %}
|
||||
<a href="{{ home_url }}" class="subdir-item">
|
||||
{% if home_thumb %}
|
||||
<img src="{{ home_thumb }}" class="subdir-thumb" alt="home thumb">
|
||||
@@ -186,7 +198,11 @@
|
||||
{% if not forloop.first %}
|
||||
<span class="crumb-sep">/</span>
|
||||
{% endif %}
|
||||
{% if crumb.path %}
|
||||
<a href="{{ crumb.path }}" class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label|truncatechars:45 }}</a>
|
||||
{% else %}
|
||||
<span class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label|truncatechars:45 }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -257,13 +273,15 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link {% if active_special == 'favorites' %}active-sort{% endif %}">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link {% if active_special == 'most-visited' %}active-sort{% endif %}">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link {% if active_special == 'recent' %}active-sort{% endif %}">Recently visited</a>
|
||||
|
||||
{% if path != '' or is_special %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% if path != '' %}
|
||||
<hr>
|
||||
|
||||
<a href="{{ back_url }}" class="subdir-item">
|
||||
{% if back_thumb %}
|
||||
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||
@@ -272,7 +290,9 @@
|
||||
{% endif %}
|
||||
<span>Back</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if path != '' or is_special %}
|
||||
<a href="{{ home_url }}" class="subdir-item">
|
||||
{% if home_thumb %}
|
||||
<img src="{{ home_thumb }}" class="subdir-thumb" alt="home thumb">
|
||||
|
||||
@@ -29,13 +29,14 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Favorites' %}active-sort{% endif %}">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Most Visited' %}active-sort{% endif %}">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Recent' %}active-sort{% endif %}">Recently visited</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="sidebar-scroll flex-grow-1">
|
||||
{% if not is_special or is_special and 'favorites' in breadcrumbs.0.path %}
|
||||
{% if prev_url %}
|
||||
<a href="{{ prev_url }}" class="subdir-item">
|
||||
{% if prev_thumb %}
|
||||
@@ -67,7 +68,18 @@
|
||||
<span>Next</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_special %}
|
||||
<a href="{{ breadcrumbs.0.path }}" 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>
|
||||
{% else %}
|
||||
<a href="{{ back_url }}" class="subdir-item">
|
||||
{% if back_thumb %}
|
||||
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||
@@ -76,6 +88,7 @@
|
||||
{% endif %}
|
||||
<span>Back</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ home_url }}" class="subdir-item">
|
||||
{% if home_thumb %}
|
||||
@@ -110,9 +123,9 @@
|
||||
<span class="crumb-sep">/</span>
|
||||
{% endif %}
|
||||
{% if crumb.path %}
|
||||
<a href="{{ crumb.path }}" class="crumb-link">{{ crumb.label }}</a>
|
||||
<a href="{{ crumb.path }}" class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label }}</a>
|
||||
{% else %}
|
||||
<span class="crumb-link-full">{{ crumb.label }}</span>
|
||||
<span class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -221,6 +234,16 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if is_special %}
|
||||
<a href="{{ breadcrumbs.0.path }}" 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>
|
||||
{% else %}
|
||||
<a href="{{ back_url }}" class="subdir-item">
|
||||
{% if back_thumb %}
|
||||
<img src="{{ back_thumb }}" class="subdir-thumb" alt="back thumb">
|
||||
@@ -229,6 +252,7 @@
|
||||
{% endif %}
|
||||
<span>Back</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ home_url }}" class="subdir-item">
|
||||
{% if home_thumb %}
|
||||
|
||||
173
viewer/test_specials.py
Normal file
173
viewer/test_specials.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
from viewer.models import Image as Im
|
||||
|
||||
|
||||
class SpecialGalleriesTests(TestCase):
|
||||
def setUp(self):
|
||||
# prepare a temporary gallery root and files
|
||||
self.tmp_gallery = tempfile.TemporaryDirectory()
|
||||
self.gallery_root = Path(self.tmp_gallery.name)
|
||||
self.settings_override = override_settings(GALLERY_ROOT=self.gallery_root)
|
||||
self.settings_override.enable()
|
||||
|
||||
self.user = User.objects.create_user("specuser", "s@example.com", "pw")
|
||||
|
||||
# create files under gallery root
|
||||
now = timezone.now()
|
||||
a = self.gallery_root / "a.jpg"
|
||||
b = self.gallery_root / "b.jpg"
|
||||
c = self.gallery_root / "c.jpg"
|
||||
for p in (a, b, c):
|
||||
p.write_bytes(b"x")
|
||||
|
||||
Im.objects.create(
|
||||
user=self.user, path=str(a), favorite=True, visits=5, last_visited=now
|
||||
)
|
||||
Im.objects.create(
|
||||
user=self.user, path=str(b), favorite=False, visits=10, last_visited=now
|
||||
)
|
||||
Im.objects.create(
|
||||
user=self.user, path=str(c), favorite=True, visits=2, last_visited=now
|
||||
)
|
||||
# client for view tests
|
||||
from django.test import Client
|
||||
|
||||
self.client = Client()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def tearDown(self):
|
||||
self.settings_override.disable()
|
||||
self.tmp_gallery.cleanup()
|
||||
|
||||
def test_get_special_paths_filters_and_orders(self):
|
||||
from viewer.specials import get_special_paths
|
||||
|
||||
favs = get_special_paths(self.user, "favorites")
|
||||
# favorites should include only those marked favorite (a and c)
|
||||
fav_names = sorted([p.name for p in favs])
|
||||
self.assertEqual(fav_names, ["a.jpg", "c.jpg"])
|
||||
|
||||
most = get_special_paths(self.user, "most-visited")
|
||||
# most-visited should be ordered descending by visits, expect b.jpg first
|
||||
self.assertGreaterEqual(len(most), 1)
|
||||
self.assertEqual(most[0].name, "b.jpg")
|
||||
|
||||
def test_favorites_directory_view_and_links(self):
|
||||
# Directory listing for favorites should show only favorite images
|
||||
resp = self.client.get("/gallery/favorites/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ctx = resp.context
|
||||
self.assertTrue(ctx.get("is_special"))
|
||||
names = [img["name"] for img in ctx.get("images")]
|
||||
self.assertEqual(sorted(names), ["a.jpg", "c.jpg"])
|
||||
# Back should be hidden (None) and home present
|
||||
self.assertIsNone(ctx.get("back_url"))
|
||||
self.assertIsNotNone(ctx.get("home_url"))
|
||||
self.assertTrue(str(ctx.get("home_url")).startswith("/gallery/"))
|
||||
# Breadcrumb should point to the special root
|
||||
breadcrumbs = ctx.get("breadcrumbs")
|
||||
self.assertEqual(breadcrumbs[0]["label"], "Favorites")
|
||||
self.assertTrue(breadcrumbs[0]["path"].startswith("/gallery/favorites/"))
|
||||
|
||||
def test_favorites_image_view_prev_next_and_breadcrumbs(self):
|
||||
# Image view under special gallery should scope prev/next to favorites only
|
||||
resp = self.client.get("/gallery/favorites/a.jpg/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ctx = resp.context
|
||||
self.assertTrue(ctx.get("is_special"))
|
||||
# Breadcrumbs: first is special root (clickable), last is filename non-clickable
|
||||
crumbs = ctx.get("breadcrumbs")
|
||||
self.assertEqual(crumbs[0]["label"], "Favorites")
|
||||
self.assertIsNotNone(crumbs[0]["path"])
|
||||
self.assertEqual(crumbs[-1]["label"], "a.jpg")
|
||||
self.assertIsNone(crumbs[-1]["path"])
|
||||
# Prev should be None for first in alphabetical order among favorites
|
||||
self.assertIsNone(ctx.get("prev"))
|
||||
# Next should be c.jpg (since favorites are a.jpg and c.jpg sorted)
|
||||
self.assertIn("c.jpg", ctx.get("next"))
|
||||
|
||||
def test_favorites_image_view_prev_next_urls_and_back_link(self):
|
||||
# Image view under favorites should produce prev/next URLs scoped to favorites
|
||||
resp = self.client.get("/gallery/favorites/a.jpg/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ctx = resp.context
|
||||
self.assertTrue(ctx.get("is_special"))
|
||||
|
||||
crumbs = ctx.get("breadcrumbs")
|
||||
# back link for template should point to the special root
|
||||
self.assertTrue(crumbs[0]["path"].startswith("/gallery/favorites/"))
|
||||
|
||||
# prev_url should be None for first favorite; next_url should be inside favorites
|
||||
self.assertIsNone(ctx.get("prev_url"))
|
||||
next_url = ctx.get("next_url")
|
||||
self.assertIsNotNone(next_url)
|
||||
self.assertTrue(next_url.startswith("/gallery/favorites/"))
|
||||
self.assertIn("c.jpg", next_url)
|
||||
|
||||
def test_most_visited_image_view_prev_next_urls(self):
|
||||
# most-visited image view should scope prev/next URLs to the most-visited gallery
|
||||
resp = self.client.get("/gallery/most-visited/b.jpg/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ctx = resp.context
|
||||
self.assertTrue(ctx.get("is_special"))
|
||||
|
||||
crumbs = ctx.get("breadcrumbs")
|
||||
self.assertTrue(crumbs[0]["path"].startswith("/gallery/most-visited/"))
|
||||
|
||||
# b.jpg is the top most-visited image in fixtures => prev_url None, next_url points to a.jpg
|
||||
self.assertIsNone(ctx.get("prev_url"))
|
||||
next_url = ctx.get("next_url")
|
||||
self.assertIsNotNone(next_url)
|
||||
self.assertTrue(next_url.startswith("/gallery/most-visited/"))
|
||||
self.assertIn("a.jpg", next_url)
|
||||
|
||||
def test_most_visited_and_recent_directory_views(self):
|
||||
# most-visited should list images ordered by visits desc
|
||||
resp = self.client.get("/gallery/most-visited/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
names = [img["name"] for img in resp.context.get("images")]
|
||||
# b.jpg had visits=10, should be first
|
||||
self.assertEqual(names[0], "b.jpg")
|
||||
|
||||
# recent: modify last_visited so that c is newest
|
||||
from viewer.models import Image as ImModel
|
||||
|
||||
now = timezone.now()
|
||||
# update c to be most recent
|
||||
ImModel.objects.filter(path=str(self.gallery_root / "c.jpg")).update(
|
||||
last_visited=now
|
||||
)
|
||||
resp2 = self.client.get("/gallery/recent/")
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
recent_names = [img["name"] for img in resp2.context.get("images")]
|
||||
# c.jpg should be first in recent
|
||||
self.assertEqual(recent_names[0], "c.jpg")
|
||||
|
||||
def test_missing_and_outside_paths_are_skipped(self):
|
||||
# Create an Image row pointing to a missing file inside gallery
|
||||
from viewer.models import Image as ImModel
|
||||
|
||||
missing_path = self.gallery_root / "missing.jpg"
|
||||
ImModel.objects.create(
|
||||
user=self.user, path=str(missing_path), favorite=True, visits=1
|
||||
)
|
||||
|
||||
# Create a file outside gallery root and an Image row pointing to it
|
||||
outside_tmp = tempfile.NamedTemporaryFile(delete=False)
|
||||
outside_tmp.write(b"x")
|
||||
outside_tmp.flush()
|
||||
outside_tmp.close()
|
||||
ImModel.objects.create(
|
||||
user=self.user, path=str(Path(outside_tmp.name)), favorite=True, visits=1
|
||||
)
|
||||
|
||||
# favorites listing should still only include existing gallery files (a.jpg and c.jpg)
|
||||
resp = self.client.get("/gallery/favorites/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
names = sorted([img["name"] for img in resp.context.get("images")])
|
||||
self.assertEqual(names, ["a.jpg", "c.jpg"])
|
||||
@@ -1,8 +1,8 @@
|
||||
# Django imports
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
|
||||
# Module imports
|
||||
from .views import gallery_view, toggle_settings
|
||||
from .views import gallery_view, toggle_settings, special_gallery_view
|
||||
|
||||
###########################################################################################
|
||||
# URL Patterns. #
|
||||
@@ -13,5 +13,17 @@ urlpatterns = [
|
||||
# Views.
|
||||
path("", gallery_view, name="gallery_view_root"),
|
||||
path("toggle-settings/", toggle_settings, name="toggle_settings"),
|
||||
# Special gallery routes (explicit list to avoid clashing with regular paths)
|
||||
re_path(
|
||||
r"^(?P<gallery>favorites|most-visited|recent)/(?P<path>.*)/$",
|
||||
special_gallery_view,
|
||||
name="special_gallery_view_path",
|
||||
),
|
||||
re_path(
|
||||
r"^(?P<gallery>favorites|most-visited|recent)/$",
|
||||
special_gallery_view,
|
||||
name="special_gallery_view_root",
|
||||
),
|
||||
# Generic gallery path (catch-all for filesystem paths)
|
||||
path("<path:path>/", gallery_view, name="gallery_view_path"),
|
||||
]
|
||||
|
||||
@@ -20,6 +20,9 @@ from .image import render_image
|
||||
from .models import UserSettings
|
||||
|
||||
|
||||
SPECIALS = ['favorites', 'most-visited', 'recent']
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
return redirect("gallery_view_root")
|
||||
@@ -51,6 +54,46 @@ def gallery_view(request, path=None):
|
||||
return render_image(request, path_text, candidate)
|
||||
|
||||
|
||||
@login_required
|
||||
def special_gallery_view(request, gallery, path=None):
|
||||
"""
|
||||
Shows a list of subdirectories and image files inside the given special gallery.
|
||||
Available special galleries are: (1) favorites; (2) most visited; (3) recently visited.
|
||||
"""
|
||||
|
||||
root = settings.GALLERY_ROOT.resolve()
|
||||
|
||||
if gallery not in SPECIALS:
|
||||
return HttpResponseNotFound("Not found")
|
||||
|
||||
try:
|
||||
candidate = root.joinpath(path).resolve() if path is not None else root
|
||||
candidate.relative_to(root)
|
||||
except Exception:
|
||||
return HttpResponseNotFound("Not found")
|
||||
|
||||
path_text = path if path is not None else ""
|
||||
|
||||
if path is not None:
|
||||
# Special galleries only have a logical root directory.
|
||||
# When there is a valid sub-directory path inside the gallery root
|
||||
# in the request then redirect to the base special gallery.
|
||||
if candidate.is_dir():
|
||||
return redirect('special_gallery_view_path', gallery, None, permanent=True)
|
||||
|
||||
else:
|
||||
# When there is no path to render, then go to the corresponding special gallery root.
|
||||
return render_directory(request, path_text, candidate, gallery)
|
||||
|
||||
# Fail if the requested image doesn' exist.
|
||||
if not candidate.exists():
|
||||
return HttpResponseNotFound("Not found")
|
||||
|
||||
# Images are rendered normally, with a control to ensure the back, next and previous buttons
|
||||
# use the special gallery.
|
||||
return render_image(request, path_text, candidate, gallery)
|
||||
|
||||
|
||||
@login_required
|
||||
def toggle_settings(request):
|
||||
"""Persist theme and/or sort for the current user and redirect back.
|
||||
|
||||
Reference in New Issue
Block a user