feat: Adds markdown view for hugo rendering

This commit is contained in:
Vincent Mahnke 2025-11-12 20:53:22 +01:00
commit 95b97a7704
Signed by: ViMaSter
GPG key ID: 6D787326BA7D6469
3 changed files with 140 additions and 4 deletions

View file

@ -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
----------------------
@ -13,6 +20,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 `<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
^^^^^^^^^^^^^^^^^
@ -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

View file

@ -191,3 +191,101 @@ class CongressScheduleView(views.APIView):
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')
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')

View file

@ -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/<str:organizer>/<str:event>/schedule.md',
HackertoursMarkdownView.as_view(),
name='schedule-md',
),
]