- 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.
110 lines
3.8 KiB
Python
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
|