From 20f30f831e48517417d7b6db4cff92bc18d4bedd Mon Sep 17 00:00:00 2001 From: Vincent Mahnke Date: Tue, 11 Nov 2025 14:57:53 +0100 Subject: [PATCH 1/4] fix: Updates description --- pretix_congressschedule/apps.py | 2 +- pretix_congressschedule/locale/django.pot | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pretix_congressschedule/apps.py b/pretix_congressschedule/apps.py index b756542..b09c51d 100644 --- a/pretix_congressschedule/apps.py +++ b/pretix_congressschedule/apps.py @@ -12,7 +12,7 @@ class PassbookApp(AppConfig): class PretixPluginMeta: name = gettext_lazy("Congress Schedule") author = "Vincent Mahnke" - description = gettext_lazy("Provides passbook tickets for pretix") + description = gettext_lazy("Provides c3voc-schedule compatible XML files") category = "API" visible = True featured = True diff --git a/pretix_congressschedule/locale/django.pot b/pretix_congressschedule/locale/django.pot index a174392..592aa9e 100644 --- a/pretix_congressschedule/locale/django.pot +++ b/pretix_congressschedule/locale/django.pot @@ -22,5 +22,5 @@ msgid "Congress Schedule" msgstr "" #: pretix_congressschedule/__init__.py:13 -msgid "Provides passbook tickets for pretix" +msgid "Provides c3voc-schedule compatible XML files" msgstr "" \ No newline at end of file From 95b97a7704093ea20d059c8baa6d21e1dad2620a Mon Sep 17 00:00:00 2001 From: Vincent Mahnke Date: Wed, 12 Nov 2025 20:53:22 +0100 Subject: [PATCH 2/4] feat: Adds markdown view for hugo rendering --- README.rst | 37 +++++++++++- pretix_congressschedule/api.py | 100 +++++++++++++++++++++++++++++++- pretix_congressschedule/urls.py | 7 ++- 3 files changed, 140 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index cd7f8f2..2dec55b 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,14 @@ pretix-congressschedule ======================= -This is a plugin for `pretix`_. It generates a `c3voc-schema`_ compatible `schedule.xml` endpoint for events. +This is a plugin for `pretix`_. It generates a `c3voc-schema`_ compatible `schedule.xml` endpoint and hackertours-compatible markdown table for event-series. +To access the endpoints without logging in, generate an `API token`_ first. + +Subevent language field +----------------------- + +To determine a subevent's language, this plugin adds a language dropdown selector. +Default: `deen` (multi-lingual of German and English) Accessing schedule.xml ---------------------- @@ -12,6 +19,32 @@ Accessing schedule.xml 3. Receive either a 200 status code with an XML document adhering to `schedule.xml.xsd`_ or a 400 error code with additional information inside `` + +Using schedule.md +---------------------- + +1. Create an `event-series`_ in pretix; a singular event or non-event shop will not work, as products won't have required start and end times associated with them + +2. Visit `/api/v1/event/{organizationSlug}/{eventSlug}/schedule.md` and replace `{organizationSlug}` and `{eventSlug}` with the respective slugs + +3. Receive either a 200 status code with a Markdown document containing a table used for hackertours.hamburg.ccc.de or a 400 error code with additional information inside `` + +4. To embed this into Hugo, use the following syntax: + +```hugo +{{ $url := "https://{prefixInstanceRoot}/api/v1/event/{organizationSlug}/{eventSlug}/schedule.md" }} +{{ $opts := dict + "headers" (dict "Authorization" "Token 6r5waszrj1qbdwqbewbmmk7h46ilocmyfh3e2gxqa9oj52vijmzo1dppk39t3hkl") +}} +{{ with try (resources.GetRemote $url $opts) }} + {{ with .Err }} + {{ errorf "%s" . }} + {{ else with .Value }} + {{ .Content | safeHTML }} + {{ end }} +{{ end }} +``` + Development setup ^^^^^^^^^^^^^^^^^ @@ -29,7 +62,6 @@ Development setup 6. Restart your local pretix server. You can now use the plugin from this repository for your events by enabling it in the 'plugins' tab in the settings. - License ------- @@ -41,6 +73,7 @@ Released under the terms of the Apache License 2.0 .. _pretix: https://github.com/pretix/pretix .. _pretix development setup: https://docs.pretix.eu/en/latest/development/setup.html +.. _API token: https://docs.pretix.eu/dev/api/tokenauth.html#obtaining-an-api-token .. _c3voc-schema: https://c3voc.de/wiki/schedule#schedule_xml .. _schedule.xml.xsd: https://c3voc.de/schedule/schema.xsd .. _event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series diff --git a/pretix_congressschedule/api.py b/pretix_congressschedule/api.py index 5f457cf..73d533c 100644 --- a/pretix_congressschedule/api.py +++ b/pretix_congressschedule/api.py @@ -190,4 +190,102 @@ class CongressScheduleView(views.APIView): # 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') \ No newline at end of file + 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') + try: + se_settings = getattr(se, 'settings', None) + lang = se_settings.get('congressschedule_language', 'de') if se_settings is not None else 'none' + except Exception: + lang = 'none' + 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') diff --git a/pretix_congressschedule/urls.py b/pretix_congressschedule/urls.py index 4f9c59d..fc50cad 100644 --- a/pretix_congressschedule/urls.py +++ b/pretix_congressschedule/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .api import CongressScheduleView +from .api import CongressScheduleView, HackertoursMarkdownView urlpatterns = [ path( @@ -7,4 +7,9 @@ urlpatterns = [ CongressScheduleView.as_view(), name='schedule-xml', ), + path( + 'api/v1/event///schedule.md', + HackertoursMarkdownView.as_view(), + name='schedule-md', + ), ] \ No newline at end of file From 5a349822d79c4b4fd1ad12d654ee2ff43ae220c6 Mon Sep 17 00:00:00 2001 From: Vincent Mahnke Date: Wed, 12 Nov 2025 13:41:30 +0100 Subject: [PATCH 3/4] feat: Adds language dropdown to subevent --- README.rst | 15 +++++ pretix_congressschedule/__init__.py | 2 +- pretix_congressschedule/api.py | 45 ++++++++++++--- pretix_congressschedule/signals.py | 87 +++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 2dec55b..599b841 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,21 @@ Development setup 6. Restart your local pretix server. You can now use the plugin from this repository for your events by enabling it in the 'plugins' tab in the settings. + +Changelog +--------- + +1.1.0 +~~~~~ + +- Add subevent-level "Language" field and use it to emit ```` per subevent (defaults to ``none``). + +1.0.0 +~~~~~ + +- Initial release + + License ------- diff --git a/pretix_congressschedule/__init__.py b/pretix_congressschedule/__init__.py index 5becc17..6849410 100644 --- a/pretix_congressschedule/__init__.py +++ b/pretix_congressschedule/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/pretix_congressschedule/api.py b/pretix_congressschedule/api.py index 73d533c..eff2d24 100644 --- a/pretix_congressschedule/api.py +++ b/pretix_congressschedule/api.py @@ -1,6 +1,10 @@ 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 @@ -182,10 +186,31 @@ class CongressScheduleView(views.APIView): # track – use room name as a simple track assignment ET.SubElement(ev_el, 'track').text = slugify(room_name) or 'general' - # Optional elements: keep minimal but include language if available - lang = getattr(ev.settings, 'locale', None) - if lang: - ET.SubElement(ev_el, 'language').text = str(lang) + # 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 'none').strip() or 'none' + 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 'none') + se_settings = getattr(subevent, 'settings', None) + try: + return se_settings.get('congressschedule_language', 'none') if se_settings is not None else 'none' + except Exception: + return 'none' + + lang = _get_lang(se) + ET.SubElement(ev_el, 'language').text = str(lang or 'none') # Leave optional complex children (persons, recording, links, attachments) empty for now @@ -257,11 +282,13 @@ class HackertoursMarkdownView(views.APIView): title = _localize(se.name) tmin = to_minutes(se.date_from) hhmm = se.date_from.strftime('%H:%M') - try: - se_settings = getattr(se, 'settings', None) - lang = se_settings.get('congressschedule_language', 'de') if se_settings is not None else 'none' - except Exception: - lang = 'none' + 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) diff --git a/pretix_congressschedule/signals.py b/pretix_congressschedule/signals.py index e69de29..dd5c0e5 100644 --- a/pretix_congressschedule/signals.py +++ b/pretix_congressschedule/signals.py @@ -0,0 +1,87 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ +try: + # Available in pretix runtime + from pretix.base.models import SubEventMetaValue +except Exception: # pragma: no cover - during docs build or without pretix + SubEventMetaValue = None + + +class SubEventLanguageForm(forms.Form): + language = forms.ChoiceField( + label=_("Language"), + required=False, + help_text=_("Select the language for this tour."), + choices=[ + ('deen', _("Bilingual")), + ('de', _("German")), + ('en', _("English")), + ], + ) + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event') + self.subevent = kwargs.pop('subevent', None) + super().__init__(*args, **kwargs) + # Pre-fill from subevent meta if available + if self.subevent: + val = ( + SubEventMetaValue.objects + .filter(subevent=self.subevent, property__name='congressschedule_language') + .values_list('value', flat=True) + .first() + ) + self.fields['language'].initial = val 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" + + @property + def title(self): + return _("Language") + + def save(self): + if not self.subevent: + return + val = (self.cleaned_data.get('language') or '').strip() or 'none' + # 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( + name='congressschedule_language', + defaults={'default': '', 'organizer': self.event.organizer} + ) + SubEventMetaValue.objects.update_or_create( + subevent=self.subevent, + property=property_obj, + defaults={'value': val}, + ) + + +def subevent_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( + data=request.POST if request.method == 'POST' else None, + event=sender, + subevent=subevent, + prefix='congressschedule', + ) + return form + + +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') + except Exception: + # Pretix not fully loaded in some contexts (e.g., docs build) + pass + + +# Connect immediately when module is imported via AppConfig.ready() +connect_signals() From be028b841aa0084ee7a8e1a14f61d7b572407ab4 Mon Sep 17 00:00:00 2001 From: Vincent Mahnke Date: Thu, 13 Nov 2025 08:16:07 +0100 Subject: [PATCH 4/4] fix: Outputs `deen` as default across XML and MD API --- README.rst | 7 ++++++- pretix_congressschedule/__init__.py | 2 +- pretix_congressschedule/api.py | 10 +++++----- pretix_congressschedule/signals.py | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 599b841..fa06bdc 100644 --- a/README.rst +++ b/README.rst @@ -66,10 +66,15 @@ Development setup Changelog --------- +1.1.1 +~~~~~ + +- ```` defaults to ``deen`` across XML and Markdown outputs. + 1.1.0 ~~~~~ -- Add subevent-level "Language" field and use it to emit ```` per subevent (defaults to ``none``). +- Add subevent-level "Language" field and use it to emit ```` per subevent (defaults to ``deen``). 1.0.0 ~~~~~ diff --git a/pretix_congressschedule/__init__.py b/pretix_congressschedule/__init__.py index 6849410..a82b376 100644 --- a/pretix_congressschedule/__init__.py +++ b/pretix_congressschedule/__init__.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" diff --git a/pretix_congressschedule/api.py b/pretix_congressschedule/api.py index eff2d24..178d827 100644 --- a/pretix_congressschedule/api.py +++ b/pretix_congressschedule/api.py @@ -196,21 +196,21 @@ class CongressScheduleView(views.APIView): .values_list('value', flat=True) .first() ) - return (v or 'none').strip() or 'none' + 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 'none') + return (md.get('congressschedule_language') or 'deen') se_settings = getattr(subevent, 'settings', None) try: - return se_settings.get('congressschedule_language', 'none') if se_settings is not None else 'none' + return se_settings.get('congressschedule_language', 'deen') if se_settings is not None else 'deen' except Exception: - return 'none' + return 'deen' lang = _get_lang(se) - ET.SubElement(ev_el, 'language').text = str(lang or 'none') + ET.SubElement(ev_el, 'language').text = str(lang or 'deen') # Leave optional complex children (persons, recording, links, attachments) empty for now diff --git a/pretix_congressschedule/signals.py b/pretix_congressschedule/signals.py index dd5c0e5..24ed1e4 100644 --- a/pretix_congressschedule/signals.py +++ b/pretix_congressschedule/signals.py @@ -43,7 +43,7 @@ class SubEventLanguageForm(forms.Form): def save(self): if not self.subevent: return - val = (self.cleaned_data.get('language') or '').strip() or 'none' + val = (self.cleaned_data.get('language') or '').strip() or 'deen' # Persist as real subevent meta value so it's scoped per subevent from pretix.base.models import EventMetaProperty