Question sharing (edits now propagate): - quiz_question_links junction table: quizzes reference questions, not own copies - Existing questions migrated to junction (idempotent ON CONFLICT DO NOTHING) - from-bank and from-category create quiz by adding junction links (no copying) - Editing a question via QuizEditPage or bank Edit modal updates the ONE record visible in all quizzes that reference it - Delete from quiz: only deletes Question record if no other quiz references it - quiz_service: fixed position counter (was using len() incorrectly, now enumerate) - question.quiz_id renamed to source_quiz_id in Python (DB column still quiz_id) - All attempts/quizzes/search code updated to use junction helper or source_quiz_id Bug fixes: - PostgreSQL en_US.utf8 collation B-tree index corruption on users.email Fixed by ALTERing column to COLLATE "C" in migration + immediate repair - get_current_user now blocks unverified users on ALL endpoints (403 + X-Unverified header) api/client.js intercepts X-Unverified=true and auto-logs out to /login - Search (quiz + bank): correct_answer and explanation now included in matching_questions so study modals work correctly - Keyword search: order_by updated to use source_quiz_id - Bulk category: changed from PATCH with body array to POST with Pydantic body (fixes null category_id removal) - Question bank: edit button added, answer highlighting removed from list view - QuestionBankPage: Keyword/Semantic/Hybrid search toggle - Login: login error message stays visible (removed immediate reload) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
37 lines
1.8 KiB
Python
37 lines
1.8 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)
|
|
|
|
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")
|