Compare commits

..

2 Commits

Author SHA1 Message Date
Hosted Weblate 582ed06be4 Translated using Weblate (French)
Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Banizath <baznitah@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings
2026-05-09 04:49:06 +02:00
Hosted Weblate aff0eb823f Translated using Weblate (Russian)
Currently translated at 88.6% (321 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: werdei <48105614+WerDei@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings
2026-05-09 04:49:06 +02:00
40 changed files with 291 additions and 152 deletions
+2 -10
View File
@@ -128,15 +128,7 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
| Used From | Format | Location |
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds the `is_hidden` column to the `tags` table (default `0`). Used for excluding entries tagged with hidden tags from library searches.
- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default.
#### Version 104
| Used From | Format | Location |
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [#1298](https://github.com/TagStudioDev/TagStudio/pull/1298) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Removes the `preferences` table, after migrating the contained extension list to the .ts_ignore file, if necessary.
- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default.
-1
View File
@@ -46,7 +46,6 @@ pyinstaller = ["Pyinstaller~=6.13"]
pytest = [
"pytest==8.3.5",
"pytest-cov==6.1.1",
"pytest-mock==3.15.1",
"pytest-qt==4.4.0",
"syrupy==4.9.1",
]
+29
View File
@@ -3,6 +3,8 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import enum
from typing import Any
from uuid import uuid4
class SettingItems(str, enum.Enum):
@@ -55,3 +57,30 @@ class MacroID(enum.Enum):
BUILD_URL = "build_url"
MATCH = "match"
CLEAN_URL = "clean_url"
class DefaultEnum(enum.Enum):
"""Allow saving multiple identical values in property called .default."""
default: Any
def __new__(cls, value):
# Create the enum instance
obj = object.__new__(cls)
# make value random
obj._value_ = uuid4()
# assign the actual value into .default property
obj.default = value
return obj
@property
def value(self):
raise AttributeError("access the value via .default property instead")
# TODO: Remove DefaultEnum and LibraryPrefs classes once remaining values are removed.
class LibraryPrefs(DefaultEnum):
"""Library preferences with default value accessible via .default property."""
IS_EXCLUDE_LIST = True
EXTENSION_LIST = [".json", ".xmp", ".aae"]
@@ -8,9 +8,10 @@ from sqlalchemy import text
SQL_FILENAME: str = "ts_library.sqlite"
JSON_FILENAME: str = "ts_library.json"
DB_VERSION_LEGACY_KEY: str = "DB_VERSION"
DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 104
DB_VERSION: int = 103
TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
+109 -36
View File
@@ -16,7 +16,7 @@ from dataclasses import dataclass
from datetime import UTC, datetime
from os import makedirs
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from uuid import uuid4
from warnings import catch_warnings
@@ -53,6 +53,7 @@ from sqlalchemy.orm import (
noload,
selectinload,
)
from typing_extensions import deprecated
from tagstudio.core.constants import (
BACKUP_FOLDER_NAME,
@@ -66,11 +67,13 @@ from tagstudio.core.constants import (
TAG_META,
TS_FOLDER_NAME,
)
from tagstudio.core.enums import LibraryPrefs
from tagstudio.core.library.alchemy import default_color_groups
from tagstudio.core.library.alchemy.constants import (
DB_VERSION,
DB_VERSION_CURRENT_KEY,
DB_VERSION_INITIAL_KEY,
DB_VERSION_LEGACY_KEY,
JSON_FILENAME,
SQL_FILENAME,
TAG_CHILDREN_QUERY,
@@ -93,6 +96,7 @@ from tagstudio.core.library.alchemy.models import (
Entry,
Folder,
Namespace,
Preferences,
Tag,
TagAlias,
TagColorGroup,
@@ -100,7 +104,6 @@ from tagstudio.core.library.alchemy.models import (
Version,
)
from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder
from tagstudio.core.library.ignore import migrate_ext_list
from tagstudio.core.library.json.library import Library as JsonLibrary
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.translations import Translations
@@ -315,10 +318,9 @@ class Library:
value=v,
)
# extension include/exclude list
(unwrap(self.library_dir) / TS_FOLDER_NAME / IGNORE_NAME).write_text(
migrate_ext_list([x.strip(".") for x in json_lib.ext_list], json_lib.is_exclude_list)
)
# Preferences
self.set_prefs(LibraryPrefs.EXTENSION_LIST, [x.strip(".") for x in json_lib.ext_list])
self.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, json_lib.is_exclude_list)
end_time = time.time()
logger.info(f"Library Converted! ({format_timespan(end_time - start_time)})")
@@ -456,6 +458,15 @@ class Library:
# Ensure version rows are present
with catch_warnings(record=True):
# NOTE: The "Preferences" table is depreciated and will be removed in the future.
# The DB_VERSION is still being set to it in order to remain backwards-compatible
# with existing TagStudio versions until it is removed.
try:
session.add(Preferences(key=DB_VERSION_LEGACY_KEY, value=DB_VERSION))
session.commit()
except IntegrityError:
session.rollback()
try:
initial = DB_VERSION if is_new else 100
session.add(Version(key=DB_VERSION_INITIAL_KEY, value=initial))
@@ -469,6 +480,15 @@ class Library:
except IntegrityError:
session.rollback()
# TODO: Remove this "Preferences" system.
for pref in LibraryPrefs:
with catch_warnings(record=True):
try:
session.add(Preferences(key=pref.name, value=pref.default))
session.commit()
except IntegrityError:
session.rollback()
for field in FieldID:
try:
session.add(
@@ -536,9 +556,10 @@ class Library:
if loaded_db_version < 103:
# changes: tags
self.__apply_db103_migration(session)
if loaded_db_version < 104:
# changes: deletes preferences
self.__apply_db104_migrations(session, library_dir)
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
# TODO: do this in the migration step that will remove the preferences table
self.migrate_sql_to_ts_ignore(library_dir)
# Update DB_VERSION
if loaded_db_version < DB_VERSION:
@@ -711,30 +732,33 @@ class Library:
)
session.rollback()
def __apply_db104_migrations(self, session: Session, library_dir: Path):
"""Migrate DB from DB_VERSION 103 to 104."""
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
self.__migrate_sql_to_ts_ignore(library_dir)
session.execute(text("DROP TABLE preferences"))
session.commit()
def __migrate_sql_to_ts_ignore(self, library_dir: Path):
def migrate_sql_to_ts_ignore(self, library_dir: Path):
# Do not continue if existing '.ts_ignore' file is found
ts_ignore = library_dir / TS_FOLDER_NAME / IGNORE_NAME
if Path(ts_ignore).exists():
if Path(library_dir / TS_FOLDER_NAME / IGNORE_NAME).exists():
return
# Load legacy extension data
with Session(self.engine) as session:
extensions: list[str] = unwrap(
session.scalar(text("SELECT value FROM preferences WHERE key = 'EXTENSION_LIST'"))
)
is_exclude_list: bool = unwrap(
session.scalar(text("SELECT value FROM preferences WHERE key = 'IS_EXCLUDE_LIST'"))
)
# Create blank '.ts_ignore' file
ts_ignore_template = (
Path(__file__).parents[3] / "resources/templates/ts_ignore_template_blank.txt"
)
ts_ignore = library_dir / TS_FOLDER_NAME / IGNORE_NAME
try:
shutil.copy2(ts_ignore_template, ts_ignore)
except Exception as e:
logger.error("[ERROR][Library] Could not generate '.ts_ignore' file!", error=e)
with open(ts_ignore, "w") as f:
f.write(migrate_ext_list(extensions, is_exclude_list))
# Load legacy extension data
extensions: list[str] = self.prefs(LibraryPrefs.EXTENSION_LIST) # pyright: ignore
is_exclude_list: bool = self.prefs(LibraryPrefs.IS_EXCLUDE_LIST) # pyright: ignore
# Copy extensions to '.ts_ignore' file
if ts_ignore.exists():
with open(ts_ignore, "a") as f:
prefix = ""
if not is_exclude_list:
prefix = "!"
f.write("*\n")
f.writelines([f"{prefix}*.{x.lstrip('.')}\n" for x in extensions])
@property
def default_fields(self) -> list[BaseField]:
@@ -1832,20 +1856,19 @@ class Library:
engine = sqlalchemy.inspect(self.engine)
try:
# "Version" table added in DB_VERSION 101
if engine and engine.has_table("versions"):
if engine and engine.has_table("Version"):
version = session.scalar(select(Version).where(Version.key == key))
assert version
return version.value
# NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4
# and is set to be removed in a future release.
else:
return int(
unwrap(
session.scalar(
text("SELECT value FROM preferences WHERE key == 'DB_VERSION'")
)
)
pref_version = session.scalar(
select(Preferences).where(Preferences.key == DB_VERSION_LEGACY_KEY)
)
assert pref_version
assert isinstance(pref_version.value, int)
return pref_version.value
except Exception:
return 0
@@ -1863,10 +1886,60 @@ class Library:
version.value = value
session.add(version)
session.commit()
# If a depreciated "Preferences" table is found, update the version value to be read
# by older TagStudio versions.
engine = sqlalchemy.inspect(self.engine)
if engine and engine.has_table("Preferences"):
pref = unwrap(
session.scalar(
select(Preferences).where(Preferences.key == DB_VERSION_LEGACY_KEY)
)
)
pref.value = value # pyright: ignore
session.add(pref)
session.commit()
except (IntegrityError, AssertionError) as e:
logger.error("[Library][ERROR] Couldn't add default tag color namespaces", error=e)
session.rollback()
# TODO: Remove this once the 'preferences' table is removed.
@deprecated("Use `get_version() for version and `ts_ignore` system for extension exclusion.")
def prefs(self, key: str | LibraryPrefs): # pyright: ignore[reportUnknownParameterType]
# load given item from Preferences table
with Session(self.engine) as session:
if isinstance(key, LibraryPrefs):
return unwrap(
session.scalar(select(Preferences).where(Preferences.key == key.name))
).value # pyright: ignore[reportUnknownVariableType]
else:
return unwrap(
session.scalar(select(Preferences).where(Preferences.key == key))
).value # pyright: ignore[reportUnknownVariableType]
# TODO: Remove this once the 'preferences' table is removed.
@deprecated("Use `get_version() for version and `ts_ignore` system for extension exclusion.")
def set_prefs(self, key: str | LibraryPrefs, value: Any) -> None: # pyright: ignore[reportExplicitAny]
# set given item in Preferences table
with Session(self.engine) as session:
# load existing preference and update value
stuff = session.scalars(select(Preferences))
logger.info([x.key for x in list(stuff)])
pref: Preferences = unwrap(
session.scalar(
select(Preferences).where(
Preferences.key == (key.name if isinstance(key, LibraryPrefs) else key)
)
)
)
logger.info("loading pref", pref=pref, key=key, value=value)
pref.value = value
session.add(pref)
session.commit()
# TODO - try/except
def mirror_entry_fields(self, *entries: Entry) -> None:
"""Mirror fields among multiple Entry items."""
fields = {}
+12 -1
View File
@@ -6,8 +6,9 @@ from datetime import datetime as dt
from pathlib import Path
from typing import override
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, event
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
@@ -326,6 +327,16 @@ def slugify_field_key(mapper, connection, target): # pyright: ignore
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"
-17
View File
@@ -91,23 +91,6 @@ def ignore_to_glob(ignore_patterns: list[str]) -> list[str]:
return glob_patterns
def migrate_ext_list(exts: list[str], is_exclude_list: bool) -> str:
# read template
ts_ignore_template = (
Path(__file__).parents[2] / "resources/templates/ts_ignore_template_blank.txt"
)
with open(ts_ignore_template) as f:
out = f.read()
# actual conversion
prefix = ""
if not is_exclude_list:
prefix = "!"
out += "*\n"
out += "\n".join([f"{prefix}*.{x.lstrip('.')}\n" for x in exts])
return out
class Ignore(metaclass=Singleton):
"""Class for processing and managing glob-like file ignore file patterns."""
+7 -20
View File
@@ -193,31 +193,18 @@ class TagStudioCore:
@staticmethod
@lru_cache(maxsize=1)
def get_most_recent_release_version() -> str | None:
"""Get the version of the most recent GitHub release."""
try:
resp = requests.get(
"https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest"
)
except Exception as e:
logger.error("Error getting most recent GitHub release.", error=e)
return None
if resp.status_code != 200:
logger.error("Error getting most recent GitHub release.", status_code=resp.status_code)
return None
def get_most_recent_release_version() -> str:
"""Get the version of the most recent Github release."""
resp = requests.get("https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest")
assert resp.status_code == 200, "Could not fetch information on latest release."
data = resp.json()
tag: str = data["tag_name"]
if not tag.startswith("v"):
logger.error("Unexpected tag format.", tag=tag)
return None
assert tag.startswith("v")
version = tag[1:]
# the assertion does not allow for prerelease/build,
# the assert does not allow for prerelease/build,
# because the latest release should never have them
if re.match(r"^\d+\.\d+\.\d+$", version) is None:
logger.error("Invalid version format.", version=version)
return None
assert re.match(r"^\d+\.\d+\.\d+$", version) is not None, "Invalid version format."
return version
@@ -4,7 +4,6 @@ from PySide6.QtWidgets import QMessageBox
from tagstudio.core.constants import VERSION
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
@@ -31,7 +30,7 @@ class OutOfDateMessageBox(QMessageBox):
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
latest_release_version = unwrap(TagStudioCore.get_most_recent_release_version())
latest_release_version = TagStudioCore.get_most_recent_release_version()
status = Translations.format(
"version_modal.status",
installed_version=f"<span style='color:{red}'>{VERSION}</span>",
+1 -2
View File
@@ -21,7 +21,6 @@ from PySide6.QtWidgets import (
from tagstudio.core.constants import VERSION, VERSION_BRANCH
from tagstudio.core.enums import Theme
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored import ffmpeg
from tagstudio.qt.resource_manager import ResourceManager
@@ -107,7 +106,7 @@ class AboutModal(QWidget):
# Version
version_title = QLabel("Version")
most_recent_release = unwrap(TagStudioCore.get_most_recent_release_version(), "UNKNOWN")
most_recent_release = TagStudioCore.get_most_recent_release_version()
version_content_style = self.form_content_style
if most_recent_release == VERSION:
version_content = QLabel(f"{VERSION}")
+48 -38
View File
@@ -8,7 +8,6 @@ from pathlib import Path
from typing import cast
import structlog
import wcmatch.fnmatch as fnmatch
from PySide6.QtCore import QObject, Qt, QThreadPool, Signal
from PySide6.QtWidgets import (
QApplication,
@@ -25,19 +24,18 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from tagstudio.core.constants import (
IGNORE_NAME,
LEGACY_TAG_FIELD_IDS,
TAG_ARCHIVED,
TAG_FAVORITE,
TAG_META,
TS_FOLDER_NAME,
)
from tagstudio.core.enums import LibraryPrefs
from tagstudio.core.library.alchemy import default_color_groups
from tagstudio.core.library.alchemy.constants import SQL_FILENAME
from tagstudio.core.library.alchemy.joins import TagParent
from tagstudio.core.library.alchemy.library import Library as SqliteLibrary
from tagstudio.core.library.alchemy.models import Entry, TagAlias
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob
from tagstudio.core.library.json.library import Library as JsonLibrary
from tagstudio.core.library.json.library import Tag as JsonTag
from tagstudio.core.utils.types import unwrap
@@ -74,6 +72,8 @@ class JsonMigrationModal(QObject):
self.old_entry_count: int = 0
self.old_tag_count: int = 0
self.old_ext_count: int = 0
self.old_ext_type: bool = None # pyright: ignore[reportAttributeAccessIssue]
self.field_parity: bool = False
self.path_parity: bool = False
@@ -82,7 +82,6 @@ class JsonMigrationModal(QObject):
self.subtag_parity: bool = False
self.alias_parity: bool = False
self.color_parity: bool = False
self.ext_parity: bool = False
self.init_page_info()
self.init_page_convert()
@@ -130,7 +129,8 @@ class JsonMigrationModal(QObject):
parent_tags_text: str = tab + Translations["json_migration.heading.parent_tags"]
aliases_text: str = tab + Translations["json_migration.heading.aliases"]
colors_text: str = tab + Translations["json_migration.heading.colors"]
ext_parity_text: str = Translations["json_migration.heading.extensions"]
ext_text: str = Translations["json_migration.heading.file_extension_list"]
ext_type_text: str = Translations["json_migration.heading.extension_list_type"]
desc_text: str = Translations["json_migration.description"]
path_parity_text: str = tab + Translations["json_migration.heading.paths"]
field_parity_text: str = tab + Translations["library_info.stats.fields"]
@@ -145,6 +145,7 @@ class JsonMigrationModal(QObject):
self.aliases_row: int = 7
self.colors_row: int = 8
self.ext_row: int = 9
self.ext_type_row: int = 10
old_lib_container: QWidget = QWidget()
old_lib_layout: QVBoxLayout = QVBoxLayout(old_lib_container)
@@ -165,7 +166,8 @@ class JsonMigrationModal(QObject):
self.old_content_layout.addWidget(QLabel(parent_tags_text), self.parent_tags_row, 0)
self.old_content_layout.addWidget(QLabel(aliases_text), self.aliases_row, 0)
self.old_content_layout.addWidget(QLabel(colors_text), self.colors_row, 0)
self.old_content_layout.addWidget(QLabel(ext_parity_text), self.ext_row, 0)
self.old_content_layout.addWidget(QLabel(ext_text), self.ext_row, 0)
self.old_content_layout.addWidget(QLabel(ext_type_text), self.ext_type_row, 0)
old_entry_count: QLabel = QLabel()
old_entry_count.setAlignment(Qt.AlignmentFlag.AlignRight)
@@ -185,8 +187,10 @@ class JsonMigrationModal(QObject):
old_alias_value.setAlignment(Qt.AlignmentFlag.AlignRight)
old_color_value: QLabel = QLabel()
old_color_value.setAlignment(Qt.AlignmentFlag.AlignRight)
old_ext_value: QLabel = QLabel()
old_ext_value.setAlignment(Qt.AlignmentFlag.AlignRight)
old_ext_count: QLabel = QLabel()
old_ext_count.setAlignment(Qt.AlignmentFlag.AlignRight)
old_ext_type: QLabel = QLabel()
old_ext_type.setAlignment(Qt.AlignmentFlag.AlignRight)
self.old_content_layout.addWidget(old_entry_count, self.entries_row, 1)
self.old_content_layout.addWidget(old_path_value, self.path_row, 1)
@@ -197,7 +201,8 @@ class JsonMigrationModal(QObject):
self.old_content_layout.addWidget(old_subtag_value, self.parent_tags_row, 1)
self.old_content_layout.addWidget(old_alias_value, self.aliases_row, 1)
self.old_content_layout.addWidget(old_color_value, self.colors_row, 1)
self.old_content_layout.addWidget(old_ext_value, self.ext_row, 1)
self.old_content_layout.addWidget(old_ext_count, self.ext_row, 1)
self.old_content_layout.addWidget(old_ext_type, self.ext_type_row, 1)
self.old_content_layout.addWidget(QLabel(), self.path_row, 2)
self.old_content_layout.addWidget(QLabel(), self.fields_row, 2)
@@ -206,7 +211,6 @@ class JsonMigrationModal(QObject):
self.old_content_layout.addWidget(QLabel(), self.parent_tags_row, 2)
self.old_content_layout.addWidget(QLabel(), self.aliases_row, 2)
self.old_content_layout.addWidget(QLabel(), self.colors_row, 2)
self.old_content_layout.addWidget(QLabel(), self.ext_row, 2)
old_lib_layout.addWidget(old_content_container)
@@ -229,7 +233,8 @@ class JsonMigrationModal(QObject):
self.new_content_layout.addWidget(QLabel(parent_tags_text), self.parent_tags_row, 0)
self.new_content_layout.addWidget(QLabel(aliases_text), self.aliases_row, 0)
self.new_content_layout.addWidget(QLabel(colors_text), self.colors_row, 0)
self.new_content_layout.addWidget(QLabel(ext_parity_text), self.ext_row, 0)
self.new_content_layout.addWidget(QLabel(ext_text), self.ext_row, 0)
self.new_content_layout.addWidget(QLabel(ext_type_text), self.ext_type_row, 0)
new_entry_count: QLabel = QLabel()
new_entry_count.setAlignment(Qt.AlignmentFlag.AlignRight)
@@ -249,8 +254,10 @@ class JsonMigrationModal(QObject):
alias_parity_value.setAlignment(Qt.AlignmentFlag.AlignRight)
new_color_value: QLabel = QLabel()
new_color_value.setAlignment(Qt.AlignmentFlag.AlignRight)
ext_parity_value: QLabel = QLabel()
ext_parity_value.setAlignment(Qt.AlignmentFlag.AlignRight)
new_ext_count: QLabel = QLabel()
new_ext_count.setAlignment(Qt.AlignmentFlag.AlignRight)
new_ext_type: QLabel = QLabel()
new_ext_type.setAlignment(Qt.AlignmentFlag.AlignRight)
self.new_content_layout.addWidget(new_entry_count, self.entries_row, 1)
self.new_content_layout.addWidget(path_parity_value, self.path_row, 1)
@@ -261,7 +268,8 @@ class JsonMigrationModal(QObject):
self.new_content_layout.addWidget(subtag_parity_value, self.parent_tags_row, 1)
self.new_content_layout.addWidget(alias_parity_value, self.aliases_row, 1)
self.new_content_layout.addWidget(new_color_value, self.colors_row, 1)
self.new_content_layout.addWidget(ext_parity_value, self.ext_row, 1)
self.new_content_layout.addWidget(new_ext_count, self.ext_row, 1)
self.new_content_layout.addWidget(new_ext_type, self.ext_type_row, 1)
self.new_content_layout.addWidget(QLabel(), self.entries_row, 2)
self.new_content_layout.addWidget(QLabel(), self.path_row, 2)
@@ -273,6 +281,7 @@ class JsonMigrationModal(QObject):
self.new_content_layout.addWidget(QLabel(), self.aliases_row, 2)
self.new_content_layout.addWidget(QLabel(), self.colors_row, 2)
self.new_content_layout.addWidget(QLabel(), self.ext_row, 2)
self.new_content_layout.addWidget(QLabel(), self.ext_type_row, 2)
new_lib_layout.addWidget(new_content_container)
@@ -325,6 +334,8 @@ class JsonMigrationModal(QObject):
# Update JSON UI
self.update_json_entry_count(len(self.json_lib.entries))
self.update_json_tag_count(len(self.json_lib.tags))
self.update_json_ext_count(len(self.json_lib.ext_list))
self.update_json_ext_type(self.json_lib.is_exclude_list)
self.migration_progress(skip_ui=skip_ui)
self.is_migration_initialized = True
@@ -418,7 +429,6 @@ class JsonMigrationModal(QObject):
check_set.add(self.check_subtag_parity())
check_set.add(self.check_alias_parity())
check_set.add(self.check_color_parity())
check_set.add(self.check_ignore_parity())
if False not in check_set:
yield Translations["json_migration.migration_complete"]
else:
@@ -445,7 +455,6 @@ class JsonMigrationModal(QObject):
self.update_parity_value(self.parent_tags_row, self.subtag_parity)
self.update_parity_value(self.aliases_row, self.alias_parity)
self.update_parity_value(self.colors_row, self.color_parity)
self.update_parity_value(self.ext_row, self.ext_parity)
self.sql_lib.close()
def update_sql_value_ui(self, show_msg_box: bool = True):
@@ -460,6 +469,16 @@ class JsonMigrationModal(QObject):
len(self.sql_lib.tags),
self.old_tag_count,
)
self.update_sql_value(
self.ext_row,
len(self.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST)),
self.old_ext_count,
)
self.update_sql_value(
self.ext_type_row,
self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST), # pyright: ignore[reportArgumentType]
self.old_ext_type,
)
logger.info("Parity check complete!")
if self.discrepancies:
logger.warning("Discrepancies found:")
@@ -491,6 +510,16 @@ class JsonMigrationModal(QObject):
label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # type:ignore
label.setText(self.color_value_default(value))
def update_json_ext_count(self, value: int):
self.old_ext_count = value
label: QLabel = self.old_content_layout.itemAtPosition(self.ext_row, 1).widget() # type:ignore
label.setText(self.color_value_default(value))
def update_json_ext_type(self, value: bool):
self.old_ext_type = value
label: QLabel = self.old_content_layout.itemAtPosition(self.ext_type_row, 1).widget() # type:ignore
label.setText(self.color_value_default(value))
def update_sql_value(self, row: int, value: int | bool, old_value: int | bool):
label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # type:ignore
warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # type:ignore
@@ -519,28 +548,6 @@ class JsonMigrationModal(QObject):
color = green if old_value == new_value else red
return str(f"<b><a style='color: {color}'>{new_value}</a></b>")
def assert_ignore_parity(self) -> None:
compiled_pats = fnmatch.compile(
ignore_to_glob(
Ignore._load_ignore_file(
unwrap(self.json_lib.library_dir) / TS_FOLDER_NAME / IGNORE_NAME
)
),
PATH_GLOB_FLAGS,
) # copied from Ignore.get_patterns since that method modifies singleton state
path = self.json_lib.library_dir / "filename"
for ext in self.json_lib.ext_list:
assert compiled_pats.match(str(path / ext)) == self.json_lib.is_exclude_list
assert compiled_pats.match(str(path / ".not_a_real_ext")) != self.json_lib.is_exclude_list
def check_ignore_parity(self) -> bool:
try:
self.assert_ignore_parity()
self.ext_parity = True
except AssertionError:
self.ext_parity = False
return self.ext_parity
def check_field_parity(self) -> bool:
"""Check if all JSON field and tag data matches the new SQL data."""
@@ -664,6 +671,9 @@ class JsonMigrationModal(QObject):
self.subtag_parity = True
return self.subtag_parity
def check_ext_type(self) -> bool:
return self.json_lib.is_exclude_list == self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST)
def check_alias_parity(self) -> bool:
"""Check if all JSON aliases match the new SQL aliases."""
with Session(self.sql_lib.engine) as session:
+1 -2
View File
@@ -613,8 +613,7 @@ class QtDriver(DriverMixin, QObject):
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
FfmpegMissingMessageBox().show()
latest_version = TagStudioCore.get_most_recent_release_version()
if latest_version and is_version_outdated(VERSION, latest_version):
if is_version_outdated(VERSION, TagStudioCore.get_most_recent_release_version()):
OutOfDateMessageBox().exec()
self.app.exec()
@@ -1 +0,0 @@
{}
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Aliase:",
"json_migration.heading.colors": "Farben:",
"json_migration.heading.differ": "Diskrepanz",
"json_migration.heading.extension_list_type": "Erweiterungslistentyp:",
"json_migration.heading.file_extension_list": "Liste der Dateiendungen:",
"json_migration.heading.match": "Übereinstimmend",
"json_migration.heading.names": "Namen:",
"json_migration.heading.parent_tags": "Übergeordnete Tags:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Ψευδώνυμα:",
"json_migration.heading.colors": "Χρώματα:",
"json_migration.heading.differ": "Ασυμφωνία",
"json_migration.heading.extension_list_type": "Τύπος λίστας επεκτάσεων:",
"json_migration.heading.file_extension_list": "Λίστα επεκτάσεων αρχείων:",
"json_migration.heading.match": "Αντιστοιχίστηκε",
"json_migration.heading.names": "Όνομα:"
}
+2 -1
View File
@@ -157,7 +157,8 @@
"json_migration.heading.aliases": "Aliases:",
"json_migration.heading.colors": "Colors:",
"json_migration.heading.differ": "Discrepancy",
"json_migration.heading.extensions": "Extensions:",
"json_migration.heading.extension_list_type": "Extension List Type:",
"json_migration.heading.file_extension_list": "File Extension List:",
"json_migration.heading.match": "Matched",
"json_migration.heading.names": "Names:",
"json_migration.heading.parent_tags": "Parent Tags:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Alias:",
"json_migration.heading.colors": "Colores:",
"json_migration.heading.differ": "Discrepancia",
"json_migration.heading.extension_list_type": "Tipo de lista de extensión:",
"json_migration.heading.file_extension_list": "Lista de extensiones de archivos:",
"json_migration.heading.match": "Igualado",
"json_migration.heading.names": "Nombres:",
"json_migration.heading.parent_tags": "Etiquetas principales:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Aliaksia:",
"json_migration.heading.colors": "Värit:",
"json_migration.heading.differ": "Poikkeavuus",
"json_migration.heading.extension_list_type": "Laajennusluettelon tyyppi:",
"json_migration.heading.file_extension_list": "Tiedostopääte lista:",
"json_migration.heading.match": "Yhdistetty",
"json_migration.heading.names": "Nimet:",
"json_migration.heading.parent_tags": "Päätunnukset:",
@@ -140,6 +140,8 @@
"json_migration.heading.aliases": "Mga alyas:",
"json_migration.heading.colors": "Mga kulay:",
"json_migration.heading.differ": "May pagkakaiba",
"json_migration.heading.extension_list_type": "Uri ng Listahan ng Extension:",
"json_migration.heading.file_extension_list": "Listahan ng Mga File Extension:",
"json_migration.heading.match": "Tumutugma",
"json_migration.heading.names": "Mga pangalan:",
"json_migration.heading.parent_tags": "Mga parent tag:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Alias :",
"json_migration.heading.colors": "Couleurs :",
"json_migration.heading.differ": "Divergence",
"json_migration.heading.extension_list_type": "Type de liste d'extension :",
"json_migration.heading.file_extension_list": "Liste des extensions de fichiers :",
"json_migration.heading.match": "Correspondant",
"json_migration.heading.names": "Noms :",
"json_migration.heading.parent_tags": "Tags Parents :",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Áljelek:",
"json_migration.heading.colors": "Színek:",
"json_migration.heading.differ": "Eltérés",
"json_migration.heading.extension_list_type": "Kiterjesztési lista típusa:",
"json_migration.heading.file_extension_list": "Fájlkiterjesztési lista:",
"json_migration.heading.match": "Egységesítve",
"json_migration.heading.names": "Nevek:",
"json_migration.heading.parent_tags": "Szülőcímkék:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Alias:",
"json_migration.heading.colors": "Colori:",
"json_migration.heading.differ": "Discrepanze",
"json_migration.heading.extension_list_type": "Tipo di Lista di Entensioni:",
"json_migration.heading.file_extension_list": "Elenco Estensioni dei File:",
"json_migration.heading.match": "Abbinato",
"json_migration.heading.names": "Nomi:",
"json_migration.heading.parent_tags": "Etichette Genitore:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "エイリアス:",
"json_migration.heading.colors": "色:",
"json_migration.heading.differ": "差異",
"json_migration.heading.extension_list_type": "拡張子リストの種類:",
"json_migration.heading.file_extension_list": "ファイルの拡張子リスト:",
"json_migration.heading.match": "一致",
"json_migration.heading.names": "名前:",
"json_migration.heading.parent_tags": "親タグ:",
@@ -148,6 +148,8 @@
"json_migration.heading.aliases": "Alternative navn:",
"json_migration.heading.colors": "Farger:",
"json_migration.heading.differ": "Avvik",
"json_migration.heading.extension_list_type": "Type av Utvidelsesliste:",
"json_migration.heading.file_extension_list": "Filutvidelse Liste:",
"json_migration.heading.match": "Matchet",
"json_migration.heading.names": "Navn:",
"json_migration.heading.parent_tags": "Overordnede Etiketter:",
@@ -139,6 +139,8 @@
"json_migration.heading.aliases": "Zastępcze nazwy:",
"json_migration.heading.colors": "Kolory:",
"json_migration.heading.differ": "Niezgodność",
"json_migration.heading.extension_list_type": "Typ listy rozszerzeń:",
"json_migration.heading.file_extension_list": "Lista rozszerzeń plików:",
"json_migration.heading.match": "Dopasowane",
"json_migration.heading.names": "Nazwy:",
"json_migration.heading.parent_tags": "Tagi nadrzędne:",
@@ -136,6 +136,8 @@
"json_migration.heading.aliases": "Pseudônimos:",
"json_migration.heading.colors": "Cores:",
"json_migration.heading.differ": "Discrepância",
"json_migration.heading.extension_list_type": "Tipo de Lista de Extensão:",
"json_migration.heading.file_extension_list": "Lista de Extensão de Ficheiro:",
"json_migration.heading.match": "Combinado",
"json_migration.heading.names": "Nomes:",
"json_migration.heading.parent_tags": "Tags Pai:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "Pseudônimos:",
"json_migration.heading.colors": "Cores:",
"json_migration.heading.differ": "Discrepância",
"json_migration.heading.extension_list_type": "Lista dos tipos de Extensão:",
"json_migration.heading.file_extension_list": "Lista de Extensão de Arquivo:",
"json_migration.heading.match": "Correspondido",
"json_migration.heading.names": "Nomes:",
"json_migration.heading.parent_tags": "Tags Pai:",
@@ -137,6 +137,8 @@
"json_migration.heading.aliases": "Andrnamae:",
"json_migration.heading.colors": "Varge:",
"json_migration.heading.differ": "Tchigauzma",
"json_migration.heading.extension_list_type": "Fal fu taksanting tumam:",
"json_migration.heading.file_extension_list": "Tumam fu mlafufal:",
"json_migration.heading.match": "Finnajena sama",
"json_migration.heading.names": "Namae:",
"json_migration.heading.parent_tags": "Atama festaretol:",
@@ -148,6 +148,8 @@
"json_migration.heading.aliases": "Псевдонимы:",
"json_migration.heading.colors": "Цвета:",
"json_migration.heading.differ": "Несоответствие",
"json_migration.heading.extension_list_type": "Тип списка расширений:",
"json_migration.heading.file_extension_list": "Список расширений файлов:",
"json_migration.heading.match": "Совпало",
"json_migration.heading.names": "Имена:",
"json_migration.heading.parent_tags": "Родительские теги:",
@@ -157,6 +157,8 @@
"json_migration.heading.aliases": "மாற்றுப்பெயர்கள்:",
"json_migration.heading.colors": "நிறங்கள்:",
"json_migration.heading.differ": "முரண்பாடு",
"json_migration.heading.extension_list_type": "நீட்டிப்பு பட்டியல் வகை:",
"json_migration.heading.file_extension_list": "கோப்பு நீட்டிப்பு பட்டியல்:",
"json_migration.heading.match": "பொருந்தியது",
"json_migration.heading.names": "பெயர்கள்:",
"json_migration.heading.parent_tags": "பெற்றோர் குறிச்சொற்கள்:",
@@ -154,6 +154,7 @@
"json_migration.heading.aliases": "nimi ante:",
"json_migration.heading.colors": "kule:",
"json_migration.heading.differ": "ante ike",
"json_migration.heading.file_extension_list": "kulupu pi namako lipu:",
"json_migration.heading.match": "sama",
"json_migration.heading.names": "nimi:",
"json_migration.heading.parent_tags": "poki mama:",
@@ -136,6 +136,8 @@
"json_migration.heading.aliases": "Takma Adlar:",
"json_migration.heading.colors": "Renkler:",
"json_migration.heading.differ": "Uyuşmazlık",
"json_migration.heading.extension_list_type": "Uzantı Listesi Türü:",
"json_migration.heading.file_extension_list": "Dosya Uzantı Listesi:",
"json_migration.heading.match": "Eşleşti",
"json_migration.heading.names": "Adlar:",
"json_migration.heading.parent_tags": "Üst Etiketler:",
@@ -152,6 +152,8 @@
"json_migration.heading.aliases": "别名:",
"json_migration.heading.colors": "颜色:",
"json_migration.heading.differ": "差异",
"json_migration.heading.extension_list_type": "扩展名列表类型:",
"json_migration.heading.file_extension_list": "文件扩展名列表:",
"json_migration.heading.match": "已匹配",
"json_migration.heading.names": "名字:",
"json_migration.heading.parent_tags": "上级标签:",
@@ -156,6 +156,8 @@
"json_migration.heading.aliases": "別名:",
"json_migration.heading.colors": "顏色:",
"json_migration.heading.differ": "差異",
"json_migration.heading.extension_list_type": "副檔名清單類型:",
"json_migration.heading.file_extension_list": "檔案副檔名清單:",
"json_migration.heading.match": "已一致",
"json_migration.heading.names": "名稱:",
"json_migration.heading.parent_tags": "父標籤:",
Binary file not shown.
+4 -3
View File
@@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory
import pytest
from tagstudio.core.constants import IGNORE_NAME
from tagstudio.core.enums import LibraryPrefs
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.refresh import RefreshTracker
from tagstudio.core.utils.types import unwrap
@@ -20,14 +20,15 @@ CWD = Path(__file__).parent
def test_refresh_new_files(library: Library, exclude_mode: bool):
library_dir = unwrap(library.library_dir)
# Given
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode)
library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"])
registry = RefreshTracker(library=library)
library.included_files.clear()
(library_dir / "FOO.MD").touch()
(library_dir / IGNORE_NAME).write_text("*.md" if exclude_mode else "*\n!*.md")
# Test if the single file was added
list(registry.refresh_dir(library_dir, force_internal_tools=True))
assert set(registry.files_not_in_library) == set([Path(IGNORE_NAME), Path("FOO.MD")])
assert registry.files_not_in_library == [Path("FOO.MD")]
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
-14
View File
@@ -1,14 +0,0 @@
from pytestqt.qtbot import QtBot
from tagstudio.qt.mixed.about_modal import AboutModal
def test_github_api_unavailable(qtbot: QtBot, mocker) -> None:
mocker.patch(
"requests.get",
side_effect=ConnectionError(
"Failed to resolve 'api.github.com' ([Errno -3] Temporary failure in name resolution)"
),
)
modal = AboutModal("/tmp")
qtbot.addWidget(modal)
+2 -2
View File
@@ -46,9 +46,9 @@ def test_library_migrations(path: str):
try:
status = library.open_library(library_dir=temp_path)
library.close()
shutil.rmtree(temp_path)
assert status.success
except Exception as e:
library.close()
raise (e)
finally:
shutil.rmtree(temp_path)
raise (e)
+8 -1
View File
@@ -6,6 +6,7 @@
from pathlib import Path
from time import time
from tagstudio.core.enums import LibraryPrefs
from tagstudio.qt.mixed.migration_modal import JsonMigrationModal
CWD = Path(__file__)
@@ -42,4 +43,10 @@ def test_json_migration():
assert modal.check_color_parity()
# Extension Filter List ====================================================
modal.assert_ignore_parity()
# Count
assert len(modal.json_lib.ext_list) == len(modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST))
# List Type
assert modal.check_ext_type()
# No Leading Dot
for ext in modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST): # pyright: ignore[reportUnknownVariableType]
assert ext[0] != "."
+24
View File
@@ -10,6 +10,7 @@ from tempfile import TemporaryDirectory
import pytest
import structlog
from tagstudio.core.enums import DefaultEnum, LibraryPrefs
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.fields import (
FieldID, # pyright: ignore[reportPrivateUsage]
@@ -198,6 +199,11 @@ def test_search_library_case_insensitive(library: Library):
assert results[0] == entry.id
def test_preferences(library: Library):
for pref in LibraryPrefs:
assert library.prefs(pref) == pref.default
def test_remove_entry_field(library: Library, entry_full: Entry):
title_field = entry_full.text_fields[0]
@@ -387,6 +393,24 @@ def test_update_field_order(library: Library, entry_full: Entry):
assert entry.text_fields[1].value == "second"
def test_library_prefs_multiple_identical_vals():
# check the preferences are inherited from DefaultEnum
assert issubclass(LibraryPrefs, DefaultEnum)
# create custom settings with identical values
class TestPrefs(DefaultEnum):
FOO = 1
BAR = 1
assert TestPrefs.FOO.default == 1
assert TestPrefs.BAR.default == 1
assert TestPrefs.BAR.name == "BAR"
# accessing .value should raise exception
with pytest.raises(AttributeError):
assert TestPrefs.BAR.value
def test_path_search_ilike(library: Library):
results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
assert results.total_count == 1