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
This commit is contained in:
Thomas Steen Rasmussen 2020-02-22 15:09:12 +01:00 committed by GitHub
parent 33383e6559
commit b6eaaa5f18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 303 additions and 2 deletions

View File

@ -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,
),
),
]

View File

@ -591,7 +591,7 @@ class Event(CampRelatedModel):
return False return False
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy( return reverse(
"program:event_detail", "program:event_detail",
kwargs={"camp_slug": self.camp.slug, "event_slug": self.slug}, kwargs={"camp_slug": self.camp.slug, "event_slug": self.slug},
) )
@ -621,6 +621,13 @@ class Event(CampRelatedModel):
class EventInstance(CampRelatedModel): class EventInstance(CampRelatedModel):
""" An instance of an event """ """ 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( event = models.ForeignKey(
"program.event", related_name="instances", on_delete=models.PROTECT "program.event", related_name="instances", on_delete=models.PROTECT
) )

View File

@ -20,6 +20,7 @@ from .views import (
FeedbackDetailView, FeedbackDetailView,
FeedbackListView, FeedbackListView,
FeedbackUpdateView, FeedbackUpdateView,
FrabXmlView,
ICSView, ICSView,
NoScriptScheduleView, NoScriptScheduleView,
ProgramControlCenter, ProgramControlCenter,
@ -42,6 +43,7 @@ urlpatterns = [
path("", ScheduleView.as_view(), name="schedule_index"), path("", ScheduleView.as_view(), name="schedule_index"),
path("noscript/", NoScriptScheduleView.as_view(), name="noscript_schedule_index"), path("noscript/", NoScriptScheduleView.as_view(), name="noscript_schedule_index"),
path("ics/", ICSView.as_view(), name="ics_view"), 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("control/", ProgramControlCenter.as_view(), name="program_control_center"),
path( path(
"proposals/", "proposals/",

View File

@ -8,13 +8,14 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.mixins import LoginRequiredMixin 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.shortcuts import get_object_or_404, redirect
from django.template import Context, Engine from django.template import Context, Engine
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic import DetailView, ListView, TemplateView, View
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from lxml import etree, objectify
from utils.middleware import RedirectException from utils.middleware import RedirectException
from utils.mixins import UserIsObjectOwnerMixin from utils.mixins import UserIsObjectOwnerMixin
@ -848,6 +849,108 @@ class CallForParticipationView(CampViewMixin, TemplateView):
template_name = "call_for_participation.html" 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 # control center csv

View File

@ -0,0 +1,163 @@
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="schedule">
<xs:complexType>
<xs:sequence>
<xs:element type="xs:string" name="version"/>
<xs:element name="conference">
<xs:complexType>
<xs:all>
<xs:element type="xs:string" name="title"/>
<xs:element type="xs:string" name="acronym"/>
<xs:element type="xs:date" name="start" minOccurs="0"/>
<xs:element type="xs:date" name="end" minOccurs="0"/>
<xs:element type="xs:integer" name="days" minOccurs="0"/>
<xs:element type="duration" name="timeslot_duration" minOccurs="0"/>
<xs:element type="httpURI" name="base_url" minOccurs="0"/>
</xs:all>
</xs:complexType>
</xs:element>
<xs:element type="day" name="day" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:unique name="guid_unique">
<xs:selector xpath="day/room/event" />
<xs:field xpath="@guid" />
</xs:unique>
<xs:unique name="id_unique">
<xs:selector xpath="day/room/event" />
<xs:field xpath="@id" />
</xs:unique>
<xs:unique name="slug_unique">
<xs:selector xpath="day/room/event/slug" />
<xs:field xpath="." />
</xs:unique>
</xs:element>
<xs:complexType name="day">
<xs:sequence>
<xs:element type="room" name="room" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute type="xs:date" name="date"/>
<xs:attribute type="dateTimeTZ" name="start"/>
<xs:attribute type="dateTimeTZ" name="end"/>
<xs:attribute type="xs:positiveInteger" name="index"/>
</xs:complexType>
<xs:complexType name="room">
<xs:sequence>
<xs:element type="event" name="event" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name"/>
</xs:complexType>
<xs:complexType name="event">
<xs:all>
<xs:element type="xs:string" name="room"/>
<xs:element type="xs:string" name="title"/>
<xs:element type="xs:string" name="subtitle"/>
<xs:element type="xs:string" name="type"/>
<xs:element type="dateTimeTZ" name="date"/>
<xs:element type="start" name="start"/>
<xs:element type="duration" name="duration"/>
<xs:element type="xs:string" name="abstract"/>
<xs:element type="xs:string" name="slug"/>
<xs:element type="xs:string" name="track"/>
<xs:element type="xs:string" name="logo" minOccurs="0"/>
<xs:element type="persons" name="persons" minOccurs="0"/>
<xs:element type="xs:string" name="language" minOccurs="0"/>
<xs:element type="xs:string" name="description" minOccurs="0"/>
<xs:element type="recording" name="recording" minOccurs="0"/>
<xs:element type="links" name="links" minOccurs="0"/>
<xs:element type="attachments" name="attachments" minOccurs="0"/>
<xs:element type="httpURI" name="video_download_url" minOccurs="0"/>
<xs:element type="httpURI" name="url" minOccurs="0"/>
</xs:all>
<xs:attribute name="id" type="xs:positiveInteger" use="required"/>
<xs:attribute name="guid" type="uuid" use="required"/>
</xs:complexType>
<xs:simpleType name="dateTimeTZ">
<xs:restriction base="xs:dateTime">
<xs:pattern value=".+(Z|\+[0-9]{1,2}:[0-9]{2}|-[0-9]{1,2}:[0-9]{2})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="duration">
<xs:restriction base="xs:string">
<xs:pattern value="([0-9]{1,2}:[0-9]{2})|([0-9]{1,2}:[0-9]{2}:[0-9]{1,2})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="start">
<xs:restriction base="xs:string">
<xs:pattern value="([0-2][0-9]:[0-5][0-9])|([0-2][0-9]:[0-5][0-9]:[0-5][0-9])"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="uuid">
<xs:restriction base="xs:string">
<xs:pattern value="[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="httpURI">
<xs:restriction base="xs:anyURI">
<xs:pattern value="https?://.*"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="recording">
<xs:all>
<xs:element type="xs:string" name="license"/>
<xs:element type="xs:boolean" name="optout"/>
</xs:all>
</xs:complexType>
<xs:complexType name="persons">
<xs:sequence>
<xs:element name="person" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:positiveInteger" name="id"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="links">
<xs:sequence>
<xs:element name="link" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="href"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="attachments">
<xs:sequence>
<xs:element name="attachment" maxOccurs="unbounded" minOccurs="0">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="href"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -20,6 +20,7 @@ html5lib==1.0.1
icalendar==4.0.4 icalendar==4.0.4
ipython==7.12.0 ipython==7.12.0
irc3==1.1.5 irc3==1.1.5
lxml==4.5.0
olefile==0.46 olefile==0.46
Pillow==6.2.1 Pillow==6.2.1
pipdeptree==0.13.2 pipdeptree==0.13.2