from datetime import datetime from typing import Any from sqlalchemy import ( ARRAY, BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db import Base class Role(Base): __tablename__ = "roles" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(64), unique=True) permissions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) is_system: Mapped[bool] = mapped_column(Boolean, default=False) class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String(64), unique=True) password_hash: Mapped[str] = mapped_column(String(255)) role_id: Mapped[int] = mapped_column(ForeignKey("roles.id")) is_active: Mapped[bool] = mapped_column(Boolean, default=True) must_change_password: Mapped[bool] = mapped_column(Boolean, default=True) must_change_username: Mapped[bool] = mapped_column(Boolean, default=False) token_version: Mapped[int] = mapped_column(Integer, default=0) f95_session: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) role: Mapped[Role] = relationship(lazy="joined") @property def permissions(self) -> list[str]: return sorted(self.role.permissions) if self.is_active else [] class Game(Base): __tablename__ = "games" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] = mapped_column(String(255)) description: Mapped[str] = mapped_column(Text, default="") tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) engine: Mapped[str] = mapped_column(String(32), default="renpy") cover_hash: Mapped[str | None] = mapped_column(String(80)) gallery_hashes: Mapped[list[str]] = mapped_column(ARRAY(String(80)), default=list, server_default="{}") # the version "Play" opens; translations are published by merging into it main_version_id: Mapped[int | None] = mapped_column( ForeignKey("game_versions.id", ondelete="SET NULL", use_alter=True)) created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL")) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) created_by: Mapped[User | None] = relationship(lazy="joined") class GameVersion(Base): __tablename__ = "game_versions" id: Mapped[int] = mapped_column(primary_key=True) game_id: Mapped[int] = mapped_column(ForeignKey("games.id", ondelete="CASCADE"), index=True) parent_id: Mapped[int | None] = mapped_column(ForeignKey("game_versions.id", ondelete="SET NULL")) owner_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL")) name: Mapped[str] = mapped_column(String(255)) description: Mapped[str] = mapped_column(Text, default="") # uploading -> queued -> converting -> ready | failed; importing = app.import_dir run status: Mapped[str] = mapped_column(String(16), default="uploading", index=True) # draft: visible to the owner and reviewers only visibility: Mapped[str] = mapped_column(String(16), default="published") kind: Mapped[str] = mapped_column(String(16), default="original") # original|translation|mod progress: Mapped[str] = mapped_column(String(255), default="") upload_name: Mapped[str | None] = mapped_column(String(255)) manifest: Mapped[dict[str, Any] | None] = mapped_column(JSONB) revision_id: Mapped[str | None] = mapped_column(String(36)) base_revision_id: Mapped[str | None] = mapped_column(String(36)) report: Mapped[dict[str, Any] | None] = mapped_column(JSONB) size_bytes: Mapped[int] = mapped_column(BigInteger, default=0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) owner: Mapped[User | None] = relationship(lazy="joined") class VersionHistory(Base): """Previous manifests of a version that was changed in place (e.g. a merged translation).""" __tablename__ = "version_history" id: Mapped[int] = mapped_column(primary_key=True) version_id: Mapped[int] = mapped_column(ForeignKey("game_versions.id", ondelete="CASCADE"), index=True) user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL")) note: Mapped[str] = mapped_column(String(255), default="") manifest: Mapped[dict[str, Any]] = mapped_column(JSONB) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Revision(Base): """Immutable snapshots retained independently of deletable version cards.""" __tablename__ = "revisions" id: Mapped[str] = mapped_column(String(36), primary_key=True) game_id: Mapped[int] = mapped_column(ForeignKey("games.id", ondelete="CASCADE"), index=True) user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL")) parents: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) manifest: Mapped[dict[str, Any]] = mapped_column(JSONB) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class Save(Base): __tablename__ = "saves" __table_args__ = (UniqueConstraint("user_id", "version_id", "slot"),) id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) version_id: Mapped[int] = mapped_column(ForeignKey("game_versions.id", ondelete="CASCADE")) slot: Mapped[str] = mapped_column(String(32)) title: Mapped[str] = mapped_column(String(255), default="") data: Mapped[dict[str, Any]] = mapped_column(JSONB) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) from app import revisions as _revisions # noqa: E402,F401