rework speaker and talk proposal stuff
This commit is contained in:
parent
1162a72627
commit
ad3b826844
|
@ -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<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(
|
||||
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<slug>[-_\w+]+)/$',
|
||||
SpeakerDetailView.as_view(),
|
||||
name='speaker_detail'
|
||||
),
|
||||
url(
|
||||
r'^(?P<slug>[-_\w+]+)/edit/$',
|
||||
SpeakerEditView.as_view(),
|
||||
name='speaker_edit'
|
||||
),
|
||||
url(
|
||||
r'^(?P<slug>[-_\w+]+)/pictures/(?P<picture>[-_\w+]+)/$',
|
||||
SpeakerPictureView.as_view(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
148
src/program/migrations/0030_auto_20170312_1230.py
Normal file
148
src/program/migrations/0030_auto_20170312_1230.py
Normal 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')]),
|
||||
),
|
||||
]
|
24
src/program/migrations/0031_auto_20170312_1529.py
Normal file
24
src/program/migrations/0031_auto_20170312_1529.py
Normal 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
36
src/program/mixins.py
Normal 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)
|
||||
|
|
@ -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'})
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
{% load bootstrap3 %}
|
||||
|
||||
{% 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">
|
||||
{% 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" %}
|
||||
</form>
|
||||
|
||||
{% endblock program_content %}
|
|
@ -36,6 +36,11 @@
|
|||
<option value="{{ loc.slug }}" {% if location and location == loc %}selected{% endif %}>{{ loc.name }}</option>
|
||||
{% endfor %}
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% block program_content %}
|
||||
|
||||
<h3>{{ speaker.name }}</h3>
|
||||
h3>{{ speaker.name }}</h3>
|
||||
|
||||
{% if speaker.picture_large and speaker.picture_small %}
|
||||
<div class="row">
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
{% for speaker in speaker_list %}
|
||||
<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 }})
|
||||
{% if not speaker.is_public %}(unpublished, {{ speaker.submission_status }}){% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
37
src/program/templates/speakersubmission_detail.html
Normal file
37
src/program/templates/speakersubmission_detail.html
Normal 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 %}
|
14
src/program/templates/speakersubmission_form.html
Normal file
14
src/program/templates/speakersubmission_form.html
Normal 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 %}
|
||||
|
69
src/program/templates/submission_list.html
Normal file
69
src/program/templates/submission_list.html
Normal 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 %}
|
|
@ -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:
|
||||
# unknown submission status!
|
||||
return
|
||||
raise Http404()
|
||||
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):
|
||||
# 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
|
||||
|
|
Loading…
Reference in a new issue