237 lines
9.3 KiB
Python
237 lines
9.3 KiB
Python
|
|
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")
|