* update font-awesome to 5.1 and add fab- and fa-regular icons, fixed #244 * also handle slugs when dealing with SingleObjectMixin CBV views, fixes #248 * add missing trailing slash to bar/menu * add missing trailing slash on event slugs, fixes #245 * fix a lot of stuff related to proposal URLs. actually display them on speaker and event detail pages. show the urltype icons. better messages on update and delete pages. fix 500 error when reapproving events. fix bug that made urls multiply when reapproving event. fixes #246 and fixes #247
806 lines
31 KiB
Python
806 lines
31 KiB
Python
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
|
|
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
|
|
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
|
|
|
|
from camps.mixins import CampViewMixin
|
|
from .mixins import (
|
|
EnsureUnapprovedProposalMixin,
|
|
EnsureUserOwnsProposalMixin,
|
|
EnsureWritableCampMixin,
|
|
EnsureCFPOpenMixin,
|
|
UrlViewMixin,
|
|
)
|
|
from .email import (
|
|
add_new_eventproposal_email,
|
|
add_new_speakerproposal_email,
|
|
add_speakerproposal_updated_email,
|
|
add_eventproposal_updated_email
|
|
)
|
|
from . import models
|
|
from .forms import SpeakerProposalForm, EventProposalForm
|
|
|
|
|
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
|
|
|
|
|
###################################################################################################
|
|
# ical calendar
|
|
|
|
|
|
class ICSView(CampViewMixin, View):
|
|
def get(self, request, *args, **kwargs):
|
|
eventinstances = models.EventInstance.objects.filter(
|
|
event__track__camp=self.camp
|
|
)
|
|
|
|
# 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())
|
|
|
|
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
|
|
|
|
|
|
class ProposalListView(LoginRequiredMixin, CampViewMixin, ListView):
|
|
model = models.SpeakerProposal
|
|
template_name = 'proposal_list.html'
|
|
context_object_name = 'speakerproposal_list'
|
|
|
|
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):
|
|
context = super().get_context_data(**kwargs)
|
|
# 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)
|
|
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'
|
|
form_class = SpeakerProposalForm
|
|
|
|
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)
|
|
|
|
def get_success_url(self):
|
|
return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})
|
|
|
|
def get_form_kwargs(self):
|
|
"""
|
|
Set camp and eventtype for the form
|
|
"""
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs.update({
|
|
'camp': self.camp,
|
|
'eventtype': self.eventproposal.event_type
|
|
})
|
|
return kwargs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['eventproposal'] = self.eventproposal
|
|
return context
|
|
|
|
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)
|
|
|
|
# send mail to content team
|
|
if not add_new_speakerproposal_email(speakerproposal):
|
|
logger.error("Unable to send email to content team after new 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'
|
|
form_class = SpeakerProposalForm
|
|
|
|
def get_form_kwargs(self):
|
|
"""
|
|
Set camp and eventtype for the form
|
|
"""
|
|
kwargs = super().get_form_kwargs()
|
|
if self.get_object().eventproposals.count() == 1:
|
|
# determine which form to use based on the type of event associated with the proposal
|
|
eventtype = 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:
|
|
eventtype = self.get_object().eventproposals.get().event_type
|
|
else:
|
|
# more than one type of event for this person, return the generic speakerproposal form
|
|
eventtype = None
|
|
|
|
# add camp and eventtype to form kwargs
|
|
kwargs.update({
|
|
'camp': self.camp,
|
|
'eventtype': eventtype
|
|
})
|
|
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
"""
|
|
Change the speakerproposal status to pending
|
|
"""
|
|
# set proposal status to pending
|
|
form.instance.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
speakerproposal = form.save()
|
|
|
|
# send mail to content team
|
|
if not add_speakerproposal_updated_email(speakerproposal):
|
|
logger.error("Unable to send email to content team after speakerproposal update")
|
|
|
|
# message user and redirect
|
|
messages.info(self.request, "Your proposal is now pending approval by the content team.")
|
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
|
|
|
|
|
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'
|
|
|
|
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, EnsureUserOwnsProposalMixin, 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
|
|
|
|
|
|
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)
|
|
|
|
def get_queryset(self, **kwargs):
|
|
""" Filter out any speakerproposals already added to this eventproposal """
|
|
return self.eventproposal.get_available_speakerproposals().all()
|
|
|
|
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'
|
|
|
|
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 """
|
|
context = super().get_context_data(**kwargs)
|
|
context['speakerproposal'] = self.speakerproposal
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
form.instance.speakers.add(self.speakerproposal)
|
|
messages.success(self.request, "%s has been added as %s for %s" % (
|
|
self.speakerproposal.name,
|
|
form.instance.event_type.host_title,
|
|
form.instance.title
|
|
))
|
|
return redirect(self.get_success_url())
|
|
|
|
|
|
class EventProposalRemovePersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView):
|
|
"""
|
|
This view is for removing a speakerproposal from an existing eventproposal
|
|
"""
|
|
model = models.EventProposal
|
|
template_name = 'event_proposal_remove_person.html'
|
|
fields = []
|
|
pk_url_kwarg = 'event_uuid'
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
""" Get the speakerproposal object and check a few things """
|
|
# get the speakerproposal object from URL kwargs
|
|
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 """
|
|
context = super().get_context_data(**kwargs)
|
|
context['speakerproposal'] = self.speakerproposal
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
""" Remove the speaker from the event """
|
|
if self.speakerproposal not in self.get_object().speakers.all():
|
|
# this speaker is not associated with this event
|
|
raise Http404
|
|
|
|
if self.get_object().speakers.count() == 1:
|
|
messages.error(self.request, "Cannot delete the last person associalted with event!")
|
|
return redirect(self.get_success_url())
|
|
|
|
# remove speakerproposal from eventproposal
|
|
form.instance.speakers.remove(self.speakerproposal)
|
|
messages.success(self.request, "%s has been removed from %s" % (
|
|
self.speakerproposal.name,
|
|
self.get_object().title
|
|
))
|
|
return redirect(self.get_success_url())
|
|
|
|
def get_success_url(self):
|
|
return reverse(
|
|
'program:eventproposal_detail', kwargs={
|
|
'camp_slug': self.camp.slug,
|
|
'pk': self.get_object().uuid
|
|
})
|
|
|
|
|
|
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'
|
|
form_class = EventProposalForm
|
|
|
|
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_kwargs(self):
|
|
"""
|
|
Set camp and eventtype for the form
|
|
"""
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs.update({
|
|
'camp': self.camp,
|
|
'eventtype': self.event_type
|
|
})
|
|
return kwargs
|
|
|
|
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)
|
|
|
|
# send mail to content team
|
|
if not add_new_eventproposal_email(eventproposal):
|
|
logger.error("Unable to send email to content team after new eventproposal")
|
|
|
|
# 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'
|
|
form_class = EventProposalForm
|
|
|
|
def get_form_kwargs(self):
|
|
"""
|
|
Set camp and eventtype for the form
|
|
"""
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs.update({
|
|
'camp': self.camp,
|
|
'eventtype': self.get_object().event_type
|
|
})
|
|
return kwargs
|
|
|
|
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 form_valid(self, form):
|
|
# set status to pending and save eventproposal
|
|
form.instance.proposal_status = models.EventProposal.PROPOSAL_PENDING
|
|
eventproposal = form.save()
|
|
|
|
# send email to content team
|
|
if not add_eventproposal_updated_email(eventproposal):
|
|
logger.error("Unable to send email to content team after eventproposal update")
|
|
|
|
# message for the user and redirect
|
|
messages.info(self.request, "Your proposal is now pending approval by the content team.")
|
|
return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
|
|
|
|
|
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})
|
|
|
|
|
|
class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, 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)
|
|
|
|
# send mail(s) to content team
|
|
if not add_new_eventproposal_email(eventproposal):
|
|
logger.error("Unable to send email to content team after new eventproposal")
|
|
if not hasattr(self, 'speakerproposal'):
|
|
if not add_new_speakerproposal_email(speakerproposal):
|
|
logger.error("Unable to send email to content team after new 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 EventProposalForm
|
|
|
|
# build our MultiModelForm
|
|
class CombinedProposalSubmitForm(MultiModelForm):
|
|
form_classes = OrderedDict((
|
|
('speakerproposal', SpeakerProposalForm),
|
|
('eventproposal', EventProposalForm),
|
|
))
|
|
|
|
# return the form class
|
|
return CombinedProposalSubmitForm
|
|
|
|
def get_form_kwargs(self):
|
|
"""
|
|
Set camp and eventtype for the form
|
|
"""
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs.update({
|
|
'camp': self.camp,
|
|
'eventtype': self.eventtype
|
|
})
|
|
return kwargs
|
|
|
|
|
|
###################################################################################################
|
|
# speaker views
|
|
|
|
|
|
class SpeakerDetailView(CampViewMixin, DetailView):
|
|
model = models.Speaker
|
|
template_name = 'speaker_detail.html'
|
|
|
|
|
|
class SpeakerListView(CampViewMixin, ListView):
|
|
model = models.Speaker
|
|
template_name = 'speaker_list.html'
|
|
|
|
|
|
###################################################################################################
|
|
# event views
|
|
|
|
|
|
class EventListView(CampViewMixin, ListView):
|
|
model = models.Event
|
|
template_name = 'event_list.html'
|
|
|
|
|
|
class EventDetailView(CampViewMixin, DetailView):
|
|
model = models.Event
|
|
template_name = 'schedule_event_detail.html'
|
|
|
|
|
|
###################################################################################################
|
|
# schedule views
|
|
|
|
|
|
class NoScriptScheduleView(CampViewMixin, TemplateView):
|
|
template_name = "noscript_schedule_view.html"
|
|
|
|
def get_context_data(self, *args, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['eventinstances'] = models.EventInstance.objects.filter(
|
|
event__track__camp=self.camp
|
|
).order_by('when')
|
|
return context
|
|
|
|
|
|
class ScheduleView(CampViewMixin, TemplateView):
|
|
template_name = 'schedule_overview.html'
|
|
|
|
def get_context_data(self, *args, **kwargs):
|
|
context = super(ScheduleView, self).get_context_data(**kwargs)
|
|
context['schedule_midnight_offset_hours'] = settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS
|
|
return context
|
|
|
|
|
|
class CallForParticipationView(CampViewMixin, TemplateView):
|
|
template_name = 'call_for_participation.html'
|
|
|
|
|
|
###################################################################################################
|
|
# 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
|
|
|
|
engine = Engine.get_default()
|
|
template = engine.get_template('control/proposal_overview.csv')
|
|
csv = template.render(Context(context))
|
|
context['csv'] = csv
|
|
|
|
return context
|
|
|
|
###################################################################################################
|
|
# URL views
|
|
|
|
class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, CreateView):
|
|
model = models.Url
|
|
template_name = 'url_form.html'
|
|
fields = ['urltype', 'url']
|
|
|
|
def form_valid(self, form):
|
|
"""
|
|
Set the proposal FK before saving
|
|
Set proposal as pending if it isn't already
|
|
"""
|
|
if hasattr(self, 'eventproposal') and self.eventproposal:
|
|
# this URL belongs to an eventproposal
|
|
form.instance.eventproposal = self.eventproposal
|
|
url = form.save()
|
|
if self.eventproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING:
|
|
self.eventproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
self.eventproposal.save()
|
|
messages.success(self.request, "%s is now pending review by the Content Team." % self.eventproposal.title)
|
|
else:
|
|
# this URL belongs to a speakerproposal
|
|
form.instance.speakerproposal = self.speakerproposal
|
|
url = form.save()
|
|
if self.speakerproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING:
|
|
self.speakerproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
self.speakerproposal.save()
|
|
messages.success(self.request, "%s is now pending review by the Content Team." % self.speakerproposal.name)
|
|
|
|
messages.success(self.request, "URL saved.")
|
|
|
|
# all good
|
|
return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
|
|
|
|
|
class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, UpdateView):
|
|
model = models.Url
|
|
template_name = 'url_form.html'
|
|
fields = ['urltype', 'url']
|
|
pk_url_kwarg = 'url_uuid'
|
|
|
|
def form_valid(self, form):
|
|
"""
|
|
Set proposal as pending if it isn't already
|
|
"""
|
|
if hasattr(self, 'eventproposal') and self.eventproposal:
|
|
# this URL belongs to a speakerproposal
|
|
url = form.save()
|
|
if self.eventproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING:
|
|
self.eventproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
self.eventproposal.save()
|
|
messages.success(self.request, "%s is now pending review by the Content Team." % self.eventproposal.title)
|
|
else:
|
|
# this URL belongs to a speakerproposal
|
|
url = form.save()
|
|
if self.speakerproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING:
|
|
self.speakerproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
self.speakerproposal.save()
|
|
messages.success(self.request, "%s is now pending review by the Content Team." % self.speakerproposal.name)
|
|
|
|
messages.success(self.request, "URL saved.")
|
|
|
|
# all good
|
|
return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
|
|
|
|
|
class UrlDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, DeleteView):
|
|
model = models.Url
|
|
template_name = 'url_delete.html'
|
|
pk_url_kwarg = 'url_uuid'
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
"""
|
|
Set proposal as pending if it isn't already
|
|
"""
|
|
if hasattr(self, 'eventproposal') and self.eventproposal:
|
|
# this URL belongs to a speakerproposal
|
|
if self.eventproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING:
|
|
self.eventproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
self.eventproposal.save()
|
|
messages.success(self.request, "%s is now pending review by the Content Team." % self.eventproposal.title)
|
|
else:
|
|
# this URL belongs to a speakerproposal
|
|
if self.speakerproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING:
|
|
self.speakerproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING
|
|
self.speakerproposal.save()
|
|
messages.success(self.request, "%s is now pending review by the Content Team." % self.speakerproposal.name)
|
|
|
|
self.object = self.get_object()
|
|
self.object.delete()
|
|
messages.success(self.request, "URL deleted.")
|
|
|
|
# all good
|
|
return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug}))
|
|
|