pdf-quiz-generator/backend/app/models/quiz.py
Daniel 45add79db3 Fix quiz delete (500 error), add trash bin, quiz resume, hide/publish, remove JS dialogs
Critical bug fix:
- DELETE /quizzes/{id} was returning 500 due to {\"quiz_id\": None} update using
  the DB column name instead of Python attribute name (source_quiz_id).
  Fixed with {QuestionModel.source_quiz_id: None, synchronize_session=False}

Extraction fix:
- Two-phase extraction was incorrectly triggering for PREP 2012/2014 format
  documents that have 'Preferred Response:' in their explanation text.
  Fix: check for inline 'Correct Answer:' first — if found, always use standard
  extraction regardless of 'Preferred Response:' appearing elsewhere.

Quiz trash bin:
- DELETE /quizzes/{id} now soft-deletes (sets deleted_at)
- GET /quizzes/trash — list deleted quizzes (moderator)
- PATCH /quizzes/{id}/restore — restore from trash
- DELETE /quizzes/{id}/permanent — permanent delete (must be in trash first)
- TrashPage.jsx — accessible via Settings → Admin → Trash

Hide/publish quizzes:
- is_published column on quizzes (1=visible, 0=hidden)
- PATCH /quizzes/{id}/publish?published=false — hide from regular users
- Moderators see all quizzes; regular users only see published
- 👁/🙈 toggle button per quiz card (moderators only)

Quiz progress resume (cross-browser via Redis):
- POST /attempts/progress — save {answers, current_idx, mode} to Redis (7 days)
- GET /attempts/progress?quiz_id=N — retrieve saved progress
- DELETE /attempts/progress/{quiz_id} — clear on submit
- QuizPage auto-saves to Redis every 1.5s (debounced) while in progress
- ModeSelectScreen loads saved progress from server, shows Resume button
- Works across browsers, devices, and after logout

Delete attempt:
- DELETE /attempts/{id} — user can delete own attempt + clears reminders for that quiz

ConfirmButton component:
- Replaces all window.confirm() / window.prompt() across the app
- Double-click pattern: first click shows [Confirm] [Cancel] inline
- Applied to: QuizzesPage, DocumentDetailPage, QuizEditPage, QuestionBankPage

Category delete (QuestionBankPage):
- window.prompt() replaced with inline modal dialog with select dropdown
- User chooses where to move questions before deletion

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 04:51:41 +02:00

39 lines
2 KiB
Python

from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
from app.database import Base
from app.models.quiz_category import QuizCategory # noqa — ensures mapper resolves "QuizCategory"
from app.models.quiz_question_link import QuizQuestionLink # noqa
class Quiz(Base):
__tablename__ = "quizzes"
id = Column(Integer, primary_key=True, index=True)
section_id = Column(Integer, ForeignKey("sections.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
category_id = Column(Integer, ForeignKey("quiz_categories.id", ondelete="SET NULL"), nullable=True)
title = Column(String, nullable=False)
questions_count = Column(Integer, default=0)
time_limit_minutes = Column(Integer, nullable=True) # null = no limit
mode = Column(String, default="timed") # timed, learning
skipped_questions = Column(Text, nullable=True) # JSON list of skipped question texts
created_at = Column(DateTime, default=datetime.utcnow)
deleted_at = Column(DateTime, nullable=True) # soft delete — null = active
is_published = Column(Integer, default=1) # 1 = visible to users, 0 = hidden (admin only)
section = relationship("Section", back_populates="quizzes")
user = relationship("User", back_populates="quizzes")
category = relationship("QuizCategory", back_populates="quizzes", foreign_keys=[category_id])
questions = relationship(
"Question",
secondary="quiz_question_links",
primaryjoin="Quiz.id == foreign(QuizQuestionLink.quiz_id)",
secondaryjoin="Question.id == foreign(QuizQuestionLink.question_id)",
order_by="QuizQuestionLink.position",
viewonly=True, # mutations handled explicitly via QuizQuestionLink
)
attempts = relationship("QuizAttempt", back_populates="quiz", cascade="all, delete-orphan")
reminders = relationship("ReminderSchedule", cascade="all, delete-orphan", foreign_keys="ReminderSchedule.quiz_id")