from flask import Blueprint, request, jsonify from server.database import Session from models.models import Event, EventMedia, MediaType, EventException from datetime import datetime, timezone, timedelta from sqlalchemy import and_ from dateutil.rrule import rrulestr from dateutil.tz import UTC import sys sys.path.append('/workspace') events_bp = Blueprint("events", __name__, url_prefix="/api/events") def get_icon_for_type(event_type): # Lucide-Icon-Namen als String return { "presentation": "Presentation", # <--- geändert! "website": "Globe", "video": "Video", "message": "MessageSquare", "webuntis": "School", }.get(event_type, "") @events_bp.route("", methods=["GET"]) def get_events(): session = Session() start = request.args.get("start") end = request.args.get("end") group_id = request.args.get("group_id") show_inactive = request.args.get( "show_inactive", "0") == "1" # Checkbox-Logik # Always let Syncfusion handle recurrence; do not expand on backend expand = False now = datetime.now(timezone.utc) events_query = session.query(Event) if group_id: events_query = events_query.filter(Event.group_id == int(group_id)) events = events_query.all() result = [] for e in events: # Zeitzonen-Korrektur für e.end if e.end and e.end.tzinfo is None: end_dt = e.end.replace(tzinfo=timezone.utc) else: end_dt = e.end # Setze is_active auf False, wenn Termin vorbei ist if end_dt and end_dt < now and e.is_active: e.is_active = False session.commit() if not (show_inactive or e.is_active): continue # Gather exception dates for this event (for recurrenceException/EXDATE) exception_dates = session.query(EventException).filter( EventException.event_id == e.id, EventException.is_skipped == True ).all() # Syncfusion expects recurrenceException as comma-separated ISO strings (yyyy-MM-ddTHH:mm:ssZ) # IMPORTANT: The time must match the event's occurrence start time. Use the event's start time-of-day. recurrence_exception = None if exception_dates: # Use event start time in UTC as baseline base_start = e.start.astimezone(UTC) if e.start.tzinfo else e.start.replace(tzinfo=UTC) tokens = [] for d in exception_dates: exd = d.exception_date # date occ_dt = datetime( exd.year, exd.month, exd.day, base_start.hour, base_start.minute, base_start.second, tzinfo=UTC ) tokens.append(occ_dt.strftime('%Y-%m-%dT%H:%M:%SZ')) recurrence_exception = ','.join(tokens) base_payload = { "Id": str(e.id), "GroupId": e.group_id, "Subject": e.title, "StartTime": e.start.isoformat() if e.start else None, "EndTime": e.end.isoformat() if e.end else None, "IsAllDay": False, "MediaId": e.event_media_id, "Type": e.event_type.value if e.event_type else None, # <-- Enum zu String! "Icon": get_icon_for_type(e.event_type.value if e.event_type else None), # Recurrence metadata "RecurrenceRule": e.recurrence_rule, "RecurrenceEnd": e.recurrence_end.isoformat() if e.recurrence_end else None, "RecurrenceException": recurrence_exception, "SkipHolidays": bool(getattr(e, 'skip_holidays', False)), } result.append(base_payload) session.close() return jsonify(result) @events_bp.route("/", methods=["DELETE"]) def delete_event(event_id): session = Session() event = session.query(Event).filter_by(id=event_id).first() if not event: session.close() return jsonify({"error": "Termin nicht gefunden"}), 404 session.delete(event) session.commit() session.close() return jsonify({"success": True}) @events_bp.route("", methods=["POST"]) def create_event(): data = request.json session = Session() # Pflichtfelder prüfen required = ["group_id", "title", "description", "start", "end", "event_type", "created_by"] for field in required: if field not in data: return jsonify({"error": f"Missing field: {field}"}), 400 event_type = data["event_type"] event_media_id = None slideshow_interval = None # Präsentation: event_media_id und slideshow_interval übernehmen if event_type == "presentation": event_media_id = data.get("event_media_id") slideshow_interval = data.get("slideshow_interval") if not event_media_id: return jsonify({"error": "event_media_id required for presentation"}), 400 # Website: Webseite als EventMedia anlegen und ID übernehmen if event_type == "website": website_url = data.get("website_url") if not website_url: return jsonify({"error": "website_url required for website"}), 400 # EventMedia für Webseite anlegen media = EventMedia( media_type=MediaType.website, url=website_url, file_path=website_url ) session.add(media) session.commit() event_media_id = media.id # created_by aus den Daten holen, Default: None created_by = data.get("created_by") # Start- und Endzeit in UTC umwandeln, falls kein Zulu-Zeitstempel start = datetime.fromisoformat(data["start"]) end = datetime.fromisoformat(data["end"]) if start.tzinfo is None: start = start.astimezone(timezone.utc) if end.tzinfo is None: end = end.astimezone(timezone.utc) # Determine skip_holidays from either camelCase or snake_case skip_holidays_val = bool(data.get("skipHolidays")) or bool(data.get("skip_holidays")) # Event anlegen event = Event( group_id=data["group_id"], title=data["title"], description=data["description"], start=start, end=end, event_type=event_type, is_active=True, event_media_id=event_media_id, slideshow_interval=slideshow_interval, created_by=created_by, # Recurrence recurrence_rule=data.get("recurrence_rule"), skip_holidays=skip_holidays_val, recurrence_end=(datetime.fromisoformat(data["recurrence_end"]) if data.get("recurrence_end") else None), ) session.add(event) session.commit() # --- Holiday exception creation (backend) --- def regenerate_event_exceptions(ev: Event): from models.models import SchoolHoliday, EventException from dateutil.rrule import rrulestr from dateutil.tz import UTC # Clear existing exceptions for this event session.query(EventException).filter_by(event_id=ev.id).delete() session.commit() if not (ev.skip_holidays and ev.recurrence_rule): return # Get holidays holidays = session.query(SchoolHoliday).all() dtstart = ev.start.astimezone(UTC) r = rrulestr(ev.recurrence_rule, dtstart=dtstart) window_start = dtstart window_end = ev.recurrence_end.astimezone(UTC) if ev.recurrence_end else dtstart.replace(year=dtstart.year + 1) # Build set of all holiday dates (inclusive) holiday_dates = set() for h in holidays: hs = h.start_date he = h.end_date d = hs while d <= he: holiday_dates.add(d) d = d + timedelta(days=1) # Create exceptions for occurrences on holiday dates for occ_start in r.between(window_start, window_end, inc=True): occ_date = occ_start.date() if occ_date in holiday_dates: session.add(EventException(event_id=ev.id, exception_date=occ_date, is_skipped=True)) session.commit() regenerate_event_exceptions(event) return jsonify({"success": True, "event_id": event.id}) @events_bp.route("/", methods=["PUT"]) def update_event(event_id): data = request.json session = Session() event = session.query(Event).filter_by(id=event_id).first() if not event: session.close() return jsonify({"error": "Termin nicht gefunden"}), 404 event.title = data.get("title", event.title) event.description = data.get("description", event.description) event.start = datetime.fromisoformat( data["start"]) if "start" in data else event.start event.end = datetime.fromisoformat( data["end"]) if "end" in data else event.end event.event_type = data.get("event_type", event.event_type) event.event_media_id = data.get("event_media_id", event.event_media_id) event.slideshow_interval = data.get("slideshow_interval", event.slideshow_interval) event.created_by = data.get("created_by", event.created_by) # Track previous values to decide on exception regeneration prev_rule = event.recurrence_rule prev_end = event.recurrence_end prev_skip = bool(getattr(event, 'skip_holidays', False)) # Recurrence updates if "recurrence_rule" in data: event.recurrence_rule = data.get("recurrence_rule") if "recurrence_end" in data: rec_end_val = data.get("recurrence_end") event.recurrence_end = datetime.fromisoformat(rec_end_val) if rec_end_val else None # Skip holidays can be updated independently if "skipHolidays" in data or "skip_holidays" in data: event.skip_holidays = bool(data.get("skipHolidays") or data.get("skip_holidays")) session.commit() # Regenerate exceptions if any relevant field changed need_regen = ( prev_rule != event.recurrence_rule or prev_end != event.recurrence_end or prev_skip != bool(getattr(event, 'skip_holidays', False)) ) if need_regen: # Re-use helper from create route def regenerate_event_exceptions(ev: Event): from models.models import SchoolHoliday, EventException from dateutil.rrule import rrulestr from dateutil.tz import UTC # Clear existing exceptions session.query(EventException).filter_by(event_id=ev.id).delete() session.commit() if not (ev.skip_holidays and ev.recurrence_rule): return holidays = session.query(SchoolHoliday).all() dtstart = ev.start.astimezone(UTC) r = rrulestr(ev.recurrence_rule, dtstart=dtstart) window_start = dtstart window_end = ev.recurrence_end.astimezone(UTC) if ev.recurrence_end else dtstart.replace(year=dtstart.year + 1) holiday_dates = set() for h in holidays: hs = h.start_date he = h.end_date d = hs while d <= he: holiday_dates.add(d) d = d + timedelta(days=1) for occ_start in r.between(window_start, window_end, inc=True): occ_date = occ_start.date() if occ_date in holiday_dates: session.add(EventException(event_id=ev.id, exception_date=occ_date, is_skipped=True)) session.commit() regenerate_event_exceptions(event) event_id_return = event.id # <-- ID vor session.close() speichern! session.close() return jsonify({"success": True, "event_id": event_id_return})