From 6461eebb4861c014b9606430f5d3b232cb406ab0 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 5 Jan 2025 19:12:18 -0800 Subject: [PATCH] refactor!: eradicate use of the term "subtag" - Removes ambiguity between the use of the term "parent tag" and "subtag" - Fixes inconstancies between the use of the term "subtag" to refer to either parent tags or child tags - Fixes duplicate and ambiguous subtags mapped relationship for the Tag model - Does NOT fix tests --- tagstudio/src/core/library/alchemy/joins.py | 4 +- tagstudio/src/core/library/alchemy/library.py | 139 +++++++++--------- tagstudio/src/core/library/alchemy/models.py | 23 +-- .../src/core/library/alchemy/visitors.py | 13 +- tagstudio/src/core/ts_core.py | 2 +- tagstudio/src/qt/modals/build_tag.py | 76 +++++----- tagstudio/src/qt/modals/folders_to_tags.py | 6 +- tagstudio/src/qt/modals/tag_database.py | 4 +- tagstudio/src/qt/ts_qt.py | 6 +- tagstudio/src/qt/widgets/migration_modal.py | 48 +++--- .../qt/widgets/preview/field_containers.py | 8 +- tagstudio/src/qt/widgets/tag_box.py | 4 +- tagstudio/tests/qt/test_build_tag_panel.py | 14 +- tagstudio/tests/test_library.py | 33 ++--- 14 files changed, 178 insertions(+), 202 deletions(-) diff --git a/tagstudio/src/core/library/alchemy/joins.py b/tagstudio/src/core/library/alchemy/joins.py index 998fbdce..b3c97e35 100644 --- a/tagstudio/src/core/library/alchemy/joins.py +++ b/tagstudio/src/core/library/alchemy/joins.py @@ -8,8 +8,8 @@ from sqlalchemy.orm import Mapped, mapped_column from .db import Base -class TagSubtag(Base): - __tablename__ = "tag_subtags" +class TagParent(Base): + __tablename__ = "tag_parents" parent_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True) child_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True) diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index d3f2e03e..26960669 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -54,7 +54,7 @@ from .fields import ( TextField, _FieldID, ) -from .joins import TagEntry, TagSubtag +from .joins import TagEntry, TagParent from .models import Entry, Folder, Preferences, Tag, TagAlias, ValueType from .visitors import SQLBoolExpressionBuilder @@ -85,7 +85,7 @@ def get_default_tags() -> tuple[Tag, ...]: id=TAG_ARCHIVED, name="Archived", aliases={TagAlias(name="Archive")}, - subtags={meta_tag}, + parent_tags={meta_tag}, color=TagColor.RED, ) favorite_tag = Tag( @@ -95,7 +95,7 @@ def get_default_tags() -> tuple[Tag, ...]: TagAlias(name="Favorited"), TagAlias(name="Favorites"), }, - subtags={meta_tag}, + parent_tags={meta_tag}, color=TagColor.YELLOW, ) @@ -192,10 +192,10 @@ class Library: break self.add_alias(name=alias, tag_id=tag.id) - # Tag Subtags + # Parent Tags (Previously known as "Subtags" in JSON) for tag in json_lib.tags: - for subtag_id in tag.subtag_ids: - self.add_subtag(parent_id=tag.id, child_id=subtag_id) + for child_id in tag.subtag_ids: + self.add_parent_tag(parent_id=tag.id, child_id=child_id) # Entries self.add_entries( @@ -216,8 +216,8 @@ class Library: if k in {6, 7, 8}: self.add_tags_to_entry(entry_id=entry.id + 1, tag_ids=v) else: - self.add_entry_field_type( - entry_ids=(entry.id + 1), # JSON IDs start at 0 instead of 1 + self.add_field_to_entry( + entry_id=(entry.id + 1), # JSON IDs start at 0 instead of 1 field_id=self.get_field_name_from_id(k), value=v, ) @@ -405,7 +405,7 @@ class Library: .options( selectinload(Entry.tags).options( joinedload(Tag.aliases), - joinedload(Tag.subtags), + joinedload(Tag.parent_tags), ) ) ) @@ -430,7 +430,7 @@ class Library: selectinload(Entry.datetime_fields), selectinload(Entry.tags).options( selectinload(Tag.aliases), - selectinload(Tag.subtags), + selectinload(Tag.parent_tags), ), ) statement = statement.distinct() @@ -476,8 +476,8 @@ class Library: @property def tags(self) -> list[Tag]: with Session(self.engine) as session: - # load all tags and join subtags - tags_query = select(Tag).options(selectinload(Tag.subtags)) + # load all tags and join parent tags + tags_query = select(Tag).options(selectinload(Tag.parent_tags)) tags = session.scalars(tags_query).unique() tags_list = list(tags) @@ -577,7 +577,7 @@ class Library: selectinload(Entry.text_fields), selectinload(Entry.datetime_fields), selectinload(Entry.tags).options( - selectinload(Tag.aliases), selectinload(Tag.subtags) + selectinload(Tag.aliases), selectinload(Tag.parent_tags) ), ) @@ -613,7 +613,7 @@ class Library: with Session(self.engine) as session: query = select(Tag) query = query.options( - selectinload(Tag.subtags), + selectinload(Tag.parent_tags), selectinload(Tag.aliases), ).limit(tag_limit) @@ -641,22 +641,22 @@ class Library: return res def get_all_child_tag_ids(self, tag_id: int) -> list[int]: - """Recursively traverse a Tag's subtags and return a list of all children tags.""" - all_subtags: set[int] = {tag_id} + """Recursively traverse a Tag's parent tags and return a list of all children tags.""" + all_parent_ids: set[int] = {tag_id} with Session(self.engine) as session: - tag = session.scalar(select(Tag).where(Tag.id == tag_id)) + tag: Tag | None = session.scalar(select(Tag).where(Tag.id == tag_id)) if tag is None: raise ValueError(f"No tag found with id {tag_id}.") - subtag_ids = tag.subtag_ids + parent_ids = tag.parent_ids - all_subtags.update(subtag_ids) + all_parent_ids.update(parent_ids) - for sub_id in subtag_ids: - all_subtags.update(self.get_all_child_tag_ids(sub_id)) + for child_id in parent_ids: + all_parent_ids.update(self.get_all_child_tag_ids(child_id)) - return list(all_subtags) + return list(all_parent_ids) def update_entry_path(self, entry_id: int | Entry, path: Path) -> None: if isinstance(entry_id, Entry): @@ -679,11 +679,11 @@ class Library: def remove_tag(self, tag: Tag): with Session(self.engine, expire_on_commit=False) as session: try: - subtags = session.scalars( - select(TagSubtag).where(TagSubtag.parent_id == tag.id) + parent_tags = session.scalars( + select(TagParent).where(TagParent.parent_id == tag.id) ).all() tags_query = select(Tag).options( - selectinload(Tag.subtags), selectinload(Tag.aliases) + selectinload(Tag.parent_tags), selectinload(Tag.aliases) ) tag = session.scalar(tags_query.where(Tag.id == tag.id)) aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)) @@ -691,9 +691,9 @@ class Library: for alias in aliases or []: session.delete(alias) - for subtag in subtags or []: - session.delete(subtag) - session.expunge(subtag) + for parent_tag in parent_tags or []: + session.delete(parent_tag) + session.expunge(parent_tag) session.delete(tag) session.commit() @@ -810,17 +810,17 @@ class Library: session.expunge(field) return field - def add_entry_field_type( + def add_field_to_entry( self, - entry_ids: list[int] | int, + entry_id: int, *, field: ValueType | None = None, field_id: _FieldID | str | None = None, - value: str | datetime | list[int] | None = None, + value: str | datetime | None = None, ) -> bool: logger.info( "add_field_to_entry", - entry_ids=entry_ids, + entry_id=entry_id, field_type=field, field_id=field_id, value=value, @@ -828,9 +828,6 @@ class Library: # supply only instance or ID, not both assert bool(field) != (field_id is not None) - if isinstance(entry_ids, int): - entry_ids = [entry_ids] - if not field: if isinstance(field_id, _FieldID): field_id = field_id.name @@ -853,11 +850,9 @@ class Library: with Session(self.engine) as session: try: - for entry_id in entry_ids: - field_model.entry_id = entry_id - session.add(field_model) - session.flush() - + field_model.entry_id = entry_id + session.add(field_model) + session.flush() session.commit() except IntegrityError as e: logger.exception(e) @@ -869,7 +864,7 @@ class Library: self.update_field_position( field_class=type(field_model), field_type=field.key, - entry_ids=entry_ids, + entry_ids=entry_id, ) return True @@ -898,7 +893,7 @@ class Library: def add_tag( self, tag: Tag, - subtag_ids: list[int] | set[int] | None = None, + parent_ids: list[int] | set[int] | None = None, alias_names: list[str] | set[str] | None = None, alias_ids: list[int] | set[int] | None = None, ) -> Tag | None: @@ -907,8 +902,8 @@ class Library: session.add(tag) session.flush() - if subtag_ids is not None: - self.update_subtags(tag, subtag_ids, session) + if parent_ids is not None: + self.update_parent_tags(tag, parent_ids, session) if alias_ids is not None and alias_names is not None: self.update_aliases(tag, alias_ids, alias_names, session) @@ -978,12 +973,14 @@ class Library: def get_tag(self, tag_id: int) -> Tag: with Session(self.engine) as session: - tags_query = select(Tag).options(selectinload(Tag.subtags), selectinload(Tag.aliases)) + tags_query = select(Tag).options( + selectinload(Tag.parent_tags), selectinload(Tag.aliases) + ) tag = session.scalar(tags_query.where(Tag.id == tag_id)) session.expunge(tag) - for subtag in tag.subtags: - session.expunge(subtag) + for parent in tag.parent_tags: + session.expunge(parent) for alias in tag.aliases: session.expunge(alias) @@ -1006,19 +1003,19 @@ class Library: return alias - def add_subtag(self, parent_id: int, child_id: int) -> bool: + def add_parent_tag(self, parent_id: int, child_id: int) -> bool: if parent_id == child_id: return False # open session and save as parent tag with Session(self.engine) as session: - subtag = TagSubtag( + parent_tag = TagParent( parent_id=parent_id, child_id=child_id, ) try: - session.add(subtag) + session.add(parent_tag) session.commit() return True except IntegrityError: @@ -1045,11 +1042,11 @@ class Library: logger.exception("IntegrityError") return False - def remove_subtag(self, base_id: int, remove_tag_id: int) -> bool: + def remove_parent_tag(self, base_id: int, remove_tag_id: int) -> bool: with Session(self.engine) as session: p_id = base_id r_id = remove_tag_id - remove = session.query(TagSubtag).filter_by(parent_id=p_id, child_id=r_id).one() + remove = session.query(TagParent).filter_by(parent_id=p_id, child_id=r_id).one() session.delete(remove) session.commit() @@ -1058,12 +1055,12 @@ class Library: def update_tag( self, tag: Tag, - subtag_ids: list[int] | set[int] | None = None, + parent_ids: list[int] | set[int] | None = None, alias_names: list[str] | set[str] | None = None, alias_ids: list[int] | set[int] | None = None, ) -> None: """Edit a Tag in the Library.""" - self.add_tag(tag, subtag_ids, alias_names, alias_ids) + self.add_tag(tag, parent_ids, alias_names, alias_ids) def update_aliases(self, tag, alias_ids, alias_names, session): prev_aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)).all() @@ -1079,28 +1076,30 @@ class Library: alias = TagAlias(alias_name, tag.id) session.add(alias) - def update_subtags(self, tag, subtag_ids, session): - if tag.id in subtag_ids: - subtag_ids.remove(tag.id) + def update_parent_tags(self, tag, parent_ids, session): + if tag.id in parent_ids: + parent_ids.remove(tag.id) - # load all tag's subtag to know which to remove - prev_subtags = session.scalars(select(TagSubtag).where(TagSubtag.parent_id == tag.id)).all() + # load all tag's parent tags to know which to remove + prev_parent_tags = session.scalars( + select(TagParent).where(TagParent.parent_id == tag.id) + ).all() - for subtag in prev_subtags: - if subtag.child_id not in subtag_ids: - session.delete(subtag) + for parent_tag in prev_parent_tags: + if parent_tag.child_id not in parent_ids: + session.delete(parent_tag) else: # no change, remove from list - subtag_ids.remove(subtag.child_id) + parent_ids.remove(parent_tag.child_id) # create remaining items - for subtag_id in subtag_ids: - # add new subtag - subtag = TagSubtag( + for parent_id in parent_ids: + # add new parent tag + parent_tag = TagParent( parent_id=tag.id, - child_id=subtag_id, + child_id=parent_id, ) - session.add(subtag) + session.add(parent_tag) def prefs(self, key: LibraryPrefs): # load given item from Preferences table @@ -1130,8 +1129,8 @@ class Library: for entry in entries: for field_key, field in fields.items(): if field_key not in existing_fields: - self.add_entry_field_type( - entry_ids=entry.id, + self.add_field_to_entry( + entry_id=entry.id, field_id=field.type_key, value=field.value, ) diff --git a/tagstudio/src/core/library/alchemy/models.py b/tagstudio/src/core/library/alchemy/models.py index d59adc9c..2765e880 100644 --- a/tagstudio/src/core/library/alchemy/models.py +++ b/tagstudio/src/core/library/alchemy/models.py @@ -17,16 +17,14 @@ from .fields import ( FieldTypeEnum, TextField, ) -from .joins import TagSubtag +from .joins import TagParent class TagAlias(Base): __tablename__ = "tag_aliases" id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(nullable=False) - tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id")) tag: Mapped["Tag"] = relationship(back_populates="aliases") @@ -54,22 +52,15 @@ class Tag(Base): aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag") parent_tags: Mapped[set["Tag"]] = relationship( - secondary=TagSubtag.__tablename__, - primaryjoin="Tag.id == TagSubtag.child_id", - secondaryjoin="Tag.id == TagSubtag.parent_id", - back_populates="subtags", - ) - - subtags: Mapped[set["Tag"]] = relationship( - secondary=TagSubtag.__tablename__, - primaryjoin="Tag.id == TagSubtag.parent_id", - secondaryjoin="Tag.id == TagSubtag.child_id", + secondary=TagParent.__tablename__, + primaryjoin="Tag.id == TagParent.parent_id", + secondaryjoin="Tag.id == TagParent.child_id", back_populates="parent_tags", ) @property - def subtag_ids(self) -> list[int]: - return [tag.id for tag in self.subtags] + def parent_ids(self) -> list[int]: + return [tag.id for tag in self.parent_tags] @property def alias_strings(self) -> list[str]: @@ -86,7 +77,6 @@ class Tag(Base): shorthand: str | None = None, aliases: set[TagAlias] | None = None, parent_tags: set["Tag"] | None = None, - subtags: set["Tag"] | None = None, icon: str | None = None, color: TagColor = TagColor.DEFAULT, is_category: bool = False, @@ -94,7 +84,6 @@ class Tag(Base): self.name = name self.aliases = aliases or set() self.parent_tags = parent_tags or set() - self.subtags = subtags or set() self.color = color self.icon = icon self.shorthand = shorthand diff --git a/tagstudio/src/core/library/alchemy/visitors.py b/tagstudio/src/core/library/alchemy/visitors.py index f593abfa..9ce07fdb 100644 --- a/tagstudio/src/core/library/alchemy/visitors.py +++ b/tagstudio/src/core/library/alchemy/visitors.py @@ -23,16 +23,17 @@ else: logger = structlog.get_logger(__name__) +# TODO: Reevaluate after subtags -> parent tags name change CHILDREN_QUERY = text(""" --- Note for this entire query that tag_subtags.child_id is the parent id and tag_subtags.parent_id is the child id due to bad naming -WITH RECURSIVE Subtags AS ( +-- Note for this entire query that tag_parents.child_id is the parent id and tag_parents.parent_id is the child id due to bad naming +WITH RECURSIVE ChildTags AS ( SELECT :tag_id AS child_id UNION ALL - SELECT ts.parent_id AS child_id - FROM tag_subtags ts - INNER JOIN Subtags s ON ts.child_id = s.child_id + SELECT tp.parent_id AS child_id + FROM tag_parents tp + INNER JOIN ChildTags c ON tp.child_id = c.child_id ) -SELECT * FROM Subtags; +SELECT * FROM ChildTags; """) # noqa: E501 diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index aaae942e..3d412d9e 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -126,7 +126,7 @@ class TagStudioCore: is_new = field["id"] not in entry_field_types field_key = field["id"] if is_new: - lib.add_entry_field_type(entry.id, field_key, field["value"]) + lib.add_field_to_entry(entry.id, field_key, field["value"]) else: lib.update_entry_field(entry.id, field_key, field["value"]) diff --git a/tagstudio/src/qt/modals/build_tag.py b/tagstudio/src/qt/modals/build_tag.py index 82b96d4c..236fc488 100644 --- a/tagstudio/src/qt/modals/build_tag.py +++ b/tagstudio/src/qt/modals/build_tag.py @@ -118,21 +118,21 @@ class BuildTagPanel(PanelWidget): self.alias_add_button.clicked.connect(self.add_alias_callback) # Parent Tags ---------------------------------------------------------- - self.subtags_widget = QWidget() - self.subtags_layout = QVBoxLayout(self.subtags_widget) - self.subtags_layout.setStretch(1, 1) - self.subtags_layout.setContentsMargins(0, 0, 0, 0) - self.subtags_layout.setSpacing(0) - self.subtags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.parent_tags_widget = QWidget() + self.parent_tags_layout = QVBoxLayout(self.parent_tags_widget) + self.parent_tags_layout.setStretch(1, 1) + self.parent_tags_layout.setContentsMargins(0, 0, 0, 0) + self.parent_tags_layout.setSpacing(0) + self.parent_tags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.subtags_title = QLabel() - Translations.translate_qobject(self.subtags_title, "tag.parent_tags") - self.subtags_layout.addWidget(self.subtags_title) + self.parent_tags_title = QLabel() + Translations.translate_qobject(self.parent_tags_title, "tag.parent_tags") + self.parent_tags_layout.addWidget(self.parent_tags_title) self.scroll_contents = QWidget() - self.subtags_scroll_layout = QVBoxLayout(self.scroll_contents) - self.subtags_scroll_layout.setContentsMargins(6, 0, 6, 0) - self.subtags_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents) + self.parent_tags_scroll_layout.setContentsMargins(6, 0, 6, 0) + self.parent_tags_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.scroll_area = QScrollArea() # self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) @@ -142,23 +142,23 @@ class BuildTagPanel(PanelWidget): self.scroll_area.setWidget(self.scroll_contents) # self.scroll_area.setMinimumHeight(60) - self.subtags_layout.addWidget(self.scroll_area) + self.parent_tags_layout.addWidget(self.scroll_area) - self.subtags_add_button = QPushButton() - self.subtags_add_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.subtags_add_button.setText("+") - self.subtags_layout.addWidget(self.subtags_add_button) + self.parent_tags_add_button = QPushButton() + self.parent_tags_add_button.setCursor(Qt.CursorShape.PointingHandCursor) + self.parent_tags_add_button.setText("+") + self.parent_tags_layout.addWidget(self.parent_tags_add_button) exclude_ids: list[int] = list() if tag is not None: exclude_ids.append(tag.id) tsp = TagSearchPanel(self.lib, exclude_ids) - tsp.tag_chosen.connect(lambda x: self.add_subtag_callback(x)) + tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x)) self.add_tag_modal = PanelModal(tsp) Translations.translate_with_setter(self.add_tag_modal.setTitle, "tag.parent_tags.add") Translations.translate_with_setter(self.add_tag_modal.setWindowTitle, "tag.parent_tags.add") - self.subtags_add_button.clicked.connect(self.add_tag_modal.show) + self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show) # Color ---------------------------------------------------------------- self.color_widget = QWidget() @@ -230,13 +230,13 @@ class BuildTagPanel(PanelWidget): self.root_layout.addWidget(self.aliases_widget) self.root_layout.addWidget(self.aliases_table) self.root_layout.addWidget(self.alias_add_button) - self.root_layout.addWidget(self.subtags_widget) + self.root_layout.addWidget(self.parent_tags_widget) self.root_layout.addWidget(self.color_widget) self.root_layout.addWidget(QLabel("

Properties

")) self.root_layout.addWidget(self.cat_widget) # self.parent().done.connect(self.update_tag) - self.subtag_ids: set[int] = set() + self.parent_ids: set[int] = set() self.alias_ids: list[int] = [] self.alias_names: list[str] = [] self.new_alias_names: dict = {} @@ -276,15 +276,15 @@ class BuildTagPanel(PanelWidget): if isinstance(focused_widget, CustomTableItem): self.add_alias_callback() - def add_subtag_callback(self, tag_id: int): - logger.info("add_subtag_callback", tag_id=tag_id) - self.subtag_ids.add(tag_id) - self.set_subtags() + def add_parent_tag_callback(self, tag_id: int): + logger.info("add_parent_tag_callback", tag_id=tag_id) + self.parent_ids.add(tag_id) + self.set_parent_tags() - def remove_subtag_callback(self, tag_id: int): - logger.info("removing subtag", tag_id=tag_id) - self.subtag_ids.remove(tag_id) - self.set_subtags() + def remove_parent_tag_callback(self, tag_id: int): + logger.info("remove_parent_tag_callback", tag_id=tag_id) + self.parent_ids.remove(tag_id) + self.set_parent_tags() def add_alias_callback(self): logger.info("add_alias_callback") @@ -305,20 +305,20 @@ class BuildTagPanel(PanelWidget): self.alias_ids.remove(alias_id) self._set_aliases() - def set_subtags(self): - while self.subtags_scroll_layout.itemAt(0): - self.subtags_scroll_layout.takeAt(0).widget().deleteLater() + def set_parent_tags(self): + while self.parent_tags_scroll_layout.itemAt(0): + self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater() c = QWidget() layout = QVBoxLayout(c) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(3) - for tag_id in self.subtag_ids: + for tag_id in self.parent_ids: tag = self.lib.get_tag(tag_id) tw = TagWidget(tag, has_edit=False, has_remove=True) - tw.on_remove.connect(lambda t=tag_id: self.remove_subtag_callback(t)) + tw.on_remove.connect(lambda t=tag_id: self.remove_parent_tag_callback(t)) layout.addWidget(tw) - self.subtags_scroll_layout.addWidget(c) + self.parent_tags_scroll_layout.addWidget(c) def add_aliases(self): names: set[str] = set() @@ -392,9 +392,9 @@ class BuildTagPanel(PanelWidget): self.alias_ids.append(alias_id) self._set_aliases() - for subtag in tag.subtag_ids: - self.subtag_ids.add(subtag) - self.set_subtags() + for parent_id in tag.parent_ids: + self.parent_ids.add(parent_id) + self.set_parent_tags() # select item in self.color_field where the userData value matched tag.color for i in range(self.color_field.count()): diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py index 1af62e43..47b7a9b8 100644 --- a/tagstudio/src/qt/modals/folders_to_tags.py +++ b/tagstudio/src/qt/modals/folders_to_tags.py @@ -41,7 +41,7 @@ def add_folders_to_tree(library: Library, tree: BranchData, items: tuple[str, .. branch = tree for folder in items: if folder not in branch.dirs: - # TODO - subtags + # TODO: Reimplement parent tags new_tag = Tag(name=folder) library.add_tag(new_tag) branch.dirs[folder] = BranchData(tag=new_tag) @@ -81,11 +81,11 @@ def reverse_tag(library: Library, tag: Tag, items: list[Tag] | None) -> list[Tag items = items or [] items.append(tag) - if not tag.subtag_ids: + if not tag.parent_ids: items.reverse() return items - for subtag_id in tag.subtag_ids: + for subtag_id in tag.parent_ids: subtag = library.get_tag(subtag_id) return reverse_tag(library, subtag, items) diff --git a/tagstudio/src/qt/modals/tag_database.py b/tagstudio/src/qt/modals/tag_database.py index c2cdc8f0..ac022156 100644 --- a/tagstudio/src/qt/modals/tag_database.py +++ b/tagstudio/src/qt/modals/tag_database.py @@ -87,7 +87,7 @@ class TagDatabasePanel(PanelWidget): lambda: ( self.lib.add_tag( tag=panel.build_tag(), - subtag_ids=panel.subtag_ids, + parent_ids=panel.parent_ids, alias_names=panel.alias_names, alias_ids=panel.alias_ids, ), @@ -169,7 +169,7 @@ class TagDatabasePanel(PanelWidget): def edit_tag_callback(self, btp: BuildTagPanel): self.lib.update_tag( - btp.build_tag(), set(btp.subtag_ids), set(btp.alias_names), set(btp.alias_ids) + btp.build_tag(), set(btp.parent_ids), set(btp.alias_names), set(btp.alias_ids) ) self.update_tags(self.search_field.text()) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 0847ee0a..0bb62a7a 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -682,7 +682,7 @@ class QtDriver(DriverMixin, QObject): lambda: ( self.lib.add_tag( panel.build_tag(), - set(panel.subtag_ids), + set(panel.parent_ids), set(panel.alias_names), set(panel.alias_ids), ), @@ -858,7 +858,7 @@ class QtDriver(DriverMixin, QObject): for field_id, value in parsed_items.items(): if isinstance(value, list) and len(value) > 0 and isinstance(value[0], str): value = self.lib.tag_from_strings(value) - self.lib.add_entry_field_type( + self.lib.add_field_to_entry( entry.id, field_id=field_id, value=value, @@ -867,7 +867,7 @@ class QtDriver(DriverMixin, QObject): elif name == MacroID.BUILD_URL: url = TagStudioCore.build_url(entry, source) if url is not None: - self.lib.add_entry_field_type(entry.id, field_id=_FieldID.SOURCE, value=url) + self.lib.add_field_to_entry(entry.id, field_id=_FieldID.SOURCE, value=url) elif name == MacroID.MATCH: TagStudioCore.match_conditions(self.lib, entry.id) elif name == MacroID.CLEAN_URL: diff --git a/tagstudio/src/qt/widgets/migration_modal.py b/tagstudio/src/qt/widgets/migration_modal.py index dcc11a95..4f8b2c79 100644 --- a/tagstudio/src/qt/widgets/migration_modal.py +++ b/tagstudio/src/qt/widgets/migration_modal.py @@ -22,7 +22,7 @@ from sqlalchemy.orm import Session from src.core.constants import TS_FOLDER_NAME from src.core.enums import LibraryPrefs from src.core.library.alchemy.enums import TagColor -from src.core.library.alchemy.joins import TagSubtag +from src.core.library.alchemy.joins import TagParent from src.core.library.alchemy.library import DEFAULT_TAG_DIFF from src.core.library.alchemy.library import Library as SqliteLibrary from src.core.library.alchemy.models import Entry, TagAlias @@ -115,7 +115,7 @@ class JsonMigrationModal(QObject): entries_text: str = Translations["json_migration.heading.entires"] tags_text: str = Translations["json_migration.heading.tags"] shorthand_text: str = tab + Translations["json_migration.heading.shorthands"] - subtags_text: str = tab + Translations["json_migration.heading.parent_tags"] + parent_tags_text: str = tab + Translations["json_migration.heading.parent_tags"] aliases_text: str = tab + Translations["json_migration.heading.aliases"] colors_text: str = tab + Translations["json_migration.heading.colors"] ext_text: str = Translations["json_migration.heading.file_extension_list"] @@ -129,7 +129,7 @@ class JsonMigrationModal(QObject): self.fields_row: int = 2 self.tags_row: int = 3 self.shorthands_row: int = 4 - self.subtags_row: int = 5 + self.parent_tags_row: int = 5 self.aliases_row: int = 6 self.colors_row: int = 7 self.ext_row: int = 8 @@ -151,7 +151,7 @@ class JsonMigrationModal(QObject): self.old_content_layout.addWidget(QLabel(field_parity_text), self.fields_row, 0) self.old_content_layout.addWidget(QLabel(tags_text), self.tags_row, 0) self.old_content_layout.addWidget(QLabel(shorthand_text), self.shorthands_row, 0) - self.old_content_layout.addWidget(QLabel(subtags_text), self.subtags_row, 0) + self.old_content_layout.addWidget(QLabel(parent_tags_text), self.parent_tags_row, 0) self.old_content_layout.addWidget(QLabel(aliases_text), self.aliases_row, 0) self.old_content_layout.addWidget(QLabel(colors_text), self.colors_row, 0) self.old_content_layout.addWidget(QLabel(ext_text), self.ext_row, 0) @@ -183,7 +183,7 @@ class JsonMigrationModal(QObject): self.old_content_layout.addWidget(old_field_value, self.fields_row, 1) self.old_content_layout.addWidget(old_tag_count, self.tags_row, 1) self.old_content_layout.addWidget(old_shorthand_count, self.shorthands_row, 1) - self.old_content_layout.addWidget(old_subtag_value, self.subtags_row, 1) + self.old_content_layout.addWidget(old_subtag_value, self.parent_tags_row, 1) self.old_content_layout.addWidget(old_alias_value, self.aliases_row, 1) self.old_content_layout.addWidget(old_color_value, self.colors_row, 1) self.old_content_layout.addWidget(old_ext_count, self.ext_row, 1) @@ -192,7 +192,7 @@ class JsonMigrationModal(QObject): self.old_content_layout.addWidget(QLabel(), self.path_row, 2) self.old_content_layout.addWidget(QLabel(), self.fields_row, 2) self.old_content_layout.addWidget(QLabel(), self.shorthands_row, 2) - self.old_content_layout.addWidget(QLabel(), self.subtags_row, 2) + self.old_content_layout.addWidget(QLabel(), self.parent_tags_row, 2) self.old_content_layout.addWidget(QLabel(), self.aliases_row, 2) self.old_content_layout.addWidget(QLabel(), self.colors_row, 2) @@ -214,7 +214,7 @@ class JsonMigrationModal(QObject): self.new_content_layout.addWidget(QLabel(field_parity_text), self.fields_row, 0) self.new_content_layout.addWidget(QLabel(tags_text), self.tags_row, 0) self.new_content_layout.addWidget(QLabel(shorthand_text), self.shorthands_row, 0) - self.new_content_layout.addWidget(QLabel(subtags_text), self.subtags_row, 0) + self.new_content_layout.addWidget(QLabel(parent_tags_text), self.parent_tags_row, 0) self.new_content_layout.addWidget(QLabel(aliases_text), self.aliases_row, 0) self.new_content_layout.addWidget(QLabel(colors_text), self.colors_row, 0) self.new_content_layout.addWidget(QLabel(ext_text), self.ext_row, 0) @@ -246,7 +246,7 @@ class JsonMigrationModal(QObject): self.new_content_layout.addWidget(field_parity_value, self.fields_row, 1) self.new_content_layout.addWidget(new_tag_count, self.tags_row, 1) self.new_content_layout.addWidget(new_shorthand_count, self.shorthands_row, 1) - self.new_content_layout.addWidget(subtag_parity_value, self.subtags_row, 1) + self.new_content_layout.addWidget(subtag_parity_value, self.parent_tags_row, 1) self.new_content_layout.addWidget(alias_parity_value, self.aliases_row, 1) self.new_content_layout.addWidget(new_color_value, self.colors_row, 1) self.new_content_layout.addWidget(new_ext_count, self.ext_row, 1) @@ -257,7 +257,7 @@ class JsonMigrationModal(QObject): self.new_content_layout.addWidget(QLabel(), self.fields_row, 2) self.new_content_layout.addWidget(QLabel(), self.shorthands_row, 2) self.new_content_layout.addWidget(QLabel(), self.tags_row, 2) - self.new_content_layout.addWidget(QLabel(), self.subtags_row, 2) + self.new_content_layout.addWidget(QLabel(), self.parent_tags_row, 2) self.new_content_layout.addWidget(QLabel(), self.aliases_row, 2) self.new_content_layout.addWidget(QLabel(), self.colors_row, 2) self.new_content_layout.addWidget(QLabel(), self.ext_row, 2) @@ -396,7 +396,7 @@ class JsonMigrationModal(QObject): self.update_parity_value(self.fields_row, self.field_parity) self.update_parity_value(self.path_row, self.path_parity) self.update_parity_value(self.shorthands_row, self.shorthand_parity) - self.update_parity_value(self.subtags_row, self.subtag_parity) + self.update_parity_value(self.parent_tags_row, self.subtag_parity) self.update_parity_value(self.aliases_row, self.alias_parity) self.update_parity_value(self.colors_row, self.color_parity) self.sql_lib.close() @@ -639,39 +639,39 @@ class JsonMigrationModal(QObject): return self.path_parity def check_subtag_parity(self) -> bool: - """Check if all JSON subtags match the new SQL subtags.""" - sql_subtags: set[int] = None - json_subtags: set[int] = None + """Check if all JSON parent tags match the new SQL parent tags.""" + sql_parent_tags: set[int] = None + json_parent_tags: set[int] = None with Session(self.sql_lib.engine) as session: for tag in self.sql_lib.tags: if tag.id in range(0, 1000): break tag_id = tag.id # Tag IDs start at 0 - sql_subtags = set( - session.scalars(select(TagSubtag.child_id).where(TagSubtag.parent_id == tag.id)) + sql_parent_tags = set( + session.scalars(select(TagParent.child_id).where(TagParent.parent_id == tag.id)) ) - # sql_subtags = sql_subtags.difference([x for x in range(0, 1000)]) + # sql_parent_tags = sql_parent_tags.difference([x for x in range(0, 1000)]) # JSON tags allowed self-parenting; SQL tags no longer allow this. - json_subtags = set(self.json_lib.get_tag(tag_id).subtag_ids) - json_subtags.discard(tag_id) + json_parent_tags = set(self.json_lib.get_tag(tag_id).subtag_ids) + json_parent_tags.discard(tag_id) logger.info( "[Subtag Parity]", tag_id=tag_id, - json_subtags=json_subtags, - sql_subtags=sql_subtags, + json_parent_tags=json_parent_tags, + sql_parent_tags=sql_parent_tags, ) if not ( - sql_subtags is not None - and json_subtags is not None - and (sql_subtags == json_subtags) + sql_parent_tags is not None + and json_parent_tags is not None + and (sql_parent_tags == json_parent_tags) ): self.discrepancies.append( f"[Subtag Parity][Tag ID: {tag_id}]:" - f"\nOLD (JSON):{json_subtags}\nNEW (SQL):{sql_subtags}" + f"\nOLD (JSON):{json_parent_tags}\nNEW (SQL):{sql_parent_tags}" ) self.subtag_parity = False return self.subtag_parity diff --git a/tagstudio/src/qt/widgets/preview/field_containers.py b/tagstudio/src/qt/widgets/preview/field_containers.py index a61d3a0d..cf2aa9cf 100644 --- a/tagstudio/src/qt/widgets/preview/field_containers.py +++ b/tagstudio/src/qt/widgets/preview/field_containers.py @@ -163,7 +163,7 @@ class FieldContainers(QWidget): """ tag_obj = self.lib.get_tag(tag_id) # Get full object if p_ids is None: - p_ids = tag_obj.subtag_ids + p_ids = tag_obj.parent_ids for p_id in p_ids: if cluster_map.get(p_id) is None: @@ -172,10 +172,10 @@ class FieldContainers(QWidget): if tag.id not in cluster_map[p_id]: cluster_map[p_id].add(tag.id) p_tag = self.lib.get_tag(p_id) # Get full object - if p_tag.subtag_ids: + if p_tag.parent_ids: add_to_cluster( tag_id, - [sub_id for sub_id in p_tag.subtag_ids if sub_id != tag_id], + [sub_id for sub_id in p_tag.parent_ids if sub_id != tag_id], ) exhausted.add(p_id) exhausted.add(tag_id) @@ -240,7 +240,7 @@ class FieldContainers(QWidget): ) for entry_id in self.driver.selected: for field_item in field_list: - self.lib.add_entry_field_type( + self.lib.add_field_to_entry( entry_id, field_id=field_item.data(Qt.ItemDataRole.UserRole), ) diff --git a/tagstudio/src/qt/widgets/tag_box.py b/tagstudio/src/qt/widgets/tag_box.py index 5fbaba83..7f576c60 100644 --- a/tagstudio/src/qt/widgets/tag_box.py +++ b/tagstudio/src/qt/widgets/tag_box.py @@ -76,7 +76,7 @@ class TagBoxWidget(FieldWidget): self.edit_modal = PanelModal( build_tag_panel, - tag.name, # TODO - display name including subtags + tag.name, # TODO - display name including parent tags "Edit Tag", done_callback=self.driver.preview_panel.update_widgets, has_save=True, @@ -85,7 +85,7 @@ class TagBoxWidget(FieldWidget): self.edit_modal.saved.connect( lambda: self.driver.lib.update_tag( build_tag_panel.build_tag(), - subtag_ids=set(build_tag_panel.subtag_ids), + parent_ids=set(build_tag_panel.parent_ids), alias_names=set(build_tag_panel.alias_names), alias_ids=set(build_tag_panel.alias_ids), ) diff --git a/tagstudio/tests/qt/test_build_tag_panel.py b/tagstudio/tests/qt/test_build_tag_panel.py index d0cdea5b..87c7d731 100644 --- a/tagstudio/tests/qt/test_build_tag_panel.py +++ b/tagstudio/tests/qt/test_build_tag_panel.py @@ -11,9 +11,9 @@ def test_build_tag_panel_add_sub_tag_callback(library, generate_tag): panel: BuildTagPanel = BuildTagPanel(library, child) - panel.add_subtag_callback(parent.id) + panel.add_parent_tag_callback(parent.id) - assert len(panel.subtag_ids) == 1 + assert len(panel.parent_ids) == 1 def test_build_tag_panel_remove_subtag_callback(library, generate_tag): @@ -30,9 +30,9 @@ def test_build_tag_panel_remove_subtag_callback(library, generate_tag): panel: BuildTagPanel = BuildTagPanel(library, child) - panel.remove_subtag_callback(parent.id) + panel.remove_parent_tag_callback(parent.id) - assert len(panel.subtag_ids) == 0 + assert len(panel.parent_ids) == 0 import os @@ -79,14 +79,14 @@ def test_build_tag_panel_set_subtags(library, generate_tag): assert parent assert child - library.add_subtag(child.id, parent.id) + library.add_parent_tag(child.id, parent.id) child = library.get_tag(child.id) panel: BuildTagPanel = BuildTagPanel(library, child) - assert len(panel.subtag_ids) == 1 - assert panel.subtags_scroll_layout.count() == 1 + assert len(panel.parent_ids) == 1 + assert panel.parent_tags_scroll_layout.count() == 1 def test_build_tag_panel_add_aliases(library, generate_tag): diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index 82f9522e..0b3f84b7 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -18,9 +18,6 @@ def test_library_add_alias(library, generate_tag): alias_names: set[str] = set() alias_names.add("test_alias") library.update_tag(tag, subtag_ids, alias_names, alias_ids) - - # Note: ask if it is expected behaviour that you need to re-request - # for the tag. Or if the tag in memory should be updated alias_ids = library.get_tag(tag.id).alias_ids assert len(alias_ids) == 1 @@ -35,7 +32,6 @@ def test_library_get_alias(library, generate_tag): alias_names: set[str] = set() alias_names.add("test_alias") library.update_tag(tag, subtag_ids, alias_names, alias_ids) - alias_ids = library.get_tag(tag.id).alias_ids assert library.get_alias(tag.id, alias_ids[0]).name == "test_alias" @@ -50,9 +46,7 @@ def test_library_update_alias(library, generate_tag): alias_names: set[str] = set() alias_names.add("test_alias") library.update_tag(tag, subtag_ids, alias_names, alias_ids) - - tag = library.get_tag(tag.id) - alias_ids = tag.alias_ids + alias_ids = library.get_tag(tag.id).alias_ids assert library.get_alias(tag.id, alias_ids[0]).name == "test_alias" @@ -61,7 +55,6 @@ def test_library_update_alias(library, generate_tag): library.update_tag(tag, subtag_ids, alias_names, alias_ids) tag = library.get_tag(tag.id) - assert len(tag.alias_ids) == 1 assert library.get_alias(tag.id, tag.alias_ids[0]).name == "alias_update" @@ -77,9 +70,7 @@ def test_library_add_file(library): ) assert not library.has_path_entry(entry.path) - assert library.add_entries([entry]) - assert library.has_path_entry(entry.path) @@ -107,7 +98,7 @@ def test_tag_subtag_itself(library, generate_tag): library.update_tag(tag, {tag.id}, {}, {}) tag = library.get_tag(tag.id) - assert len(tag.subtag_ids) == 0 + assert len(tag.parent_ids) == 0 def test_library_search(library, generate_tag, entry_full): @@ -133,11 +124,8 @@ def test_tag_search(library): tag = library.tags[0] assert library.search_tags(tag.name.lower()) - assert library.search_tags(tag.name.upper()) - assert library.search_tags(tag.name[2:-2]) - assert not library.search_tags(tag.name * 2) @@ -168,11 +156,10 @@ def test_add_field_to_entry(library): ) # meta tags + content tags assert len(entry.tag_box_fields) == 2 - assert library.add_entries([entry]) # When - library.add_entry_field_type(entry.id, field_id=_FieldID.TAGS) + library.add_field_to_entry(entry.id, field_id=_FieldID.TAGS) # Then entry = [x for x in library.get_entries(with_joins=True) if x.path == entry.path][0] @@ -195,22 +182,22 @@ def test_add_field_tag(library: Library, entry_full, generate_tag): assert [x.name for x in tag_field.tags if x.name == tag_name] -def test_subtags_add(library, generate_tag): +def test_parents_add(library, generate_tag): # Given - tag = library.tags[0] + tag: Tag = library.tags[0] assert tag.id is not None - subtag = generate_tag("subtag1") - subtag = library.add_tag(subtag) - assert subtag.id is not None + parent_tag = generate_tag("subtag1") + parent_tag = library.add_tag(parent_tag) + assert parent_tag.id is not None # When - assert library.add_subtag(tag.id, subtag.id) + assert library.add_parent_tag(tag.id, parent_tag.id) # Then assert tag.id is not None tag = library.get_tag(tag.id) - assert tag.subtag_ids + assert tag.parent_ids def test_remove_tag(library, generate_tag):