diff --git a/README.rst b/README.rst
index fa06bdc..83bac22 100644
--- a/README.rst
+++ b/README.rst
@@ -66,6 +66,12 @@ Development setup
Changelog
---------
+2.0.0
+~~~~~
+
+- Removes XML endpoint (breaking)
+- Replaces language section with "Hackertours Settings" section containing language of tour and links for English and German tour details
+
1.1.1
~~~~~
diff --git a/pretix_congressschedule/api.py b/pretix_congressschedule/api.py
index ebc1ba7..88d3d90 100644
--- a/pretix_congressschedule/api.py
+++ b/pretix_congressschedule/api.py
@@ -5,219 +5,13 @@ 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):
@@ -313,7 +107,7 @@ class CongressScheduleJSONView(views.APIView):
text = text.strip('-_')
return text or 'item'
- def _get_lang(subevent: SubEvent) -> str:
+ def _get_language(subevent: SubEvent) -> str:
if SubEventMetaValue is not None:
try:
v = (
@@ -333,6 +127,48 @@ class CongressScheduleJSONView(views.APIView):
return se_settings.get('congressschedule_language', 'deen') if se_settings is not None else 'deen'
except Exception:
return 'deen'
+
+ def _get_websiteDE(subevent: SubEvent) -> str:
+ if SubEventMetaValue is not None:
+ try:
+ v = (
+ SubEventMetaValue.objects
+ .filter(subevent=subevent, key='congressschedule_website_de')
+ .values_list('value', flat=True)
+ .first()
+ )
+ return (v or '').strip()
+ except Exception:
+ pass
+ md = getattr(subevent, 'meta_data', None) or {}
+ if isinstance(md, dict) and 'congressschedule_website_de' in md:
+ return (md.get('congressschedule_website_de') or '').strip()
+ se_settings = getattr(subevent, 'settings', None)
+ try:
+ return se_settings.get('congressschedule_website_de', '') if se_settings is not None else ''
+ except Exception:
+ return ''
+
+ def _get_websiteEN(subevent: SubEvent) -> str:
+ if SubEventMetaValue is not None:
+ try:
+ v = (
+ SubEventMetaValue.objects
+ .filter(subevent=subevent, key='congressschedule_website_en')
+ .values_list('value', flat=True)
+ .first()
+ )
+ return (v or '').strip()
+ except Exception:
+ pass
+ md = getattr(subevent, 'meta_data', None) or {}
+ if isinstance(md, dict) and 'congressschedule_website_en' in md:
+ return (md.get('congressschedule_website_en') or '').strip()
+ se_settings = getattr(subevent, 'settings', None)
+ try:
+ return se_settings.get('congressschedule_website_en', '') if se_settings is not None else ''
+ except Exception:
+ return ''
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]
@@ -369,7 +205,9 @@ class CongressScheduleJSONView(views.APIView):
if len(second) < 4:
second = f"{second}-{se.pk}"
- lang = _get_lang(se)
+ language = _get_language(se)
+ websiteDE = _get_websiteDE(se)
+ websiteEN = _get_websiteEN(se)
ev_obj = {
"id": se.pk,
@@ -384,13 +222,17 @@ class CongressScheduleJSONView(views.APIView):
"subtitle": "",
"track": "Hackertours",
"type": "Tour",
- "language": str(lang or "de, en"),
+ "language": str(language 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",
+ "url": str(websiteDE),
+ "title": title + " (DE)",
+ },
+ {
+ "url": str(websiteEN),
+ "title": title + " (DE)",
}
],
}
diff --git a/pretix_congressschedule/signals.py b/pretix_congressschedule/signals.py
index 24ed1e4..587ae8d 100644
--- a/pretix_congressschedule/signals.py
+++ b/pretix_congressschedule/signals.py
@@ -7,7 +7,7 @@ except Exception: # pragma: no cover - during docs build or without pretix
SubEventMetaValue = None
-class SubEventLanguageForm(forms.Form):
+class SubEventHackertoursForm(forms.Form):
language = forms.ChoiceField(
label=_("Language"),
required=False,
@@ -18,6 +18,16 @@ class SubEventLanguageForm(forms.Form):
('en', _("English")),
],
)
+ website_en = forms.URLField(
+ label=_("Website (English)"),
+ required=False,
+ help_text=_("Link to the English tour details on https://hackertours.hamburg.ccc.de/en/"),
+ )
+ website_de = forms.URLField(
+ label=_("Website (German)"),
+ required=False,
+ help_text=_("Link to the German tour details on https://hackertours.hamburg.ccc.de/de/"),
+ )
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
@@ -25,45 +35,83 @@ class SubEventLanguageForm(forms.Form):
super().__init__(*args, **kwargs)
# Pre-fill from subevent meta if available
if self.subevent:
- val = (
+ languageValue = (
SubEventMetaValue.objects
.filter(subevent=self.subevent, property__name='congressschedule_language')
.values_list('value', flat=True)
.first()
)
- self.fields['language'].initial = val or ''
+ self.fields['language'].initial = languageValue or ''
+ websiteENValue = (
+ SubEventMetaValue.objects
+ .filter(subevent=self.subevent, property__name='congressschedule_website_en')
+ .values_list('value', flat=True)
+ .first()
+ )
+ self.fields['website_en'].initial = websiteENValue or ''
+ websiteDEValue = (
+ SubEventMetaValue.objects
+ .filter(subevent=self.subevent, property__name='congressschedule_website_de')
+ .values_list('value', flat=True)
+ .first()
+ )
+ self.fields['website_de'].initial = websiteDEValue or ''
elif self.subevent and hasattr(self.subevent, 'settings'):
# Fallback (older pretix): might be event-wide, keep as last resort
self.fields['language'].initial = "deen"
+ self.fields['website_en'].initial = ""
+ self.fields['website_de'].initial = ""
@property
def title(self):
- return _("Language")
+ return _("Hackertours Settings")
def save(self):
if not self.subevent:
return
- val = (self.cleaned_data.get('language') or '').strip() or 'deen'
+ languageValue = (self.cleaned_data.get('language') or '').strip() or 'deen'
+ websiteENValue = (self.cleaned_data.get('website_en') or '').strip()
+ websiteDEValue = (self.cleaned_data.get('website_de') or '').strip()
# Persist as real subevent meta value so it's scoped per subevent
from pretix.base.models import EventMetaProperty
- property_obj, _ = EventMetaProperty.objects.get_or_create(
+ language_property_obj, _ = EventMetaProperty.objects.get_or_create(
name='congressschedule_language',
defaults={'default': '', 'organizer': self.event.organizer}
)
SubEventMetaValue.objects.update_or_create(
subevent=self.subevent,
- property=property_obj,
- defaults={'value': val},
+ property=language_property_obj,
+ defaults={'value': languageValue},
+ )
+
+ website_en_property_obj, _ = EventMetaProperty.objects.get_or_create(
+ name='congressschedule_website_en',
+ defaults={'default': '', 'organizer': self.event.organizer}
+ )
+ SubEventMetaValue.objects.update_or_create(
+ subevent=self.subevent,
+ property=website_en_property_obj,
+ defaults={'value': websiteENValue},
+ )
+
+ website_de_property_obj, _ = EventMetaProperty.objects.get_or_create(
+ name='congressschedule_website_de',
+ defaults={'default': '', 'organizer': self.event.organizer}
+ )
+ SubEventMetaValue.objects.update_or_create(
+ subevent=self.subevent,
+ property=website_de_property_obj,
+ defaults={'value': websiteDEValue},
)
-def subevent_forms(sender, request, subevent, **kwargs):
+def subevent_hackertours_forms(sender, request, subevent, **kwargs):
# Provide our additional subevent form
import logging
logger = logging.getLogger(__name__)
logger.debug("Providing congressschedule subevent form for event %s, subevent %s", sender.slug, getattr(subevent, 'name', 'no-subevent'))
- form = SubEventLanguageForm(
+ form = SubEventHackertoursForm(
data=request.POST if request.method == 'POST' else None,
event=sender,
subevent=subevent,
@@ -73,11 +121,10 @@ def subevent_forms(sender, request, subevent, **kwargs):
def connect_signals():
- # Connect to pretix.control.signals.subevent_forms at import time
try:
from pretix.control import signals as control_signals
- control_signals.subevent_forms.connect(subevent_forms, dispatch_uid='pretix_congressschedule_subevent_language')
+ control_signals.subevent_forms.connect(subevent_hackertours_forms, dispatch_uid='pretix_congressschedule_subevent_hackertours')
except Exception:
# Pretix not fully loaded in some contexts (e.g., docs build)
pass
diff --git a/pretix_congressschedule/urls.py b/pretix_congressschedule/urls.py
index d10b71a..8953ec0 100644
--- a/pretix_congressschedule/urls.py
+++ b/pretix_congressschedule/urls.py
@@ -1,12 +1,7 @@
from django.urls import path
-from .api import CongressScheduleXMLView, CongressScheduleJSONView, HackertoursMarkdownView
+from .api import CongressScheduleJSONView, HackertoursMarkdownView
urlpatterns = [
- path(
- 'api/v1/event///schedule.xml',
- CongressScheduleXMLView.as_view(),
- name='schedule-xml',
- ),
path(
'api/v1/event///schedule.json',
CongressScheduleJSONView.as_view(),