mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-08-19 10:32:35 +02:00
feat: add per-tag category display options (#1470)
* feat: add per-tag display options. * feat: bump DB_VERSION to 301, add migration. * tests: add tests. * docs: document cateogry display options. * docs: document library versions 300 and 301. * fix: resolve ruff and pyright issues. * fix: redo migration in the new style. * fix: Remove unnecessary @staticmethod decorators. * fix: Bump library version to 400. * fix: Fix color order in colored_radio_button_style. * fix: Commit the library produced by pytest. * test: Add test for removing a category during tag creation. * fix: Fix removing categories during tag creation. * test: Add test for adding another tag after removing an existing category. * fix: Fix adding another tag after removing an existing category. * feat: Add separators between widgets. * docs: Fix version in library-changes.md. * test: Test removing a category inherited both directly and indirectly. * fix: Fix removing a category inherited both directly and indirectly. --------- Co-authored-by: Travis Abendshien <46939827+cyanvoxel@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
@@ -4,13 +4,16 @@
|
||||
# pyright: reportPrivateUsage = false
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import cast
|
||||
|
||||
from PySide6.QtWidgets import QCheckBox
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagAlias
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel, CustomTableItem
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
|
||||
@@ -171,3 +174,312 @@ def test_build_tag_panel_build_tag(qtbot: QtBot, library: Library):
|
||||
tag: Tag = panel.build_tag()
|
||||
|
||||
assert tag.name == Translations["tag.new"]
|
||||
|
||||
|
||||
def test_build_tag_panel_show_category_from_parent(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124, parent_tags={parent})))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == parent
|
||||
|
||||
|
||||
def test_build_tag_panel_show_category_from_grandparent(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
grandparent = unwrap(library.add_tag(generate_tag("grandparent", id=122, is_category=True)))
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, parent_tags={grandparent})))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124, parent_tags={parent})))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == grandparent
|
||||
|
||||
|
||||
def test_build_tag_panel_add_category_through_parent(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124)))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
assert __find_category_tag_widget(panel) is None
|
||||
|
||||
child.parent_tags.add(parent)
|
||||
|
||||
panel._add_parent_tag_callback(parent.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == parent
|
||||
|
||||
|
||||
def test_build_tag_panel_add_category_through_grandparent(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
grandparent = unwrap(library.add_tag(generate_tag("grandparent", id=122, is_category=True)))
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, parent_tags={grandparent})))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124)))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
assert __find_category_tag_widget(panel) is None
|
||||
|
||||
child.parent_tags.add(parent)
|
||||
|
||||
panel._add_parent_tag_callback(parent.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == grandparent
|
||||
|
||||
|
||||
def test_build_tag_panel_remove_category_through_parent(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124, parent_tags={parent})))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == parent
|
||||
|
||||
panel._remove_parent_tag_callback(parent.id)
|
||||
|
||||
assert __find_category_tag_widget(panel) is None
|
||||
|
||||
|
||||
def test_build_tag_panel_remove_category_through_grandparent(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
grandparent = unwrap(library.add_tag(generate_tag("grandparent", id=122, is_category=True)))
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, parent_tags={grandparent})))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124, parent_tags={parent})))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == grandparent
|
||||
|
||||
panel._remove_parent_tag_callback(parent.id)
|
||||
|
||||
assert __find_category_tag_widget(panel) is None
|
||||
|
||||
|
||||
def test_build_tag_panel_exclude_from_category(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124, parent_tags={parent})))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
assert len(panel.exclusion_ids) == 0
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
checkbox = __find_include_checkbox(tag_widget)
|
||||
assert checkbox.isChecked()
|
||||
|
||||
checkbox.click()
|
||||
|
||||
assert parent.id in panel.exclusion_ids
|
||||
|
||||
|
||||
def test_build_tag_panel_include_in_category(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child = unwrap(
|
||||
library.add_tag(
|
||||
generate_tag("child", id=124, parent_tags={parent}, category_exclusions={parent})
|
||||
)
|
||||
)
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
assert parent.id in panel.exclusion_ids
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
checkbox = __find_include_checkbox(tag_widget)
|
||||
assert not checkbox.isChecked()
|
||||
|
||||
checkbox.click()
|
||||
|
||||
assert len(panel.exclusion_ids) == 0
|
||||
|
||||
|
||||
def test_build_tag_panel_remove_duplicate_category_retained(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
grandparent = unwrap(library.add_tag(generate_tag("grandparent", id=122, is_category=True)))
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, parent_tags={grandparent})))
|
||||
other_parent = unwrap(
|
||||
library.add_tag(generate_tag("other_parent", id=124, parent_tags={grandparent}))
|
||||
)
|
||||
child = unwrap(
|
||||
library.add_tag(generate_tag("child", id=125, parent_tags={parent, other_parent}))
|
||||
)
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == grandparent
|
||||
|
||||
panel._remove_parent_tag_callback(parent.id)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == grandparent
|
||||
|
||||
|
||||
def test_build_tag_panel_new_tag_multiple_categories(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
other_parent = unwrap(library.add_tag(generate_tag("other_parent", id=124, is_category=True)))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is None
|
||||
|
||||
panel._add_parent_tag_callback(parent.id)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == parent
|
||||
|
||||
panel._add_parent_tag_callback(other_parent.id)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel, 1)
|
||||
assert tag_widget is not None
|
||||
assert tag_widget.tag == other_parent
|
||||
|
||||
|
||||
def test_build_tag_panel_category_not_shown_for_self(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
library.add_tag(generate_tag("category", id=123, is_category=True))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is None
|
||||
|
||||
|
||||
def test_build_tag_panel_remove_inherited_from_multiple_parents_during_tag_creation(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child1 = unwrap(library.add_tag(generate_tag("child1", id=124, parent_tags={parent})))
|
||||
child2 = unwrap(library.add_tag(generate_tag("child2", id=125, parent_tags={parent})))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
panel._add_parent_tag_callback(124)
|
||||
panel._add_parent_tag_callback(125)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
panel._remove_parent_tag_callback(child1.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
panel._remove_parent_tag_callback(child2.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is None
|
||||
|
||||
|
||||
def test_build_tag_panel_add_different_category_after_removing_other_category(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
category = unwrap(library.add_tag(generate_tag("category", id=123, is_category=True)))
|
||||
tag = unwrap(library.add_tag(generate_tag("tag", id=124, parent_tags={category})))
|
||||
other = unwrap(library.add_tag(generate_tag("other", id=125)))
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, tag)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
panel._remove_parent_tag_callback(category.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is None
|
||||
|
||||
panel._add_parent_tag_callback(other.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is None
|
||||
|
||||
|
||||
def test_build_tag_panel_remove_category_inherited_directly_and_indirectly(
|
||||
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
parent = unwrap(library.add_tag(generate_tag("parent", id=123, is_category=True)))
|
||||
child = unwrap(library.add_tag(generate_tag("child", id=124, parent_tags={parent})))
|
||||
grandchild = unwrap(
|
||||
library.add_tag(generate_tag("grandchild", id=125, parent_tags={parent, child}))
|
||||
)
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(library, grandchild)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
panel._remove_parent_tag_callback(parent.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is not None
|
||||
|
||||
panel._remove_parent_tag_callback(child.id)
|
||||
tag_widget = __find_category_tag_widget(panel)
|
||||
assert tag_widget is None
|
||||
|
||||
|
||||
def __find_category_tag_widget(panel: BuildTagPanel, index: int = 0) -> TagWidget | None:
|
||||
item = panel.category_scroll_layout.itemAt(0).widget().layout().itemAt(index)
|
||||
while item is not None:
|
||||
if isinstance(item.widget(), TagWidget):
|
||||
break
|
||||
item = item.widget().layout().itemAt(0)
|
||||
|
||||
if item is not None:
|
||||
return cast(TagWidget, item.widget())
|
||||
return None
|
||||
|
||||
|
||||
def __find_include_checkbox(tag_widget: TagWidget) -> QCheckBox:
|
||||
layout_item = tag_widget.parentWidget().layout().itemAt(1)
|
||||
assert layout_item is not None
|
||||
|
||||
widget = layout_item.widget()
|
||||
assert isinstance(widget, QCheckBox)
|
||||
|
||||
return widget
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
# pyright: reportPrivateUsage=false
|
||||
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
|
||||
@@ -182,3 +185,26 @@ def test_custom_tag_category(qt_driver: QtDriver, entry_full: Entry):
|
||||
assert container.title != "<h4>Tags</h4>"
|
||||
case _:
|
||||
pass
|
||||
|
||||
|
||||
def test_exclude_tag_category(
|
||||
qt_driver: QtDriver, library: Library, generate_tag: Callable[..., Tag]
|
||||
):
|
||||
panel = PreviewPanel(qt_driver)
|
||||
|
||||
category_parent = unwrap(generate_tag("category_parent", id=123, is_category=True))
|
||||
library.add_tag(category_parent)
|
||||
|
||||
tag = unwrap(generate_tag("tag", id=124))
|
||||
library.add_tag(tag, parent_ids={category_parent.id}, exclusion_ids={category_parent.id})
|
||||
|
||||
entry = Entry(id=777, path=Path("test.txt"), fields=[])
|
||||
|
||||
library.add_entries([entry])
|
||||
library.add_tags_to_entries(entry.id, tag.id)
|
||||
|
||||
qt_driver.toggle_item_selection(entry.id, append=False, bridge=False)
|
||||
panel.set_selection(qt_driver.selected)
|
||||
|
||||
assert len(panel.containers._containers) == 1
|
||||
assert panel.containers._containers[0].title == "<h4>Tags</h4>"
|
||||
|
||||
Reference in New Issue
Block a user