pdf-quiz-generator/backend/app/models/question.py
Daniel 8137d5b20c Decouple course quizzes, replace passlib, add security hardening
- Fully decouple course quizzes from main quiz system (hidden from
  dashboard stats, history, search, results page)
- Course quiz results show "Back to Course" instead of retake/delete
- Add allow_review toggle for course creators to control answer review
- Show quiz title on course page, hide pool size from students
- Add course thumbnails to browse cards
- Replace passlib with bcrypt directly (compatible with existing hashes)
- Add HIBP breached password warnings on register/reset/change password
- Add CLI management tools (reset-password, set-role, stats, etc.)
- Fix quiz PATCH endpoint: ownership check instead of moderator-only
- Add max_length validation on course/module/lesson titles
- Fix score display bug on results page (0 of N when review disabled)
- Fix question count on course quiz start (show per-attempt, not pool)
- Improve suspend warning for timed course quizzes with max attempts
- Clean up validation error messages (show "Invalid email" not Pydantic dump)
- Add DDL migration for allow_review column

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 21:31:05 +02:00

31 lines
1.7 KiB
Python

from pgvector.sqlalchemy import Vector
from sqlalchemy import Column, Integer, String, Text, JSON, ForeignKey
from sqlalchemy.orm import relationship, deferred
from app.config import settings
from app.database import Base
from app.models.question_category import QuestionCategory # noqa — ensures mapper resolves
from app.models.quiz_question_link import QuizQuestionLink # noqa
class Question(Base):
__tablename__ = "questions"
id = Column(Integer, primary_key=True, index=True)
# source_quiz_id: which quiz this question was originally extracted for (informational).
# Content membership is tracked via quiz_question_links junction table.
source_quiz_id = Column("quiz_id", Integer, ForeignKey("quizzes.id", ondelete="SET NULL"), nullable=True)
question_category_id = Column(Integer, ForeignKey("question_categories.id", ondelete="SET NULL"), nullable=True)
question_text = Column(Text, nullable=False)
question_type = Column(String, nullable=False) # mcq, true_false, fill_blank
options = Column(JSON, nullable=True) # list of strings for mcq
correct_answer = Column(String, nullable=False)
explanation = Column(Text, nullable=True)
page_reference = Column(Integer, nullable=True)
image_path = Column(String, nullable=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
is_shared = Column(Integer, default=1) # 1 = visible in bank, 0 = private (only owner sees it)
embedding = deferred(Column(Vector(1024), nullable=True)) # semantic search vector — deferred: not loaded in standard queries
question_category = relationship("QuestionCategory", back_populates="questions",
foreign_keys=[question_category_id])