Skip to content

feat(parity): backend for aliases and parent tags #596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 82 additions & 45 deletions tagstudio/src/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import (
Session,
aliased,
contains_eager,
make_transient,
selectinload,
Expand Down Expand Up @@ -417,13 +418,18 @@ def search_library(
statement = select(Entry)

if search.tag:
SubtagAlias = aliased(Tag) # noqa: N806
statement = (
statement.join(Entry.tag_box_fields)
.join(TagBoxField.tags)
.outerjoin(Tag.aliases)
.outerjoin(SubtagAlias, Tag.subtags)
.where(
or_(
Tag.name.ilike(search.tag),
Tag.shorthand.ilike(search.tag),
TagAlias.name.ilike(search.tag),
SubtagAlias.name.ilike(search.tag),
)
)
)
Expand Down Expand Up @@ -752,18 +758,23 @@ def add_entry_field_type(
)
return True

def add_tag(self, tag: Tag, subtag_ids: list[int] | None = None) -> Tag | None:
def add_tag(
self,
tag: Tag,
subtag_ids: set[int] | None = None,
alias_names: set[str] | None = None,
alias_ids: set[int] | None = None,
) -> Tag | None:
with Session(self.engine, expire_on_commit=False) as session:
try:
session.add(tag)
session.flush()

for subtag_id in subtag_ids or []:
subtag = TagSubtag(
parent_id=tag.id,
child_id=subtag_id,
)
session.add(subtag)
if subtag_ids is not None:
self.update_subtags(tag, subtag_ids, session)

if alias_ids is not None and alias_names is not None:
self.update_aliases(tag, alias_ids, alias_names, session)

session.commit()

Expand Down Expand Up @@ -847,75 +858,101 @@ def save_library_backup_to_disk(self) -> Path:

def get_tag(self, tag_id: int) -> Tag:
with Session(self.engine) as session:
tags_query = select(Tag).options(selectinload(Tag.subtags))
tags_query = select(Tag).options(selectinload(Tag.subtags), 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 alias in tag.aliases:
session.expunge(alias)

return tag

def get_alias(self, tag_id: int, alias_id: int) -> TagAlias:
with Session(self.engine) as session:
alias_query = select(TagAlias).where(TagAlias.id == alias_id, TagAlias.tag_id == tag_id)
alias = session.scalar(alias_query.where(TagAlias.id == alias_id))

return alias

def add_subtag(self, base_id: int, new_tag_id: int) -> bool:
if base_id == new_tag_id:
return False

# open session and save as parent tag
with Session(self.engine) as session:
tag = TagSubtag(
subtag = TagSubtag(
parent_id=base_id,
child_id=new_tag_id,
)

try:
session.add(tag)
session.add(subtag)
session.commit()
return True
except IntegrityError:
session.rollback()
logger.exception("IntegrityError")
return False

def update_tag(self, tag: Tag, subtag_ids: list[int]) -> None:
def remove_subtag(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()
session.delete(remove)
session.commit()

return True

def update_tag(
self,
tag: Tag,
subtag_ids: set[int] | None = None,
alias_names: set[str] | None = None,
alias_ids: set[int] | None = None,
) -> None:
"""Edit a Tag in the Library."""
# TODO - maybe merge this with add_tag?
self.add_tag(tag, subtag_ids, alias_names, alias_ids)

if tag.shorthand:
tag.shorthand = slugify(tag.shorthand)
def update_aliases(self, tag, alias_ids, alias_names, session):
prev_aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id)).all()

if tag.aliases:
# TODO
...
for alias in prev_aliases:
if alias.id not in alias_ids or alias.name not in alias_names:
session.delete(alias)
else:
alias_ids.remove(alias.id)
alias_names.remove(alias.name)

# save the tag
with Session(self.engine) as session:
try:
# update the existing tag
session.add(tag)
session.flush()
for alias_name in alias_names:
alias = TagAlias(alias_name, tag.id)
session.add(alias)

# load all tag's subtag to know which to remove
prev_subtags = session.scalars(
select(TagSubtag).where(TagSubtag.parent_id == tag.id)
).all()
def update_subtags(self, tag, subtag_ids, session):
if tag.id in subtag_ids:
subtag_ids.remove(tag.id)

for subtag in prev_subtags:
if subtag.child_id not in subtag_ids:
session.delete(subtag)
else:
# no change, remove from list
subtag_ids.remove(subtag.child_id)
# load all tag's subtag to know which to remove
prev_subtags = session.scalars(select(TagSubtag).where(TagSubtag.parent_id == tag.id)).all()

# create remaining items
for subtag_id in subtag_ids:
# add new subtag
subtag = TagSubtag(
parent_id=tag.id,
child_id=subtag_id,
)
session.add(subtag)
for subtag in prev_subtags:
if subtag.child_id not in subtag_ids:
session.delete(subtag)
else:
# no change, remove from list
subtag_ids.remove(subtag.child_id)

session.commit()
except IntegrityError:
session.rollback()
logger.exception("IntegrityError")
# create remaining items
for subtag_id in subtag_ids:
# add new subtag
subtag = TagSubtag(
parent_id=tag.id,
child_id=subtag_id,
)
session.add(subtag)

def prefs(self, key: LibraryPrefs) -> Any:
# load given item from Preferences table
Expand Down
11 changes: 7 additions & 4 deletions tagstudio/src/core/library/alchemy/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
from typing import Optional

from sqlalchemy import JSON, ForeignKey, Integer, event
from sqlalchemy.orm import Mapped, mapped_column, relationship
Expand Down Expand Up @@ -29,11 +28,11 @@ class TagAlias(Base):
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"))
tag: Mapped["Tag"] = relationship(back_populates="aliases")

def __init__(self, name: str, tag: Optional["Tag"] = None):
def __init__(self, name: str, tag_id: int | None = None):
self.name = name

if tag:
self.tag = tag
if tag_id is not None:
self.tag_id = tag_id

super().__init__()

Expand Down Expand Up @@ -73,6 +72,10 @@ def subtag_ids(self) -> list[int]:
def alias_strings(self) -> list[str]:
return [alias.name for alias in self.aliases]

@property
def alias_ids(self) -> list[int]:
return [tag.id for tag in self.aliases]

def __init__(
self,
name: str,
Expand Down
3 changes: 3 additions & 0 deletions tagstudio/src/qt/flowlayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,7 @@ def _do_layout(self, rect: QRect, test_only: bool) -> float:
x = next_x
line_height = max(line_height, item.sizeHint().height())

if len(self._item_list) == 0:
return 0

return y + line_height - rect.y() * ((len(self._item_list)) / len(self._item_list))
Loading