Initial import

This commit is contained in:
2026-01-25 13:26:45 +01:00
parent a5fe0f73a0
commit f4b7fb62f5
8 changed files with 834 additions and 149 deletions

View File

@@ -178,6 +178,39 @@ def _try_delete_upload(file_path: str | None, upload_root: str):
bp = Blueprint("company", __name__, url_prefix="/company")
def _parse_schedule_local_to_utc(*, date_str: str | None, time_str: str | None) -> datetime | None:
"""Parse local date+time form inputs into a naive UTC datetime.
Inputs come from <input type="date"> and <input type="time">.
We interpret them as *local* time of the server.
Note: this project currently does not store per-company timezone; in most deployments
server timezone matches users. If you need per-company timezone later, we can extend
this function.
"""
d = (date_str or "").strip()
t = (time_str or "").strip()
if not d and not t:
return None
if not d or not t:
# Require both parts for clarity
raise ValueError("Both date and time are required")
# Basic parsing: YYYY-MM-DD and HH:MM
try:
year, month, day = [int(x) for x in d.split("-")]
hh, mm = [int(x) for x in t.split(":")[:2]]
except Exception:
raise ValueError("Invalid date/time")
# Interpret as local time, convert to UTC naive
local_dt = datetime(year, month, day, hh, mm)
# local_dt.timestamp() uses local timezone when naive.
utc_ts = local_dt.timestamp()
return datetime.utcfromtimestamp(utc_ts)
def company_user_required():
if not current_user.is_authenticated:
abort(403)
@@ -347,6 +380,7 @@ def dashboard():
return render_template(
"company/dashboard.html",
playlists=playlists,
now_utc=datetime.utcnow(),
playlists_json=playlists_json,
displays=displays,
)
@@ -374,7 +408,7 @@ def playlist_detail(playlist_id: int):
playlist = db.session.get(Playlist, playlist_id)
if not playlist or playlist.company_id != current_user.company_id:
abort(404)
return render_template("company/playlist_detail.html", playlist=playlist)
return render_template("company/playlist_detail.html", playlist=playlist, now_utc=datetime.utcnow())
@bp.post("/playlists/<int:playlist_id>")
@@ -405,6 +439,89 @@ def update_playlist(playlist_id: int):
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
@bp.post("/playlists/<int:playlist_id>/schedule")
@login_required
def update_playlist_schedule(playlist_id: int):
"""Update playlist schedule window + priority flag."""
company_user_required()
playlist = db.session.get(Playlist, playlist_id)
if not playlist or playlist.company_id != current_user.company_id:
abort(404)
try:
start = _parse_schedule_local_to_utc(
date_str=request.form.get("schedule_start_date"),
time_str=request.form.get("schedule_start_time"),
)
end = _parse_schedule_local_to_utc(
date_str=request.form.get("schedule_end_date"),
time_str=request.form.get("schedule_end_time"),
)
except ValueError as e:
flash(str(e), "danger")
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
if start and end and end < start:
flash("End must be after start", "danger")
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
playlist.schedule_start = start
playlist.schedule_end = end
db.session.commit()
flash("Schedule updated", "success")
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
@bp.post("/playlists/<int:playlist_id>/schedule/delete")
@login_required
def clear_playlist_schedule(playlist_id: int):
"""Clear schedule for a playlist (sets start/end to NULL)."""
company_user_required()
playlist = db.session.get(Playlist, playlist_id)
if not playlist or playlist.company_id != current_user.company_id:
abort(404)
playlist.schedule_start = None
playlist.schedule_end = None
db.session.commit()
flash("Schedule removed", "success")
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
@bp.post("/playlists/<int:playlist_id>/priority")
@login_required
def update_playlist_priority(playlist_id: int):
"""Update playlist priority flag."""
company_user_required()
playlist = db.session.get(Playlist, playlist_id)
if not playlist or playlist.company_id != current_user.company_id:
abort(404)
wants_json = (
(request.headers.get("X-Requested-With") == "XMLHttpRequest")
or ("application/json" in (request.headers.get("Accept") or ""))
or request.is_json
)
# Accept both form and JSON payloads.
raw = request.form.get("is_priority")
if raw is None and request.is_json:
raw = (request.get_json(silent=True) or {}).get("is_priority")
playlist.is_priority = bool((raw or "").strip())
db.session.commit()
if wants_json:
return jsonify({"ok": True, "is_priority": bool(playlist.is_priority)})
flash("Priority updated", "success")
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
@bp.post("/playlists/<int:playlist_id>/delete")
@login_required
def delete_playlist(playlist_id: int):
@@ -850,6 +967,14 @@ def update_display(display_id: int):
def _json_error(message: str, status: int = 400):
return jsonify({"ok": False, "error": message}), status
def _normalize_transition(val: str | None) -> str | None:
v = (val or "").strip().lower()
if not v:
return None
if v not in {"none", "fade", "slide"}:
return None
return v
# Inputs from either form or JSON
payload = request.get_json(silent=True) if request.is_json else None
@@ -869,6 +994,16 @@ def update_display(display_id: int):
desc = desc[:200]
display.description = desc
# Slide transition
if request.is_json:
if payload is None:
return _json_error("Invalid JSON")
if "transition" in payload:
display.transition = _normalize_transition(payload.get("transition"))
else:
# Form POST implies full update
display.transition = _normalize_transition(request.form.get("transition"))
# Playlist assignment
if request.is_json:
if "playlist_id" in payload:
@@ -908,6 +1043,7 @@ def update_display(display_id: int):
"id": display.id,
"name": display.name,
"description": display.description,
"transition": display.transition,
"assigned_playlist_id": display.assigned_playlist_id,
},
}