commit before start of work sunday, mostly fixing schedule stuff, including setting settings.TIME_ZONE properly, this means we will need to convert production data times to be UTC

This commit is contained in:
Thomas Steen Rasmussen 2017-01-22 12:59:57 +01:00
parent 125d433860
commit 518611534e
19 changed files with 291 additions and 70 deletions

View file

@ -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

View file

@ -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'))

View file

@ -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<slug>[-_\w+]+)/$',
EventDetailView.as_view(),
name='event'
name='event_detail'
),
])
url(
r'^call-for-speakers/$',
CallForSpeakersView.as_view(),
name='call_for_speakers'
),
])
),
url(

View file

@ -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')

View file

@ -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

View file

@ -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'),
),
]

View file

@ -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. """

View file

@ -9,7 +9,7 @@
<div class="list-group">
{% for event in event_list %}
{% if event.event_type.name != "Facilities" %}
<a href="{% url 'schedule:event' slug=event.slug %}" class="list-group-item">
<a href="{% url 'event_detail' camp_slug=camp.slug slug=event.slug %}" class="list-group-item">
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }}
</small>

View file

@ -2,17 +2,15 @@
{% block schedule_content %}
<a href="{% url 'schedule:index' %}" class="btn btn-default" style="display: inline-block; padding: 5px;">
Overview
</a>
{% for day in days %}
{% with day.date|date:"m" as month_padded %}
{% with day.date|date:"d" as day_padded %}
<a href="{% url 'schedule:day' year=day.date.year month=month_padded day=day_padded %}" class="btn btn-default" style="display: inline-block; padding: 5px;">
{{ day.date|date:"l" }}
</a>
{% endwith %}
{% endwith %}
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" class="btn btn-default" style="display: inline-block; padding: 5px;">Overview</a>
{% for day in camp.camp_days %}
{% with day.lower.date|date:"m" as month_padded %}
{% with day.lower.date|date:"d" as day_padded %}
<a href="{% url 'schedule_day' camp_slug=camp.slug year=day.lower.date.year month=month_padded day=day_padded %}" class="btn btn-default" style="display: inline-block; padding: 5px;">
{{ day.lower.date|date:"l" }}
</a>
{% endwith %}
{% endwith %}
{% endfor %}
<hr />

View file

@ -3,17 +3,14 @@
{% block program_content %}
<h2>{{ date|date:"l, F jS" }}</h2>
{% for event in events %}
{% ifchanged event.event_type %}
{% if not forloop.first %}</div>{% endif %}
<h3>{{ event.event_type }}</h3>
<div style="display: flex; flex-wrap: wrap;">
{% endifchanged %}
<a class="event"
href="{% url 'schedule:event' slug=event.slug %}"
href="{% url 'event_detail' camp_slut=camp.slug slug=event.slug %}"
style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %};">
<small>{{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }}</small>
<br />
@ -25,6 +22,27 @@
</a>
{% endfor %}
</div>
<table>
<tbody>
<table>
{% for timeslot in timeslots %}
<tr>
<td style="height: 50px; padding: 5px;">{{ timeslot.time }}</td>
{% for eventinstance in eventinstances %}
{% if eventinstance.when.lower.time == timeslot.time %}
<td style="background-color: {{ eventinstance.event.event_type.color }}; color: {% if event.event_type.light_text %}white{% else %}black{% endif %};" class="event" rowspan={{ eventinstance.timeslots }}>
<a style="color:inherit;" href="{% url 'event_detail' camp_slug=camp.slug slug=eventinstance.event.slug %}">
{{ eventinstance.event.title }}<br>
{{ eventinstance.when.lower.time }}-{{ eventinstance.when.upper.time }}
</a>
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
</tbody>
</table>
{% endblock program_content %}

View file

@ -26,7 +26,7 @@ Not scheduled yet
{% if event.speakers.exists %}
{% for speaker in event.speakers.all %}
<h3><a href="{% url 'schedule:speaker_detail' slug=speaker.slug %}">{{ speaker }}</a></h3>
<h3><a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}">{{ speaker }}</a></h3>
{{ speaker.biography|commonmark }}
{% endfor %}

View file

@ -1,36 +1,37 @@
{% extends 'program_base.html' %}
{% block program_content %}
<a href="{% url 'schedule:index' %}" style="background-color: black; border: 0; color: white; display: inline-block; padding: 5px;">
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" style="background-color: black; border: 0; color: white; display: inline-block; padding: 5px;">
All
</a>
{% for event_type in camp.event_types %}
<a href="{% url 'schedule_index' %}?type={{ event_type.slug }}" style="background-color: {{ event_type.color }}; border: 0; color: {% if event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
<a href="{% url 'schedule_index' camp_slug=camp.slug %}?type={{ event_type.slug }}" style="background-color: {{ event_type.color }}; border: 0; color: {% if event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event_type.name }}
</a>
{% endfor %}
<hr />
{% for day, events in day_events.items %}
{{ day.date|date:"D d/m" }} <br />
{% for day in camp.camp_days %}
{{ day.lower.date|date:"D d/m" }} <br />
<div style="display: flex; flex-wrap: wrap;">
{% for event in events %}
{% for event in camp.events.all %}
{% for eventinstance in event.instances.all %}
{% if eventinstance.schedule_date == day.lower.date %}
<a class="event"
href="{% url 'schedule:event' slug=event.slug %}"
style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %};">
<small>{{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }}</small>
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 %};">
<small>{{ eventinstance.when.lower|date:"H:i" }} - {{ eventinstance.when.upper|date:"H:i" }}</small>
<br />
{{ event }}
<br />
{% if event.speakers.exists %}
<i>by {{ event.speakers.all|join:", " }}
{% endif %}</i>
{% if event.speakers.exists %}<i>by {{ event.speakers.all|join:", " }}{% endif %}</i>
</a>
{% endfor %}
{% endif %}
{% endfor %}
{% endfor %}
</div>
<hr />
{% endfor %}
{% endfor %}
{% endblock program_content %}

View file

@ -1,11 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<hr />
<p>
<a href="{% url 'schedule:index' %}" class="btn btn-default">Schedule</a>
<a href="{% url 'call-for-speakers' %}" class="btn btn-default">Call for Speakers</a>
<a href="{% url 'schedule:speaker_index' %}" class="btn btn-default">Speakers</a>
<a href="{% url 'schedule:event_index' %}" class="btn btn-default">Talks &amp; Events</a>
<a href="{% url 'schedule_index' camp_slug=camp.slug %}" class="btn btn-default">Schedule</a>
<a href="{% url 'call_for_speakers' camp_slug=camp.slug %}" class="btn btn-default">Call for Speakers</a>
<a href="{% url 'speaker_index' camp_slug=camp.slug %}" class="btn btn-default">Speakers</a>
<a href="{% url 'event_index' camp_slug=camp.slug %}" class="btn btn-default">Talks &amp; Events</a>
</p>
<hr />

View file

@ -15,7 +15,7 @@
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }}
</small><br>
<a href="{% url 'schedule:event' slug=event.slug %}">{{ event.title }}</a></h3>
<a href="{% url 'event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a></h3>
{{ 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

View file

@ -8,7 +8,7 @@
<div class="list-group">
{% for speaker in speaker_list %}
<a href="{% url 'schedule:speaker_detail' slug=speaker.slug %}" class="list-group-item">
<a href="{% url 'speaker_detail' camp_slug=camp.slug slug=speaker.slug %}" class="list-group-item">
{{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }})
</a>
{% endfor %}

View file

@ -1,11 +0,0 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\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<slug>[-_\w+]+)/$', views.SpeakerDetailView.as_view(), name='speaker_detail'),
url(r'^events/$', views.EventListView.as_view(), name='event_index'),
url(r'^(?P<slug>[-_\w+]+)/$', views.EventDetailView.as_view(), name='event'),
]

View file

@ -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

View file

@ -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;
}