taboola
quiz/bank wipe option, LLM answer check
f66acbf
Raw
History Blame Contribute Delete
3.9 kB
"""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())