lomda-hub/backend/app/models.py
2026-02-04 16:50:33 +02:00

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")