From b6eaaa5f188b01d43639029747c7adb9e67272af Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 22 Feb 2020 15:09:12 +0100 Subject: [PATCH] Frab xml export (#459) * first stab at a frab xml generator * delete unused xsd file * remove camp title from location name * add uuid field to eventinstance and use it in the frab xml --- .../migrations/0079_eventinstance_uuid.py | 25 +++ src/program/models.py | 9 +- src/program/urls.py | 2 + src/program/views.py | 105 ++++++++++- src/program/xsd/schedule.xml.xsd | 163 ++++++++++++++++++ src/requirements/production.txt | 1 + 6 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 src/program/migrations/0079_eventinstance_uuid.py create mode 100644 src/program/xsd/schedule.xml.xsd diff --git a/src/program/migrations/0079_eventinstance_uuid.py b/src/program/migrations/0079_eventinstance_uuid.py new file mode 100644 index 00000000..ddfc0244 --- /dev/null +++ b/src/program/migrations/0079_eventinstance_uuid.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.3 on 2020-02-22 13:59 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0078_auto_20200214_2100"), + ] + + operations = [ + migrations.AddField( + model_name="eventinstance", + name="uuid", + field=models.UUIDField( + default=uuid.uuid4, + editable=False, + help_text="This field is mostly here to keep Frab happy, it is not the PK of the model", + unique=True, + ), + ), + ] diff --git a/src/program/models.py b/src/program/models.py index 35c9d7ca..10f61ae0 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -591,7 +591,7 @@ class Event(CampRelatedModel): return False def get_absolute_url(self): - return reverse_lazy( + return reverse( "program:event_detail", kwargs={"camp_slug": self.camp.slug, "event_slug": self.slug}, ) @@ -621,6 +621,13 @@ class Event(CampRelatedModel): class EventInstance(CampRelatedModel): """ An instance of an event """ + uuid = models.UUIDField( + default=uuid.uuid4, + unique=True, + editable=False, + help_text="This field is mostly here to keep Frab happy, it is not the PK of the model", + ) + event = models.ForeignKey( "program.event", related_name="instances", on_delete=models.PROTECT ) diff --git a/src/program/urls.py b/src/program/urls.py index 0a0e2311..90f67cb7 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -20,6 +20,7 @@ from .views import ( FeedbackDetailView, FeedbackListView, FeedbackUpdateView, + FrabXmlView, ICSView, NoScriptScheduleView, ProgramControlCenter, @@ -42,6 +43,7 @@ urlpatterns = [ path("", ScheduleView.as_view(), name="schedule_index"), path("noscript/", NoScriptScheduleView.as_view(), name="noscript_schedule_index"), path("ics/", ICSView.as_view(), name="ics_view"), + path("frab.xml", FrabXmlView.as_view(), name="frab_view"), path("control/", ProgramControlCenter.as_view(), name="program_control_center"), path( "proposals/", diff --git a/src/program/views.py b/src/program/views.py index bb5b275e..98337b20 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -8,13 +8,14 @@ from django.conf import settings from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import Http404, HttpResponse +from django.http import Http404, HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect from django.template import Context, Engine from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic.edit import CreateView, DeleteView, UpdateView +from lxml import etree, objectify from utils.middleware import RedirectException from utils.mixins import UserIsObjectOwnerMixin @@ -848,6 +849,108 @@ class CallForParticipationView(CampViewMixin, TemplateView): template_name = "call_for_participation.html" +class FrabXmlView(CampViewMixin, View): + """ + This view returns an XML schedule in Frab format + XSD is from https://raw.githubusercontent.com/wiki/frab/frab/images/schedule.xsd + """ + + def get(self, *args, **kwargs): + qs = models.EventInstance.objects.filter(event__track__camp=self.camp).order_by( + "when", "location" + ) + E = objectify.ElementMaker(annotate=False) + days = () + i = 0 + for day in self.camp.get_days("camp")[:-1]: + i += 1 + locations = () + for location in models.EventLocation.objects.filter( + id__in=qs.values_list("location_id", flat=True) + ): + instances = () + for instance in qs.filter(when__contained_by=day, location=location): + speakers = () + for speaker in instance.event.speakers.all(): + speakers += (E.person(speaker.name, id=str(speaker.pk)),) + urls = () + for url in instance.event.urls.all(): + urls += (E.link(url.urltype.name, href=url.url),) + instances += ( + E.event( + E.date(instance.when.lower.isoformat()), + E.start(instance.when.lower.time()), + E.duration(instance.when.upper - instance.when.lower), + E.room(location.name), + E.slug(f"{instance.pk}-{instance.event.slug}"), + E.url( + self.request.build_absolute_uri( + instance.event.get_absolute_url() + ) + ), + E.recording( + E.license("CC BY-SA 4.0"), + E.optout( + "false" + if instance.event.video_recording + else "true" + ), + ), + E.title(instance.event.title), + E.subtitle(""), + E.track(instance.event.track), + E.type(instance.event.event_type), + E.language("en"), + E.abstract(instance.event.abstract), + E.description(""), + E.persons(*speakers), + E.links(*urls), + E.attachments, + id=str(instance.id), + guid=str(instance.uuid), + ), + ) + if instances: + locations += (E.room(*instances, name=location.name),) + days += ( + E.day( + *locations, + index=str(i), + date=str(day.lower.date()), + start=day.lower.isoformat(), + end=day.upper.isoformat(), + ), + ) + + xml = E.schedule( + E.version("BornHack Frab XML Generator v0.9"), + E.conference( + E.title(self.camp.title), + E.acronym(str(self.camp.camp.lower.year)), + E.start(self.camp.camp.lower.date().isoformat()), + E.end(self.camp.camp.upper.date().isoformat()), + E.days(len(self.camp.get_days("camp"))), + E.timeslot_duration("00:30"), + E.base_url(self.request.build_absolute_uri("/")), + ), + *days, + ) + xml = etree.tostring(xml, pretty_print=True, xml_declaration=True) + + # let's play nice - validate the XML before returning it + schema = etree.XMLSchema(file="program/xsd/schedule.xml.xsd") + parser = objectify.makeparser(schema=schema) + try: + _ = objectify.fromstring(xml, parser) + except etree.XMLSyntaxError: + # we are generating invalid XML + logger.exception("Something went sideways when validating frab xml :(") + return HttpResponseServerError() + response = HttpResponse(content_type="application/xml") + response.write(xml) + return response + + ################################################################################################### # control center csv diff --git a/src/program/xsd/schedule.xml.xsd b/src/program/xsd/schedule.xml.xsd new file mode 100644 index 00000000..c459d25c --- /dev/null +++ b/src/program/xsd/schedule.xml.xsd @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/requirements/production.txt b/src/requirements/production.txt index be8b7de1..12359708 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -20,6 +20,7 @@ html5lib==1.0.1 icalendar==4.0.4 ipython==7.12.0 irc3==1.1.5 +lxml==4.5.0 olefile==0.46 Pillow==6.2.1 pipdeptree==0.13.2