fix: reimport dict-cursor bug + add admin cleanup endpoint
Some checks failed
Deploy Development / deploy (push) Successful in 54s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m55s

Problem 1: Reimport fails completely (all exercises fail)
Root cause: Line 465 existing[0] instead of existing['id']
Fix: Changed to existing['id'] for dict-cursor compatibility

Problem 2: 221 exercises skipped, only 2 in database
Root cause: Orphaned wiki_import_references from failed imports
Fix: New admin endpoint to manually cleanup orphaned references

New endpoints:
- DELETE /api/import/mediawiki/admin/cleanup-orphaned-references
  Deletes all references where local_id doesn't exist in target table
  Returns count of deleted references per type

- GET /api/import/mediawiki/admin/stats
  Shows references vs. actual count + orphaned count
  Helps diagnose import state issues

Usage:
1. Call cleanup endpoint to remove orphaned references
2. Run normal import (reimport=false)
3. Should import all previously failed exercises
This commit is contained in:
Lars 2026-04-27 08:52:05 +02:00
parent 3a4160fd1c
commit 97a7fe7cba
3 changed files with 98 additions and 2 deletions

View File

@ -70,7 +70,7 @@ def read_root():
}
# Register routers
from routers import auth, profiles, exercises, clubs, skills, training_planning, catalogs, import_wiki
from routers import auth, profiles, exercises, clubs, skills, training_planning, catalogs, import_wiki, import_wiki_admin
app.include_router(auth.router)
app.include_router(profiles.router)
@ -80,6 +80,7 @@ app.include_router(skills.router)
app.include_router(training_planning.router)
app.include_router(catalogs.router)
app.include_router(import_wiki.router)
app.include_router(import_wiki_admin.router)
if __name__ == "__main__":
import uvicorn

View File

@ -462,7 +462,7 @@ def _upsert_exercise(mapped: dict, reimport: bool) -> Optional[int]:
equipment = _json.dumps(mapped.get("equipment", [])) if mapped.get("equipment") else None
if existing and reimport:
ex_id = existing[0]
ex_id = existing['id']
cur.execute(
"""UPDATE exercises SET
title = %s, summary = %s, goal = %s, execution = %s,

View File

@ -0,0 +1,95 @@
"""
Admin-Hilfsfunktionen für MediaWiki-Import
"""
from fastapi import APIRouter, Depends, HTTPException
from db import get_db, get_cursor
from routers.import_wiki import require_admin
router = APIRouter(prefix="/api/import/mediawiki/admin", tags=["import-admin"])
@router.delete("/cleanup-orphaned-references")
def cleanup_orphaned_references(session: dict = Depends(require_admin)):
"""
Löscht verwaiste Import-Referenzen (Referenz existiert, aber Übung fehlt).
Nützlich nach fehlgeschlagenen Imports.
"""
deleted = {"exercises": 0, "skills": 0, "methods": 0}
with get_db() as conn:
cur = get_cursor(conn)
# Exercises
cur.execute("""
DELETE FROM wiki_import_references
WHERE content_type = 'exercise'
AND local_id NOT IN (SELECT id FROM exercises)
""")
deleted["exercises"] = cur.rowcount
# Skills
cur.execute("""
DELETE FROM wiki_import_references
WHERE content_type = 'skill'
AND local_id NOT IN (SELECT id FROM skills)
""")
deleted["skills"] = cur.rowcount
# Methods
cur.execute("""
DELETE FROM wiki_import_references
WHERE content_type = 'method'
AND local_id NOT IN (SELECT id FROM training_methods)
""")
deleted["methods"] = cur.rowcount
conn.commit()
return {
"ok": True,
"deleted": deleted,
"message": f"Deleted {sum(deleted.values())} orphaned references"
}
@router.get("/stats")
def get_import_stats(session: dict = Depends(require_admin)):
"""Statistik über Import-Referenzen vs. tatsächliche Einträge."""
with get_db() as conn:
cur = get_cursor(conn)
# Exercises
cur.execute("SELECT COUNT(*) as count FROM wiki_import_references WHERE content_type = 'exercise'")
refs_exercises = cur.fetchone()['count']
cur.execute("SELECT COUNT(*) as count FROM exercises WHERE import_source = 'mediawiki'")
actual_exercises = cur.fetchone()['count']
# Skills
cur.execute("SELECT COUNT(*) as count FROM wiki_import_references WHERE content_type = 'skill'")
refs_skills = cur.fetchone()['count']
cur.execute("SELECT COUNT(*) as count FROM skills")
actual_skills = cur.fetchone()['count']
# Methods
cur.execute("SELECT COUNT(*) as count FROM wiki_import_references WHERE content_type = 'method'")
refs_methods = cur.fetchone()['count']
cur.execute("SELECT COUNT(*) as count FROM training_methods")
actual_methods = cur.fetchone()['count']
return {
"exercises": {
"references": refs_exercises,
"actual": actual_exercises,
"orphaned": refs_exercises - actual_exercises
},
"skills": {
"references": refs_skills,
"actual": actual_skills,
"orphaned": refs_skills - actual_skills
},
"methods": {
"references": refs_methods,
"actual": actual_methods,
"orphaned": refs_methods - actual_methods
}
}