add nginx.dev.conf for development environment
add functionality of scheduler to send right event data to the clients added route for file download
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Enum, TIMESTAMP, func, Boolean, ForeignKey, Float, Text, Index, DateTime
|
||||
)
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
import enum
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -122,6 +122,9 @@ class Event(Base):
|
||||
updated_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Add relationship to EventMedia
|
||||
event_media = relationship("EventMedia", foreign_keys=[event_media_id])
|
||||
|
||||
|
||||
class EventMedia(Base):
|
||||
__tablename__ = 'event_media'
|
||||
@@ -130,7 +133,8 @@ class EventMedia(Base):
|
||||
url = Column(String(255), nullable=False)
|
||||
file_path = Column(String(255), nullable=True)
|
||||
message_content = Column(Text, nullable=True)
|
||||
uploaded_at = Column(TIMESTAMP, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
uploaded_at = Column(TIMESTAMP, nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
||||
47
nginx.dev.conf
Normal file
47
nginx.dev.conf
Normal file
@@ -0,0 +1,47 @@
|
||||
events {}
|
||||
http {
|
||||
upstream dashboard {
|
||||
# Vite dev server inside the dashboard container
|
||||
server infoscreen-dashboard:5173;
|
||||
}
|
||||
upstream infoscreen_api {
|
||||
server infoscreen-api:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Proxy /api and /screenshots to the Flask API
|
||||
location /api/ {
|
||||
proxy_pass http://infoscreen_api/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /screenshots/ {
|
||||
proxy_pass http://infoscreen_api/screenshots/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Everything else to the Vite dev server
|
||||
location / {
|
||||
proxy_pass http://dashboard;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# WebSocket upgrade for Vite HMR
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import sessionmaker, joinedload
|
||||
from sqlalchemy import create_engine
|
||||
from models.models import Event
|
||||
# import sys
|
||||
# sys.path.append('/workspace/server')
|
||||
from models.models import Event, EventMedia
|
||||
|
||||
load_dotenv('/workspace/.env')
|
||||
|
||||
@@ -15,14 +13,66 @@ DB_CONN = os.environ.get("DB_CONN", "mysql+pymysql://user:password@db/dbname")
|
||||
engine = create_engine(DB_CONN)
|
||||
Session = sessionmaker(bind=engine)
|
||||
|
||||
# Base URL from .env for file URLs
|
||||
API_BASE_URL = os.environ.get("API_BASE_URL", "http://server:8000")
|
||||
|
||||
|
||||
def get_active_events(start: datetime, end: datetime, group_id: int = None):
|
||||
session = Session()
|
||||
query = session.query(Event).filter(Event.is_active == True)
|
||||
if start and end:
|
||||
query = query.filter(Event.start < end, Event.end > start)
|
||||
if group_id:
|
||||
query = query.filter(Event.group_id == group_id)
|
||||
events = query.all()
|
||||
session.close()
|
||||
return events
|
||||
try:
|
||||
# Now this will work with the relationship defined
|
||||
query = session.query(Event).options(
|
||||
joinedload(Event.event_media)
|
||||
).filter(Event.is_active == True)
|
||||
|
||||
if start and end:
|
||||
query = query.filter(Event.start < end, Event.end > start)
|
||||
if group_id:
|
||||
query = query.filter(Event.group_id == group_id)
|
||||
|
||||
events = query.all()
|
||||
|
||||
formatted_events = []
|
||||
for event in events:
|
||||
formatted_event = format_event_with_media(event)
|
||||
formatted_events.append(formatted_event)
|
||||
|
||||
return formatted_events
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def format_event_with_media(event):
|
||||
"""Transform Event + EventMedia into client-expected format"""
|
||||
event_dict = {
|
||||
"id": event.id,
|
||||
"title": event.title,
|
||||
"start": str(event.start),
|
||||
"end": str(event.end),
|
||||
"group_id": event.group_id,
|
||||
}
|
||||
|
||||
# Now you can directly access event.event_media
|
||||
if event.event_media:
|
||||
media = event.event_media
|
||||
|
||||
if event.event_type.value == "presentation":
|
||||
event_dict["presentation"] = {
|
||||
"type": "slideshow",
|
||||
"files": [],
|
||||
"slide_interval": event.slideshow_interval or 5000,
|
||||
"auto_advance": True
|
||||
}
|
||||
|
||||
if media.file_path:
|
||||
filename = os.path.basename(media.file_path)
|
||||
event_dict["presentation"]["files"].append({
|
||||
"name": filename,
|
||||
"url": f"{API_BASE_URL}/api/files/{media.id}/{filename}",
|
||||
"checksum": None,
|
||||
"size": None
|
||||
})
|
||||
|
||||
# Add other event types...
|
||||
|
||||
return event_dict
|
||||
|
||||
@@ -55,22 +55,17 @@ def main():
|
||||
|
||||
while True:
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
# Hole alle aktiven Events (Vergleich mit UTC)
|
||||
# Hole alle aktiven Events (bereits formatierte Dictionaries)
|
||||
events = get_active_events(now, now)
|
||||
|
||||
# Gruppiere Events nach group_id
|
||||
groups = {}
|
||||
for event in events:
|
||||
gid = getattr(event, "group_id", None)
|
||||
gid = event.get("group_id")
|
||||
if gid not in groups:
|
||||
groups[gid] = []
|
||||
groups[gid].append({
|
||||
"id": event.id,
|
||||
"title": getattr(event, "title", ""),
|
||||
"start": str(getattr(event, "start", "")),
|
||||
"end": str(getattr(event, "end", "")),
|
||||
"group_id": gid,
|
||||
})
|
||||
# Event ist bereits ein Dictionary im gewünschten Format
|
||||
groups[gid].append(event)
|
||||
|
||||
# Sende pro Gruppe die Eventliste als retained Message, nur bei Änderung
|
||||
for gid, event_list in groups.items():
|
||||
|
||||
57
server/routes/files.py
Normal file
57
server/routes/files.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from flask import Blueprint, jsonify, send_from_directory
|
||||
from server.database import Session
|
||||
from models.models import EventMedia
|
||||
import os
|
||||
|
||||
# Blueprint for direct file downloads by media ID
|
||||
files_bp = Blueprint("files", __name__, url_prefix="/api/files")
|
||||
|
||||
# Reuse the same media root convention as eventmedia.py
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
|
||||
@files_bp.route("/<int:media_id>/<path:filename>", methods=["GET"])
|
||||
def download_media_file(media_id: int, filename: str):
|
||||
"""
|
||||
Download the stored media file for a given EventMedia ID.
|
||||
|
||||
URL format example:
|
||||
/api/files/26/LPUV4I_Folien_Nowitzki_Bewertungskriterien.pptx
|
||||
|
||||
Behavior:
|
||||
- Looks up EventMedia by ID
|
||||
- Validates requested filename against stored metadata (best-effort)
|
||||
- Serves the file from server/media using the stored relative file_path
|
||||
"""
|
||||
session = Session()
|
||||
media = session.query(EventMedia).get(media_id)
|
||||
if not media:
|
||||
session.close()
|
||||
return jsonify({"error": "Not found"}), 404
|
||||
|
||||
# Prefer the stored relative file_path; fall back to the URL/filename
|
||||
rel_path = media.file_path or media.url
|
||||
|
||||
# Basic filename consistency check to avoid leaking other files
|
||||
# Only enforce if media.url is present
|
||||
if media.url and os.path.basename(filename) != os.path.basename(media.url):
|
||||
session.close()
|
||||
return jsonify({
|
||||
"error": "Filename mismatch",
|
||||
"expected": os.path.basename(media.url),
|
||||
"got": os.path.basename(filename),
|
||||
}), 400
|
||||
|
||||
abs_path = os.path.join(MEDIA_ROOT, rel_path)
|
||||
|
||||
# Ensure file exists
|
||||
if not os.path.isfile(abs_path):
|
||||
session.close()
|
||||
return jsonify({"error": "File not found on server"}), 404
|
||||
|
||||
# Serve as attachment (download)
|
||||
directory = os.path.dirname(abs_path)
|
||||
served_name = os.path.basename(abs_path)
|
||||
session.close()
|
||||
return send_from_directory(directory, served_name, as_attachment=True)
|
||||
@@ -1,5 +1,6 @@
|
||||
# server/wsgi.py
|
||||
from server.routes.eventmedia import eventmedia_bp
|
||||
from server.routes.files import files_bp
|
||||
from server.routes.events import events_bp
|
||||
from server.routes.groups import groups_bp
|
||||
from server.routes.clients import clients_bp
|
||||
@@ -18,6 +19,7 @@ app.register_blueprint(clients_bp)
|
||||
app.register_blueprint(groups_bp)
|
||||
app.register_blueprint(events_bp)
|
||||
app.register_blueprint(eventmedia_bp)
|
||||
app.register_blueprint(files_bp)
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
|
||||
Reference in New Issue
Block a user