refactor(main): simplify get_tag/list_tags/update_tag
Removes _parse_character_fandom (no longer needed — names are bare). update_tag respects the new (name, kind[, fandom_id]) uniqueness shape. get_tag and list_tags return display_name so clients render without re-parsing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+51
-75
@@ -11,20 +11,11 @@ from app.models import ImageRecord, Tag, ArchiveRecord, image_tags, ImportTask,
|
|||||||
from app import db
|
from app import db
|
||||||
from app.utils.tag_names import normalize_display_name
|
from app.utils.tag_names import normalize_display_name
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
main = Blueprint('main', __name__)
|
main = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
|
||||||
def _parse_character_fandom(name_after_prefix):
|
|
||||||
"""Parse 'Name (Fandom)' -> ('Name', 'Fandom') or ('Name', None)."""
|
|
||||||
m = re.match(r'^(.+?)\s*\((.+)\)$', name_after_prefix)
|
|
||||||
if m:
|
|
||||||
return m.group(1).strip(), m.group(2).strip()
|
|
||||||
return name_after_prefix, None
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_fandom_tag(fandom_name):
|
def _ensure_fandom_tag(fandom_name):
|
||||||
"""Find or create a fandom tag by bare name. Returns the Tag object.
|
"""Find or create a fandom tag by bare name. Returns the Tag object.
|
||||||
|
|
||||||
@@ -703,14 +694,24 @@ def search_tags():
|
|||||||
@main.get("/image/<int:image_id>/tags")
|
@main.get("/image/<int:image_id>/tags")
|
||||||
def list_tags(image_id):
|
def list_tags(image_id):
|
||||||
img = ImageRecord.query.get_or_404(image_id)
|
img = ImageRecord.query.get_or_404(image_id)
|
||||||
return jsonify(ok=True, tags=[
|
tags_with_fandom = (
|
||||||
|
db.session.query(Tag)
|
||||||
|
.join(image_tags, image_tags.c.tag_id == Tag.id)
|
||||||
|
.filter(image_tags.c.image_id == image_id)
|
||||||
|
.options(joinedload(Tag.fandom))
|
||||||
|
.order_by(Tag.kind.asc(), Tag.name.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return jsonify(tags=[
|
||||||
{
|
{
|
||||||
|
"id": t.id,
|
||||||
"name": t.name,
|
"name": t.name,
|
||||||
|
"display_name": t.display_name,
|
||||||
"kind": t.kind,
|
"kind": t.kind,
|
||||||
"fandom_id": t.fandom_id if t.kind == "character" else None,
|
"fandom_id": t.fandom_id if t.kind == "character" else None,
|
||||||
"fandom_name": (t.fandom.name.split(":", 1)[1] if t.fandom else None) if t.kind == "character" else None
|
"fandom_name": (t.fandom.name if t.fandom else None) if t.kind == "character" else None,
|
||||||
}
|
}
|
||||||
for t in img.tags
|
for t in tags_with_fandom
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -1020,16 +1021,14 @@ def remove_tag(image_id):
|
|||||||
@main.get("/api/tag/<int:tag_id>")
|
@main.get("/api/tag/<int:tag_id>")
|
||||||
def get_tag(tag_id):
|
def get_tag(tag_id):
|
||||||
"""Get a single tag's details."""
|
"""Get a single tag's details."""
|
||||||
tag = Tag.query.get_or_404(tag_id)
|
tag = Tag.query.options(joinedload(Tag.fandom)).get_or_404(tag_id)
|
||||||
# Get display name (without prefix)
|
|
||||||
display_name = tag.name.split(":", 1)[1] if ":" in tag.name else tag.name
|
|
||||||
result = {
|
result = {
|
||||||
"id": tag.id,
|
"id": tag.id,
|
||||||
"name": tag.name,
|
"name": tag.name,
|
||||||
"display_name": display_name,
|
"display_name": tag.display_name,
|
||||||
"kind": tag.kind,
|
"kind": tag.kind,
|
||||||
"fandom_id": tag.fandom_id,
|
"fandom_id": tag.fandom_id,
|
||||||
"fandom_name": tag.fandom.name.split(":", 1)[1] if tag.fandom else None
|
"fandom_name": tag.fandom.name if tag.fandom else None,
|
||||||
}
|
}
|
||||||
return jsonify(ok=True, tag=result)
|
return jsonify(ok=True, tag=result)
|
||||||
|
|
||||||
@@ -1167,13 +1166,12 @@ def get_post_metadata_by_name(tag_name):
|
|||||||
def update_tag(tag_id):
|
def update_tag(tag_id):
|
||||||
"""
|
"""
|
||||||
Update a tag's name and/or kind.
|
Update a tag's name and/or kind.
|
||||||
For prefixed kinds (artist, archive, character, series, rating),
|
|
||||||
the name will be stored as 'kind:name'.
|
|
||||||
|
|
||||||
If the new name matches an existing tag, merge them:
|
For character tags: rename = update Tag.name (bare). Fandom association
|
||||||
- Move all images from source tag to target tag
|
is NOT changed here — use /api/tag/<id>/set-fandom for that.
|
||||||
- Delete the source tag
|
|
||||||
- Return the target tag info with merged=True
|
If the new (name, kind [, fandom_id]) combination collides with an
|
||||||
|
existing tag, offer a merge (force_merge=true to commit).
|
||||||
"""
|
"""
|
||||||
tag = Tag.query.get_or_404(tag_id)
|
tag = Tag.query.get_or_404(tag_id)
|
||||||
|
|
||||||
@@ -1184,22 +1182,28 @@ def update_tag(tag_id):
|
|||||||
if not new_name:
|
if not new_name:
|
||||||
return jsonify(ok=False, error="Name is required"), 400
|
return jsonify(ok=False, error="Name is required"), 400
|
||||||
|
|
||||||
# Build the full tag name based on kind
|
if new_kind in ("character", "fandom"):
|
||||||
prefixed_kinds = ("artist", "archive", "character", "series", "fandom", "rating")
|
new_name = normalize_display_name(new_name)
|
||||||
if new_kind in prefixed_kinds:
|
|
||||||
# Remove any existing prefix from name if present
|
# Check for duplicates using the new uniqueness shape
|
||||||
if ":" in new_name:
|
if new_kind == "character":
|
||||||
new_name = new_name.split(":", 1)[1]
|
existing_q = Tag.query.filter(
|
||||||
full_name = f"{new_kind}:{new_name}"
|
Tag.kind == "character",
|
||||||
else:
|
Tag.name == new_name,
|
||||||
# User tags don't have prefixes
|
Tag.fandom_id.is_(tag.fandom_id) if tag.fandom_id is None
|
||||||
full_name = new_name
|
else Tag.fandom_id == tag.fandom_id,
|
||||||
|
Tag.id != tag_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
existing_q = Tag.query.filter(
|
||||||
|
Tag.kind == new_kind,
|
||||||
|
Tag.name == new_name,
|
||||||
|
Tag.id != tag_id,
|
||||||
|
)
|
||||||
|
existing = existing_q.first()
|
||||||
|
|
||||||
# Check for duplicates (excluding current tag)
|
|
||||||
existing = Tag.query.filter(Tag.name == full_name, Tag.id != tag_id).first()
|
|
||||||
if existing:
|
if existing:
|
||||||
if not force_merge:
|
if not force_merge:
|
||||||
# Ask user to confirm merge
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
ok=False,
|
ok=False,
|
||||||
error="A tag with this name already exists",
|
error="A tag with this name already exists",
|
||||||
@@ -1207,66 +1211,38 @@ def update_tag(tag_id):
|
|||||||
target_tag={
|
target_tag={
|
||||||
"id": existing.id,
|
"id": existing.id,
|
||||||
"name": existing.name,
|
"name": existing.name,
|
||||||
"kind": existing.kind
|
"display_name": existing.display_name,
|
||||||
}
|
"kind": existing.kind,
|
||||||
), 409 # Conflict status
|
},
|
||||||
|
), 409
|
||||||
|
|
||||||
# Merge: move all images from source tag to target tag
|
# Merge: move all images from source tag to target tag
|
||||||
source_images = tag.images[:] # Copy list to avoid mutation during iteration
|
source_images = tag.images[:]
|
||||||
merged_count = 0
|
merged_count = 0
|
||||||
for img in source_images:
|
for img in source_images:
|
||||||
if existing not in img.tags:
|
if existing not in img.tags:
|
||||||
img.tags.append(existing)
|
img.tags.append(existing)
|
||||||
merged_count += 1
|
merged_count += 1
|
||||||
img.tags.remove(tag)
|
img.tags.remove(tag)
|
||||||
|
|
||||||
# Delete the source tag
|
|
||||||
db.session.delete(tag)
|
db.session.delete(tag)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(ok=True, merged=True, merged_count=merged_count, tag={
|
return jsonify(ok=True, merged=True, merged_count=merged_count, tag={
|
||||||
"id": existing.id,
|
"id": existing.id,
|
||||||
"name": existing.name,
|
"name": existing.name,
|
||||||
"kind": existing.kind
|
"display_name": existing.display_name,
|
||||||
|
"kind": existing.kind,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Handle fandom-related updates
|
# Plain rename
|
||||||
old_kind = tag.kind
|
tag.name = new_name
|
||||||
old_name = tag.name
|
|
||||||
|
|
||||||
# No conflict - just update the tag
|
|
||||||
tag.name = full_name
|
|
||||||
tag.kind = new_kind
|
tag.kind = new_kind
|
||||||
|
|
||||||
# If editing a character tag, handle fandom from the name suffix
|
|
||||||
if new_kind == "character":
|
|
||||||
char_part = full_name.split(":", 1)[1]
|
|
||||||
char_name, fandom_name = _parse_character_fandom(char_part)
|
|
||||||
if fandom_name:
|
|
||||||
fandom_tag = _ensure_fandom_tag(fandom_name)
|
|
||||||
tag.fandom_id = fandom_tag.id
|
|
||||||
else:
|
|
||||||
tag.fandom_id = None
|
|
||||||
|
|
||||||
# If renaming a fandom tag, cascade rename to all character tags that reference it
|
|
||||||
if new_kind == "fandom" and old_kind == "fandom" and full_name != old_name:
|
|
||||||
new_fandom_display = full_name.split(":", 1)[1]
|
|
||||||
old_fandom_display = old_name.split(":", 1)[1] if ":" in old_name else old_name
|
|
||||||
# Find all character tags linked to this fandom
|
|
||||||
linked_chars = Tag.query.filter_by(fandom_id=tag.id, kind="character").all()
|
|
||||||
for char_tag in linked_chars:
|
|
||||||
# Replace old fandom suffix with new one
|
|
||||||
char_part = char_tag.name.split(":", 1)[1] if ":" in char_tag.name else char_tag.name
|
|
||||||
char_base, _ = _parse_character_fandom(char_part)
|
|
||||||
char_tag.name = f"character:{char_base} ({new_fandom_display})"
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(ok=True, tag={
|
return jsonify(ok=True, tag={
|
||||||
"id": tag.id,
|
"id": tag.id,
|
||||||
"name": tag.name,
|
"name": tag.name,
|
||||||
|
"display_name": tag.display_name,
|
||||||
"kind": tag.kind,
|
"kind": tag.kind,
|
||||||
"fandom_id": tag.fandom_id
|
"fandom_id": tag.fandom_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user