bornhack-website/src/program/views.py
2020-06-30 20:00:34 +02:00

1495 lines
50 KiB
Python

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