Skip to content

Commit

Permalink
feat: delete and create tags from tag database panel (#569)
Browse files Browse the repository at this point in the history
* [feat] can now add a new tag from the tag library panel

* [feat] can remove tags from the tag library panel
[fix] library panel updates tag list when a new tag is create

* [test] added test for library->remove_tag

* [fix] type error

* removed redundant lambda

Co-authored-by: VasigaranAndAngel <[email protected]>

* fix: tags with a reserved id could be edited or removed, now they cannot.

* fix: when a tag is removed or edited the preivew panel will update to reflect the changes

Co-authored-by: Sean Krueger <[email protected]>

* fix: mypy check

* fix: aliases and subtags not being removed from DB when tag they reference was removed.

* feat: added a confirmation message box when removing tags.

* fix: mypy

---------

Co-authored-by: VasigaranAndAngel <[email protected]>
Co-authored-by: Sean Krueger <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent 8387676 commit fdfd649
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 2 deletions.
1 change: 1 addition & 0 deletions tagstudio/src/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@

TAG_FAVORITE = 1
TAG_ARCHIVED = 0
RESERVED_TAG_IDS = range(0, 999)
33 changes: 33 additions & 0 deletions tagstudio/src/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,39 @@ def update_entry_path(self, entry_id: int | Entry, path: Path) -> None:
session.execute(update_stmt)
session.commit()

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)
).all()

tags_query = select(Tag).options(
selectinload(Tag.subtags), selectinload(Tag.aliases)
)
tag = session.scalar(tags_query.where(Tag.id == tag.id))

aliases = session.scalars(select(TagAlias).where(TagAlias.tag_id == tag.id))

for alias in aliases or []:
session.delete(alias)

for subtag in subtags or []:
session.delete(subtag)
session.expunge(subtag)

session.delete(tag)

session.commit()

session.expunge(tag)
return tag

except IntegrityError as e:
logger.exception(e)
session.rollback()
return None

def remove_tag_from_field(self, tag: Tag, field: TagBoxField) -> None:
with Session(self.engine) as session:
field_ = session.scalars(select(TagBoxField).where(TagBoxField.id == field.id)).one()
Expand Down
56 changes: 55 additions & 1 deletion tagstudio/src/qt/modals/tag_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
QFrame,
QHBoxLayout,
QLineEdit,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from src.core.constants import RESERVED_TAG_IDS
from src.core.library import Library, Tag
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.widgets.panel import PanelModal, PanelWidget
Expand Down Expand Up @@ -59,8 +62,32 @@ def __init__(self, library: Library):
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setWidget(self.scroll_contents)

self.create_tag_button = QPushButton()
self.create_tag_button.setText("Create Tag")
self.create_tag_button.clicked.connect(self.build_tag)

self.root_layout.addWidget(self.search_field)
self.root_layout.addWidget(self.scroll_area)
self.root_layout.addWidget(self.create_tag_button)
self.update_tags()

def build_tag(self):
self.modal = PanelModal(
BuildTagPanel(self.lib),
"New Tag",
"Add Tag",
has_save=True,
)

panel: BuildTagPanel = self.modal.widget
self.modal.saved.connect(
lambda: (
self.lib.add_tag(panel.build_tag(), panel.subtag_ids),
self.modal.hide(),
self.update_tags(),
)
)
self.modal.show()

def on_return(self, text: str):
if text and self.first_tag_id >= 0:
Expand All @@ -84,14 +111,41 @@ def update_tags(self, query: str | None = None):
row = QHBoxLayout(container)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(3)
tag_widget = TagWidget(tag, has_edit=True, has_remove=False)

if tag.id in RESERVED_TAG_IDS:
tag_widget = TagWidget(tag, has_edit=False, has_remove=False)
else:
tag_widget = TagWidget(tag, has_edit=True, has_remove=True)

tag_widget.on_edit.connect(lambda checked=False, t=tag: self.edit_tag(t))
tag_widget.on_remove.connect(lambda t=tag: self.remove_tag(t))
row.addWidget(tag_widget)
self.scroll_layout.addWidget(container)

self.search_field.setFocus()

def remove_tag(self, tag: Tag):
if tag.id in RESERVED_TAG_IDS:
return

message_box = QMessageBox()
message_box.setWindowTitle("Remove tag")
message_box.setText("Are you sure you want to remove " + tag.name + "?")
message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore
message_box.setIcon(QMessageBox.Question) # type: ignore

result = message_box.exec()

if result != QMessageBox.Ok: # type: ignore
return

self.lib.remove_tag(tag)
self.update_tags()

def edit_tag(self, tag: Tag):
if tag.id in RESERVED_TAG_IDS:
return

build_tag_panel = BuildTagPanel(self.lib, tag=tag)

self.edit_modal = PanelModal(
Expand Down
4 changes: 4 additions & 0 deletions tagstudio/src/qt/widgets/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ def __init__(
self.root_layout.setStretch(1, 2)
self.root_layout.addWidget(self.button_container)

def closeEvent(self, event): # noqa: N802
self.done_button.click()
event.accept()


class PanelWidget(QWidget):
"""Used for widgets that go in a modal panel, ex. for editing or searching."""
Expand Down
1 change: 0 additions & 1 deletion tagstudio/src/qt/widgets/thumb_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import cv2
import numpy as np
import pillow_jxl # noqa: F401
import rawpy
import structlog
from mutagen import MutagenError, flac, id3, mp4
Expand Down
11 changes: 11 additions & 0 deletions tagstudio/tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ def test_subtags_add(library, generate_tag):
assert tag.subtag_ids


def test_remove_tag(library, generate_tag):
tag = library.add_tag(generate_tag("food", id=123))

assert tag

tag_count = len(library.tags)

library.remove_tag(tag)
assert len(library.tags) == tag_count - 1


@pytest.mark.parametrize("is_exclude", [True, False])
def test_search_filter_extensions(library, is_exclude):
# Given
Expand Down

0 comments on commit fdfd649

Please sign in to comment.