230 lines
10 KiB
Python
230 lines
10 KiB
Python
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")
|
|
|
|
|