diff --git a/bornhack/.env.dist b/bornhack/.env.dist index 4cce93ec..f485fd14 100644 --- a/bornhack/.env.dist +++ b/bornhack/.env.dist @@ -30,4 +30,7 @@ BANKACCOUNT_IBAN='123' BANKACCOUNT_SWIFTBIC='123' BANKACCOUNT_REG='123' BANKACCOUNT_ACCOUNT='123' +TIME_ZONE='Europe/Copenhagen' +SCHEDULE_MIDNIGHT_OFFSET_HOURS=6 +SCHEDULE_TIMESLOT_LENGTH_MINUTES=30 diff --git a/bornhack/settings.py b/bornhack/settings.py index 7c5d3050..55432775 100644 --- a/bornhack/settings.py +++ b/bornhack/settings.py @@ -54,10 +54,14 @@ STATIC_ROOT = local_dir('static') STATICFILES_DIRS = [local_dir('static_src')] MEDIA_ROOT = env('MEDIA_ROOT') LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_L10N = True +TIME_ZONE = env('TIME_ZONE') +#USE_I18N = True +#USE_L10N = True USE_TZ = True +SHORT_DATE_FORMAT = 'd/m-Y' +DATE_FORMAT = 'd/m-Y' +DATETIME_FORMAT = 'd/m-Y H:i' +TIME_FORMAT = 'H:i' TEMPLATES = [ { @@ -167,3 +171,7 @@ LOGGING = { } } +# schedule settings +SCHEDULE_MIDNIGHT_OFFSET_HOURS=int(env('SCHEDULE_MIDNIGHT_OFFSET_HOURS')) +SCHEDULE_TIMESLOT_LENGTH_MINUTES=int(env('SCHEDULE_TIMESLOT_LENGTH_MINUTES')) + diff --git a/bornhack/urls.py b/bornhack/urls.py index 3133c851..87455843 100644 --- a/bornhack/urls.py +++ b/bornhack/urls.py @@ -49,11 +49,6 @@ urlpatterns = [ TemplateView.as_view(template_name='sponsors.html'), name='call-for-sponsors' ), - url( - r'^speakers/', - TemplateView.as_view(template_name='speakers.html'), - name='call-for-speakers' - ), url( r'^login/$', LoginView.as_view(), @@ -123,9 +118,14 @@ urlpatterns = [ url( r'^(?P[-_\w+]+)/$', EventDetailView.as_view(), - name='event' + name='event_detail' ), - ]) + url( + r'^call-for-speakers/$', + CallForSpeakersView.as_view(), + name='call_for_speakers' + ), + ]) ), url( diff --git a/camps/models.py b/camps/models.py index 032addb5..52a6a41a 100644 --- a/camps/models.py +++ b/camps/models.py @@ -3,6 +3,9 @@ from django.db import models from utils.models import UUIDModel, CreatedUpdatedModel from program.models import EventType from django.contrib.postgres.fields import DateTimeRangeField +from psycopg2.extras import DateTimeTZRange +from django.core.exceptions import ValidationError +from datetime import timedelta class Camp(CreatedUpdatedModel, UUIDModel): @@ -42,6 +45,36 @@ class Camp(CreatedUpdatedModel, UUIDModel): help_text='The camp teardown period.', ) + def clean(self): + ''' Make sure the dates make sense - meaning no overlaps and buildup before camp before teardown ''' + errors = [] + # sanity checking for buildup + if self.buildup.lower > self.buildup.upper: + errors.append(ValidationError({'buildup', 'Start of buildup must be before end of buildup'})) + + # sanity checking for camp + if self.camp.lower > self.camp.upper: + errors.append(ValidationError({'camp', 'Start of camp must be before end of camp'})) + + # sanity checking for teardown + if self.teardown.lower > self.teardown.upper: + errors.append(ValidationError({'teardown', 'Start of teardown must be before end of teardown'})) + + # check for overlaps buildup vs. camp + if self.buildup.upper > self.camp.lower: + msg = "End of buildup must not be after camp start" + errors.append(ValidationError({'buildup', msg})) + errors.append(ValidationError({'camp', msg})) + + # check for overlaps camp vs. teardown + if self.camp.upper > self.teardown.lower: + msg = "End of camp must not be after teardown start" + errors.append(ValidationError({'camp', msg})) + errors.append(ValidationError({'teardown', msg})) + + if errors: + raise ValidationError(errors) + def __unicode__(self): return "%s - %s" % (self.title, self.tagline) @@ -58,3 +91,68 @@ class Camp(CreatedUpdatedModel, UUIDModel): def logo_large(self): return 'img/%(slug)s/logo/%(slug)s-logo-large.png' % {'slug': self.slug} + def get_days(self, camppart): + ''' + Returns a list of DateTimeTZRanges representing the days during the specified part of the camp. + ''' + if not hasattr(self, camppart): + print("nonexistant field/attribute") + return False + + field = getattr(self, camppart) + + if not hasattr(field, '__class__') or not hasattr(field.__class__, '__name__') or not field.__class__.__name__ == 'DateTimeTZRange': + print("this attribute is not a datetimetzrange field: %s" % field) + return False + + daycount = (field.upper - field.lower).days + days = [] + for i in range(0, daycount): + if i == 0: + # on the first day use actual start time instead of midnight + days.append( + DateTimeTZRange( + field.lower, + (field.lower+timedelta(days=i+1)).replace(hour=0) + ) + ) + elif i == daycount-1: + # on the last day use actual end time instead of midnight + days.append( + DateTimeTZRange( + (field.lower+timedelta(days=i)).replace(hour=0), + field.lower+timedelta(days=i+1) + ) + ) + else: + # neither first nor last day, goes from midnight to midnight + days.append( + DateTimeTZRange( + (field.lower+timedelta(days=i)).replace(hour=0), + (field.lower+timedelta(days=i+1)).replace(hour=0) + ) + ) + return days + + @property + def buildup_days(self): + ''' + Returns a list of DateTimeTZRanges representing the days during the buildup. + ''' + return self.get_days('buildup') + + @property + def camp_days(self): + ''' + Returns a list of DateTimeTZRanges representing the days during the camp. + ''' + return self.get_days('camp') + + @property + def teardown_days(self): + ''' + Returns a list of DateTimeTZRanges representing the days during the buildup. + ''' + return self.get_days('teardown') + + diff --git a/program/admin.py b/program/admin.py index 1e2d908c..af5b07e9 100644 --- a/program/admin.py +++ b/program/admin.py @@ -1,8 +1,12 @@ from django.contrib import admin -from .models import Event, Speaker, EventType +from .models import Event, Speaker, EventType, EventInstance +@admin.register(EventInstance) +class EventInstanceAdmin(admin.ModelAdmin): + pass + @admin.register(EventType) class EventTypeAdmin(admin.ModelAdmin): pass diff --git a/program/migrations/0013_auto_20170121_1312.py b/program/migrations/0013_auto_20170121_1312.py new file mode 100644 index 00000000..1784940f --- /dev/null +++ b/program/migrations/0013_auto_20170121_1312.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-21 12:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0012_auto_20161229_2150'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='camp', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='camps.Camp'), + ), + ] diff --git a/program/models.py b/program/models.py index 7bfa4956..b4231d30 100644 --- a/program/models.py +++ b/program/models.py @@ -2,8 +2,10 @@ from __future__ import unicode_literals from django.contrib.postgres.fields import DateTimeRangeField from django.db import models from django.utils.text import slugify - +from django.conf import settings from utils.models import CreatedUpdatedModel +from django.core.exceptions import ValidationError +from datetime import timedelta class EventType(CreatedUpdatedModel): @@ -23,13 +25,13 @@ class Event(CreatedUpdatedModel): slug = models.SlugField(blank=True, max_length=255) abstract = models.TextField() event_type = models.ForeignKey(EventType) - camp = models.ForeignKey('camps.Camp', null=True) + camp = models.ForeignKey('camps.Camp', null=True, related_name="events") class Meta: ordering = ['title'] def __unicode__(self): - return self.title + return '%s (%s)' % (self.title, self.camp.title) def save(self, **kwargs): if not self.slug: @@ -48,6 +50,39 @@ class EventInstance(CreatedUpdatedModel): def __unicode__(self): return '%s (%s)' % (self.event, self.when) + def __clean__(self): + errors = [] + if self.when.lower > self.when.upper: + errors.append(ValidationError({'when', "Start should be earlier than finish"})) + + if self.when.lower.time().minute != 0 and self.when.lower.time().minute != 30: + errors.append(ValidationError({'when', "Start time minute should be 0 or 30."})) + + if self.when.upper.time().minute != 0 and self.when.upper.time().minute != 30: + errors.append(ValidationError({'when', "End time minute should be 0 or 30."})) + + if errors: + raise ValidationError(errors) + + @property + def schedule_date(self): + """ + Returns the schedule date of this eventinstance. Schedule date is determined by substracting + settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS from the eventinstance start time. This means that if + an event is scheduled for 00:30 wednesday evening (technically thursday) then the date + after substracting 5 hours would be wednesdays date, not thursdays. + """ + return (self.when.lower-timedelta(hours=settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS)).date() + + @property + def timeslots(self): + """ + Find the number of timeslots this eventinstance takes up + """ + seconds = (self.when.upper-self.when.lower).seconds + minutes = seconds / 60 + return minutes / settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES + class Speaker(CreatedUpdatedModel): """ A Person anchoring an event. """ diff --git a/templates/speakers.html b/program/templates/call_for_speakers_bornhack-2016.html similarity index 100% rename from templates/speakers.html rename to program/templates/call_for_speakers_bornhack-2016.html diff --git a/program/templates/event_list.html b/program/templates/event_list.html index 5c889534..2fe49aea 100644 --- a/program/templates/event_list.html +++ b/program/templates/event_list.html @@ -9,7 +9,7 @@
{% for event in event_list %} {% if event.event_type.name != "Facilities" %} - + {{ event.event_type.name }} diff --git a/program/templates/program_base.html b/program/templates/program_base.html index 03f61294..ab1615df 100644 --- a/program/templates/program_base.html +++ b/program/templates/program_base.html @@ -2,17 +2,15 @@ {% block schedule_content %} - - Overview - -{% for day in days %} -{% with day.date|date:"m" as month_padded %} -{% with day.date|date:"d" as day_padded %} - - {{ day.date|date:"l" }} - -{% endwith %} -{% endwith %} +Overview +{% for day in camp.camp_days %} + {% with day.lower.date|date:"m" as month_padded %} + {% with day.lower.date|date:"d" as day_padded %} + + {{ day.lower.date|date:"l" }} + + {% endwith %} + {% endwith %} {% endfor %}
diff --git a/program/templates/program_day.html b/program/templates/program_day.html index ea0b4133..b032a4c0 100644 --- a/program/templates/program_day.html +++ b/program/templates/program_day.html @@ -3,17 +3,14 @@ {% block program_content %}

{{ date|date:"l, F jS" }}

{% for event in events %} - {% ifchanged event.event_type %} - {% if not forloop.first %}
{% endif %}

{{ event.event_type }}

- {% endifchanged %} {{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }}
@@ -25,6 +22,27 @@
{% endfor %} -
+ + + +
+ {% for timeslot in timeslots %} + + + {% for eventinstance in eventinstances %} + {% if eventinstance.when.lower.time == timeslot.time %} + + {% endif %} + {% endfor %} + + {% endfor %} +
{{ timeslot.time }} + + {{ eventinstance.event.title }}
+ {{ eventinstance.when.lower.time }}-{{ eventinstance.when.upper.time }} +
+
+ + {% endblock program_content %} diff --git a/program/templates/program_event_detail.html b/program/templates/program_event_detail.html index d6e71118..191e56ea 100644 --- a/program/templates/program_event_detail.html +++ b/program/templates/program_event_detail.html @@ -26,7 +26,7 @@ Not scheduled yet {% if event.speakers.exists %} {% for speaker in event.speakers.all %} -

{{ speaker }}

+

{{ speaker }}

{{ speaker.biography|commonmark }} {% endfor %} diff --git a/program/templates/program_overview.html b/program/templates/program_overview.html index acbc30fe..865dfa67 100644 --- a/program/templates/program_overview.html +++ b/program/templates/program_overview.html @@ -1,36 +1,37 @@ {% extends 'program_base.html' %} {% block program_content %} - + All {% for event_type in camp.event_types %} - + {{ event_type.name }} {% endfor %}
-{% for day, events in day_events.items %} - {{ day.date|date:"D d/m" }}
+{% for day in camp.camp_days %} + {{ day.lower.date|date:"D d/m" }}
- {% for event in events %} + {% for event in camp.events.all %} + {% for eventinstance in event.instances.all %} + {% if eventinstance.schedule_date == day.lower.date %} - {{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }} + href="{% url 'event_detail' camp_slug=camp.slug slug=eventinstance.event.slug %}" + style="background-color: {{ eventinstance.event.event_type.color }}; border: 0; color: {% if eveninstance.event.event_type.light_text %}white{% else %}black{% endif %};"> + {{ eventinstance.when.lower|date:"H:i" }} - {{ eventinstance.when.upper|date:"H:i" }}
{{ event }}
- {% if event.speakers.exists %} - by {{ event.speakers.all|join:", " }} - {% endif %} - + {% if event.speakers.exists %}by {{ event.speakers.all|join:", " }}{% endif %}
- {% endfor %} + {% endif %} + {% endfor %} + {% endfor %}

-{% endfor %} + {% endfor %} {% endblock program_content %} diff --git a/program/templates/schedule_base.html b/program/templates/schedule_base.html index f74402ce..79b90e76 100644 --- a/program/templates/schedule_base.html +++ b/program/templates/schedule_base.html @@ -1,11 +1,14 @@ {% extends 'base.html' %} {% block content %} + +
+

- Schedule - Call for Speakers - Speakers - Talks & Events + Schedule + Call for Speakers + Speakers + Talks & Events


diff --git a/program/templates/speaker_detail.html b/program/templates/speaker_detail.html index 0f4725cd..16a1968f 100644 --- a/program/templates/speaker_detail.html +++ b/program/templates/speaker_detail.html @@ -15,7 +15,7 @@ {{ event.event_type.name }}
-{{ event.title }} +{{ event.title }} {{ event.abstract|commonmark }} {% if event.start and event.end and event.days.all.exists %} At {{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }} on diff --git a/program/templates/speaker_list.html b/program/templates/speaker_list.html index 30dfbf1a..c5cbbc40 100644 --- a/program/templates/speaker_list.html +++ b/program/templates/speaker_list.html @@ -8,7 +8,7 @@
{% for speaker in speaker_list %} - + {{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }}) {% endfor %} diff --git a/program/urls.py b/program/urls.py deleted file mode 100644 index bc311055..00000000 --- a/program/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf.urls import url -from . import views - -urlpatterns = [ - url(r'^(?P\d{4})-(?P\d{2})-(?P\d{2})/$', views.ProgramDayView.as_view(), name='day'), - url(r'^$', views.ProgramOverviewView.as_view(), name='index'), - url(r'^speakers/$', views.SpeakerListView.as_view(), name='speaker_index'), - url(r'^speakers/(?P[-_\w+]+)/$', views.SpeakerDetailView.as_view(), name='speaker_detail'), - url(r'^events/$', views.EventListView.as_view(), name='event_index'), - url(r'^(?P[-_\w+]+)/$', views.EventDetailView.as_view(), name='event'), -] diff --git a/program/views.py b/program/views.py index 12398281..120a43ad 100644 --- a/program/views.py +++ b/program/views.py @@ -1,10 +1,11 @@ from collections import OrderedDict - import datetime from django.views.generic import ListView, TemplateView, DetailView from camps.mixins import CampViewMixin - from . import models +from django.http import Http404 +import datetime +from django.conf import settings class SpeakerDetailView(CampViewMixin, DetailView): @@ -29,6 +30,47 @@ class ProgramOverviewView(CampViewMixin, ListView): class ProgramDayView(CampViewMixin, TemplateView): template_name = 'program_day.html' + def dispatch(self, *args, **kwargs): + """ If an event type has been supplied check if it is valid """ + if 'type' in self.request.GET: + try: + eventtype = EventType.objects.get( + slug=self.request.GET['type'] + ) + except EventType.DoesNotExist: + raise Http404 + return super(ProgramDayView, self).dispatch(*args, **kwargs) + + def get_context_data(self, *args, **kwargs): + context = super(ProgramDayView, self).get_context_data(**kwargs) + when = datetime.datetime(year=int(self.kwargs['year']), month=int(self.kwargs['month']), day=int(self.kwargs['day'])) + eventinstances = models.EventInstance.objects.filter(event__in=self.camp.events.all()) + skip = [] + for ei in eventinstances: + if ei.schedule_date != when.date(): + print "skipping ei %s (wrong date %s vs %s)" % (ei, ei.schedule_date, when.date()) + skip.append(ei.id) + else: + if 'type' in self.request.GET: + eventtype = EventType.objects.get( + slug=self.request.GET['type'] + ) + if ei.event.event_type != eventtype: + print "skipping ei %s (wrong type)" % ei + skip.append(ei.id) + print "skipping %s" % skip + context['eventinstances'] = eventinstances.exclude(id__in=skip).order_by('event__event_type') + + start = when + datetime.timedelta(hours=settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS) + timeslots = [] + # calculate how many timeslots we have in the schedule based on the lenght of the timeslots in minutes, + # and the number of minutes in 24 hours + for i in range(0,(24*60)/settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES): + timeslot = start + datetime.timedelta(minutes=i*settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES) + timeslots.append(timeslot) + context['timeslots'] = timeslots + + return context class EventDetailView(CampViewMixin, DetailView): @@ -36,3 +78,8 @@ class EventDetailView(CampViewMixin, DetailView): template_name = 'program_event_detail.html' +class CallForSpeakersView(CampViewMixin, TemplateView): + def get_template_names(self): + return 'call_for_speakers_%s.html' % self.get_object().slug + + diff --git a/static_src/css/bornhack.css b/static_src/css/bornhack.css index c549c03b..cf2e9e21 100644 --- a/static_src/css/bornhack.css +++ b/static_src/css/bornhack.css @@ -105,13 +105,8 @@ footer { } .event { - max-width: 200px; width: 200px; - height: 150px; - display: inline-block; - margin: 5px 5px; padding: 5px; - flex: 1 1 auto; } .event:hover { @@ -119,3 +114,4 @@ footer { color: white !important; text-decoration: none; } +