from __future__ import annotations from datetime import datetime, timezone from typing import List, Optional import pytz from flask_sqlalchemy import SQLAlchemy from flask import current_app from sqlalchemy import Boolean, Column, DateTime, String, TypeDecorator, ForeignKey, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship db : SQLAlchemy = SQLAlchemy() deck_password = db.Table( "deck_password", Column("deck_id", db.Integer, db.ForeignKey("deck.id"), primary_key=True), Column("password_id", db.Integer, db.ForeignKey("password.id"), primary_key=True) ) deck_token = db.Table( "deck_token", Column("deck_id", db.Integer, db.ForeignKey("deck.id"), primary_key=True), Column("token_id", db.Integer, db.ForeignKey("token.id"), primary_key=True) ) class UTCDateTime(TypeDecorator): impl = DateTime def process_bind_param(self, value, dialect): # Ensure datetime is stored in naive UTC if value is not None and value.tzinfo is not None: value = value.astimezone(pytz.utc).replace(tzinfo=None) return value def process_result_value(self, value, dialect): # Re-attach UTC tzinfo when reading if value is not None: value = value.replace(tzinfo=pytz.utc) return value class Password(db.Model): __tablename__ = "password" id : Mapped[int] = mapped_column(primary_key=True) value : Mapped[str] = mapped_column(String(60)) expires_at: Mapped[Optional[datetime]] = mapped_column(UTCDateTime()) # Optional expiration decks : Mapped[List[Deck]] = relationship("Deck", secondary=deck_password, back_populates='passwords') def __init__(self, value: str, expires_at: datetime|None) -> None: self.value = value self.expires_at = expires_at def is_expired(self) -> bool: """Check if the password is expired""" return self.expires_at is not None and datetime.now(timezone.utc) > self.expires_at def __repr__(self) -> str: return f"" class Deck(db.Model): __tablename__ = "deck" __table_args__ = ( UniqueConstraint('course_id', 'deck', name='_course_deck_uc'), ) id : Mapped[int] = mapped_column(primary_key=True) name : Mapped[str] = mapped_column(String(60), unique=True, nullable=False) deck : Mapped[str] = mapped_column(String(60), nullable=False) description : Mapped[str] = mapped_column(String(60)) index_file: Mapped[str] = mapped_column(String(40), nullable=False) passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks') tokens : Mapped[List[Token]] = relationship("Token", secondary=deck_token, back_populates='decks') course_id: Mapped[int] = mapped_column(ForeignKey("course.id")) course: Mapped['Course'] = relationship("Course", back_populates='decks') def __init__(self, name: str, deck: str, course: Course, description: str = "", index_file: str = "index.html") -> None: self.name = name self.deck = deck self.description = description self.course = course self.index_file = index_file def __repr__(self) -> str: return f"" class Token(db.Model): __tablename__ = "token" id : Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False) active_course_id: Mapped[int] = mapped_column(ForeignKey("course.id"), nullable=True) active_course: Mapped[Course] = relationship("Course", foreign_keys=[active_course_id]) decks : Mapped[List[Deck]]= relationship("Deck", secondary=deck_token, back_populates='tokens') def __init__(self, name: str) -> None: self.name = name self.active_course_id = current_app.config["DEFAULT_COURSE"] def __repr__(self) -> str: return f"" def is_unlocked(self, deck: str) -> bool : return deck in [deck.deck for deck in self.decks] class Course(db.Model): __tablename__ = "course" id : Mapped[int] = mapped_column(primary_key=True) folder: Mapped[str] = mapped_column(String(20), unique=True, nullable=False) name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False) decks: Mapped[List['Deck']] = relationship(back_populates='course') def __init__(self, name: str, folder: str) -> None: self.name = name self.folder = folder def __repr__(self) -> str: return f"" class InfoBanner(db.Model): __tablename__ = "info_banner" id : Mapped[int] = mapped_column(primary_key=True) content : Mapped[str] = mapped_column(String(280)) is_active : Mapped[bool] = mapped_column(Boolean) def __init__(self, content : str) -> None: self.content = content self.is_active = False