diff --git a/bornhack/settings/base.py b/bornhack/settings/base.py index 269257f9..3505ecfc 100644 --- a/bornhack/settings/base.py +++ b/bornhack/settings/base.py @@ -30,6 +30,7 @@ INSTALLED_APPS = [ 'news', 'utils', 'villages', + 'program', 'allauth', 'allauth.account', diff --git a/bornhack/static_src/css/bornhack.css b/bornhack/static_src/css/bornhack.css index 641b97d2..0cbb7cb3 100644 --- a/bornhack/static_src/css/bornhack.css +++ b/bornhack/static_src/css/bornhack.css @@ -119,3 +119,18 @@ footer { align-items: center; height: 200px; } + +.event { + max-width: 200px; + height: 100px; + display: inline-block; + margin: 5px 5px; + padding: 5px; + flex: 1 1 auto; +} + +.event:hover { + background-color: black !important; + color: white !important; + text-decoration: none; +} diff --git a/bornhack/urls.py b/bornhack/urls.py index 5cdd323b..433c95eb 100644 --- a/bornhack/urls.py +++ b/bornhack/urls.py @@ -78,6 +78,10 @@ urlpatterns = [ r'^villages/', include('villages.urls', namespace='villages') ), + url( + r'^program/', + include('program.urls', namespace='program') + ), url(r'^accounts/', include('allauth.urls')), url(r'^admin/', include(admin.site.urls)), ] diff --git a/camps/migrations/0006_auto_20160804_1705.py b/camps/migrations/0006_auto_20160804_1705.py new file mode 100644 index 00000000..8ae75891 --- /dev/null +++ b/camps/migrations/0006_auto_20160804_1705.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-04 17:05 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0005_auto_20160510_2011'), + ] + + operations = [ + migrations.AlterModelOptions( + name='day', + options={'ordering': ['date'], 'verbose_name': 'Day', 'verbose_name_plural': 'Days'}, + ), + ] diff --git a/camps/models.py b/camps/models.py index 881d225f..bcbeb731 100644 --- a/camps/models.py +++ b/camps/models.py @@ -60,6 +60,7 @@ class Day(CreatedUpdatedModel, UUIDModel): class Meta: verbose_name = _('Day') verbose_name_plural = _('Days') + ordering = ['date'] camp = models.ForeignKey( 'camps.Camp', @@ -73,6 +74,12 @@ class Day(CreatedUpdatedModel, UUIDModel): help_text=_('What date?') ) + def __str__(self): + return '{} ({})'.format( + self.date.strftime('%A'), + self.date + ) + class Expense(CreatedUpdatedModel, UUIDModel): class Meta: diff --git a/program/__init__.py b/program/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/program/admin.py b/program/admin.py new file mode 100644 index 00000000..5256637a --- /dev/null +++ b/program/admin.py @@ -0,0 +1,41 @@ +from django.contrib import admin + +from .models import Event, Speaker, EventType + + +@admin.register(EventType) +class EventTypeAdmin(admin.ModelAdmin): + pass + + +@admin.register(Speaker) +class SpeakerAdmin(admin.ModelAdmin): + pass + + +class SpeakerInline(admin.StackedInline): + model = Speaker.events.through + + +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + list_display = [ + 'title', + 'event_type', + 'get_days', + 'start', + 'end', + ] + + def get_days(self, obj): + return ', '.join([ + str(day.date.strftime('%a')) + for day in obj.days.all() + ]) + + inlines = [ + SpeakerInline + ] + + + diff --git a/program/apps.py b/program/apps.py new file mode 100644 index 00000000..42b3e8ae --- /dev/null +++ b/program/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class ProgramConfig(AppConfig): + name = 'program' diff --git a/program/migrations/0001_initial.py b/program/migrations/0001_initial.py new file mode 100644 index 00000000..ade6047f --- /dev/null +++ b/program/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-07-13 19:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('camps', '0005_auto_20160510_2011'), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('start', models.TimeField()), + ('end', models.TimeField()), + ('days', models.ManyToManyField(to='camps.Day')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EventType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Speaker', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=150)), + ('biography', models.TextField()), + ('picture', models.ImageField(blank=True, null=True, upload_to=b'')), + ('events', models.ManyToManyField(related_name='speakers', related_query_name='speaker', to='program.Event')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='event', + name='event_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='program.EventType'), + ), + ] diff --git a/program/migrations/0002_eventtype_color.py b/program/migrations/0002_eventtype_color.py new file mode 100644 index 00000000..b7f91fc3 --- /dev/null +++ b/program/migrations/0002_eventtype_color.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-04 17:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='eventtype', + name='color', + field=models.CharField(default='#ff0000', max_length=50), + preserve_default=False, + ), + ] diff --git a/program/migrations/0003_eventtype_light_writing.py b/program/migrations/0003_eventtype_light_writing.py new file mode 100644 index 00000000..7d4dac56 --- /dev/null +++ b/program/migrations/0003_eventtype_light_writing.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-04 17:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0002_eventtype_color'), + ] + + operations = [ + migrations.AddField( + model_name='eventtype', + name='light_writing', + field=models.BooleanField(default=False), + ), + ] diff --git a/program/migrations/0004_auto_20160804_1712.py b/program/migrations/0004_auto_20160804_1712.py new file mode 100644 index 00000000..e93a05c1 --- /dev/null +++ b/program/migrations/0004_auto_20160804_1712.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-04 17:12 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0003_eventtype_light_writing'), + ] + + operations = [ + migrations.RenameField( + model_name='eventtype', + old_name='light_writing', + new_name='light_text', + ), + ] diff --git a/program/migrations/0005_auto_20160807_1312.py b/program/migrations/0005_auto_20160807_1312.py new file mode 100644 index 00000000..ffe56c25 --- /dev/null +++ b/program/migrations/0005_auto_20160807_1312.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-07 13:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0004_auto_20160804_1712'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='slug', + field=models.SlugField(default='', blank=True), + preserve_default=False, + ), + migrations.AlterField( + model_name='speaker', + name='events', + field=models.ManyToManyField(blank=True, related_name='speakers', related_query_name='speaker', to='program.Event'), + ), + ] diff --git a/program/migrations/0006_auto_20160807_1320.py b/program/migrations/0006_auto_20160807_1320.py new file mode 100644 index 00000000..e26841e8 --- /dev/null +++ b/program/migrations/0006_auto_20160807_1320.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-07 13:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0005_auto_20160807_1312'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='slug', + field=models.SlugField(blank=True, max_length=255), + ), + ] diff --git a/program/migrations/0007_auto_20160807_1333.py b/program/migrations/0007_auto_20160807_1333.py new file mode 100644 index 00000000..fe9ba2a2 --- /dev/null +++ b/program/migrations/0007_auto_20160807_1333.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-08-07 13:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0006_auto_20160807_1320'), + ] + + operations = [ + migrations.RenameField( + model_name='event', + old_name='description', + new_name='abstract', + ), + ] diff --git a/program/migrations/__init__.py b/program/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/program/models.py b/program/models.py new file mode 100644 index 00000000..a1b5ba29 --- /dev/null +++ b/program/models.py @@ -0,0 +1,52 @@ +from __future__ import unicode_literals + +from django.db import models +from django.utils.text import slugify + +from utils.models import CreatedUpdatedModel + + +class EventType(CreatedUpdatedModel): + """ Every event needs to have a type. """ + name = models.CharField(max_length=100) + slug = models.SlugField() + color = models.CharField(max_length=50) + light_text = models.BooleanField(default=False) + + def __str__(self): + return self.name + + +class Event(CreatedUpdatedModel): + """ Something that is on the program. """ + title = models.CharField(max_length=255) + slug = models.SlugField(blank=True, max_length=255) + abstract = models.TextField() + event_type = models.ForeignKey(EventType) + days = models.ManyToManyField('camps.Day') + start = models.TimeField() + end = models.TimeField() + + def __str__(self): + return self.title + + def save(self, **kwargs): + if not self.slug: + self.slug = slugify(self.title) + super(Event, self).save(**kwargs) + + +class Speaker(CreatedUpdatedModel): + """ Person anchoring an event. """ + name = models.CharField(max_length=150) + biography = models.TextField() + picture = models.ImageField(null=True, blank=True) + events = models.ManyToManyField( + Event, + related_name='speakers', + related_query_name='speaker', + blank=True, + ) + + def __str__(self): + return self.name diff --git a/program/templates/program_base.html b/program/templates/program_base.html new file mode 100644 index 00000000..40987e9f --- /dev/null +++ b/program/templates/program_base.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block 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 %} +{% endfor %} + +
+ +{% block program_content %} +{% endblock %} + +{% endblock %} diff --git a/program/templates/program_day.html b/program/templates/program_day.html new file mode 100644 index 00000000..fab9de23 --- /dev/null +++ b/program/templates/program_day.html @@ -0,0 +1,22 @@ +{% extends 'program_base.html' %} + +{% block program_content %} + +{% 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" }} +
+ {{ event }} +
+ +{% endfor %} + +{% endblock %} diff --git a/program/templates/program_event_detail.html b/program/templates/program_event_detail.html new file mode 100644 index 00000000..91e955fa --- /dev/null +++ b/program/templates/program_event_detail.html @@ -0,0 +1,32 @@ +{% extends 'program_base.html' %} +{% load commonmark %} + +{% block program_content %} + +

+ + {{ event.event_type.name }} + + {{ event.title }} +

+ +

+ {{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }} at + {% for day in event.days.all %}{{ day.date|date:"l" }}{% if not forloop.last %}, {% endif %}{% endfor %}
+

+ +{{ event.abstract|commonmark }} + +
+ +{% if event.speakers.exists %} +{% for speaker in event.speakers.all %} + +

{{ speaker }}

+ {{ speaker.biography|commonmark }} + +{% endfor %} + +{% endif %} + +{% endblock %} diff --git a/program/templates/program_overview.html b/program/templates/program_overview.html new file mode 100644 index 00000000..d48630c8 --- /dev/null +++ b/program/templates/program_overview.html @@ -0,0 +1,32 @@ +{% extends 'program_base.html' %} + +{% block program_content %} + + + All + +{% for event_type in event_types %} + + {{ event_type.name }} + +{% endfor %} + +
+ +{% for day, events in day_events.items %} + {{ day.date|date:"D d/m" }}
+
+ {% for event in events %} + + {{ event.start|date:"H:i" }} - {{ event.end|date:"H:i" }} +
+ {{ event }} +
+ {% endfor %} +
+
+{% endfor %} + +{% endblock %} diff --git a/program/tests.py b/program/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/program/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/program/urls.py b/program/urls.py new file mode 100644 index 00000000..2871b5a2 --- /dev/null +++ b/program/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url +from . import views + +urlpatterns = [ + url(r'^(?P[-_\w+]+)/$', views.EventDetailView.as_view(), name='event'), + 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'), +] diff --git a/program/views.py b/program/views.py new file mode 100644 index 00000000..3cdba13e --- /dev/null +++ b/program/views.py @@ -0,0 +1,69 @@ +from collections import OrderedDict + +import datetime +from django.views.generic import ListView, TemplateView, DetailView + +from camps.models import Day +from . import models + + +class ProgramOverviewView(ListView): + model = models.Event + template_name = 'program_overview.html' + + def get_context_data(self, **kwargs): + context = super( + ProgramOverviewView, self + ).get_context_data(**kwargs) + + days = Day.objects.all() + context['days'] = days + + filter = {} + if 'type' in self.request.GET: + event_type = self.request.GET['type'] + filter["event_type__slug"] = event_type + + context['day_events'] = OrderedDict([ + ( + day, + self.get_queryset().filter( + days__in=[day], + **filter + ).order_by( + 'start' + ) + ) + for day in days + ]) + + context['event_types'] = models.EventType.objects.all() + + return context + + +class ProgramDayView(TemplateView): + template_name = 'program_day.html' + + def get_context_data(self, **kwargs): + context = super(ProgramDayView, self).get_context_data(**kwargs) + year = int(kwargs['year']) + month = int(kwargs['month']) + day = int(kwargs['day']) + date = datetime.date(year=year, month=month, day=day) + day = Day.objects.filter(date=date) + context['events'] = models.Event.objects.filter(days=day).order_by('start', 'event_type') + context['event_types'] = models.EventType.objects.all() + context['days'] = Day.objects.filter(date__year=year) + return context + + +class EventDetailView(DetailView): + model = models.Event + template_name = 'program_event_detail.html' + + def get_context_data(self, **kwargs): + context = super(EventDetailView, self).get_context_data(**kwargs) + # TODO: date__year is hardcoded here - need fix for 2017 :P + context['days'] = Day.objects.filter(date__year=2016) + return context