Spaces:
Sleeping
Sleeping
| """One-time cleanup script. | |
| Tasks: | |
| 1. Delete question_bank rows referencing nonexistent passages / images. | |
| 2. Delete every quiz (and all cascaded data: student_results, reports, | |
| raw_files, parsed_data, answer_grids) EXCEPT the lowest-id quiz. | |
| Run: | |
| cd chatkit/backend | |
| python cleanup.py # dry-run (shows what would be deleted) | |
| python cleanup.py --confirm # actually deletes | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import re | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from sqlalchemy import delete, select, text | |
| from app.db import get_sessionmaker | |
| from app.models import QuestionBank, Quiz, Report, RawFile, StudentResult | |
| # Same skip pattern as seed_from_table.py | |
| _SKIP_RE = re.compile( | |
| r"look at the picture" | |
| r"|in the picture" | |
| r"|the picture shows" | |
| r"|the following picture" | |
| r"|according to the (?:passage|article|reading|text|graph|chart|table)" | |
| r"|based on the (?:passage|article|reading|text)" | |
| r"|the (?:passage|article|reading) (?:above|below|following)" | |
| r"|the following (?:passage|article|reading)", | |
| re.I, | |
| ) | |
| DRY_RUN = "--confirm" not in sys.argv | |
| async def main() -> None: | |
| if DRY_RUN: | |
| print("DRY RUN β pass --confirm to actually delete\n") | |
| async with get_sessionmaker()() as db: | |
| # ββ 1. Question bank: find passage/image questions βββββββββββββββββββββ | |
| res = await db.execute(select(QuestionBank.id, QuestionBank.question_text)) | |
| rows = res.all() | |
| bad_ids = [r.id for r in rows if _SKIP_RE.search(r.question_text or "")] | |
| print(f"Question bank: {len(rows)} total, {len(bad_ids)} reference passages/images") | |
| if bad_ids: | |
| print(" Samples:") | |
| for qid in bad_ids[:5]: | |
| text_sample = next(r.question_text for r in rows if r.id == qid) | |
| print(f" [{qid}] {text_sample[:80]}β¦") | |
| if bad_ids and not DRY_RUN: | |
| await db.execute(delete(QuestionBank).where(QuestionBank.id.in_(bad_ids))) | |
| print(f" β Deleted {len(bad_ids)} question bank rows") | |
| # ββ 2. Quizzes: keep only the lowest-id quiz βββββββββββββββββββββββββββ | |
| res = await db.execute( | |
| select(Quiz.id, Quiz.title, Quiz.created_at).order_by(Quiz.id) | |
| ) | |
| quizzes = res.all() | |
| if not quizzes: | |
| print("\nNo quizzes found.") | |
| else: | |
| keep = quizzes[0] | |
| to_delete = quizzes[1:] | |
| print(f"\nQuizzes: {len(quizzes)} total") | |
| print(f" KEEP [{keep.id}] {keep.title} ({keep.created_at})") | |
| for q in to_delete: | |
| print(f" DEL [{q.id}] {q.title} ({q.created_at})") | |
| if to_delete and not DRY_RUN: | |
| del_ids = [q.id for q in to_delete] | |
| # Count what will cascade-delete | |
| r_count = (await db.execute( | |
| select(Report.id).where(Report.quiz_id.in_(del_ids)) | |
| )).fetchall() | |
| sr_count = (await db.execute( | |
| select(StudentResult.id).where(StudentResult.quiz_id.in_(del_ids)) | |
| )).fetchall() | |
| rf_count = (await db.execute( | |
| select(RawFile.id).where(RawFile.quiz_id.in_(del_ids)) | |
| )).fetchall() | |
| await db.execute(delete(Quiz).where(Quiz.id.in_(del_ids))) | |
| print( | |
| f" β Deleted {len(to_delete)} quizzes " | |
| f"(+{len(r_count)} reports, {len(sr_count)} results, {len(rf_count)} files)" | |
| ) | |
| if not DRY_RUN: | |
| await db.commit() | |
| print("\nDone.") | |
| else: | |
| print("\nDry run complete β nothing deleted.") | |
| asyncio.run(main()) | |