Skip to content

Commit

Permalink
feat(parity): backend for aliases and parent tags (#596)
Browse files Browse the repository at this point in the history
* backend for aliases and parents

* resolve merge conflics
  • Loading branch information
DandyDev01 authored Nov 21, 2024
1 parent f6a1ca6 commit 0d166e6
Show file tree
Hide file tree
Showing 12 changed files with 687 additions and 105 deletions.
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

0 comments on commit 0d166e6

Please sign in to comment.