diff --git a/app/main.py b/app/main.py index dc4b5d0..156438e 100644 --- a/app/main.py +++ b/app/main.py @@ -932,61 +932,76 @@ def add_tag(image_id): """ JSON endpoint to add a tag to an 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. + Resolution order: + - If 'tag_id' is provided, attach that exact tag (used when the + autocomplete picker resolves a specific row). Auto-applies the + character's fandom if set. Skips parse_kind_prefix entirely. + - Else parse 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). + For character tags created via the parse path, optional 'fandom_id' + (preferred) or 'fandom_name' attaches the character to a fandom. Both + omitted = null fandom (allowed). """ 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) - - kind, name = parse_kind_prefix(raw) - if kind is None: - # No recognized prefix — treat as a user-typed general tag. - kind = "user" - name = underscores_to_spaces(name) - elif kind in ("character", "fandom"): - name = normalize_display_name(name) - img = ImageRecord.query.get_or_404(image_id) + explicit_tag_id = request.form.get("tag_id", type=int) 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 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 explicit_tag_id: + # Autocomplete-picker path: attach the exact clicked tag. + tag = Tag.query.get(explicit_tag_id) if tag is None: - tag = Tag( + return jsonify(ok=False, error="tag_not_found"), 404 + if tag.kind == "character" and tag.fandom_id: + fandom_tag = Tag.query.get(tag.fandom_id) + else: + raw = (request.form.get("name") or "").strip() + if not raw: + abort(400) + + kind, name = parse_kind_prefix(raw) + if kind is None: + # No recognized prefix — treat as a user-typed general tag. + kind = "user" + name = underscores_to_spaces(name) + elif kind in ("character", "fandom"): + name = normalize_display_name(name) + + if kind == "character": + fandom_id = request.form.get("fandom_id", type=int) + fandom_name = (request.form.get("fandom_name") or "").strip() or None + + 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, - ) - 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() + ).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() if tag not in img.tags: img.tags.append(tag) diff --git a/app/static/js/view-modal.js b/app/static/js/view-modal.js index ed4447f..4a9212c 100644 --- a/app/static/js/view-modal.js +++ b/app/static/js/view-modal.js @@ -658,13 +658,20 @@ document.addEventListener('DOMContentLoaded', () => { }); } - async function addTag(imageId, name, { fandomId, fandomName } = {}) { + async function addTag(imageId, name, { tagId, fandomId, fandomName } = {}) { const fd = new FormData(); - fd.append('name', name); - if (fandomId) { - fd.append('fandom_id', String(fandomId)); - } else if (fandomName) { - fd.append('fandom_name', fandomName); + if (tagId) { + // Autocomplete-picker path: server attaches this exact tag, skipping + // parse_kind_prefix so a same-name character/user ambiguity is + // resolved by the row the user clicked. + fd.append('tag_id', String(tagId)); + } else { + fd.append('name', name); + if (fandomId) { + fd.append('fandom_id', String(fandomId)); + } else if (fandomName) { + fd.append('fandom_name', fandomName); + } } const r = await fetch(`/image/${imageId}/tags/add`, { method: 'POST', body: fd }); const j = await r.json(); @@ -747,7 +754,7 @@ document.addEventListener('DOMContentLoaded', () => { tagAutocomplete.innerHTML = tags.map((t, i) => { const icon = getTagIcon(t.kind); const displayName = getTagDisplayName(t.name); - return `