bornhack-website/src/program/views.py

603 lines
24 KiB
Python
Raw Normal View History

import logging
import os
from collections import OrderedDict
from django.views.generic import ListView, TemplateView, DetailView, View
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.conf import settings
from django.views.decorators.http import require_safe
2017-03-12 14:43:41 +00:00
from django.http import Http404, HttpResponse
from django.utils.decorators import method_decorator
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.urls import reverse, reverse_lazy
2017-08-22 11:00:42 +00:00
from django.template import Engine, Context
from django.shortcuts import redirect
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import get_object_or_404
from betterforms.multiform import MultiModelForm
import icalendar
2017-03-12 14:43:41 +00:00
from camps.mixins import CampViewMixin
from .mixins import (
EnsureUnapprovedProposalMixin,
EnsureUserOwnsProposalMixin,
EnsureWritableCampMixin,
EnsureCFPOpenMixin
)
from .email import (
add_speakerproposal_updated_email,
add_eventproposal_updated_email
)
2017-03-12 14:43:41 +00:00
from . import models
from .utils import get_speakerproposal_form_class, get_eventproposal_form_class
from .forms import BaseSpeakerProposalForm
logger = logging.getLogger("bornhack.%s" % __name__)
2017-03-31 17:25:48 +00:00
###################################################################################################
2018-03-04 14:48:57 +00:00
# ical calendar
2017-03-31 17:25:48 +00:00
class ICSView(CampViewMixin, View):
2017-03-31 17:25:48 +00:00
def get(self, request, *args, **kwargs):
2017-04-13 12:01:50 +00:00
eventinstances = models.EventInstance.objects.filter(event__camp=self.camp)
2017-08-13 22:50:54 +00:00
# Type query
type_query = request.GET.get('type', None)
if type_query:
type_slugs = type_query.split(',')
types = models.EventType.objects.filter(
slug__in=type_slugs
)
eventinstances = eventinstances.filter(event__event_type__in=types)
# Location query
location_query = request.GET.get('location', None)
if location_query:
location_slugs = location_query.split(',')
locations = models.EventLocation.objects.filter(
slug__in=location_slugs,
camp=self.camp,
)
eventinstances = eventinstances.filter(location__in=locations)
# Video recording query
video_query = request.GET.get('video', None)
if video_query:
video_states = video_query.split(',')
query_kwargs = {}
if 'has-recording' in video_states:
query_kwargs['event__video_url__isnull'] = False
if 'to-be-recorded' in video_states:
query_kwargs['event__video_recording'] = True
if 'not-to-be-recorded' in video_states:
if 'event__video_recording' in query_kwargs:
del query_kwargs['event__video_recording']
else:
query_kwargs['event__video_recording'] = False
eventinstances = eventinstances.filter(**query_kwargs)
cal = icalendar.Calendar()
for event_instance in eventinstances:
cal.add_component(event_instance.get_ics_event())
2017-04-13 12:14:44 +00:00
response = HttpResponse(cal.to_ical())
response['Content-Type'] = 'text/calendar'
response['Content-Disposition'] = 'inline; filename={}.ics'.format(self.camp.slug)
return response
###################################################################################################
# proposals list view
2017-03-12 14:43:41 +00:00
class ProposalListView(LoginRequiredMixin, CampViewMixin, ListView):
model = models.SpeakerProposal
template_name = 'proposal_list.html'
context_object_name = 'speakerproposal_list'
2017-03-12 14:43:41 +00:00
def get_queryset(self, **kwargs):
# only show speaker proposals for the current user
2017-03-12 14:43:41 +00:00
return super().get_queryset().filter(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2017-03-14 17:06:23 +00:00
# also add eventproposals to the context
context['eventproposal_list'] = models.EventProposal.objects.filter(track__camp=self.camp, user=self.request.user)
context['eventtype_list'] = models.EventType.objects.filter(public=True)
2017-03-12 14:43:41 +00:00
return context
###################################################################################################
# speakerproposal views
class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, CreateView):
""" This view allows a user to create a new SpeakerProposal linked to an existing EventProposal """
model = models.SpeakerProposal
template_name = 'speakerproposal_form.html'
2017-03-12 14:43:41 +00:00
def dispatch(self, request, *args, **kwargs):
""" Get the eventproposal object """
self.eventproposal = get_object_or_404(models.EventProposal, pk=kwargs['event_uuid'])
return super().dispatch(request, *args, **kwargs)
2017-03-14 17:06:23 +00:00
def get_success_url(self):
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
def get_form_class(self):
return get_speakerproposal_form_class(eventtype=self.eventproposal.event_type)
2017-03-14 17:06:23 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['eventproposal'] = self.eventproposal
return context
2017-03-12 14:43:41 +00:00
def form_valid(self, form):
# set user before saving
form.instance.user = self.request.user
form.instance.camp = self.camp
speakerproposal = form.save()
# add speakerproposal to eventproposal
self.eventproposal.speakers.add(speakerproposal)
return redirect(
reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
)
class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, UpdateView):
"""
This view allows a user to update an existing SpeakerProposal.
"""
model = models.SpeakerProposal
template_name = 'speakerproposal_form.html'
2017-03-12 14:43:41 +00:00
def get_success_url(self):
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
def get_form_class(self):
""" Get the appropriate form class based on the eventtype """
if self.get_object().eventproposals.count() == 1:
# determine which form to use based on the type of event associated with the proposal
return get_speakerproposal_form_class(self.get_object().eventproposals.get().event_type)
else:
# more than one eventproposal. If all events are the same type we can still show a non-generic form here
eventtypes = set()
for ep in self.get_object().eventproposals.all():
eventtypes.add(ep.event_type)
if len(eventtypes) == 1:
return get_speakerproposal_form_class(ep.event_type)
# more than one type of event for this person, return the generic speakerproposal form
return BaseSpeakerProposalForm
class SpeakerProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DeleteView):
"""
This view allows a user to delete an existing SpeakerProposal object, as long as it is not linked to any EventProposals
"""
model = models.SpeakerProposal
template_name = 'proposal_delete.html'
2017-03-12 14:43:41 +00:00
def get(self, request, *args, **kwargs):
# do not permit deleting if this speakerproposal is linked to any eventproposals
if self.get_object().eventproposals.exists():
messages.error(request, "Cannot delete a person while it is associated with one or more eventproposals. Delete those first.")
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
# continue with the request
return super().get(request, *args, **kwargs)
def get_success_url(self):
messages.success(self.request, "Proposal '%s' has been deleted." % self.object.name)
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DetailView):
model = models.SpeakerProposal
template_name = 'speakerproposal_detail.html'
###################################################################################################
# eventproposal views
class EventProposalTypeSelectView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, ListView):
"""
This view is for selecting the type of event to submit (when adding a new eventproposal to an existing speakerproposal)
"""
model = models.EventType
template_name = 'event_type_select.html'
def dispatch(self, request, *args, **kwargs):
""" Get the speakerproposal object """
self.speaker = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
""" We only allow submissions of events with EventTypes where public=True """
return super().get_queryset().filter(public=True)
def get_context_data(self, *args, **kwargs):
""" Make speakerproposal object available in template """
context = super().get_context_data(**kwargs)
context['speaker'] = self.speaker
return context
2017-03-12 14:43:41 +00:00
class EventProposalSelectPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, ListView):
"""
This view is for selecting an existing speakerproposal to add to an existing eventproposal
"""
model = models.SpeakerProposal
template_name = 'event_proposal_select_person.html'
def dispatch(self, request, *args, **kwargs):
""" Get EventProposal from url kwargs """
self.eventproposal = get_object_or_404(models.EventProposal, pk=kwargs['event_uuid'], user=request.user)
return super().dispatch(request, *args, **kwargs)
2017-03-12 14:43:41 +00:00
def get_queryset(self, **kwargs):
""" Filter out any speakerproposals already added to this eventproposal """
return self.eventproposal.get_available_speakerproposals().all()
2017-03-29 22:20:14 +00:00
def get_context_data(self, *args, **kwargs):
""" Make eventproposal object available in template """
context = super().get_context_data(**kwargs)
context['eventproposal'] = self.eventproposal
return context
class EventProposalAddPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView):
"""
This view is for adding an existing speakerproposal to an existing eventproposal
"""
model = models.EventProposal
template_name = 'event_proposal_add_person.html'
fields = []
pk_url_kwarg = 'event_uuid'
2017-03-12 14:43:41 +00:00
def dispatch(self, request, *args, **kwargs):
""" Get the speakerproposal object """
self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'], user=request.user)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
""" Make speakerproposal object available in template """
2017-03-12 14:43:41 +00:00
context = super().get_context_data(**kwargs)
context['speakerproposal'] = self.speakerproposal
2017-03-12 14:43:41 +00:00
return context
def form_valid(self, form):
form.instance.speakers.add(self.speakerproposal)
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, CreateView):
"""
This view allows a user to create a new eventproposal linked to an existing speakerproposal
"""
model = models.EventProposal
template_name = 'eventproposal_form.html'
def get_form_class(self):
""" Get the appropriate form class based on the eventtype """
return get_eventproposal_form_class(self.event_type)
2017-03-12 15:16:24 +00:00
def dispatch(self, request, *args, **kwargs):
""" Get the speakerproposal object """
self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=self.kwargs['speaker_uuid'])
self.event_type = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
""" Make speakerproposal object available in template """
context = super().get_context_data(**kwargs)
context['speaker'] = self.speakerproposal
context['event_type'] = self.event_type
return context
def get_form(self):
"""
Override get_form() method so we can set the queryset for the track selector.
Usually this kind of thing would go into get_initial() but that does not work for some reason, so we do it here instead.
"""
form_class = self.get_form_class()
form = form_class(**self.get_form_kwargs())
form.fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp)
return form
def form_valid(self, form):
# set camp and user for this eventproposal
eventproposal = form.save(commit=False)
eventproposal.user = self.request.user
eventproposal.event_type = self.event_type
eventproposal.save()
# add the speakerproposal to the eventproposal
eventproposal.speakers.add(self.speakerproposal)
# all good
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, UpdateView):
model = models.EventProposal
template_name = 'eventproposal_form.html'
def get_form_class(self):
""" Get the appropriate form class based on the eventtype """
return get_eventproposal_form_class(self.get_object().event_type)
def get_success_url(self):
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
def get_context_data(self, *args, **kwargs):
""" Make speakerproposal and eventtype objects available in the template """
context = super().get_context_data(**kwargs)
context['event_type'] = self.get_object().event_type
return context
def get_form(self):
"""
Override get_form() method so we can set the queryset for the track selector.
Usually this kind of thing would go into get_initial() but that does not work for some reason, so we do it here instead.
"""
form_class = self.get_form_class()
form = form_class(**self.get_form_kwargs())
form.fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp)
return form
class EventProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DeleteView):
model = models.EventProposal
template_name = 'proposal_delete.html'
def get_success_url(self):
messages.success(self.request, "Proposal '%s' has been deleted." % self.object.title)
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
2017-03-12 14:43:41 +00:00
class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DetailView):
model = models.EventProposal
template_name = 'eventproposal_detail.html'
###################################################################################################
# combined proposal views
class CombinedProposalTypeSelectView(LoginRequiredMixin, CampViewMixin, ListView):
"""
A view which allows the user to select event type without anything else on the page
"""
model = models.EventType
template_name = 'event_type_select.html'
def get_queryset(self, **kwargs):
""" We only allow submissions of events with EventTypes where public=True """
return super().get_queryset().filter(public=True)
class CombinedProposalPersonSelectView(LoginRequiredMixin, CampViewMixin, ListView):
"""
A view which allows the user to 1) choose between existing SpeakerProposals or
2) pressing a button to create a new SpeakerProposal.
Redirect straight to 2) if no existing SpeakerProposals exist.
"""
model = models.SpeakerProposal
template_name = 'combined_proposal_select_person.html'
def dispatch(self, request, *args, **kwargs):
"""
Check that we have a valid EventType
"""
# get EventType from url kwargs
self.eventtype = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
# only show speaker proposals for the current user
return super().get_queryset().filter(user=self.request.user)
def get_context_data(self, **kwargs):
"""
Add EventType to template context
"""
context = super().get_context_data(**kwargs)
context['eventtype'] = self.eventtype
return context
def get(self, request, *args, **kwargs):
""" If we don't have any existing SpeakerProposals just redirect directly to the combined submit view """
if not self.get_queryset().exists():
return redirect(reverse_lazy('program:proposal_combined_submit', kwargs={'camp_slug': self.camp.slug, 'event_type_slug': self.eventtype.slug}))
return super().get(request, *args, **kwargs)
class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView):
"""
This view is used by users to submit CFP proposals.
It allows the user to submit an EventProposal and a SpeakerProposal together.
It can also be used with a preselected SpeakerProposal uuid in url kwargs
"""
template_name = 'combined_proposal_submit.html'
def dispatch(self, request, *args, **kwargs):
"""
Check that we have a valid EventType
"""
# get EventType from url kwargs
self.eventtype = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug'])
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
Add EventType to template context
"""
context = super().get_context_data(**kwargs)
context['eventtype'] = self.eventtype
return context
def form_valid(self, form):
"""
Save the object(s) here before redirecting
"""
if hasattr(self, 'speakerproposal'):
eventproposal = form.save(commit=False)
eventproposal.user = self.request.user
eventproposal.event_type = self.eventtype
eventproposal.save()
eventproposal.speakers.add(self.speakerproposal)
else:
# first save the SpeakerProposal
speakerproposal = form['speakerproposal'].save(commit=False)
speakerproposal.camp = self.camp
speakerproposal.user = self.request.user
speakerproposal.save()
# then save the eventproposal
eventproposal = form['eventproposal'].save(commit=False)
eventproposal.user = self.request.user
eventproposal.event_type = self.eventtype
eventproposal.save()
# add the speakerproposal to the eventproposal
eventproposal.speakers.add(speakerproposal)
# all good
return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
def get_form_class(self):
"""
Unless we have an existing SpeakerProposal we must show two forms on the page.
We use betterforms.MultiModelForm to combine two forms on the page
"""
if hasattr(self, 'speakerproposal'):
# we already have a speakerproposal, just show an eventproposal form
return get_eventproposal_form_class(eventtype=self.eventtype)
# get the two forms we need to build the MultiModelForm
SpeakerProposalForm = get_speakerproposal_form_class(eventtype=self.eventtype)
EventProposalForm = get_eventproposal_form_class(eventtype=self.eventtype)
# build our MultiModelForm
class CombinedProposalSubmitForm(MultiModelForm):
form_classes = OrderedDict((
('speakerproposal', SpeakerProposalForm),
('eventproposal', EventProposalForm),
))
# return the form class
return CombinedProposalSubmitForm
def get_form(self):
"""
Override get_form() method so we can set the queryset for the track selector.
Usually this kind of thing would go into get_initial() but that does not work for some reason, so we do it here instead.
"""
form_class = self.get_form_class()
form = form_class(**self.get_form_kwargs())
form.forms['eventproposal'].fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp)
return form
###################################################################################################
# speaker views
2016-07-13 20:37:20 +00:00
2016-08-08 17:45:32 +00:00
class SpeakerDetailView(CampViewMixin, DetailView):
2016-08-08 17:45:32 +00:00
model = models.Speaker
template_name = 'speaker_detail.html'
class SpeakerListView(CampViewMixin, ListView):
2016-08-08 17:36:13 +00:00
model = models.Speaker
template_name = 'speaker_list.html'
###################################################################################################
# event views
2017-01-23 17:57:30 +00:00
class EventListView(CampViewMixin, ListView):
2016-08-08 17:36:13 +00:00
model = models.Event
template_name = 'event_list.html'
2016-07-13 20:37:20 +00:00
2017-03-12 14:43:41 +00:00
class EventDetailView(CampViewMixin, DetailView):
model = models.Event
template_name = 'schedule_event_detail.html'
###################################################################################################
# schedule views
2017-03-12 14:43:41 +00:00
class NoScriptScheduleView(CampViewMixin, TemplateView):
template_name = "noscript_schedule_view.html"
2016-07-13 20:37:20 +00:00
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
context['eventinstances'] = models.EventInstance.objects.filter(event__camp=self.camp).order_by('when')
return context
class ScheduleView(CampViewMixin, TemplateView):
template_name = 'schedule_overview.html'
2017-01-23 22:58:41 +00:00
def get_context_data(self, *args, **kwargs):
context = super(ScheduleView, self).get_context_data(**kwargs)
2018-03-04 14:48:57 +00:00
context['schedule_midnight_offset_hours'] = settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS
return context
2016-08-07 13:49:30 +00:00
class CallForParticipationView(CampViewMixin, TemplateView):
def get_template_names(self):
return '%s_call_for_participation.html' % self.camp.slug
###################################################################################################
# control center csv
class ProgramControlCenter(CampViewMixin, TemplateView):
template_name = "control/index.html"
@method_decorator(staff_member_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
proposals = models.EventProposal.objects.filter(
camp=self.camp
).select_related('user', 'event')
context['proposals'] = proposals
2017-08-22 11:00:42 +00:00
engine = Engine.get_default()
template = engine.get_template('control/proposal_overview.csv')
csv = template.render(Context(context))
context['csv'] = csv
return context