From 4e663779f90172f95aa10018d02b27e4175df9fc Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Mon, 9 Sep 2024 07:54:01 +0700 Subject: [PATCH] rename LibraryField, add is_default property --- tagstudio/src/core/library/alchemy/fields.py | 21 ++++++---- tagstudio/src/core/library/alchemy/library.py | 36 ++++++++++------ tagstudio/src/core/library/alchemy/models.py | 41 +++++++++++++------ tagstudio/src/core/library/json/library.py | 5 ++- tagstudio/src/core/utils/refresh_dir.py | 1 + tagstudio/src/qt/widgets/preview_panel.py | 4 +- tagstudio/tests/conftest.py | 3 +- tagstudio/tests/macros/test_dupe_entries.py | 3 ++ tagstudio/tests/qt/test_driver.py | 6 ++- tagstudio/tests/test_library.py | 19 +++++++-- 10 files changed, 98 insertions(+), 41 deletions(-) diff --git a/tagstudio/src/core/library/alchemy/fields.py b/tagstudio/src/core/library/alchemy/fields.py index 168e5f7e8..1d2b9ced3 100644 --- a/tagstudio/src/core/library/alchemy/fields.py +++ b/tagstudio/src/core/library/alchemy/fields.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from typing import Any, TYPE_CHECKING @@ -11,7 +11,7 @@ from .enums import FieldTypeEnum if TYPE_CHECKING: - from .models import Entry, Tag, LibraryField + from .models import Entry, Tag, ValueType class BaseField(Base): @@ -26,7 +26,7 @@ def type_key(cls) -> Mapped[str]: return mapped_column(ForeignKey("library_fields.key")) @declared_attr - def type(cls) -> Mapped[LibraryField]: + def type(cls) -> Mapped[ValueType]: return relationship(foreign_keys=[cls.type_key], lazy=False) # type: ignore @declared_attr @@ -127,21 +127,28 @@ def __eq__(self, value) -> bool: class DefaultField: id: int name: str - type: Any # TextFieldTypes | TagBoxTypes | DateTimeTypes + type: FieldTypeEnum + is_default: bool = field(default=False) class _FieldID(Enum): """Only for bootstrapping content of DB table""" - TITLE = DefaultField(id=0, name="Title", type=FieldTypeEnum.TEXT_LINE) + TITLE = DefaultField( + id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True + ) AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE) ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE) URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE) DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_LINE) NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX) TAGS = DefaultField(id=6, name="Tags", type=FieldTypeEnum.TAGS) - TAGS_CONTENT = DefaultField(id=7, name="Content Tags", type=FieldTypeEnum.TAGS) - TAGS_META = DefaultField(id=8, name="Meta Tags", type=FieldTypeEnum.TAGS) + TAGS_CONTENT = DefaultField( + id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True + ) + TAGS_META = DefaultField( + id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True + ) COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE) DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME) DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME) diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index f262c82b5..7cf065694 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -38,7 +38,7 @@ BaseField, ) from .joins import TagSubtag, TagField -from .models import Entry, Preferences, Tag, TagAlias, LibraryField, Folder +from .models import Entry, Preferences, Tag, TagAlias, ValueType, Folder from ...constants import ( LibraryPrefs, TS_FOLDER_NAME, @@ -148,16 +148,17 @@ def open_library( for field in _FieldID: try: session.add( - LibraryField( + ValueType( + key=field.name, name=field.value.name, type=field.value.type, - order=field.value.id, - key=field.name, + position=field.value.id, + is_default=field.value.is_default, ) ) session.commit() except IntegrityError: - logger.debug("preference already exists", pref=pref) + logger.debug("ValueType already exists", field=field) session.rollback() # check if folder matching current path exists already @@ -178,6 +179,17 @@ def open_library( # load ignored extensions self.ignored_extensions = self.prefs(LibraryPrefs.EXTENSION_LIST) + @property + def default_fields(self) -> list[BaseField]: + with Session(self.engine) as session: + types = session.scalars( + select(ValueType).where( + # check if field is default + ValueType.is_default.is_(True) + ) + ) + return [x.as_field for x in types] + def delete_item(self, item): logger.info("deleting item", item=item) with Session(self.engine) as session: @@ -579,15 +591,13 @@ def update_entry_field( session.commit() @property - def field_types(self) -> dict[str, LibraryField]: + def field_types(self) -> dict[str, ValueType]: with Session(self.engine) as session: - return {x.key: x for x in session.scalars(select(LibraryField)).all()} + return {x.key: x for x in session.scalars(select(ValueType)).all()} - def get_library_field(self, field_key: str) -> LibraryField: + def get_value_type(self, field_key: str) -> ValueType: with Session(self.engine) as session: - field = session.scalar( - select(LibraryField).where(LibraryField.key == field_key) - ) + field = session.scalar(select(ValueType).where(ValueType.key == field_key)) session.expunge(field) return field @@ -595,7 +605,7 @@ def add_entry_field_type( self, entry_ids: list[int] | int, *, - field: LibraryField | None = None, + field: ValueType | None = None, field_id: _FieldID | str | None = None, value: str | datetime | list[str] | None = None, ) -> bool: @@ -615,7 +625,7 @@ def add_entry_field_type( if not field: if isinstance(field_id, _FieldID): field_id = field_id.name - field = self.get_library_field(field_id) + field = self.get_value_type(field_id) field_model: TextField | DatetimeField | TagBoxField if field.type in (FieldTypeEnum.TEXT_LINE, FieldTypeEnum.TEXT_BOX): diff --git a/tagstudio/src/core/library/alchemy/models.py b/tagstudio/src/core/library/alchemy/models.py index d9384df55..185b4bc89 100644 --- a/tagstudio/src/core/library/alchemy/models.py +++ b/tagstudio/src/core/library/alchemy/models.py @@ -13,6 +13,7 @@ FieldTypeEnum, _FieldID, BaseField, + BooleanField, ) from .joins import TagSubtag from ...constants import TAG_FAVORITE, TAG_ARCHIVED @@ -139,7 +140,7 @@ def fields(self) -> list[BaseField]: fields.extend(self.tag_box_fields) fields.extend(self.text_fields) fields.extend(self.datetime_fields) - fields = sorted(fields, key=lambda field: field.type.order) + fields = sorted(fields, key=lambda field: field.type.position) return fields @property @@ -171,18 +172,11 @@ def __init__( self, path: Path, folder: Folder, - fields: list[BaseField] | None = None, + fields: list[BaseField], ) -> None: self.path = path self.folder = folder - if fields is None: - fields = [ - TagBoxField(type_key=_FieldID.TAGS_META.name, position=0), - TagBoxField(type_key=_FieldID.TAGS_CONTENT.name, position=0), - TextField(type_key=_FieldID.TITLE.name, position=0), - ] - for field in fields: if isinstance(field, TextField): self.text_fields.append(field) @@ -210,13 +204,15 @@ def remove_tag(self, tag: Tag, field: TagBoxField | None = None) -> None: tag_box_field.tags.remove(tag) -class LibraryField(Base): +class ValueType(Base): """Define Field Types in the Library. Example: key: content_tags (this field is slugified `name`) name: Content Tags (this field is human readable name) kind: type of content (Text Line, Text Box, Tags, Datetime, Checkbox) + is_default: Should the field be present in new Entry? + order: position of the field widget in the Entry form """ @@ -225,7 +221,8 @@ class LibraryField(Base): key: Mapped[str] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(nullable=False) type: Mapped[FieldTypeEnum] = mapped_column(default=FieldTypeEnum.TEXT_LINE) - order: Mapped[int] + is_default: Mapped[bool] + position: Mapped[int] # add relations to other tables text_fields: Mapped[list[TextField]] = relationship( @@ -237,9 +234,27 @@ class LibraryField(Base): tag_box_fields: Mapped[list[TagBoxField]] = relationship( "TagBoxField", back_populates="type" ) + boolean_fields: Mapped[list[BooleanField]] = relationship( + "BooleanField", back_populates="type" + ) - -@event.listens_for(LibraryField, "before_insert") + @property + def as_field(self) -> BaseField: + FieldClass = { + FieldTypeEnum.TEXT_LINE: TextField, + FieldTypeEnum.TEXT_BOX: TextField, + FieldTypeEnum.TAGS: TagBoxField, + FieldTypeEnum.DATETIME: DatetimeField, + FieldTypeEnum.BOOLEAN: BooleanField, + } + + return FieldClass[self.type]( + type_key=self.key, + position=self.position, + ) + + +@event.listens_for(ValueType, "before_insert") def slugify_field_key(mapper, connection, target): """Slugify the field key before inserting into the database.""" if not target.key: diff --git a/tagstudio/src/core/library/json/library.py b/tagstudio/src/core/library/json/library.py index 78870d955..c95a2c791 100644 --- a/tagstudio/src/core/library/json/library.py +++ b/tagstudio/src/core/library/json/library.py @@ -1253,7 +1253,10 @@ def add_new_files_as_entries(self) -> list[int]: path = Path(file) # print(os.path.split(file)) entry = Entry( - id=self._next_entry_id, filename=path.name, path=path.parent, fields=[] + id=self._next_entry_id, + filename=path.name, + path=path.parent, + fields=[], ) self._next_entry_id += 1 self.add_entry_to_library(entry) diff --git a/tagstudio/src/core/utils/refresh_dir.py b/tagstudio/src/core/utils/refresh_dir.py index 5d56dfb41..b265f3953 100644 --- a/tagstudio/src/core/utils/refresh_dir.py +++ b/tagstudio/src/core/utils/refresh_dir.py @@ -28,6 +28,7 @@ def save_new_files(self) -> Iterator[int]: Entry( path=entry_path, folder=self.library.folder, + fields=self.library.default_fields, ) ] ) diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index ac2573881..386fa9ddc 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -676,7 +676,7 @@ def update_widgets(self) -> bool: mixed_fields.append(f) self.common_fields = common_fields - self.mixed_fields = sorted(mixed_fields, key=lambda x: x.type.order) + self.mixed_fields = sorted(mixed_fields, key=lambda x: x.type.position) self.selected = list(self.driver.selected) logger.info( @@ -946,7 +946,7 @@ def remove_field(self, field: BaseField): self.driver.update_badges(self.selected) def update_field(self, field: BaseField, content: str) -> None: - """Remove a field from all selected Entries, given a field object.""" + """Update a field in all selected Entries, given a field object.""" assert isinstance( field, (TextField, DatetimeField, TagBoxField) ), f"instance: {type(field)}" diff --git a/tagstudio/tests/conftest.py b/tagstudio/tests/conftest.py index 2822fe9fd..3a431b82c 100644 --- a/tagstudio/tests/conftest.py +++ b/tagstudio/tests/conftest.py @@ -56,6 +56,7 @@ def library(request): entry = Entry( folder=lib.folder, path=pathlib.Path("foo.txt"), + fields=lib.default_fields, ) entry.tag_box_fields = [ @@ -63,13 +64,13 @@ def library(request): TagBoxField( type_key=_FieldID.TAGS_META.name, position=0, - # tags={tag2} ), ] entry2 = Entry( folder=lib.folder, path=pathlib.Path("one/two/bar.md"), + fields=lib.default_fields, ) entry2.tag_box_fields = [ TagBoxField( diff --git a/tagstudio/tests/macros/test_dupe_entries.py b/tagstudio/tests/macros/test_dupe_entries.py index 9b10a5811..2272e1fcd 100644 --- a/tagstudio/tests/macros/test_dupe_entries.py +++ b/tagstudio/tests/macros/test_dupe_entries.py @@ -10,10 +10,13 @@ def test_refresh_dupe_files(library): entry = Entry( folder=library.folder, path=pathlib.Path("bar/foo.txt"), + fields=library.default_fields, ) + entry2 = Entry( folder=library.folder, path=pathlib.Path("foo/foo.txt"), + fields=library.default_fields, ) library.add_entries([entry, entry2]) diff --git a/tagstudio/tests/qt/test_driver.py b/tagstudio/tests/qt/test_driver.py index 715ad2245..7116ff6b4 100644 --- a/tagstudio/tests/qt/test_driver.py +++ b/tagstudio/tests/qt/test_driver.py @@ -9,7 +9,11 @@ def test_update_thumbs(qt_driver): qt_driver.frame_content = [ - Entry(folder=qt_driver.lib.folder, path=Path("/tmp/foo")) + Entry( + folder=qt_driver.lib.folder, + path=Path("/tmp/foo"), + fields=qt_driver.lib.default_fields, + ) ] qt_driver.item_thumbs = [] diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index 00ef8f968..4bd8bc641 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -27,7 +27,11 @@ def test_library_add_file(): lib = Library() lib.open_library(tmp_dir) - entry = Entry(path=file_path, folder=lib.folder) + entry = Entry( + path=file_path, + folder=lib.folder, + fields=lib.default_fields, + ) assert not lib.has_path_entry(entry.path) @@ -94,7 +98,10 @@ def test_get_entry(library, entry_min): def test_entries_count(library): - entries = [Entry(path=Path(f"{x}.txt"), folder=library.folder) for x in range(10)] + entries = [ + Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[]) + for x in range(10) + ] library.add_entries(entries) matches, page = library.search_library( FilterState( @@ -111,6 +118,7 @@ def test_add_field_to_entry(library): entry = Entry( folder=library.folder, path=Path("xxx"), + fields=library.default_fields, ) # meta tags + content tags assert len(entry.tag_box_fields) == 2 @@ -208,7 +216,12 @@ def test_preferences(library): def test_save_windows_path(library, generate_tag): # pretend we are on windows and create `Path` - entry = Entry(path=PureWindowsPath("foo\\bar.txt"), folder=library.folder) + + entry = Entry( + path=PureWindowsPath("foo\\bar.txt"), + folder=library.folder, + fields=library.default_fields, + ) tag = generate_tag("win_path") tag_name = tag.name