fix: restore page navigation state (#933)

* refactor: store browsing history for navigation purposes

* refactor: remove page_size from FilterState

* refactor: move on from the term "filter" in favor of "BrowsingState"

* fix: refactors didn't propagate to the tests

* fix: ruff complaints

* fix: remaing refactoring errors

* fix: navigation works again

* fix: also store and restore query
This commit is contained in:
Jann Stute
2025-06-04 09:29:07 +02:00
committed by GitHub
parent 1e783a5e3c
commit cf6c56c9d2
12 changed files with 196 additions and 155 deletions

View File

@@ -4,7 +4,7 @@ from pathlib import Path
import structlog
from tagstudio.core.query_lang.ast import AST, Constraint, ConstraintType
from tagstudio.core.query_lang.ast import AST
from tagstudio.core.query_lang.parser import Parser
MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140
@@ -72,59 +72,57 @@ class SortingModeEnum(enum.Enum):
@dataclass
class FilterState:
class BrowsingState:
"""Represent a state of the Library grid view."""
# these should remain
page_size: int
page_index: int = 0
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
ascending: bool = True
# these should be erased on update
query: str | None = None
# Abstract Syntax Tree Of the current Search Query
ast: AST | None = None
@property
def limit(self):
return self.page_size
@property
def offset(self):
return self.page_size * self.page_index
def ast(self) -> AST | None:
if self.query is None:
return None
return Parser(self.query).parse()
@classmethod
def show_all(cls, page_size: int) -> "FilterState":
return FilterState(page_size=page_size)
def show_all(cls) -> "BrowsingState":
return BrowsingState()
@classmethod
def from_search_query(cls, search_query: str, page_size: int) -> "FilterState":
return cls(ast=Parser(search_query).parse(), page_size=page_size)
def from_search_query(cls, search_query: str) -> "BrowsingState":
return cls(query=search_query)
@classmethod
def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size)
def from_tag_id(cls, tag_id: int | str) -> "BrowsingState":
return cls(query=f"tag_id:{str(tag_id)}")
@classmethod
def from_path(cls, path: Path | str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size)
def from_path(cls, path: Path | str) -> "BrowsingState":
return cls(query=f'path:"{str(path).strip()}"')
@classmethod
def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size)
def from_mediatype(cls, mediatype: str) -> "BrowsingState":
return cls(query=f"mediatype:{mediatype}")
@classmethod
def from_filetype(cls, filetype: str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size)
def from_filetype(cls, filetype: str) -> "BrowsingState":
return cls(query=f"filetype:{filetype}")
@classmethod
def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState":
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size)
def from_tag_name(cls, tag_name: str) -> "BrowsingState":
return cls(query=f'tag:"{tag_name}"')
def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState":
def with_page_index(self, index: int) -> "BrowsingState":
return replace(self, page_index=index)
def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
return replace(self, sorting_mode=mode)
def with_sorting_direction(self, ascending: bool) -> "FilterState":
def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
return replace(self, ascending=ascending)

View File

@@ -61,8 +61,8 @@ from tagstudio.core.library.alchemy import default_color_groups
from tagstudio.core.library.alchemy.db import make_tables
from tagstudio.core.library.alchemy.enums import (
MAX_SQL_VARIABLES,
BrowsingState,
FieldTypeEnum,
FilterState,
SortingModeEnum,
)
from tagstudio.core.library.alchemy.fields import (
@@ -857,13 +857,14 @@ class Library:
def search_library(
self,
search: FilterState,
search: BrowsingState,
page_size: int,
) -> SearchResult:
"""Filter library by search query.
:return: number of entries matching the query and one page of results.
"""
assert isinstance(search, FilterState)
assert isinstance(search, BrowsingState)
assert self.engine
with Session(self.engine, expire_on_commit=False) as session:
@@ -902,7 +903,7 @@ class Library:
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)
statement = statement.limit(page_size).offset(search.page_index * page_size)
logger.info(
"searching library",

View File

@@ -4,7 +4,7 @@ from pathlib import Path
import structlog
from tagstudio.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
@@ -52,7 +52,7 @@ class DupeRegistry:
continue
results = self.library.search_library(
FilterState.from_path(path_relative, page_size=500),
BrowsingState.from_path(path_relative), 500
)
if not results:

View File

@@ -63,7 +63,7 @@ class FixUnlinkedEntriesModal(QWidget):
self.relink_class.done.connect(
# refresh the grid
lambda: (
self.driver.filter_items(),
self.driver.update_browsing_state(),
self.refresh_missing_files(),
)
)
@@ -78,7 +78,7 @@ class FixUnlinkedEntriesModal(QWidget):
lambda: (
self.set_missing_count(),
# refresh the grid
self.driver.filter_items(),
self.driver.update_browsing_state(),
)
)
self.delete_button.clicked.connect(self.delete_modal.show)

View File

@@ -24,7 +24,7 @@ from PySide6.QtWidgets import (
)
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
from tagstudio.core.library.alchemy.enums import FilterState, TagColorEnum
from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.core.palette import ColorType, get_tag_color
@@ -292,9 +292,7 @@ class TagSearchPanel(PanelWidget):
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=tag.id: (
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
self.driver.filter_items(
FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size)
),
self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
)
)
tag_widget.search_for_tag_action.setEnabled(True)

View File

@@ -10,7 +10,6 @@
import contextlib
import ctypes
import dataclasses
import math
import os
import platform
@@ -21,6 +20,7 @@ from argparse import Namespace
from pathlib import Path
from queue import Queue
from shutil import which
from typing import Generic, TypeVar
from warnings import catch_warnings
import structlog
@@ -60,8 +60,8 @@ from tagstudio.core.driver import DriverMixin
from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption
from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
from tagstudio.core.library.alchemy.enums import (
BrowsingState,
FieldTypeEnum,
FilterState,
ItemType,
SortingModeEnum,
)
@@ -121,6 +121,10 @@ else:
logger = structlog.get_logger(__name__)
def clamp(value, lower_bound, upper_bound):
return max(lower_bound, min(upper_bound, value))
class Consumer(QThread):
MARKER_QUIT = "MARKER_QUIT"
@@ -139,6 +143,38 @@ class Consumer(QThread):
pass
T = TypeVar("T")
# Ex. User visits | A ->[B] |
# | A B ->[C]|
# | A [B]<- C |
# |[A]<- B C | Previous routes still exist
# | A ->[D] | Stack is cut from [:A] on new route
class History(Generic[T]):
__history: list[T]
__index: int = 0
def __init__(self, initial_value: T):
self.__history = [initial_value]
super().__init__()
def erase_future(self) -> None:
self.__history = self.__history[: self.__index + 1]
def push(self, value: T) -> None:
self.erase_future()
self.__history.append(value)
self.__index = len(self.__history) - 1
def move(self, delta: int):
self.__index = clamp(self.__index + delta, 0, len(self.__history) - 1)
@property
def current(self) -> T:
return self.__history[self.__index]
class QtDriver(DriverMixin, QObject):
"""A Qt GUI frontend driver for TagStudio."""
@@ -154,10 +190,13 @@ class QtDriver(DriverMixin, QObject):
about_modal: AboutModal
unlinked_modal: FixUnlinkedEntriesModal
dupe_modal: FixDupeFilesModal
applied_theme: Theme
lib: Library
browsing_history: History[BrowsingState]
def __init__(self, args: Namespace):
super().__init__()
# prevent recursive badges update when multiple items selected
@@ -167,7 +206,6 @@ class QtDriver(DriverMixin, QObject):
self.args = args
self.frame_content: list[int] = [] # List of Entry IDs on the current page
self.pages_count = 0
self.applied_theme = None
self.scrollbar_pos = 0
self.thumb_size = 128
@@ -195,7 +233,9 @@ class QtDriver(DriverMixin, QObject):
"[Settings] Global Settings File does not exist creating",
path=self.global_settings_path,
)
self.filter = FilterState.show_all(page_size=self.settings.page_size)
self.applied_theme = self.settings.theme
self.__reset_navigation()
if self.args.cache_file:
path = Path(self.args.cache_file)
@@ -237,6 +277,9 @@ class QtDriver(DriverMixin, QObject):
self.add_tag_to_selected_action: QAction | None = None
def __reset_navigation(self) -> None:
self.browsing_history = History(BrowsingState.show_all())
def init_workers(self):
"""Init workers for rendering thumbnails."""
if not self.thumb_threads:
@@ -281,7 +324,6 @@ class QtDriver(DriverMixin, QObject):
self.app.styleHints().setColorScheme(
Qt.ColorScheme.Dark if self.settings.theme == Theme.DARK else Qt.ColorScheme.Light
)
self.applied_theme = self.settings.theme
if (
platform.system() == "Darwin" or platform.system() == "Windows"
@@ -700,7 +742,6 @@ class QtDriver(DriverMixin, QObject):
]
self.item_thumbs: list[ItemThumb] = []
self.thumb_renderers: list[ThumbRenderer] = []
self.filter = FilterState.show_all(page_size=self.settings.page_size)
self.init_library_window()
self.migration_modal: JsonMigrationModal = None
@@ -744,12 +785,10 @@ class QtDriver(DriverMixin, QObject):
# in a global dict for methods to access for different DPIs.
# adj_font_size = math.floor(12 * self.main_window.devicePixelRatio())
def _filter_items():
def _update_browsing_state():
try:
self.filter_items(
FilterState.from_search_query(
self.main_window.searchField.text(), page_size=self.settings.page_size
)
self.update_browsing_state(
BrowsingState.from_search_query(self.main_window.searchField.text())
.with_sorting_mode(self.sorting_mode)
.with_sorting_direction(self.sorting_direction)
)
@@ -758,21 +797,21 @@ class QtDriver(DriverMixin, QObject):
f"{Translations['status.results.invalid_syntax']} "
f'"{self.main_window.searchField.text()}"'
)
logger.error("[QtDriver] Could not filter items", error=e)
logger.error("[QtDriver] Could not update BrowsingState", error=e)
# Search Button
search_button: QPushButton = self.main_window.searchButton
search_button.clicked.connect(_filter_items)
search_button.clicked.connect(_update_browsing_state)
# Search Field
search_field: QLineEdit = self.main_window.searchField
search_field.returnPressed.connect(_filter_items)
search_field.returnPressed.connect(_update_browsing_state)
# Sorting Dropdowns
sort_mode_dropdown: QComboBox = self.main_window.sorting_mode_combobox
for sort_mode in SortingModeEnum:
sort_mode_dropdown.addItem(Translations[sort_mode.value], sort_mode)
sort_mode_dropdown.setCurrentIndex(
list(SortingModeEnum).index(self.filter.sorting_mode)
) # set according to self.filter
list(SortingModeEnum).index(self.browsing_history.current.sorting_mode)
) # set according to navigation state
sort_mode_dropdown.currentIndexChanged.connect(self.sorting_mode_callback)
sort_dir_dropdown: QComboBox = self.main_window.sorting_direction_combobox
@@ -794,9 +833,9 @@ class QtDriver(DriverMixin, QObject):
self._init_thumb_grid()
back_button: QPushButton = self.main_window.backButton
back_button.clicked.connect(lambda: self.page_move(-1))
back_button.clicked.connect(lambda: self.navigation_callback(-1))
forward_button: QPushButton = self.main_window.forwardButton
forward_button.clicked.connect(lambda: self.page_move(1))
forward_button.clicked.connect(lambda: self.navigation_callback(1))
# NOTE: Putting this early will result in a white non-responsive
# window until everything is loaded. Consider adding a splash screen
@@ -805,7 +844,7 @@ class QtDriver(DriverMixin, QObject):
self.main_window.activateWindow()
self.main_window.toggle_landing_page(enabled=True)
self.main_window.pagination.index.connect(lambda i: self.page_move(page_id=i))
self.main_window.pagination.index.connect(lambda i: self.page_move(i, absolute=True))
self.splash.finish(self.main_window)
@@ -825,7 +864,9 @@ class QtDriver(DriverMixin, QObject):
)
self.file_extension_panel.setTitle(Translations["ignore_list.title"])
self.file_extension_panel.setWindowTitle(Translations["ignore_list.title"])
self.file_extension_panel.saved.connect(lambda: (panel.save(), self.filter_items()))
self.file_extension_panel.saved.connect(
lambda: (panel.save(), self.update_browsing_state())
)
self.manage_file_ext_action.triggered.connect(self.file_extension_panel.show)
def show_grid_filenames(self, value: bool):
@@ -871,7 +912,7 @@ class QtDriver(DriverMixin, QObject):
self.main_window.searchField.setText("")
scrollbar: QScrollArea = self.main_window.scrollArea
scrollbar.verticalScrollBar().setValue(0)
self.filter = FilterState.show_all(page_size=self.settings.page_size)
self.__reset_navigation()
self.lib.close()
@@ -1059,7 +1100,7 @@ class QtDriver(DriverMixin, QObject):
self.selected.clear()
if deleted_count > 0:
self.filter_items()
self.update_browsing_state()
self.preview_panel.update_widgets()
if len(self.selected) <= 1 and deleted_count == 0:
@@ -1211,7 +1252,7 @@ class QtDriver(DriverMixin, QObject):
pw.hide(),
pw.deleteLater(),
# refresh the library only when new items are added
files_count and self.filter_items(), # type: ignore
files_count and self.update_browsing_state(), # type: ignore
)
)
QThreadPool.globalInstance().start(r)
@@ -1282,7 +1323,9 @@ class QtDriver(DriverMixin, QObject):
def sorting_direction_callback(self):
logger.info("Sorting Direction Changed", ascending=self.sorting_direction)
self.filter_items()
self.update_browsing_state(
self.browsing_history.current.with_sorting_direction(self.sorting_direction)
)
@property
def sorting_mode(self) -> SortingModeEnum:
@@ -1291,7 +1334,9 @@ class QtDriver(DriverMixin, QObject):
def sorting_mode_callback(self):
logger.info("Sorting Mode Changed", mode=self.sorting_mode)
self.filter_items()
self.update_browsing_state(
self.browsing_history.current.with_sorting_mode(self.sorting_mode)
)
def thumb_size_callback(self, index: int):
"""Perform actions needed when the thumbnail size selection is changed.
@@ -1324,41 +1369,42 @@ class QtDriver(DriverMixin, QObject):
def mouse_navigation(self, event: QMouseEvent):
# print(event.button())
if event.button() == Qt.MouseButton.ForwardButton:
self.page_move(1)
self.navigation_callback(1)
elif event.button() == Qt.MouseButton.BackButton:
self.page_move(-1)
self.navigation_callback(-1)
def page_move(self, delta: int = None, page_id: int = None) -> None:
"""Navigate a step further into the navigation stack."""
logger.info(
"page_move",
delta=delta,
page_id=page_id,
def page_move(self, value: int, absolute=False) -> None:
logger.info("page_move", value=value, absolute=absolute)
if not absolute:
value += self.browsing_history.current.page_index
self.browsing_history.push(
self.browsing_history.current.with_page_index(clamp(value, 0, self.pages_count - 1))
)
# Ex. User visits | A ->[B] |
# | A B ->[C]|
# | A [B]<- C |
# |[A]<- B C | Previous routes still exist
# | A ->[D] | Stack is cut from [:A] on new route
# sb: QScrollArea = self.main_window.scrollArea
# sb_pos = sb.verticalScrollBar().value()
page_index = page_id if page_id is not None else self.filter.page_index + delta
page_index = max(0, min(page_index, self.pages_count - 1))
self.filter.page_index = page_index
# TODO: Re-allow selecting entries across multiple pages at once.
# This works fine with additive selection but becomes a nightmare with bridging.
self.filter_items()
self.update_browsing_state()
def navigation_callback(self, delta: int) -> None:
"""Callback for the Forwads and Backwards Navigation Buttons next to the search bar."""
logger.info(
"navigation_callback",
delta=delta,
)
self.browsing_history.move(delta)
self.update_browsing_state()
def remove_grid_item(self, grid_idx: int):
self.frame_content[grid_idx] = None
self.item_thumbs[grid_idx].hide()
def _update_thumb_count(self):
missing_count = max(0, self.filter.page_size - len(self.item_thumbs))
missing_count = max(0, self.settings.page_size - len(self.item_thumbs))
layout = self.flow_container.layout()
for _ in range(missing_count):
item_thumb = ItemThumb(
@@ -1742,17 +1788,17 @@ class QtDriver(DriverMixin, QObject):
pending_entries.get(badge_type, []), BADGE_TAGS[badge_type]
)
def filter_items(self, filter: FilterState | None = None) -> None:
def update_browsing_state(self, state: BrowsingState | None = None) -> None:
"""Navigates to a new BrowsingState when state is given, otherwise updates the results."""
if not self.lib.library_dir:
logger.info("Library not loaded")
return
assert self.lib.engine
if filter:
self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter))
else:
self.filter.sorting_mode = self.sorting_mode
self.filter.ascending = self.sorting_direction
if state:
self.browsing_history.push(state)
self.main_window.searchField.setText(self.browsing_history.current.query or "")
# inform user about running search
self.main_window.statusbar.showMessage(Translations["status.library_search_query"])
@@ -1760,7 +1806,7 @@ class QtDriver(DriverMixin, QObject):
# search the library
start_time = time.time()
results = self.lib.search_library(self.filter)
results = self.lib.search_library(self.browsing_history.current, self.settings.page_size)
logger.info("items to render", count=len(results))
end_time = time.time()
@@ -1778,9 +1824,9 @@ class QtDriver(DriverMixin, QObject):
self.update_thumbs()
# update pagination
self.pages_count = math.ceil(results.total_count / self.filter.page_size)
self.pages_count = math.ceil(results.total_count / self.settings.page_size)
self.main_window.pagination.update_buttons(
self.pages_count, self.filter.page_index, emit=False
self.pages_count, self.browsing_history.current.page_index, emit=False
)
def remove_recent_library(self, item_key: str):
@@ -1915,14 +1961,14 @@ class QtDriver(DriverMixin, QObject):
if open_status.json_migration_req:
self.migration_modal = JsonMigrationModal(path)
self.migration_modal.migration_finished.connect(
lambda: self.init_library(path, self.lib.open_library(path))
lambda: self._init_library(path, self.lib.open_library(path))
)
self.main_window.landing_widget.set_status_label("")
self.migration_modal.paged_panel.show()
else:
self.init_library(path, open_status)
self._init_library(path, open_status)
def init_library(self, path: Path, open_status: LibraryStatus):
def _init_library(self, path: Path, open_status: LibraryStatus):
if not open_status.success:
self.show_error_message(
error_name=open_status.message
@@ -1933,7 +1979,7 @@ class QtDriver(DriverMixin, QObject):
self.init_workers()
self.filter.page_size = self.settings.page_size
self.__reset_navigation()
# TODO - make this call optional
if self.lib.entries_count < 10000:
@@ -1973,7 +2019,7 @@ class QtDriver(DriverMixin, QObject):
self.preview_panel.update_widgets()
# page (re)rendering, extract eventually
self.filter_items()
self.update_browsing_state()
self.main_window.toggle_landing_page(enabled=False)
return open_status

View File

@@ -8,7 +8,7 @@ import typing
import structlog
from PySide6.QtCore import Signal
from tagstudio.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.qt.flowlayout import FlowLayout
from tagstudio.qt.modals.build_tag import BuildTagPanel
@@ -67,9 +67,7 @@ class TagBoxWidget(FieldWidget):
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=tag.id: (
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
self.driver.filter_items(
FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size)
),
self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
)
)

View File

@@ -3,7 +3,7 @@ from tempfile import TemporaryDirectory
import pytest
from tagstudio.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.missing_files import MissingRegistry
@@ -28,5 +28,5 @@ def test_refresh_missing_files(library: Library):
assert list(registry.fix_unlinked_entries()) == [0, 1]
# `bar.md` should be relinked to new correct path
results = library.search_library(FilterState.from_path("bar.md", page_size=500))
results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
assert results[0].path == Path("bar.md")

View File

@@ -135,7 +135,7 @@ def test_title_update(
qt_driver.folders_to_tags_action = QAction(menu_bar)
# Trigger the update
qt_driver.init_library(library_dir, open_status)
qt_driver._init_library(library_dir, open_status)
# Assert the title is updated correctly
qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title))

View File

@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from tagstudio.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.json.library import ItemType
from tagstudio.qt.widgets.item_thumb import ItemThumb
@@ -66,7 +66,7 @@ if TYPE_CHECKING:
# assert qt_driver.selected == [0, 1, 2]
def test_library_state_update(qt_driver: "QtDriver"):
def test_browsing_state_update(qt_driver: "QtDriver"):
# Given
for entry in qt_driver.lib.get_entries(with_joins=True):
thumb = ItemThumb(ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100))
@@ -74,27 +74,25 @@ def test_library_state_update(qt_driver: "QtDriver"):
qt_driver.frame_content.append(entry)
# no filter, both items are returned
qt_driver.filter_items()
qt_driver.update_browsing_state()
assert len(qt_driver.frame_content) == 2
# filter by tag
state = FilterState.from_tag_name("foo", page_size=10)
qt_driver.filter_items(state)
assert qt_driver.filter.page_size == 10
state = BrowsingState.from_tag_name("foo")
qt_driver.update_browsing_state(state)
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert list(entry.tags)[0].name == "foo"
# When state is not changed, previous one is still applied
qt_driver.filter_items()
assert qt_driver.filter.page_size == 10
qt_driver.update_browsing_state()
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert list(entry.tags)[0].name == "foo"
# When state property is changed, previous one is overwritten
state = FilterState.from_path("*bar.md", page_size=qt_driver.settings.page_size)
qt_driver.filter_items(state)
state = BrowsingState.from_path("*bar.md")
qt_driver.update_browsing_state(state)
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert list(entry.tags)[0].name == "bar"

View File

@@ -4,7 +4,7 @@ from tempfile import TemporaryDirectory
import pytest
from tagstudio.core.enums import DefaultEnum, LibraryPrefs
from tagstudio.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.fields import TextField, _FieldID
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry, Tag
@@ -123,7 +123,8 @@ def test_library_search(library: Library, generate_tag, entry_full):
tag = list(entry_full.tags)[0]
results = library.search_library(
FilterState.from_tag_name(tag.name, page_size=500),
BrowsingState.from_tag_name(tag.name),
page_size=500,
)
assert results.total_count == 1
@@ -152,7 +153,7 @@ def test_entries_count(library: Library):
new_ids = library.add_entries(entries)
assert len(new_ids) == 10
results = library.search_library(FilterState.show_all(page_size=5))
results = library.search_library(BrowsingState.show_all(), page_size=5)
assert results.total_count == 12
assert len(results) == 5
@@ -199,9 +200,7 @@ def test_search_filter_extensions(library: Library, is_exclude: bool):
library.set_prefs(LibraryPrefs.EXTENSION_LIST, ["md"])
# When
results = library.search_library(
FilterState.show_all(page_size=500),
)
results = library.search_library(BrowsingState.show_all(), page_size=500)
# Then
assert results.total_count == 1
@@ -221,7 +220,8 @@ def test_search_library_case_insensitive(library: Library):
# When
results = library.search_library(
FilterState.from_tag_name(tag.name.upper(), page_size=500),
BrowsingState.from_tag_name(tag.name.upper()),
page_size=500,
)
# Then
@@ -443,100 +443,102 @@ def test_library_prefs_multiple_identical_vals():
def test_path_search_ilike(library: Library):
results = library.search_library(FilterState.from_path("bar.md", page_size=500))
results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_like(library: Library):
results = library.search_library(FilterState.from_path("BAR.MD", page_size=500))
results = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500)
assert results.total_count == 0
assert len(results.items) == 0
def test_path_search_default_with_sep(library: Library):
results = library.search_library(FilterState.from_path("one/two", page_size=500))
results = library.search_library(BrowsingState.from_path("one/two"), page_size=500)
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_glob_after(library: Library):
results = library.search_library(FilterState.from_path("foo*", page_size=500))
results = library.search_library(BrowsingState.from_path("foo*"), page_size=500)
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_glob_in_front(library: Library):
results = library.search_library(FilterState.from_path("*bar.md", page_size=500))
results = library.search_library(BrowsingState.from_path("*bar.md"), page_size=500)
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_glob_both_sides(library: Library):
results = library.search_library(FilterState.from_path("*one/two*", page_size=500))
results = library.search_library(BrowsingState.from_path("*one/two*"), page_size=500)
assert results.total_count == 1
assert len(results.items) == 1
# TODO: deduplicate this code with pytest parametrisation or a for loop
def test_path_search_ilike_glob_equality(library: Library):
results_ilike = library.search_library(FilterState.from_path("one/two", page_size=500))
results_glob = library.search_library(FilterState.from_path("*one/two*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("one/two"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*one/two*"), page_size=500)
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500))
results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500)
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar", page_size=500))
results_glob = library.search_library(FilterState.from_path("*bar*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("bar"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*bar*"), page_size=500)
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500))
results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500)
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
# TODO: isn't this the exact same as the one before?
def test_path_search_like_glob_equality(library: Library):
results_ilike = library.search_library(FilterState.from_path("ONE/two", page_size=500))
results_glob = library.search_library(FilterState.from_path("*ONE/two*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("ONE/two"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*ONE/two*"), page_size=500)
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500))
results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*BAR.MD*"), page_size=500)
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500))
results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500)
assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500))
results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500))
results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
results_glob = library.search_library(BrowsingState.from_path("*BAR.MD*"), page_size=500)
assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)])
def test_filetype_search(library: Library, filetype, num_of_filetype):
results = library.search_library(FilterState.from_filetype(filetype, page_size=500))
results = library.search_library(BrowsingState.from_filetype(filetype), page_size=500)
assert len(results.items) == num_of_filetype
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)])
def test_filetype_return_one_filetype(file_mediatypes_library: Library, filetype, num_of_filetype):
results = file_mediatypes_library.search_library(
FilterState.from_filetype(filetype, page_size=500)
BrowsingState.from_filetype(filetype), page_size=500
)
assert len(results.items) == num_of_filetype
@pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)])
def test_mediatype_search(library: Library, mediatype, num_of_mediatype):
results = library.search_library(FilterState.from_mediatype(mediatype, page_size=500))
results = library.search_library(BrowsingState.from_mediatype(mediatype), page_size=500)
assert len(results.items) == num_of_mediatype

View File

@@ -1,12 +1,12 @@
import pytest
from tagstudio.core.library.alchemy.enums import FilterState
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.query_lang.util import ParsingError
def verify_count(lib: Library, query: str, count: int):
results = lib.search_library(FilterState.from_search_query(query, page_size=500))
results = lib.search_library(BrowsingState.from_search_query(query), page_size=500)
assert results.total_count == count
assert len(results.items) == count
@@ -136,4 +136,4 @@ def test_parent_tags(search_library: Library, query: str, count: int):
)
def test_syntax(search_library: Library, invalid_query: str):
with pytest.raises(ParsingError) as e_info: # noqa: F841
search_library.search_library(FilterState.from_search_query(invalid_query, page_size=500))
search_library.search_library(BrowsingState.from_search_query(invalid_query), page_size=500)