pretix-congressschedule/pretix_congressschedule/api.py

318 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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__
class CongressScheduleView(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'<?xml version="1.0"?><error>Event not found</error>', status=404, content_type='application/xml')
if not ev.has_subevents:
return HttpResponse(
b'<?xml version="1.0"?><error>Event is not an event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series</error>',
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 <day> 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 <room> 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 <event> 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 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')