trymeapp/app/models.py

237 lines
9.3 KiB
Python
Raw Permalink Normal View History

2026-11-11 22:06:26 +00:00
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import (
JSON,
Boolean,
DateTime,
Float,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
def utcnow() -> datetime:
return datetime.now(timezone.utc)
AVATAR_ANIMALS = (
"fox",
"owl",
"bear",
"otter",
"koala",
"tiger",
"frog",
"penguin",
)
AVATAR_COLORS = (
"avatar-1",
"avatar-2",
"avatar-3",
"avatar-4",
"avatar-5",
"avatar-6",
"avatar-7",
"avatar-8",
)
GENDER_OPTIONS = (
"woman",
"man",
"non_binary",
"agender",
"genderqueer",
"two_spirit",
"other",
"prefer_not_to_say",
)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
auth_sub: Mapped[str] = mapped_column(String(190), unique=True, nullable=False, index=True)
email: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
nickname: Mapped[str] = mapped_column(String(48), unique=True, nullable=False, index=True)
first_name: Mapped[str] = mapped_column(String(80), nullable=False, default="")
last_name: Mapped[str] = mapped_column(String(80), nullable=False, default="")
age: Mapped[int | None] = mapped_column(Integer, nullable=True)
gender: Mapped[str] = mapped_column(String(40), nullable=False, default="prefer_not_to_say")
gender_custom: Mapped[str | None] = mapped_column(String(80), nullable=True)
avatar_animal: Mapped[str] = mapped_column(String(32), nullable=False, default="fox")
avatar_color: Mapped[str] = mapped_column(String(32), nullable=False, default="avatar-1")
public_website_consent: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
consent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
onboarded: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_banned: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
ban_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utcnow)
api_keys: Mapped[list[ApiKey]] = relationship(
"ApiKey", back_populates="user", cascade="all, delete-orphan"
)
quizzes: Mapped[list[Quiz]] = relationship(
"Quiz", back_populates="author", cascade="all, delete-orphan"
)
attempts: Mapped[list[QuizAttempt]] = relationship(
"QuizAttempt", back_populates="user", cascade="all, delete-orphan"
)
votes: Mapped[list[QuizVote]] = relationship(
"QuizVote", back_populates="user", cascade="all, delete-orphan"
)
favourites: Mapped[list[Favourite]] = relationship(
"Favourite",
back_populates="user",
cascade="all, delete-orphan",
order_by="Favourite.position",
)
class ApiKey(Base):
__tablename__ = "api_keys"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(String(80), nullable=False)
key_prefix: Mapped[str] = mapped_column(String(24), nullable=False)
key_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=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, default=utcnow)
user: Mapped[User] = relationship("User", back_populates="api_keys")
class Quiz(Base):
__tablename__ = "quizzes"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
share_slug: Mapped[str] = mapped_column(String(40), unique=True, nullable=False, index=True)
author_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String(140), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
is_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True)
is_removed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
author: Mapped[User] = relationship("User", back_populates="quizzes")
questions: Mapped[list[Question]] = relationship(
"Question",
back_populates="quiz",
cascade="all, delete-orphan",
order_by="Question.position",
)
attempts: Mapped[list[QuizAttempt]] = relationship(
"QuizAttempt", back_populates="quiz", cascade="all, delete-orphan"
)
votes: Mapped[list[QuizVote]] = relationship(
"QuizVote", back_populates="quiz", cascade="all, delete-orphan"
)
favourited_by: Mapped[list[Favourite]] = relationship(
"Favourite", back_populates="quiz", cascade="all, delete-orphan"
)
class Question(Base):
__tablename__ = "questions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
quiz_id: Mapped[int] = mapped_column(
Integer, ForeignKey("quizzes.id", ondelete="CASCADE"), nullable=False, index=True
)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
question_type: Mapped[str] = mapped_column(String(24), nullable=False)
prompt: Mapped[str] = mapped_column(Text, nullable=False)
options: Mapped[list[dict[str, Any]]] = mapped_column(JSON, nullable=False, default=list)
correct_answer: Mapped[str] = mapped_column(String(500), nullable=False)
accepted_answers: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
quiz: Mapped[Quiz] = relationship("Quiz", back_populates="questions")
class QuizAttempt(Base):
__tablename__ = "quiz_attempts"
__table_args__ = (
Index("ix_quiz_attempts_quiz_score", "quiz_id", "score_percent", "completed_at"),
Index("ix_quiz_attempts_user_quiz", "user_id", "quiz_id", "completed_at"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
quiz_id: Mapped[int] = mapped_column(
Integer, ForeignKey("quizzes.id", ondelete="CASCADE"), nullable=False
)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
score_count: Mapped[int] = mapped_column(Integer, nullable=False)
total_questions: Mapped[int] = mapped_column(Integer, nullable=False)
score_percent: Mapped[float] = mapped_column(Float, nullable=False)
answers_payload: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict)
completed_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
quiz: Mapped[Quiz] = relationship("Quiz", back_populates="attempts")
user: Mapped[User] = relationship("User", back_populates="attempts")
class QuizVote(Base):
__tablename__ = "quiz_votes"
__table_args__ = (UniqueConstraint("quiz_id", "user_id", name="uq_quiz_vote_user"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
quiz_id: Mapped[int] = mapped_column(
Integer, ForeignKey("quizzes.id", ondelete="CASCADE"), nullable=False, index=True
)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
value: Mapped[int] = mapped_column(Integer, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow, onupdate=utcnow
)
quiz: Mapped[Quiz] = relationship("Quiz", back_populates="votes")
user: Mapped[User] = relationship("User", back_populates="votes")
class Favourite(Base):
__tablename__ = "favourites"
__table_args__ = (UniqueConstraint("user_id", "quiz_id", name="uq_favourite_user_quiz"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
quiz_id: Mapped[int] = mapped_column(
Integer, ForeignKey("quizzes.id", ondelete="CASCADE"), nullable=False, index=True
)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=utcnow
)
user: Mapped[User] = relationship("User", back_populates="favourites")
quiz: Mapped[Quiz] = relationship("Quiz", back_populates="favourited_by")