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
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
)

View file

@ -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/",

View file

@ -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

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
ipython==7.12.0
irc3==1.1.5
lxml==4.5.0
olefile==0.46
Pillow==6.2.1
pipdeptree==0.13.2