refactor(main): add_tag parses prefix at boundary; bare storage
Accepts 'character:Saber' user shortcut, parses via parse_kind_prefix, stores tag.name bare. Character path accepts fandom_id (preferred) or fandom_name for fandom association. Returns display_name in the response so the client can render pills without a second round-trip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+58
-44
@@ -861,72 +861,86 @@ def reject_image_suggestion(image_id):
|
|||||||
@main.post("/image/<int:image_id>/tags/add")
|
@main.post("/image/<int:image_id>/tags/add")
|
||||||
def add_tag(image_id):
|
def add_tag(image_id):
|
||||||
"""
|
"""
|
||||||
Minimal JSON endpoint to add a tag to an image.
|
JSON endpoint to add a tag to an image.
|
||||||
Accepts either 'artist:NAME' / 'archive:...' or a bare freeform tag ('mytag').
|
|
||||||
|
|
||||||
For character tags with a fandom suffix (e.g. 'character:Saber (Fate)'),
|
Accepts the user-facing 'kind:name' shortcut in the 'name' field
|
||||||
automatically creates/finds the fandom tag and adds it to the image.
|
(e.g. 'character:Saber'). Bare strings without a recognized prefix are
|
||||||
|
treated as user tags.
|
||||||
|
|
||||||
|
For character tags, optional 'fandom_id' (preferred) or 'fandom_name'
|
||||||
|
attaches the character to a fandom. Both omitted = null fandom (allowed).
|
||||||
"""
|
"""
|
||||||
name = (request.form.get("name") or "").strip()
|
from app.utils.tag_prefix import parse_kind_prefix
|
||||||
if not name:
|
from app.utils.tag_names import underscores_to_spaces
|
||||||
|
|
||||||
|
raw = (request.form.get("name") or "").strip()
|
||||||
|
if not raw:
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
# Normalize: keep explicit kind prefixes, otherwise treat as user tag
|
kind, name = parse_kind_prefix(raw)
|
||||||
if ":" in name:
|
if kind is None:
|
||||||
kind = name.split(":", 1)[0]
|
# No recognized prefix — treat as a user-typed general tag.
|
||||||
tag_name = name
|
|
||||||
else:
|
|
||||||
# No prefix — treat as a user-typed general tag. Strip underscores so
|
|
||||||
# WD14-style names pasted into the add box converge with our convention.
|
|
||||||
from app.utils.tag_names import underscores_to_spaces
|
|
||||||
kind = "user"
|
kind = "user"
|
||||||
tag_name = underscores_to_spaces(name)
|
name = underscores_to_spaces(name)
|
||||||
|
elif kind in ("character", "fandom"):
|
||||||
|
name = normalize_display_name(name)
|
||||||
|
|
||||||
img = ImageRecord.query.get_or_404(image_id)
|
img = ImageRecord.query.get_or_404(image_id)
|
||||||
tag = Tag.query.filter_by(name=tag_name).first()
|
|
||||||
|
|
||||||
fandom_tag = None
|
fandom_tag = None
|
||||||
|
if kind == "character":
|
||||||
|
fandom_id = request.form.get("fandom_id", type=int)
|
||||||
|
fandom_name = (request.form.get("fandom_name") or "").strip() or None
|
||||||
|
|
||||||
if not tag:
|
if fandom_id:
|
||||||
# For new character tags, parse + normalize + link fandom
|
fandom_tag = Tag.query.filter_by(id=fandom_id, kind="fandom").first()
|
||||||
if kind == "character":
|
if fandom_tag is None:
|
||||||
char_part = tag_name.split(":", 1)[1]
|
return jsonify(ok=False, error="fandom_not_found"), 400
|
||||||
char_name, fandom_name = _parse_character_fandom(char_part)
|
elif fandom_name:
|
||||||
norm_char = normalize_display_name(char_name)
|
fandom_tag = _ensure_fandom_tag(fandom_name)
|
||||||
if fandom_name:
|
|
||||||
fandom_tag = _ensure_fandom_tag(fandom_name)
|
tag = Tag.query.filter_by(
|
||||||
tag_name = f"character:{norm_char} ({fandom_tag.name.split(':', 1)[1]})"
|
kind="character",
|
||||||
else:
|
name=name,
|
||||||
tag_name = f"character:{norm_char}"
|
fandom_id=fandom_tag.id if fandom_tag else None,
|
||||||
elif kind == "fandom":
|
).first()
|
||||||
fandom_part = tag_name.split(":", 1)[1]
|
if tag is None:
|
||||||
tag_name = f"fandom:{normalize_display_name(fandom_part)}"
|
tag = Tag(
|
||||||
# Look up again under the normalized name — an existing row may match.
|
kind="character",
|
||||||
tag = Tag.query.filter_by(name=tag_name).first()
|
name=name,
|
||||||
if not tag:
|
fandom_id=fandom_tag.id if fandom_tag else None,
|
||||||
tag = Tag(name=tag_name, kind=kind)
|
)
|
||||||
if kind == "character" and fandom_name:
|
db.session.add(tag)
|
||||||
tag.fandom_id = fandom_tag.id
|
db.session.flush()
|
||||||
|
else:
|
||||||
|
tag = Tag.query.filter_by(kind=kind, name=name).first()
|
||||||
|
if tag is None:
|
||||||
|
tag = Tag(kind=kind, name=name)
|
||||||
db.session.add(tag)
|
db.session.add(tag)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
elif kind == "character" and tag.fandom_id:
|
|
||||||
fandom_tag = Tag.query.get(tag.fandom_id)
|
|
||||||
elif tag.kind == "character" and tag.fandom_id:
|
|
||||||
# Existing character tag with a fandom — grab the fandom tag for auto-apply
|
|
||||||
fandom_tag = Tag.query.get(tag.fandom_id)
|
|
||||||
|
|
||||||
if tag not in img.tags:
|
if tag not in img.tags:
|
||||||
img.tags.append(tag)
|
img.tags.append(tag)
|
||||||
|
|
||||||
# Auto-apply fandom tag to the image
|
# Auto-apply fandom tag to the image (preserves today's UX feature).
|
||||||
if fandom_tag and fandom_tag not in img.tags:
|
if fandom_tag and fandom_tag not in img.tags:
|
||||||
img.tags.append(fandom_tag)
|
img.tags.append(fandom_tag)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
result = {"name": tag.name, "kind": tag.kind}
|
result = {
|
||||||
|
"id": tag.id,
|
||||||
|
"name": tag.name,
|
||||||
|
"display_name": tag.display_name,
|
||||||
|
"kind": tag.kind,
|
||||||
|
}
|
||||||
if fandom_tag:
|
if fandom_tag:
|
||||||
result["fandom_tag"] = {"name": fandom_tag.name, "kind": fandom_tag.kind}
|
result["fandom_tag"] = {
|
||||||
|
"id": fandom_tag.id,
|
||||||
|
"name": fandom_tag.name,
|
||||||
|
"display_name": fandom_tag.display_name,
|
||||||
|
"kind": fandom_tag.kind,
|
||||||
|
}
|
||||||
return jsonify(ok=True, tag=result)
|
return jsonify(ok=True, tag=result)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user