From d89f910a6d786d297073baa4d4540b9afbfe3a0b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 22 Apr 2026 13:47:53 -0400 Subject: [PATCH] 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 --- app/main.py | 102 +++++++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/app/main.py b/app/main.py index 4b04799..e95d26d 100644 --- a/app/main.py +++ b/app/main.py @@ -861,72 +861,86 @@ def reject_image_suggestion(image_id): @main.post("/image//tags/add") def add_tag(image_id): """ - Minimal JSON endpoint to add a tag to an image. - Accepts either 'artist:NAME' / 'archive:...' or a bare freeform tag ('mytag'). + JSON endpoint to add a tag to an image. - For character tags with a fandom suffix (e.g. 'character:Saber (Fate)'), - automatically creates/finds the fandom tag and adds it to the image. + Accepts the user-facing 'kind:name' shortcut in the 'name' field + (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() - if not name: + from app.utils.tag_prefix import parse_kind_prefix + from app.utils.tag_names import underscores_to_spaces + + raw = (request.form.get("name") or "").strip() + if not raw: abort(400) - # Normalize: keep explicit kind prefixes, otherwise treat as user tag - if ":" in name: - kind = name.split(":", 1)[0] - 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, name = parse_kind_prefix(raw) + if kind is None: + # No recognized prefix — treat as a user-typed general tag. 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) - tag = Tag.query.filter_by(name=tag_name).first() 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: - # For new character tags, parse + normalize + link fandom - if kind == "character": - char_part = tag_name.split(":", 1)[1] - char_name, fandom_name = _parse_character_fandom(char_part) - norm_char = normalize_display_name(char_name) - if fandom_name: - fandom_tag = _ensure_fandom_tag(fandom_name) - tag_name = f"character:{norm_char} ({fandom_tag.name.split(':', 1)[1]})" - else: - tag_name = f"character:{norm_char}" - elif kind == "fandom": - fandom_part = tag_name.split(":", 1)[1] - tag_name = f"fandom:{normalize_display_name(fandom_part)}" - # Look up again under the normalized name — an existing row may match. - tag = Tag.query.filter_by(name=tag_name).first() - if not tag: - tag = Tag(name=tag_name, kind=kind) - if kind == "character" and fandom_name: - tag.fandom_id = fandom_tag.id + if fandom_id: + fandom_tag = Tag.query.filter_by(id=fandom_id, kind="fandom").first() + if fandom_tag is None: + return jsonify(ok=False, error="fandom_not_found"), 400 + elif fandom_name: + fandom_tag = _ensure_fandom_tag(fandom_name) + + tag = Tag.query.filter_by( + kind="character", + name=name, + fandom_id=fandom_tag.id if fandom_tag else None, + ).first() + if tag is None: + tag = Tag( + kind="character", + name=name, + fandom_id=fandom_tag.id if fandom_tag else None, + ) + db.session.add(tag) + 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.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: 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: img.tags.append(fandom_tag) 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: - 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)