from __future__ import annotations from datetime import datetime from enum import Enum from typing import List, Optional from sqlalchemy import Boolean, DateTime, Enum as SAEnum, ForeignKey, Integer, String, Text, UniqueConstraint, Float, JSON from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.db import Base class UserRole(str, Enum): admin = "admin" learner = "learner" class ModuleType(str, Enum): content = "content" quiz = "quiz" class ProgressStatus(str, Enum): locked = "locked" unlocked = "unlocked" completed = "completed" class QuestionType(str, Enum): mcq = "mcq" class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) email: Mapped[str] = mapped_column(String(255), unique=True, index=True) password_hash: Mapped[str] = mapped_column(String(255)) role: Mapped[UserRole] = mapped_column(SAEnum(UserRole, name="role_enum"), default=UserRole.learner) is_active: Mapped[bool] = mapped_column(Boolean, default=True) oauth_provider: Mapped[Optional[str]] = mapped_column(String(50)) oauth_subject: Mapped[Optional[str]] = mapped_column(String(255)) picture_url: Mapped[Optional[str]] = mapped_column(String(512)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) enrollments: Mapped[List[Enrollment]] = relationship("Enrollment", back_populates="user") group_memberships: Mapped[List[GroupMembership]] = relationship("GroupMembership", back_populates="user") class Course(Base): __tablename__ = "courses" id: Mapped[int] = mapped_column(Integer, primary_key=True) title: Mapped[str] = mapped_column(String(255)) description: Mapped[Optional[str]] = mapped_column(Text) is_published: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) modules: Mapped[List[Module]] = relationship("Module", back_populates="course", cascade="all, delete-orphan") course_groups: Mapped[List[CourseGroup]] = relationship("CourseGroup", back_populates="course", cascade="all, delete-orphan") class Module(Base): __tablename__ = "modules" id: Mapped[int] = mapped_column(Integer, primary_key=True) course_id: Mapped[int] = mapped_column(ForeignKey("courses.id", ondelete="CASCADE"), index=True) order_index: Mapped[int] = mapped_column(Integer) type: Mapped[ModuleType] = mapped_column(SAEnum(ModuleType, name="module_type_enum")) title: Mapped[str] = mapped_column(String(255)) content_text: Mapped[Optional[str]] = mapped_column(Text) pass_score: Mapped[Optional[int]] = mapped_column(Integer) upload_id: Mapped[Optional[int]] = mapped_column(ForeignKey("uploads.id", ondelete="SET NULL"), index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) course: Mapped[Course] = relationship("Course", back_populates="modules") questions: Mapped[List[QuizQuestion]] = relationship("QuizQuestion", back_populates="module", cascade="all, delete-orphan") upload: Mapped[Optional["Upload"]] = relationship("Upload", back_populates="modules") class QuizQuestion(Base): __tablename__ = "quiz_questions" id: Mapped[int] = mapped_column(Integer, primary_key=True) module_id: Mapped[int] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), index=True) prompt: Mapped[str] = mapped_column(Text) question_type: Mapped[QuestionType] = mapped_column(SAEnum(QuestionType, name="question_type_enum"), default=QuestionType.mcq) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) module: Mapped[Module] = relationship("Module", back_populates="questions") choices: Mapped[List[QuizChoice]] = relationship("QuizChoice", back_populates="question", cascade="all, delete-orphan") class QuizChoice(Base): __tablename__ = "quiz_choices" id: Mapped[int] = mapped_column(Integer, primary_key=True) question_id: Mapped[int] = mapped_column(ForeignKey("quiz_questions.id", ondelete="CASCADE"), index=True) text: Mapped[str] = mapped_column(Text) is_correct: Mapped[bool] = mapped_column(Boolean, default=False) question: Mapped[QuizQuestion] = relationship("QuizQuestion", back_populates="choices") class Enrollment(Base): __tablename__ = "enrollments" __table_args__ = (UniqueConstraint("user_id", "course_id", name="uq_enrollment"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) course_id: Mapped[int] = mapped_column(ForeignKey("courses.id", ondelete="CASCADE"), index=True) enrolled_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[User] = relationship("User", back_populates="enrollments") course: Mapped[Course] = relationship("Course") class ModuleProgress(Base): __tablename__ = "module_progress" __table_args__ = (UniqueConstraint("user_id", "module_id", name="uq_module_progress"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) module_id: Mapped[int] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), index=True) status: Mapped[ProgressStatus] = mapped_column(SAEnum(ProgressStatus, name="progress_status_enum"), default=ProgressStatus.locked) completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) score: Mapped[Optional[float]] = mapped_column(Float) class QuizAttempt(Base): __tablename__ = "quiz_attempts" id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) module_id: Mapped[int] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), index=True) submitted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) score: Mapped[float] = mapped_column(Float) passed: Mapped[bool] = mapped_column(Boolean) answers_json: Mapped[dict] = mapped_column(JSON) class Group(Base): __tablename__ = "groups" id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(255), unique=True, index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) memberships: Mapped[List[GroupMembership]] = relationship("GroupMembership", back_populates="group", cascade="all, delete-orphan") course_groups: Mapped[List[CourseGroup]] = relationship("CourseGroup", back_populates="group", cascade="all, delete-orphan") class GroupMembership(Base): __tablename__ = "group_memberships" __table_args__ = (UniqueConstraint("group_id", "user_id", name="uq_group_membership"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) group_id: Mapped[int] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), index=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) group: Mapped[Group] = relationship("Group", back_populates="memberships") user: Mapped[User] = relationship("User", back_populates="group_memberships") class CourseGroup(Base): __tablename__ = "course_groups" __table_args__ = (UniqueConstraint("course_id", "group_id", name="uq_course_group"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) course_id: Mapped[int] = mapped_column(ForeignKey("courses.id", ondelete="CASCADE"), index=True) group_id: Mapped[int] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), index=True) course: Mapped[Course] = relationship("Course", back_populates="course_groups") group: Mapped[Group] = relationship("Group", back_populates="course_groups") class Upload(Base): __tablename__ = "uploads" id: Mapped[int] = mapped_column(Integer, primary_key=True) filename: Mapped[str] = mapped_column(String(255)) content_type: Mapped[str] = mapped_column(String(100)) file_path: Mapped[str] = mapped_column(String(512)) size_bytes: Mapped[int] = mapped_column(Integer) uploaded_by: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) uploader: Mapped[User] = relationship("User") modules: Mapped[List[Module]] = relationship("Module", back_populates="upload") class Achievement(Base): __tablename__ = "achievements" id: Mapped[int] = mapped_column(Integer, primary_key=True) code: Mapped[str] = mapped_column(String(100), unique=True, index=True) name: Mapped[str] = mapped_column(String(255)) description: Mapped[Optional[str]] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user_achievements: Mapped[List["UserAchievement"]] = relationship("UserAchievement", back_populates="achievement", cascade="all, delete-orphan") class UserAchievement(Base): __tablename__ = "user_achievements" __table_args__ = (UniqueConstraint("user_id", "achievement_id", name="uq_user_achievement"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) achievement_id: Mapped[int] = mapped_column(ForeignKey("achievements.id", ondelete="CASCADE"), index=True) earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[User] = relationship("User") achievement: Mapped[Achievement] = relationship("Achievement", back_populates="user_achievements") class LoginEvent(Base): __tablename__ = "login_events" id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) logged_in_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) user: Mapped[User] = relationship("User")