elemes/services/models.py

119 lines
4.7 KiB
Python

"""
Model SQLAlchemy 2.0 — source of truth schema migrasi CSV→PostgreSQL.
users — identitas + role eksplisit (teacher/student)
access_tokens — hash token (HMAC-SHA256 + pepper), bukan plaintext
lessons — registry materi (konten tetap Markdown; DB hanya metadata)
student_progress— status per (user, lesson), unik; skor terstruktur utk state 'scored'
"""
from datetime import datetime
from uuid import uuid4
from sqlalchemy import (
Boolean,
CheckConstraint,
DateTime,
ForeignKey,
Integer,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from services.database import Base
def _uuid() -> str:
return str(uuid4())
class User(Base):
__tablename__ = "users"
__table_args__ = (CheckConstraint("role IN ('teacher','student')", name="ck_users_role"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(10), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
tokens: Mapped[list["AccessToken"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
progress: Mapped[list["StudentProgress"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
class AccessToken(Base):
__tablename__ = "access_tokens"
__table_args__ = (UniqueConstraint("token_hash", name="uq_access_tokens_token_hash"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
token_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
user: Mapped["User"] = relationship(back_populates="tokens")
class Lesson(Base):
__tablename__ = "lessons"
__table_args__ = (UniqueConstraint("slug", name="uq_lessons_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
slug: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
order_index: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
progress: Mapped[list["StudentProgress"]] = relationship(back_populates="lesson")
class StudentProgress(Base):
__tablename__ = "student_progress"
__table_args__ = (
UniqueConstraint("user_id", "lesson_id", name="uq_student_progress_user_lesson"),
CheckConstraint(
"state IN ('not_started','completed','scored')", name="ck_student_progress_state"
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
user_id: Mapped[str] = mapped_column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
lesson_id: Mapped[str] = mapped_column(
String(36), ForeignKey("lessons.id", ondelete="CASCADE"), nullable=False
)
state: Mapped[str] = mapped_column(
String(20), nullable=False, default="not_started", server_default="not_started"
)
score_earned: Mapped[int | None] = mapped_column(Integer, nullable=True)
score_total: Mapped[int | None] = mapped_column(Integer, nullable=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
user: Mapped["User"] = relationship(back_populates="progress")
lesson: Mapped["Lesson"] = relationship(back_populates="progress")