rework speaker and talk proposal stuff

This commit is contained in:
Thomas Steen Rasmussen 2017-03-12 15:43:41 +01:00
parent 1162a72627
commit ad3b826844
15 changed files with 714 additions and 136 deletions

View file

@ -133,6 +133,58 @@ urlpatterns = [
ScheduleView.as_view(), ScheduleView.as_view(),
name='schedule_index' 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<pk>[a-f0-9-]+)/$',
SpeakerSubmissionDetailView.as_view(),
name='speakersubmission_detail'
),
url(
r'^(?P<pk>[a-f0-9-]+)/edit/$',
SpeakerSubmissionUpdateView.as_view(),
name='speakersubmission_update'
),
url(
r'^(?P<pk>[a-f0-9-]+)/pictures/(?P<picture>[-_\w+]+)/$',
SpeakerSubmissionPictureView.as_view(),
name='speakersubmission_picture',
),
])
),
url(
r'^events/', include([
url(
r'^create/$',
EventSubmissionCreateView.as_view(),
name='eventsubmission_create'
),
url(
r'^(?P<pk>[a-f0-9-]+)/$',
EventSubmissionDetailView.as_view(),
name='eventsubmission_detail'
),
url(
r'^(?P<pk>[a-f0-9-]+)/edit/$',
EventSubmissionUpdateView.as_view(),
name='eventsubmission_update'
),
])
),
])
),
url( url(
r'^speakers/', include([ r'^speakers/', include([
url( url(
@ -140,21 +192,11 @@ urlpatterns = [
SpeakerListView.as_view(), SpeakerListView.as_view(),
name='speaker_index' name='speaker_index'
), ),
url(
r'^create/$',
SpeakerCreateView.as_view(),
name='speaker_create'
),
url( url(
r'^(?P<slug>[-_\w+]+)/$', r'^(?P<slug>[-_\w+]+)/$',
SpeakerDetailView.as_view(), SpeakerDetailView.as_view(),
name='speaker_detail' name='speaker_detail'
), ),
url(
r'^(?P<slug>[-_\w+]+)/edit/$',
SpeakerEditView.as_view(),
name='speaker_edit'
),
url( url(
r'^(?P<slug>[-_\w+]+)/pictures/(?P<picture>[-_\w+]+)/$', r'^(?P<slug>[-_\w+]+)/pictures/(?P<picture>[-_\w+]+)/$',
SpeakerPictureView.as_view(), SpeakerPictureView.as_view(),

View file

@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404
class CampViewMixin(object): class CampViewMixin(object):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.camp = get_object_or_404(Camp, slug=self.kwargs['camp_slug']) 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): def get_queryset(self):
queryset = super(CampViewMixin, self).get_queryset() queryset = super(CampViewMixin, self).get_queryset()

View file

@ -1,6 +1,16 @@
from django.contrib import admin 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) @admin.register(EventLocation)

View file

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

View file

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

36
src/program/mixins.py Normal file
View file

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

View file

@ -7,12 +7,28 @@ from utils.models import CreatedUpdatedModel, CampRelatedModel
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from datetime import timedelta from datetime import timedelta
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
import uuid
class UserSubmittedModel(CampRelatedModel): class UserSubmittedModel(CampRelatedModel):
"""
An abstract model containing the stuff that is shared
between the SpeakerSubmission and EventSubmission models.
"""
class Meta: class Meta:
abstract = True abstract = True
uuid = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
user = models.ForeignKey(
'auth.User',
)
SUBMISSION_DRAFT = 'draft' SUBMISSION_DRAFT = 'draft'
SUBMISSION_PENDING = 'pending' SUBMISSION_PENDING = 'pending'
SUBMISSION_APPROVED = 'approved' SUBMISSION_APPROVED = 'approved'
@ -38,20 +54,114 @@ class UserSubmittedModel(CampRelatedModel):
default=SUBMISSION_DRAFT, 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 @property
def is_public(self): def headline(self):
if self.submission_status == self.SUBMISSION_APPROVED: return self.name
return True
else: def get_absolute_url(self):
return False 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): class EventLocation(CampRelatedModel):
""" The places where stuff happens """ """ The places where stuff happens """
name = models.CharField(max_length=100)
name = models.CharField(
max_length=100
)
slug = models.SlugField() 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): def __str__(self):
return self.name return self.name
@ -62,33 +172,68 @@ class EventLocation(CampRelatedModel):
class EventType(CreatedUpdatedModel): class EventType(CreatedUpdatedModel):
""" Every event needs to have a type. """ """ 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() slug = models.SlugField()
color = models.CharField(max_length=50)
light_text = models.BooleanField(default=False) color = models.CharField(
notifications = models.BooleanField(default=False) max_length=50
)
light_text = models.BooleanField(
default=False
)
notifications = models.BooleanField(
default=False
)
def __str__(self): def __str__(self):
return self.name return self.name
class Event(UserSubmittedModel): class Event(CampRelatedModel):
""" Something that is on the program one or more times. """ """ Something that is on the program one or more times. """
title = models.CharField(max_length=255)
slug = models.SlugField(blank=True, max_length=255) title = models.CharField(
abstract = models.TextField() max_length=255,
event_type = models.ForeignKey(EventType) help_text='The title of this event',
camp = models.ForeignKey('camps.Camp', null=True, related_name="events") )
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( video_url = models.URLField(
max_length=1000, max_length=1000,
null=True, null=True,
blank=True, blank=True,
help_text=_('URL to the recording.') help_text='URL to the recording'
) )
video_recording = models.BooleanField( video_recording = models.BooleanField(
default=True, 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: class Meta:
@ -115,10 +260,22 @@ class Event(UserSubmittedModel):
class EventInstance(CampRelatedModel): class EventInstance(CampRelatedModel):
""" An instance of an event """ """ An instance of an event """
event = models.ForeignKey('program.event', related_name='instances')
event = models.ForeignKey(
'program.event',
related_name='instances'
)
when = DateTimeRangeField() 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: class Meta:
ordering = ['when'] ordering = ['when']
@ -168,41 +325,61 @@ def get_speaker_picture_upload_path(instance, filename):
} }
class Speaker(UserSubmittedModel): class Speaker(CampRelatedModel):
""" A Person anchoring an event. """ """ A Person (co)anchoring one or more events on a camp. """
name = models.CharField(max_length=150)
name = models.CharField(
max_length=150,
help_text='Name or alias of the speaker',
)
biography = models.TextField( biography = models.TextField(
help_text='Markdown is supported.' help_text='Markdown is supported.'
) )
picture_small = models.ImageField( picture_small = models.ImageField(
null=True, null=True,
blank=True, blank=True,
upload_to=get_speaker_picture_upload_path, 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( picture_large = models.ImageField(
null=True, null=True,
blank=True, blank=True,
upload_to=get_speaker_picture_upload_path, 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( events = models.ManyToManyField(
Event, Event,
blank=True, blank=True,
help_text='The event(s) this speaker is anchoring',
) )
user = models.ForeignKey( submission = models.OneToOneField(
'auth.User', 'program.SpeakerSubmission',
on_delete=models.PROTECT,
null=True, null=True,
blank=True blank=True,
help_text='The speaker submission object this speaker was created from',
) )
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = (('camp', 'name'), ('camp', 'slug'), ('camp', 'user')) unique_together = (('camp', 'name'), ('camp', 'slug'))
def __str__(self): def __str__(self):
return '%s (%s)' % (self.name, self.camp) return '%s (%s)' % (self.name, self.camp)
@ -215,8 +392,4 @@ class Speaker(UserSubmittedModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) 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'})

View file

@ -2,12 +2,12 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% block program_content %} {% block program_content %}
<h3>{% if object %}Update{% else %}Create{% endif %} your {{ camp.title }} speaker biography</h3> <h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Event Proposal</h3>
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% bootstrap_button "Save as draft" button_type="submit" button_class="btn-primary" %} {% 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" %}
</form> </form>
{% endblock program_content %} {% endblock program_content %}

View file

@ -36,6 +36,11 @@
<option value="{{ loc.slug }}" {% if location and location == loc %}selected{% endif %}>{{ loc.name }}</option> <option value="{{ loc.slug }}" {% if location and location == loc %}selected{% endif %}>{{ loc.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% if request.user.is_authenticated %}
<a href="{% url 'submission_list' camp_slug=camp.slug %}" class="btn btn-default">Manage My Proposals</a>
{% endif %}
</div> </div>
</form> </form>
</div> </div>

View file

@ -3,7 +3,7 @@
{% block program_content %} {% block program_content %}
<h3>{{ speaker.name }}</h3> h3>{{ speaker.name }}</h3>
{% if speaker.picture_large and speaker.picture_small %} {% if speaker.picture_large and speaker.picture_small %}
<div class="row"> <div class="row">

View file

@ -11,6 +11,7 @@
{% for speaker in speaker_list %} {% for speaker in speaker_list %}
<a href="{% url 'speaker_detail' camp_slug=camp.slug 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 }}) {{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }})
{% if not speaker.is_public %}(unpublished, {{ speaker.submission_status }}){% endif %}
</a> </a>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -0,0 +1,37 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% block program_content %}
<h2>{{ camp.title }} Speaker Proposal Details</h2>
<ul>
<li class="list">Status: <span class="badge">{{ speakersubmission.submission_status }}</span></li>
<li class="list">ID: <span class="badge">{{ speakersubmission.uuid }}</span></li>
</ul>
<p>
<a href="{% url 'submission_list' camp_slug=camp.slug %}" class="btn btn-primary">Proposal List</a>
</p>
<div class="panel panel-default">
<div class="panel-heading">{{ speakersubmission.name }}</div>
<div class="panel-body">
{% if speakersubmission.picture_large and speakersubmission.picture_small %}
<div class="row">
<div class="col-md-8 text-container">
{{ speakersubmission.biography|commonmark }}
</div>
<div class="col-md-4">
<a href="{% url 'speakersubmission_picture' camp_slug=camp.slug pk=speakersubmission.pk picture='large' %}" >
<img src="{% url 'speakersubmission_picture' camp_slug=camp.slug pk=speakersubmission.pk picture='thumbnail' %}" alt="{{ camp.title }} speaker picture of {{ speakersubmission.name }}">
</a>
</div>
</div>
{% else %}
{{ speakersubmission.biography|commonmark }}
{% endif %}
</div>
</div>
{% endblock program_content %}

View file

@ -0,0 +1,14 @@
{% extends 'program_base.html' %}
{% load bootstrap3 %}
{% block program_content %}
<h3>{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Speaker Proposal</h3>
<form method="POST">
{% 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" %}
</form>
{% endblock program_content %}

View file

@ -0,0 +1,69 @@
{% extends 'program_base.html' %}
{% block title %}
Proposals | {{ block.super }}
{% endblock %}
{% block program_content %}
<h3>Your {{ camp.title }} Speaker Proposals</h3>
{% if speakersubmission_list %}
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for speakersubmission in speakersubmission_list %}
<tr>
<td><b>{{ speakersubmission.name }}</b></td>
<td><span class="badge">{{ speakersubmission.submission_status }}</span></td>
<td>
<a href="{% url 'speakersubmission_update' camp_slug=camp.slug pk=speakersubmission.uuid %}" class="btn btn-primary btn-xs">Modify</a>
<a href="#" class="btn btn-danger btn-xs">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No speaker proposals found</h3>
{% endif %}
<a href="{% url 'speakersubmission_create' camp_slug=camp.slug %}" class="btn btn-primary">Propose New Speaker</a>
<p>
<h3>Your {{ camp.title }} Event Proposals</h3>
{% if eventsubmission_list %}
<table class="table table-striped">
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for eventsubmission in eventsubmission_list %}
<tr>
<td><b>{{ eventsubmission.title }}</b></td>
<td><span class="badge">{{ eventsubmission.submission_status }}</span></td>
<td>
<a href="{% url 'eventsubmission_update' camp_slug=camp.slug pk=eventsubmission.uuid %}" class="btn btn-primary btn-xs">Modify</a>
<a href="#" class="btn btn-danger btn-xs">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No event proposals found</h3>
{% endif %}
<a href="{% url 'eventsubmission_create' camp_slug=camp.slug %}" class="btn btn-primary">Propose New Event</a>
{% endblock %}

View file

@ -1,85 +1,114 @@
from collections import OrderedDict
import datetime
from django.views.generic import ListView, TemplateView, DetailView from django.views.generic import ListView, TemplateView, DetailView
from django.views.generic.edit import CreateView, UpdateView 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.conf import settings
from django.views import View
from django.views.decorators.http import require_safe 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.utils.decorators import method_decorator
from django.http import HttpResponse
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse 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): ############## submissions ########################################################
model = models.Speaker
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'] 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): def get(self, request, *args, **kwargs):
# first make sure we don't already have a speaker for this user for this camp # is the speaker public, or owned by current user?
try: if not self.get_object().user != request.user:
speaker = models.Speaker.objects.get(user=request.user, camp=self.camp) raise Http404()
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? # do we have the requested picture?
if speaker.submission_status == models.Speaker.SUBMISSION_DRAFT: if kwargs['picture'] == 'thumbnail':
messages.info(request, "You already have a draft speaker profile for %s, you can modify and submit it here" % self.camp.title) if self.get_object().picture_small:
return redirect('speaker_edit', camp_slug=self.camp.slug, slug=speaker.slug) picture = self.get_object().picture_small
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: else:
# unknown submission status! raise Http404()
return elif kwargs['picture'] == 'large':
if self.get_object().picture_large:
picture = self.get_object().picture_large
else:
raise Http404()
else:
raise Http404()
def form_valid(self, form): # make nginx return the picture using X-Accel-Redirect
# set camp before saving # (this works for nginx only, other webservers use x-sendfile),
form.instance.camp = self.camp # TODO: what about runserver mode here?
form.instance.user = self.request.user response = HttpResponse()
speaker = form.save() response['X-Accel-Redirect'] = '/public/speakersubmissions/%(campslug)s/%(submissionuuid)s/%(filename)s' % {
return redirect(reverse('speaker_detail', kwargs={'camp_slug': speaker.camp.slug, 'slug': speaker.slug})) '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): class EventSubmissionCreateView(LoginRequiredMixin, CampViewMixin, CreateUserSubmissionMixin, CreateView):
model = models.Speaker model = models.EventSubmission
fields = ['name', 'biography', 'picture_small', 'picture_large'] fields = ['title', 'abstract', 'event_type', 'speakers']
template_name = 'speaker_form.html' template_name = 'eventsubmission_form.html'
def dispatch(self, request, *args, **kwargs): def get_context_data(self, **kwargs):
# call super dispatch now because it ets self.camp which is needed below context = super().get_context_data(**kwargs)
response = super(SpeakerEditView, self).dispatch(request, *args, **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: class EventSubmissionUpdateView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, EnsureUnpprovedSubmissionMixin, UpdateView):
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) model = models.EventSubmission
elif self.get_object().submission_status == models.Speaker.SUBMISSION_REJECTED: fields = ['title', 'abstract', 'event_tyoe', 'speakers']
messages.info(request, "When you are done editing you will have to resubmit your speaker profile." % self.get_object().camp.title) template_name = 'eventsubmission_form.html'
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) class EventSubmissionDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsSubmissionMixin, DetailView):
model = models.EventSubmission
template_name = 'eventsubmission_detail.html'
################## speakers ###############################################
@method_decorator(require_safe, name='dispatch') @method_decorator(require_safe, name='dispatch')
@ -87,6 +116,10 @@ class SpeakerPictureView(CampViewMixin, DetailView):
model = models.Speaker model = models.Speaker
def get(self, request, *args, **kwargs): 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? # do we have the requested picture?
if kwargs['picture'] == 'thumbnail': if kwargs['picture'] == 'thumbnail':
if self.get_object().picture_small: if self.get_object().picture_small:
@ -118,30 +151,13 @@ class SpeakerDetailView(CampViewMixin, DetailView):
model = models.Speaker model = models.Speaker
template_name = 'speaker_detail.html' 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): class SpeakerListView(CampViewMixin, ListView):
model = models.Speaker model = models.Speaker
template_name = 'speaker_list.html' 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): class EventListView(CampViewMixin, ListView):
@ -149,6 +165,14 @@ class EventListView(CampViewMixin, ListView):
template_name = 'event_list.html' template_name = 'event_list.html'
class EventDetailView(CampViewMixin, DetailView):
model = models.Event
template_name = 'schedule_event_detail.html'
################## schedule #############################################
class ScheduleView(CampViewMixin, TemplateView): class ScheduleView(CampViewMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if 'day' in self.kwargs: if 'day' in self.kwargs:
@ -227,11 +251,6 @@ class ScheduleView(CampViewMixin, TemplateView):
return context return context
class EventDetailView(CampViewMixin, DetailView):
model = models.Event
template_name = 'schedule_event_detail.html'
class CallForSpeakersView(CampViewMixin, TemplateView): class CallForSpeakersView(CampViewMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
return '%s_call_for_speakers.html' % self.camp.slug return '%s_call_for_speakers.html' % self.camp.slug