import logging from collections import OrderedDict import icalendar from camps.mixins import CampViewMixin from django import forms from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.mixins import LoginRequiredMixin from django.http import Http404, HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect from django.template import Context, Engine from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic.edit import CreateView, DeleteView, UpdateView from lxml import etree, objectify from utils.middleware import RedirectException from utils.mixins import UserIsObjectOwnerMixin from . import models from .email import ( add_event_proposal_updated_email, add_new_event_proposal_email, add_new_speaker_proposal_email, add_speaker_proposal_updated_email, ) from .forms import EventProposalForm, SpeakerProposalForm from .mixins import ( AvailabilityMatrixViewMixin, EnsureCFPOpenMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, EventFeedbackViewMixin, EventViewMixin, UrlViewMixin, ) from .multiform import MultiModelForm from .utils import get_speaker_availability_form_matrix, save_speaker_availability logger = logging.getLogger("bornhack.%s" % __name__) ################################################################### # ical calendar class ICSView(CampViewMixin, View): def get(self, request, *args, **kwargs): query_kwargs = {} # Type query type_query = request.GET.get("type", None) if type_query: type_slugs = type_query.split(",") query_kwargs["event__event_type__in"] = models.EventType.objects.filter( slug__in=type_slugs ) # Location query location_query = request.GET.get("location", None) if location_query: location_slugs = location_query.split(",") query_kwargs["location__in"] = models.EventLocation.objects.filter( slug__in=location_slugs, camp=self.camp ) # Video recording query video_query = request.GET.get("video", None) if video_query: video_states = video_query.split(",") 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 event_slots = models.EventSlot.objects.filter( event__track__camp=self.camp, **query_kwargs, ).prefetch_related("event", "event_session__event_location") cal = icalendar.Calendar() cal.add("prodid", "-//BornHack Website iCal Generator//bornhack.dk//") cal.add("version", "2.0") for slot in event_slots: cal.add_component(slot.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 = "speaker_proposal_list" def get_queryset(self, **kwargs): """ Only show speaker proposals for the current user """ return ( super() .get_queryset() .filter(user=self.request.user) .prefetch_related( "event_proposals", "event_proposals__event_type", "urls__url_type", "speaker", ) ) def get_context_data(self, **kwargs): """ Add event_proposals to the context """ context = super().get_context_data(**kwargs) context["event_proposal_list"] = models.EventProposal.objects.filter( track__camp=self.camp, user=self.request.user ).prefetch_related("event_type", "track", "urls__url_type", "event", "speakers") context["event_type_list"] = models.EventType.objects.filter(public=True) return context ################################################################### # speaker_proposal 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 = "speaker_proposal_form.html" form_class = SpeakerProposalForm def setup(self, *args, **kwargs): """ Get the event_proposal object and speaker availability matrix""" super().setup(*args, **kwargs) """ Get the event_proposal and availability matrix """ self.event_proposal = get_object_or_404( models.EventProposal, pk=kwargs["event_uuid"] ) self.matrix = get_speaker_availability_form_matrix( sessions=self.camp.event_sessions.filter( event_type=self.event_proposal.event_type ) ) def get_success_url(self): return reverse("program:proposal_list", kwargs={"camp_slug": self.camp.slug}) def get_form_kwargs(self): """ Set camp and event_type for the form """ kwargs = super().get_form_kwargs() kwargs.update( { "camp": self.camp, "event_type": self.event_proposal.event_type, "matrix": self.matrix, } ) return kwargs def get_initial(self, *args, **kwargs): """ Populate the speaker_availability checkboxes """ initial = super().get_initial(*args, **kwargs) # loop over dates in the matrix for date in self.matrix.keys(): # loop over daychunks and check if we need a checkbox for daychunk in self.matrix[date].keys(): if self.matrix[date][daychunk]: # default to checked for new speakers initial[self.matrix[date][daychunk]["fieldname"]] = True return initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["event_proposal"] = self.event_proposal context["matrix"] = self.matrix return context def form_valid(self, form): """ Set user and camp before saving, then save availability """ speaker_proposal = form.save(commit=False) speaker_proposal.user = self.request.user speaker_proposal.camp = self.camp if not form.cleaned_data["email"]: # default to submitters email speaker_proposal.email = self.request.user.email # save speaker_proposal speaker_proposal = form.save() form.save_m2m() # then save speaker availability objects save_speaker_availability(form, speaker_proposal) # add speaker_proposal to event_proposal self.event_proposal.speakers.add(speaker_proposal) # send mail to content team if not add_new_speaker_proposal_email(speaker_proposal): logger.error( "Unable to send email to content team after new speaker_proposal" ) return redirect( reverse("program:proposal_list", kwargs={"camp_slug": self.camp.slug}) ) class SpeakerProposalUpdateView( LoginRequiredMixin, AvailabilityMatrixViewMixin, EnsureWritableCampMixin, UserIsObjectOwnerMixin, EnsureCFPOpenMixin, UpdateView, ): """ This view allows a user to update an existing SpeakerProposal. """ model = models.SpeakerProposal template_name = "speaker_proposal_form.html" form_class = SpeakerProposalForm def get_object(self, queryset=None): """ Prefetch availabilities for this SpeakerProposal """ qs = self.model.objects.filter(pk=self.kwargs.get(self.pk_url_kwarg)) qs = qs.prefetch_related("availabilities") return qs.get() def get_form_kwargs(self): """ Set camp, matrix and event_type for the form """ kwargs = super().get_form_kwargs() # get all event types this SpeakerProposal is involved in all_event_types = models.EventType.objects.filter( event_proposals__in=self.get_object().event_proposals.all() ).distinct() if len(all_event_types) == 1: # use the event_type to customise the speaker form event_type = all_event_types[0] else: # more than one event_type for this speaker, show a non-generic form event_type = None # add camp and event_type to form kwargs kwargs.update( {"camp": self.camp, "event_type": event_type, "matrix": self.matrix} ) return kwargs def form_valid(self, form): """ Change status and save availability """ speaker_proposal = form.save(commit=False) # set proposal status to pending speaker_proposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING if not speaker_proposal.email: # default to submitters email speaker_proposal.email = self.request.user.email # ok save() for real speaker_proposal.save() form.save_m2m() # then save speaker availability objects save_speaker_availability(form, speaker_proposal) # send mail to content team if not add_speaker_proposal_updated_email(speaker_proposal): logger.error( "Unable to send email to content team after speaker_proposal update" ) # message user and redirect messages.info( self.request, f"Changes to your proposal is now pending approval by the content team.", ) return redirect( reverse( "program:speaker_proposal_detail", kwargs={"camp_slug": self.camp.slug, "pk": speaker_proposal.pk}, ) ) 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 speaker_proposal is linked to any event_proposals if self.get_object().event_proposals.exists(): messages.error( request, "Cannot delete a person while it is associated with one or more event_proposals. 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 delete(self, *args, **kwargs): """ Delete availabilities before deleting the proposal """ self.get_object().availabilities.all().delete() return super().delete(*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, AvailabilityMatrixViewMixin, EnsureUserOwnsProposalMixin, DetailView, ): model = models.SpeakerProposal template_name = "speaker_proposal_detail.html" context_object_name = "speaker_proposal" ################################################################### # event_proposal views class EventProposalTypeSelectView( LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, ListView, ): """ This view is for selecting the type of event to submit (when adding a new event_proposal to an existing speaker_proposal) """ model = models.EventType template_name = "event_type_select.html" context_object_name = "event_type_list" def setup(self, *args, **kwargs): """ Get the speaker_proposal object """ super().setup(*args, **kwargs) self.speaker = get_object_or_404( models.SpeakerProposal, pk=kwargs["speaker_uuid"] ) 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 speaker_proposal 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 speaker_proposal to add to an existing event_proposal """ model = models.SpeakerProposal template_name = "event_proposal_select_person.html" context_object_name = "speaker_proposal_list" def dispatch(self, request, *args, **kwargs): """ Get EventProposal from url kwargs """ self.event_proposal = 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 speaker_proposals already added to this event_proposal """ return self.event_proposal.get_available_speaker_proposals().all() def get_context_data(self, *args, **kwargs): """ Make event_proposal object available in template """ context = super().get_context_data(**kwargs) context["event_proposal"] = self.event_proposal return context class EventProposalAddPersonView( LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView, ): """ This view is for adding an existing speaker_proposal to an existing event_proposal """ model = models.EventProposal template_name = "event_proposal_add_person.html" fields = [] pk_url_kwarg = "event_uuid" context_object_name = "event_proposal" def dispatch(self, request, *args, **kwargs): """ Get the speaker_proposal object """ self.speaker_proposal = 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 speaker_proposal object available in template """ context = super().get_context_data(**kwargs) context["speaker_proposal"] = self.speaker_proposal return context def form_valid(self, form): form.instance.speakers.add(self.speaker_proposal) messages.success( self.request, "%s has been added as %s for %s" % ( self.speaker_proposal.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 speaker_proposal from an existing event_proposal """ model = models.EventProposal template_name = "event_proposal_remove_person.html" fields = [] pk_url_kwarg = "event_uuid" def dispatch(self, request, *args, **kwargs): """ Get the speaker_proposal object and check a few things """ # get the speaker_proposal object from URL kwargs self.speaker_proposal = 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 speaker_proposal object available in template """ context = super().get_context_data(**kwargs) context["speaker_proposal"] = self.speaker_proposal return context def form_valid(self, form): """ Remove the speaker from the event """ if self.speaker_proposal 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 speaker_proposal from event_proposal form.instance.speakers.remove(self.speaker_proposal) messages.success( self.request, "%s has been removed from %s" % (self.speaker_proposal.name, self.get_object().title), ) return redirect(self.get_success_url()) def get_success_url(self): return reverse( "program:event_proposal_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 event_proposal linked to an existing speaker_proposal """ model = models.EventProposal template_name = "event_proposal_form.html" form_class = EventProposalForm def setup(self, *args, **kwargs): """ Get the speaker_proposal object """ super().setup(*args, **kwargs) self.speaker_proposal = 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"] ) def get_context_data(self, *args, **kwargs): """ Make speaker_proposal object available in template """ context = super().get_context_data(**kwargs) context["speaker"] = self.speaker_proposal context["event_type"] = self.event_type return context def get_form_kwargs(self): """ Set camp and event_type for the form """ kwargs = super().get_form_kwargs() kwargs.update({"camp": self.camp, "event_type": self.event_type}) return kwargs def form_valid(self, form): """ set camp and user for this event_proposal, save slideurl """ event_proposal = form.save(commit=False) event_proposal.user = self.request.user event_proposal.event_type = self.event_type # save for real event_proposal.save() form.save_m2m() # save or update slides url slideurl = form.cleaned_data.get("slides_url") if slideurl: slides_url, created = models.Url.objects.get_or_create( event_proposal=event_proposal, url_type=models.UrlType.objects.get(name="Slides"), defaults={"url": slideurl}, ) # add the speaker_proposal to the event_proposal event_proposal.speakers.add(self.speaker_proposal) # send mail to content team if not add_new_event_proposal_email(event_proposal): logger.error( "Unable to send email to content team after new event_proposal" ) messages.success( self.request, "Your event proposal is now pending approval by the content team.", ) # 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 = "event_proposal_form.html" form_class = EventProposalForm def get_form_kwargs(self): """ Set camp and event_type for the form """ kwargs = super().get_form_kwargs() kwargs.update({"camp": self.camp, "event_type": self.get_object().event_type}) return kwargs def get_context_data(self, *args, **kwargs): """ Make speaker_proposal and event_type 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 event_proposal event_proposal = form.save(commit=False) event_proposal.proposal_status = models.EventProposal.PROPOSAL_PENDING # save or update slides url slideurl = form.cleaned_data.get("slides_url") if slideurl: slides_url, created = models.Url.objects.get_or_create( event_proposal=event_proposal, url_type=models.UrlType.objects.get(name="Slides"), defaults={"url": slideurl}, ) # save for real event_proposal.save() form.save_m2m() # send email to content team if not add_event_proposal_updated_email(event_proposal): logger.error( "Unable to send email to content team after event_proposal update" ) # message for the user and redirect messages.info( self.request, "Changes to your event proposal is now pending approval by the content team.", ) return redirect( reverse( "program:event_proposal_detail", kwargs={"camp_slug": self.camp.slug, "pk": event_proposal.pk}, ) ) 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 = "event_proposal_detail.html" context_object_name = "event_proposal" ################################################################### # 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" context_object_name = "speaker_proposal_list" def setup(self, *args, **kwargs): """ Check that we have a valid EventType """ super().setup(*args, **kwargs) self.event_type = get_object_or_404( models.EventType, slug=self.kwargs["event_type_slug"] ) 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["event_type"] = self.event_type 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.event_type.slug, }, ) ) return super().get(request, *args, **kwargs) class CombinedProposalSubmitView( LoginRequiredMixin, CampViewMixin, EnsureCFPOpenMixin, CreateView ): """ This view is used by users to submit CFP proposals. It allows the user to submit an EventProposal and a SpeakerProposal together. """ template_name = "combined_proposal_submit.html" def setup(self, *args, **kwargs): """ Check that we have a valid EventType in url kwargs """ super().setup(*args, **kwargs) self.event_type = get_object_or_404( models.EventType, slug=self.kwargs["event_type_slug"] ) self.matrix = get_speaker_availability_form_matrix( sessions=self.camp.event_sessions.filter( event_type=self.event_type ).prefetch_related("event_type") ) def get_context_data(self, **kwargs): """ Add EventType to template context """ context = super().get_context_data(**kwargs) context["event_type"] = self.event_type context["matrix"] = self.matrix return context def form_valid(self, form): """ Save the object(s) here before redirecting """ # first save the SpeakerProposal speaker_proposal = form["speaker_proposal"].save(commit=False) speaker_proposal.camp = self.camp speaker_proposal.user = self.request.user if not speaker_proposal.email: speaker_proposal.email = self.request.user.email speaker_proposal.save() form["speaker_proposal"].save_m2m() # then save speaker availability objects save_speaker_availability(form["speaker_proposal"], speaker_proposal) # then save the event_proposal event_proposal = form["event_proposal"].save(commit=False) event_proposal.user = self.request.user event_proposal.event_type = self.event_type event_proposal.save() form["event_proposal"].save_m2m() # save or update slides url slideurl = form.cleaned_data.get("slides_url") if slideurl: slides_url, created = models.Url.objects.get_or_create( event_proposal=event_proposal, url_type=models.UrlType.objects.get(name="Slides"), defaults={"url": slideurl}, ) # add the speaker_proposal to the event_proposal event_proposal.speakers.add(speaker_proposal) # send mail(s) to content team if not add_new_event_proposal_email(event_proposal): logger.error( "Unable to send email to content team after new event_proposal" ) if not hasattr(self, "speaker_proposal"): if not add_new_speaker_proposal_email(speaker_proposal): logger.error( "Unable to send email to content team after new speaker_proposal" ) messages.success( self.request, f"Your {self.event_type.host_title} proposal and {self.event_type.name} proposal have been submitted for review. You will receive an email when they have been accepted or rejected.", ) # all good return redirect( reverse_lazy("program:proposal_list", kwargs={"camp_slug": self.camp.slug}) ) def get_form_class(self): """ We must show two forms on the page. We use betterforms.MultiModelForm to combine two forms """ # build our MultiModelForm class CombinedProposalSubmitForm(MultiModelForm): form_classes = OrderedDict( ( ("speaker_proposal", SpeakerProposalForm), ("event_proposal", EventProposalForm), ) ) # return the form class return CombinedProposalSubmitForm def get_form_kwargs(self): """ Set camp and event_type and matrix for the form """ kwargs = super().get_form_kwargs() kwargs.update( {"camp": self.camp, "event_type": self.event_type, "matrix": self.matrix} ) return kwargs def get_initial(self): """ We default to True for all daychunks in the speaker availability table when submitting a new speaker """ initial = super().get_initial() # we use betterforms.MultiModelForm so our initial dict has to be nested, # make sure we have a speaker_proposal dict to work with if "speaker_proposal" not in initial: initial["speaker_proposal"] = {} # loop over days in the matrix for date in self.matrix.keys(): # loop over the daychunks on this day for daychunk in self.matrix[date].keys(): # do we have/want a checkbox here? if self.matrix[date][daychunk]: # all initial values for news speakers should be True/checked initial["speaker_proposal"][ self.matrix[date][daychunk]["fieldname"] ] = True # but we set the initial in the dict to None to indicate we have no info self.matrix[date][daychunk]["initial"] = None return initial ################################################################### # 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" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("events__event_type") return qs ################################################################### # event views class EventListView(CampViewMixin, ListView): model = models.Event template_name = "event_list.html" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "event_type", "track", "speakers", "event_slots__event_session__event_location", "tags", ) return qs class EventDetailView(CampViewMixin, DetailView): model = models.Event template_name = "event_detail.html" slug_url_kwarg = "event_slug" ################################################################### # schedule views class NoScriptScheduleView(CampViewMixin, TemplateView): template_name = "noscript_schedule_view.html" def setup(self, *args, **kwargs): """ If no events are scheduled redirect to the event page """ super().setup(*args, **kwargs) # we redirect to the list of events if we dont show the schedule event_list_url = reverse( "program:event_index", kwargs={"camp_slug": self.camp.slug} ) # do we have a schedule to show? if not self.camp.event_slots.filter(event__isnull=False).exists(): raise RedirectException(event_list_url) # if camp.show_schedule is False we only show it to superusers if not self.camp.show_schedule and not self.request.user.is_superuser: raise RedirectException(event_list_url) def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) context["event_slots"] = ( models.EventSlot.objects.filter(event__track__camp=self.camp) .prefetch_related( "event_session__event_location", "event_session__event_type", "event__speakers", "event__tags", "event__event_type", ) .order_by("when") ) return context class CallForParticipationView(CampViewMixin, TemplateView): template_name = "call_for_participation.html" class FrabXmlView(CampViewMixin, View): """ This view returns an XML schedule in Frab format XSD is from https://raw.githubusercontent.com/wiki/frab/frab/images/schedule.xsd """ def get(self, *args, **kwargs): # get all EventSlots with something scheduled qs = ( models.EventSlot.objects.filter(event__track__camp=self.camp) .prefetch_related( "event__urls", "event__speakers", "event_session__event_location", ) .order_by("when", "event_session__event_location") ) E = objectify.ElementMaker(annotate=False) days = () i = 0 # loop over days for day in self.camp.get_days("camp")[:-1]: i += 1 # build a tuple of locations locations = () # loop over locations for location in models.EventLocation.objects.filter( id__in=qs.filter(event__isnull=False).values_list( "event_session__event_location_id", flat=True ) ): # build a tuple of scheduled events at this location instances = () for slot in qs.filter( when__contained_by=day, event_session__event_location=location ): # build a tuple of speakers for this event speakers = () for speaker in slot.event.speakers.all(): speakers += (E.person(speaker.name, id=str(speaker.pk)),) # build a tuple of URLs for this Event urls = () for url in slot.event.urls.all(): urls += (E.link(url.url_type.name, href=url.url),) # add this event to the instances tuple instances += ( E.event( E.date(slot.when.lower.isoformat()), E.start(slot.when.lower.time()), E.duration(slot.event.duration), E.room(location.name), E.slug(f"{slot.pk}-{slot.event.slug}"), E.url( self.request.build_absolute_uri( slot.event.get_absolute_url() ) ), E.recording( E.license("CC BY-SA 4.0"), E.optout( "false" if slot.event.video_recording else "true" ), ), E.title(slot.event.title), # our Events have no subtitle E.subtitle(""), E.track(slot.event.track), E.type(slot.event.event_type), # our Events have no language attribute but are mostly english E.language("en"), E.abstract(slot.event.abstract), # our Events have no long description E.description(""), E.persons(*speakers), E.links(*urls), E.attachments, id=str(slot.id), guid=str(slot.uuid), ), ) # add the events for this location on this day to the locations tuple locations += (E.room(*instances, name=location.name),) # add this day to the days tuple days += ( E.day( *locations, index=str(i), date=str(day.lower.date()), start=day.lower.isoformat(), end=day.upper.isoformat(), ), ) # put the XML together xml = E.schedule( E.version("BornHack Frab XML Generator v2.0"), E.conference( E.title(self.camp.title), E.acronym(str(self.camp.camp.lower.year)), E.start(self.camp.camp.lower.date().isoformat()), E.end(self.camp.camp.upper.date().isoformat()), E.days(len(self.camp.get_days("camp"))), E.timeslot_duration("00:30"), E.base_url(self.request.build_absolute_uri("/")), ), *days, ) xml = etree.tostring(xml, pretty_print=True, xml_declaration=True) # let's play nice - validate the XML before returning it schema = etree.XMLSchema(file="program/xsd/schedule.xml.xsd") parser = objectify.makeparser(schema=schema) try: objectify.fromstring(xml, parser) except etree.XMLSyntaxError: # we are generating invalid XML logger.exception("Something went sideways when validating frab xml :(") return HttpResponseServerError() response = HttpResponse(content_type="application/xml") response.write(xml) return response ################################################################### # 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 = ["url_type", "url"] def form_valid(self, form): """ Set the proposal FK before saving Set proposal as pending if it isn't already """ if hasattr(self, "event_proposal") and self.event_proposal: # this URL belongs to an event_proposal form.instance.event_proposal = self.event_proposal form.save() if ( self.event_proposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING ): self.event_proposal.proposal_status = ( models.SpeakerProposal.PROPOSAL_PENDING ) self.event_proposal.save() messages.success( self.request, "%s is now pending review by the Content Team." % self.event_proposal.title, ) else: # this URL belongs to a speaker_proposal form.instance.speaker_proposal = self.speaker_proposal form.save() if ( self.speaker_proposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING ): self.speaker_proposal.proposal_status = ( models.SpeakerProposal.PROPOSAL_PENDING ) self.speaker_proposal.save() messages.success( self.request, "%s is now pending review by the Content Team." % self.speaker_proposal.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 = ["url_type", "url"] pk_url_kwarg = "url_uuid" def form_valid(self, form): """ Set proposal as pending if it isn't already """ if hasattr(self, "event_proposal") and self.event_proposal: # this URL belongs to a speaker_proposal form.save() if ( self.event_proposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING ): self.event_proposal.proposal_status = ( models.SpeakerProposal.PROPOSAL_PENDING ) self.event_proposal.save() messages.success( self.request, "%s is now pending review by the Content Team." % self.event_proposal.title, ) else: # this URL belongs to a speaker_proposal form.save() if ( self.speaker_proposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING ): self.speaker_proposal.proposal_status = ( models.SpeakerProposal.PROPOSAL_PENDING ) self.speaker_proposal.save() messages.success( self.request, "%s is now pending review by the Content Team." % self.speaker_proposal.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, "event_proposal") and self.event_proposal: # this URL belongs to a speaker_proposal if ( self.event_proposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING ): self.event_proposal.proposal_status = ( models.SpeakerProposal.PROPOSAL_PENDING ) self.event_proposal.save() messages.success( self.request, "%s is now pending review by the Content Team." % self.event_proposal.title, ) else: # this URL belongs to a speaker_proposal if ( self.speaker_proposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING ): self.speaker_proposal.proposal_status = ( models.SpeakerProposal.PROPOSAL_PENDING ) self.speaker_proposal.save() messages.success( self.request, "%s is now pending review by the Content Team." % self.speaker_proposal.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}) ) ################################################################### # Feedback views class FeedbackRedirectView(LoginRequiredMixin, EventViewMixin, DetailView): """ Redirect to the appropriate view """ model = models.Event slug_url_kwarg = "event_slug" def setup(self, *args, **kwargs): super().setup(*args, **kwargs) if not self.request.user.is_authenticated: messages.error( self.request, "You must be logged in to provide Event Feedback" ) raise RedirectException( reverse( "program:event_detail", kwargs={ "camp_slug": self.camp.slug, "event_slug": self.event.slug, }, ) ) if self.event.proposal.user == self.request.user: # user is owner of the event raise RedirectException( reverse( "program:event_feedback_list", kwargs={ "camp_slug": self.camp.slug, "event_slug": self.event.slug, }, ) ) elif self.event.feedbacks.filter(user=self.request.user).exists(): # user has existing feedback raise RedirectException( reverse( "program:event_feedback_detail", kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug}, ) ) else: # user has no existing feedback raise RedirectException( reverse( "program:event_feedback_create", kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug}, ) ) class FeedbackListView(LoginRequiredMixin, EventViewMixin, ListView): """ The FeedbackListView is used by the event owner to see approved Feedback for the Event. """ model = models.EventFeedback template_name = "event_feedback_list.html" context_object_name = "event_feedback_list" def setup(self, *args, **kwargs): super().setup(*args, **kwargs) if not self.event.proposal or not self.event.proposal.user == self.request.user: messages.error(self.request, "Only the event owner can read feedback!") raise RedirectException( reverse( "program:event_detail", kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug}, ) ) def get_queryset(self, *args, **kwargs): return models.EventFeedback.objects.filter(event=self.event, approved=True) class FeedbackCreateView(LoginRequiredMixin, EventViewMixin, CreateView): """ Used by users to create Feedback for an Event. Available to all logged in users. """ model = models.EventFeedback fields = ["expectations_fulfilled", "attend_speaker_again", "rating", "comment"] template_name = "event_feedback_form.html" def setup(self, *args, **kwargs): super().setup(*args, **kwargs) if models.EventFeedback.objects.filter( event=self.event, user=self.request.user ).exists(): raise RedirectException( reverse( "program:event_feedback_detail", kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug}, ) ) def get_form(self, *args, **kwargs): form = super().get_form(*args, **kwargs) form.fields["expectations_fulfilled"].widget = forms.RadioSelect( choices=models.EventFeedback.YESNO_CHOICES, ) form.fields["attend_speaker_again"].widget = forms.RadioSelect( choices=models.EventFeedback.YESNO_CHOICES, ) form.fields["rating"].widget = forms.RadioSelect( choices=models.EventFeedback.RATING_CHOICES, ) return form def form_valid(self, form): feedback = form.save(commit=False) feedback.user = self.request.user feedback.event = self.event feedback.save() messages.success( self.request, "Your feedback was submitted, it is now pending approval." ) return redirect(feedback.get_absolute_url()) class FeedbackDetailView( LoginRequiredMixin, EventFeedbackViewMixin, UserIsObjectOwnerMixin, DetailView ): """ Used by the EventFeedback owner to see their own feedback. """ model = models.EventFeedback template_name = "event_feedback_detail.html" context_object_name = "event_feedback" class FeedbackUpdateView( LoginRequiredMixin, EventFeedbackViewMixin, UserIsObjectOwnerMixin, UpdateView ): """ Used by the EventFeedback owner to update their feedback. """ model = models.EventFeedback fields = ["expectations_fulfilled", "attend_speaker_again", "rating", "comment"] template_name = "event_feedback_form.html" def form_valid(self, form): feedback = form.save(commit=False) feedback.approved = False feedback.save() messages.success(self.request, "Your feedback was updated") return redirect(feedback.get_absolute_url()) class FeedbackDeleteView( LoginRequiredMixin, EventFeedbackViewMixin, UserIsObjectOwnerMixin, DeleteView ): """ Used by the EventFeedback owner to delete their own feedback. """ model = models.EventFeedback template_name = "event_feedback_delete.html" def get_success_url(self): messages.success(self.request, "Your feedback was deleted") return self.event.get_absolute_url()