Just_UNIARC / src /database /models.py
UNI12345's picture
feat: sync backend codebase to space
feeaf83
Raw
History Blame Contribute Delete
13.7 kB
from datetime import datetime, timedelta
from typing import List, Optional
from sqlalchemy import create_engine, Column, Integer, String, Text, Float, DateTime, Boolean, ForeignKey, JSON, Index
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy import event
from src.config.settings import DATABASE_URL
Base = declarative_base()
class RawNews(Base):
__tablename__ = "raw_news"
id = Column(Integer, primary_key=True, index=True)
source_id = Column(String, index=True)
source_name = Column(String)
author = Column(String, nullable=True)
title = Column(String)
description = Column(Text, nullable=True)
url = Column(String, unique=True, index=True)
url_to_image = Column(String, nullable=True)
published_at = Column(DateTime)
content = Column(Text, nullable=True)
collected_at = Column(DateTime, default=datetime.utcnow)
# Metadata for processing status
is_verified = Column(Boolean, default=False)
verification_score = Column(Float, default=0.0)
processed = Column(Boolean, default=False)
country = Column(String, nullable=True, index=True)
class VerifiedNews(Base):
__tablename__ = "verified_news"
__table_args__ = (
Index('idx_verified_news_query', 'country', 'category', 'created_at'),
Index('idx_verified_news_impact', 'country', 'impact_score', 'created_at'),
)
id = Column(Integer, primary_key=True, index=True)
raw_news_id = Column(Integer, ForeignKey("raw_news.id"))
title = Column(String)
content = Column(Text)
summary_bullets = Column(JSON) # List of strings
# Analysis Fields
analysis = Column(JSON, nullable=True) # Flexible storage for extra metadata
impact_tags = Column(JSON) # e.g. ["Jobs", "Market"]
bias_rating = Column(String) # e.g. "Neutral", "Slightly Biased"
category = Column(String, index=True)
sub_category = Column(String, nullable=True, index=True) # e.g. "Scholarships", "Exams"
country = Column(String, nullable=True, index=True)
credibility_score = Column(Float)
impact_score = Column(Integer) # 1-10
why_it_matters = Column(Text)
who_is_affected = Column(Text, nullable=True)
short_term_impact = Column(Text, nullable=True)
long_term_impact = Column(Text, nullable=True)
sentiment = Column(String)
lang = Column(String, default='english', index=True) # Source language
is_fake = Column(Boolean, default=False)
flag_count = Column(Integer, default=0)
published_at = Column(DateTime)
created_at = Column(DateTime, default=datetime.utcnow)
# New Perfection Fields
translation_cache = Column(JSON, default=dict) # {lang: {title, why, impacted}}
audio_url = Column(String, nullable=True) # Path to local TTS mp3
raw_news = relationship("RawNews")
@property
def image_url(self) -> Optional[str]:
"""Backward compatibility for templates and logic expecting image_url attribute."""
if self.raw_news and self.raw_news.url_to_image:
return self.raw_news.url_to_image
return None
@property
def url(self) -> str:
"""Helper to access the source URL."""
if self.raw_news:
return self.raw_news.url
return "#"
@property
def source_name(self) -> str:
"""Helper to access the source name."""
if self.raw_news:
return self.raw_news.source_name
return "Unknown"
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"content": self.content,
"summary_bullets": self.summary_bullets,
"analysis": self.analysis,
"impact_tags": self.impact_tags,
"bias_rating": self.bias_rating,
"category": self.category,
"sub_category": self.sub_category,
"country": self.country,
"credibility_score": self.credibility_score,
"impact_score": self.impact_score,
"why_it_matters": self.why_it_matters,
"who_is_affected": self.who_is_affected,
"short_term_impact": self.short_term_impact,
"long_term_impact": self.long_term_impact,
"sentiment": self.sentiment,
"published_at": self.published_at.isoformat() if self.published_at else None,
"created_at": self.created_at.isoformat() if self.created_at else None,
"source_name": self.raw_news.source_name if self.raw_news else "Unknown",
"url": self.url,
"image_url": self.image_url
}
class DailyDigest(Base):
__tablename__ = "daily_digests"
id = Column(Integer, primary_key=True, index=True)
date = Column(DateTime, default=datetime.utcnow)
content_json = Column(JSON) # Full structured digest
is_published = Column(Boolean, default=False)
class TopicTracking(Base):
__tablename__ = "topic_tracking"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
news_id = Column(Integer, ForeignKey("verified_news.id"), nullable=True)
topic_keywords = Column(JSON) # ["AI", "Nvidia"]
language = Column(String, default="english")
notify_sms = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
expires_at = Column(DateTime, default=lambda: datetime.utcnow() + timedelta(days=30))
user = relationship("User", back_populates="tracked_topics")
news = relationship("VerifiedNews")
class TrackNotification(Base):
__tablename__ = "track_notifications"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
news_id = Column(Integer, ForeignKey("verified_news.id"))
notified_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User")
news = relationship("VerifiedNews")
class OTPVerification(Base):
__tablename__ = "otp_verifications"
id = Column(Integer, primary_key=True, index=True)
phone = Column(String, index=True)
otp_code = Column(String)
expires_at = Column(DateTime)
is_verified = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
firebase_uid = Column(String, unique=True, index=True)
email = Column(String, nullable=True)
phone = Column(String, nullable=True)
push_token = Column(String, nullable=True)
bounty_points = Column(Integer, default=0)
preferred_language = Column(String, default="english")
bio = Column(Text, nullable=True)
profile_image_url = Column(String, nullable=True)
# Premium Streak & Rewards
current_streak = Column(Integer, default=0)
streak_history = Column(JSON, default=dict) # e.g. {"2026-04-04": "success", "2026-04-03": "missed"}
subscription_status = Column(String, default="free") # "free", "premium_eligible", "activated"
last_active_date = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
subscriptions = relationship("Subscription", back_populates="user")
folders = relationship("Folder", back_populates="user")
saved_articles = relationship("SavedArticle", back_populates="user")
read_history = relationship("ReadHistory", back_populates="user")
tracked_topics = relationship("TopicTracking", back_populates="user")
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
category = Column(String) # e.g. "Technology", "All"
user = relationship("User", back_populates="subscriptions")
class Folder(Base):
__tablename__ = "folders"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
name = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="folders")
saved_articles = relationship("SavedArticle", back_populates="folder")
class SavedArticle(Base):
__tablename__ = "saved_articles"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
folder_id = Column(Integer, ForeignKey("folders.id"), nullable=True)
news_id = Column(Integer, ForeignKey("verified_news.id"))
saved_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="saved_articles")
folder = relationship("Folder", back_populates="saved_articles")
news = relationship("VerifiedNews")
class ReadHistory(Base):
__tablename__ = "read_history"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
news_id = Column(Integer, ForeignKey("verified_news.id"))
read_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="read_history")
news = relationship("VerifiedNews")
class FlaggedArticle(Base):
__tablename__ = "flagged_articles"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
news_id = Column(Integer, ForeignKey("verified_news.id"))
reason = Column(String, nullable=True)
flagged_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User")
news = relationship("VerifiedNews")
class BreakingNews(Base):
__tablename__ = "breaking_news"
id = Column(Integer, primary_key=True, index=True)
verified_news_id = Column(Integer, ForeignKey("verified_news.id"))
classification = Column(String) # Breaking News, Developing News, Top Headline
breaking_headline = Column(String)
what_happened = Column(JSON) # List of bullet points
why_matters = Column(Text)
next_updates = Column(JSON) # List of possible next updates
confidence_level = Column(String) # High, Medium, Low
impact_score = Column(Integer) # 1-10
recency_minutes = Column(Integer)
url = Column(String, nullable=True) # Direct access for performance
image_url = Column(String, nullable=True) # Cached image link
created_at = Column(DateTime, default=datetime.utcnow)
last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
verified_news = relationship("VerifiedNews")
class Advertisement(Base):
__tablename__ = "advertisements"
id = Column(Integer, primary_key=True, index=True)
image_url = Column(String, nullable=False)
caption = Column(String, nullable=True)
position = Column(String, default="both") # "left", "right", "both", "mobile"
target_node = Column(String, default="Global")
target_url = Column(String, nullable=True)
target_platform = Column(String, default="both") # "main", "student", "both"
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
class Newspaper(Base):
__tablename__ = "newspapers"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
url = Column(String, nullable=False)
logo_text = Column(String, nullable=True) # e.g. "NYT"
logo_color = Column(String, nullable=True) # e.g. "#000000"
country = Column(String, default="Global")
created_at = Column(DateTime, default=datetime.utcnow)
class ProtocolHistory(Base):
__tablename__ = "protocol_history"
id = Column(Integer, primary_key=True, index=True)
action = Column(String, nullable=False) # e.g. 'deploy', 'delete', 'register'
target_type = Column(String, nullable=False) # e.g. 'article', 'source', 'ad'
target_id = Column(String, nullable=True)
admin_user = Column(String, nullable=False)
details = Column(Text, nullable=True)
timestamp = Column(DateTime, default=datetime.utcnow)
class SystemConfig(Base):
__tablename__ = "system_config"
id = Column(Integer, primary_key=True, index=True)
config_key = Column(String, unique=True, index=True) # e.g. 'show_exams_section'
config_value = Column(String) # 'true' or 'false' or JSON
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
if DATABASE_URL.startswith("sqlite"):
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
# Production Optimized Engine for PostgreSQL (Supabase/Railway with pgBouncer compatibility)
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # Checks if connection is alive before using it
pool_recycle=600, # Recycles connections every 10 minutes to prevent stale pgBouncer links
pool_size=15, # Cap baseline connection pool size
max_overflow=25, # Maximum transient bursting connections
connect_args={
"connect_timeout": 30,
"application_name": "UniArcBackendPooling"
}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def init_db():
print(f"[DEBUG] Initializing database with engine: {engine.url}")
Base.metadata.create_all(bind=engine)
@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
if DATABASE_URL.startswith("sqlite"):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA synchronous=NORMAL")
cursor.close()