diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000000000000000000000000000000000000..2ebbf3b6729da7f1ed04187c8742007fb4e756d6 --- /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/models.py b/symposion/schedule/models.py index 0d7f642e9b47b3f763334247a3f767c5cd288f8b..a47afb9d909a3f4d169a76f6ac7ca1c188b3d7b6 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/tests/factories.py b/symposion/schedule/tests/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..ccddb58c070e9f3e876a949ccbc00494c9717e27 --- /dev/null +++ b/symposion/schedule/tests/factories.py @@ -0,0 +1,63 @@ +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 0000000000000000000000000000000000000000..dcb464556497ad74fed3fcb9f2cb2e9a83b0157c --- /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 0000000000000000000000000000000000000000..3bc2606b9719d01ec033603ccb6ec5769e9c95e7 --- /dev/null +++ b/symposion/schedule/tests/test_views.py @@ -0,0 +1,30 @@ +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 59e9b55f72846083dd0bd70d3398f1a02541733b..94acd3e1baf456be7e67e0a9788ec8df6e15e951 100644 --- a/symposion/schedule/urls.py +++ b/symposion/schedule/urls.py @@ -1,7 +1,6 @@ # flake8: noqa from django.conf.urls.defaults import url, patterns - urlpatterns = patterns("symposion.schedule.views", url(r"^$", "schedule_conference", name="schedule_conference"), url(r"^edit/$", "schedule_edit", name="schedule_edit"), @@ -13,4 +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", "schedule_json", name="schedule_json"), ) diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index 98771b597088f9913392131f2ee44d182f0ba16e..b03aa28a6bd8b831f53209211a056148711ed47a 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -1,8 +1,12 @@ +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 @@ -156,3 +160,49 @@ def schedule_presentation_detail(request, pk): "schedule": schedule, } return render(request, "schedule/presentation_detail.html", ctx) + + +def schedule_json(request): + 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: + slot_data = { + "room": ", ".join(room["name"] for room in slot.rooms.values()), + "rooms": [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, + "section": slot.day.schedule.section.slug, + } + if hasattr(slot.content, "proposal"): + slot_data.update({ + "name": slot.content.title, + "authors": [s.name for s in slot.content.speakers()], + "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, + "content_href": "%s://%s%s" % ( + protocol, + Site.objects.get_current().domain, + reverse("schedule_presentation_detail", args=[slot.content.pk]) + ), + "cancelled": slot.content.cancelled, + }) + else: + slot_data.update({ + "name": slot.content_override.raw if slot.content_override else "Slot", + }) + data.append(slot_data) + + return HttpResponse( + json.dumps({'schedule': data}), + content_type="application/json" + )