Initial commit

This commit is contained in:
2026-02-01 09:31:38 +01:00
commit e02db93960
4396 changed files with 1511612 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
"""Modèles SQLAlchemy de l'application."""
from .user import User
from .tweet import Tweet
from .reddit_post import RedditPost, RedditComment
from .rss_article import RSSArticle
from .sentiment_score import SentimentScore
from .energy_score import EnergyScore
from .match import Match
from .prediction import Prediction
from .user_prediction import UserPrediction
from .badge import Badge, UserBadge
from .api_key import ApiKey
__all__ = ["User", "Tweet", "RedditPost", "RedditComment", "RSSArticle", "SentimentScore", "EnergyScore", "Match", "Prediction", "UserPrediction", "Badge", "UserBadge", "ApiKey"]

View File

@@ -0,0 +1,55 @@
"""
SQLAlchemy model for API keys.
This module defines the database model for storing API keys used for public API authentication.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from app.database import Base
class ApiKey(Base):
"""
Model for storing user API keys.
Attributes:
id: Primary key
user_id: Foreign key to users table
key_hash: Hashed API key (never store plain keys)
key_prefix: First 8 characters of key for identification
is_active: Whether the key is active
rate_limit: Rate limit per minute for this key
last_used_at: Timestamp of last API usage
created_at: Timestamp when key was created
"""
__tablename__ = "api_keys"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
key_hash = Column(String(255), nullable=False, unique=True, index=True)
key_prefix = Column(String(8), nullable=False, index=True)
is_active = Column(Boolean, default=True, nullable=False)
rate_limit = Column(Integer, default=100, nullable=False) # Default: 100 req/min
last_used_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="api_keys")
def __repr__(self) -> str:
return f"<ApiKey(id={self.id}, user_id={self.user_id}, prefix={self.key_prefix})>"
def to_dict(self) -> dict:
"""Convert API key model to dictionary (safe version)."""
return {
'id': self.id,
'user_id': self.user_id,
'key_prefix': self.key_prefix,
'is_active': self.is_active,
'rate_limit': self.rate_limit,
'last_used_at': self.last_used_at.isoformat() if self.last_used_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None
}

View File

@@ -0,0 +1,45 @@
"""
Badge models for SQLAlchemy ORM
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from app.database import Base
class Badge(Base):
"""Badge definition model"""
__tablename__ = "badges"
id = Column(Integer, primary_key=True, autoincrement=True)
badge_id = Column(String(50), unique=True, nullable=False, index=True)
name = Column(String(100), nullable=False)
description = Column(Text, nullable=False)
icon = Column(String(10), nullable=False) # emoji or icon string
category = Column(String(50), nullable=False) # predictions, accuracy, engagement, social
criteria_type = Column(String(50), nullable=False) # predictions_count, correct_predictions, etc.
criteria_value = Column(Integer, nullable=False)
criteria_description = Column(String(200), nullable=False)
rarity = Column(String(20), nullable=False) # common, rare, epic, legendary
points = Column(Integer, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
user_badges = relationship("UserBadge", back_populates="badge")
class UserBadge(Base):
"""User's unlocked badges"""
__tablename__ = "user_badges"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
badge_id = Column(Integer, ForeignKey("badges.id"), nullable=False, index=True)
unlocked_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
user = relationship("User", back_populates="badges")
badge = relationship("Badge", back_populates="user_badges")
def __repr__(self):
return f"<UserBadge user_id={self.user_id} badge_id={self.badge_id}>"

View File

@@ -0,0 +1,101 @@
"""
SQLAlchemy model for energy scores.
This module defines the database model for storing energy score calculations
from multiple sources (Twitter, Reddit, RSS).
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Float, DateTime, Index, ForeignKey, JSON
from sqlalchemy.orm import relationship
from app.database import Base
class EnergyScore(Base):
"""
Model for storing energy score calculations.
Attributes:
id: Primary key
match_id: Foreign key to the match
team_id: Foreign key to the team
score: Final energy score (0-100)
confidence: Confidence level (0-1)
sources_used: List of sources used in calculation (JSON)
twitter_score: Energy score from Twitter component
reddit_score: Energy score from Reddit component
rss_score: Energy score from RSS component
temporal_factor: Temporal weighting factor applied
twitter_weight: Adjusted weight for Twitter (in degraded mode)
reddit_weight: Adjusted weight for Reddit (in degraded mode)
rss_weight: Adjusted weight for RSS (in degraded mode)
created_at: Timestamp when the energy score was calculated
updated_at: Timestamp when the energy score was last updated
"""
__tablename__ = "energy_scores"
id = Column(Integer, primary_key=True, index=True)
match_id = Column(Integer, nullable=False, index=True, comment="ID of the match")
team_id = Column(Integer, nullable=False, index=True, comment="ID of the team")
score = Column(Float, nullable=False, index=True, comment="Final energy score (0-100)")
confidence = Column(Float, nullable=False, default=0.0, comment="Confidence level (0-1)")
# Sources used (stored as JSON array)
sources_used = Column(JSON, nullable=False, default=list, comment="List of sources used")
# Component scores
twitter_score = Column(Float, nullable=True, comment="Energy score from Twitter component")
reddit_score = Column(Float, nullable=True, comment="Energy score from Reddit component")
rss_score = Column(Float, nullable=True, comment="Energy score from RSS component")
# Temporal weighting
temporal_factor = Column(Float, nullable=True, comment="Temporal weighting factor applied")
# Adjusted weights (for degraded mode tracking)
twitter_weight = Column(Float, nullable=True, comment="Adjusted weight for Twitter")
reddit_weight = Column(Float, nullable=True, comment="Adjusted weight for Reddit")
rss_weight = Column(Float, nullable=True, comment="Adjusted weight for RSS")
# Timestamps
created_at = Column(DateTime, nullable=False, index=True, default=datetime.utcnow, comment="Creation timestamp")
updated_at = Column(DateTime, nullable=False, index=True, default=datetime.utcnow, onupdate=datetime.utcnow, comment="Last update timestamp")
# Indexes for performance
__table_args__ = (
Index('idx_energy_scores_match_team', 'match_id', 'team_id'),
Index('idx_energy_scores_score', 'score'),
Index('idx_energy_scores_confidence', 'confidence'),
Index('idx_energy_scores_created_at', 'created_at'),
Index('idx_energy_scores_updated_at', 'updated_at'),
)
def __repr__(self) -> str:
return (f"<EnergyScore(id={self.id}, match_id={self.match_id}, "
f"team_id={self.team_id}, score={self.score}, "
f"confidence={self.confidence})>")
def to_dict(self) -> dict:
"""
Convert energy score model to dictionary.
Returns:
Dictionary representation of the energy score
"""
return {
'id': self.id,
'match_id': self.match_id,
'team_id': self.team_id,
'score': self.score,
'confidence': self.confidence,
'sources_used': self.sources_used,
'twitter_score': self.twitter_score,
'reddit_score': self.reddit_score,
'rss_score': self.rss_score,
'temporal_factor': self.temporal_factor,
'twitter_weight': self.twitter_weight,
'reddit_weight': self.reddit_weight,
'rss_weight': self.rss_weight,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}

View File

@@ -0,0 +1,62 @@
"""
SQLAlchemy model for matches.
This module defines the database model for storing match information.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Index
from sqlalchemy.orm import relationship
from app.database import Base
class Match(Base):
"""
Model for storing match information.
Attributes:
id: Primary key
home_team: Name of the home team
away_team: Name of the away team
date: Match date and time
league: League name
status: Match status (scheduled, in_progress, completed, etc.)
"""
__tablename__ = "matches"
id = Column(Integer, primary_key=True, index=True)
home_team = Column(String(255), nullable=False, index=True)
away_team = Column(String(255), nullable=False, index=True)
date = Column(DateTime, nullable=False, index=True)
league = Column(String(255), nullable=False, index=True)
status = Column(String(50), nullable=False, index=True)
actual_winner = Column(String(255), nullable=True, index=True, comment='Actual winner: home, away, or draw')
# Relationships
predictions = relationship("Prediction", back_populates="match", cascade="all, delete-orphan")
tweets = relationship("Tweet", back_populates="match", cascade="all, delete-orphan")
posts_reddit = relationship("RedditPost", back_populates="match", cascade="all, delete-orphan")
rss_articles = relationship("RSSArticle", back_populates="match", cascade="all, delete-orphan")
# Indexes for performance
__table_args__ = (
Index('idx_matches_date_league', 'date', 'league'),
Index('idx_matches_home_away', 'home_team', 'away_team'),
Index('idx_matches_actual_winner', 'actual_winner'),
)
def __repr__(self) -> str:
return f"<Match(id={self.id}, {self.home_team} vs {self.away_team}, date={self.date})>"
def to_dict(self) -> dict:
"""Convert match model to dictionary."""
return {
'id': self.id,
'home_team': self.home_team,
'away_team': self.away_team,
'date': self.date.isoformat() if self.date else None,
'league': self.league,
'status': self.status,
'actual_winner': self.actual_winner
}

View File

@@ -0,0 +1,57 @@
"""
SQLAlchemy model for predictions.
This module defines the database model for storing match predictions.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Index
from sqlalchemy.orm import relationship
from app.database import Base
class Prediction(Base):
"""
Model for storing match predictions.
Attributes:
id: Primary key
match_id: Foreign key to matches table
energy_score: Energy score for the prediction
confidence: Confidence level of the prediction
predicted_winner: Predicted winner team name
created_at: Timestamp when prediction was created
"""
__tablename__ = "predictions"
id = Column(Integer, primary_key=True, index=True)
match_id = Column(Integer, ForeignKey("matches.id", ondelete="CASCADE"), nullable=False, index=True)
energy_score = Column(String(50), nullable=False)
confidence = Column(String(50), nullable=False)
predicted_winner = Column(String(255), nullable=False)
created_at = Column(DateTime, nullable=False, index=True)
# Relationships
match = relationship("Match", back_populates="predictions")
user_predictions = relationship("UserPrediction", back_populates="prediction", cascade="all, delete-orphan")
# Indexes for performance
__table_args__ = (
Index('idx_predictions_match_id_created', 'match_id', 'created_at'),
Index('idx_predictions_confidence', 'confidence'),
)
def __repr__(self) -> str:
return f"<Prediction(id={self.id}, match_id={self.match_id}, confidence={self.confidence})>"
def to_dict(self) -> dict:
"""Convert prediction model to dictionary."""
return {
'id': self.id,
'match_id': self.match_id,
'energy_score': self.energy_score,
'confidence': self.confidence,
'predicted_winner': self.predicted_winner,
'created_at': self.created_at.isoformat() if self.created_at else None
}

View File

@@ -0,0 +1,118 @@
"""
SQLAlchemy model for Reddit posts.
This module defines database models for storing posts and comments
collected from Reddit API.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Index, Text, ForeignKey
from sqlalchemy.orm import relationship
from app.database import Base
class RedditPost(Base):
"""
Model for storing Reddit posts about football matches.
Attributes:
id: Primary key
post_id: Unique identifier from Reddit
title: Post title
text: Post content
upvotes: Number of upvotes
created_at: Timestamp when post was created
match_id: Foreign key to matches table
subreddit: Subreddit name
source: Source platform (reddit)
"""
__tablename__ = "posts_reddit"
id = Column(Integer, primary_key=True, index=True)
post_id = Column(String(255), unique=True, nullable=False, index=True)
title = Column(String(500), nullable=False)
text = Column(Text, nullable=True)
upvotes = Column(Integer, default=0)
created_at = Column(DateTime, nullable=False, index=True)
match_id = Column(Integer, ForeignKey('matches.id'), nullable=True, index=True)
subreddit = Column(String(100), nullable=False)
source = Column(String(50), default="reddit")
# Indexes for performance
__table_args__ = (
Index('idx_posts_reddit_match_id', 'match_id'),
Index('idx_posts_reddit_created_at', 'created_at'),
Index('idx_posts_reddit_subreddit', 'subreddit'),
)
# Relationship with match
match = relationship("Match", back_populates="posts_reddit")
# Relationship with comments
comments = relationship("RedditComment", back_populates="post", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"<RedditPost(id={self.id}, post_id={self.post_id}, subreddit={self.subreddit})>"
def to_dict(self) -> dict:
"""Convert Reddit post model to dictionary."""
return {
'id': self.id,
'post_id': self.post_id,
'title': self.title,
'text': self.text,
'upvotes': self.upvotes,
'created_at': self.created_at.isoformat() if self.created_at else None,
'match_id': self.match_id,
'subreddit': self.subreddit,
'source': self.source
}
class RedditComment(Base):
"""
Model for storing Reddit comments.
Attributes:
id: Primary key
comment_id: Unique identifier from Reddit
post_id: Foreign key to posts_reddit table
text: Comment content
upvotes: Number of upvotes
created_at: Timestamp when comment was created
source: Source platform (reddit)
"""
__tablename__ = "comments_reddit"
id = Column(Integer, primary_key=True, index=True)
comment_id = Column(String(255), unique=True, nullable=False, index=True)
post_id = Column(Integer, ForeignKey('posts_reddit.id'), nullable=False, index=True)
text = Column(Text, nullable=False)
upvotes = Column(Integer, default=0)
created_at = Column(DateTime, nullable=False, index=True)
source = Column(String(50), default="reddit")
# Indexes for performance
__table_args__ = (
Index('idx_comments_reddit_post_id', 'post_id'),
Index('idx_comments_reddit_created_at', 'created_at'),
)
# Relationship with post
post = relationship("RedditPost", back_populates="comments")
def __repr__(self) -> str:
return f"<RedditComment(id={self.id}, comment_id={self.comment_id}, post_id={self.post_id})>"
def to_dict(self) -> dict:
"""Convert Reddit comment model to dictionary."""
return {
'id': self.id,
'comment_id': self.comment_id,
'post_id': self.post_id,
'text': self.text,
'upvotes': self.upvotes,
'created_at': self.created_at.isoformat() if self.created_at else None,
'source': self.source
}

View File

@@ -0,0 +1,64 @@
"""
SQLAlchemy model for RSS articles.
This module defines the database model for storing RSS articles
collected from various RSS feeds.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Text, Index, ForeignKey
from sqlalchemy.orm import relationship
from app.database import Base
class RSSArticle(Base):
"""
Model for storing RSS articles collected from sports feeds.
Attributes:
id: Primary key
article_id: Unique identifier from RSS feed
title: Article title
content: Article content/description
published_at: Timestamp when article was published
source_url: URL of the RSS feed source
match_id: Foreign key to matches table
source: Source feed name (ESPN, BBC Sport, etc.)
"""
__tablename__ = "rss_articles"
id = Column(Integer, primary_key=True, index=True)
article_id = Column(String(255), unique=True, nullable=False, index=True)
title = Column(String(500), nullable=False)
content = Column(Text, nullable=True)
published_at = Column(DateTime, nullable=False, index=True)
source_url = Column(String(1000), nullable=False)
match_id = Column(Integer, ForeignKey('matches.id'), nullable=True, index=True)
source = Column(String(100), default="rss")
# Indexes for performance
__table_args__ = (
Index('idx_rss_articles_match_id_source', 'match_id', 'source'),
Index('idx_rss_articles_published_at', 'published_at'),
Index('idx_rss_articles_source_url', 'source_url'),
)
# Relationship with match
match = relationship("Match", back_populates="rss_articles")
def __repr__(self) -> str:
return f"<RSSArticle(id={self.id}, article_id={self.article_id}, match_id={self.match_id})>"
def to_dict(self) -> dict:
"""Convert RSS article model to dictionary."""
return {
'id': self.id,
'article_id': self.article_id,
'title': self.title,
'content': self.content,
'published_at': self.published_at.isoformat() if self.published_at else None,
'source_url': self.source_url,
'match_id': self.match_id,
'source': self.source
}

View File

@@ -0,0 +1,65 @@
"""
SQLAlchemy model for sentiment scores.
This module defines the database model for storing sentiment analysis results
from tweets and posts.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Float, DateTime, Index, ForeignKey
from sqlalchemy.orm import relationship
from app.database import Base
class SentimentScore(Base):
"""
Model for storing sentiment analysis results.
Attributes:
id: Primary key
entity_id: Foreign key to the entity being analyzed (tweet_id or post_id)
entity_type: Type of entity ('tweet' or 'reddit_post')
score: Overall compound sentiment score (-1 to 1)
sentiment_type: Classification ('positive', 'negative', or 'neutral')
positive: Positive proportion score (0 to 1)
negative: Negative proportion score (0 to 1)
neutral: Neutral proportion score (0 to 1)
created_at: Timestamp when the sentiment was analyzed
"""
__tablename__ = "sentiment_scores"
id = Column(Integer, primary_key=True, index=True)
entity_id = Column(String(255), nullable=False, index=True)
entity_type = Column(String(50), nullable=False, index=True) # 'tweet' or 'reddit_post'
score = Column(Float, nullable=False, index=True) # Compound score
sentiment_type = Column(String(20), nullable=False, index=True) # 'positive', 'negative', 'neutral'
positive = Column(Float, nullable=False, default=0.0)
negative = Column(Float, nullable=False, default=0.0)
neutral = Column(Float, nullable=False, default=0.0)
created_at = Column(DateTime, nullable=False, index=True, default=datetime.utcnow)
# Indexes for performance
__table_args__ = (
Index('idx_sentiment_scores_entity', 'entity_id', 'entity_type'),
Index('idx_sentiment_scores_score', 'score'),
Index('idx_sentiment_scores_type', 'sentiment_type'),
Index('idx_sentiment_scores_created_at', 'created_at'),
)
def __repr__(self) -> str:
return f"<SentimentScore(id={self.id}, entity_id={self.entity_id}, sentiment_type={self.sentiment_type})>"
def to_dict(self) -> dict:
"""Convert sentiment score model to dictionary."""
return {
'id': self.id,
'entity_id': self.entity_id,
'entity_type': self.entity_type,
'score': self.score,
'sentiment_type': self.sentiment_type,
'positive': self.positive,
'negative': self.negative,
'neutral': self.neutral,
'created_at': self.created_at.isoformat() if self.created_at else None
}

View File

@@ -0,0 +1,63 @@
"""
SQLAlchemy model for tweets.
This module defines the database model for storing tweets collected
from Twitter, Reddit, and RSS sources.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Index, ForeignKey
from sqlalchemy.orm import relationship
from app.database import Base
class Tweet(Base):
"""
Model for storing tweets collected from social media sources.
Attributes:
id: Primary key
tweet_id: Unique identifier from the source platform
text: Tweet content
created_at: Timestamp when the tweet was created
retweet_count: Number of retweets
like_count: Number of likes
match_id: Foreign key to matches table
source: Source platform (twitter, reddit, rss)
"""
__tablename__ = "tweets"
id = Column(Integer, primary_key=True, index=True)
tweet_id = Column(String(255), unique=True, nullable=False, index=True)
text = Column(String(1000), nullable=False)
created_at = Column(DateTime, nullable=False, index=True)
retweet_count = Column(Integer, default=0)
like_count = Column(Integer, default=0)
match_id = Column(Integer, ForeignKey('matches.id'), nullable=True, index=True)
source = Column(String(50), default="twitter")
# Indexes for performance
__table_args__ = (
Index('idx_tweets_match_id_source', 'match_id', 'source'),
Index('idx_tweets_created_at', 'created_at'),
)
# Relationship with match
match = relationship("Match", back_populates="tweets")
def __repr__(self) -> str:
return f"<Tweet(id={self.id}, tweet_id={self.tweet_id}, match_id={self.match_id})>"
def to_dict(self) -> dict:
"""Convert tweet model to dictionary."""
return {
'id': self.id,
'tweet_id': self.tweet_id,
'text': self.text,
'created_at': self.created_at.isoformat() if self.created_at else None,
'retweet_count': self.retweet_count,
'like_count': self.like_count,
'match_id': self.match_id,
'source': self.source
}

View File

@@ -0,0 +1,27 @@
"""Modèle SQLAlchemy pour les utilisateurs."""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.orm import relationship
from app.database import Base
class User(Base):
"""Modèle de base de données pour les utilisateurs."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
name = Column(String, nullable=True)
password_hash = Column(String, nullable=True)
is_premium = Column(Boolean, default=False, nullable=False)
referral_code = Column(String, unique=True, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
daily_predictions_count = Column(Integer, default=0)
last_prediction_date = Column(DateTime, nullable=True)
# Relationships
user_predictions = relationship("UserPrediction", back_populates="user", cascade="all, delete-orphan")
badges = relationship("UserBadge", back_populates="user", cascade="all, delete-orphan")
api_keys = relationship("ApiKey", back_populates="user", cascade="all, delete-orphan")

View File

@@ -0,0 +1,62 @@
"""
SQLAlchemy model for user_predictions.
This module defines the database model for tracking which predictions
each user has viewed and their accuracy.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, DateTime, Boolean, ForeignKey, Index, UniqueConstraint
from sqlalchemy.orm import relationship
from app.database import Base
class UserPrediction(Base):
"""
Model for tracking predictions viewed by users.
This table tracks:
- Which predictions each user has viewed
- When they viewed them
- Whether the prediction was correct (when match result is known)
Attributes:
id: Primary key
user_id: Foreign key to users table
prediction_id: Foreign key to predictions table
viewed_at: Timestamp when user viewed the prediction
was_correct: True if prediction was correct, False if incorrect, NULL if match not completed
"""
__tablename__ = "user_predictions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
prediction_id = Column(Integer, ForeignKey("predictions.id", ondelete="CASCADE"), nullable=False, index=True)
viewed_at = Column(DateTime, nullable=False, index=True)
was_correct = Column(Boolean, nullable=True, comment='True if prediction was correct, False if incorrect, NULL if match not completed')
# Relationships
user = relationship("User", back_populates="user_predictions")
prediction = relationship("Prediction", back_populates="user_predictions")
# Constraints and indexes
__table_args__ = (
Index('idx_user_predictions_user_id', 'user_id'),
Index('idx_user_predictions_prediction_id', 'prediction_id'),
Index('idx_user_predictions_viewed_at', 'viewed_at'),
UniqueConstraint('user_id', 'prediction_id', name='uq_user_predictions_user_prediction'),
)
def __repr__(self) -> str:
return f"<UserPrediction(id={self.id}, user_id={self.user_id}, prediction_id={self.prediction_id}, viewed_at={self.viewed_at})>"
def to_dict(self) -> dict:
"""Convert user prediction model to dictionary."""
return {
'id': self.id,
'user_id': self.user_id,
'prediction_id': self.prediction_id,
'viewed_at': self.viewed_at.isoformat() if self.viewed_at else None,
'was_correct': self.was_correct
}