initial
This commit is contained in:
278
backend/models.py
Normal file
278
backend/models.py
Normal file
@@ -0,0 +1,278 @@
|
||||
from datetime import datetime
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import Enum
|
||||
import enum
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
"""User model for authenticated users via Authelia OIDC"""
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
authelia_sub = db.Column(db.String(255), unique=True, nullable=False) # OIDC 'sub' claim
|
||||
email = db.Column(db.String(255), nullable=True) # Email may not always be provided by OIDC
|
||||
name = db.Column(db.String(255), nullable=True)
|
||||
preferred_username = db.Column(db.String(100), nullable=True)
|
||||
groups = db.Column(db.JSON, default=list, nullable=False) # Array of group names from OIDC
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_login = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
"""Check if user is in trivia-admins group"""
|
||||
return 'trivia-admins' in (self.groups or [])
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert user to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'email': self.email,
|
||||
'name': self.name,
|
||||
'username': self.preferred_username,
|
||||
'groups': self.groups,
|
||||
'is_admin': self.is_admin,
|
||||
'last_login': self.last_login.isoformat() if self.last_login else None
|
||||
}
|
||||
|
||||
|
||||
class QuestionType(enum.Enum):
|
||||
"""Enum for question types"""
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
YOUTUBE_AUDIO = "youtube_audio"
|
||||
|
||||
|
||||
class Category(db.Model):
|
||||
"""Category model for organizing questions"""
|
||||
__tablename__ = 'categories'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert category to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
class Question(db.Model):
|
||||
"""Question model for trivia questions"""
|
||||
__tablename__ = 'questions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(Enum(QuestionType), nullable=False, default=QuestionType.TEXT)
|
||||
question_content = db.Column(db.Text, nullable=False)
|
||||
answer = db.Column(db.Text, nullable=False)
|
||||
image_path = db.Column(db.String(255), nullable=True) # For image questions
|
||||
category = db.Column(db.String(100), nullable=True) # Question category (e.g., "History", "Science")
|
||||
|
||||
# YouTube audio fields
|
||||
youtube_url = db.Column(db.String(500), nullable=True) # YouTube video URL
|
||||
audio_path = db.Column(db.String(255), nullable=True) # Path to trimmed audio file
|
||||
start_time = db.Column(db.Integer, nullable=True) # Start time in seconds
|
||||
end_time = db.Column(db.Integer, nullable=True) # End time in seconds
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
game_questions = db.relationship('GameQuestion', back_populates='question', cascade='all, delete-orphan')
|
||||
|
||||
def to_dict(self, include_answer=False):
|
||||
"""Convert question to dictionary"""
|
||||
data = {
|
||||
'id': self.id,
|
||||
'type': self.type.value,
|
||||
'question_content': self.question_content,
|
||||
'image_path': self.image_path,
|
||||
'youtube_url': self.youtube_url,
|
||||
'audio_path': self.audio_path,
|
||||
'start_time': self.start_time,
|
||||
'end_time': self.end_time,
|
||||
'category': self.category,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
if include_answer:
|
||||
data['answer'] = self.answer
|
||||
return data
|
||||
|
||||
|
||||
class Game(db.Model):
|
||||
"""Game model representing a trivia game session"""
|
||||
__tablename__ = 'games'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(200), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
is_active = db.Column(db.Boolean, default=False) # Only one game active at a time
|
||||
current_question_index = db.Column(db.Integer, default=0) # Track current question
|
||||
is_template = db.Column(db.Boolean, default=False) # Mark as reusable template
|
||||
|
||||
# Relationships
|
||||
teams = db.relationship('Team', back_populates='game', cascade='all, delete-orphan')
|
||||
game_questions = db.relationship('GameQuestion', back_populates='game',
|
||||
cascade='all, delete-orphan',
|
||||
order_by='GameQuestion.order')
|
||||
scores = db.relationship('Score', back_populates='game', cascade='all, delete-orphan')
|
||||
|
||||
@classmethod
|
||||
def get_active(cls):
|
||||
"""Get the currently active game"""
|
||||
return cls.query.filter_by(is_active=True).first()
|
||||
|
||||
def get_current_question(self):
|
||||
"""Get the current question in the game"""
|
||||
if 0 <= self.current_question_index < len(self.game_questions):
|
||||
return self.game_questions[self.current_question_index].question
|
||||
return None
|
||||
|
||||
def to_dict(self, include_questions=False, include_teams=False):
|
||||
"""Convert game to dictionary"""
|
||||
data = {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'is_active': self.is_active,
|
||||
'current_question_index': self.current_question_index,
|
||||
'total_questions': len(self.game_questions),
|
||||
'is_template': self.is_template
|
||||
}
|
||||
|
||||
if include_questions:
|
||||
data['questions'] = [gq.to_dict() for gq in self.game_questions]
|
||||
|
||||
if include_teams:
|
||||
data['teams'] = [team.to_dict() for team in self.teams]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class GameQuestion(db.Model):
|
||||
"""Junction table linking games to questions with ordering"""
|
||||
__tablename__ = 'game_questions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
game_id = db.Column(db.Integer, db.ForeignKey('games.id'), nullable=False)
|
||||
question_id = db.Column(db.Integer, db.ForeignKey('questions.id'), nullable=False)
|
||||
order = db.Column(db.Integer, nullable=False) # Question order in game
|
||||
|
||||
# Relationships
|
||||
game = db.relationship('Game', back_populates='game_questions')
|
||||
question = db.relationship('Question', back_populates='game_questions')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('game_id', 'order', name='unique_game_question_order'),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
return {
|
||||
'order': self.order,
|
||||
'question': self.question.to_dict()
|
||||
}
|
||||
|
||||
|
||||
class Team(db.Model):
|
||||
"""Team model representing a trivia team"""
|
||||
__tablename__ = 'teams'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
game_id = db.Column(db.Integer, db.ForeignKey('games.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
phone_a_friend_count = db.Column(db.Integer, default=5) # Number of phone-a-friend lifelines
|
||||
|
||||
# Relationships
|
||||
game = db.relationship('Game', back_populates='teams')
|
||||
scores = db.relationship('Score', back_populates='team', cascade='all, delete-orphan')
|
||||
|
||||
@property
|
||||
def total_score(self):
|
||||
"""Calculate total score for the team"""
|
||||
return sum(score.points for score in self.scores)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert team to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'game_id': self.game_id,
|
||||
'total_score': self.total_score,
|
||||
'phone_a_friend_count': self.phone_a_friend_count,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
class Score(db.Model):
|
||||
"""Score model tracking points per team per question"""
|
||||
__tablename__ = 'scores'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
|
||||
game_id = db.Column(db.Integer, db.ForeignKey('games.id'), nullable=False)
|
||||
question_index = db.Column(db.Integer, nullable=False) # Which question this score is for
|
||||
points = db.Column(db.Integer, default=0)
|
||||
awarded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
team = db.relationship('Team', back_populates='scores')
|
||||
game = db.relationship('Game', back_populates='scores')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('team_id', 'question_index', name='unique_team_question_score'),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert score to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'team_id': self.team_id,
|
||||
'game_id': self.game_id,
|
||||
'question_index': self.question_index,
|
||||
'points': self.points,
|
||||
'awarded_at': self.awarded_at.isoformat() if self.awarded_at else None
|
||||
}
|
||||
|
||||
|
||||
class DownloadJobStatus(enum.Enum):
|
||||
"""Enum for download job statuses"""
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class DownloadJob(db.Model):
|
||||
"""Track YouTube audio download jobs"""
|
||||
__tablename__ = 'download_jobs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
question_id = db.Column(db.Integer, db.ForeignKey('questions.id'), nullable=False)
|
||||
celery_task_id = db.Column(db.String(255), nullable=False, unique=True)
|
||||
status = db.Column(Enum(DownloadJobStatus), default=DownloadJobStatus.PENDING, nullable=False)
|
||||
progress = db.Column(db.Integer, default=0) # 0-100
|
||||
error_message = db.Column(db.Text, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
completed_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
question = db.relationship('Question', backref='download_job')
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert download job to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'question_id': self.question_id,
|
||||
'task_id': self.celery_task_id,
|
||||
'status': self.status.value,
|
||||
'progress': self.progress,
|
||||
'error_message': self.error_message,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'completed_at': self.completed_at.isoformat() if self.completed_at else None
|
||||
}
|
||||
Reference in New Issue
Block a user