from django.http import HttpResponse from rest_framework import views from pretix.base.models import Event, SubEvent try: from pretix.base.models import SubEventMetaValue except Exception: # pragma: no cover SubEventMetaValue = None import xml.etree.ElementTree as ET from collections import defaultdict from datetime import timedelta import uuid import re from . import __version__ import json class CongressScheduleXMLView(views.APIView): def get(self, request, organizer, event, *args, **kwargs): try: ev = Event.objects.get(organizer__slug=organizer, slug=event) except Event.DoesNotExist: return HttpResponse(b'Event not found', status=404, content_type='application/xml') if not ev.has_subevents: return HttpResponse( b'Event is not an event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series', status=400, content_type='application/xml' ) subs = SubEvent.objects.filter(event=ev).order_by('date_from') root = ET.Element('schedule') gen = ET.SubElement(root, 'generator') gen.set('name', 'pretix-congressschedule') gen.set('version', __version__) try: feed_url = request.build_absolute_uri() ET.SubElement(root, 'url').text = feed_url except Exception: pass # Version string – keep simple and stable per event ET.SubElement(root, 'version').text = f"{ev.slug}-v1" conf = ET.SubElement(root, 'conference') conf_title = ev.name.localize(ev.settings.locale) if hasattr(ev.name, 'localize') else str(ev.name) ET.SubElement(conf, 'title').text = conf_title or str(ev.slug) acronym = f"{organizer}_{event}".lower() ET.SubElement(conf, 'acronym').text = acronym # start/end/days based on subevents if available, else fall back to event all_starts = [se.date_from for se in subs if se.date_from] all_ends = [se.date_to for se in subs if se.date_to] if all_starts: ET.SubElement(conf, 'start').text = min(all_starts).isoformat() if all_ends: ET.SubElement(conf, 'end').text = max(all_ends).isoformat() # days count – unique calendar days from subevents unique_days = sorted({(se.date_from.date() if se.date_from else None) for se in subs} - {None}) if unique_days: ET.SubElement(conf, 'days').text = str(len(unique_days)) # time zone name – try Event.timezone or settings tz_name = getattr(ev, 'timezone', None) or getattr(ev.settings, 'timezone', None) if tz_name: tz_text = tz_name if isinstance(tz_name, str) else str(tz_name) ET.SubElement(conf, 'time_zone_name').text = tz_text # Group subevents into days and rooms # days: {date -> {room_name -> [subevents]}} days: dict = defaultdict(lambda: defaultdict(list)) def get_room_name(se): # Try SubEvent.location if present, else fallback to `Main` loc = getattr(se, 'location', None) if hasattr(loc, 'localize'): try: txt = loc.localize(ev.settings.locale) except Exception: txt = str(loc) else: txt = str(loc) if loc else '' return (txt or 'Main').strip() or 'Main' for se in subs: if not se.date_from: # Skip entries without a start continue day_key = se.date_from.date() room = get_room_name(se) days[day_key][room].append(se) # Emit elements in chronological order for day_index, (day_date, rooms) in enumerate(sorted(days.items()), start=1): # Compute day start/end from all events this day starts = [se.date_from for r in rooms.values() for se in r if se.date_from] ends = [se.date_to for r in rooms.values() for se in r if se.date_to] day_start = min(starts) if starts else None # If end is missing for any, approximate using +0 duration => start if ends: day_end = max(ends) else: day_end = (day_start + timedelta(minutes=0)) if day_start else None day_el = ET.SubElement(root, 'day') if day_date: day_el.set('date', day_date.isoformat()) if day_start: day_el.set('start', day_start.isoformat()) if day_end: day_el.set('end', day_end.isoformat()) day_el.set('index', str(day_index)) # Emit containers for room_name, events_in_room in sorted(rooms.items(), key=lambda x: x[0].lower()): room_el = ET.SubElement(day_el, 'room') room_el.set('name', room_name) # Optional guid on room – stable UUID5 based on names room_el.set('guid', str(uuid.uuid5(uuid.NAMESPACE_DNS, f"room:{organizer}:{event}:{room_name}"))) # Emit each in chronological order within the room for se in sorted(events_in_room, key=lambda s: s.date_from or 0): ev_el = ET.SubElement(room_el, 'event') ev_el.set('id', str(se.pk)) ev_el.set('guid', str(uuid.uuid5(uuid.NAMESPACE_DNS, f"subevent:{ev.pk}:{se.pk}"))) # Helper: localize strings def _localize(val): if hasattr(val, 'localize'): try: return val.localize(ev.settings.locale) except Exception: return str(val) return str(val) if val is not None else '' # Required children according to schema ET.SubElement(ev_el, 'room').text = room_name title = _localize(se.name) ET.SubElement(ev_el, 'title').text = title ET.SubElement(ev_el, 'subtitle').text = '' ET.SubElement(ev_el, 'type').text = 'subevent' # date (full datetime with TZ) if se.date_from: ET.SubElement(ev_el, 'date').text = se.date_from.isoformat() # start (HH:MM or HH:MM:SS) if se.date_from: ET.SubElement(ev_el, 'start').text = se.date_from.strftime('%H:%M') # duration from date_to - date_from dur_txt = '00:00' if se.date_from and se.date_to and se.date_to >= se.date_from: delta: timedelta = se.date_to - se.date_from total_seconds = int(delta.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 seconds = total_seconds % 60 # prefer HH:MM if no seconds, else HH:MM:SS if seconds == 0: dur_txt = f"{hours:02d}:{minutes:02d}" else: dur_txt = f"{hours:02d}:{minutes:02d}:{seconds:02d}" ET.SubElement(ev_el, 'duration').text = dur_txt ET.SubElement(ev_el, 'abstract').text = '' # slug (pattern: "[a-z0-9_]{4,}-[a-z0-9\-_]{4,}") def slugify(text: str) -> str: text = (text or '').lower() text = re.sub(r'\s+', '-', text) text = re.sub(r'[^a-z0-9\-_]', '', text) text = text.strip('-_') return text or 'item' base = f"{organizer}_{event}".lower() second = slugify(title) if len(second) < 4: second = f"{second}-{se.pk}" ET.SubElement(ev_el, 'slug').text = f"{base}-{second}" # track – use room name as a simple track assignment ET.SubElement(ev_el, 'track').text = slugify(room_name) or 'general' # Optional elements: language – per subevent via SubEventMetaValue def _get_lang(subevent: SubEvent) -> str: if SubEventMetaValue is not None: try: v = ( SubEventMetaValue.objects .filter(subevent=subevent, key='congressschedule_language') .values_list('value', flat=True) .first() ) return (v or 'deen').strip() or 'deen' except Exception: pass # Fallbacks for environments without pretix DB access md = getattr(subevent, 'meta_data', None) or {} if isinstance(md, dict) and 'congressschedule_language' in md: return (md.get('congressschedule_language') or 'deen') se_settings = getattr(subevent, 'settings', None) try: return se_settings.get('congressschedule_language', 'deen') if se_settings is not None else 'deen' except Exception: return 'deen' lang = _get_lang(se) ET.SubElement(ev_el, 'language').text = str(lang or 'deen') # Leave optional complex children (persons, recording, links, attachments) empty for now xml_bytes = ET.tostring(root, encoding='utf-8', xml_declaration=True) return HttpResponse(xml_bytes, content_type='application/xml') class CongressScheduleJSONView(views.APIView): def get(self, request, organizer, event): try: ev = Event.objects.get(organizer__slug=organizer, slug=event) except Event.DoesNotExist: return HttpResponse( json.dumps({"error": "Event not found"}), status=404, content_type='application/json; charset=utf-8' ) if not ev.has_subevents: return HttpResponse( json.dumps({"error": "Event is not an event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series"}), status=400, content_type='application/json; charset=utf-8' ) subs = SubEvent.objects.filter(event=ev).order_by('date_from') schedule = { "generator": {"name": "voc/schedule/hackertours", "version": __version__}, "version": f"{ev.slug}-v1", "conference": { "acronym": f"{organizer}_{event}".lower(), "title": (ev.name.localize(ev.settings.locale) if hasattr(ev.name, 'localize') else str(ev.name)) or str(ev.slug), "start": None, "end": None, "daysCount": None, "timeslot_duration": "00:15", "time_zone_name": "Europe/Berlin", }, "days": [] } all_starts = [se.date_from for se in subs if se.date_from] all_ends = [se.date_to for se in subs if se.date_to] if all_starts: schedule["conference"]["start"] = min(all_starts).isoformat() if all_ends: schedule["conference"]["end"] = max(all_ends).isoformat() unique_days = sorted({(se.date_from.date() if se.date_from else None) for se in subs} - {None}) if unique_days: schedule["conference"]["days"] = len(unique_days) tz_name = getattr(ev, 'timezone', None) or getattr(ev.settings, 'timezone', None) if tz_name: schedule["conference"]["time_zone_name"] = tz_name if isinstance(tz_name, str) else str(tz_name) days = defaultdict(lambda: defaultdict(list)) def get_room_name(se): loc = getattr(se, 'location', None) if hasattr(loc, 'localize'): try: txt = loc.localize(ev.settings.locale) except Exception: txt = str(loc) else: txt = str(loc) if loc else '' return (txt or 'Main').strip() or 'Main' for se in subs: if not se.date_from: continue day_key = se.date_from.date() room = get_room_name(se) days[day_key][room].append(se) def _localize(val): if hasattr(val, 'localize'): try: return val.localize(ev.settings.locale) except Exception: return str(val) return str(val) if val is not None else '' def slugify(text: str) -> str: text = (text or '').lower() text = re.sub(r'\s+', '-', text) text = re.sub(r'[^a-z0-9\-_]', '', text) text = text.strip('-_') return text or 'item' def _get_lang(subevent: SubEvent) -> str: if SubEventMetaValue is not None: try: v = ( SubEventMetaValue.objects .filter(subevent=subevent, key='congressschedule_language') .values_list('value', flat=True) .first() ) return (v or 'deen').strip() or 'deen' except Exception: pass md = getattr(subevent, 'meta_data', None) or {} if isinstance(md, dict) and 'congressschedule_language' in md: return (md.get('congressschedule_language') or 'deen') se_settings = getattr(subevent, 'settings', None) try: return se_settings.get('congressschedule_language', 'deen') if se_settings is not None else 'deen' except Exception: return 'deen' for day_index, (day_date, rooms) in enumerate(sorted(days.items()), start=1): starts = [se.date_from for r in rooms.values() for se in r if se.date_from] ends = [se.date_to for r in rooms.values() for se in r if se.date_to] day_start = min(starts).isoformat() if starts else None day_end = (max(ends).isoformat() if ends else (min(starts).isoformat() if starts else None)) day_obj = { "index": day_index, "date": day_date.isoformat() if day_date else None, "day_start": day_start, "day_end": day_end, "rooms": [] } day_obj["rooms"] = {} for room_name, events_in_room in sorted(rooms.items(), key=lambda x: x[0].lower()): room_events = [] for se in sorted(events_in_room, key=lambda s: s.date_from or 0): dur_txt = '00:00' if se.date_from and se.date_to and se.date_to >= se.date_from: delta = se.date_to - se.date_from total_seconds = int(delta.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 seconds = total_seconds % 60 dur_txt = f"{hours:02d}:{minutes:02d}" if seconds == 0 else f"{hours:02d}:{minutes:02d}:{seconds:02d}" title = _localize(se.name) base = f"{organizer}_{event}".lower() second = slugify(title) if len(second) < 4: second = f"{second}-{se.pk}" lang = _get_lang(se) ev_obj = { "id": se.pk, "guid": str(uuid.uuid5(uuid.NAMESPACE_DNS, f"subevent:{ev.pk}:{se.pk}")), "date": se.date_from.isoformat() if se.date_from else None, "start": se.date_from.strftime('%H:%M') if se.date_from else None, "duration": dur_txt, "room": room_name, "slug": f"{base}-{second}", "url": "TODO", "title": title, "subtitle": "", "track": "Hackertours", "type": "Tour", "language": str(lang or "de, en"), "abstract": se.frontpage_text.localize(ev.settings.locale) if hasattr(se.frontpage_text, 'localize') else str(se.frontpage_text) if se.frontpage_text else "", "persons": [], "links": [ { "url": "TODO", "title": "TODO", } ], } room_events.append(ev_obj) day_obj["rooms"][room_name] = room_events schedule["days"].append(day_obj) return HttpResponse( json.dumps(schedule, ensure_ascii=False), content_type='application/json; charset=utf-8' ) class HackertoursMarkdownView(views.APIView): def get(self, request, organizer, event, *args, **kwargs): try: ev = Event.objects.get(organizer__slug=organizer, slug=event) except Event.DoesNotExist: return HttpResponse("Event not found", status=404, content_type='text/plain; charset=utf-8') if not ev.has_subevents: return HttpResponse( 'Event is not an event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series', status=400, content_type='text/plain; charset=utf-8' ) subs = SubEvent.objects.filter(event=ev).order_by('date_from') # Helper: localize strings def _localize(val): if hasattr(val, 'localize'): try: return val.localize(ev.settings.locale) except Exception: return str(val) return str(val) if val is not None else '' # Slugify for links def slugify(text: str) -> str: text = (text or '').lower() text = re.sub(r'\s+', '-', text) text = re.sub(r'[^a-z0-9\-_]', '', text) text = text.strip('-_') return text or 'item' # Build day -> events map and gather all start times days = defaultdict(list) # {date -> [SubEvent]} start_dts = [] for se in subs: if not se.date_from: continue days[se.date_from.date()].append(se) start_dts.append(se.date_from) if not start_dts: return HttpResponse("No subevents found", content_type='text/plain; charset=utf-8') unique_days = sorted(days.keys()) # Build time slots (minutes since midnight) def to_minutes(dt): return dt.hour * 60 + dt.minute event_minutes = {to_minutes(dt) for dt in start_dts} min_hour = min(dt.hour for dt in start_dts) max_hour = max(dt.hour for dt in start_dts) hour_minutes = {h * 60 for h in range(min_hour, max_hour + 1)} all_minutes = sorted(event_minutes | hour_minutes) # For each day, map minute -> list of markdown items day_slots = {d: defaultdict(list) for d in unique_days} for d in unique_days: for se in days[d]: title = _localize(se.name) tmin = to_minutes(se.date_from) hhmm = se.date_from.strftime('%H:%M') v = ( SubEventMetaValue.objects .filter(subevent=se, property__name='congressschedule_language') .values_list('value', flat=True) .first() ) lang = (v or 'deen').strip() or 'deen' link = f"./{slugify(title)}/" md_item = f"{hhmm} [{title}]({link}) {{< lang {lang} >}}" day_slots[d][tmin].append(md_item) # Build markdown table lines = [] # Header header = ["Zeit"] + [f"Tag {i} ({d.strftime('%d.%m.')})" for i, d in enumerate(unique_days, start=1)] lines.append("| " + " | ".join(header) + " |") lines.append("|" + "|".join(["---"] * len(header)) + "|") # Rows for m in all_minutes: is_full_hour = (m % 60) == 0 zeit_label = f"{m // 60:02d}:{m % 60:02d}" if is_full_hour else "" row = [zeit_label] for d in unique_days: items = day_slots[d].get(m, []) if items: cell = " ".join(items) else: cell = "-" if is_full_hour else "" row.append(cell) lines.append("| " + " | ".join(row) + " |") md = "\n".join(lines) + "\n" return HttpResponse(md.encode('utf-8'), content_type='text/markdown; charset=utf-8')