From 334f0477fecf7aec79d6673cac932ca4b35d2176 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Wed, 8 Mar 2017 00:00:17 +0100 Subject: [PATCH] start work on speaker and talk submissions --- src/bornhack/urls.py | 10 +++ src/program/migrations/0026_speaker_user.py | 23 ++++++ .../migrations/0027_auto_20170307_1701.py | 22 ++++++ .../migrations/0028_auto_20170307_2014.py | 25 +++++++ .../migrations/0029_auto_20170307_2042.py | 25 +++++++ src/program/models.py | 50 ++++++++++++- src/program/templates/speaker_form.html | 14 ++++ src/program/views.py | 70 +++++++++++++++++++ src/villages/models.py | 4 -- 9 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 src/program/migrations/0026_speaker_user.py create mode 100644 src/program/migrations/0027_auto_20170307_1701.py create mode 100644 src/program/migrations/0028_auto_20170307_2014.py create mode 100644 src/program/migrations/0029_auto_20170307_2042.py create mode 100644 src/program/templates/speaker_form.html diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index 0bee4386..db7f7e35 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -135,6 +135,16 @@ urlpatterns = [ SpeakerListView.as_view(), name='speaker_index' ), + url( + r'^speakers/create/$', + SpeakerCreateView.as_view(), + name='speaker_create' + ), + url( + r'^speakers/(?P[-_\w+]+)/edit/$', + SpeakerEditView.as_view(), + name='speaker_edit' + ), url( r'^speakers/(?P[-_\w+]+)/$', SpeakerDetailView.as_view(), diff --git a/src/program/migrations/0026_speaker_user.py b/src/program/migrations/0026_speaker_user.py new file mode 100644 index 00000000..9f19a273 --- /dev/null +++ b/src/program/migrations/0026_speaker_user.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-06 19:20 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('program', '0025_auto_20170306_1938'), + ] + + operations = [ + migrations.AddField( + model_name='speaker', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/src/program/migrations/0027_auto_20170307_1701.py b/src/program/migrations/0027_auto_20170307_1701.py new file mode 100644 index 00000000..ca07c0c4 --- /dev/null +++ b/src/program/migrations/0027_auto_20170307_1701.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-07 16:01 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0019_auto_20170131_1849'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('program', '0026_speaker_user'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='speaker', + unique_together=set([('camp', 'user'), ('camp', 'slug'), ('camp', 'name')]), + ), + ] diff --git a/src/program/migrations/0028_auto_20170307_2014.py b/src/program/migrations/0028_auto_20170307_2014.py new file mode 100644 index 00000000..bbda54c7 --- /dev/null +++ b/src/program/migrations/0028_auto_20170307_2014.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-07 19:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0027_auto_20170307_1701'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='submission_status', + field=models.CharField(choices=[('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=50), + ), + migrations.AddField( + model_name='speaker', + name='submission_status', + field=models.CharField(choices=[('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=50), + ), + ] diff --git a/src/program/migrations/0029_auto_20170307_2042.py b/src/program/migrations/0029_auto_20170307_2042.py new file mode 100644 index 00000000..eb5dd861 --- /dev/null +++ b/src/program/migrations/0029_auto_20170307_2042.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-07 19:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0028_auto_20170307_2014'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='submission_status', + field=models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='draft', max_length=50), + ), + migrations.AlterField( + model_name='speaker', + name='submission_status', + field=models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='draft', max_length=50), + ), + ] diff --git a/src/program/models.py b/src/program/models.py index 700ca2cb..4023522b 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -6,6 +6,37 @@ from django.utils.translation import ugettext_lazy as _ from utils.models import CreatedUpdatedModel from django.core.exceptions import ValidationError from datetime import timedelta +from django.core.urlresolvers import reverse_lazy + + +class UserSubmittedModel(CreatedUpdatedModel): + class Meta: + abstract = True + + SUBMISSION_DRAFT = 'draft' + SUBMISSION_PENDING = 'pending' + SUBMISSION_APPROVED = 'approved' + SUBMISSION_REJECTED = 'rejected' + + SUBMISSION_STATUSES = [ + SUBMISSION_DRAFT, + SUBMISSION_PENDING, + SUBMISSION_APPROVED, + SUBMISSION_REJECTED + ] + + SUBMISSION_STATUS_CHOICES = [ + (SUBMISSION_DRAFT, 'Draft'), + (SUBMISSION_PENDING, 'Pending approval'), + (SUBMISSION_APPROVED, 'Approved'), + (SUBMISSION_REJECTED, 'Rejected'), + ] + + submission_status = models.CharField( + max_length=50, + choices=SUBMISSION_STATUS_CHOICES, + default=SUBMISSION_DRAFT, + ) class EventLocation(CreatedUpdatedModel): @@ -34,7 +65,7 @@ class EventType(CreatedUpdatedModel): return self.name -class Event(CreatedUpdatedModel): +class Event(UserSubmittedModel): """ Something that is on the program one or more times. """ title = models.CharField(max_length=255) slug = models.SlugField(blank=True, max_length=255) @@ -71,6 +102,9 @@ class Event(CreatedUpdatedModel): return ", ".join(self.speakers.all().values_list('name', flat=True)) return False + def get_absolute_url(self): + return reverse_lazy('event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) + class EventInstance(CreatedUpdatedModel): """ An instance of an event """ @@ -123,7 +157,7 @@ def get_speaker_picture_upload_path(instance, filename): } -class Speaker(CreatedUpdatedModel): +class Speaker(UserSubmittedModel): """ A Person anchoring an event. """ name = models.CharField(max_length=150) biography = models.TextField() @@ -136,9 +170,16 @@ class Speaker(CreatedUpdatedModel): blank=True, ) + user = models.ForeignKey( + 'auth.User', + on_delete=models.PROTECT, + null=True, + blank=True + ) + class Meta: ordering = ['name'] - unique_together = (('camp', 'name'), ('camp', 'slug')) + unique_together = (('camp', 'name'), ('camp', 'slug'), ('camp', 'user')) def __str__(self): return '%s (%s)' % (self.name, self.camp) @@ -148,4 +189,7 @@ class Speaker(CreatedUpdatedModel): self.slug = slugify(self.name) super(Speaker, self).save(**kwargs) + def get_absolute_url(self): + return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) + diff --git a/src/program/templates/speaker_form.html b/src/program/templates/speaker_form.html new file mode 100644 index 00000000..582c8fd6 --- /dev/null +++ b/src/program/templates/speaker_form.html @@ -0,0 +1,14 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

{% if object %}Update{% else %}Create{% endif %} speaker biography

+
+ {% 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" %} +
+ +{% endblock program_content %} + diff --git a/src/program/views.py b/src/program/views.py index d8802197..7abecc60 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -1,6 +1,7 @@ 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 @@ -11,6 +12,75 @@ from django.views.decorators.http import require_safe from django.http import Http404 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 + + +class SpeakerCreateView(LoginRequiredMixin, CampViewMixin, CreateView): + model = models.Speaker + fields = ['name', 'biography', 'picture_small', 'picture_large'] + template_name = 'speaker_form.html' + + 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) + + # 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) + else: + # unknown submission status! + return + + 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})) + + +class SpeakerEditView(LoginRequiredMixin, CampViewMixin, UpdateView): + model = models.Speaker + fields = ['name', 'biography', 'picture_small', 'picture_large'] + template_name = 'speaker_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) + + # 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})) + + # alright, render the form + return super(SpeakerEditView, self).dispatch(request, *args, **kwargs) + @method_decorator(require_safe, name='dispatch') class SpeakerPictureView(CampViewMixin, DetailView): diff --git a/src/villages/models.py b/src/villages/models.py index 84be713c..b01cd8f4 100644 --- a/src/villages/models.py +++ b/src/villages/models.py @@ -1,11 +1,7 @@ - - from django.core.urlresolvers import reverse_lazy from django.db import models from django.utils.text import slugify - from utils.models import CreatedUpdatedModel, UUIDModel - from .managers import VillageQuerySet