import logging from collections import OrderedDict import icalendar 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 camps.mixins import CampViewMixin 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, "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()