208 lines
8.4 KiB
Python
208 lines
8.4 KiB
Python
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)
|
|
|
|
# Optional schedule window in UTC.
|
|
# - If both are NULL: playlist is always active.
|
|
# - If start is set: playlist is active from start onward.
|
|
# - If end is set: playlist is active until end.
|
|
schedule_start = db.Column(db.DateTime, nullable=True)
|
|
schedule_end = db.Column(db.DateTime, nullable=True)
|
|
|
|
# If true, this playlist's items take precedence over non-priority playlists
|
|
# when multiple playlists are assigned to a display.
|
|
is_priority = db.Column(db.Boolean, default=False, 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)
|
|
# Transition animation between slides: none|fade|slide
|
|
transition = db.Column(db.String(20), 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,
|
|
)
|