Files
infoscreen/server/routes/holidays.py
olaf 41194000a4 feat: Add academic periods system for educational institutions
- Add AcademicPeriod model with support for schuljahr/semester/trimester
- Extend Event and EventMedia models with optional academic_period_id
- Create Alembic migration (8d1df7199cb7) for academic periods system
- Add init script for Austrian school year defaults (2024/25-2026/27)
- Maintain full backward compatibility for existing events/media
- Update program-info.json to version 2025.1.0-alpha.6

Database changes:
- New academic_periods table with unique name constraint
- Foreign key relationships with proper indexing
- Support for multiple period types with single active period

This lays the foundation for period-based organization of events
and media content, specifically designed for school environments
with future extensibility for universities.
2025-09-20 11:16:56 +00:00

110 lines
3.8 KiB
Python

from flask import Blueprint, request, jsonify
from server.database import Session
from models.models import SchoolHoliday
from datetime import datetime
import csv
import io
holidays_bp = Blueprint("holidays", __name__, url_prefix="/api/holidays")
@holidays_bp.route("", methods=["GET"])
def list_holidays():
session = Session()
region = request.args.get("region")
q = session.query(SchoolHoliday)
if region:
q = q.filter(SchoolHoliday.region == region)
rows = q.order_by(SchoolHoliday.start_date.asc()).all()
data = [r.to_dict() for r in rows]
session.close()
return jsonify({"holidays": data})
@holidays_bp.route("/upload", methods=["POST"])
def upload_holidays():
"""
Accepts a CSV file upload (multipart/form-data) with columns like:
name,start_date,end_date,region
Dates can be in ISO (YYYY-MM-DD) or common European format (DD.MM.YYYY).
"""
if "file" not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files["file"]
if file.filename == "":
return jsonify({"error": "No selected file"}), 400
try:
content = file.read().decode("utf-8", errors="ignore")
# Try to auto-detect delimiter; default ','
sniffer = csv.Sniffer()
dialect = None
try:
dialect = sniffer.sniff(content[:1024])
except Exception:
pass
reader = csv.DictReader(io.StringIO(
content), dialect=dialect) if dialect else csv.DictReader(io.StringIO(content))
required = {"name", "start_date", "end_date"}
if not required.issubset(set(h.lower() for h in reader.fieldnames or [])):
return jsonify({"error": "CSV must contain headers: name, start_date, end_date"}), 400
def parse_date(s: str):
s = (s or "").strip()
if not s:
return None
# Try ISO first
for fmt in ("%Y-%m-%d", "%d.%m.%Y", "%Y/%m/%d"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
raise ValueError(f"Unsupported date format: {s}")
session = Session()
inserted = 0
updated = 0
for row in reader:
# Normalize headers to lower-case keys
norm = {k.lower(): (v or "").strip() for k, v in row.items()}
name = norm.get("name")
start_date = parse_date(norm.get("start_date"))
end_date = parse_date(norm.get("end_date"))
region = norm.get("region") or None
if not name or not start_date or not end_date:
continue
existing = (
session.query(SchoolHoliday)
.filter(
SchoolHoliday.name == name,
SchoolHoliday.start_date == start_date,
SchoolHoliday.end_date == end_date,
SchoolHoliday.region.is_(
region) if region is None else SchoolHoliday.region == region,
)
.first()
)
if existing:
# Optionally update region or source_file_name
existing.region = region
existing.source_file_name = file.filename
updated += 1
else:
session.add(SchoolHoliday(
name=name,
start_date=start_date,
end_date=end_date,
region=region,
source_file_name=file.filename,
))
inserted += 1
session.commit()
session.close()
return jsonify({"success": True, "inserted": inserted, "updated": updated})
except Exception as e:
return jsonify({"error": str(e)}), 400