1499 lines
50 KiB
Python
1499 lines
50 KiB
Python
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()
|