mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
BIN
tests/fixtures/empty_libraries/DB_VERSION_9/.TagStudio/ts_library.sqlite
vendored
Normal file
BIN
tests/fixtures/empty_libraries/DB_VERSION_9/.TagStudio/ts_library.sqlite
vendored
Normal file
Binary file not shown.
Binary file not shown.
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user