feat: add filename and path sorting (#842)

* feat: add filename sorting to dropdown

* feat: add file path sorting to dropdown

* feat: implement path sorting

* feat: add filename column and bump db version

* feat: implement filename sorting

* doc: tick off roadmap item for filename sorting

* fix: use existing filename translation instead

* fix: populate Entry.filename in constructor

* fix: add missing assertion in search_library fixture

* fix: update search test library

* feat: add db migration test

* fix: add missing library for test
This commit is contained in:
Jann Stute
2025-03-13 01:28:42 +01:00
committed by GitHub
parent 93dcfdd51c
commit 31833245a4
11 changed files with 54 additions and 4 deletions

View File

@@ -94,7 +94,7 @@ These version milestones are rough estimations for when the previous core featur
- [ ] Field content search [HIGH]
- [ ] Sort by date created [HIGH]
- [ ] Sort by date modified [HIGH]
- [ ] Sort by filename [HIGH]
- [x] Sort by filename [HIGH]
- [ ] HAS operator for composition tags [HIGH]
- [ ] Search bar rework
- [ ] Improved tag autocomplete [HIGH]

View File

@@ -82,4 +82,4 @@ class LibraryPrefs(DefaultEnum):
IS_EXCLUDE_LIST = True
EXTENSION_LIST = [".json", ".xmp", ".aae"]
PAGE_SIZE = 500
DB_VERSION = 8
DB_VERSION = 9

View File

@@ -67,6 +67,8 @@ class ItemType(enum.Enum):
class SortingModeEnum(enum.Enum):
DATE_ADDED = "file.date_added"
FILE_NAME = "generic.filename"
PATH = "file.path"
@dataclass

View File

@@ -472,12 +472,25 @@ class Library:
# Apply any post-SQL migration patches.
if not is_new:
# save backup if patches will be applied
if LibraryPrefs.DB_VERSION.default != db_version:
self.library_dir = library_dir
self.save_library_backup_to_disk()
self.library_dir = None
# schema changes first
if db_version < 8:
self.apply_db8_schema_changes(session)
if db_version < 9:
self.apply_db9_schema_changes(session)
# now the data changes
if db_version == 6:
self.apply_repairs_for_db6(session)
if db_version >= 6 and db_version < 8:
self.apply_db8_default_data(session)
if db_version < 9:
self.apply_db9_filename_population(session)
# Update DB_VERSION
if LibraryPrefs.DB_VERSION.default > db_version:
@@ -580,6 +593,29 @@ class Library:
)
session.rollback()
def apply_db9_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 9."""
add_filename_column = text(
"ALTER TABLE entries ADD COLUMN filename TEXT NOT NULL DEFAULT ''"
)
try:
session.execute(add_filename_column)
session.commit()
logger.info("[Library][Migration] Added filename column to entries table")
except Exception as e:
logger.error(
"[Library][Migration] Could not create filename column in entries table!",
error=e,
)
session.rollback()
def apply_db9_filename_population(self, session: Session):
"""Populate the filename column introduced in DB_VERSION 9."""
for entry in self.get_entries():
session.merge(entry).filename = entry.path.name
session.commit()
logger.info("[Library][Migration] Populated filename column in entries table")
@property
def default_fields(self) -> list[BaseField]:
with Session(self.engine) as session:
@@ -852,7 +888,7 @@ class Library:
statement = statement.distinct(Entry.id)
start_time = time.time()
query_count = select(func.count()).select_from(statement.alias("entries"))
count_all: int = session.execute(query_count).scalar()
count_all: int = session.execute(query_count).scalar() or 0
end_time = time.time()
logger.info(f"finished counting ({format_timespan(end_time - start_time)})")
@@ -860,6 +896,10 @@ class Library:
match search.sorting_mode:
case SortingModeEnum.DATE_ADDED:
sort_on = Entry.id
case SortingModeEnum.FILE_NAME:
sort_on = func.lower(Entry.filename)
case SortingModeEnum.PATH:
sort_on = func.lower(Entry.path)
statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on))
statement = statement.limit(search.limit).offset(search.offset)
@@ -1371,6 +1411,8 @@ class Library:
target_path,
)
logger.info("Library backup saved to disk.", path=target_path)
return target_path
def get_tag(self, tag_id: int) -> Tag | None:

View File

@@ -187,6 +187,7 @@ class Entry(Base):
folder: Mapped[Folder] = relationship("Folder")
path: Mapped[Path] = mapped_column(PathType, unique=True)
filename: Mapped[str] = mapped_column()
suffix: Mapped[str] = mapped_column()
date_created: Mapped[dt | None]
date_modified: Mapped[dt | None]
@@ -232,6 +233,7 @@ class Entry(Base):
self.path = path
self.folder = folder
self.id = id
self.filename = path.name
self.suffix = path.suffix.lstrip(".").lower()
# The date the file associated with this entry was created.

View File

@@ -66,6 +66,7 @@
"file.date_added": "Hinzufügungsdatum",
"file.date_created": "Erstellungsdatum",
"file.date_modified": "Datum geändert",
"file.path": "Dateipfad",
"file.dimensions": "Abmessungen",
"file.duplicates.description": "TagStudio unterstützt das Importieren von DupeGuru-Ergebnissen um Dateiduplikate zu verwalten.",
"file.duplicates.dupeguru.advice": "Nach dem Kopiervorgang kann DupeGuru benutzt werden und ungewollte Dateien zu löschen. Anschließend kann TagStudios \"Unverknüpfte Einträge reparieren\" Funktion im \"Werkzeuge\" Menü benutzt werden um die nicht verknüpften Einträge zu löschen.",

View File

@@ -68,6 +68,7 @@
"file.date_added": "Date Added",
"file.date_created": "Date Created",
"file.date_modified": "Date Modified",
"file.path": "File Path",
"file.dimensions": "Dimensions",
"file.duplicates.description": "TagStudio supports importing DupeGuru results to manage duplicate files.",
"file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to delete the unlinked Entries.",

View File

@@ -114,7 +114,8 @@ def library(request):
@pytest.fixture
def search_library() -> Library:
lib = Library()
lib.open_library(Path(CWD / "fixtures" / "search_library"))
status = lib.open_library(Path(CWD / "fixtures" / "search_library"))
assert status.success
return lib

View File

@@ -22,6 +22,7 @@ EMPTY_LIBRARIES = "empty_libraries"
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_6")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_7")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_8")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_9")),
],
)
def test_library_migrations(path: str):