import logging from django import forms from django.conf import settings from django.contrib import messages from django.db.models import Count, Q from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.safestring import mark_safe from django.views.generic import DetailView, ListView, TemplateView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from camps.mixins import CampViewMixin from program.autoscheduler import AutoScheduler from program.mixins import AvailabilityMatrixViewMixin from program.models import ( Event, EventLocation, EventProposal, EventSession, EventSlot, EventType, Speaker, SpeakerProposal, ) from program.utils import save_speaker_availability from ..forms import ( AutoScheduleApplyForm, AutoScheduleValidateForm, EventScheduleForm, SpeakerForm, ) from ..mixins import ContentTeamPermissionMixin logger = logging.getLogger("bornhack.%s" % __name__) ####################################### # MANAGE SPEAKER/EVENT PROPOSAL VIEWS class PendingProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This convenience view shows a list of pending proposals""" model = SpeakerProposal template_name = "pending_proposals.html" context_object_name = "speaker_proposal_list" def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs).filter(proposal_status="pending") qs = qs.prefetch_related("user", "urls", "speaker") return qs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["event_proposal_list"] = self.camp.event_proposals.filter( proposal_status=EventProposal.PROPOSAL_PENDING ).prefetch_related("event_type", "track", "speakers", "tags", "user", "event") return context class ProposalApproveBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateView): """ Shared logic between SpeakerProposalApproveView and EventProposalApproveView """ fields = ["reason"] def form_valid(self, form): """ We have two submit buttons in this form, Approve and Reject """ if "approve" in form.data: # approve button was pressed form.instance.mark_as_approved(self.request) elif "reject" in form.data: # reject button was pressed form.instance.mark_as_rejected(self.request) else: messages.error(self.request, "Unknown submit action") return redirect( reverse( "backoffice:pending_proposals", kwargs={"camp_slug": self.camp.slug} ) ) class SpeakerProposalListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view permits Content Team members to list SpeakerProposals""" model = SpeakerProposal template_name = "speaker_proposal_list.html" context_object_name = "speaker_proposal_list" def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) qs = qs.prefetch_related("user", "urls", "speaker") return qs class SpeakerProposalDetailView( AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, DetailView, ): """This view permits Content Team members to see SpeakerProposal details""" model = SpeakerProposal template_name = "speaker_proposal_detail_backoffice.html" context_object_name = "speaker_proposal" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("user", "urls") return qs class SpeakerProposalApproveRejectView(ProposalApproveBaseView): """This view allows ContentTeam members to approve/reject SpeakerProposals""" model = SpeakerProposal template_name = "speaker_proposal_approve_reject.html" context_object_name = "speaker_proposal" class EventProposalListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view permits Content Team members to list EventProposals""" model = EventProposal template_name = "event_proposal_list.html" context_object_name = "event_proposal_list" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "user", "urls", "event", "event_type", "speakers__event_proposals", "track", "tags", ) return qs class EventProposalDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView): """This view permits Content Team members to see EventProposal details""" model = EventProposal template_name = "event_proposal_detail_backoffice.html" context_object_name = "event_proposal" class EventProposalApproveRejectView(ProposalApproveBaseView): """This view allows ContentTeam members to approve/reject EventProposals""" model = EventProposal template_name = "event_proposal_approve_reject.html" context_object_name = "event_proposal" ################################ # MANAGE SPEAKER VIEWS class SpeakerListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view is used by the Content Team to see Speaker objects.""" model = Speaker template_name = "speaker_list_backoffice.html" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "proposal__user", "events__event_slots", "events__event_type", "event_conflicts", ) return qs class SpeakerDetailView( AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, DetailView ): """This view is used by the Content Team to see details for Speaker objects""" model = Speaker template_name = "speaker_detail_backoffice.html" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "event_conflicts", "events__event_slots", "events__event_type" ) return qs class SpeakerUpdateView( AvailabilityMatrixViewMixin, ContentTeamPermissionMixin, UpdateView ): """This view is used by the Content Team to update Speaker objects""" model = Speaker template_name = "speaker_update.html" form_class = SpeakerForm def get_form_kwargs(self): """Set camp for the form""" kwargs = super().get_form_kwargs() kwargs.update({"camp": self.camp}) return kwargs def form_valid(self, form): """Save object and availability""" speaker = form.save() save_speaker_availability(form, obj=speaker) messages.success(self.request, "Speaker has been updated") return redirect( reverse( "backoffice:speaker_detail", kwargs={"camp_slug": self.camp.slug, "slug": self.get_object().slug}, ) ) class SpeakerDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView): """This view is used by the Content Team to delete Speaker objects""" model = Speaker template_name = "speaker_delete.html" def delete(self, *args, **kwargs): speaker = self.get_object() # delete related objects first speaker.availabilities.all().delete() speaker.urls.all().delete() return super().delete(*args, **kwargs) def get_success_url(self): messages.success( self.request, f"Speaker '{self.get_object().name}' has been deleted" ) return reverse("backoffice:speaker_list", kwargs={"camp_slug": self.camp.slug}) ################################ # MANAGE EVENTTYPE VIEWS class EventTypeListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view is used by the Content Team to list EventTypes""" model = EventType template_name = "event_type_list.html" context_object_name = "event_type_list" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.annotate( # only count events for the current camp event_count=Count( "events", distinct=True, filter=Q(events__track__camp=self.camp) ), # only count EventSessions for the current camp event_sessions_count=Count( "event_sessions", distinct=True, filter=Q(event_sessions__camp=self.camp), ), # only count EventSlots for the current camp event_slots_count=Count( "event_sessions__event_slots", distinct=True, filter=Q(event_sessions__camp=self.camp), ), ) return qs class EventTypeDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView): """This view is used by the Content Team to see details for EventTypes""" model = EventType template_name = "event_type_detail.html" context_object_name = "event_type" def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["event_sessions"] = self.camp.event_sessions.filter( event_type=self.get_object() ).prefetch_related("event_location", "event_slots") context["events"] = self.camp.events.filter( event_type=self.get_object() ).prefetch_related( "speakers", "event_slots__event_session__event_location", "event_type" ) return context ################################ # MANAGE EVENTLOCATION VIEWS class EventLocationListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view is used by the Content Team to list EventLocation objects.""" model = EventLocation template_name = "event_location_list.html" context_object_name = "event_location_list" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("event_sessions__event_slots", "conflicts") return qs class EventLocationDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView): """This view is used by the Content Team to see details for EventLocation objects""" model = EventLocation template_name = "event_location_detail.html" context_object_name = "event_location" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "conflicts", "event_sessions__event_slots", "event_sessions__event_type" ) return qs class EventLocationCreateView(CampViewMixin, ContentTeamPermissionMixin, CreateView): """This view is used by the Content Team to create EventLocation objects""" model = EventLocation fields = ["name", "icon", "capacity", "conflicts"] template_name = "event_location_form.html" def get_form(self, *args, **kwargs): form = super().get_form(*args, **kwargs) form.fields["conflicts"].queryset = self.camp.event_locations.all() return form def form_valid(self, form): location = form.save(commit=False) location.camp = self.camp location.save() form.save_m2m() messages.success( self.request, f"EventLocation {location.name} has been created" ) return redirect( reverse( "backoffice:event_location_detail", kwargs={"camp_slug": self.camp.slug, "slug": location.slug}, ) ) class EventLocationUpdateView(CampViewMixin, ContentTeamPermissionMixin, UpdateView): """This view is used by the Content Team to update EventLocation objects""" model = EventLocation fields = ["name", "icon", "capacity", "conflicts"] template_name = "event_location_form.html" def get_form(self, *args, **kwargs): form = super().get_form(*args, **kwargs) form.fields["conflicts"].queryset = self.camp.event_locations.exclude( pk=self.get_object().pk ) return form def get_success_url(self): messages.success( self.request, f"EventLocation {self.get_object().name} has been updated" ) return reverse( "backoffice:event_location_detail", kwargs={"camp_slug": self.camp.slug, "slug": self.get_object().slug}, ) class EventLocationDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView): """This view is used by the Content Team to delete EventLocation objects""" model = EventLocation template_name = "event_location_delete.html" context_object_name = "event_location" def delete(self, *args, **kwargs): slotsdeleted, slotdetails = self.get_object().event_slots.all().delete() sessionsdeleted, sessiondetails = ( self.get_object().event_sessions.all().delete() ) return super().delete(*args, **kwargs) def get_success_url(self): messages.success( self.request, f"EventLocation '{self.get_object().name}' has been deleted." ) return reverse( "backoffice:event_location_list", kwargs={"camp_slug": self.camp.slug} ) ################################ # MANAGE EVENT VIEWS class EventListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view is used by the Content Team to see Event objects.""" model = Event template_name = "event_list_backoffice.html" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "speakers__events", "event_type", "event_slots__event_session__event_location", "tags", ) return qs class EventDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView): """This view is used by the Content Team to see details for Event objects""" model = Event template_name = "event_detail_backoffice.html" class EventUpdateView(CampViewMixin, ContentTeamPermissionMixin, UpdateView): """This view is used by the Content Team to update Event objects""" model = Event fields = [ "title", "abstract", "video_recording", "duration_minutes", "demand", "tags", ] template_name = "event_update.html" def get_success_url(self): messages.success(self.request, "Event has been updated") return reverse( "backoffice:event_detail", kwargs={"camp_slug": self.camp.slug, "slug": self.get_object().slug}, ) class EventDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView): """This view is used by the Content Team to delete Event objects""" model = Event template_name = "event_delete.html" def delete(self, *args, **kwargs): self.get_object().urls.all().delete() return super().delete(*args, **kwargs) def get_success_url(self): messages.success( self.request, f"Event '{self.get_object().title}' has been deleted!", ) return reverse("backoffice:event_list", kwargs={"camp_slug": self.camp.slug}) class EventScheduleView(CampViewMixin, ContentTeamPermissionMixin, FormView): """This view is used by the Content Team to manually schedule Events. It shows a table with radioselect buttons for the available slots for the EventType of the Event""" form_class = EventScheduleForm template_name = "event_schedule.html" def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self.event = get_object_or_404( Event, track__camp=self.camp, slug=kwargs["slug"] ) def get_form(self, *args, **kwargs): form = super().get_form(*args, **kwargs) self.slots = [] slotindex = 0 # loop over sessions, get free slots for session in self.camp.event_sessions.filter( event_type=self.event.event_type, event_duration_minutes__gte=self.event.duration_minutes, ): for slot in session.get_available_slots(): # loop over speakers to see if they are all available for speaker in self.event.speakers.all(): if not speaker.is_available(slot.when): # this speaker is not available, skip this slot break else: # all speakers are available for this slot self.slots.append({"index": slotindex, "slot": slot}) slotindex += 1 # add the slot choicefield form.fields["slot"] = forms.ChoiceField( widget=forms.RadioSelect, choices=[(s["index"], s["index"]) for s in self.slots], ) return form def get_context_data(self, *args, **kwargs): """ Add event to context """ context = super().get_context_data(*args, **kwargs) context["event"] = self.event context["event_slots"] = self.slots return context def form_valid(self, form): """ Set needed values, save slot and return """ slot = self.slots[int(form.cleaned_data["slot"])]["slot"] slot.event = self.event slot.autoscheduled = False slot.save() messages.success( self.request, f"{self.event.title} has been scheduled to begin at {slot.when.lower} at location {slot.event_location.name} successfully!", ) return redirect( reverse( "backoffice:event_detail", kwargs={"camp_slug": self.camp.slug, "slug": self.event.slug}, ) ) ################################ # MANAGE EVENTSESSION VIEWS class EventSessionCreateTypeSelectView( CampViewMixin, ContentTeamPermissionMixin, ListView ): """ This view is shown first when creating a new EventSession """ model = EventType template_name = "event_session_create_type_select.html" context_object_name = "event_type_list" class EventSessionCreateLocationSelectView( CampViewMixin, ContentTeamPermissionMixin, ListView ): """ This view is shown second when creating a new EventSession """ model = EventLocation template_name = "event_session_create_location_select.html" context_object_name = "event_location_list" def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self.event_type = get_object_or_404(EventType, slug=kwargs["event_type_slug"]) def get_context_data(self, *args, **kwargs): """ Add event_type to context """ context = super().get_context_data(*args, **kwargs) context["event_type"] = self.event_type return context class EventSessionFormViewMixin: """ A mixin with the stuff shared between EventSession{Create|Update}View """ def get_form(self, *args, **kwargs): """ The default range widgets are a bit shit because they eat the help_text and have no indication of which field is for what. So we add a nice placeholder. We also limit the event_location dropdown to only the current camps locations. """ form = super().get_form(*args, **kwargs) form.fields["when"].widget.widgets[0].attrs = { "placeholder": f"Start Date and Time (YYYY-MM-DD HH:MM). Time zone is {settings.TIME_ZONE}.", } form.fields["when"].widget.widgets[1].attrs = { "placeholder": f"End Date and Time (YYYY-MM-DD HH:MM). Time zone is {settings.TIME_ZONE}.", } if hasattr(form.fields, "event_location"): form.fields["event_location"].queryset = EventLocation.objects.filter( camp=self.camp ) return form def get_context_data(self, *args, **kwargs): """ Add event_type and location and existing sessions to context """ context = super().get_context_data(*args, **kwargs) if not hasattr(self, "event_type"): self.event_type = self.get_object().event_type context["event_type"] = self.event_type if not hasattr(self, "event_location"): self.event_location = self.get_object().event_location context["event_location"] = self.event_location context["sessions"] = self.event_type.event_sessions.filter(camp=self.camp) return context class EventSessionCreateView( CampViewMixin, ContentTeamPermissionMixin, EventSessionFormViewMixin, CreateView ): """ This view is used by the Content Team to create EventSession objects """ model = EventSession fields = ["description", "when", "event_duration_minutes"] template_name = "event_session_form.html" def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self.event_type = get_object_or_404(EventType, slug=kwargs["event_type_slug"]) self.event_location = get_object_or_404( EventLocation, camp=self.camp, slug=kwargs["event_location_slug"] ) def form_valid(self, form): """ Set camp and event_type, check for overlaps and save """ session = form.save(commit=False) session.event_type = self.event_type session.event_location = self.event_location session.camp = self.camp session.save() messages.success(self.request, f"{session} has been created successfully!") return redirect( reverse( "backoffice:event_session_list", kwargs={"camp_slug": self.camp.slug} ) ) class EventSessionListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """ This view is used by the Content Team to see EventSession objects. """ model = EventSession template_name = "event_session_list.html" context_object_name = "event_session_list" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("event_type", "event_location", "event_slots") return qs class EventSessionDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView): """ This view is used by the Content Team to see details for EventSession objects """ model = EventSession template_name = "event_session_detail.html" context_object_name = "session" class EventSessionUpdateView( CampViewMixin, ContentTeamPermissionMixin, EventSessionFormViewMixin, UpdateView ): """ This view is used by the Content Team to update EventSession objects """ model = EventSession fields = ["when", "description", "event_duration_minutes"] template_name = "event_session_form.html" def form_valid(self, form): """ Just save, we have a post_save signal which takes care of fixing EventSlots """ session = form.save() messages.success(self.request, f"{session} has been updated successfully!") return redirect( reverse( "backoffice:event_session_list", kwargs={"camp_slug": self.camp.slug} ) ) class EventSessionDeleteView(CampViewMixin, ContentTeamPermissionMixin, DeleteView): """ This view is used by the Content Team to delete EventSession objects """ model = EventSession template_name = "event_session_delete.html" context_object_name = "session" def get(self, *args, **kwargs): """Show a warning if we have something scheduled in this EventSession""" if self.get_object().event_slots.filter(event__isnull=False).exists(): messages.warning( self.request, "NOTE: One or more EventSlots in this EventSession has an Event scheduled. Make sure you are deleting the correct session!", ) return super().get(*args, **kwargs) def delete(self, *args, **kwargs): session = self.get_object() session.event_slots.all().delete() return super().delete(*args, **kwargs) def get_success_url(self): messages.success( self.request, "EventSession and related EventSlots was deleted successfully!", ) return reverse( "backoffice:event_session_list", kwargs={"camp_slug": self.camp.slug} ) ################################ # MANAGE EVENTSLOT VIEWS class EventSlotListView(CampViewMixin, ContentTeamPermissionMixin, ListView): """This view is used by the Content Team to see EventSlot objects.""" model = EventSlot template_name = "event_slot_list.html" context_object_name = "event_slot_list" def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related( "event__speakers", "event_session__event_location", "event_session__event_type", ) return qs class EventSlotDetailView(CampViewMixin, ContentTeamPermissionMixin, DetailView): """This view is used by the Content Team to see details for EventSlot objects""" model = EventSlot template_name = "event_slot_detail.html" context_object_name = "event_slot" class EventSlotUnscheduleView(CampViewMixin, ContentTeamPermissionMixin, UpdateView): """This view is used by the Content Team to remove an Event from the schedule/EventSlot""" model = EventSlot template_name = "event_slot_unschedule.html" fields = [] context_object_name = "event_slot" def form_valid(self, form): event_slot = self.get_object() event = event_slot.event event_slot.unschedule() messages.success( self.request, f"The Event '{event.title}' has been removed from the slot {event_slot}", ) return redirect( reverse( "backoffice:event_detail", kwargs={"camp_slug": self.camp.slug, "slug": event.slug}, ) ) ################################ # AUTOSCHEDULER VIEWS class AutoScheduleManageView(CampViewMixin, ContentTeamPermissionMixin, TemplateView): """Just an index type view with links to the various actions""" template_name = "autoschedule_index.html" class AutoScheduleCrashCourseView( CampViewMixin, ContentTeamPermissionMixin, TemplateView ): """A short crash course on the autoscheduler""" template_name = "autoschedule_crash_course.html" class AutoScheduleValidateView(CampViewMixin, ContentTeamPermissionMixin, FormView): """This view is used to validate schedules. It uses the AutoScheduler and can either validate the currently applied schedule or a new similar schedule, or a brand new schedule""" template_name = "autoschedule_validate.html" form_class = AutoScheduleValidateForm def form_valid(self, form): # initialise AutoScheduler scheduler = AutoScheduler(camp=self.camp) # get autoschedule if form.cleaned_data["schedule"] == "current": autoschedule = scheduler.build_current_autoschedule() message = f"The currently scheduled Events form a valid schedule! AutoScheduler has {len(scheduler.autoslots)} Slots based on {scheduler.event_sessions.count()} EventSessions for {scheduler.event_types.count()} EventTypes. {scheduler.events.count()} Events in the schedule." elif form.cleaned_data["schedule"] == "similar": original_autoschedule = scheduler.build_current_autoschedule() autoschedule, diff = scheduler.calculate_similar_autoschedule( original_autoschedule ) message = f"The new similar schedule is valid! AutoScheduler has {len(scheduler.autoslots)} Slots based on {scheduler.event_sessions.count()} EventSessions for {scheduler.event_types.count()} EventTypes. Differences to the current schedule: {len(diff['event_diffs'])} Event diffs and {len(diff['slot_diffs'])} Slot diffs." elif form.cleaned_data["schedule"] == "new": autoschedule = scheduler.calculate_autoschedule() message = f"The new schedule is valid! AutoScheduler has {len(scheduler.autoslots)} Slots based on {scheduler.event_sessions.count()} EventSessions for {scheduler.event_types.count()} EventTypes. {scheduler.events.count()} Events in the schedule." # check validity valid, violations = scheduler.is_valid(autoschedule, return_violations=True) if valid: messages.success(self.request, message) else: messages.error(self.request, "Schedule is NOT valid!") message = "Schedule violations:
" for v in violations: message += v + "
" messages.error(self.request, mark_safe(message)) return redirect( reverse( "backoffice:autoschedule_validate", kwargs={"camp_slug": self.camp.slug} ) ) class AutoScheduleDiffView(CampViewMixin, ContentTeamPermissionMixin, TemplateView): template_name = "autoschedule_diff.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) scheduler = AutoScheduler(camp=self.camp) autoschedule, diff = scheduler.calculate_similar_autoschedule() context["diff"] = diff context["scheduler"] = scheduler return context class AutoScheduleApplyView(CampViewMixin, ContentTeamPermissionMixin, FormView): """This view is used by the Content Team to apply a new schedules by unscheduling all autoscheduled Events, and scheduling all Event/Slot combinations in the schedule. TODO: see comment in program.autoscheduler.AutoScheduler.apply() method. """ template_name = "autoschedule_apply.html" form_class = AutoScheduleApplyForm def form_valid(self, form): # initialise AutoScheduler scheduler = AutoScheduler(camp=self.camp) # get autoschedule if form.cleaned_data["schedule"] == "similar": autoschedule, diff = scheduler.calculate_similar_autoschedule() elif form.cleaned_data["schedule"] == "new": autoschedule = scheduler.calculate_autoschedule() # check validity valid, violations = scheduler.is_valid(autoschedule, return_violations=True) if valid: # schedule is valid, apply it deleted, created = scheduler.apply(autoschedule) messages.success( self.request, f"Schedule has been applied! {deleted} Events removed from schedule, {created} new Events scheduled. Differences to the previous schedule: {len(diff['event_diffs'])} Event diffs and {len(diff['slot_diffs'])} Slot diffs.", ) else: messages.error(self.request, "Schedule is NOT valid, cannot apply!") return redirect( reverse( "backoffice:autoschedule_apply", kwargs={"camp_slug": self.camp.slug} ) ) class AutoScheduleDebugEventSlotUnavailabilityView( CampViewMixin, ContentTeamPermissionMixin, TemplateView ): template_name = "autoschedule_debug_slots.html" def get_context_data(self, **kwargs): scheduler = AutoScheduler(camp=self.camp) context = { "scheduler": scheduler, } return context class AutoScheduleDebugEventConflictsView( CampViewMixin, ContentTeamPermissionMixin, TemplateView ): template_name = "autoschedule_debug_events.html" def get_context_data(self, **kwargs): scheduler = AutoScheduler(camp=self.camp) context = { "scheduler": scheduler, } return context