import uuid from datetime import datetime from flask_login import UserMixin from werkzeug.security import check_password_hash, generate_password_hash from .extensions import db class Company(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(120), unique=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) # Optional per-company storage quota for uploaded media (bytes). # If NULL or <=0: unlimited. storage_max_bytes = db.Column(db.BigInteger, nullable=True) users = db.relationship("User", back_populates="company", cascade="all, delete-orphan") displays = db.relationship("Display", back_populates="company", cascade="all, delete-orphan") playlists = db.relationship("Playlist", back_populates="company", cascade="all, delete-orphan") class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) # Backwards compatibility: older SQLite DBs (and some templates) expect a username column. # The app no longer uses username for login/display, but we keep it populated (= email) # to avoid integrity errors without introducing Alembic migrations. username = db.Column(db.String(255), unique=True, nullable=False) email = db.Column(db.String(255), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=True) is_admin = db.Column(db.Boolean, default=False, nullable=False) company_id = db.Column(db.Integer, db.ForeignKey("company.id"), nullable=True) company = db.relationship("Company", back_populates="users") created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) def set_password(self, password: str): self.password_hash = generate_password_hash(password) def check_password(self, password: str) -> bool: if not self.password_hash: return False return check_password_hash(self.password_hash, password) class Playlist(db.Model): id = db.Column(db.Integer, primary_key=True) company_id = db.Column(db.Integer, db.ForeignKey("company.id"), nullable=False) name = db.Column(db.String(120), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) company = db.relationship("Company", back_populates="playlists") items = db.relationship( "PlaylistItem", back_populates="playlist", cascade="all, delete-orphan", order_by="PlaylistItem.position.asc()", ) class PlaylistItem(db.Model): id = db.Column(db.Integer, primary_key=True) playlist_id = db.Column(db.Integer, db.ForeignKey("playlist.id"), nullable=False) # image|video|webpage|youtube item_type = db.Column(db.String(20), nullable=False) title = db.Column(db.String(200), nullable=True) # For image/video: stored under /static/uploads file_path = db.Column(db.String(400), nullable=True) # For webpage url = db.Column(db.String(1000), nullable=True) # For image/webpage duration (seconds). For video we play until ended. duration_seconds = db.Column(db.Integer, default=10, nullable=False) position = db.Column(db.Integer, default=0, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) playlist = db.relationship("Playlist", back_populates="items") class Display(db.Model): id = db.Column(db.Integer, primary_key=True) company_id = db.Column(db.Integer, db.ForeignKey("company.id"), nullable=False) name = db.Column(db.String(120), nullable=False) # Optional short description (e.g. "entrance", "office") description = db.Column(db.String(200), nullable=True) token = db.Column(db.String(64), unique=True, nullable=False, default=lambda: uuid.uuid4().hex) assigned_playlist_id = db.Column(db.Integer, db.ForeignKey("playlist.id"), nullable=True) assigned_playlist = db.relationship("Playlist") # Multi-playlist support (active playlists per display). # If a display has any rows in display_playlist, those are used by the player. # If not, we fall back to assigned_playlist_id for backwards compatibility. display_playlists = db.relationship( "DisplayPlaylist", back_populates="display", cascade="all, delete-orphan", ) playlists = db.relationship( "Playlist", secondary="display_playlist", viewonly=True, order_by="Playlist.name.asc()", ) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) company = db.relationship("Company", back_populates="displays") class DisplaySession(db.Model): """Tracks active viewers of a display token so we can limit concurrent usage.""" id = db.Column(db.Integer, primary_key=True) display_id = db.Column(db.Integer, db.ForeignKey("display.id"), nullable=False, index=True) sid = db.Column(db.String(64), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) last_seen_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True) # Optional diagnostics ip = db.Column(db.String(64), nullable=True) user_agent = db.Column(db.String(300), nullable=True) display = db.relationship("Display") __table_args__ = (db.UniqueConstraint("display_id", "sid", name="uq_display_session_display_sid"),) class DisplayPlaylist(db.Model): """Association table: which playlists are active on a display.""" # NOTE: Some existing databases include an `id` INTEGER PRIMARY KEY column and a # NOT NULL `position` column on display_playlist. We keep the mapper primary key as # (display_id, playlist_id) for portability, while allowing an optional `id` column # to exist in the underlying table. id = db.Column(db.Integer, nullable=True) # Composite mapper PK ensures uniqueness per display. display_id = db.Column(db.Integer, db.ForeignKey("display.id"), primary_key=True) playlist_id = db.Column(db.Integer, db.ForeignKey("playlist.id"), primary_key=True) # Ordering of playlists within a display. position = db.Column(db.Integer, default=1, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) display = db.relationship("Display", back_populates="display_playlists") playlist = db.relationship("Playlist") __table_args__ = ( db.UniqueConstraint("display_id", "playlist_id", name="uq_display_playlist_display_playlist"), ) class AppSettings(db.Model): """Singleton-ish app-wide settings. For this small project we avoid Alembic migrations; this table can be created via `flask init-db` (db.create_all) and is also created best-effort on app startup. NOTE: SMTP password is stored in plaintext in the database. Prefer environment variables / secrets management in production when possible. """ id = db.Column(db.Integer, primary_key=True) smtp_host = db.Column(db.String(255), nullable=True) smtp_port = db.Column(db.Integer, nullable=True) smtp_username = db.Column(db.String(255), nullable=True) smtp_password = db.Column(db.String(255), nullable=True) smtp_from = db.Column(db.String(255), nullable=True) smtp_starttls = db.Column(db.Boolean, default=True, nullable=False) smtp_timeout_seconds = db.Column(db.Float, default=10.0, nullable=False) smtp_debug = db.Column(db.Boolean, default=False, nullable=False) # Public domain for generating absolute links in emails. # Example: "signage.example.com" (no scheme) public_domain = db.Column(db.String(255), nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated_at = db.Column( db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False, )