#!/usr/bin/env python3 """Beta Demo Clear Script — DocDoe AI. Removes ONLY the data created by seed_beta_demo.py: - Video render jobs owned by demo@docdoe.in - Document chunks owned by demo@docdoe.in - Documents owned by demo@docdoe.in (+ stub files on disk) - UserPlan owned by demo@docdoe.in - StudyProfile owned by demo@docdoe.in - All normalized learning-state rows owned by demo@docdoe.in - The demo user account itself Safe to run when demo data is absent (no-ops on missing rows). Does NOT touch any other user data. Usage: cd backend python scripts/clear_beta_demo.py python scripts/clear_beta_demo.py --dry-run """ from __future__ import annotations import argparse import sys from pathlib import Path # ── Path setup ──────────────────────────────────────────────────────────────── BACKEND_DIR = Path(__file__).resolve().parents[1] if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) DEMO_EMAIL = "demo@docdoe.in" def clear(dry_run: bool = False) -> None: from app.core.database import SessionLocal, init_db from app.models.document import Document from app.models.document_chunk import DocumentChunk from app.models.learning_state import ( Chapter, DailyTask, GeneratedResource, LessonProgress, QuizAttempt, StudentProfileState, StudyPlan, StudySession, Subject, Subscription, TopicMastery, UsageEvent, ) from app.models.study_profile import StudyProfile from app.models.user import User from app.models.user_plan import UserPlan from app.models.video_render_job import VideoRenderJob init_db() prefix = "[DRY RUN] " if dry_run else "" with SessionLocal() as db: user = db.query(User).filter(User.email == DEMO_EMAIL).first() if user is None: print(f"Demo user '{DEMO_EMAIL}' not found — nothing to clear.") return uid = user.id print(f"{prefix}Clearing demo data for user {uid} ({DEMO_EMAIL})") # ── 1. Video render jobs ────────────────────────────────────────────── jobs = db.query(VideoRenderJob).filter(VideoRenderJob.user_id == uid).all() print(f"{prefix} Video jobs : {len(jobs)}") if not dry_run: for job in jobs: db.delete(job) db.flush() # ── 2. Document chunks + documents + stub files ─────────────────────── docs = db.query(Document).filter(Document.user_id == uid).all() chunk_total = 0 file_total = 0 for doc in docs: chunks = ( db.query(DocumentChunk) .filter(DocumentChunk.document_id == doc.id) .all() ) chunk_total += len(chunks) if not dry_run: for chunk in chunks: db.delete(chunk) if not dry_run: db.flush() # flush chunks before deleting parent docs for doc in docs: # Remove stub file from disk if it is a seeded placeholder if doc.file_path: stub = Path(doc.file_path) if stub.exists() and stub.name.startswith("demo_"): file_total += 1 if not dry_run: stub.unlink(missing_ok=True) if not dry_run: db.delete(doc) if not dry_run: db.flush() # flush docs before deleting user print(f"{prefix} Documents : {len(docs)} (chunks: {chunk_total}, stub files: {file_total})") # ── 3. User plan ────────────────────────────────────────────────────── plans = db.query(UserPlan).filter(UserPlan.user_id == uid).all() print(f"{prefix} User plans : {len(plans)}") if not dry_run: for p in plans: db.delete(p) db.flush() # ── 4. Study profile ────────────────────────────────────────────────── profiles = db.query(StudyProfile).filter(StudyProfile.user_id == uid).all() print(f"{prefix} Study profiles : {len(profiles)}") if not dry_run: for sp in profiles: db.delete(sp) db.flush() # ── 5. Normalized learning state ────────────────────────────────────── learning_models = [ (UsageEvent, "Usage events"), (GeneratedResource, "Generated resources"), (TopicMastery, "Topic mastery"), (QuizAttempt, "Quiz attempts"), (LessonProgress, "Lesson progress"), (StudySession, "Study sessions"), (DailyTask, "Daily tasks"), (StudyPlan, "Study plans"), (Chapter, "Chapters"), (Subject, "Subjects"), (Subscription, "Subscriptions"), (StudentProfileState, "Student profile state"), ] for model, label in learning_models: rows = db.query(model).filter(model.user_id == uid).all() print(f"{prefix} {label:<22}: {len(rows)}") if not dry_run: for row in rows: db.delete(row) db.flush() # ── 6. User ─────────────────────────────────────────────────────────── print(f"{prefix} User account : 1 ({DEMO_EMAIL})") if not dry_run: db.delete(user) if not dry_run: db.commit() print() print("[OK] Demo data cleared.") else: print() print("Dry run complete — no changes made.") print("Run without --dry-run to delete.") # ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == "__main__": parser = argparse.ArgumentParser(description="Remove beta demo seed data from DocDoe AI.") parser.add_argument( "--dry-run", action="store_true", help="Show what would be deleted without making changes.", ) args = parser.parse_args() clear(dry_run=args.dry_run)