From 32723631dd298d8116dea5cb91a34b0e2c656eb8 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Fri, 24 May 2024 12:26:27 +0200 Subject: [PATCH 01/19] schedule/feeds: make json feed schema compatible --- apps/schedule/feeds.py | 137 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 59cd597fe..458f46e9d 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -1,10 +1,13 @@ import json +from datetime import timedelta from icalendar import Calendar, Event from flask import request, abort, current_app as app, Response from flask_cors import cross_origin from flask_login import current_user +from math import ceil -from models import event_year +from main import external_url +from models import event_year, event_start, event_end from models.user import User from models.cfp import Proposal @@ -17,7 +20,7 @@ _convert_time_to_str, _get_upcoming, ) -from . import schedule +from . import event_tz, schedule def _format_event_description(event): @@ -56,6 +59,136 @@ def schedule_json(year): return Response(json.dumps(schedule), mimetype="application/json") +@schedule.route("/schedule/schedule-.json") # TODO validate url with upstream +@cross_origin(methods=["GET"]) +def schedule_json_schema(year): + if year != event_year(): + return feed_historic(year, "json") + + if not feature_enabled('SCHEDULE'): + abort(404) + + def duration_hhmm(duration_minutes): + if not duration_minutes or duration_minutes < 1: + return "00:00" + return "{}:{}".format( + int(duration_minutes/60), + str(duration_minutes%60).zfill(2), + ) + + 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() + ) + + duration_days = ceil((event_end() - event_end()).total_seconds / 86400), + + rooms = [ + proposal.scheduled_venue.name + for proposal in schedule + ] + + schedule_json = { + "version": "1.0-public", + "conference": { + "acronym": "emf{}".format(event_year()), + "days": [], + "daysCount": duration_days, + "end": event_end().strftime("%Y-%m-%d"), + "rooms": [ + { + "name": room, + } + for room in rooms + ], + "start": event_start().strftime("%Y-%m-%d"), + "time_zone_name": event_tz, + "timeslot_duration": "00:10", + "title": "Electromagnetic Field {}".format(event_year()), + "url": external_url("main"), + }, + } + + for day in range(0, duration_days): + day_dt = event_start() + timedelta(days=day) + day_schedule = { + "date": day_dt.strftime("%Y-%m-%d"), + "day_end": (day_dt.replace(hour=3, minute=59, second=59) + timedelta(days=1)).isoformat(), + "day_start": day_dt.replace(hour=4, minute=0, second=0).isoformat(), + "index": day, + "rooms": {}, + } + for room in rooms: + day_schedule["rooms"][room] = [] + for proposal in schedule: + if proposal.scheduled_venue.name != room: + # TODO find a better way to do that + continue + links = { + proposal.c3voc_url, + proposal.youtube_url, + proposal.thumbnail_url, + proposal.map_link, + } + links.discard(None) + links.discard("") + day_schedule["rooms"][room].append({ + "abstract": None, # The proposal model does not implement abstracts + "attachments": [], + "date": event_tz.localize(proposal.start_date).isoformat(), + "description": proposal.description, + "do_not_record": False if proposal.may_record else True, + "duration": duration_hhmm(proposal.duration_minutes), + "guid": None, + "id": proposal.id, + # This assumes there will never be a non-english talk, + # which is probably fine for a conference in the UK. + "language": "en", + "links": sorted(links), + "persons": [ + { + "name": name.strip(), + "public_name": name.strip(), + } + for name in (proposal.published_names or proposal.user.name).split(",") + ], + "recording_license": "CC BY-SA 3.0", + "room": room, + "slug": "emf{}-{}-{}".format( + event_year(), + proposal.id, + proposal.slug, + ), + "start": event_tz.localize(proposal.start_date).strftime("%H:%M"), + "subtitle": None, + "title": proposal.display_title, + "track": None, # TODO does emf have tracks? + "type": proposal.type, + "url": external_url( + ".item", + year=event_year(), + proposal_id=proposal.id, + slug=proposal.slug, + ), + }) + schedule_json["conference"]["days"].append(day_schedule) + + return Response(json.dumps({ + "schedule": schedule_json, + "$schema": "https://c3voc.de/schedule/schema.json", + "generator": { + "name": "emfcamp-website", + "url": "https://github.com/emfcamp/Website", + }, + }), mimetype="application/json") + + @schedule.route("/schedule/.frab") def schedule_frab(year): if year != event_year(): From 239e5bc3598151016350282586dff830575f0fdc Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Fri, 24 May 2024 12:50:42 +0200 Subject: [PATCH 02/19] run `ruff format` for apps/schedule/feeds.py --- apps/schedule/feeds.py | 126 +++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 458f46e9d..7ecacbb49 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -50,7 +50,7 @@ def schedule_json(year): if year != event_year(): return feed_historic(year, "json") - if not feature_enabled('SCHEDULE'): + if not feature_enabled("SCHEDULE"): abort(404) schedule = [_convert_time_to_str(p) for p in _get_scheduled_proposals(request.args)] @@ -59,21 +59,21 @@ def schedule_json(year): return Response(json.dumps(schedule), mimetype="application/json") -@schedule.route("/schedule/schedule-.json") # TODO validate url with upstream +@schedule.route("/schedule/schedule-.json") # TODO validate url with upstream @cross_origin(methods=["GET"]) def schedule_json_schema(year): if year != event_year(): return feed_historic(year, "json") - if not feature_enabled('SCHEDULE'): + if not feature_enabled("SCHEDULE"): abort(404) def duration_hhmm(duration_minutes): if not duration_minutes or duration_minutes < 1: return "00:00" return "{}:{}".format( - int(duration_minutes/60), - str(duration_minutes%60).zfill(2), + int(duration_minutes / 60), + str(duration_minutes % 60).zfill(2), ) schedule = ( @@ -87,12 +87,9 @@ def duration_hhmm(duration_minutes): .all() ) - duration_days = ceil((event_end() - event_end()).total_seconds / 86400), + duration_days = (ceil((event_end() - event_end()).total_seconds / 86400),) - rooms = [ - proposal.scheduled_venue.name - for proposal in schedule - ] + rooms = [proposal.scheduled_venue.name for proposal in schedule] schedule_json = { "version": "1.0-public", @@ -138,55 +135,62 @@ def duration_hhmm(duration_minutes): } links.discard(None) links.discard("") - day_schedule["rooms"][room].append({ - "abstract": None, # The proposal model does not implement abstracts - "attachments": [], - "date": event_tz.localize(proposal.start_date).isoformat(), - "description": proposal.description, - "do_not_record": False if proposal.may_record else True, - "duration": duration_hhmm(proposal.duration_minutes), - "guid": None, - "id": proposal.id, - # This assumes there will never be a non-english talk, - # which is probably fine for a conference in the UK. - "language": "en", - "links": sorted(links), - "persons": [ - { - "name": name.strip(), - "public_name": name.strip(), - } - for name in (proposal.published_names or proposal.user.name).split(",") - ], - "recording_license": "CC BY-SA 3.0", - "room": room, - "slug": "emf{}-{}-{}".format( - event_year(), - proposal.id, - proposal.slug, - ), - "start": event_tz.localize(proposal.start_date).strftime("%H:%M"), - "subtitle": None, - "title": proposal.display_title, - "track": None, # TODO does emf have tracks? - "type": proposal.type, - "url": external_url( - ".item", - year=event_year(), - proposal_id=proposal.id, - slug=proposal.slug, - ), - }) + day_schedule["rooms"][room].append( + { + "abstract": None, # The proposal model does not implement abstracts + "attachments": [], + "date": event_tz.localize(proposal.start_date).isoformat(), + "description": proposal.description, + "do_not_record": False if proposal.may_record else True, + "duration": duration_hhmm(proposal.duration_minutes), + "guid": None, + "id": proposal.id, + # This assumes there will never be a non-english talk, + # which is probably fine for a conference in the UK. + "language": "en", + "links": sorted(links), + "persons": [ + { + "name": name.strip(), + "public_name": name.strip(), + } + for name in (proposal.published_names or proposal.user.name).split(",") + ], + "recording_license": "CC BY-SA 3.0", + "room": room, + "slug": "emf{}-{}-{}".format( + event_year(), + proposal.id, + proposal.slug, + ), + "start": event_tz.localize(proposal.start_date).strftime("%H:%M"), + "subtitle": None, + "title": proposal.display_title, + "track": None, # TODO does emf have tracks? + "type": proposal.type, + "url": external_url( + ".item", + year=event_year(), + proposal_id=proposal.id, + slug=proposal.slug, + ), + } + ) schedule_json["conference"]["days"].append(day_schedule) - return Response(json.dumps({ - "schedule": schedule_json, - "$schema": "https://c3voc.de/schedule/schema.json", - "generator": { - "name": "emfcamp-website", - "url": "https://github.com/emfcamp/Website", - }, - }), mimetype="application/json") + return Response( + json.dumps( + { + "schedule": schedule_json, + "$schema": "https://c3voc.de/schedule/schema.json", + "generator": { + "name": "emfcamp-website", + "url": "https://github.com/emfcamp/Website", + }, + } + ), + mimetype="application/json", + ) @schedule.route("/schedule/.frab") @@ -194,7 +198,7 @@ def schedule_frab(year): if year != event_year(): return feed_historic(year, "frab") - if not feature_enabled('SCHEDULE'): + if not feature_enabled("SCHEDULE"): abort(404) schedule = ( @@ -221,7 +225,7 @@ def schedule_ical(year): if year != event_year(): return feed_historic(year, "ics") - if not feature_enabled('SCHEDULE'): + if not feature_enabled("SCHEDULE"): abort(404) schedule = _get_scheduled_proposals(request.args) @@ -307,9 +311,7 @@ def favourites_ical(): @schedule.route("/schedule/now-and-next.json") def now_and_next_json(): - return Response( - json.dumps(_get_upcoming(request.args)), mimetype="application/json" - ) + return Response(json.dumps(_get_upcoming(request.args)), mimetype="application/json") @schedule.route("/schedule//.json") From bf214a4d17116862b3e9431035819495e4d3da9b Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 12 Jun 2024 15:33:32 +0200 Subject: [PATCH 03/19] make schedule-compatible json the default view also add request argument to show the old-style json schedule --- apps/schedule/feeds.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 7ecacbb49..b2f8d6eef 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -45,7 +45,6 @@ def _format_event_description(event): @schedule.route("/schedule/.json") -@cross_origin(methods=["GET"]) def schedule_json(year): if year != event_year(): return feed_historic(year, "json") @@ -53,20 +52,11 @@ def schedule_json(year): if not feature_enabled("SCHEDULE"): abort(404) - schedule = [_convert_time_to_str(p) for p in _get_scheduled_proposals(request.args)] + if request.args.get('format') == 'array': + schedule = [_convert_time_to_str(p) for p in _get_scheduled_proposals(request.args)] - # NB this is JSON in a top-level array (security issue for low-end browsers) - return Response(json.dumps(schedule), mimetype="application/json") - - -@schedule.route("/schedule/schedule-.json") # TODO validate url with upstream -@cross_origin(methods=["GET"]) -def schedule_json_schema(year): - if year != event_year(): - return feed_historic(year, "json") - - if not feature_enabled("SCHEDULE"): - abort(404) + # NB this is JSON in a top-level array (security issue for low-end browsers) + return Response(json.dumps(schedule), mimetype="application/json") def duration_hhmm(duration_minutes): if not duration_minutes or duration_minutes < 1: From c9b4a486aa4fddc6e50d295fd7c17fbfac0ce52f Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 12 Jun 2024 15:40:49 +0200 Subject: [PATCH 04/19] schedule json: fix trailing comma --- apps/schedule/feeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index b2f8d6eef..3544f0636 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -77,7 +77,7 @@ def duration_hhmm(duration_minutes): .all() ) - duration_days = (ceil((event_end() - event_end()).total_seconds / 86400),) + duration_days = ceil((event_end() - event_end()).total_seconds / 86400) rooms = [proposal.scheduled_venue.name for proposal in schedule] From 78a4eda49ce140dc13083e0ff792c5c21e685f54 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 17:25:45 +0200 Subject: [PATCH 05/19] move frab json export to /schedule/frab-.json --- apps/schedule/feeds.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 3544f0636..6fd325597 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -52,11 +52,19 @@ def schedule_json(year): if not feature_enabled("SCHEDULE"): abort(404) - if request.args.get('format') == 'array': - schedule = [_convert_time_to_str(p) for p in _get_scheduled_proposals(request.args)] + schedule = [_convert_time_to_str(p) for p in _get_scheduled_proposals(request.args)] - # NB this is JSON in a top-level array (security issue for low-end browsers) - return Response(json.dumps(schedule), mimetype="application/json") + # NB this is JSON in a top-level array (security issue for low-end browsers) + return Response(json.dumps(schedule), mimetype="application/json") + + +@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) def duration_hhmm(duration_minutes): if not duration_minutes or duration_minutes < 1: From 550b857d1dbe4e50e75afd68e02b9eb24aee505e Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 17:26:14 +0200 Subject: [PATCH 06/19] move frab xml schedule to /schedule/frab-.xml --- apps/schedule/feeds.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 6fd325597..79ba7ee9a 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -1,7 +1,7 @@ import json from datetime import timedelta from icalendar import Calendar, Event -from flask import request, abort, current_app as app, Response +from flask import request, abort, current_app as app, redirect, url_for, Response from flask_cors import cross_origin from flask_login import current_user from math import ceil @@ -193,6 +193,10 @@ def duration_hhmm(duration_minutes): @schedule.route("/schedule/.frab") def schedule_frab(year): + return redirect(url_for('schedule_frab_xml', year=year), code=301) + +@schedule.route("/schedule/frab-.xml") +def schedule_frab_xml(year): if year != event_year(): return feed_historic(year, "frab") From 4d13889af4a76333d706ed017fd56fa0630aa4b9 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 17:31:39 +0200 Subject: [PATCH 07/19] schedule_frab_json: proposal.may_record has been replaced by proposal.video_privacy --- apps/schedule/feeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 79ba7ee9a..f1af54d53 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -139,7 +139,7 @@ def duration_hhmm(duration_minutes): "attachments": [], "date": event_tz.localize(proposal.start_date).isoformat(), "description": proposal.description, - "do_not_record": False if proposal.may_record else True, + "do_not_record": proposal.video_privacy != "public", "duration": duration_hhmm(proposal.duration_minutes), "guid": None, "id": proposal.id, From ee99a1a9e77b3ed715b6ac385a5e234e4ad0e844 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 17:34:54 +0200 Subject: [PATCH 08/19] schedule_json: re-add CORS header --- apps/schedule/feeds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index f1af54d53..03da52a42 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -45,6 +45,7 @@ def _format_event_description(event): @schedule.route("/schedule/.json") +@cross_origin(methods=["GET"]) def schedule_json(year): if year != event_year(): return feed_historic(year, "json") From b153466f0285d6879e015f78adfcbcf19f2e9cf9 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 17:49:58 +0200 Subject: [PATCH 09/19] schedule_frab_json: remove comment about tracks --- apps/schedule/feeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 03da52a42..57c12ff77 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -165,7 +165,7 @@ def duration_hhmm(duration_minutes): "start": event_tz.localize(proposal.start_date).strftime("%H:%M"), "subtitle": None, "title": proposal.display_title, - "track": None, # TODO does emf have tracks? + "track": None, "type": proposal.type, "url": external_url( ".item", From d375a70ae36a6afa86d23d46d0c92e0685c7b377 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 18:13:03 +0200 Subject: [PATCH 10/19] schedule_frab_json: use existing functions from schedule_xml --- apps/schedule/feeds.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 57c12ff77..b7a30df47 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -12,7 +12,7 @@ from models.cfp import Proposal from ..common import feature_flag, feature_enabled, json_response -from .schedule_xml import export_frab +from .schedule_xml import export_frab, get_day_start_end, get_duration from .historic import feed_historic from .data import ( _get_scheduled_proposals, @@ -67,14 +67,6 @@ def schedule_frab_json(year): if not feature_enabled("SCHEDULE"): abort(404) - def duration_hhmm(duration_minutes): - if not duration_minutes or duration_minutes < 1: - return "00:00" - return "{}:{}".format( - int(duration_minutes / 60), - str(duration_minutes % 60).zfill(2), - ) - schedule = ( Proposal.query.filter( Proposal.is_accepted, @@ -113,10 +105,11 @@ def duration_hhmm(duration_minutes): for day in range(0, duration_days): day_dt = event_start() + timedelta(days=day) + day_start, day_end = get_day_start_end(day_dt) day_schedule = { "date": day_dt.strftime("%Y-%m-%d"), - "day_end": (day_dt.replace(hour=3, minute=59, second=59) + timedelta(days=1)).isoformat(), - "day_start": day_dt.replace(hour=4, minute=0, second=0).isoformat(), + "day_end": day_start.isoformat(), + "day_start": day_end.isoformat(), "index": day, "rooms": {}, } @@ -141,7 +134,7 @@ def duration_hhmm(duration_minutes): "date": event_tz.localize(proposal.start_date).isoformat(), "description": proposal.description, "do_not_record": proposal.video_privacy != "public", - "duration": duration_hhmm(proposal.duration_minutes), + "duration": get_duration(proposal.start_date, proposal.end_date), "guid": None, "id": proposal.id, # This assumes there will never be a non-english talk, From c2e160379ac58feae01adbbc9c0f0e663426c1c2 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 18:53:36 +0200 Subject: [PATCH 11/19] move generation of frab-style json schedule to separate file --- apps/schedule/feeds.py | 111 +++------------------------- apps/schedule/schedule_json.py | 128 +++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 102 deletions(-) create mode 100644 apps/schedule/schedule_json.py diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index b7a30df47..19a72214e 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -1,18 +1,16 @@ import json -from datetime import timedelta from icalendar import Calendar, Event from flask import request, abort, current_app as app, redirect, url_for, Response from flask_cors import cross_origin from flask_login import current_user -from math import ceil -from main import external_url -from models import event_year, event_start, event_end +from models import event_year from models.user import User from models.cfp import Proposal from ..common import feature_flag, feature_enabled, json_response -from .schedule_xml import export_frab, get_day_start_end, get_duration +from .schedule_json import export_frab_json +from .schedule_xml import export_frab from .historic import feed_historic from .data import ( _get_scheduled_proposals, @@ -20,7 +18,7 @@ _convert_time_to_str, _get_upcoming, ) -from . import event_tz, schedule +from . import schedule def _format_event_description(event): @@ -37,9 +35,9 @@ def _format_event_description(event): venue_str = event["venue"] if event["map_link"]: venue_str = f'{venue_str} ({event["map_link"]})' - footer_block.append(f'Venue: {venue_str}') + footer_block.append(f"Venue: {venue_str}") if footer_block: - description += '\n\n' + '\n'.join(footer_block) + description += "\n\n" + "\n".join(footer_block) return description @@ -78,102 +76,10 @@ def schedule_frab_json(year): .all() ) - duration_days = ceil((event_end() - event_end()).total_seconds / 86400) - - rooms = [proposal.scheduled_venue.name for proposal in schedule] - - schedule_json = { - "version": "1.0-public", - "conference": { - "acronym": "emf{}".format(event_year()), - "days": [], - "daysCount": duration_days, - "end": event_end().strftime("%Y-%m-%d"), - "rooms": [ - { - "name": room, - } - for room in rooms - ], - "start": event_start().strftime("%Y-%m-%d"), - "time_zone_name": event_tz, - "timeslot_duration": "00:10", - "title": "Electromagnetic Field {}".format(event_year()), - "url": external_url("main"), - }, - } - - for day in range(0, duration_days): - day_dt = event_start() + timedelta(days=day) - day_start, day_end = get_day_start_end(day_dt) - day_schedule = { - "date": day_dt.strftime("%Y-%m-%d"), - "day_end": day_start.isoformat(), - "day_start": day_end.isoformat(), - "index": day, - "rooms": {}, - } - for room in rooms: - day_schedule["rooms"][room] = [] - for proposal in schedule: - if proposal.scheduled_venue.name != room: - # TODO find a better way to do that - continue - links = { - proposal.c3voc_url, - proposal.youtube_url, - proposal.thumbnail_url, - proposal.map_link, - } - links.discard(None) - links.discard("") - day_schedule["rooms"][room].append( - { - "abstract": None, # The proposal model does not implement abstracts - "attachments": [], - "date": event_tz.localize(proposal.start_date).isoformat(), - "description": proposal.description, - "do_not_record": proposal.video_privacy != "public", - "duration": get_duration(proposal.start_date, proposal.end_date), - "guid": None, - "id": proposal.id, - # This assumes there will never be a non-english talk, - # which is probably fine for a conference in the UK. - "language": "en", - "links": sorted(links), - "persons": [ - { - "name": name.strip(), - "public_name": name.strip(), - } - for name in (proposal.published_names or proposal.user.name).split(",") - ], - "recording_license": "CC BY-SA 3.0", - "room": room, - "slug": "emf{}-{}-{}".format( - event_year(), - proposal.id, - proposal.slug, - ), - "start": event_tz.localize(proposal.start_date).strftime("%H:%M"), - "subtitle": None, - "title": proposal.display_title, - "track": None, - "type": proposal.type, - "url": external_url( - ".item", - year=event_year(), - proposal_id=proposal.id, - slug=proposal.slug, - ), - } - ) - schedule_json["conference"]["days"].append(day_schedule) - return Response( json.dumps( { - "schedule": schedule_json, + "schedule": export_frab_json(schedule), "$schema": "https://c3voc.de/schedule/schema.json", "generator": { "name": "emfcamp-website", @@ -187,7 +93,8 @@ def schedule_frab_json(year): @schedule.route("/schedule/.frab") def schedule_frab(year): - return redirect(url_for('schedule_frab_xml', year=year), code=301) + return redirect(url_for("schedule_frab_xml", year=year), code=301) + @schedule.route("/schedule/frab-.xml") def schedule_frab_xml(year): diff --git a/apps/schedule/schedule_json.py b/apps/schedule/schedule_json.py new file mode 100644 index 000000000..33b8661a1 --- /dev/null +++ b/apps/schedule/schedule_json.py @@ -0,0 +1,128 @@ +from datetime import timedelta +from math import ceil + +from main import external_url +from models import event_end, event_start, event_year + +from . import event_tz +from .schedule_xml import get_day_start_end, get_duration + + +def events_per_day_and_room(schedule): + days = { + current_date.date(): { + "index": index + 1, + "start": get_day_start_end(event_start() + timedelta(days=index))[0], + "end": get_day_start_end(event_start() + timedelta(days=index))[1], + "rooms": {}, + } + for index, current_date in enumerate( + event_start() + timedelta(days=i) for i in range((event_end() - event_start()).days + 1) + ) + } + + for proposal in schedule: + talk_date = proposal.start_date.date() + if proposal.start_date.hour < 4 and talk_date != event_start().date(): + talk_date -= timedelta(days=1) + if talk_date not in days: + # Event is outside the scheduled event duration. + continue + if proposal.scheduled_venue.name not in days[talk_date]: + days[talk_date][proposal.scheduled_venue.name] = [proposal] + else: + days[talk_date][proposal.scheduled_venue.name].append(proposal) + + return days.values() + + +def export_frab_json(schedule): + duration_days = ceil((event_end() - event_end()).total_seconds / 86400) + + rooms = set([proposal.scheduled_venue.name for proposal in schedule]) + + schedule_json = { + "version": "1.0-public", + "conference": { + "acronym": "emf{}".format(event_year()), + "days": [], + "daysCount": duration_days, + "end": event_end().strftime("%Y-%m-%d"), + "rooms": [ + { + "name": room, + } + for room in sorted(rooms) + ], + "start": event_start().strftime("%Y-%m-%d"), + "time_zone_name": event_tz, + "timeslot_duration": "00:10", + "title": "Electromagnetic Field {}".format(event_year()), + "url": external_url("main"), + }, + } + + for day in events_per_day_and_room: + day_schedule = { + "date": day["start"].strftime("%Y-%m-%d"), + "day_end": day["start"].isoformat(), + "day_start": day["end"].isoformat(), + "index": day["index"], + "rooms": {}, + } + for room, events in sorted(day["rooms"].items()): + day_schedule["rooms"][room] = [] + for proposal in events: + links = { + proposal.c3voc_url, + proposal.youtube_url, + proposal.thumbnail_url, + proposal.map_link, + } + links.discard(None) + links.discard("") + day_schedule["rooms"][room].append( + { + "abstract": None, # The proposal model does not implement abstracts + "attachments": [], + "date": event_tz.localize(proposal.start_date).isoformat(), + "description": proposal.description, + "do_not_record": proposal.video_privacy != "public", + "duration": get_duration(proposal.start_date, proposal.end_date), + "guid": None, + "id": proposal.id, + # This assumes there will never be a non-english talk, + # which is probably fine for a conference in the UK. + "language": "en", + "links": sorted(links), + "persons": [ + { + "name": name.strip(), + "public_name": name.strip(), + } + for name in (proposal.published_names or proposal.user.name).split(",") + ], + "recording_license": "CC BY-SA 3.0", + "room": room, + "slug": "emf{}-{}-{}".format( + event_year(), + proposal.id, + proposal.slug, + ), + "start": event_tz.localize(proposal.start_date).strftime("%H:%M"), + "subtitle": None, + "title": proposal.display_title, + # Contrary to the infobeamer frab module, the json module does not allow users to set colours + # for tracks themselves. It instead relies on the schedule itself to provide those colours. + "track": None, + "type": proposal.type, + "url": external_url( + ".item", + year=event_year(), + proposal_id=proposal.id, + slug=proposal.slug, + ), + } + ) + schedule_json["conference"]["days"].append(day_schedule) + return schedule_json From e77a8e789827970bb1cec6359a52c2a6c9ff0adc Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 20:20:36 +0200 Subject: [PATCH 12/19] export_db: adjust for changed frab xml endpoint and added frab json endpoint --- apps/base/tasks_export.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/base/tasks_export.py b/apps/base/tasks_export.py index 46777248d..263fec8b0 100644 --- a/apps/base/tasks_export.py +++ b/apps/base/tasks_export.py @@ -131,8 +131,13 @@ def export_db(table): return with app.test_client() as client: - for file_type in ["frab", "json", "ics"]: - url = f"/schedule/{year}.{file_type}" + for file_type, file_url in ( + ("frab", f"frab-{year}.xml"), + ("frab_json", f"frab-{year}.json"), + ("json", f"{year}.json"), + ("ics", f"{year}.ics"), + ): + url = f"/schedule/{file_url}" dest_path = os.path.join(path, "public", f"schedule.{file_type}") response = client.get(url) if response.status_code != 200: From c499dca605e4801e7e1aed27ffb4963051713aa0 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 20:21:04 +0200 Subject: [PATCH 13/19] lineup_talk_redirect: frab xml has new function name --- apps/schedule/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/schedule/__init__.py b/apps/schedule/__init__.py index cd83e938f..1f9166e4a 100644 --- a/apps/schedule/__init__.py +++ b/apps/schedule/__init__.py @@ -57,7 +57,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", } From d41b13b83b5e4ca4748788d24a7b960e04eb167b Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Wed, 14 Aug 2024 20:21:37 +0200 Subject: [PATCH 14/19] templates/schedule: mention frab xml and frab json export --- templates/schedule/line-up.html | 4 ++++ templates/schedule/user_schedule.html | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/schedule/line-up.html b/templates/schedule/line-up.html index bd28e0c0b..82c1accbe 100644 --- a/templates/schedule/line-up.html +++ b/templates/schedule/line-up.html @@ -17,6 +17,10 @@

Line-up

You can also get this list as an iCal feed for your calendar, and a json feed for your giant robot.

+

If you use an app for viewing the schedule, you might be looking for a + Frab JSON or + Frab XML feed. +

{% endif %} {% else %}
diff --git a/templates/schedule/user_schedule.html b/templates/schedule/user_schedule.html index 74b6a67fb..ab3663784 100644 --- a/templates/schedule/user_schedule.html +++ b/templates/schedule/user_schedule.html @@ -14,7 +14,8 @@ Schedule feeds: JSON | iCal | - Frab + Frab JSON | + Frab XML

{% endblock %} {% block foot %} From 7f311c6f993eb4d4dc54b4ff78649c7d321f0d9a Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sat, 31 Aug 2024 14:22:31 +0200 Subject: [PATCH 15/19] apps.schedule.schedule_json: fix some mistakes --- apps/schedule/schedule_json.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/schedule/schedule_json.py b/apps/schedule/schedule_json.py index 33b8661a1..dde0c1bde 100644 --- a/apps/schedule/schedule_json.py +++ b/apps/schedule/schedule_json.py @@ -29,15 +29,15 @@ def events_per_day_and_room(schedule): # Event is outside the scheduled event duration. continue if proposal.scheduled_venue.name not in days[talk_date]: - days[talk_date][proposal.scheduled_venue.name] = [proposal] + days[talk_date]["rooms"][proposal.scheduled_venue.name] = [proposal] else: - days[talk_date][proposal.scheduled_venue.name].append(proposal) + days[talk_date]["rooms"][proposal.scheduled_venue.name].append(proposal) return days.values() def export_frab_json(schedule): - duration_days = ceil((event_end() - event_end()).total_seconds / 86400) + duration_days = ceil((event_end() - event_start()).total_seconds() / 86400) rooms = set([proposal.scheduled_venue.name for proposal in schedule]) @@ -55,14 +55,14 @@ def export_frab_json(schedule): for room in sorted(rooms) ], "start": event_start().strftime("%Y-%m-%d"), - "time_zone_name": event_tz, + "time_zone_name": str(event_tz), "timeslot_duration": "00:10", "title": "Electromagnetic Field {}".format(event_year()), - "url": external_url("main"), + "url": external_url(".main"), }, } - for day in events_per_day_and_room: + for day in events_per_day_and_room(schedule): day_schedule = { "date": day["start"].strftime("%Y-%m-%d"), "day_end": day["start"].isoformat(), From 96253c8544acf0cd4f40ac70d6976dd8a9ea3e08 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sat, 31 Aug 2024 21:44:22 +0200 Subject: [PATCH 16/19] apps.schedule: rename schedule_{json,xml} to schedule_frab_{json,xml} --- apps/schedule/feeds.py | 4 ++-- apps/schedule/{schedule_json.py => schedule_frab_json.py} | 2 +- apps/schedule/{schedule_xml.py => schedule_frab_xml.py} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename apps/schedule/{schedule_json.py => schedule_frab_json.py} (98%) rename apps/schedule/{schedule_xml.py => schedule_frab_xml.py} (100%) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index 19a72214e..cbefd670f 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -9,8 +9,8 @@ from models.cfp import Proposal from ..common import feature_flag, feature_enabled, json_response -from .schedule_json import export_frab_json -from .schedule_xml import export_frab +from .schedule_frab_json import export_frab_json +from .schedule_frab_xml import export_frab from .historic import feed_historic from .data import ( _get_scheduled_proposals, diff --git a/apps/schedule/schedule_json.py b/apps/schedule/schedule_frab_json.py similarity index 98% rename from apps/schedule/schedule_json.py rename to apps/schedule/schedule_frab_json.py index dde0c1bde..b873448e1 100644 --- a/apps/schedule/schedule_json.py +++ b/apps/schedule/schedule_frab_json.py @@ -5,7 +5,7 @@ from models import event_end, event_start, event_year from . import event_tz -from .schedule_xml import get_day_start_end, get_duration +from .schedule_frab_xml import get_day_start_end, get_duration def events_per_day_and_room(schedule): diff --git a/apps/schedule/schedule_xml.py b/apps/schedule/schedule_frab_xml.py similarity index 100% rename from apps/schedule/schedule_xml.py rename to apps/schedule/schedule_frab_xml.py From 28c3a8cde0c3b5519a8cf5814175dd82f198c48d Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sat, 31 Aug 2024 21:45:04 +0200 Subject: [PATCH 17/19] apps.schedule.feeds: move /schedule/frab-{year}.{json,xml} to /schedule/{year}.frab.{json,xml} --- apps/schedule/feeds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/schedule/feeds.py b/apps/schedule/feeds.py index cbefd670f..cfe1b3c82 100644 --- a/apps/schedule/feeds.py +++ b/apps/schedule/feeds.py @@ -57,7 +57,7 @@ def schedule_json(year): return Response(json.dumps(schedule), mimetype="application/json") -@schedule.route("/schedule/frab-.json") +@schedule.route("/schedule/.frab.json") def schedule_frab_json(year): if year != event_year(): return feed_historic(year, "frab_json") @@ -96,7 +96,7 @@ def schedule_frab(year): return redirect(url_for("schedule_frab_xml", year=year), code=301) -@schedule.route("/schedule/frab-.xml") +@schedule.route("/schedule/.frab.xml") def schedule_frab_xml(year): if year != event_year(): return feed_historic(year, "frab") From d015bb7d530aa8bec8654c9c645b695f9cebf19e Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sat, 31 Aug 2024 21:46:32 +0200 Subject: [PATCH 18/19] apps.schedule.schedule_frab_json: fix licence version --- apps/schedule/schedule_frab_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/schedule/schedule_frab_json.py b/apps/schedule/schedule_frab_json.py index b873448e1..c5c53acf7 100644 --- a/apps/schedule/schedule_frab_json.py +++ b/apps/schedule/schedule_frab_json.py @@ -102,7 +102,7 @@ def export_frab_json(schedule): } for name in (proposal.published_names or proposal.user.name).split(",") ], - "recording_license": "CC BY-SA 3.0", + "recording_license": "CC BY-SA 4.0", "room": room, "slug": "emf{}-{}-{}".format( event_year(), From 33ecbd2994ff1f1c5984c68cae998b88423ad547 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sat, 31 Aug 2024 21:51:06 +0200 Subject: [PATCH 19/19] tests/test_{frab_export,schedule}: fix imports of apps.schedule.schedule_frab_xml --- tests/test_frab_export.py | 2 +- tests/test_schedule.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_frab_export.py b/tests/test_frab_export.py index 640b31c84..382b0b4ed 100644 --- a/tests/test_frab_export.py +++ b/tests/test_frab_export.py @@ -3,7 +3,7 @@ from lxml import etree from apps.schedule import event_tz -from apps.schedule.schedule_xml import ( +from apps.schedule.schedule_frab_xml import ( make_root, add_day, add_room, diff --git a/tests/test_schedule.py b/tests/test_schedule.py index 18550113b..5a0118ce7 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 schedule_frab_xml @pytest.mark.parametrize('start_time, end_time, expected', [ @@ -17,4 +17,4 @@ def test_get_duration(start_time, end_time, expected): 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 schedule_frab_xml.get_duration(start_time, end_time) == expected