From ad3b826844f41011499225c138a74d4016e0640a Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 12 Mar 2017 15:43:41 +0100 Subject: [PATCH] rework speaker and talk proposal stuff --- src/bornhack/urls.py | 62 ++++- src/camps/mixins.py | 2 +- src/program/admin.py | 12 +- .../migrations/0030_auto_20170312_1230.py | 148 +++++++++++ .../migrations/0031_auto_20170312_1529.py | 24 ++ src/program/mixins.py | 36 +++ src/program/models.py | 251 +++++++++++++++--- ...er_form.html => eventsubmission_form.html} | 4 +- src/program/templates/schedule_base.html | 5 + src/program/templates/speaker_detail.html | 2 +- src/program/templates/speaker_list.html | 1 + .../templates/speakersubmission_detail.html | 37 +++ .../templates/speakersubmission_form.html | 14 + src/program/templates/submission_list.html | 69 +++++ src/program/views.py | 183 +++++++------ 15 files changed, 714 insertions(+), 136 deletions(-) create mode 100644 src/program/migrations/0030_auto_20170312_1230.py create mode 100644 src/program/migrations/0031_auto_20170312_1529.py create mode 100644 src/program/mixins.py rename src/program/templates/{speaker_form.html => eventsubmission_form.html} (59%) create mode 100644 src/program/templates/speakersubmission_detail.html create mode 100644 src/program/templates/speakersubmission_form.html create mode 100644 src/program/templates/submission_list.html diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index c94444d8..733849ed 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -133,6 +133,58 @@ urlpatterns = [ ScheduleView.as_view(), name='schedule_index' ), + url( + r'^submissions/', include([ + url( + r'^$', + SubmissionListView.as_view(), + name='submission_list', + ), + url( + r'^speakers/', include([ + url( + r'^create/$', + SpeakerSubmissionCreateView.as_view(), + name='speakersubmission_create' + ), + url( + r'^(?P[a-f0-9-]+)/$', + SpeakerSubmissionDetailView.as_view(), + name='speakersubmission_detail' + ), + url( + r'^(?P[a-f0-9-]+)/edit/$', + SpeakerSubmissionUpdateView.as_view(), + name='speakersubmission_update' + ), + url( + r'^(?P[a-f0-9-]+)/pictures/(?P[-_\w+]+)/$', + SpeakerSubmissionPictureView.as_view(), + name='speakersubmission_picture', + ), + ]) + ), + url( + r'^events/', include([ + url( + r'^create/$', + EventSubmissionCreateView.as_view(), + name='eventsubmission_create' + ), + url( + r'^(?P[a-f0-9-]+)/$', + EventSubmissionDetailView.as_view(), + name='eventsubmission_detail' + ), + url( + r'^(?P[a-f0-9-]+)/edit/$', + EventSubmissionUpdateView.as_view(), + name='eventsubmission_update' + ), + ]) + ), + ]) + ), url( r'^speakers/', include([ url( @@ -140,21 +192,11 @@ urlpatterns = [ SpeakerListView.as_view(), name='speaker_index' ), - url( - r'^create/$', - SpeakerCreateView.as_view(), - name='speaker_create' - ), url( r'^(?P[-_\w+]+)/$', SpeakerDetailView.as_view(), name='speaker_detail' ), - url( - r'^(?P[-_\w+]+)/edit/$', - SpeakerEditView.as_view(), - name='speaker_edit' - ), url( r'^(?P[-_\w+]+)/pictures/(?P[-_\w+]+)/$', SpeakerPictureView.as_view(), diff --git a/src/camps/mixins.py b/src/camps/mixins.py index fca375da..a529e0df 100644 --- a/src/camps/mixins.py +++ b/src/camps/mixins.py @@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404 class CampViewMixin(object): def dispatch(self, request, *args, **kwargs): self.camp = get_object_or_404(Camp, slug=self.kwargs['camp_slug']) - return super(CampViewMixin, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) def get_queryset(self): queryset = super(CampViewMixin, self).get_queryset() diff --git a/src/program/admin.py b/src/program/admin.py index 4895a450..e25a5158 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -1,6 +1,16 @@ from django.contrib import admin -from .models import Event, Speaker, EventType, EventInstance, EventLocation +from .models import Event, Speaker, EventType, EventInstance, EventLocation, SpeakerSubmission, EventSubmission + + +@admin.register(SpeakerSubmission) +class SpeakerSubmissionAdmin(admin.ModelAdmin): + pass + + +@admin.register(EventSubmission) +class EventSubmissionAdmin(admin.ModelAdmin): + pass @admin.register(EventLocation) diff --git a/src/program/migrations/0030_auto_20170312_1230.py b/src/program/migrations/0030_auto_20170312_1230.py new file mode 100644 index 00000000..27126658 --- /dev/null +++ b/src/program/migrations/0030_auto_20170312_1230.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-12 11:30 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import program.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0020_camp_read_only'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('program', '0029_auto_20170307_2042'), + ] + + operations = [ + migrations.CreateModel( + name='EventSubmission', + fields=[ + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('submission_status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='draft', max_length=50)), + ('title', models.CharField(help_text='The title of this event', max_length=255)), + ('abstract', models.TextField(help_text='The abstract for this event')), + ('camp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='eventsubmissions', to='camps.Camp')), + ('event_type', models.ForeignKey(help_text='The type of event', on_delete=django.db.models.deletion.CASCADE, to='program.EventType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SpeakerSubmission', + fields=[ + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('submission_status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='draft', max_length=50)), + ('name', models.CharField(help_text='Name or alias of the speaker', max_length=150)), + ('biography', models.TextField(help_text='Markdown is supported.')), + ('picture_large', models.ImageField(blank=True, help_text='A picture of the speaker', null=True, upload_to=program.models.get_speakersubmission_picture_upload_path)), + ('picture_small', models.ImageField(blank=True, help_text='A thumbnail of the speaker picture', null=True, upload_to=program.models.get_speakersubmission_picture_upload_path)), + ('camp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='speakersubmissions', to='camps.Camp')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='event', + name='submission_status', + ), + migrations.AlterField( + model_name='event', + name='abstract', + field=models.TextField(help_text='The abstract for this event'), + ), + migrations.AlterField( + model_name='event', + name='camp', + field=models.ForeignKey(help_text='The camp this event belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='events', to='camps.Camp'), + ), + migrations.AlterField( + model_name='event', + name='event_type', + field=models.ForeignKey(help_text='The type of this event', on_delete=django.db.models.deletion.CASCADE, to='program.EventType'), + ), + migrations.AlterField( + model_name='event', + name='slug', + field=models.SlugField(blank=True, help_text='The slug for this event, created automatically', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='title', + field=models.CharField(help_text='The title of this event', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='video_recording', + field=models.BooleanField(default=True, help_text='Do we intend to record video of this event?'), + ), + migrations.AlterField( + model_name='event', + name='video_url', + field=models.URLField(blank=True, help_text='URL to the recording', max_length=1000, null=True), + ), + migrations.AlterField( + model_name='eventlocation', + name='camp', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='eventlocations', to='camps.Camp'), + ), + migrations.AlterField( + model_name='speaker', + name='biography', + field=models.TextField(help_text='Markdown is supported.'), + ), + migrations.AlterField( + model_name='speaker', + name='camp', + field=models.ForeignKey(help_text='The camp this speaker belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='speakers', to='camps.Camp'), + ), + migrations.AlterField( + model_name='speaker', + name='events', + field=models.ManyToManyField(blank=True, help_text='The event(s) this speaker is anchoring', to='program.Event'), + ), + migrations.AlterField( + model_name='speaker', + name='name', + field=models.CharField(help_text='Name or alias of the speaker', max_length=150), + ), + migrations.AlterField( + model_name='speaker', + name='picture_large', + field=models.ImageField(blank=True, help_text='A picture of the speaker', null=True, upload_to=program.models.get_speaker_picture_upload_path), + ), + migrations.AlterField( + model_name='speaker', + name='picture_small', + field=models.ImageField(blank=True, help_text='A thumbnail of the speaker picture', null=True, upload_to=program.models.get_speaker_picture_upload_path), + ), + migrations.AlterField( + model_name='speaker', + name='slug', + field=models.SlugField(blank=True, help_text='The slug for this speaker, will be autocreated', max_length=255), + ), + migrations.RemoveField( + model_name='speaker', + name='submission_status', + ), + migrations.AddField( + model_name='speaker', + name='submission', + field=models.OneToOneField(blank=True, help_text='The speaker submission object this speaker was created from', null=True, on_delete=django.db.models.deletion.CASCADE, to='program.SpeakerSubmission'), + ), + migrations.AlterUniqueTogether( + name='speaker', + unique_together=set([('camp', 'slug'), ('camp', 'name')]), + ), + ] diff --git a/src/program/migrations/0031_auto_20170312_1529.py b/src/program/migrations/0031_auto_20170312_1529.py new file mode 100644 index 00000000..1fe6552a --- /dev/null +++ b/src/program/migrations/0031_auto_20170312_1529.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-12 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0030_auto_20170312_1230'), + ] + + operations = [ + migrations.RemoveField( + model_name='speaker', + name='user', + ), + migrations.AddField( + model_name='eventsubmission', + name='speakers', + field=models.ManyToManyField(blank=True, help_text='The speaker(s) for this event', to='program.SpeakerSubmission'), + ), + ] diff --git a/src/program/mixins.py b/src/program/mixins.py new file mode 100644 index 00000000..92c4b357 --- /dev/null +++ b/src/program/mixins.py @@ -0,0 +1,36 @@ +from django.views.generic.detail import SingleObjectMixin +from django.shortcuts import redirect +from django.urls import reverse +from . import models + + +class CreateUserSubmissionMixin(SingleObjectMixin): + def form_valid(self, form): + # set camp and user before saving + form.instance.camp = self.camp + form.instance.user = self.request.user + speaker = form.save() + return redirect(reverse('submission_list', kwargs={'camp_slug': self.camp.slug})) + + +class EnsureUnpprovedSubmissionMixin(SingleObjectMixin): + def dispatch(self, request, *args, **kwargs): + # do not permit editing if the submission is already approved + if self.get_object().submission_status == models.UserSubmittedModel.SUBMISSION_APPROVED: + messages.error(request, "This submission has already been approved. Please contact the organisers if you need to modify something." % self.camp.title) + return redirect(reverse('submissions_list', kwargs={'camp_slug': self.camp.slug})) + + # alright, continue with the request + return super().dispatch(request, *args, **kwargs) + + +class EnsureUserOwnsSubmissionMixin(SingleObjectMixin): + def dispatch(self, request, *args, **kwargs): + # make sure that this submission belongs to the logged in user + if self.get_object().user.username != request.user.username: + messages.error(request, "No thanks") + return redirect(reverse('submissions_list', kwargs={'camp_slug': self.camp.slug})) + + # alright, continue with the request + return super().dispatch(request, *args, **kwargs) + diff --git a/src/program/models.py b/src/program/models.py index b570e08c..b85ebb91 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -7,12 +7,28 @@ from utils.models import CreatedUpdatedModel, CampRelatedModel from django.core.exceptions import ValidationError from datetime import timedelta from django.core.urlresolvers import reverse_lazy +import uuid class UserSubmittedModel(CampRelatedModel): + """ + An abstract model containing the stuff that is shared + between the SpeakerSubmission and EventSubmission models. + """ + class Meta: abstract = True + uuid = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + editable=False, + ) + + user = models.ForeignKey( + 'auth.User', + ) + SUBMISSION_DRAFT = 'draft' SUBMISSION_PENDING = 'pending' SUBMISSION_APPROVED = 'approved' @@ -38,20 +54,114 @@ class UserSubmittedModel(CampRelatedModel): default=SUBMISSION_DRAFT, ) + def __str__(self): + return '%s (submitted by: %s, status: %s)' % (self.headline, self.user, self.submission_status) + + +def get_speakersubmission_picture_upload_path(instance, filename): + """ We want speakersubmission pictures saved as MEDIA_ROOT/public/speakersubmissions/camp-slug/submission-uuid/filename """ + return 'public/speakersubmissions/%(campslug)s/%(submissionuuid)s/%(filename)s' % { + 'campslug': instance.camp.slug, + 'submissionuuidd': instance.uuid, + 'filename': filename + } + + +class SpeakerSubmission(UserSubmittedModel): + """ A speaker submission """ + + camp = models.ForeignKey( + 'camps.Camp', + related_name='speakersubmissions' + ) + + name = models.CharField( + max_length=150, + help_text='Name or alias of the speaker', + ) + + biography = models.TextField( + help_text='Markdown is supported.' + ) + + picture_large = models.ImageField( + null=True, + blank=True, + upload_to=get_speakersubmission_picture_upload_path, + help_text='A picture of the speaker' + ) + + picture_small = models.ImageField( + null=True, + blank=True, + upload_to=get_speakersubmission_picture_upload_path, + help_text='A thumbnail of the speaker picture' + ) + @property - def is_public(self): - if self.submission_status == self.SUBMISSION_APPROVED: - return True - else: - return False + def headline(self): + return self.name + + def get_absolute_url(self): + return reverse_lazy('speakersubmission_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}) + + +class EventSubmission(UserSubmittedModel): + """ An event submission """ + + camp = models.ForeignKey( + 'camps.Camp', + related_name='eventsubmissions' + ) + + title = models.CharField( + max_length=255, + help_text='The title of this event', + ) + + abstract = models.TextField( + help_text='The abstract for this event' + ) + + event_type = models.ForeignKey( + 'program.EventType', + help_text='The type of event', + ) + + speakers = models.ManyToManyField( + 'program.SpeakerSubmission', + blank=True, + help_text='Pick the speaker(s) for this event', + ) + + @property + def headline(self): + return self.title + + def get_absolute_url(self): + return reverse_lazy('eventsubmission_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}) + + +############################################################################################# class EventLocation(CampRelatedModel): """ The places where stuff happens """ - name = models.CharField(max_length=100) + + name = models.CharField( + max_length=100 + ) + slug = models.SlugField() - icon = models.CharField(max_length=100) - camp = models.ForeignKey('camps.Camp', null=True, related_name="eventlocations") + + icon = models.CharField( + max_length=100 + ) + + camp = models.ForeignKey( + 'camps.Camp', + related_name='eventlocations' + ) def __str__(self): return self.name @@ -62,33 +172,68 @@ class EventLocation(CampRelatedModel): class EventType(CreatedUpdatedModel): """ Every event needs to have a type. """ - name = models.CharField(max_length=100, unique=True) + name = models.CharField( + max_length=100, + unique=True + ) + slug = models.SlugField() - color = models.CharField(max_length=50) - light_text = models.BooleanField(default=False) - notifications = models.BooleanField(default=False) + + color = models.CharField( + max_length=50 + ) + + light_text = models.BooleanField( + default=False + ) + + notifications = models.BooleanField( + default=False + ) def __str__(self): return self.name -class Event(UserSubmittedModel): +class Event(CampRelatedModel): """ Something that is on the program one or more times. """ - title = models.CharField(max_length=255) - slug = models.SlugField(blank=True, max_length=255) - abstract = models.TextField() - event_type = models.ForeignKey(EventType) - camp = models.ForeignKey('camps.Camp', null=True, related_name="events") + + title = models.CharField( + max_length=255, + help_text='The title of this event', + ) + + abstract = models.TextField( + help_text='The abstract for this event' + ) + + event_type = models.ForeignKey( + 'program.EventType', + help_text='The type of this event', + ) + + slug = models.SlugField( + blank=True, + max_length=255, + help_text='The slug for this event, created automatically', + ) + + camp = models.ForeignKey( + 'camps.Camp', + related_name='events', + help_text='The camp this event belongs to', + ) video_url = models.URLField( max_length=1000, null=True, blank=True, - help_text=_('URL to the recording.') + help_text='URL to the recording' ) + video_recording = models.BooleanField( default=True, - help_text=_('Whether the event will be video recorded or not.') + help_text='Do we intend to record video of this event?' ) class Meta: @@ -115,10 +260,22 @@ class Event(UserSubmittedModel): class EventInstance(CampRelatedModel): """ An instance of an event """ - event = models.ForeignKey('program.event', related_name='instances') + + event = models.ForeignKey( + 'program.event', + related_name='instances' + ) + when = DateTimeRangeField() - notifications_sent = models.BooleanField(default=False) - location = models.ForeignKey('program.EventLocation', related_name='eventinstances') + + notifications_sent = models.BooleanField( + default=False + ) + + location = models.ForeignKey( + 'program.EventLocation', + related_name='eventinstances' + ) class Meta: ordering = ['when'] @@ -168,41 +325,61 @@ def get_speaker_picture_upload_path(instance, filename): } -class Speaker(UserSubmittedModel): - """ A Person anchoring an event. """ - name = models.CharField(max_length=150) +class Speaker(CampRelatedModel): + """ A Person (co)anchoring one or more events on a camp. """ + + name = models.CharField( + max_length=150, + help_text='Name or alias of the speaker', + ) + biography = models.TextField( help_text='Markdown is supported.' ) + picture_small = models.ImageField( null=True, blank=True, upload_to=get_speaker_picture_upload_path, - help_text='A thumbnail of your picture' + help_text='A thumbnail of the speaker picture' ) + picture_large = models.ImageField( null=True, blank=True, upload_to=get_speaker_picture_upload_path, - help_text='A picture of you' + help_text='A picture of the speaker' ) - slug = models.SlugField(blank=True, max_length=255) - camp = models.ForeignKey('camps.Camp', null=True, related_name="speakers") + + slug = models.SlugField( + blank=True, + max_length=255, + help_text='The slug for this speaker, will be autocreated', + ) + + camp = models.ForeignKey( + 'camps.Camp', + null=True, + related_name='speakers', + help_text='The camp this speaker belongs to', + ) + events = models.ManyToManyField( Event, blank=True, + help_text='The event(s) this speaker is anchoring', ) - user = models.ForeignKey( - 'auth.User', - on_delete=models.PROTECT, + submission = models.OneToOneField( + 'program.SpeakerSubmission', null=True, - blank=True + blank=True, + help_text='The speaker submission object this speaker was created from', ) class Meta: ordering = ['name'] - unique_together = (('camp', 'name'), ('camp', 'slug'), ('camp', 'user')) + unique_together = (('camp', 'name'), ('camp', 'slug')) def __str__(self): return '%s (%s)' % (self.name, self.camp) @@ -215,8 +392,4 @@ class Speaker(UserSubmittedModel): def get_absolute_url(self): return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) - def clean(self): - if self.slug == "create": - # this is a reserved word used in urls.py - raise ValidationError({'name': 'This name is reserved, please choose another'}) diff --git a/src/program/templates/speaker_form.html b/src/program/templates/eventsubmission_form.html similarity index 59% rename from src/program/templates/speaker_form.html rename to src/program/templates/eventsubmission_form.html index 2c544d3a..7f1a8483 100644 --- a/src/program/templates/speaker_form.html +++ b/src/program/templates/eventsubmission_form.html @@ -2,12 +2,12 @@ {% load bootstrap3 %} {% block program_content %} -

{% if object %}Update{% else %}Create{% endif %} your {{ camp.title }} speaker biography

+

{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Event Proposal

{% csrf_token %} {% bootstrap_form form %} {% bootstrap_button "Save as draft" button_type="submit" button_class="btn-primary" %} - {% bootstrap_button "Save and submit" button_type="submit" button_class="btn-primary" %} + {% bootstrap_button "Save and submit for approval" button_type="submit" button_class="btn-primary" %}
{% endblock program_content %} diff --git a/src/program/templates/schedule_base.html b/src/program/templates/schedule_base.html index b5564b11..6b54301b 100644 --- a/src/program/templates/schedule_base.html +++ b/src/program/templates/schedule_base.html @@ -36,6 +36,11 @@ {% endfor %} + + {% if request.user.is_authenticated %} + Manage My Proposals + {% endif %} + diff --git a/src/program/templates/speaker_detail.html b/src/program/templates/speaker_detail.html index 0471d39f..cb406fb4 100644 --- a/src/program/templates/speaker_detail.html +++ b/src/program/templates/speaker_detail.html @@ -3,7 +3,7 @@ {% block program_content %} -

{{ speaker.name }}

+h3>{{ speaker.name }} {% if speaker.picture_large and speaker.picture_small %}
diff --git a/src/program/templates/speaker_list.html b/src/program/templates/speaker_list.html index 80855446..6c69c0b1 100644 --- a/src/program/templates/speaker_list.html +++ b/src/program/templates/speaker_list.html @@ -11,6 +11,7 @@ {% for speaker in speaker_list %} {{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }}) + {% if not speaker.is_public %}(unpublished, {{ speaker.submission_status }}){% endif %} {% endfor %}
diff --git a/src/program/templates/speakersubmission_detail.html b/src/program/templates/speakersubmission_detail.html new file mode 100644 index 00000000..f6fdb9b2 --- /dev/null +++ b/src/program/templates/speakersubmission_detail.html @@ -0,0 +1,37 @@ +{% extends 'program_base.html' %} +{% load commonmark %} + +{% block program_content %} + +

{{ camp.title }} Speaker Proposal Details

+ +
    +
  • Status: {{ speakersubmission.submission_status }}
  • +
  • ID: {{ speakersubmission.uuid }}
  • +
+ +

+ Proposal List +

+ +
+
{{ speakersubmission.name }}
+
+ {% if speakersubmission.picture_large and speakersubmission.picture_small %} +
+
+ {{ speakersubmission.biography|commonmark }} +
+
+ + {{ camp.title }} speaker picture of {{ speakersubmission.name }} + +
+
+ {% else %} + {{ speakersubmission.biography|commonmark }} + {% endif %} +
+
+ +{% endblock program_content %} diff --git a/src/program/templates/speakersubmission_form.html b/src/program/templates/speakersubmission_form.html new file mode 100644 index 00000000..2b8f12ae --- /dev/null +++ b/src/program/templates/speakersubmission_form.html @@ -0,0 +1,14 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Speaker Proposal

+
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button "Save as draft" button_type="submit" button_class="btn-primary" %} + {% bootstrap_button "Save and submit for approval" button_type="submit" button_class="btn-primary" %} +
+ +{% endblock program_content %} + diff --git a/src/program/templates/submission_list.html b/src/program/templates/submission_list.html new file mode 100644 index 00000000..400681db --- /dev/null +++ b/src/program/templates/submission_list.html @@ -0,0 +1,69 @@ +{% extends 'program_base.html' %} + +{% block title %} +Proposals | {{ block.super }} +{% endblock %} + +{% block program_content %} + +

Your {{ camp.title }} Speaker Proposals

+{% if speakersubmission_list %} + + + + + + + + + + {% for speakersubmission in speakersubmission_list %} + + + + + + {% endfor %} + +
NameStatusActions
{{ speakersubmission.name }}{{ speakersubmission.submission_status }} + Modify + Delete +
+{% else %} +

No speaker proposals found

+{% endif %} + +Propose New Speaker + +

+ +

Your {{ camp.title }} Event Proposals

+{% if eventsubmission_list %} + + + + + + + + + + {% for eventsubmission in eventsubmission_list %} + + + + + + {% endfor %} + +
TitleStatusActions
{{ eventsubmission.title }}{{ eventsubmission.submission_status }} + Modify + Delete +
+{% else %} +

No event proposals found

+{% endif %} + +Propose New Event + +{% endblock %} diff --git a/src/program/views.py b/src/program/views.py index e2b06851..50dc4825 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -1,85 +1,114 @@ -from collections import OrderedDict -import datetime from django.views.generic import ListView, TemplateView, DetailView from django.views.generic.edit import CreateView, UpdateView -from camps.mixins import CampViewMixin -from . import models -from django.http import Http404 -import datetime, os from django.conf import settings -from django.views import View from django.views.decorators.http import require_safe -from django.http import Http404 +from django.http import Http404, HttpResponse from django.utils.decorators import method_decorator -from django.http import HttpResponse from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse +from camps.mixins import CampViewMixin +from .mixins import CreateUserSubmissionMixin, EnsureUnpprovedSubmissionMixin, EnsureUserOwnsSubmissionMixin +from . import models +import datetime, os -class SpeakerCreateView(LoginRequiredMixin, CampViewMixin, CreateView): - model = models.Speaker +############## submissions ######################################################## + + +class SubmissionListView(LoginRequiredMixin, CampViewMixin, ListView): + model = models.SpeakerSubmission + template_name = 'submission_list.html' + context_object_name = 'speakersubmission_list' + + def get_queryset(self, **kwargs): + # only show speaker submissions for the current user + return super().get_queryset().filter(user=self.request.user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # add eventsubmissions to the context + context['eventsubmission_list'] = models.EventSubmission.objects.filter(camp=self.camp, user=self.request.user) + return context + + +class SpeakerSubmissionCreateView(LoginRequiredMixin, CampViewMixin, CreateUserSubmissionMixin, CreateView): + model = models.SpeakerSubmission fields = ['name', 'biography', 'picture_small', 'picture_large'] - template_name = 'speaker_form.html' + template_name = 'speakersubmission_form.html' + + +class SpeakerSubmissionUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, EnsureUnpprovedSubmissionMixin, UpdateView): + model = models.SpeakerSubmission + fields = ['name', 'biography', 'picture_small', 'picture_large'] + template_name = 'speakersubmission_form.html' + + +class SpeakerSubmissionDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, DetailView): + model = models.SpeakerSubmission + template_name = 'speakersubmission_detail.html' + + +@method_decorator(require_safe, name='dispatch') +class SpeakerSubmissionPictureView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, DetailView): + model = models.SpeakerSubmission def get(self, request, *args, **kwargs): - # first make sure we don't already have a speaker for this user for this camp - try: - speaker = models.Speaker.objects.get(user=request.user, camp=self.camp) - except models.Speaker.DoesNotExist: - # no speaker exists, just show the create speaker form - return super(SpeakerCreateView, self).get(request, *args, **kwargs) + # is the speaker public, or owned by current user? + if not self.get_object().user != request.user: + raise Http404() - # speaker already exists, where do we want to redirect? - if speaker.submission_status == models.Speaker.SUBMISSION_DRAFT: - messages.info(request, "You already have a draft speaker profile for %s, you can modify and submit it here" % self.camp.title) - return redirect('speaker_edit', camp_slug=self.camp.slug, slug=speaker.slug) - elif speaker.submission_status == models.Speaker.SUBMISSION_PENDING: - messages.info(request, "You already have a pending speaker profile for %s, you can modify and resubmit it here" % self.camp.title) - return redirect('speaker_edit', camp_slug=self.camp.slug, slug=speaker.slug) - elif speaker.submission_status == models.Speaker.SUBMISSION_REJECTED: - messages.info(request, "You already have a rejected speaker profile for %s, you can modify and resubmit it here" % self.camp.title) - return redirect('speaker_edit', camp_slug=self.camp.slug, slug=speaker.slug) - elif speaker.submission_status == models.Speaker.SUBMISSION_APPROVED: - messages.info(request, "You already have an accepted speaker profile for %s, please contact the organisers if you want to modify it." % self.camp.title) - return redirect('speaker_detail', camp_slug=self.camp.slug, slug=speaker.slug) + # do we have the requested picture? + if kwargs['picture'] == 'thumbnail': + if self.get_object().picture_small: + picture = self.get_object().picture_small + else: + raise Http404() + elif kwargs['picture'] == 'large': + if self.get_object().picture_large: + picture = self.get_object().picture_large + else: + raise Http404() else: - # unknown submission status! - return + raise Http404() - def form_valid(self, form): - # set camp before saving - form.instance.camp = self.camp - form.instance.user = self.request.user - speaker = form.save() - return redirect(reverse('speaker_detail', kwargs={'camp_slug': speaker.camp.slug, 'slug': speaker.slug})) + # make nginx return the picture using X-Accel-Redirect + # (this works for nginx only, other webservers use x-sendfile), + # TODO: what about runserver mode here? + response = HttpResponse() + response['X-Accel-Redirect'] = '/public/speakersubmissions/%(campslug)s/%(submissionuuid)s/%(filename)s' % { + 'campslug': self.camp.slug, + 'submissionuuid': self.get_object().uuid, + 'filename': os.path.basename(picture.name), + } + response['Content-Type'] = '' + return response -class SpeakerEditView(LoginRequiredMixin, CampViewMixin, UpdateView): - model = models.Speaker - fields = ['name', 'biography', 'picture_small', 'picture_large'] - template_name = 'speaker_form.html' +class EventSubmissionCreateView(LoginRequiredMixin, CampViewMixin, CreateUserSubmissionMixin, CreateView): + model = models.EventSubmission + fields = ['title', 'abstract', 'event_type', 'speakers'] + template_name = 'eventsubmission_form.html' - def dispatch(self, request, *args, **kwargs): - # call super dispatch now because it ets self.camp which is needed below - response = super(SpeakerEditView, self).dispatch(request, *args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'].fields['speakers'].queryset = models.SpeakerSubmission.objects.filter(camp=self.camp, user=self.request.user) + return context - # first make sure that this speaker belongs to the logged in user - if self.get_object().user.username != request.user.username: - messages.error(request, "No thanks") - return redirect(reverse('speaker_detail', kwargs={'camp_slug': self.get_object().camp.slug, 'slug': self.get_object().slug})) - if self.get_object().submission_status == models.Speaker.SUBMISSION_PENDING: - messages.info(request, "Your speaker profile for %s has already been submitted. If you modify it you will have to resubmit it." % self.get_object().camp.title) - elif self.get_object().submission_status == models.Speaker.SUBMISSION_REJECTED: - messages.info(request, "When you are done editing you will have to resubmit your speaker profile." % self.get_object().camp.title) - elif self.get_object().submission_status == models.Speaker.SUBMISSION_APPROVED: - messages.error(request, "Your speaker profile for %s has already been approved. Please contact the organisers if you want to modify it." % self.get_object().camp.title) - return redirect(reverse('speaker_detail', kwargs={'camp_slug': self.get_object().camp.slug, 'slug': self.get_object().slug})) +class EventSubmissionUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, EnsureUnpprovedSubmissionMixin, UpdateView): + model = models.EventSubmission + fields = ['title', 'abstract', 'event_tyoe', 'speakers'] + template_name = 'eventsubmission_form.html' - # alright, render the form - return super(SpeakerEditView, self).dispatch(request, *args, **kwargs) + +class EventSubmissionDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, DetailView): + model = models.EventSubmission + template_name = 'eventsubmission_detail.html' + + +################## speakers ############################################### @method_decorator(require_safe, name='dispatch') @@ -87,6 +116,10 @@ class SpeakerPictureView(CampViewMixin, DetailView): model = models.Speaker def get(self, request, *args, **kwargs): + # is the speaker public, or owned by current user? + if not self.get_object().is_public and self.get_object().user != request.user: + raise Http404() + # do we have the requested picture? if kwargs['picture'] == 'thumbnail': if self.get_object().picture_small: @@ -118,30 +151,13 @@ class SpeakerDetailView(CampViewMixin, DetailView): model = models.Speaker template_name = 'speaker_detail.html' - def get(self, request, *args, **kwargs): - if not self.get_object().is_public and self.get_object().user != request.user: - raise Http404() - else: - return super().get(request, *args, **kwargs) - class SpeakerListView(CampViewMixin, ListView): model = models.Speaker template_name = 'speaker_list.html' - def get_queryset(self, *args, **kwargs): - # get all approved speakers - speakers = models.Speaker.objects.filter( - camp=self.camp, - submission_status=models.Speaker.SUBMISSION_APPROVED - ) - # also get the users own speaker, in case he has an unapproved - userspeakers = models.Speaker.objects.filter( - camp=self.camp, - user=self.request.user - ).exclude(submission_status=models.Speaker.SUBMISSION_APPROVED) - return speakers | userspeakers +################## events ############################################## class EventListView(CampViewMixin, ListView): @@ -149,6 +165,14 @@ class EventListView(CampViewMixin, ListView): template_name = 'event_list.html' +class EventDetailView(CampViewMixin, DetailView): + model = models.Event + template_name = 'schedule_event_detail.html' + + +################## schedule ############################################# + + class ScheduleView(CampViewMixin, TemplateView): def get_template_names(self): if 'day' in self.kwargs: @@ -227,11 +251,6 @@ class ScheduleView(CampViewMixin, TemplateView): return context -class EventDetailView(CampViewMixin, DetailView): - model = models.Event - template_name = 'schedule_event_detail.html' - - class CallForSpeakersView(CampViewMixin, TemplateView): def get_template_names(self): return '%s_call_for_speakers.html' % self.camp.slug