Compare commits
No commits in common. "be028b841aa0084ee7a8e1a14f61d7b572407ab4" and "cfa40c6918c21374f015a3e5ac53cf6a8a3c5e9c" have entirely different histories.
be028b841a
...
cfa40c6918
7 changed files with 10 additions and 280 deletions
55
README.rst
55
README.rst
|
|
@ -1,14 +1,7 @@
|
||||||
pretix-congressschedule
|
pretix-congressschedule
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
This is a plugin for `pretix`_. It generates a `c3voc-schema`_ compatible `schedule.xml` endpoint and hackertours-compatible markdown table for event-series.
|
This is a plugin for `pretix`_. It generates a `c3voc-schema`_ compatible `schedule.xml` endpoint for events.
|
||||||
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
|
Accessing schedule.xml
|
||||||
----------------------
|
----------------------
|
||||||
|
|
@ -19,32 +12,6 @@ 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 `<error>`
|
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 `<error>`
|
||||||
|
|
||||||
|
|
||||||
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 `<error>`
|
|
||||||
|
|
||||||
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
|
Development setup
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
@ -63,25 +30,6 @@ Development setup
|
||||||
the 'plugins' tab in the settings.
|
the 'plugins' tab in the settings.
|
||||||
|
|
||||||
|
|
||||||
Changelog
|
|
||||||
---------
|
|
||||||
|
|
||||||
1.1.1
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
- ``<language>`` defaults to ``deen`` across XML and Markdown outputs.
|
|
||||||
|
|
||||||
1.1.0
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
- Add subevent-level "Language" field and use it to emit ``<language>`` per subevent (defaults to ``deen``).
|
|
||||||
|
|
||||||
1.0.0
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
- Initial release
|
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
@ -93,7 +41,6 @@ Released under the terms of the Apache License 2.0
|
||||||
|
|
||||||
.. _pretix: https://github.com/pretix/pretix
|
.. _pretix: https://github.com/pretix/pretix
|
||||||
.. _pretix development setup: https://docs.pretix.eu/en/latest/development/setup.html
|
.. _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
|
.. _c3voc-schema: https://c3voc.de/wiki/schedule#schedule_xml
|
||||||
.. _schedule.xml.xsd: https://c3voc.de/schedule/schema.xsd
|
.. _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
|
.. _event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "1.1.1"
|
__version__ = "1.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from pretix.base.models import Event, SubEvent
|
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
|
import xml.etree.ElementTree as ET
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
@ -186,133 +182,12 @@ class CongressScheduleView(views.APIView):
|
||||||
# track – use room name as a simple track assignment
|
# track – use room name as a simple track assignment
|
||||||
ET.SubElement(ev_el, 'track').text = slugify(room_name) or 'general'
|
ET.SubElement(ev_el, 'track').text = slugify(room_name) or 'general'
|
||||||
|
|
||||||
# Optional elements: language – per subevent via SubEventMetaValue
|
# Optional elements: keep minimal but include language if available
|
||||||
def _get_lang(subevent: SubEvent) -> str:
|
lang = getattr(ev.settings, 'locale', None)
|
||||||
if SubEventMetaValue is not None:
|
if lang:
|
||||||
try:
|
ET.SubElement(ev_el, 'language').text = str(lang)
|
||||||
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
|
# Leave optional complex children (persons, recording, links, attachments) empty for now
|
||||||
|
|
||||||
xml_bytes = ET.tostring(root, encoding='utf-8', xml_declaration=True)
|
xml_bytes = ET.tostring(root, encoding='utf-8', xml_declaration=True)
|
||||||
return HttpResponse(xml_bytes, content_type='application/xml')
|
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')
|
|
||||||
|
|
@ -12,7 +12,7 @@ class PassbookApp(AppConfig):
|
||||||
class PretixPluginMeta:
|
class PretixPluginMeta:
|
||||||
name = gettext_lazy("Congress Schedule")
|
name = gettext_lazy("Congress Schedule")
|
||||||
author = "Vincent Mahnke"
|
author = "Vincent Mahnke"
|
||||||
description = gettext_lazy("Provides c3voc-schedule compatible XML files")
|
description = gettext_lazy("Provides passbook tickets for pretix")
|
||||||
category = "API"
|
category = "API"
|
||||||
visible = True
|
visible = True
|
||||||
featured = True
|
featured = True
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,5 @@ msgid "Congress Schedule"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix_congressschedule/__init__.py:13
|
#: pretix_congressschedule/__init__.py:13
|
||||||
msgid "Provides c3voc-schedule compatible XML files"
|
msgid "Provides passbook tickets for pretix"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
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 'deen'
|
|
||||||
# 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()
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .api import CongressScheduleView, HackertoursMarkdownView
|
from .api import CongressScheduleView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
|
|
@ -7,9 +7,4 @@ urlpatterns = [
|
||||||
CongressScheduleView.as_view(),
|
CongressScheduleView.as_view(),
|
||||||
name='schedule-xml',
|
name='schedule-xml',
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
'api/v1/event/<str:organizer>/<str:event>/schedule.md',
|
|
||||||
HackertoursMarkdownView.as_view(),
|
|
||||||
name='schedule-md',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue