From c4db94b7e5fd60e1d22ece1c3e1abb783f8db2f2 Mon Sep 17 00:00:00 2001 From: Taavi Burns Date: Sun, 29 Sep 2013 17:02:01 -0400 Subject: [PATCH 1/5] Adds a schedule_json view which provides a /schedule/conference.json endpoint, of the kind that Carl uses for producing conference videos. Also useful to feed into mobile schedule apps! It is expected that someone might have to customize this for their own installation (PyCon Canada definitely did, with modifications to some of the models). --- symposion/schedule/models.py | 25 ++++++++++++++++++ symposion/schedule/urls.py | 4 +++ symposion/schedule/views.py | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/symposion/schedule/models.py b/symposion/schedule/models.py index 0d7f642e..7f8d0e25 100644 --- a/symposion/schedule/models.py +++ b/symposion/schedule/models.py @@ -1,3 +1,5 @@ +import datetime + from django.core.exceptions import ObjectDoesNotExist from django.db import models @@ -92,6 +94,29 @@ class Slot(models.Model): except ObjectDoesNotExist: return None + @property + def start_datetime(self): + return datetime.datetime( + self.day.date.year, + self.day.date.month, + self.day.date.day, + self.start.hour, + self.start.minute) + + @property + def end_datetime(self): + return datetime.datetime( + self.day.date.year, + self.day.date.month, + self.day.date.day, + self.end.hour, + self.end.minute) + + @property + def length_in_minutes(self): + return int( + (self.end_datetime - self.start_datetime).total_seconds() / 60) + @property def rooms(self): return Room.objects.filter(pk__in=self.slotroom_set.values("room")) diff --git a/symposion/schedule/urls.py b/symposion/schedule/urls.py index 59e9b55f..c25a84ba 100644 --- a/symposion/schedule/urls.py +++ b/symposion/schedule/urls.py @@ -1,5 +1,8 @@ # flake8: noqa from django.conf.urls.defaults import url, patterns +from django.views.decorators.cache import cache_page + +from symposion.schedule.views import schedule_json urlpatterns = patterns("symposion.schedule.views", @@ -13,4 +16,5 @@ urlpatterns = patterns("symposion.schedule.views", url(r"^([\w\-]+)/list/$", "schedule_list", name="schedule_list"), url(r"^([\w\-]+)/presentations.csv$", "schedule_list_csv", name="schedule_list_csv"), url(r"^([\w\-]+)/edit/slot/(\d+)/", "schedule_slot_edit", name="schedule_slot_edit"), + url(r"^conference.json", cache_page(300)(schedule_json), name="schedule_json"), ) diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index 98771b59..a4d65b65 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -156,3 +156,52 @@ def schedule_presentation_detail(request, pk): "schedule": schedule, } return render(request, "schedule/presentation_detail.html", ctx) + + +def schedule_json(request): + everything = bool(request.GET.get('everything')) + slots = Slot.objects.all().order_by("start") + data = [] + for slot in slots: + if slot.content: + slot_data = { + "name": slot.content.title, + "room": ", ".join(room["name"] for room in slot.rooms.values()), + "start": slot.start_datetime.isoformat(), + "end": slot.end_datetime.isoformat(), + "duration": slot.length_in_minutes, + "authors": [s.name for s in slot.content.speakers()], + "released": slot.content.proposal.recording_release, + # You may wish to change this... + "license": "All Rights Reserved", + "contact": + [s.email for s in slot.content.speakers()] + if request.user.is_staff + else ["redacted"], + "abstract": slot.content.abstract.raw, + "description": slot.content.description.raw, + "conf_key": slot.content.pk, + "conf_url": "https://%s%s" % ( + Site.objects.get_current().domain, + reverse("schedule_presentation_detail", args=[slot.content.pk]) + ), + "kind": slot.kind.label, + "tags": "", + } + elif everything: + slot_data = { + "room": ", ".join(room["name"] for room in slot.rooms.values()), + "start": slot.start_datetime.isoformat(), + "end": slot.end_datetime.isoformat(), + "duration": slot.length_in_minutes, + "kind": slot.kind.label, + "title": slot.content_override.raw, + } + else: + continue + data.append(slot_data) + + return HttpResponse( + json.dumps({'schedule': data}), + content_type="application/json" + ) From 51709c6eaf398c50cbeaf5032758e1c1c0c39438 Mon Sep 17 00:00:00 2001 From: Sheila Miguez Date: Sat, 20 Sep 2014 20:20:04 -0500 Subject: [PATCH 2/5] adds a schedule json endpoint. based on @taavi's PR #45 with some changes from the @pyohio/pyohio repo --- requirements-test.txt | 11 +++++ symposion/schedule/tests/factories.py | 62 +++++++++++++++++++++++ symposion/schedule/tests/runtests.py | 68 ++++++++++++++++++++++++++ symposion/schedule/tests/test_views.py | 31 ++++++++++++ symposion/schedule/urls.py | 6 +-- symposion/schedule/views.py | 58 +++++++++++----------- 6 files changed, 201 insertions(+), 35 deletions(-) create mode 100644 requirements-test.txt create mode 100644 symposion/schedule/tests/factories.py create mode 100755 symposion/schedule/tests/runtests.py create mode 100644 symposion/schedule/tests/test_views.py diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..2ebbf3b6 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,11 @@ +Django==1.4.15 +Pillow==2.5.3 +django-discover-runner==1.0 +django-markitup==2.2.2 +django-model-utils==1.5.0 +django-nose==1.2 +django-reversion==1.8.0 +django-timezones==0.2 +factory-boy==2.4.1 +nose==1.3.4 +pytz==2014.7 diff --git a/symposion/schedule/tests/factories.py b/symposion/schedule/tests/factories.py new file mode 100644 index 00000000..f4304e9a --- /dev/null +++ b/symposion/schedule/tests/factories.py @@ -0,0 +1,62 @@ +import datetime +import random + +import factory +from factory import fuzzy + +from symposion.schedule.models import Schedule, Day, Slot, SlotKind +from symposion.conference.models import Section, Conference + + +class ConferenceFactory(factory.DjangoModelFactory): + title = fuzzy.FuzzyText() + start_date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1)) + end_date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1) + datetime.timedelta(days=random.randint(1,10))) + #timezone = TimeZoneField("UTC") + + class Meta: + model = Conference + + +class SectionFactory(factory.DjangoModelFactory): + conference = factory.SubFactory(ConferenceFactory) + name = fuzzy.FuzzyText() + slug = fuzzy.FuzzyText() + + class Meta: + model = Section + + +class ScheduleFactory(factory.DjangoModelFactory): + section = factory.SubFactory(SectionFactory) + published = True + hidden = False + + class Meta: + model = Schedule + + +class SlotKindFactory(factory.DjangoModelFactory): + schedule = factory.SubFactory(ScheduleFactory) + label = fuzzy.FuzzyText() + + class Meta: + model = SlotKind + + +class DayFactory(factory.DjangoModelFactory): + schedule = factory.SubFactory(ScheduleFactory) + date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1)) + + class Meta: + model = Day + + +class SlotFactory(factory.DjangoModelFactory): + day = factory.SubFactory(DayFactory) + kind = factory.SubFactory(SlotKindFactory) + start = datetime.time(random.randint(0,23), random.randint(0,59)) + end = datetime.time(random.randint(0,23), random.randint(0,59)) + + class Meta: + model = Slot diff --git a/symposion/schedule/tests/runtests.py b/symposion/schedule/tests/runtests.py new file mode 100755 index 00000000..dcb46455 --- /dev/null +++ b/symposion/schedule/tests/runtests.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# see runtests.py in https://github.com/pydanny/cookiecutter-djangopackage + +import sys + +try: + from django.conf import settings + + settings.configure( + DEBUG=True, + USE_TZ=True, + DATABASES={ + "default": { + "ENGINE": "django.db.backends.sqlite3", + } + }, + ROOT_URLCONF="symposion.schedule.urls", + INSTALLED_APPS=[ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sites", + + "markitup", + "reversion", + + "symposion", + "symposion.conference", + "symposion.speakers", + "symposion.schedule", + "symposion.proposals", + + ], + SITE_ID=1, + NOSE_ARGS=['-s'], + + MARKITUP_FILTER=('django.contrib.markup.templatetags.markup.textile', {}), + ) + + try: + import django + setup = django.setup + except AttributeError: + pass + else: + setup() + + from django_nose import NoseTestSuiteRunner +except ImportError: + raise ImportError("To fix this error, run: pip install -r requirements-test.txt") + + +def run_tests(*test_args): + if not test_args: + test_args = ['tests'] + + # Run tests + test_runner = NoseTestSuiteRunner(verbosity=1) + + failures = test_runner.run_tests(test_args) + + if failures: + sys.exit(failures) + + +if __name__ == '__main__': + run_tests("symposion.schedule.tests.test_views") diff --git a/symposion/schedule/tests/test_views.py b/symposion/schedule/tests/test_views.py new file mode 100644 index 00000000..04e04efd --- /dev/null +++ b/symposion/schedule/tests/test_views.py @@ -0,0 +1,31 @@ +import json + +from django.test.client import Client +from django.test import TestCase + +from . import factories + + +class ScheduleViewTests(TestCase): + + def test_empty_json(self): + c = Client() + r = c.get('/conference.json') + assert r.status_code == 200 + + conference = json.loads(r.content) + assert 'schedule' in conference + assert len(conference['schedule']) == 0 + + + def test_populated_empty_presentations(self): + + factories.SlotFactory.create_batch(size=5) + + c = Client() + r = c.get('/conference.json') + assert r.status_code == 200 + + conference = json.loads(r.content) + assert 'schedule' in conference + assert len(conference['schedule']) == 5 diff --git a/symposion/schedule/urls.py b/symposion/schedule/urls.py index c25a84ba..94acd3e1 100644 --- a/symposion/schedule/urls.py +++ b/symposion/schedule/urls.py @@ -1,9 +1,5 @@ # flake8: noqa from django.conf.urls.defaults import url, patterns -from django.views.decorators.cache import cache_page - -from symposion.schedule.views import schedule_json - urlpatterns = patterns("symposion.schedule.views", url(r"^$", "schedule_conference", name="schedule_conference"), @@ -16,5 +12,5 @@ urlpatterns = patterns("symposion.schedule.views", url(r"^([\w\-]+)/list/$", "schedule_list", name="schedule_list"), url(r"^([\w\-]+)/presentations.csv$", "schedule_list_csv", name="schedule_list_csv"), url(r"^([\w\-]+)/edit/slot/(\d+)/", "schedule_slot_edit", name="schedule_slot_edit"), - url(r"^conference.json", cache_page(300)(schedule_json), name="schedule_json"), + url(r"^conference.json", "schedule_json", name="schedule_json"), ) diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index a4d65b65..aa043994 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -1,8 +1,13 @@ +from datetime import datetime +import json + +from django.core.urlresolvers import reverse from django.http import Http404, HttpResponse from django.shortcuts import render, get_object_or_404, redirect from django.template import loader, Context from django.contrib.auth.decorators import login_required +from django.contrib.sites.models import Site from symposion.schedule.forms import SlotEditForm from symposion.schedule.models import Schedule, Day, Slot, Presentation @@ -159,46 +164,39 @@ def schedule_presentation_detail(request, pk): def schedule_json(request): - everything = bool(request.GET.get('everything')) - slots = Slot.objects.all().order_by("start") + slots = Slot.objects.filter(day__schedule__published=True, day__schedule__hidden=False).order_by("start") + + protocol = request.META.get('HTTP_X_FORWARDED_PROTO', 'http') data = [] for slot in slots: - if slot.content: - slot_data = { + slot_data = { + "room": ", ".join(room["name"] for room in slot.rooms.values()), + "rooms": [room["name"] for room in slot.rooms.values()], + "start": datetime.combine(slot.day.date, slot.start).isoformat(), + "end": datetime.combine(slot.day.date, slot.end).isoformat(), + "duration": slot.length_in_minutes, + "kind": slot.kind.label, + "section": slot.day.schedule.section.slug, + } + if hasattr(slot.content, "proposal"): + slot_data.update({ "name": slot.content.title, - "room": ", ".join(room["name"] for room in slot.rooms.values()), - "start": slot.start_datetime.isoformat(), - "end": slot.end_datetime.isoformat(), - "duration": slot.length_in_minutes, "authors": [s.name for s in slot.content.speakers()], - "released": slot.content.proposal.recording_release, - # You may wish to change this... - "license": "All Rights Reserved", - "contact": - [s.email for s in slot.content.speakers()] - if request.user.is_staff - else ["redacted"], + "contact": [ + s.email for s in slot.content.speakers() + ] if request.user.is_staff else ["redacted"], "abstract": slot.content.abstract.raw, "description": slot.content.description.raw, - "conf_key": slot.content.pk, - "conf_url": "https://%s%s" % ( + "content_href": "%s://%s%s" % ( + protocol, Site.objects.get_current().domain, reverse("schedule_presentation_detail", args=[slot.content.pk]) ), - "kind": slot.kind.label, - "tags": "", - } - elif everything: - slot_data = { - "room": ", ".join(room["name"] for room in slot.rooms.values()), - "start": slot.start_datetime.isoformat(), - "end": slot.end_datetime.isoformat(), - "duration": slot.length_in_minutes, - "kind": slot.kind.label, - "title": slot.content_override.raw, - } + }) else: - continue + slot_data.update({ + "name": slot.content_override.raw if slot.content_override else "Slot", + }) data.append(slot_data) return HttpResponse( From 0ebcc2f1247ab9573afccab9436c39effaf063f4 Mon Sep 17 00:00:00 2001 From: Sheila Miguez Date: Sun, 21 Sep 2014 21:11:12 -0500 Subject: [PATCH 3/5] flake8 fixes --- symposion/schedule/models.py | 6 +++--- symposion/schedule/tests/factories.py | 9 +++++---- symposion/schedule/tests/test_views.py | 1 - symposion/schedule/views.py | 5 ++++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/symposion/schedule/models.py b/symposion/schedule/models.py index 7f8d0e25..a47afb9d 100644 --- a/symposion/schedule/models.py +++ b/symposion/schedule/models.py @@ -102,7 +102,7 @@ class Slot(models.Model): self.day.date.day, self.start.hour, self.start.minute) - + @property def end_datetime(self): return datetime.datetime( @@ -111,12 +111,12 @@ class Slot(models.Model): self.day.date.day, self.end.hour, self.end.minute) - + @property def length_in_minutes(self): return int( (self.end_datetime - self.start_datetime).total_seconds() / 60) - + @property def rooms(self): return Room.objects.filter(pk__in=self.slotroom_set.values("room")) diff --git a/symposion/schedule/tests/factories.py b/symposion/schedule/tests/factories.py index f4304e9a..ccddb58c 100644 --- a/symposion/schedule/tests/factories.py +++ b/symposion/schedule/tests/factories.py @@ -11,8 +11,9 @@ from symposion.conference.models import Section, Conference class ConferenceFactory(factory.DjangoModelFactory): title = fuzzy.FuzzyText() start_date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1)) - end_date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1) + datetime.timedelta(days=random.randint(1,10))) - #timezone = TimeZoneField("UTC") + end_date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1) + + datetime.timedelta(days=random.randint(1, 10))) + # timezone = TimeZoneField("UTC") class Meta: model = Conference @@ -55,8 +56,8 @@ class DayFactory(factory.DjangoModelFactory): class SlotFactory(factory.DjangoModelFactory): day = factory.SubFactory(DayFactory) kind = factory.SubFactory(SlotKindFactory) - start = datetime.time(random.randint(0,23), random.randint(0,59)) - end = datetime.time(random.randint(0,23), random.randint(0,59)) + start = datetime.time(random.randint(0, 23), random.randint(0, 59)) + end = datetime.time(random.randint(0, 23), random.randint(0, 59)) class Meta: model = Slot diff --git a/symposion/schedule/tests/test_views.py b/symposion/schedule/tests/test_views.py index 04e04efd..3bc2606b 100644 --- a/symposion/schedule/tests/test_views.py +++ b/symposion/schedule/tests/test_views.py @@ -17,7 +17,6 @@ class ScheduleViewTests(TestCase): assert 'schedule' in conference assert len(conference['schedule']) == 0 - def test_populated_empty_presentations(self): factories.SlotFactory.create_batch(size=5) diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index aa043994..a025834b 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -164,7 +164,10 @@ def schedule_presentation_detail(request, pk): def schedule_json(request): - slots = Slot.objects.filter(day__schedule__published=True, day__schedule__hidden=False).order_by("start") + slots = Slot.objects.filter( + day__schedule__published=True, + day__schedule__hidden=False + ).order_by("start") protocol = request.META.get('HTTP_X_FORWARDED_PROTO', 'http') data = [] From 2b91a7296ccf48fc33d0fbdbbbad8f60ad5c3d69 Mon Sep 17 00:00:00 2001 From: Sheila Miguez Date: Sun, 21 Sep 2014 21:25:18 -0500 Subject: [PATCH 4/5] added cancelled element to json, used taavis start/end props --- symposion/schedule/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index a025834b..204254df 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -175,8 +175,8 @@ def schedule_json(request): slot_data = { "room": ", ".join(room["name"] for room in slot.rooms.values()), "rooms": [room["name"] for room in slot.rooms.values()], - "start": datetime.combine(slot.day.date, slot.start).isoformat(), - "end": datetime.combine(slot.day.date, slot.end).isoformat(), + "start": slot.start_datetime.isoformat(), + "end": slot.end_datetime.isoformat(), "duration": slot.length_in_minutes, "kind": slot.kind.label, "section": slot.day.schedule.section.slug, @@ -195,6 +195,7 @@ def schedule_json(request): Site.objects.get_current().domain, reverse("schedule_presentation_detail", args=[slot.content.pk]) ), + "cancelled": slot.content.cancelled, }) else: slot_data.update({ From 40a55c24c7c7ca52bdd0ff547793efa06672631c Mon Sep 17 00:00:00 2001 From: Sheila Miguez Date: Sun, 21 Sep 2014 21:26:19 -0500 Subject: [PATCH 5/5] flake8 fix --- symposion/schedule/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index 204254df..b03aa28a 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -1,4 +1,3 @@ -from datetime import datetime import json from django.core.urlresolvers import reverse