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 (
|
from sqlalchemy import (
|
||||||
Column, Integer, String, Enum, TIMESTAMP, func, Boolean, ForeignKey, Float, Text, Index, DateTime
|
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
|
import enum
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
@@ -122,6 +122,9 @@ class Event(Base):
|
|||||||
updated_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
updated_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
|
# Add relationship to EventMedia
|
||||||
|
event_media = relationship("EventMedia", foreign_keys=[event_media_id])
|
||||||
|
|
||||||
|
|
||||||
class EventMedia(Base):
|
class EventMedia(Base):
|
||||||
__tablename__ = 'event_media'
|
__tablename__ = 'event_media'
|
||||||
@@ -130,7 +133,8 @@ class EventMedia(Base):
|
|||||||
url = Column(String(255), nullable=False)
|
url = Column(String(255), nullable=False)
|
||||||
file_path = Column(String(255), nullable=True)
|
file_path = Column(String(255), nullable=True)
|
||||||
message_content = Column(Text, 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):
|
def to_dict(self):
|
||||||
return {
|
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
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker, joinedload
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from models.models import Event
|
from models.models import Event, EventMedia
|
||||||
# import sys
|
|
||||||
# sys.path.append('/workspace/server')
|
|
||||||
|
|
||||||
load_dotenv('/workspace/.env')
|
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)
|
engine = create_engine(DB_CONN)
|
||||||
Session = sessionmaker(bind=engine)
|
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):
|
def get_active_events(start: datetime, end: datetime, group_id: int = None):
|
||||||
session = Session()
|
session = Session()
|
||||||
query = session.query(Event).filter(Event.is_active == True)
|
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:
|
if start and end:
|
||||||
query = query.filter(Event.start < end, Event.end > start)
|
query = query.filter(Event.start < end, Event.end > start)
|
||||||
if group_id:
|
if group_id:
|
||||||
query = query.filter(Event.group_id == group_id)
|
query = query.filter(Event.group_id == group_id)
|
||||||
|
|
||||||
events = query.all()
|
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()
|
session.close()
|
||||||
return events
|
|
||||||
|
|
||||||
|
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:
|
while True:
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
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)
|
events = get_active_events(now, now)
|
||||||
|
|
||||||
# Gruppiere Events nach group_id
|
# Gruppiere Events nach group_id
|
||||||
groups = {}
|
groups = {}
|
||||||
for event in events:
|
for event in events:
|
||||||
gid = getattr(event, "group_id", None)
|
gid = event.get("group_id")
|
||||||
if gid not in groups:
|
if gid not in groups:
|
||||||
groups[gid] = []
|
groups[gid] = []
|
||||||
groups[gid].append({
|
# Event ist bereits ein Dictionary im gewünschten Format
|
||||||
"id": event.id,
|
groups[gid].append(event)
|
||||||
"title": getattr(event, "title", ""),
|
|
||||||
"start": str(getattr(event, "start", "")),
|
|
||||||
"end": str(getattr(event, "end", "")),
|
|
||||||
"group_id": gid,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sende pro Gruppe die Eventliste als retained Message, nur bei Änderung
|
# Sende pro Gruppe die Eventliste als retained Message, nur bei Änderung
|
||||||
for gid, event_list in groups.items():
|
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
|
# server/wsgi.py
|
||||||
from server.routes.eventmedia import eventmedia_bp
|
from server.routes.eventmedia import eventmedia_bp
|
||||||
|
from server.routes.files import files_bp
|
||||||
from server.routes.events import events_bp
|
from server.routes.events import events_bp
|
||||||
from server.routes.groups import groups_bp
|
from server.routes.groups import groups_bp
|
||||||
from server.routes.clients import clients_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(groups_bp)
|
||||||
app.register_blueprint(events_bp)
|
app.register_blueprint(events_bp)
|
||||||
app.register_blueprint(eventmedia_bp)
|
app.register_blueprint(eventmedia_bp)
|
||||||
|
app.register_blueprint(files_bp)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/health")
|
@app.route("/health")
|
||||||
|
|||||||
Reference in New Issue
Block a user