mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-29 06:10:51 +00:00
refactor: merge cyclicly imported files into library.py
This commit is contained in:
@@ -20,3 +20,14 @@ WITH RECURSIVE ChildTags AS (
|
||||
)
|
||||
SELECT * FROM ChildTags;
|
||||
""")
|
||||
|
||||
TAG_CHILDREN_ID_QUERY = text("""
|
||||
WITH RECURSIVE ChildTags AS (
|
||||
SELECT :tag_id AS tag_id
|
||||
UNION
|
||||
SELECT tp.child_id AS tag_id
|
||||
FROM tag_parents tp
|
||||
INNER JOIN ChildTags c ON tp.parent_id = c.tag_id
|
||||
)
|
||||
SELECT tag_id FROM ChildTags;
|
||||
""")
|
||||
|
||||
@@ -1,572 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.models import Namespace, TagColorGroup
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def namespaces() -> list[Namespace]:
|
||||
tagstudio_standard = Namespace("tagstudio-standard", "TagStudio Standard")
|
||||
tagstudio_pastels = Namespace("tagstudio-pastels", "TagStudio Pastels")
|
||||
tagstudio_shades = Namespace("tagstudio-shades", "TagStudio Shades")
|
||||
tagstudio_earth_tones = Namespace("tagstudio-earth-tones", "TagStudio Earth Tones")
|
||||
tagstudio_grayscale = Namespace("tagstudio-grayscale", "TagStudio Grayscale")
|
||||
tagstudio_neon = Namespace("tagstudio-neon", "TagStudio Neon")
|
||||
return [
|
||||
tagstudio_standard,
|
||||
tagstudio_pastels,
|
||||
tagstudio_shades,
|
||||
tagstudio_earth_tones,
|
||||
tagstudio_grayscale,
|
||||
tagstudio_neon,
|
||||
]
|
||||
|
||||
|
||||
def json_to_sql_color(json_color: str) -> tuple[str | None, str | None]:
|
||||
"""Convert a color string from a <=9.4 JSON library to a 9.5+ (namespace, slug) tuple."""
|
||||
json_color_ = json_color.lower()
|
||||
match json_color_:
|
||||
case "black":
|
||||
return ("tagstudio-grayscale", "black")
|
||||
case "dark gray":
|
||||
return ("tagstudio-grayscale", "dark-gray")
|
||||
case "gray":
|
||||
return ("tagstudio-grayscale", "gray")
|
||||
case "light gray":
|
||||
return ("tagstudio-grayscale", "light-gray")
|
||||
case "white":
|
||||
return ("tagstudio-grayscale", "white")
|
||||
case "light pink":
|
||||
return ("tagstudio-pastels", "light-pink")
|
||||
case "pink":
|
||||
return ("tagstudio-standard", "pink")
|
||||
case "magenta":
|
||||
return ("tagstudio-standard", "magenta")
|
||||
case "red":
|
||||
return ("tagstudio-standard", "red")
|
||||
case "red orange":
|
||||
return ("tagstudio-standard", "red-orange")
|
||||
case "salmon":
|
||||
return ("tagstudio-pastels", "salmon")
|
||||
case "orange":
|
||||
return ("tagstudio-standard", "orange")
|
||||
case "yellow orange":
|
||||
return ("tagstudio-standard", "amber")
|
||||
case "yellow":
|
||||
return ("tagstudio-standard", "yellow")
|
||||
case "mint":
|
||||
return ("tagstudio-pastels", "mint")
|
||||
case "lime":
|
||||
return ("tagstudio-standard", "lime")
|
||||
case "light green":
|
||||
return ("tagstudio-pastels", "light-green")
|
||||
case "green":
|
||||
return ("tagstudio-standard", "green")
|
||||
case "teal":
|
||||
return ("tagstudio-standard", "teal")
|
||||
case "cyan":
|
||||
return ("tagstudio-standard", "cyan")
|
||||
case "light blue":
|
||||
return ("tagstudio-pastels", "light-blue")
|
||||
case "blue":
|
||||
return ("tagstudio-standard", "blue")
|
||||
case "blue violet":
|
||||
return ("tagstudio-shades", "navy")
|
||||
case "violet":
|
||||
return ("tagstudio-standard", "indigo")
|
||||
case "purple":
|
||||
return ("tagstudio-standard", "purple")
|
||||
case "peach":
|
||||
return ("tagstudio-earth-tones", "peach")
|
||||
case "brown":
|
||||
return ("tagstudio-earth-tones", "brown")
|
||||
case "lavender":
|
||||
return ("tagstudio-pastels", "lavender")
|
||||
case "blonde":
|
||||
return ("tagstudio-earth-tones", "blonde")
|
||||
case "auburn":
|
||||
return ("tagstudio-shades", "auburn")
|
||||
case "light brown":
|
||||
return ("tagstudio-earth-tones", "light-brown")
|
||||
case "dark brown":
|
||||
return ("tagstudio-earth-tones", "dark-brown")
|
||||
case "cool gray":
|
||||
return ("tagstudio-earth-tones", "cool-gray")
|
||||
case "warm gray":
|
||||
return ("tagstudio-earth-tones", "warm-gray")
|
||||
case "olive":
|
||||
return ("tagstudio-shades", "olive")
|
||||
case "berry":
|
||||
return ("tagstudio-shades", "berry")
|
||||
case _:
|
||||
return (None, None)
|
||||
|
||||
|
||||
def standard() -> list[TagColorGroup]:
|
||||
red = TagColorGroup(
|
||||
slug="red",
|
||||
namespace="tagstudio-standard",
|
||||
name="Red",
|
||||
primary="#E22C3C",
|
||||
)
|
||||
red_orange = TagColorGroup(
|
||||
slug="red-orange",
|
||||
namespace="tagstudio-standard",
|
||||
name="Red Orange",
|
||||
primary="#E83726",
|
||||
)
|
||||
orange = TagColorGroup(
|
||||
slug="orange",
|
||||
namespace="tagstudio-standard",
|
||||
name="Orange",
|
||||
primary="#ED6022",
|
||||
)
|
||||
amber = TagColorGroup(
|
||||
slug="amber",
|
||||
namespace="tagstudio-standard",
|
||||
name="Amber",
|
||||
primary="#FA9A2C",
|
||||
)
|
||||
yellow = TagColorGroup(
|
||||
slug="yellow",
|
||||
namespace="tagstudio-standard",
|
||||
name="Yellow",
|
||||
primary="#FFD63D",
|
||||
)
|
||||
lime = TagColorGroup(
|
||||
slug="lime",
|
||||
namespace="tagstudio-standard",
|
||||
name="Lime",
|
||||
primary="#92E649",
|
||||
)
|
||||
green = TagColorGroup(
|
||||
slug="green",
|
||||
namespace="tagstudio-standard",
|
||||
name="Green",
|
||||
primary="#45D649",
|
||||
)
|
||||
teal = TagColorGroup(
|
||||
slug="teal",
|
||||
namespace="tagstudio-standard",
|
||||
name="Teal",
|
||||
primary="#22D589",
|
||||
)
|
||||
cyan = TagColorGroup(
|
||||
slug="cyan",
|
||||
namespace="tagstudio-standard",
|
||||
name="Cyan",
|
||||
primary="#3DDBDB",
|
||||
)
|
||||
blue = TagColorGroup(
|
||||
slug="blue",
|
||||
namespace="tagstudio-standard",
|
||||
name="Blue",
|
||||
primary="#3B87F0",
|
||||
)
|
||||
indigo = TagColorGroup(
|
||||
slug="indigo",
|
||||
namespace="tagstudio-standard",
|
||||
name="Indigo",
|
||||
primary="#874FF5",
|
||||
)
|
||||
purple = TagColorGroup(
|
||||
slug="purple",
|
||||
namespace="tagstudio-standard",
|
||||
name="Purple",
|
||||
primary="#BB4FF0",
|
||||
)
|
||||
magenta = TagColorGroup(
|
||||
slug="magenta",
|
||||
namespace="tagstudio-standard",
|
||||
name="Magenta",
|
||||
primary="#F64680",
|
||||
)
|
||||
pink = TagColorGroup(
|
||||
slug="pink",
|
||||
namespace="tagstudio-standard",
|
||||
name="Pink",
|
||||
primary="#FF62AF",
|
||||
)
|
||||
return [
|
||||
red,
|
||||
red_orange,
|
||||
orange,
|
||||
amber,
|
||||
yellow,
|
||||
lime,
|
||||
green,
|
||||
teal,
|
||||
cyan,
|
||||
blue,
|
||||
indigo,
|
||||
purple,
|
||||
pink,
|
||||
magenta,
|
||||
]
|
||||
|
||||
|
||||
def pastels() -> list[TagColorGroup]:
|
||||
coral = TagColorGroup(
|
||||
slug="coral",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Coral",
|
||||
primary="#F2525F",
|
||||
)
|
||||
salmon = TagColorGroup(
|
||||
slug="salmon",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Salmon",
|
||||
primary="#F66348",
|
||||
)
|
||||
light_orange = TagColorGroup(
|
||||
slug="light-orange",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Orange",
|
||||
primary="#FF9450",
|
||||
)
|
||||
light_amber = TagColorGroup(
|
||||
slug="light-amber",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Amber",
|
||||
primary="#FFBA57",
|
||||
)
|
||||
light_yellow = TagColorGroup(
|
||||
slug="light-yellow",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Yellow",
|
||||
primary="#FFE173",
|
||||
)
|
||||
light_lime = TagColorGroup(
|
||||
slug="light-lime",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Lime",
|
||||
primary="#C9FF7A",
|
||||
)
|
||||
light_green = TagColorGroup(
|
||||
slug="light-green",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Green",
|
||||
primary="#81FF76",
|
||||
)
|
||||
mint = TagColorGroup(
|
||||
slug="mint",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Mint",
|
||||
primary="#68FFB4",
|
||||
)
|
||||
sky_blue = TagColorGroup(
|
||||
slug="sky-blue",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Sky Blue",
|
||||
primary="#8EFFF4",
|
||||
)
|
||||
light_blue = TagColorGroup(
|
||||
slug="light-blue",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Blue",
|
||||
primary="#64C6FF",
|
||||
)
|
||||
lavender = TagColorGroup(
|
||||
slug="lavender",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Lavender",
|
||||
primary="#908AF6",
|
||||
)
|
||||
lilac = TagColorGroup(
|
||||
slug="lilac",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Lilac",
|
||||
primary="#DF95FF",
|
||||
)
|
||||
light_pink = TagColorGroup(
|
||||
slug="light-pink",
|
||||
namespace="tagstudio-pastels",
|
||||
name="Light Pink",
|
||||
primary="#FF87BA",
|
||||
)
|
||||
return [
|
||||
coral,
|
||||
salmon,
|
||||
light_orange,
|
||||
light_amber,
|
||||
light_yellow,
|
||||
light_lime,
|
||||
light_green,
|
||||
mint,
|
||||
sky_blue,
|
||||
light_blue,
|
||||
lavender,
|
||||
lilac,
|
||||
light_pink,
|
||||
]
|
||||
|
||||
|
||||
def shades() -> list[TagColorGroup]:
|
||||
burgundy = TagColorGroup(
|
||||
slug="burgundy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Burgundy",
|
||||
primary="#6E1C24",
|
||||
)
|
||||
auburn = TagColorGroup(
|
||||
slug="auburn",
|
||||
namespace="tagstudio-shades",
|
||||
name="Auburn",
|
||||
primary="#A13220",
|
||||
)
|
||||
olive = TagColorGroup(
|
||||
slug="olive",
|
||||
namespace="tagstudio-shades",
|
||||
name="Olive",
|
||||
primary="#4C652E",
|
||||
)
|
||||
dark_teal = TagColorGroup(
|
||||
slug="dark-teal",
|
||||
namespace="tagstudio-shades",
|
||||
name="Dark Teal",
|
||||
primary="#1F5E47",
|
||||
)
|
||||
navy = TagColorGroup(
|
||||
slug="navy",
|
||||
namespace="tagstudio-shades",
|
||||
name="Navy",
|
||||
primary="#104B98",
|
||||
)
|
||||
dark_lavender = TagColorGroup(
|
||||
slug="dark_lavender",
|
||||
namespace="tagstudio-shades",
|
||||
name="Dark Lavender",
|
||||
primary="#3D3B6C",
|
||||
)
|
||||
berry = TagColorGroup(
|
||||
slug="berry",
|
||||
namespace="tagstudio-shades",
|
||||
name="Berry",
|
||||
primary="#9F2AA7",
|
||||
)
|
||||
return [burgundy, auburn, olive, dark_teal, navy, dark_lavender, berry]
|
||||
|
||||
|
||||
def earth_tones() -> list[TagColorGroup]:
|
||||
dark_brown = TagColorGroup(
|
||||
slug="dark-brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Dark Brown",
|
||||
primary="#4C2315",
|
||||
)
|
||||
brown = TagColorGroup(
|
||||
slug="brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Brown",
|
||||
primary="#823216",
|
||||
)
|
||||
light_brown = TagColorGroup(
|
||||
slug="light-brown",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Light Brown",
|
||||
primary="#BE5B2D",
|
||||
)
|
||||
blonde = TagColorGroup(
|
||||
slug="blonde",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Blonde",
|
||||
primary="#EFC664",
|
||||
)
|
||||
peach = TagColorGroup(
|
||||
slug="peach",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Peach",
|
||||
primary="#F1C69C",
|
||||
)
|
||||
warm_gray = TagColorGroup(
|
||||
slug="warm-gray",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Warm Gray",
|
||||
primary="#625550",
|
||||
)
|
||||
cool_gray = TagColorGroup(
|
||||
slug="cool-gray",
|
||||
namespace="tagstudio-earth-tones",
|
||||
name="Cool Gray",
|
||||
primary="#515768",
|
||||
)
|
||||
return [dark_brown, brown, light_brown, blonde, peach, warm_gray, cool_gray]
|
||||
|
||||
|
||||
def grayscale() -> list[TagColorGroup]:
|
||||
black = TagColorGroup(
|
||||
slug="black",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Black",
|
||||
primary="#111018",
|
||||
)
|
||||
dark_gray = TagColorGroup(
|
||||
slug="dark-gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Dark Gray",
|
||||
primary="#242424",
|
||||
)
|
||||
gray = TagColorGroup(
|
||||
slug="gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Gray",
|
||||
primary="#53525A",
|
||||
)
|
||||
light_gray = TagColorGroup(
|
||||
slug="light-gray",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="Light Gray",
|
||||
primary="#AAAAAA",
|
||||
)
|
||||
white = TagColorGroup(
|
||||
slug="white",
|
||||
namespace="tagstudio-grayscale",
|
||||
name="White",
|
||||
primary="#F2F1F8",
|
||||
)
|
||||
return [black, dark_gray, gray, light_gray, white]
|
||||
|
||||
|
||||
def neon() -> list[TagColorGroup]:
|
||||
neon_red = TagColorGroup(
|
||||
slug="neon-red",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Red",
|
||||
primary="#180607",
|
||||
secondary="#E22C3C",
|
||||
color_border=True,
|
||||
)
|
||||
neon_red_orange = TagColorGroup(
|
||||
slug="neon-red-orange",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Red Orange",
|
||||
primary="#220905",
|
||||
secondary="#E83726",
|
||||
color_border=True,
|
||||
)
|
||||
neon_orange = TagColorGroup(
|
||||
slug="neon-orange",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Orange",
|
||||
primary="#1F0D05",
|
||||
secondary="#ED6022",
|
||||
color_border=True,
|
||||
)
|
||||
neon_amber = TagColorGroup(
|
||||
slug="neon-amber",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Amber",
|
||||
primary="#251507",
|
||||
secondary="#FA9A2C",
|
||||
color_border=True,
|
||||
)
|
||||
neon_yellow = TagColorGroup(
|
||||
slug="neon-yellow",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Yellow",
|
||||
primary="#2B1C0B",
|
||||
secondary="#FFD63D",
|
||||
color_border=True,
|
||||
)
|
||||
neon_lime = TagColorGroup(
|
||||
slug="neon-lime",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Lime",
|
||||
primary="#1B220C",
|
||||
secondary="#92E649",
|
||||
color_border=True,
|
||||
)
|
||||
neon_green = TagColorGroup(
|
||||
slug="neon-green",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Green",
|
||||
primary="#091610",
|
||||
secondary="#45D649",
|
||||
color_border=True,
|
||||
)
|
||||
neon_teal = TagColorGroup(
|
||||
slug="neon-teal",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Teal",
|
||||
primary="#09191D",
|
||||
secondary="#22D589",
|
||||
color_border=True,
|
||||
)
|
||||
neon_cyan = TagColorGroup(
|
||||
slug="neon-cyan",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Cyan",
|
||||
primary="#0B191C",
|
||||
secondary="#3DDBDB",
|
||||
color_border=True,
|
||||
)
|
||||
neon_blue = TagColorGroup(
|
||||
slug="neon-blue",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Blue",
|
||||
primary="#09101C",
|
||||
secondary="#3B87F0",
|
||||
color_border=True,
|
||||
)
|
||||
neon_indigo = TagColorGroup(
|
||||
slug="neon-indigo",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Indigo",
|
||||
primary="#150B24",
|
||||
secondary="#874FF5",
|
||||
color_border=True,
|
||||
)
|
||||
neon_purple = TagColorGroup(
|
||||
slug="neon-purple",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Purple",
|
||||
primary="#1E0B26",
|
||||
secondary="#BB4FF0",
|
||||
color_border=True,
|
||||
)
|
||||
neon_magenta = TagColorGroup(
|
||||
slug="neon-magenta",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Magenta",
|
||||
primary="#220A13",
|
||||
secondary="#F64680",
|
||||
color_border=True,
|
||||
)
|
||||
neon_pink = TagColorGroup(
|
||||
slug="neon-pink",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon Pink",
|
||||
primary="#210E15",
|
||||
secondary="#FF62AF",
|
||||
color_border=True,
|
||||
)
|
||||
neon_white = TagColorGroup(
|
||||
slug="neon-white",
|
||||
namespace="tagstudio-neon",
|
||||
name="Neon White",
|
||||
primary="#131315",
|
||||
secondary="#F2F1F8",
|
||||
color_border=True,
|
||||
)
|
||||
return [
|
||||
neon_red,
|
||||
neon_red_orange,
|
||||
neon_orange,
|
||||
neon_amber,
|
||||
neon_yellow,
|
||||
neon_lime,
|
||||
neon_green,
|
||||
neon_teal,
|
||||
neon_cyan,
|
||||
neon_blue,
|
||||
neon_indigo,
|
||||
neon_purple,
|
||||
neon_pink,
|
||||
neon_magenta,
|
||||
neon_white,
|
||||
]
|
||||
@@ -1,140 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
|
||||
|
||||
from tagstudio.core.library.alchemy.db import Base
|
||||
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.models import Entry, ValueType
|
||||
|
||||
|
||||
class BaseField(Base):
|
||||
__abstract__ = True
|
||||
|
||||
@declared_attr
|
||||
def id(self) -> Mapped[int]:
|
||||
return mapped_column(primary_key=True, autoincrement=True)
|
||||
|
||||
@declared_attr
|
||||
def type_key(self) -> Mapped[str]:
|
||||
return mapped_column(ForeignKey("value_type.key"))
|
||||
|
||||
@declared_attr
|
||||
def type(self) -> Mapped[ValueType]:
|
||||
return relationship(foreign_keys=[self.type_key], lazy=False) # type: ignore
|
||||
|
||||
@declared_attr
|
||||
def entry_id(self) -> Mapped[int]:
|
||||
return mapped_column(ForeignKey("entries.id"))
|
||||
|
||||
@declared_attr
|
||||
def entry(self) -> Mapped[Entry]:
|
||||
return relationship(foreign_keys=[self.entry_id]) # type: ignore
|
||||
|
||||
@declared_attr
|
||||
def position(self) -> Mapped[int]:
|
||||
return mapped_column(default=0)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__key())
|
||||
|
||||
def __key(self):
|
||||
raise NotImplementedError
|
||||
|
||||
value: Any
|
||||
|
||||
|
||||
class BooleanField(BaseField):
|
||||
__tablename__ = "boolean_fields"
|
||||
|
||||
value: Mapped[bool]
|
||||
|
||||
def __key(self):
|
||||
return (self.type, self.value)
|
||||
|
||||
def __eq__(self, value) -> bool:
|
||||
if isinstance(value, BooleanField):
|
||||
return self.__key() == value.__key()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TextField(BaseField):
|
||||
__tablename__ = "text_fields"
|
||||
|
||||
value: Mapped[str | None]
|
||||
|
||||
def __key(self) -> tuple:
|
||||
return self.type, self.value
|
||||
|
||||
def __eq__(self, value) -> bool:
|
||||
if isinstance(value, TextField):
|
||||
return self.__key() == value.__key()
|
||||
elif isinstance(value, DatetimeField):
|
||||
return False
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DatetimeField(BaseField):
|
||||
__tablename__ = "datetime_fields"
|
||||
|
||||
value: Mapped[str | None]
|
||||
|
||||
def __key(self):
|
||||
return (self.type, self.value)
|
||||
|
||||
def __eq__(self, value) -> bool:
|
||||
if isinstance(value, DatetimeField):
|
||||
return self.__key() == value.__key()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class DefaultField:
|
||||
id: int
|
||||
name: str
|
||||
type: FieldTypeEnum
|
||||
is_default: bool = field(default=False)
|
||||
|
||||
|
||||
class _FieldID(Enum):
|
||||
"""Only for bootstrapping content of DB table."""
|
||||
|
||||
TITLE = DefaultField(id=0, name="Title", type=FieldTypeEnum.TEXT_LINE, is_default=True)
|
||||
AUTHOR = DefaultField(id=1, name="Author", type=FieldTypeEnum.TEXT_LINE)
|
||||
ARTIST = DefaultField(id=2, name="Artist", type=FieldTypeEnum.TEXT_LINE)
|
||||
URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE)
|
||||
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_BOX)
|
||||
NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX)
|
||||
COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE)
|
||||
DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME)
|
||||
DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME)
|
||||
DATE_MODIFIED = DefaultField(id=12, name="Date Modified", type=FieldTypeEnum.DATETIME)
|
||||
DATE_TAKEN = DefaultField(id=13, name="Date Taken", type=FieldTypeEnum.DATETIME)
|
||||
DATE_PUBLISHED = DefaultField(id=14, name="Date Published", type=FieldTypeEnum.DATETIME)
|
||||
# ARCHIVED = DefaultField(id=15, name="Archived", type=CheckboxField.checkbox)
|
||||
# FAVORITE = DefaultField(id=16, name="Favorite", type=CheckboxField.checkbox)
|
||||
BOOK = DefaultField(id=17, name="Book", type=FieldTypeEnum.TEXT_LINE)
|
||||
COMIC = DefaultField(id=18, name="Comic", type=FieldTypeEnum.TEXT_LINE)
|
||||
SERIES = DefaultField(id=19, name="Series", type=FieldTypeEnum.TEXT_LINE)
|
||||
MANGA = DefaultField(id=20, name="Manga", type=FieldTypeEnum.TEXT_LINE)
|
||||
SOURCE = DefaultField(id=21, name="Source", type=FieldTypeEnum.TEXT_LINE)
|
||||
DATE_UPLOADED = DefaultField(id=22, name="Date Uploaded", type=FieldTypeEnum.DATETIME)
|
||||
DATE_RELEASED = DefaultField(id=23, name="Date Released", type=FieldTypeEnum.DATETIME)
|
||||
VOLUME = DefaultField(id=24, name="Volume", type=FieldTypeEnum.TEXT_LINE)
|
||||
ANTHOLOGY = DefaultField(id=25, name="Anthology", type=FieldTypeEnum.TEXT_LINE)
|
||||
MAGAZINE = DefaultField(id=26, name="Magazine", type=FieldTypeEnum.TEXT_LINE)
|
||||
PUBLISHER = DefaultField(id=27, name="Publisher", type=FieldTypeEnum.TEXT_LINE)
|
||||
GUEST_ARTIST = DefaultField(id=28, name="Guest Artist", type=FieldTypeEnum.TEXT_LINE)
|
||||
COMPOSER = DefaultField(id=29, name="Composer", type=FieldTypeEnum.TEXT_LINE)
|
||||
COMMENTS = DefaultField(id=30, name="Comments", type=FieldTypeEnum.TEXT_LINE)
|
||||
@@ -1,23 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tagstudio.core.library.alchemy.db import Base
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TagEntry(Base):
|
||||
__tablename__ = "tag_entries"
|
||||
|
||||
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
|
||||
entry_id: Mapped[int] = mapped_column(ForeignKey("entries.id"), primary_key=True)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,331 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from datetime import datetime as dt
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.library.alchemy.db import Base, PathType
|
||||
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
BaseField,
|
||||
BooleanField,
|
||||
DatetimeField,
|
||||
TextField,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
|
||||
|
||||
class Namespace(Base):
|
||||
__tablename__ = "namespaces"
|
||||
|
||||
namespace: Mapped[str] = mapped_column(primary_key=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
name: str,
|
||||
):
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
super().__init__()
|
||||
|
||||
|
||||
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")
|
||||
|
||||
def __init__(self, name: str, tag_id: int | None = None):
|
||||
self.name = name
|
||||
|
||||
if tag_id is not None:
|
||||
self.tag_id = tag_id
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
class TagColorGroup(Base):
|
||||
__tablename__ = "tag_colors"
|
||||
|
||||
slug: Mapped[str] = mapped_column(primary_key=True, nullable=False)
|
||||
namespace: Mapped[str] = mapped_column(
|
||||
ForeignKey("namespaces.namespace"), primary_key=True, nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column()
|
||||
primary: Mapped[str] = mapped_column(nullable=False)
|
||||
secondary: Mapped[str | None]
|
||||
color_border: Mapped[bool] = mapped_column(nullable=False, default=False)
|
||||
|
||||
# TODO: Determine if slug and namespace can be optional and generated/added here if needed.
|
||||
def __init__(
|
||||
self,
|
||||
slug: str,
|
||||
namespace: str,
|
||||
name: str,
|
||||
primary: str,
|
||||
secondary: str | None = None,
|
||||
color_border: bool = False,
|
||||
):
|
||||
self.slug = slug
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
self.primary = primary
|
||||
if secondary:
|
||||
self.secondary = secondary
|
||||
self.color_border = color_border
|
||||
super().__init__()
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
__tablename__ = "tags"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str]
|
||||
shorthand: Mapped[str | None]
|
||||
color_namespace: Mapped[str | None] = mapped_column()
|
||||
color_slug: Mapped[str | None] = mapped_column()
|
||||
color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
|
||||
is_category: Mapped[bool]
|
||||
icon: Mapped[str | None]
|
||||
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
|
||||
parent_tags: Mapped[set["Tag"]] = relationship(
|
||||
secondary=TagParent.__tablename__,
|
||||
primaryjoin="Tag.id == TagParent.child_id",
|
||||
secondaryjoin="Tag.id == TagParent.parent_id",
|
||||
back_populates="parent_tags",
|
||||
)
|
||||
disambiguation_id: Mapped[int | None]
|
||||
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
[color_namespace, color_slug], [TagColorGroup.namespace, TagColorGroup.slug]
|
||||
),
|
||||
{"sqlite_autoincrement": True},
|
||||
)
|
||||
|
||||
@property
|
||||
def parent_ids(self) -> list[int]:
|
||||
return [tag.id for tag in self.parent_tags]
|
||||
|
||||
@property
|
||||
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,
|
||||
id: int | None = None,
|
||||
shorthand: str | None = None,
|
||||
aliases: set[TagAlias] | None = None,
|
||||
parent_tags: set["Tag"] | None = None,
|
||||
icon: str | None = None,
|
||||
color_namespace: str | None = None,
|
||||
color_slug: str | None = None,
|
||||
disambiguation_id: int | None = None,
|
||||
is_category: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.aliases = aliases or set()
|
||||
self.parent_tags = parent_tags or set()
|
||||
self.color_namespace = color_namespace
|
||||
self.color_slug = color_slug
|
||||
self.icon = icon
|
||||
self.shorthand = shorthand
|
||||
self.disambiguation_id = disambiguation_id
|
||||
self.is_category = is_category
|
||||
self.id = id # pyright: ignore[reportAttributeAccessIssue]
|
||||
super().__init__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<Tag ID: {self.id} Name: {self.name}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.name < other.name
|
||||
|
||||
def __le__(self, other) -> bool:
|
||||
return self.name <= other.name
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
return self.name > other.name
|
||||
|
||||
def __ge__(self, other) -> bool:
|
||||
return self.name >= other.name
|
||||
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = "folders"
|
||||
|
||||
# TODO - implement this
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
path: Mapped[Path] = mapped_column(PathType, unique=True)
|
||||
uuid: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
|
||||
class Entry(Base):
|
||||
__tablename__ = "entries"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
||||
folder_id: Mapped[int] = mapped_column(ForeignKey("folders.id"))
|
||||
folder: Mapped[Folder] = relationship("Folder")
|
||||
|
||||
path: Mapped[Path] = mapped_column(PathType, unique=True)
|
||||
filename: Mapped[str] = mapped_column()
|
||||
suffix: Mapped[str] = mapped_column()
|
||||
date_created: Mapped[dt | None]
|
||||
date_modified: Mapped[dt | None]
|
||||
date_added: Mapped[dt | None]
|
||||
|
||||
tags: Mapped[set[Tag]] = relationship(secondary="tag_entries")
|
||||
|
||||
text_fields: Mapped[list[TextField]] = relationship(
|
||||
back_populates="entry",
|
||||
cascade="all, delete",
|
||||
)
|
||||
datetime_fields: Mapped[list[DatetimeField]] = relationship(
|
||||
back_populates="entry",
|
||||
cascade="all, delete",
|
||||
)
|
||||
|
||||
@property
|
||||
def fields(self) -> list[BaseField]:
|
||||
fields: list[BaseField] = []
|
||||
fields.extend(self.text_fields)
|
||||
fields.extend(self.datetime_fields)
|
||||
fields = sorted(fields, key=lambda field: field.type.position)
|
||||
return fields
|
||||
|
||||
@property
|
||||
def is_favorite(self) -> bool:
|
||||
return any(tag.id == TAG_FAVORITE for tag in self.tags)
|
||||
|
||||
@property
|
||||
def is_archived(self) -> bool:
|
||||
return any(tag.id == TAG_ARCHIVED for tag in self.tags)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
folder: Folder,
|
||||
fields: list[BaseField],
|
||||
id: int | None = None,
|
||||
date_created: dt | None = None,
|
||||
date_modified: dt | None = None,
|
||||
date_added: dt | None = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.folder = folder
|
||||
self.id = id # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.filename = path.name
|
||||
self.suffix = path.suffix.lstrip(".").lower()
|
||||
|
||||
# The date the file associated with this entry was created.
|
||||
# st_birthtime on Windows and Mac, st_ctime on Linux.
|
||||
self.date_created = date_created
|
||||
# The date the file associated with this entry was last modified: st_mtime.
|
||||
self.date_modified = date_modified
|
||||
# The date this entry was added to the library.
|
||||
self.date_added = date_added
|
||||
|
||||
for field in fields:
|
||||
if isinstance(field, TextField):
|
||||
self.text_fields.append(field)
|
||||
elif isinstance(field, DatetimeField):
|
||||
self.datetime_fields.append(field)
|
||||
else:
|
||||
raise ValueError(f"Invalid field type: {field}")
|
||||
|
||||
def has_tag(self, tag: Tag) -> bool:
|
||||
return tag in self.tags
|
||||
|
||||
def remove_tag(self, tag: Tag) -> None:
|
||||
"""Removes a Tag from the Entry."""
|
||||
self.tags.remove(tag)
|
||||
|
||||
|
||||
class ValueType(Base):
|
||||
"""Define Field Types in the Library.
|
||||
|
||||
Example:
|
||||
key: content_tags (this field is slugified `name`)
|
||||
name: Content Tags (this field is human readable name)
|
||||
kind: type of content (Text Line, Text Box, Tags, Datetime, Checkbox)
|
||||
is_default: Should the field be present in new Entry?
|
||||
order: position of the field widget in the Entry form
|
||||
|
||||
"""
|
||||
|
||||
__tablename__ = "value_type"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
type: Mapped[FieldTypeEnum] = mapped_column(default=FieldTypeEnum.TEXT_LINE)
|
||||
is_default: Mapped[bool]
|
||||
position: Mapped[int]
|
||||
|
||||
# add relations to other tables
|
||||
text_fields: Mapped[list[TextField]] = relationship("TextField", back_populates="type")
|
||||
datetime_fields: Mapped[list[DatetimeField]] = relationship(
|
||||
"DatetimeField", back_populates="type"
|
||||
)
|
||||
boolean_fields: Mapped[list[BooleanField]] = relationship("BooleanField", back_populates="type")
|
||||
|
||||
@property
|
||||
def as_field(self) -> BaseField:
|
||||
FieldClass = { # noqa: N806
|
||||
FieldTypeEnum.TEXT_LINE: TextField,
|
||||
FieldTypeEnum.TEXT_BOX: TextField,
|
||||
FieldTypeEnum.DATETIME: DatetimeField,
|
||||
FieldTypeEnum.BOOLEAN: BooleanField,
|
||||
}
|
||||
|
||||
return FieldClass[self.type](
|
||||
type_key=self.key,
|
||||
position=self.position,
|
||||
)
|
||||
|
||||
|
||||
@event.listens_for(ValueType, "before_insert")
|
||||
def slugify_field_key(mapper, connection, target):
|
||||
"""Slugify the field key before inserting into the database."""
|
||||
if not target.key:
|
||||
from tagstudio.core.library.alchemy.library import slugify
|
||||
|
||||
target.key = slugify(target.tag)
|
||||
|
||||
|
||||
# NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4
|
||||
# and is set to be removed in a future release.
|
||||
@deprecated("Use `Version` for storing version, and `ts_ignore` system for file exclusion.")
|
||||
class Preferences(Base):
|
||||
__tablename__ = "preferences"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
value: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
|
||||
|
||||
class Version(Base):
|
||||
__tablename__ = "versions"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
value: Mapped[int] = mapped_column(nullable=False, default=0)
|
||||
@@ -1,195 +0,0 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import ColumnElement, and_, distinct, func, or_, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql.operators import ilike_op
|
||||
|
||||
from tagstudio.core.library.alchemy.joins import TagEntry
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag, TagAlias
|
||||
from tagstudio.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories
|
||||
from tagstudio.core.query_lang.ast import (
|
||||
AST,
|
||||
ANDList,
|
||||
BaseVisitor,
|
||||
Constraint,
|
||||
ConstraintType,
|
||||
Not,
|
||||
ORList,
|
||||
Property,
|
||||
)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
else:
|
||||
Library = None # don't import library because of circular imports
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
TAG_CHILDREN_ID_QUERY = text("""
|
||||
WITH RECURSIVE ChildTags AS (
|
||||
SELECT :tag_id AS tag_id
|
||||
UNION
|
||||
SELECT tp.child_id AS tag_id
|
||||
FROM tag_parents tp
|
||||
INNER JOIN ChildTags c ON tp.parent_id = c.tag_id
|
||||
)
|
||||
SELECT tag_id FROM ChildTags;
|
||||
""")
|
||||
|
||||
|
||||
def get_filetype_equivalency_list(item: str) -> list[str] | set[str]:
|
||||
for s in FILETYPE_EQUIVALENTS:
|
||||
if item in s:
|
||||
return s
|
||||
return [item]
|
||||
|
||||
|
||||
class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
|
||||
def __init__(self, lib: Library) -> None:
|
||||
super().__init__()
|
||||
self.lib = lib
|
||||
|
||||
def visit_or_list(self, node: ORList) -> ColumnElement[bool]:
|
||||
tag_ids, bool_expressions = self.__separate_tags(node.elements, only_single=False)
|
||||
if len(tag_ids) > 0:
|
||||
bool_expressions.append(self.__entry_has_any_tags(tag_ids))
|
||||
return or_(*bool_expressions)
|
||||
|
||||
def visit_and_list(self, node: ANDList) -> ColumnElement[bool]:
|
||||
tag_ids, bool_expressions = self.__separate_tags(node.terms, only_single=True)
|
||||
if len(tag_ids) > 0:
|
||||
bool_expressions.append(self.__entry_has_all_tags(tag_ids))
|
||||
return and_(*bool_expressions)
|
||||
|
||||
def visit_constraint(self, node: Constraint) -> ColumnElement[bool]:
|
||||
"""Returns a Boolean Expression that is true, if the Entry satisfies the constraint."""
|
||||
if len(node.properties) != 0:
|
||||
raise NotImplementedError("Properties are not implemented yet") # TODO TSQLANG
|
||||
|
||||
if node.type == ConstraintType.Tag:
|
||||
return self.__entry_has_any_tags(self.__get_tag_ids(node.value))
|
||||
elif node.type == ConstraintType.TagID:
|
||||
return self.__entry_has_any_tags([int(node.value)])
|
||||
elif node.type == ConstraintType.Path:
|
||||
ilike = False
|
||||
glob = False
|
||||
|
||||
# Smartcase check
|
||||
if node.value == node.value.lower():
|
||||
ilike = True
|
||||
if node.value.startswith("*") or node.value.endswith("*"):
|
||||
glob = True
|
||||
|
||||
if ilike and glob:
|
||||
logger.info("ConstraintType.Path", ilike=True, glob=True)
|
||||
return func.lower(Entry.path).op("GLOB")(f"{node.value.lower()}")
|
||||
elif ilike:
|
||||
logger.info("ConstraintType.Path", ilike=True, glob=False)
|
||||
return ilike_op(Entry.path, f"%{node.value}%")
|
||||
elif glob:
|
||||
logger.info("ConstraintType.Path", ilike=False, glob=True)
|
||||
return Entry.path.op("GLOB")(node.value)
|
||||
else:
|
||||
logger.info(
|
||||
"ConstraintType.Path", ilike=False, glob=False, re=re.escape(node.value)
|
||||
)
|
||||
return Entry.path.regexp_match(re.escape(node.value))
|
||||
elif node.type == ConstraintType.MediaType:
|
||||
extensions: set[str] = set[str]()
|
||||
for media_cat in MediaCategories.ALL_CATEGORIES:
|
||||
if node.value == media_cat.name:
|
||||
extensions = extensions | media_cat.extensions
|
||||
break
|
||||
return Entry.suffix.in_(map(lambda x: x.replace(".", ""), extensions))
|
||||
elif node.type == ConstraintType.FileType:
|
||||
return or_(
|
||||
*[Entry.suffix.ilike(ft) for ft in get_filetype_equivalency_list(node.value)]
|
||||
)
|
||||
elif node.type == ConstraintType.Special: # noqa: SIM102 unnecessary once there is a second special constraint
|
||||
if node.value.lower() == "untagged":
|
||||
return ~Entry.id.in_(select(Entry.id).join(TagEntry))
|
||||
|
||||
# raise exception if Constraint stays unhandled
|
||||
raise NotImplementedError("This type of constraint is not implemented yet")
|
||||
|
||||
def visit_property(self, node: Property) -> ColumnElement[bool]:
|
||||
raise NotImplementedError("This should never be reached!")
|
||||
|
||||
def visit_not(self, node: Not) -> ColumnElement[bool]:
|
||||
return ~self.visit(node.child)
|
||||
|
||||
def __get_tag_ids(self, tag_name: str, include_children: bool = True) -> list[int]:
|
||||
"""Given a tag name find the ids of all tags that this name could refer to."""
|
||||
with Session(self.lib.engine) as session:
|
||||
tag_ids = list(
|
||||
session.scalars(
|
||||
select(Tag.id)
|
||||
.where(or_(Tag.name.ilike(tag_name), Tag.shorthand.ilike(tag_name)))
|
||||
.union(select(TagAlias.tag_id).where(TagAlias.name.ilike(tag_name)))
|
||||
)
|
||||
)
|
||||
if len(tag_ids) > 1:
|
||||
logger.debug(
|
||||
f'Tag Constraint "{tag_name}" is ambiguous, {len(tag_ids)} matching tags found',
|
||||
tag_ids=tag_ids,
|
||||
include_children=include_children,
|
||||
)
|
||||
if not include_children:
|
||||
return tag_ids
|
||||
outp = []
|
||||
for tag_id in tag_ids:
|
||||
outp.extend(list(session.scalars(TAG_CHILDREN_ID_QUERY, {"tag_id": tag_id})))
|
||||
return outp
|
||||
|
||||
def __separate_tags(
|
||||
self, terms: list[AST], only_single: bool = True
|
||||
) -> tuple[list[int], list[ColumnElement[bool]]]:
|
||||
tag_ids: list[int] = []
|
||||
bool_expressions: list[ColumnElement[bool]] = []
|
||||
|
||||
for term in terms:
|
||||
if isinstance(term, Constraint) and len(term.properties) == 0:
|
||||
match term.type:
|
||||
case ConstraintType.TagID:
|
||||
try:
|
||||
tag_ids.append(int(term.value))
|
||||
except ValueError:
|
||||
logger.error(
|
||||
"[SQLBoolExpressionBuilder] Could not cast value to an int Tag ID",
|
||||
value=term.value,
|
||||
)
|
||||
continue
|
||||
case ConstraintType.Tag:
|
||||
ids = self.__get_tag_ids(term.value)
|
||||
if not only_single:
|
||||
tag_ids.extend(ids)
|
||||
continue
|
||||
elif len(ids) == 1:
|
||||
tag_ids.append(ids[0])
|
||||
continue
|
||||
|
||||
bool_expressions.append(self.visit(term))
|
||||
return tag_ids, bool_expressions
|
||||
|
||||
def __entry_has_all_tags(self, tag_ids: list[int]) -> ColumnElement[bool]:
|
||||
"""Returns Binary Expression that is true if the Entry has all provided tag ids."""
|
||||
# Relational Division Query
|
||||
return Entry.id.in_(
|
||||
select(TagEntry.entry_id)
|
||||
.where(TagEntry.tag_id.in_(tag_ids))
|
||||
.group_by(TagEntry.entry_id)
|
||||
.having(func.count(distinct(TagEntry.tag_id)) == len(tag_ids))
|
||||
)
|
||||
|
||||
def __entry_has_any_tags(self, tag_ids: list[int]) -> ColumnElement[bool]:
|
||||
"""Returns Binary Expression that is true if the Entry has any of the provided tag ids."""
|
||||
return Entry.id.in_(
|
||||
select(TagEntry.entry_id).where(TagEntry.tag_id.in_(tag_ids)).distinct()
|
||||
)
|
||||
83
src/tagstudio/core/library/helpers/migration.py
Normal file
83
src/tagstudio/core/library/helpers/migration.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
def json_to_sql_color(json_color: str) -> tuple[str | None, str | None]:
|
||||
"""Convert a color string from a <=9.4 JSON library to a 9.5+ (namespace, slug) tuple."""
|
||||
json_color_ = json_color.lower()
|
||||
match json_color_:
|
||||
case "black":
|
||||
return ("tagstudio-grayscale", "black")
|
||||
case "dark gray":
|
||||
return ("tagstudio-grayscale", "dark-gray")
|
||||
case "gray":
|
||||
return ("tagstudio-grayscale", "gray")
|
||||
case "light gray":
|
||||
return ("tagstudio-grayscale", "light-gray")
|
||||
case "white":
|
||||
return ("tagstudio-grayscale", "white")
|
||||
case "light pink":
|
||||
return ("tagstudio-pastels", "light-pink")
|
||||
case "pink":
|
||||
return ("tagstudio-standard", "pink")
|
||||
case "magenta":
|
||||
return ("tagstudio-standard", "magenta")
|
||||
case "red":
|
||||
return ("tagstudio-standard", "red")
|
||||
case "red orange":
|
||||
return ("tagstudio-standard", "red-orange")
|
||||
case "salmon":
|
||||
return ("tagstudio-pastels", "salmon")
|
||||
case "orange":
|
||||
return ("tagstudio-standard", "orange")
|
||||
case "yellow orange":
|
||||
return ("tagstudio-standard", "amber")
|
||||
case "yellow":
|
||||
return ("tagstudio-standard", "yellow")
|
||||
case "mint":
|
||||
return ("tagstudio-pastels", "mint")
|
||||
case "lime":
|
||||
return ("tagstudio-standard", "lime")
|
||||
case "light green":
|
||||
return ("tagstudio-pastels", "light-green")
|
||||
case "green":
|
||||
return ("tagstudio-standard", "green")
|
||||
case "teal":
|
||||
return ("tagstudio-standard", "teal")
|
||||
case "cyan":
|
||||
return ("tagstudio-standard", "cyan")
|
||||
case "light blue":
|
||||
return ("tagstudio-pastels", "light-blue")
|
||||
case "blue":
|
||||
return ("tagstudio-standard", "blue")
|
||||
case "blue violet":
|
||||
return ("tagstudio-shades", "navy")
|
||||
case "violet":
|
||||
return ("tagstudio-standard", "indigo")
|
||||
case "purple":
|
||||
return ("tagstudio-standard", "purple")
|
||||
case "peach":
|
||||
return ("tagstudio-earth-tones", "peach")
|
||||
case "brown":
|
||||
return ("tagstudio-earth-tones", "brown")
|
||||
case "lavender":
|
||||
return ("tagstudio-pastels", "lavender")
|
||||
case "blonde":
|
||||
return ("tagstudio-earth-tones", "blonde")
|
||||
case "auburn":
|
||||
return ("tagstudio-shades", "auburn")
|
||||
case "light brown":
|
||||
return ("tagstudio-earth-tones", "light-brown")
|
||||
case "dark brown":
|
||||
return ("tagstudio-earth-tones", "dark-brown")
|
||||
case "cool gray":
|
||||
return ("tagstudio-earth-tones", "cool-gray")
|
||||
case "warm gray":
|
||||
return ("tagstudio-earth-tones", "warm-gray")
|
||||
case "olive":
|
||||
return ("tagstudio-shades", "olive")
|
||||
case "berry":
|
||||
return ("tagstudio-shades", "berry")
|
||||
case _:
|
||||
return (None, None)
|
||||
@@ -8,9 +8,7 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
from tagstudio.core.constants import TS_FOLDER_NAME
|
||||
from tagstudio.core.library.alchemy.fields import _FieldID
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, FieldID, Library
|
||||
from tagstudio.core.utils.missing_files import logger
|
||||
|
||||
|
||||
@@ -43,27 +41,27 @@ class TagStudioCore:
|
||||
return {}
|
||||
|
||||
if source == "twitter":
|
||||
info[_FieldID.DESCRIPTION] = json_dump["content"].strip()
|
||||
info[_FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
info[FieldID.DESCRIPTION] = json_dump["content"].strip()
|
||||
info[FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
elif source == "instagram":
|
||||
info[_FieldID.DESCRIPTION] = json_dump["description"].strip()
|
||||
info[_FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
info[FieldID.DESCRIPTION] = json_dump["description"].strip()
|
||||
info[FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
elif source == "artstation":
|
||||
info[_FieldID.TITLE] = json_dump["title"].strip()
|
||||
info[_FieldID.ARTIST] = json_dump["user"]["full_name"].strip()
|
||||
info[_FieldID.DESCRIPTION] = json_dump["description"].strip()
|
||||
info[_FieldID.TAGS] = json_dump["tags"]
|
||||
info[FieldID.TITLE] = json_dump["title"].strip()
|
||||
info[FieldID.ARTIST] = json_dump["user"]["full_name"].strip()
|
||||
info[FieldID.DESCRIPTION] = json_dump["description"].strip()
|
||||
info[FieldID.TAGS] = json_dump["tags"]
|
||||
# info["tags"] = [x for x in json_dump["mediums"]["name"]]
|
||||
info[_FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
info[FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
elif source == "newgrounds":
|
||||
# info["title"] = json_dump["title"]
|
||||
# info["artist"] = json_dump["artist"]
|
||||
# info["description"] = json_dump["description"]
|
||||
info[_FieldID.TAGS] = json_dump["tags"]
|
||||
info[_FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
info[_FieldID.ARTIST] = json_dump["user"].strip()
|
||||
info[_FieldID.DESCRIPTION] = json_dump["description"].strip()
|
||||
info[_FieldID.SOURCE] = json_dump["post_url"].strip()
|
||||
info[FieldID.TAGS] = json_dump["tags"]
|
||||
info[FieldID.DATE_PUBLISHED] = json_dump["date"]
|
||||
info[FieldID.ARTIST] = json_dump["user"].strip()
|
||||
info[FieldID.DESCRIPTION] = json_dump["description"].strip()
|
||||
info[FieldID.SOURCE] = json_dump["post_url"].strip()
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error handling sidecar file.", path=_filepath)
|
||||
|
||||
@@ -5,8 +5,7 @@ from pathlib import Path
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ from pathlib import Path
|
||||
import structlog
|
||||
from wcmatch import pathlib
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ from time import time
|
||||
import structlog
|
||||
from wcmatch import pathlib
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob
|
||||
from tagstudio.qt.helpers.silent_popen import silent_run
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.enums import TagClickActionOption
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.view.components.tag_box_view import TagBoxWidgetView
|
||||
|
||||
@@ -22,8 +22,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core import palette
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library, slugify
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import Library, TagColorGroup, slugify
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
|
||||
@@ -11,8 +11,12 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QLabel, QLineEdit, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.library import Library, ReservedNamespaceError, slugify
|
||||
from tagstudio.core.library.alchemy.models import Namespace
|
||||
from tagstudio.core.library.alchemy.library import (
|
||||
Library,
|
||||
Namespace,
|
||||
ReservedNamespaceError,
|
||||
slugify,
|
||||
)
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
|
||||
@@ -26,8 +26,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag, TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.modals.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.modals.tag_search import TagSearchModal
|
||||
|
||||
@@ -23,8 +23,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
|
||||
@@ -19,8 +19,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import Library, TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -7,8 +7,7 @@ import structlog
|
||||
from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.modals.tag_search import TagSearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -25,8 +25,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
|
||||
@@ -57,9 +57,7 @@ from tagstudio.core.library.alchemy.enums import (
|
||||
ItemType,
|
||||
SortingModeEnum,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.fields import _FieldID
|
||||
from tagstudio.core.library.alchemy.library import Library, LibraryStatus
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, FieldID, Library, LibraryStatus
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
@@ -1112,7 +1110,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
elif name == MacroID.BUILD_URL:
|
||||
url = TagStudioCore.build_url(entry, source)
|
||||
if url is not None:
|
||||
self.lib.add_field_to_entry(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:
|
||||
|
||||
@@ -7,8 +7,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.widgets.fields import FieldWidget
|
||||
from tagstudio.qt.widgets.tag import TagWidget
|
||||
|
||||
@@ -16,8 +16,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controller.widgets.preview.preview_thumb_controller import PreviewThumb
|
||||
|
||||
@@ -12,7 +12,7 @@ from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.modals.build_color import BuildColorPanel
|
||||
|
||||
@@ -30,10 +30,9 @@ from tagstudio.core.constants import (
|
||||
TS_FOLDER_NAME,
|
||||
)
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.core.library.alchemy import default_color_groups
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
from tagstudio.core.library.alchemy.library import Entry, TagAlias, TagParent
|
||||
from tagstudio.core.library.alchemy.library import Library as SqliteLibrary
|
||||
from tagstudio.core.library.alchemy.models import Entry, TagAlias
|
||||
from tagstudio.core.library.helpers.migration import json_to_sql_color
|
||||
from tagstudio.core.library.json.library import Library as JsonLibrary
|
||||
from tagstudio.core.library.json.library import Tag as JsonTag
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
@@ -779,7 +778,7 @@ class JsonMigrationModal(QObject):
|
||||
for tag in self.sql_lib.tags:
|
||||
tag_id = tag.id # Tag IDs start at 0
|
||||
sql_color = (tag.color_namespace, tag.color_slug)
|
||||
json_color = default_color_groups.json_to_sql_color(self.json_lib.get_tag(tag_id).color)
|
||||
json_color = json_to_sql_color(self.json_lib.get_tag(tag_id).color)
|
||||
|
||||
logger.info(
|
||||
"[Color Parity]",
|
||||
|
||||
@@ -24,14 +24,15 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
|
||||
from tagstudio.core.library.alchemy.library import (
|
||||
BaseField,
|
||||
DatetimeField,
|
||||
FieldTypeEnum,
|
||||
Entry,
|
||||
Library,
|
||||
Tag,
|
||||
TextField,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controller.components.tag_box_controller import TagBoxWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -12,7 +12,7 @@ from PySide6.QtGui import QAction, QColor, QEnterEvent, QFontMetrics
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.helpers.escape_text import escape_text
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -10,7 +10,7 @@ from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QAction, QColor, QEnterEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import TagColorGroup
|
||||
from tagstudio.qt.helpers.escape_text import escape_text
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
|
||||
@@ -11,7 +11,7 @@ from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.library.alchemy.library import TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
|
||||
@@ -16,8 +16,7 @@ CWD = Path(__file__).parent
|
||||
sys.path.insert(0, str(CWD.parent))
|
||||
|
||||
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library, Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.core.utils.dupe_files import DupeRegistry
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ from collections.abc import Callable
|
||||
|
||||
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.library.alchemy.library import Library, Tag, TagAlias
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel, CustomTableItem
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library, Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
@@ -16,8 +16,7 @@ from PySide6.QtWidgets import QMenu, QMenuBar
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption
|
||||
from tagstudio.core.library.alchemy.library import Library, LibraryStatus
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library, LibraryStatus
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
|
||||
from tagstudio.qt.modals.settings_panel import SettingsPanel
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.alchemy.library import Entry, Library
|
||||
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.library.alchemy.library import Library, Tag
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import structlog
|
||||
|
||||
from tagstudio.core.enums import DefaultEnum, LibraryPrefs
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
from tagstudio.core.library.alchemy.library import (
|
||||
Entry,
|
||||
FieldID,
|
||||
Library,
|
||||
Tag,
|
||||
TextField,
|
||||
_FieldID, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
|
||||
logger = structlog.get_logger()
|
||||
@@ -270,7 +271,7 @@ def test_mirror_entry_fields(library: Library, entry_full: Entry):
|
||||
path=Path("xxx"),
|
||||
fields=[
|
||||
TextField(
|
||||
type_key=_FieldID.NOTES.name,
|
||||
type_key=FieldID.NOTES.name,
|
||||
value="notes",
|
||||
position=0,
|
||||
)
|
||||
@@ -292,8 +293,8 @@ def test_mirror_entry_fields(library: Library, entry_full: Entry):
|
||||
# make sure fields are there after getting it from the library again
|
||||
assert len(entry.fields) == 2
|
||||
assert {x.type_key for x in entry.fields} == {
|
||||
_FieldID.TITLE.name,
|
||||
_FieldID.NOTES.name,
|
||||
FieldID.TITLE.name,
|
||||
FieldID.NOTES.name,
|
||||
}
|
||||
|
||||
|
||||
@@ -308,14 +309,14 @@ def test_merge_entries(library: Library):
|
||||
folder=folder,
|
||||
path=Path("a"),
|
||||
fields=[
|
||||
TextField(type_key=_FieldID.AUTHOR.name, value="Author McAuthorson", position=0),
|
||||
TextField(type_key=_FieldID.DESCRIPTION.name, value="test description", position=2),
|
||||
TextField(type_key=FieldID.AUTHOR.name, value="Author McAuthorson", position=0),
|
||||
TextField(type_key=FieldID.DESCRIPTION.name, value="test description", position=2),
|
||||
],
|
||||
)
|
||||
b = Entry(
|
||||
folder=folder,
|
||||
path=Path("b"),
|
||||
fields=[TextField(type_key=_FieldID.NOTES.name, value="test note", position=1)],
|
||||
fields=[TextField(type_key=FieldID.NOTES.name, value="test note", position=1)],
|
||||
)
|
||||
ids = library.add_entries([a, b])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user