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