fc3k: /api/admin tag endpoints — Tier-B delete + merge + usage-count helper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,3 +112,70 @@ async def images_bulk_delete():
|
|||||||
from ..tasks.admin import bulk_delete_images_task
|
from ..tasks.admin import bulk_delete_images_task
|
||||||
async_result = bulk_delete_images_task.delay(image_ids=image_ids)
|
async_result = bulk_delete_images_task.delay(image_ids=image_ids)
|
||||||
return jsonify({"task_id": async_result.id}), 202
|
return jsonify({"task_id": async_result.id}), 202
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/tags/<int:tag_id>", methods=["DELETE"])
|
||||||
|
async def tag_delete(tag_id: int):
|
||||||
|
"""Tier-B sync delete. UI yes/no modal is the only confirmation."""
|
||||||
|
from ..services.cleanup_service import delete_tag
|
||||||
|
|
||||||
|
async with get_session() as session:
|
||||||
|
try:
|
||||||
|
result = await session.run_sync(
|
||||||
|
lambda sync_sess: delete_tag(sync_sess, tag_id=tag_id)
|
||||||
|
)
|
||||||
|
except LookupError:
|
||||||
|
return _bad("not_found", status=404)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/tags/<int:dest_id>/merge", methods=["POST"])
|
||||||
|
async def tag_merge(dest_id: int):
|
||||||
|
"""Wraps TagService.merge. Source repoints to dest, dest survives,
|
||||||
|
source row deleted, protective alias auto-created if source was
|
||||||
|
ML-applied or allowlisted."""
|
||||||
|
from ..services.tag_service import TagMergeConflict, TagService, TagValidationError
|
||||||
|
|
||||||
|
body = await request.get_json(silent=True) or {}
|
||||||
|
source_id = body.get("source_id")
|
||||||
|
if not isinstance(source_id, int) or source_id == dest_id:
|
||||||
|
return _bad("invalid_source_id", detail="source_id must be int and differ from dest")
|
||||||
|
|
||||||
|
async with get_session() as session:
|
||||||
|
try:
|
||||||
|
result = await TagService(session).merge(
|
||||||
|
source_id=source_id, target_id=dest_id,
|
||||||
|
)
|
||||||
|
except TagMergeConflict as exc:
|
||||||
|
return _bad("merge_conflict", status=409, detail=str(exc))
|
||||||
|
except TagValidationError as exc:
|
||||||
|
return _bad("tag_kind_mismatch", detail=str(exc))
|
||||||
|
except LookupError:
|
||||||
|
return _bad("not_found", status=404)
|
||||||
|
|
||||||
|
# MergeResult is a frozen dataclass — flatten to dict.
|
||||||
|
return jsonify({
|
||||||
|
"result": {
|
||||||
|
"target_id": result.target_id,
|
||||||
|
"target_name": result.target_name,
|
||||||
|
"target_kind": result.target_kind,
|
||||||
|
"merged_count": result.merged_count,
|
||||||
|
"alias_created": result.alias_created,
|
||||||
|
"source_deleted": result.source_deleted,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route("/tags/<int:tag_id>/usage-count", methods=["GET"])
|
||||||
|
async def tag_usage_count(tag_id: int):
|
||||||
|
"""Helper for the Tier-B yes/no prompt; surfaces "N associations"
|
||||||
|
in the dialog so the operator knows what they're nuking."""
|
||||||
|
from ..services.cleanup_service import count_tag_associations
|
||||||
|
|
||||||
|
async with get_session() as session:
|
||||||
|
count = await session.run_sync(
|
||||||
|
lambda sync_sess: count_tag_associations(
|
||||||
|
sync_sess, tag_id=tag_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify({"count": count})
|
||||||
|
|||||||
Reference in New Issue
Block a user