Initial import
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user