From bde5f6adb32450595512e9e6ee12c81ed6529277 Mon Sep 17 00:00:00 2001 From: Afonso Coutinho Date: Wed, 3 Jun 2026 05:23:08 +0100 Subject: [PATCH] fix: gallery tag filters and tag-cleanup are empty in single-user mode (#1771) --- routes/gallery_helpers.py | 12 +++- .../test_gallery_owner_filter_single_user.py | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/test_gallery_owner_filter_single_user.py diff --git a/routes/gallery_helpers.py b/routes/gallery_helpers.py index 77ed383..b36befe 100644 --- a/routes/gallery_helpers.py +++ b/routes/gallery_helpers.py @@ -110,9 +110,17 @@ def _image_to_dict(img: GalleryImage, session_name: str = None) -> Dict[str, Any def _owner_filter(q, user): - """Apply owner filtering to a gallery query.""" + """Apply owner filtering to a gallery query. + + When auth is disabled (single-user mode) get_current_user returns None + and there is no per-user scoping. The main library list and stats already + treat None as "show everything" (`if user is not None`), so this helper + must too — otherwise the tag/model filter sidebars come back empty and the + tag-cleanup endpoints (clear-user-tags, clear-ai-tags, dedupe-tags) + silently affect zero rows in the most common self-hosted deployment. + """ if user is None: - return q.filter(False) + return q return q.filter(GalleryImage.owner == user) diff --git a/tests/test_gallery_owner_filter_single_user.py b/tests/test_gallery_owner_filter_single_user.py new file mode 100644 index 0000000..dc3211b --- /dev/null +++ b/tests/test_gallery_owner_filter_single_user.py @@ -0,0 +1,56 @@ +"""_owner_filter must not blank out the gallery in single-user mode. + +When AUTH_ENABLED=false, get_current_user returns None. The gallery main +list and stats treat None as "show all images" (`if user is not None`), but +_owner_filter returned q.filter(False) (zero rows) for None. So the tag and +model filter chips were always empty and clear-user-tags / clear-ai-tags / +dedupe-tags silently no-oped. _owner_filter must match the main list: no +filter when user is None, owner-scoped otherwise. +""" +import tempfile +import uuid + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool + +import core.database as cdb +from core.database import GalleryImage +from routes.gallery_helpers import _owner_filter + +_TMPDB = tempfile.NamedTemporaryFile(suffix=".db", delete=False) +_ENGINE = create_engine(f"sqlite:///{_TMPDB.name}", connect_args={"check_same_thread": False}, poolclass=NullPool) +cdb.Base.metadata.create_all(_ENGINE) +_TS = sessionmaker(bind=_ENGINE, autoflush=False, autocommit=False) + + +def _seed(*owners): + db = _TS() + try: + db.query(GalleryImage).delete() + for o in owners: + db.add(GalleryImage(id=str(uuid.uuid4()), filename=f"{uuid.uuid4().hex}.png", owner=o)) + db.commit() + finally: + db.close() + + +def test_none_user_returns_all_rows(): + _seed(None, None, "alice") + db = _TS() + try: + n = _owner_filter(db.query(GalleryImage), None).count() + assert n == 3 # old code returned 0 + finally: + db.close() + + +def test_named_user_is_still_scoped(): + _seed("alice", "alice", "bob", None) + db = _TS() + try: + assert _owner_filter(db.query(GalleryImage), "alice").count() == 2 + assert _owner_filter(db.query(GalleryImage), "bob").count() == 1 + finally: + db.close()