diff --git a/apps/base/tasks_export.py b/apps/base/tasks_export.py index 58626cdf5..a5e87984f 100644 --- a/apps/base/tasks_export.py +++ b/apps/base/tasks_export.py @@ -144,8 +144,13 @@ def export_db(stdout, table): return with app.test_client() as client: - for file_type in ["frab", "json", "ics"]: - url = f"/schedule/{year}.{file_type}" + for file_type, url_suffix in [ + ("frab", "frab.xml"), + ("frab_json", "frab.json"), + ("json", "json"), + ("ics", "ics"), + ]: + url = f"/schedule/{year}.{url_suffix}" dest_path = os.path.join(path, "public", f"schedule.{file_type}") response = client.get(url) if response.status_code != 200: diff --git a/apps/schedule/__init__.py b/apps/schedule/__init__.py index 69a5931f9..58b4b21be 100644 --- a/apps/schedule/__init__.py +++ b/apps/schedule/__init__.py @@ -56,7 +56,7 @@ def lineup_talk_redirect(year, proposal_id, slug=None): def feed_redirect(fmt): routes = { "json": "schedule.schedule_json", - "frab": "schedule.schedule_frab", + "frab": "schedule.schedule_frab_xml", "ical": "schedule.schedule_ical", "ics": "schedule.schedule_ical", } diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index f58927792..2c205b8cf 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -1,26 +1,21 @@ import json -from flask import Response, abort, request +from flask import Response, abort, redirect, request, url_for from flask import current_app as app from flask_cors import cross_origin from flask_login import current_user from icalendar import Calendar, Event -from main import db, get_or_404 +from main import db, external_url, get_or_404 from models import event_year from models.cfp import Proposal from models.user import User from ..common import feature_enabled, feature_flag, json_response from . import schedule -from .data import ( - _convert_time_to_str, - _get_proposal_dict, - _get_scheduled_proposals, - _get_upcoming, -) +from .data import _convert_time_to_str, _get_proposal_dict, _get_scheduled_proposals, _get_upcoming +from .frab_exporter import FrabJsonExporter, FrabXmlExporter from .historic import feed_historic -from .schedule_xml import export_frab def _format_event_description(event): @@ -64,6 +59,14 @@ def schedule_frab(year): if year != event_year(): return feed_historic(year, "frab") + return redirect(url_for("schedule.schedule_frab_xml", year=year)) + + +@schedule.route("/schedule/.frab.xml") +def schedule_frab_xml(year): + if year != event_year(): + return feed_historic(year, "frab") + if not feature_enabled("SCHEDULE"): abort(404) @@ -78,13 +81,53 @@ def schedule_frab(year): .all() ) - schedule = [_get_proposal_dict(p, []) for p in schedule] + scheduled_content_only = request.args.get("scheduled_content_only") in ("true", "yes") + village_id = request.args.get("village_id") + venue_ids = request.args.get("venue_ids", "").split(",") - frab = export_frab(schedule) + exporter = FrabXmlExporter( + schedule, scheduled_content_only=scheduled_content_only, village_id=village_id, venue_ids=venue_ids + ) + frab = exporter.run() return Response(frab, mimetype="application/xml") +@schedule.route("/schedule/.frab.json") +def schedule_frab_json(year): + if year != event_year(): + return feed_historic(year, "frab_json") + + if not feature_enabled("SCHEDULE"): + abort(404) + + schedule = ( + Proposal.query.filter( + Proposal.is_accepted, + Proposal.scheduled_time.isnot(None), + Proposal.scheduled_venue_id.isnot(None), + Proposal.scheduled_duration.isnot(None), + ) + .order_by(Proposal.scheduled_time) + .all() + ) + + scheduled_content_only = request.args.get("scheduled_content_only") in ("true", "yes") + village_id = request.args.get("village_id") + venue_ids = request.args.get("venue_ids", "").split(",") + + exporter = FrabJsonExporter( + schedule, + url=external_url("schedule.schedule_frab_json", year=year), + scheduled_content_only=scheduled_content_only, + village_id=village_id, + venue_ids=venue_ids, + ) + frab = exporter.run() + + return Response(json.dumps(frab, indent=4), mimetype="application/json") + + @schedule.route("/schedule/.ical") @schedule.route("/schedule/.ics") def schedule_ical(year): diff --git a/apps/schedule/frab_exporter.py b/apps/schedule/frab_exporter.py new file mode 100644 index 000000000..52cc83fa0 --- /dev/null +++ b/apps/schedule/frab_exporter.py @@ -0,0 +1,347 @@ +from datetime import datetime, time, timedelta +from functools import cached_property +from hashlib import md5 +from uuid import NAMESPACE_URL, uuid5 + +from lxml import etree + +from main import external_url +from models import event_end, event_start, event_year +from models.cfp import HUMAN_CFP_TYPES, Venue + +from . import event_tz +from .data import _get_proposal_dict + +LICENCE = "CC BY-SA 4.0" +VERSION = "1.0-public" + +TRACK_COLOURS = { + "talk": "#FB0558", + "performance": "#2EADD9", + "workshop": "#F9E200", + "youthworkshop": "#FF8101", + "lightning": "#FC0220", +} + +for slug, human_readable in HUMAN_CFP_TYPES.items(): + if slug not in TRACK_COLOURS: + TRACK_COLOURS[slug] = f"#{md5(human_readable.encode('utf-8')).hexdigest()[:6]}" + + +class FrabExporter: + def __init__(self, schedule, scheduled_content_only=False, village_id=None, venue_ids=None): + self._schedule = schedule + self._scheduled_content_only = scheduled_content_only + self._village_id = int(village_id) if village_id else None + if venue_ids: + self._venue_ids = [int(id.strip()) for id in venue_ids if id.strip()] + else: + self._venue_ids = [] + + def format_duration(self, start_time: datetime, end_time: datetime) -> str: + # str(timedelta) creates e.g. hrs:min:sec... + duration = (end_time - start_time).total_seconds() / 60 + hours = int(duration // 60) + minutes = int(duration % 60) + if hours < 24: + return f"{hours:d}:{minutes:02d}" + days = int(hours // 24) + hours = int(hours % 24) + return f"{days:d}:{hours:02d}:{minutes:02d}" + + def get_day_start_end(self, dt: datetime, start_time: time = time(4, 0)) -> tuple[datetime, datetime]: + # A day changeover of 4am allows us to have late events. + # All in local time because that's what people deal in. + start_date = dt.date() + if dt.time() < start_time: + start_date -= timedelta(days=1) + + end_date = start_date + timedelta(days=1) + + start_dt = datetime.combine(start_date, start_time) + end_dt = datetime.combine(end_date, start_time) + + start_dt = event_tz.localize(start_dt) + end_dt = event_tz.localize(end_dt) + + return start_dt, end_dt + + @cached_property + def schedule(self): + if not self._schedule: + return [] + data = {} + index = 0 + for event in self._schedule: + event_dict = _get_proposal_dict(event, []) + day_start, day_end = self.get_day_start_end(event_dict["start_date"]) + day_key = day_start.strftime("%Y-%m-%d") + venue_key = event.scheduled_venue.name + + if self._scheduled_content_only and not event.scheduled_venue.scheduled_content_only: + continue + + if self._village_id and event.scheduled_venue.village_id != self._village_id: + continue + + if self._venue_ids and event.scheduled_venue.id not in self._venue_ids: + continue + + if day_key not in data: + data[day_key] = { + "index": index, + "start": day_start, + "end": day_end, + "rooms": {}, + } + index += 1 + + day = data[day_key] + if venue_key not in day["rooms"]: + day["rooms"][venue_key] = { + "id": event.scheduled_venue.id, + "name": event.scheduled_venue.name, + "priority": event.scheduled_venue.priority, + "talks": [], + } + + day["rooms"][venue_key]["talks"].append(event_dict) + + for day in data.values(): + day["rooms"] = sorted( + day["rooms"].values(), + key=lambda room: -room["priority"] if room["priority"] else room["name"], + ) + return data.values() + + +class FrabJsonExporter(FrabExporter): + def __init__(self, schedule, url=None, scheduled_content_only=False, village_id=None, venue_ids=None): + super().__init__( + schedule, + scheduled_content_only=scheduled_content_only, + village_id=village_id, + venue_ids=venue_ids, + ) + self.url = url + + @cached_property + def rooms(self): + rooms = Venue.query.order_by(-Venue.priority, Venue.name).all() + result = [] + for room in rooms: + if self._scheduled_content_only and not room.scheduled_content_only: + continue + + if self._village_id and room.village_id != self._village_id: + continue + + if self._venue_ids and room.id not in self._venue_ids: + continue + + result.append(room) + return result + + def run(self): + return { + "$schema": "https://c3voc.de/schedule/schema.json", + "schedule": { + "url": self.url, + "version": VERSION, + "base_url": external_url("base.main"), + "conference": { + "acronym": f"emf{event_year()}", + "title": f"Electromagnetic Field {event_year()}", + "start": event_start().strftime("%Y-%m-%d"), + "end": event_end().strftime("%Y-%m-%d"), + "daysCount": 3, + "timeslot_duration": "00:10", + "time_zone_name": event_tz.zone, + "rooms": [ + { + "name": room.name, + "capacity": room.capacity, + # TODO do we have an URL for listing the schedule in a specific room? + "guid": str(uuid5(NAMESPACE_URL, room.name)), + } + for room in self.rooms + ], + "tracks": [ + { + "name": human_readable, + "slug": slug, + "color": TRACK_COLOURS[slug], + } + for slug, human_readable in sorted(HUMAN_CFP_TYPES.items()) + ], + "days": [ + { + "index": day["index"], + "date": day["start"].strftime("%Y-%m-%d"), + "day_start": day["start"].isoformat(), + "day_end": day["end"].isoformat(), + "rooms": { + room["name"]: [ + { + "guid": str(uuid5(NAMESPACE_URL, event["link"])), + "id": event["id"], + "date": event["start_date"].isoformat(), + "start": event["start_date"].strftime("%H:%M"), + "duration": self.format_duration( + event["start_date"], event["end_date"] + ), + "room": room["name"], + "slug": "emf{}-{}-{}".format( + event_year(), event["id"], event["slug"] + ), + "url": event["link"], + "title": event["title"], + "subtitle": "", + "track": HUMAN_CFP_TYPES[event["type"]], + "type": event["type"], + "language": "en", + "abstract": event["description"], + "description": "", + "recording_license": LICENCE, + "do_not_record": bool(event.get("video_privacy") != "public"), + "persons": [ + { + "name": event["speaker"], + } + ], + "links": [ + { + "title": "ccc", + "url": event["video"]["ccc"], + "type": "related", + } + ] + if "ccc" in event.get("video") + else [ + { + "title": "youtube", + "url": event["video"]["youtube"], + "type": "related", + } + ] + if "youtube" in event.get("video") + else [], + } + for event in room["talks"] + ] + for room in day["rooms"] + }, + } + for day in self.schedule + ], + }, + }, + } + + +class FrabXmlExporter(FrabExporter): + def _add_sub_with_text(self, parent, element, text, **extra): + node = etree.SubElement(parent, element, **extra) + node.text = text + return node + + def make_root(self): + root = etree.Element("schedule") + + self._add_sub_with_text(root, "version", VERSION) + + conference = etree.SubElement(root, "conference") + + self._add_sub_with_text(conference, "title", f"Electromagnetic Field {event_year()}") + self._add_sub_with_text(conference, "acronym", f"emf{event_year()}") + self._add_sub_with_text(conference, "start", event_start().strftime("%Y-%m-%d")) + self._add_sub_with_text(conference, "end", event_end().strftime("%Y-%m-%d")) + self._add_sub_with_text(conference, "days", "3") + self._add_sub_with_text(conference, "timeslot_duration", "00:10") + self._add_sub_with_text(conference, "time_zone_name", event_tz.zone) + self._add_sub_with_text(conference, "url", external_url("base.main")) + + for slug, human_readable in sorted(HUMAN_CFP_TYPES.items()): + etree.SubElement(conference, "track", name=human_readable, slug=slug, color=TRACK_COLOURS[slug]) + + return root + + def add_day(self, root, index, start, end): + return etree.SubElement( + root, + "day", + index=str(index), + date=start.strftime("%Y-%m-%d"), + start=start.isoformat(), + end=end.isoformat(), + ) + + def add_room(self, day, name): + return etree.SubElement(day, "room", name=name) + + def add_event(self, room, event): + event_node = etree.SubElement( + room, "event", id=str(event["id"]), guid=str(uuid5(NAMESPACE_URL, event["link"])) + ) + + self._add_sub_with_text(event_node, "room", room.attrib["name"]) + self._add_sub_with_text(event_node, "title", event["title"]) + + event_type = event.get("type", "talk") + self._add_sub_with_text(event_node, "type", event_type) + # infobeamer frab scheduler can color by "track" + self._add_sub_with_text(event_node, "track", HUMAN_CFP_TYPES[event_type]) + + self._add_sub_with_text(event_node, "date", event["start_date"].isoformat()) + self._add_sub_with_text(event_node, "url", event["link"]) + + # Start time + self._add_sub_with_text(event_node, "start", event["start_date"].strftime("%H:%M")) + + duration = self.format_duration(event["start_date"], event["end_date"]) + self._add_sub_with_text(event_node, "duration", duration) + + self._add_sub_with_text(event_node, "abstract", event["description"]) + self._add_sub_with_text(event_node, "description", "") + + self._add_sub_with_text( + event_node, + "slug", + "emf{}-{}-{}".format(event_year(), event["id"], event["slug"]), + ) + + self._add_sub_with_text(event_node, "subtitle", "") + + self.add_persons(event_node, event) + self.add_recording(event_node, event) + + def add_persons(self, event_node, event): + persons_node = etree.SubElement(event_node, "persons") + self._add_sub_with_text(persons_node, "person", event["speaker"], id=str(event["user_id"])) + + def add_recording(self, event_node, event): + recording_node = etree.SubElement(event_node, "recording") + self._add_sub_with_text(recording_node, "license", LICENCE) + + if event.get("video_privacy") == "public": + video = event.get("video", {}) + self._add_sub_with_text(recording_node, "optout", "false") + if "ccc" in video: + self._add_sub_with_text(recording_node, "url", video["ccc"]) + elif "youtube" in video: + self._add_sub_with_text(recording_node, "url", video["youtube"]) + else: + self._add_sub_with_text(recording_node, "optout", "true") + + def run(self): + root = self.make_root() + for day in self.schedule: + room_node = self.add_day(root, day["index"], day["start"], day["end"]) + + for venue in day["rooms"]: + venue_node = self.add_room(room_node, venue["name"]) + + for event in venue["talks"]: + self.add_event(venue_node, event) + + return etree.tostring(root) diff --git a/apps/schedule/schedule_xml.py b/apps/schedule/schedule_xml.py deleted file mode 100644 index 8a7d6b0f4..000000000 --- a/apps/schedule/schedule_xml.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Utils to format schedule in the de facto standard Frab XML format. - -Frab XML is consumed by a number of external tools such as C3VOC. -""" - -from datetime import datetime, time, timedelta -from uuid import NAMESPACE_URL, uuid5 - -from lxml import etree - -from main import external_url -from models import event_end, event_start, event_year - -from . import event_tz - - -def get_duration(start_time, end_time): - # str(timedelta) creates e.g. hrs:min:sec... - duration = (end_time - start_time).total_seconds() / 60 - hours = int(duration // 60) - minutes = int(duration % 60) - if hours < 24: - return f"{hours:d}:{minutes:02d}" - days = int(hours // 24) - hours = int(hours % 24) - return f"{days:d}:{hours:02d}:{minutes:02d}" - - -def get_day_start_end(dt, start_time=time(4, 0)): - # A day changeover of 4am allows us to have late events. - # All in local time because that's what people deal in. - start_date = dt.date() - if dt.time() < start_time: - start_date -= timedelta(days=1) - - end_date = start_date + timedelta(days=1) - - start_dt = datetime.combine(start_date, start_time) - end_dt = datetime.combine(end_date, start_time) - - start_dt = event_tz.localize(start_dt) - end_dt = event_tz.localize(end_dt) - - return start_dt, end_dt - - -def _add_sub_with_text(parent, element, text, **extra): - node = etree.SubElement(parent, element, **extra) - node.text = text - return node - - -def make_root(): - root = etree.Element("schedule") - - _add_sub_with_text(root, "version", "1.0-public") - - conference = etree.SubElement(root, "conference") - - _add_sub_with_text(conference, "title", f"Electromagnetic Field {event_year()}") - _add_sub_with_text(conference, "acronym", f"emf{event_year()}") - _add_sub_with_text(conference, "start", event_start().strftime("%Y-%m-%d")) - _add_sub_with_text(conference, "end", event_end().strftime("%Y-%m-%d")) - _add_sub_with_text(conference, "days", "3") - _add_sub_with_text(conference, "timeslot_duration", "00:10") - - return root - - -def add_day(root, index, start, end): - return etree.SubElement( - root, - "day", - index=str(index), - date=start.strftime("%Y-%m-%d"), - start=start.isoformat(), - end=end.isoformat(), - ) - - -def add_room(day, name): - return etree.SubElement(day, "room", name=name) - - -def add_event(room, event): - url = external_url("schedule.item", year=event_year(), proposal_id=event["id"], slug=event["slug"]) - - event_node = etree.SubElement(room, "event", id=str(event["id"]), guid=str(uuid5(NAMESPACE_URL, url))) - - _add_sub_with_text(event_node, "room", room.attrib["name"]) - _add_sub_with_text(event_node, "title", event["title"]) - - event_type = event.get("type", "talk") - _add_sub_with_text(event_node, "type", event_type) - # infobeamer frab scheduler can color by "track" - _add_sub_with_text(event_node, "track", event_type) - - _add_sub_with_text(event_node, "date", event["start_date"].isoformat()) - _add_sub_with_text(event_node, "url", url) - - # Start time - _add_sub_with_text(event_node, "start", event["start_date"].strftime("%H:%M")) - - duration = get_duration(event["start_date"], event["end_date"]) - _add_sub_with_text(event_node, "duration", duration) - - _add_sub_with_text(event_node, "abstract", event["description"]) - _add_sub_with_text(event_node, "description", "") - - _add_sub_with_text( - event_node, - "slug", - "emf{}-{}-{}".format(event_year(), event["id"], event["slug"]), - ) - - _add_sub_with_text(event_node, "subtitle", "") - - add_persons(event_node, event) - add_recording(event_node, event) - - -def add_persons(event_node, event): - persons_node = etree.SubElement(event_node, "persons") - - _add_sub_with_text(persons_node, "person", event["speaker"], id=str(event["user_id"])) - - -def add_recording(event_node, event): - video = event.get("video", {}) - - recording_node = etree.SubElement(event_node, "recording") - - _add_sub_with_text(recording_node, "license", "CC BY-SA 4.0") - _add_sub_with_text( - recording_node, "optout", "false" if event.get("video_privacy") == "public" else "true" - ) - if "ccc" in video: - _add_sub_with_text(recording_node, "url", video["ccc"]) - elif "youtube" in video: - _add_sub_with_text(recording_node, "url", video["youtube"]) - - -def export_frab(schedule): - root = make_root() - days_dict = {} - index = 0 - - for event in schedule: - day_start, day_end = get_day_start_end(event["start_date"]) - day_key = day_start.strftime("%Y-%m-%d") - venue_key = event["venue"] - - if day_key not in days_dict: - index += 1 - node = add_day(root, index, day_start, day_end) - days_dict[day_key] = {"node": node, "rooms": {}} - - day = days_dict[day_key] - - if venue_key not in day["rooms"]: - day["rooms"][venue_key] = add_room(day["node"], venue_key) - - add_event(day["rooms"][venue_key], event) - - return etree.tostring(root) diff --git a/templates/schedule/user_schedule.html b/templates/schedule/user_schedule.html index 74b6a67fb..b6126129b 100644 --- a/templates/schedule/user_schedule.html +++ b/templates/schedule/user_schedule.html @@ -14,7 +14,8 @@ Schedule feeds: JSON | iCal | - Frab + Frab XML | + Frab JSON

{% endblock %} {% block foot %} diff --git a/tests/frabs_schema.xml b/tests/frabs_schema.xml index 8a19a8a6e..d575bcff2 100644 --- a/tests/frabs_schema.xml +++ b/tests/frabs_schema.xml @@ -1,4 +1,33 @@ + @@ -10,6 +39,7 @@ + @@ -43,6 +73,11 @@ + + + + + @@ -81,27 +116,34 @@ - + - + + - + + + + + + + @@ -133,6 +175,12 @@ + + + + + + @@ -145,9 +193,16 @@ + + + + + + - + + @@ -156,6 +211,12 @@ + + + + + + @@ -166,7 +227,7 @@ - + @@ -185,7 +246,7 @@ - + @@ -201,6 +262,8 @@ + + @@ -215,6 +278,7 @@ + diff --git a/tests/test_frab_export.py b/tests/test_frab_export.py index 199bb62be..b38b47670 100644 --- a/tests/test_frab_export.py +++ b/tests/test_frab_export.py @@ -4,14 +4,7 @@ from lxml import etree from apps.schedule import event_tz -from apps.schedule.schedule_xml import ( - add_day, - add_event, - add_room, - export_frab, - get_duration, - make_root, -) +from apps.schedule.frab_exporter import FrabExporter, FrabXmlExporter def _local_datetime(*args): @@ -34,8 +27,9 @@ def test_empty_frab_schema_fails(frab_schema): def test_min_version_is_valid(frab_schema, request_context): - root = make_root() - add_day( + exporter = FrabXmlExporter([]) + root = exporter.make_root() + exporter.add_day( root, index=1, start=_local_datetime(2016, 8, 5, 4, 0), @@ -46,27 +40,29 @@ def test_min_version_is_valid(frab_schema, request_context): def test_simple_room(frab_schema, request_context): - root = make_root() - day = add_day( + exporter = FrabXmlExporter([]) + root = exporter.make_root() + day = exporter.add_day( root, index=1, start=_local_datetime(2016, 8, 5, 4, 0), end=_local_datetime(2016, 8, 6, 4, 0), ) - add_room(day, "the hinterlands") + exporter.add_room(day, "the hinterlands") frab_schema.assert_(root) def test_simple_event(frab_schema, request_context): - root = make_root() - day = add_day( + exporter = FrabXmlExporter([]) + root = exporter.make_root() + day = exporter.add_day( root, index=1, start=_local_datetime(2016, 8, 5, 4, 0), end=_local_datetime(2016, 8, 6, 4, 0), ) - room = add_room(day, "the hinterlands") + room = exporter.add_room(day, "the hinterlands") event = { "id": 1, @@ -77,72 +73,75 @@ def test_simple_event(frab_schema, request_context): "user_id": 123, "end_date": _local_datetime(2016, 8, 5, 11, 00), "start_date": _local_datetime(2016, 8, 5, 10, 30), + "link": "http://example.com", } - add_event(room, event) + exporter.add_event(room, event) frab_schema.assert_(root) -def test_export_frab(frab_schema, request_context): - events = [ - { - "id": 1, - "slug": "the-foo-bar", - "title": "The foo bar", - "venue": "here", - "description": "The foo bar", - "speaker": "Someone", - "user_id": 123, - "end_date": _local_datetime(2016, 8, 5, 11, 00), - "start_date": _local_datetime(2016, 8, 5, 10, 30), - "video": { - "ccc": "http://example.com/media.ccc.de", - }, - }, - { - "id": 2, - "slug": "the-foo-bartt", - "title": "The foo bartt", - "venue": "There", - "description": "The foo bar", - "speaker": "Someone", - "user_id": 123, - "end_date": _local_datetime(2016, 8, 5, 11, 00), - "start_date": _local_datetime(2016, 8, 5, 10, 30), - "video": { - "youtube": "http://example.com/youtube.com", - }, - }, - { - "id": 3, - "slug": "the-foo-bartt2", - "title": "The foo bartt2", - "venue": "here", - "type": "workshop", - "description": "The foo bar", - "speaker": "Someone", - "user_id": 123, - "end_date": _local_datetime(2016, 8, 6, 11, 00), - "start_date": _local_datetime(2016, 8, 6, 10, 30), - "video": { - "ccc": "http://example.com/media.ccc.de", - "youtube": "http://example.com/youtube.com", - }, - }, - ] - - frab = export_frab(events) - frab_doc = etree.fromstring(frab) - - frab_schema.assert_(frab_doc) +# TODO rework this. FrabExporter now wants a QuerySet instead of a dict +# def test_export_frab(frab_schema, request_context): +# events = [ +# { +# "id": 1, +# "slug": "the-foo-bar", +# "title": "The foo bar", +# "venue": "here", +# "description": "The foo bar", +# "speaker": "Someone", +# "user_id": 123, +# "end_date": _local_datetime(2016, 8, 5, 11, 00), +# "start_date": _local_datetime(2016, 8, 5, 10, 30), +# "video": { +# "ccc": "http://example.com/media.ccc.de", +# }, +# }, +# { +# "id": 2, +# "slug": "the-foo-bartt", +# "title": "The foo bartt", +# "venue": "There", +# "description": "The foo bar", +# "speaker": "Someone", +# "user_id": 123, +# "end_date": _local_datetime(2016, 8, 5, 11, 00), +# "start_date": _local_datetime(2016, 8, 5, 10, 30), +# "video": { +# "youtube": "http://example.com/youtube.com", +# }, +# }, +# { +# "id": 3, +# "slug": "the-foo-bartt2", +# "title": "The foo bartt2", +# "venue": "here", +# "type": "workshop", +# "description": "The foo bar", +# "speaker": "Someone", +# "user_id": 123, +# "end_date": _local_datetime(2016, 8, 6, 11, 00), +# "start_date": _local_datetime(2016, 8, 6, 10, 30), +# "video": { +# "ccc": "http://example.com/media.ccc.de", +# "youtube": "http://example.com/youtube.com", +# }, +# }, +# ] +# +# frab = export_frab(events) +# frab_doc = etree.fromstring(frab) +# +# frab_schema.assert_(frab_doc) def test_get_duration(): + exporter = FrabExporter([]) start = datetime(2016, 8, 15, 11, 0) stop = datetime(2016, 8, 15, 11, 30) - assert get_duration(start, stop) == "0:30" + assert exporter.format_duration(start, stop) == "0:30" stop = datetime(2016, 8, 15, 11, 5) - assert get_duration(start, stop) == "0:05" + assert exporter.format_duration(start, stop) == "0:05" stop = datetime(2016, 8, 15, 12, 0) - assert get_duration(start, stop) == "1:00" + assert exporter.format_duration(start, stop) == "1:00" diff --git a/tests/test_schedule.py b/tests/test_schedule.py index 3c8b67eb3..d1bd99047 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -2,7 +2,7 @@ import pytest -from apps.schedule import schedule_xml +from apps.schedule import frab_exporter @pytest.mark.parametrize( @@ -17,7 +17,8 @@ ], ) def test_get_duration(start_time, end_time, expected): + exporter = frab_exporter.FrabExporter([]) fmt = "%Y-%m-%d %H:%M:%S" start_time = datetime.strptime(start_time, fmt) end_time = datetime.strptime(end_time, fmt) - assert schedule_xml.get_duration(start_time, end_time) == expected + assert exporter.format_duration(start_time, end_time) == expected