feat(maintenance): backfill fandom tags onto legacy character attachments
New Celery task sync_character_fandoms_to_images on the maintenance queue. For every character tag with a non-null fandom_id, runs the same INSERT ... SELECT ... ON CONFLICT DO NOTHING that set_tag_fandom already runs inline, attaching the fandom tag to every image that has the character attached. Why this exists: migration j26042101 extracted the '(Fandom)' suffix from pre-refactor character names into tag.fandom_id, but it did NOT walk image_tags to attach the fandom rows to every image that had the character. Post-refactor auto-apply (in add_tag / accept_image_suggestion / set_tag_fandom) only fires on NEW add/accept events — pre-existing character attachments from before the migration still show the character pill in the modal but no fandom pill, because the fandom row was never added to image_tags. Surfaced by Settings → Maintenance → 'Sync fandoms to images'. Safe, additive, idempotent. Run once after the migration to close the gap; subsequent set-fandom operations continue to maintain the invariant inline. Verified locally: seeded a character+fandom pair with only the character attached to image 1; task added the fandom to image_tags; final state has both rows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,7 @@ def make_celery(app=None):
|
||||
'app.tasks.scan.update_system_stats': {'queue': 'maintenance'},
|
||||
'app.tasks.scan.update_batch_stats': {'queue': 'maintenance'},
|
||||
'app.tasks.maintenance.sweep_blocklisted_tag_from_images': {'queue': 'maintenance'},
|
||||
'app.tasks.maintenance.sync_character_fandoms_to_images': {'queue': 'maintenance'},
|
||||
|
||||
# Import tasks - handled by worker (heavy processing)
|
||||
'app.tasks.import_file.*': {'queue': 'import'},
|
||||
|
||||
@@ -641,6 +641,13 @@ def trigger_recompute_all_centroids():
|
||||
return redirect(url_for('main.settings', tab='maintenance'))
|
||||
|
||||
|
||||
@main.post('/settings/maintenance/sync-character-fandoms-to-images')
|
||||
def trigger_sync_character_fandoms_to_images():
|
||||
from app.tasks.maintenance import sync_character_fandoms_to_images
|
||||
sync_character_fandoms_to_images.apply_async(queue='maintenance')
|
||||
return redirect(url_for('main.settings', tab='maintenance'))
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Tag add/remove endpoints
|
||||
# ----------------------------
|
||||
|
||||
@@ -15,6 +15,70 @@ from app.models import Tag
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task(
|
||||
name='app.tasks.maintenance.sync_character_fandoms_to_images',
|
||||
soft_time_limit=300,
|
||||
time_limit=600,
|
||||
)
|
||||
def sync_character_fandoms_to_images() -> dict:
|
||||
"""For every character tag with a non-null fandom_id, ensure the fandom
|
||||
tag is attached to every image that has the character attached.
|
||||
|
||||
Backfills the gap left by migration j26042101 — the migration set
|
||||
tag.fandom_id from the old '(Fandom)' suffix but did not retroactively
|
||||
add fandom rows to image_tags. Set operations made AFTER the migration
|
||||
via /api/tag/<id>/set-fandom do this inline, so running this task once
|
||||
(or whenever the user suspects drift) is sufficient.
|
||||
|
||||
Additive only — never removes fandom attachments. Idempotent via
|
||||
ON CONFLICT DO NOTHING.
|
||||
|
||||
Returns a summary with per-character counts so the result is
|
||||
introspectable.
|
||||
"""
|
||||
chars = (
|
||||
Tag.query
|
||||
.filter_by(kind='character')
|
||||
.filter(Tag.fandom_id.isnot(None))
|
||||
.all()
|
||||
)
|
||||
total_links_added = 0
|
||||
characters_processed = 0
|
||||
failures = 0
|
||||
for char in chars:
|
||||
try:
|
||||
result = db.session.execute(
|
||||
text("""
|
||||
INSERT INTO image_tags (image_id, tag_id)
|
||||
SELECT it.image_id, :fandom_id
|
||||
FROM image_tags it
|
||||
WHERE it.tag_id = :char_id
|
||||
ON CONFLICT DO NOTHING
|
||||
"""),
|
||||
{'char_id': char.id, 'fandom_id': char.fandom_id},
|
||||
)
|
||||
total_links_added += result.rowcount or 0
|
||||
db.session.commit()
|
||||
characters_processed += 1
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
failures += 1
|
||||
log.warning(
|
||||
"sync_character_fandoms_to_images: char tag %s (%s) failed: %s",
|
||||
char.id, char.name, e,
|
||||
)
|
||||
log.info(
|
||||
"sync_character_fandoms_to_images: processed %d characters, added %d links, %d failures",
|
||||
characters_processed, total_links_added, failures,
|
||||
)
|
||||
return {
|
||||
'characters_scanned': len(chars),
|
||||
'characters_processed': characters_processed,
|
||||
'links_added': total_links_added,
|
||||
'characters_failed': failures,
|
||||
}
|
||||
|
||||
|
||||
@shared_task(
|
||||
name='app.tasks.maintenance.sweep_blocklisted_tag_from_images',
|
||||
soft_time_limit=60,
|
||||
|
||||
@@ -456,6 +456,18 @@
|
||||
<button type="submit" class="btn secondary-btn">Recompute all centroids</button>
|
||||
</form>
|
||||
|
||||
<p class="settings-hint mt-paragraph">
|
||||
For every character tag with a fandom assigned, attach the fandom
|
||||
tag to every image already tagged with that character. Additive
|
||||
only — never removes fandom attachments. Safe to re-run. Backfills
|
||||
data migrated from pre-refactor tag names that embedded the
|
||||
"(Fandom)" suffix, since the migration set tag.fandom_id but did
|
||||
not retroactively attach the fandom row to image_tags.
|
||||
</p>
|
||||
<form action="{{ url_for('main.trigger_sync_character_fandoms_to_images') }}" method="post" onsubmit="return confirm('Attach each character\\'s fandom tag to every image with that character?');">
|
||||
<button type="submit" class="btn secondary-btn">Sync fandoms to images</button>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
|
||||
Reference in New Issue
Block a user