Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
run: |
pytest
# TODO: Change the coverage check and generally figure out how we want to use one.
coverage:
name: Check coverage
runs-on: ubuntu-latest
Expand All @@ -89,9 +90,9 @@ jobs:
- name: Check coverage
uses: yedpodtrzitko/coverage@main
with:
thresholdAll: 0.1
thresholdNew: 0.1
thresholdModified: 0.1
thresholdAll: 0
thresholdNew: 0
thresholdModified: 0
coverageFile: coverage.xml
token: ${{ secrets.GITHUB_TOKEN }}
sourceDir: tagstudio/src
Binary file added docs/assets/add_fields.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/datetime_field_editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/field_template_editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/field_template_manager.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/fields_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/text_field_editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 40 additions & 11 deletions docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,53 @@ icon: material/text-box

# :material-text-box: Fields

Fields are additional types of metadata that you can attach to [file entries](./entries.md). Like [tags](tags.md), fields are not stored inside files themselves nor in sidecar files, but rather inside the respective TagStudio [library](./index.md) save file.
Fields are extra pieces of information you can add to [file entries](./entries.md), similar to how [tags](tags.md) are added to entries. Fields are useful for storing information that doesn't nessisarily need to be a tag, such as titles, comments, notes, specific dates or times, etc.

## Field Types
To add a field to an entry, click the "Add Field" button in the preview panel. From there you can search and/or select a [field template](#field-templates) to choose from, or create a new one from the search bar. Alternatively you can create new field templates from **Edit -> Manage Field Templates**.

### Text Line
<figure markdown="span">
![Fields Example](assets/fields_example.png)
<figcaption>Example of tags and various fields on a file entry.</figcaption>
</figure>

A string of text, displayed as a single line.
## :material-text-box-plus-outline: Field Templates

- e.g: Title, Author, Artist, URL, etc.
Field templates are handy templates to use when adding fields to entries that contain preconfigured options but no actual data. When you add a field to an entry from the "Add Field" button, you choose from a template to add and then fill in the information afterwards. TagStudio includes a handful of field templates to start you off with, but you're free to modify or delete them, or simply create your own.

### Text Box
Field templates can be viewed, created, and deleted from the **Edit -> Manage Field Templates** window. You can also edit field templates from the "Add Field" menu, and create new ones on the fly from the search bar. Note that you can not currently delete field templates from the "Add Field" menu, just like tags.

A long string of text displayed as a box of text.
<figure markdown="span">
![Field Template Manager](assets/field_template_manager.png)
<figcaption>Field Template Manager from <b>Edit -> Manage Field Templates</b>.</figcaption>
</figure>

- e.g: Description, Notes, etc.
<figure markdown="span">
![Field Template Editor](assets/field_template_editor.png)
<figcaption>The field template editor, shown creating a new "Citations" field.</figcaption>
</figure>

### Datetime
## :material-format-list-bulleted-type: Field Types

A date and time value.
Fields come in a variety of types that are better suited for different types of information, and may provide additional options unique to those types. Single lines are good for fields like titles, while multiline blocks are good for things like comments and notes.

- e.g: Date Published, Date Taken, etc.
### :material-text-box: Text

Text fields contain a piece of text with the option to display it either a single line or a multiline body of text.

| Option | Value | Description |
| --------- | ---------- | ------------------------------------------------------------------------ |
| Multiline | True/False | Indicates if the text should be displayed on multiple lines or just one. |

<figure markdown="span">
![Text Field Editor](assets/text_field_editor.png)
<figcaption>The text field editor, editing a "Comments" field on an entry.</figcaption>
</figure>

### :material-calendar-month: Datetime

Datetime fields contain a date and time value. Dates are formatted using the format specified in your application settings.

<figure markdown="span">
![Datetime Field Editor](assets/datetime_field_editor.png)
<figcaption>The datetime field editor, expanded to show the date picker.</figcaption>
</figure>
8 changes: 4 additions & 4 deletions src/tagstudio/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

VERSION: str = "9.5.7" # Major.Minor.Patch
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
GITHUB_REPO_URL = "https://github.com/TagStudioDev/TagStudio"
GITHUB_RELEASE_URL = "https://github.com/TagStudioDev/TagStudio/releases/latest"
DOCS_URL = "https://docs.tagstud.io"
DISCORD_URL = "https://discord.com/invite/hRNnVKhF2G"

# The folder & file names where TagStudio keeps its data relative to a library.
TS_FOLDER_NAME: str = ".TagStudio"
Expand All @@ -13,9 +16,7 @@
IGNORE_NAME: str = ".ts_ignore"
THUMB_CACHE_NAME: str = "thumbs"

FONT_SAMPLE_TEXT: str = (
"""ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?@$%(){}[]"""
)
FONT_SAMPLE_TEXT: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?@$%(){}[]"
FONT_SAMPLE_SIZES: list[int] = [10, 15, 20]

# NOTE: These were the field IDs used for the "Tags", "Content Tags", and "Meta Tags" fields inside
Expand All @@ -27,5 +28,4 @@
TAG_META = 2
RESERVED_TAG_START = 0
RESERVED_TAG_END = 999

RESERVED_NAMESPACE_PREFIX = "tagstudio"
123 changes: 119 additions & 4 deletions src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
Engine,
NullPool,
ScalarResult,
Update,
and_,
asc,
create_engine,
Expand Down Expand Up @@ -1313,14 +1314,122 @@ def sort_key(text: str):

return direct_tags, descendant_tags

def add_field_template(self, field_template: BaseFieldTemplate) -> BaseFieldTemplate | None:
"""Add a new field template to the library."""
if not (isinstance(field_template, (TextFieldTemplate, DatetimeFieldTemplate))):
logger.error("[Library] BaseFieldTemplate attempted to be added to the library.")
return None

with Session(self.engine) as session:
try:
session.add(field_template)
session.flush()
make_transient(field_template)
session.commit()
return field_template
except IntegrityError as e:
logger.error(e)
session.rollback()
return None

def update_field_template(self, old_field_type: str, field_template: BaseFieldTemplate) -> bool:
"""Update a field template in the library.

old_field_class:str
field_template: BaseFieldTemplate
"""
with Session(self.engine) as session:
logger.warning(f"Updating old type {old_field_type} to new {field_template.class_name}")
is_same_type: bool = old_field_type == field_template.class_name
try:
update_stmt: Update | None = None
# If the template is changing type, remove the old one and add the updated
# template to the proper table.
if not is_same_type:
old_template: BaseFieldTemplate | None = None
if old_field_type == "TextFieldTemplate":
old_template = session.scalar(
select(TextFieldTemplate)
.where(TextFieldTemplate.id == field_template.id)
.limit(1)
)
elif old_field_type == "DatetimeFieldTemplate":
old_template = session.scalar(
select(DatetimeFieldTemplate)
.where(DatetimeFieldTemplate.id == field_template.id)
.limit(1)
)
if old_template is None:
logger.error("[Library] old_template is None")
return False
session.delete(old_template)
session.flush()
field_template.id = None # The id should not transfer between tables
session.add(field_template)
session.commit()
# Otherwise, update the existing template in-place
elif isinstance(field_template, TextFieldTemplate):
update_stmt = (
update(TextFieldTemplate)
.where(TextFieldTemplate.id == field_template.id)
.values(name=field_template.name, is_multiline=field_template.is_multiline)
)
elif isinstance(field_template, DatetimeFieldTemplate):
update_stmt = (
update(DatetimeFieldTemplate)
.where(DatetimeFieldTemplate.id == field_template.id)
.values(name=field_template.name)
)
if is_same_type:
if update_stmt is None:
return False
session.execute(update_stmt)
session.commit()

except IntegrityError as e:
logger.error(e)
session.rollback()
return False

return True

def remove_field_template(self, field_template: BaseFieldTemplate) -> bool:
"""Remove a field template from the library."""
with Session(self.engine) as session:
try:
session_item: BaseFieldTemplate | None = None
if isinstance(field_template, TextFieldTemplate):
session_item = session.scalar(
select(TextFieldTemplate)
.where(TextFieldTemplate.id == field_template.id)
.limit(1)
)
elif isinstance(field_template, DatetimeFieldTemplate):
session_item = session.scalar(
select(DatetimeFieldTemplate)
.where(DatetimeFieldTemplate.id == field_template.id)
.limit(1)
)

if session_item is not None:
session.delete(session_item)
session.commit()

except IntegrityError as e:
logger.error(e)
session.rollback()
return False

return True

def search_field_templates(self, name: str | None, limit: int = 100) -> list[BaseFieldTemplate]:
"""Return field template rows matching the query, detached from the session."""
if limit <= 0:
limit = sys.maxsize

search_query: str = name.lower() if name else ""

def sort_key(template: BaseFieldTemplate) -> tuple:
def sort_key(template: BaseFieldTemplate) -> tuple[str] | tuple[bool, int, str]:
text = template.name.lower()
if not search_query:
return (text,)
Expand Down Expand Up @@ -1431,7 +1540,12 @@ def remove_entry_field(
session.commit()

def update_text_field(
self, entry_ids: list[int] | int, field: TextField, value: str, is_multiline: bool
self,
entry_ids: list[int] | int,
field: TextField,
name: str,
value: str,
is_multiline: bool,
):
"""Update a TextField field on one or more Entries."""
if isinstance(entry_ids, int):
Expand All @@ -1443,7 +1557,7 @@ def update_text_field(
update_stmt = (
update(field_type)
.where(and_(field_type.id == field.id, field_type.entry_id.in_(entry_ids)))
.values(value=value, is_multiline=is_multiline)
.values(name=name, value=value, is_multiline=is_multiline)
)

session.execute(update_stmt)
Expand All @@ -1453,6 +1567,7 @@ def update_datetime_field(
self,
entry_ids: list[int] | int,
field: DatetimeField,
name: str,
value: datetime,
):
"""Update a DatetimeField field on one or more Entries."""
Expand All @@ -1465,7 +1580,7 @@ def update_datetime_field(
update_stmt = (
update(field_type)
.where(and_(field_type.id == field.id, field_type.entry_id.in_(entry_ids)))
.values(value=value)
.values(name=name, value=value)
)

session.execute(update_stmt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only


from typing import override
from typing import Any, override

from PySide6.QtCore import Signal
from PySide6.QtGui import QMouseEvent
Expand All @@ -14,8 +14,8 @@ class ClickableLabel(QLabel):

clicked = Signal()

def __init__(self):
super().__init__()
def __init__(self, *args: Any, **kwarg: Any): # pyright: ignore[reportExplicitAny]
super().__init__(*args, **kwarg)

@override
def mousePressEvent(self, ev: QMouseEvent):
Expand Down
Loading
Loading