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:
parent
33383e6559
commit
b6eaaa5f18
25
src/program/migrations/0079_eventinstance_uuid.py
Normal file
25
src/program/migrations/0079_eventinstance_uuid.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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/",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
163
src/program/xsd/schedule.xml.xsd
Normal file
163
src/program/xsd/schedule.xml.xsd
Normal 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>
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue