Skip to content
Open
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ packages = ["src/tagstudio"]
[tool.pytest.ini_options]
#addopts = "-m 'not qt'"
qt_api = "pyside6"
pythonpath = ["src"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was having some issues with pytest not recognizing new files, and adding this line fixed it.


[tool.pyright]
ignore = [
Expand Down
66 changes: 55 additions & 11 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -1251,32 +1251,33 @@ def search_tags(self, name: str | None, limit: int = 100) -> tuple[list[Tag], li
if limit <= 0:
limit = sys.maxsize

name = name or ""
name = name.lower()
search_query: str = name.lower() if name else ""

def sort_key(text: str):
priority = text.startswith(name)
priority = text.startswith(search_query)
p_ordering = len(text) if priority else sys.maxsize
return (not priority, p_ordering, text)
return not priority, p_ordering, text

with Session(self.engine) as session:
query = select(Tag.id, Tag.name)

if limit > 0 and not name:
if limit > 0 and not search_query:
query = query.order_by(Tag.name).limit(limit)

if name:
if search_query:
query = query.where(
or_(
Tag.name.icontains(name),
Tag.shorthand.icontains(name),
Tag.name.icontains(search_query),
Tag.shorthand.icontains(search_query),
)
)

tags = list(session.execute(query))

if name:
query = select(TagAlias.tag_id, TagAlias.name).where(TagAlias.name.icontains(name))
if search_query:
query = select(TagAlias.tag_id, TagAlias.name).where(
TagAlias.name.icontains(search_query)
)
tags.extend(session.execute(query))

tags.sort(key=lambda t: sort_key(t[1]))
Expand All @@ -1286,7 +1287,7 @@ def sort_key(text: str):

logger.info(
"searching tags",
search=name,
search=search_query,
limit=limit,
statement=str(query),
results=len(tag_ids),
Expand All @@ -1312,6 +1313,49 @@ def sort_key(text: str):

return direct_tags, descendant_tags

def search_field_templates(self, name: str | None, limit: int = 100) -> list[BaseFieldTemplate]:
"""Return field template rows matching the query, detached from the session."""
if limit <= 0:
limit = sys.maxsize

search_query: str = name.lower() if name else ""

def sort_key(template: BaseFieldTemplate) -> tuple:
text = template.name.lower()
if not search_query:
return (text,)
priority = text.startswith(search_query)
p_ordering = len(text) if priority else sys.maxsize
return (not priority, p_ordering, text)

with Session(self.engine) as session:
text_stmt = select(TextFieldTemplate)
datetime_stmt = select(DatetimeFieldTemplate)
if search_query:
text_stmt = text_stmt.where(TextFieldTemplate.name.icontains(search_query))
datetime_stmt = datetime_stmt.where(
DatetimeFieldTemplate.name.icontains(search_query)
)

field_templates: list[BaseFieldTemplate] = [
*session.scalars(text_stmt),
*session.scalars(datetime_stmt),
]
field_templates.sort(key=sort_key)
field_templates = field_templates[:limit]

for ft in field_templates:
session.expunge(ft)
make_transient(ft)

logger.info(
"Searching field templates",
search=search_query,
limit=limit,
results=len(field_templates),
)
return field_templates

def update_entry_path(self, entry_id: int | Entry, path: Path) -> bool:
"""Set the path field of an entry.

Expand Down
114 changes: 114 additions & 0 deletions src/tagstudio/qt/controllers/field_template_search_panel_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only


from warnings import catch_warnings

import structlog
from PySide6.QtCore import Signal

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget

logger = structlog.get_logger(__name__)


class FieldTemplateSearchModal(PanelModal):
def __init__(
self,
library: Library,
is_field_template_chooser: bool = True,
done_callback=None,
save_callback=None,
has_save=False,
) -> None:
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
library, is_field_template_chooser
)
super().__init__(
self.search_panel,
Translations["field.add.plural"],
done_callback=done_callback,
save_callback=save_callback,
has_save=has_save,
)


class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate], FieldTemplateSearchPanelView):
field_template_chosen = Signal(object)

def __init__(
self,
library: Library,
is_field_template_chooser: bool = True,
) -> None:
super().__init__([], is_field_template_chooser)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend explicitly calling the base classes' __init__() methods rather than using super() when using multiple inheritance for the reasons described here

Suggested change
super().__init__([], is_field_template_chooser)
SearchPanel.__init__(self, [], is_field_template_chooser)
FieldTemplateSearchPanelView.__init__(self, is_field_template_chooser)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly calling the base classes' __init__() results in SearchPanelView being initialized twice and triggering a crash

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang, I must've forgotten about that when I was originally reviewing this a few days ago. I think the basedpyright error led me to this and then that crash led me to look into the init calls further and discover the issues with the inheritance tree.

Disregard these recommendations then, but the underlying issue with the super() call and inheritance tree is still present.

self.__lib = library

self._unlimited_limit_item_label = Translations["field_template.all_field_templates"]
self._create_and_add_button_label_key = "field_template.create_add"

def _get_max_limit(self) -> int:
return len(self.__lib.field_templates)

def _on_item_create(self) -> None:
# TODO: Allow creation of field templates
pass

def on_item_edit(self, item: BaseFieldTemplate) -> None:
# TODO: Allow creation of field templates
pass

def _on_item_remove(self, item: BaseFieldTemplate) -> None:
if self.is_chooser:
return

# TODO: Allow creation of field templates
pass

def _on_item_create_and_add(self) -> None:
# TODO: Allow creation of field templates
pass

def _on_item_chosen(self, item: BaseFieldTemplate) -> None:
self.field_template_chosen.emit(item)

def search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseFieldTemplate]]:
return self.__lib.search_field_templates(name=query, limit=self._get_limit()[1]), []

def set_item_widget(self, item: BaseFieldTemplate | None, index: int) -> None:
"""Set the field template of a field template widget at a specific index."""
field_template_widget: FieldTemplateWidget = self.get_item_widget(index, self.__lib)
field_template_widget.set_field_template(item)
field_template_widget.setHidden(item is None)

if item is None:
return

# field_template_widget.has_remove = not self.is_chooser

# Disconnect previous callbacks
with catch_warnings(record=True):
# tag_widget.on_edit.disconnect()
# tag_widget.on_remove.disconnect()
field_template_widget.on_click.disconnect()

# Connect callbacks
# tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
# tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
field_template_widget.on_click.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)

def create_item(self, build_item_modal: PanelModal, choose_item: bool = False) -> None:
# TODO: Allow creation of field templates
pass

def edit_item(self, edit_item_panel: PanelWidget) -> None:
# TODO: Allow creation of field templates
pass
22 changes: 22 additions & 0 deletions src/tagstudio/qt/controllers/field_template_widget_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.views.field_template_widget_view import FieldTemplateWidgetView


class FieldTemplateWidget(FieldTemplateWidgetView):
def __init__(self) -> None:
super().__init__()

self.__field_template: BaseFieldTemplate | None = None

def set_field_template(self, field_template: BaseFieldTemplate | None) -> None:
self.__field_template = field_template

if field_template is None:
return

field_name_key: str = FIELD_TYPE_KEYS.get(field_template.class_name, "field_type.unknown")
self._bg_button.setText(f"{field_template.name} ({Translations[field_name_key]})")
33 changes: 17 additions & 16 deletions src/tagstudio/qt/controllers/preview_panel_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,48 @@
import typing
from warnings import catch_warnings

from PySide6.QtWidgets import QListWidgetItem

from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.mixed.add_field import AddFieldModal
from tagstudio.qt.mixed.tag_search import TagSearchModal
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.views.preview_panel_view import PreviewPanelView

if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver


class PreviewPanel(PreviewPanelView):
def __init__(self, library: Library, driver: "QtDriver"):
def __init__(self, library: Library, driver: "QtDriver") -> None:
super().__init__(library, driver)

self.__add_field_modal = AddFieldModal(self.lib)
self.__add_field_modal = FieldTemplateSearchModal(self.lib, is_field_template_chooser=True)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)

@typing.override
def _add_field_button_callback(self):
def _add_field_button_callback(self) -> None:
self.__add_field_modal.show()

@typing.override
def _add_tag_button_callback(self):
def _add_tag_button_callback(self) -> None:
self.__add_tag_modal.show()

@typing.override
def _set_selection_callback(self):
def _set_selection_callback(self) -> None:
with catch_warnings(record=True):
self.__add_field_modal.done.disconnect()
self.__add_tag_modal.tsp.tag_chosen.disconnect()
self.__add_field_modal.search_panel.field_template_chosen.disconnect()
self.__add_tag_modal.tsp.item_chosen.disconnect()

self.__add_field_modal.done.connect(self._add_field_to_selected)
self.__add_tag_modal.tsp.tag_chosen.connect(self._add_tag_to_selected)
self.__add_field_modal.search_panel.field_template_chosen.connect(
self._add_field_to_selected
)
self.__add_tag_modal.tsp.item_chosen.connect(self._add_tag_to_selected)

def _add_field_to_selected(self, field_list: list[QListWidgetItem]):
self._fields.add_field_to_selected(field_list)
def _add_field_to_selected(self, template: BaseFieldTemplate) -> None:
self._fields.add_field_to_selected(template)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])

def _add_tag_to_selected(self, tag_id: int):
def _add_tag_to_selected(self, tag_id: int) -> None:
self._fields.add_tags_to_selected(tag_id)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])
Loading
Loading