#!/usr/bin/env python3 """PedQuiz management CLI. Usage (inside container): python manage.py reset-password python manage.py list-users python manage.py reembed # Regenerate all question embeddings python manage.py extract # Run extraction from CLI python manage.py list-sections [doc_id] # List documents/sections python manage.py jobs # Show recent extraction jobs """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) def reset_password(email: str, new_password: str): from app.database import SessionLocal from app.models.user import User from app.utils.auth import get_password_hash if len(new_password) < 8: print("Error: Password must be at least 8 characters"); sys.exit(1) db = SessionLocal() try: user = db.query(User).filter(User.email == email).first() if not user: print(f"Error: No user found with email '{email}'"); sys.exit(1) user.hashed_password = get_password_hash(new_password) db.commit() print(f"Password reset for {user.email} (name={user.name!r}, role={user.role})") finally: db.close() def list_users(): from app.database import SessionLocal from app.models.user import User from app.models.email_verification import EmailVerification db = SessionLocal() try: users = db.query(User).order_by(User.created_at).all() print(f"{'ID':>4} {'Email':40} {'Role':12} {'Verified':8} Name") print("-" * 90) for u in users: v = db.query(EmailVerification).filter(EmailVerification.user_id == u.id).first() verified = "yes" if (v and v.verified_at) else "no" print(f"{u.id:>4} {u.email:40} {u.role:12} {verified:8} {u.name}") finally: db.close() def reembed(): from app.database import SessionLocal from app.models.question import Question from app.services import embedding_service db = SessionLocal() try: questions = db.query(Question).all() print(f"Re-embedding {len(questions)} questions...") ok = fail = 0 for q in questions: q.embedding = None if embedding_service.embed_question(q): ok += 1 else: fail += 1 db.commit() print(f"Done: {ok} succeeded, {fail} failed") finally: db.close() def list_sections(doc_id=None): from app.database import SessionLocal from app.models.pdf_document import PDFDocument from app.models.section import Section db = SessionLocal() try: docs_q = db.query(PDFDocument) if doc_id: docs_q = docs_q.filter(PDFDocument.id == int(doc_id)) docs = docs_q.order_by(PDFDocument.id).all() for doc in docs: print(f"Doc {doc.id}: {doc.original_filename} ({doc.status}, {doc.total_pages or '?'} pages)") sections = db.query(Section).filter(Section.document_id == doc.id).all() if sections: for s in sections: print(f" Section {s.id}: {s.name!r} pages {s.start_page}–{s.end_page}") else: print(" (no sections defined yet)") finally: db.close() def extract(section_id: int, title: str, mode: str, email: str, background: bool): """Run quiz extraction — either in background (Celery) or inline (blocking).""" from app.database import SessionLocal from app.models.user import User from app.models.section import Section import uuid db = SessionLocal() try: section = db.query(Section).filter(Section.id == section_id).first() if not section: print(f"Error: Section {section_id} not found"); sys.exit(1) user = db.query(User).filter(User.email == email).first() if not user: print(f"Error: User '{email}' not found"); sys.exit(1) if not title: title = f"Quiz: {section.name}" print(f"Extracting from section {section_id}: {section.name!r} (pages {section.start_page}–{section.end_page})") print(f"Title: {title!r} Mode: {mode} User: {email}") if background: job_id = str(uuid.uuid4()) try: from app.tasks.quiz_tasks import extract_quiz import redis as redis_lib from app.config import settings r = redis_lib.from_url(settings.REDIS_URL, decode_responses=True) r.set(f"extraction:status:{job_id}", "pending", ex=3600) r.set(f"extraction:job_title:{job_id}", title, ex=3600) r.lpush(f"extraction:user_jobs:{user.id}", job_id) r.expire(f"extraction:user_jobs:{user.id}", 86400) extract_quiz.delay(job_id=job_id, user_id=user.id, section_id=section_id, title=title, mode=mode, time_limit_minutes=None, model_id=None, question_category_id=None) print(f"\nJob started in background: {job_id}") print(f"Monitor: python manage.py jobs") except Exception as e: print(f"Error dispatching to Celery: {e}") print("Falling back to inline extraction...") _extract_inline(db, user, section_id, title, mode) else: _extract_inline(db, user, section_id, title, mode) finally: db.close() def _extract_inline(db, user, section_id, title, mode): """Run extraction synchronously with live output.""" from app.services.quiz_service import create_quiz_from_section print("\nRunning extraction (this may take several minutes for large sections)...") try: quiz = create_quiz_from_section( db=db, user_id=user.id, section_id=section_id, title=title, mode=mode, ) print(f"\nDone! Quiz created: id={quiz.id}, title={quiz.title!r}, questions={quiz.questions_count}") except Exception as e: print(f"\nExtraction failed: {e}") sys.exit(1) def show_jobs(user_email=None): import redis, json from app.config import settings from app.database import SessionLocal from app.models.user import User r = redis.from_url(settings.REDIS_URL, decode_responses=True) db = SessionLocal() try: if user_email: user = db.query(User).filter(User.email == user_email).first() if not user: print(f"User not found: {user_email}"); return users = [user] else: users = db.query(User).all() for user in users: job_ids = r.lrange(f"extraction:user_jobs:{user.id}", 0, 9) if not job_ids: continue print(f"\n{user.email}:") for jid in job_ids: status = r.get(f"extraction:status:{jid}") or "expired" title = r.get(f"extraction:job_title:{jid}") or "?" steps = r.lrange(f"extraction:steps:{jid}", 0, -1) quiz_id = r.get(f"extraction:quiz_id:{jid}") last = json.loads(steps[-1]).get("message", "") if steps else "" print(f" [{status:10}] {title[:40]:40} steps={len(steps):3}" + (f" quiz_id={quiz_id}" if quiz_id else "") + (f"\n {last[:80]}" if last else "")) finally: db.close() if __name__ == "__main__": parser = argparse.ArgumentParser(description="PedQuiz management CLI") sub = parser.add_subparsers(dest="command") rp = sub.add_parser("reset-password", help="Reset a user password") rp.add_argument("email"); rp.add_argument("password") sub.add_parser("list-users", help="List all users") sub.add_parser("reembed", help="Regenerate all question embeddings") ls = sub.add_parser("list-sections", help="List documents and sections") ls.add_argument("doc_id", nargs="?", help="Optional: filter by document ID") ex = sub.add_parser("extract", help="Run quiz extraction from a section") ex.add_argument("section_id", type=int, help="Section ID to extract from") ex.add_argument("--title", default="", help="Quiz title (default: auto)") ex.add_argument("--mode", default="timed", choices=["timed", "learning"]) ex.add_argument("--user", default="", help="User email to assign quiz to (default: first admin)") ex.add_argument("--bg", action="store_true", help="Run in background via Celery") jb = sub.add_parser("jobs", help="Show recent extraction jobs") jb.add_argument("--user", default=None, help="Filter by user email") args = parser.parse_args() if args.command == "reset-password": reset_password(args.email, args.password) elif args.command == "list-users": list_users() elif args.command == "reembed": reembed() elif args.command == "list-sections": list_sections(args.doc_id) elif args.command == "extract": email = args.user if not email: from app.database import SessionLocal from app.models.user import User db = SessionLocal() admin = db.query(User).filter(User.role == "admin").first() email = admin.email if admin else "" db.close() if not email: print("Error: No admin user found. Use --user "); sys.exit(1) extract(args.section_id, args.title, args.mode, email, args.bg) elif args.command == "jobs": show_jobs(args.user) else: parser.print_help()