diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd98f64a..430e3b34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,3 +12,10 @@ repos: rev: "v5.8.0" hooks: - id: "isort" + - repo: https://github.com/myint/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: + - --in-place + - --remove-all-unused-imports diff --git a/src/backoffice/views.py b/src/backoffice/views.py deleted file mode 100644 index 5b2dbb50..00000000 --- a/src/backoffice/views.py +++ /dev/null @@ -1,2467 +0,0 @@ -import logging -import os -from itertools import chain - -import requests -from camps.mixins import CampViewMixin -from django import forms -from django.conf import settings -from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied -from django.core.files import File -from django.db.models import Count, Q, Sum -from django.forms import modelformset_factory -from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse -from django.utils import timezone -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 economy.models import ( - Chain, - Credebtor, - Expense, - Pos, - PosReport, - Reimbursement, - Revenue, -) -from facilities.models import ( - Facility, - FacilityFeedback, - FacilityOpeningHours, - FacilityType, -) -from leaflet.forms.widgets import LeafletWidget -from profiles.models import Profile -from program.autoscheduler import AutoScheduler -from program.mixins import AvailabilityMatrixViewMixin -from program.models import ( - Event, - EventFeedback, - EventLocation, - EventProposal, - EventSession, - EventSlot, - EventType, - Speaker, - SpeakerProposal, - Url, - UrlType, -) -from program.utils import save_speaker_availability -from shop.models import Order, OrderProductRelation -from teams.models import Team -from tickets.models import DiscountTicket, ShopTicket, SponsorTicket, TicketType -from tokens.models import Token, TokenFind -from utils.models import OutgoingEmail - -from .forms import ( - AutoScheduleApplyForm, - AutoScheduleValidateForm, - EventScheduleForm, - SpeakerForm, - AddRecordingForm, -) -from .mixins import ( - ContentTeamPermissionMixin, - EconomyTeamPermissionMixin, - InfoTeamPermissionMixin, - OrgaTeamPermissionMixin, - PosViewMixin, - RaisePermissionRequiredMixin, -) - -logger = logging.getLogger("bornhack.%s" % __name__) - - -class BackofficeIndexView(CampViewMixin, RaisePermissionRequiredMixin, TemplateView): - """ - The Backoffice index view only requires camps.backoffice_permission so we use RaisePermissionRequiredMixin directly - """ - - permission_required = "camps.backoffice_permission" - template_name = "index.html" - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - context["facilityfeedback_teams"] = Team.objects.filter( - id__in=set( - FacilityFeedback.objects.filter( - facility__facility_type__responsible_team__camp=self.camp, - handled=False, - ).values_list( - "facility__facility_type__responsible_team__id", flat=True - ) - ) - ) - context["held_email_count"] = OutgoingEmail.objects.filter(hold=True, responsible_team__isnull=True).count() + OutgoingEmail.objects.filter(hold=True, responsible_team__camp=self.camp).count() - return context - - -class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView): - template_name = "product_handout.html" - - def get_queryset(self, **kwargs): - return OrderProductRelation.objects.filter( - ticket_generated=False, - order__paid=True, - order__refunded=False, - order__cancelled=False, - ).order_by("order") - - -class BadgeHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView): - template_name = "badge_handout.html" - context_object_name = "tickets" - - def get_queryset(self, **kwargs): - shoptickets = ShopTicket.objects.filter(badge_ticket_generated=False) - sponsortickets = SponsorTicket.objects.filter(badge_ticket_generated=False) - discounttickets = DiscountTicket.objects.filter(badge_ticket_generated=False) - return list(chain(shoptickets, sponsortickets, discounttickets)) - - -class TicketCheckinView(CampViewMixin, InfoTeamPermissionMixin, ListView): - template_name = "ticket_checkin.html" - context_object_name = "tickets" - - def get_queryset(self, **kwargs): - shoptickets = ShopTicket.objects.filter(used=False) - sponsortickets = SponsorTicket.objects.filter(used=False) - discounttickets = DiscountTicket.objects.filter(used=False) - return list(chain(shoptickets, sponsortickets, discounttickets)) - - -class ApproveNamesView(CampViewMixin, OrgaTeamPermissionMixin, ListView): - template_name = "approve_public_credit_names.html" - context_object_name = "profiles" - - def get_queryset(self, **kwargs): - return Profile.objects.filter(public_credit_name_approved=False).exclude( - public_credit_name="" - ) - - -class ApproveFeedbackView(CampViewMixin, ContentTeamPermissionMixin, FormView): - """ - This view shows a list of EventFeedback objects which are pending approval. - """ - - model = EventFeedback - template_name = "approve_feedback.html" - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - self.queryset = EventFeedback.objects.filter( - event__track__camp=self.camp, approved__isnull=True - ) - - self.form_class = modelformset_factory( - EventFeedback, - fields=("approved",), - min_num=self.queryset.count(), - validate_min=True, - max_num=self.queryset.count(), - validate_max=True, - extra=0, - ) - - def get_context_data(self, *args, **kwargs): - """ - Include the queryset used for the modelformset_factory so we have - some idea which object is which in the template - Why the hell do the forms in the formset not include the object? - """ - context = super().get_context_data(*args, **kwargs) - context["event_feedback_list"] = self.queryset - context["formset"] = self.form_class(queryset=self.queryset) - return context - - def form_valid(self, form): - form.save() - if form.changed_objects: - messages.success( - self.request, f"Updated {len(form.changed_objects)} EventFeedbacks" - ) - return redirect(self.get_success_url()) - - def get_success_url(self, *args, **kwargs): - return reverse( - "backoffice:approve_event_feedback", kwargs={"camp_slug": self.camp.slug} - ) - - -class AddRecordingView(CampViewMixin, ContentTeamPermissionMixin, FormView): - """ - This view shows a list of events that is set to be recorded, but without a recording URL attached. - """ - - model = Event - template_name = "add_recording.html" - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - self.queryset = Event.objects.filter( - track__camp=self.camp, video_recording=True - ).exclude( - urls__url_type__name="Recording" - ) - - self.form_class = modelformset_factory( - Event, - form=AddRecordingForm, - min_num=self.queryset.count(), - validate_min=True, - max_num=self.queryset.count(), - validate_max=True, - extra=0, - ) - - def get_context_data(self, *args, **kwargs): - """ - Include the queryset used for the modelformset_factory so we have - some idea which object is which in the template - Why the hell do the forms in the formset not include the object? - """ - context = super().get_context_data(*args, **kwargs) - context["event_list"] = self.queryset - context["formset"] = self.form_class(queryset=self.queryset) - return context - - def form_valid(self, form): - form.save() - - for event_data in form.cleaned_data: - if event_data['recording_url']: - url = event_data['recording_url'] - if not event_data['id'].urls.filter(url=url).exists(): - recording_url = Url() - recording_url.event = event_data['id'] - recording_url.url = url - recording_url.url_type = UrlType.objects.get(name="Recording") - recording_url.save() - - if form.changed_objects: - messages.success( - self.request, f"Updated {len(form.changed_objects)} Event" - ) - return redirect(self.get_success_url()) - - def get_success_url(self, *args, **kwargs): - return reverse( - "backoffice:add_eventrecording", kwargs={"camp_slug": self.camp.slug} - ) - - -########################## -# MANAGE FACILITIES VIEWS - - -class FacilityTypeListView(CampViewMixin, OrgaTeamPermissionMixin, ListView): - model = FacilityType - template_name = "facility_type_list_backoffice.html" - context_object_name = "facility_type_list" - - -class FacilityTypeCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView): - model = FacilityType - template_name = "facility_type_form.html" - fields = [ - "name", - "description", - "icon", - "marker", - "responsible_team", - "quickfeedback_options", - ] - - def get_context_data(self, **kwargs): - """ - Do not show teams that are not part of the current camp in the dropdown - """ - context = super().get_context_data(**kwargs) - context["form"].fields["responsible_team"].queryset = Team.objects.filter( - camp=self.camp - ) - return context - - def get_success_url(self): - return reverse( - "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug} - ) - - -class FacilityTypeUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView): - model = FacilityType - template_name = "facility_type_form.html" - fields = [ - "name", - "description", - "icon", - "marker", - "responsible_team", - "quickfeedback_options", - ] - - def get_context_data(self, **kwargs): - """ - Do not show teams that are not part of the current camp in the dropdown - """ - context = super().get_context_data(**kwargs) - context["form"].fields["responsible_team"].queryset = Team.objects.filter( - camp=self.camp - ) - return context - - def get_success_url(self): - return reverse( - "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug} - ) - - -class FacilityTypeDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView): - model = FacilityType - template_name = "facility_type_delete.html" - context_object_name = "facility_type" - - def delete(self, *args, **kwargs): - for facility in self.get_object().facilities.all(): - facility.feedbacks.all().delete() - facility.opening_hours.all().delete() - facility.delete() - return super().delete(*args, **kwargs) - - def get_success_url(self): - messages.success(self.request, "The FacilityType has been deleted") - return reverse( - "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug} - ) - - -class FacilityListView(CampViewMixin, OrgaTeamPermissionMixin, ListView): - model = Facility - template_name = "facility_list_backoffice.html" - - -class FacilityDetailView(CampViewMixin, OrgaTeamPermissionMixin, DetailView): - model = Facility - template_name = "facility_detail_backoffice.html" - pk_url_kwarg = "facility_uuid" - - def get_queryset(self, *args, **kwargs): - qs = super().get_queryset(*args, **kwargs) - return qs.prefetch_related("opening_hours") - - -class FacilityCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView): - model = Facility - template_name = "facility_form.html" - fields = ["facility_type", "name", "description", "location"] - - def get_form(self, *args, **kwargs): - form = super().get_form(*args, **kwargs) - form.fields["location"].widget = LeafletWidget( - attrs={ - "display_raw": "true", - } - ) - return form - - def get_context_data(self, **kwargs): - """ - Do not show types that are not part of the current camp in the dropdown - """ - context = super().get_context_data(**kwargs) - context["form"].fields["facility_type"].queryset = FacilityType.objects.filter( - responsible_team__camp=self.camp - ) - return context - - def get_success_url(self): - messages.success(self.request, "The Facility has been created") - return reverse("backoffice:facility_list", kwargs={"camp_slug": self.camp.slug}) - - -class FacilityUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView): - model = Facility - template_name = "facility_form.html" - pk_url_kwarg = "facility_uuid" - fields = ["facility_type", "name", "description", "location"] - - def get_form(self, *args, **kwargs): - form = super().get_form(*args, **kwargs) - form.fields["location"].widget = LeafletWidget( - attrs={ - "display_raw": "true", - } - ) - return form - - def get_success_url(self): - messages.success(self.request, "The Facility has been updated") - return reverse( - "backoffice:facility_detail", - kwargs={ - "camp_slug": self.camp.slug, - "facility_uuid": self.get_object().uuid, - }, - ) - - -class FacilityDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView): - model = Facility - template_name = "facility_delete.html" - pk_url_kwarg = "facility_uuid" - - def delete(self, *args, **kwargs): - self.get_object().feedbacks.all().delete() - self.get_object().opening_hours.all().delete() - return super().delete(*args, **kwargs) - - def get_success_url(self): - messages.success(self.request, "The Facility has been deleted") - return reverse("backoffice:facility_list", kwargs={"camp_slug": self.camp.slug}) - - -class FacilityFeedbackView(CampViewMixin, RaisePermissionRequiredMixin, FormView): - template_name = "facilityfeedback_backoffice.html" - - def get_permission_required(self): - """ - This view requires two permissions, camps.backoffice_permission and - the permission_set for the team in question. - """ - if not self.team.permission_set: - raise PermissionDenied("No permissions set defined for this team") - return ["camps.backoffice_permission", self.team.permission_set] - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - self.team = get_object_or_404( - Team, camp=self.camp, slug=self.kwargs["team_slug"] - ) - self.queryset = FacilityFeedback.objects.filter( - facility__facility_type__responsible_team=self.team, handled=False - ) - self.form_class = modelformset_factory( - FacilityFeedback, - fields=("handled",), - min_num=self.queryset.count(), - validate_min=True, - max_num=self.queryset.count(), - validate_max=True, - extra=0, - ) - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - context["team"] = self.team - context["feedback_list"] = self.queryset - context["formset"] = self.form_class(queryset=self.queryset) - return context - - def form_valid(self, form): - form.save() - if form.changed_objects: - messages.success( - self.request, - f"Marked {len(form.changed_objects)} FacilityFeedbacks as handled!", - ) - return redirect(self.get_success_url()) - - def get_success_url(self, *args, **kwargs): - return reverse( - "backoffice:facilityfeedback", - kwargs={"camp_slug": self.camp.slug, "team_slug": self.team.slug}, - ) - - -class FacilityMixin(CampViewMixin): - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - self.facility = get_object_or_404(Facility, uuid=kwargs["facility_uuid"]) - - 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. - """ - form = super().get_form(*args, **kwargs) - form.fields["when"].widget.widgets[0].attrs = { - "placeholder": f"Open Date and Time (YYYY-MM-DD HH:MM). Active time zone is {settings.TIME_ZONE}.", - } - form.fields["when"].widget.widgets[1].attrs = { - "placeholder": f"Close Date and Time (YYYY-MM-DD HH:MM). Active time zone is {settings.TIME_ZONE}.", - } - return form - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["facility"] = self.facility - return context - - -class FacilityOpeningHoursCreateView( - FacilityMixin, OrgaTeamPermissionMixin, CreateView -): - model = FacilityOpeningHours - template_name = "facility_opening_hours_form.html" - fields = ["when", "notes"] - - def form_valid(self, form): - """ - Set facility before saving - """ - hours = form.save(commit=False) - hours.facility = self.facility - hours.save() - messages.success(self.request, "New opening hours created successfully!") - return redirect( - reverse( - "backoffice:facility_detail", - kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk}, - ) - ) - - -class FacilityOpeningHoursUpdateView( - FacilityMixin, OrgaTeamPermissionMixin, UpdateView -): - model = FacilityOpeningHours - template_name = "facility_opening_hours_form.html" - fields = ["when", "notes"] - - def get_success_url(self): - messages.success(self.request, "Opening hours have been updated successfully") - return reverse( - "backoffice:facility_detail", - kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk}, - ) - - -class FacilityOpeningHoursDeleteView( - FacilityMixin, OrgaTeamPermissionMixin, DeleteView -): - model = FacilityOpeningHours - template_name = "facility_opening_hours_delete.html" - - def get_success_url(self): - messages.success(self.request, "Opening hours have been deleted successfully") - return reverse( - "backoffice:facility_detail", - kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk}, - ) - - -####################################### -# 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 - - -################################ -# MERCHANDISE VIEWS - - -class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView): - template_name = "orders_merchandise.html" - - def get_queryset(self, **kwargs): - camp_prefix = "BornHack {}".format(timezone.now().year) - - return ( - OrderProductRelation.objects.filter( - order__refunded=False, - order__cancelled=False, - product__category__name="Merchandise", - ) - .filter(product__name__startswith=camp_prefix) - .order_by("order") - ) - - -class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView): - template_name = "merchandise_to_order.html" - - def get_context_data(self, **kwargs): - camp_prefix = "BornHack {}".format(timezone.now().year) - - order_relations = OrderProductRelation.objects.filter( - order__refunded=False, - order__cancelled=False, - product__category__name="Merchandise", - ).filter(product__name__startswith=camp_prefix) - - merchandise_orders = {} - for relation in order_relations: - try: - quantity = merchandise_orders[relation.product.name] + relation.quantity - merchandise_orders[relation.product.name] = quantity - except KeyError: - merchandise_orders[relation.product.name] = relation.quantity - - context = super().get_context_data(**kwargs) - context["merchandise"] = merchandise_orders - return context - - -################################ -# VILLAGE VIEWS - - -class VillageOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView): - template_name = "orders_village.html" - - def get_queryset(self, **kwargs): - camp_prefix = "BornHack {}".format(timezone.now().year) - - return ( - OrderProductRelation.objects.filter( - ticket_generated=False, - order__paid=True, - order__refunded=False, - order__cancelled=False, - product__category__name="Villages", - ) - .filter(product__name__startswith=camp_prefix) - .order_by("order") - ) - - -class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView): - template_name = "village_to_order.html" - - def get_context_data(self, **kwargs): - camp_prefix = "BornHack {}".format(timezone.now().year) - - order_relations = OrderProductRelation.objects.filter( - ticket_generated=False, - order__paid=True, - order__refunded=False, - order__cancelled=False, - product__category__name="Villages", - ).filter(product__name__startswith=camp_prefix) - - village_orders = {} - for relation in order_relations: - try: - quantity = village_orders[relation.product.name] + relation.quantity - village_orders[relation.product.name] = quantity - except KeyError: - village_orders[relation.product.name] = relation.quantity - - context = super().get_context_data(**kwargs) - context["village"] = village_orders - return context - - -################################ -# CHAINS & CREDEBTORS - - -class ChainListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): - model = Chain - template_name = "chain_list_backoffice.html" - - def get_queryset(self, *args, **kwargs): - """Annotate the total count and amount for expenses and revenues for all credebtors in each chain.""" - qs = Chain.objects.annotate( - camp_expenses_amount=Sum( - "credebtors__expenses__amount", - filter=Q(credebtors__expenses__camp=self.camp), - distinct=True, - ), - camp_expenses_count=Count( - "credebtors__expenses", - filter=Q(credebtors__expenses__camp=self.camp), - distinct=True, - ), - camp_revenues_amount=Sum( - "credebtors__revenues__amount", - filter=Q(credebtors__revenues__camp=self.camp), - distinct=True, - ), - camp_revenues_count=Count( - "credebtors__revenues", - filter=Q(credebtors__revenues__camp=self.camp), - distinct=True, - ), - ) - return qs - - -class ChainDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView): - model = Chain - template_name = "chain_detail_backoffice.html" - slug_url_kwarg = "chain_slug" - - def get_queryset(self, *args, **kwargs): - """Annotate the Chain object with the camp filtered expense and revenue info.""" - qs = super().get_queryset(*args, **kwargs) - qs = qs.annotate( - camp_expenses_amount=Sum( - "credebtors__expenses__amount", - filter=Q(credebtors__expenses__camp=self.camp), - distinct=True, - ), - camp_expenses_count=Count( - "credebtors__expenses", - filter=Q(credebtors__expenses__camp=self.camp), - distinct=True, - ), - camp_revenues_amount=Sum( - "credebtors__revenues__amount", - filter=Q(credebtors__revenues__camp=self.camp), - distinct=True, - ), - camp_revenues_count=Count( - "credebtors__revenues", - filter=Q(credebtors__revenues__camp=self.camp), - distinct=True, - ), - ) - return qs - - def get_context_data(self, *args, **kwargs): - """Add credebtors, expenses and revenues to the context in camp-filtered versions.""" - context = super().get_context_data(*args, **kwargs) - - # include credebtors as a seperate queryset with annotations for total number and - # amount of expenses and revenues - context["credebtors"] = Credebtor.objects.filter( - chain=self.get_object() - ).annotate( - camp_expenses_amount=Sum( - "expenses__amount", filter=Q(expenses__camp=self.camp), distinct=True - ), - camp_expenses_count=Count( - "expenses", filter=Q(expenses__camp=self.camp), distinct=True - ), - camp_revenues_amount=Sum( - "revenues__amount", filter=Q(revenues__camp=self.camp), distinct=True - ), - camp_revenues_count=Count( - "revenues", filter=Q(revenues__camp=self.camp), distinct=True - ), - ) - - # Include expenses and revenues for the Chain in context as seperate querysets, - # since accessing them through the relatedmanager returns for all camps - context["expenses"] = Expense.objects.filter( - camp=self.camp, creditor__chain=self.get_object() - ).prefetch_related("responsible_team", "user", "creditor") - context["revenues"] = Revenue.objects.filter( - camp=self.camp, debtor__chain=self.get_object() - ).prefetch_related("responsible_team", "user", "debtor") - return context - - -class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView): - model = Credebtor - template_name = "credebtor_detail_backoffice.html" - slug_url_kwarg = "credebtor_slug" - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - context["expenses"] = ( - self.get_object() - .expenses.filter(camp=self.camp) - .prefetch_related("responsible_team", "user", "creditor") - ) - context["revenues"] = ( - self.get_object() - .revenues.filter(camp=self.camp) - .prefetch_related("responsible_team", "user", "debtor") - ) - return context - - -################################ -# EXPENSES - - -class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): - model = Expense - template_name = "expense_list_backoffice.html" - - def get_queryset(self, **kwargs): - """ - Exclude unapproved expenses, they are shown seperately - """ - queryset = super().get_queryset(**kwargs) - return queryset.exclude(approved__isnull=True).prefetch_related( - "creditor", - "user", - "responsible_team", - ) - - def get_context_data(self, **kwargs): - """ - Include unapproved expenses seperately - """ - context = super().get_context_data(**kwargs) - context["unapproved_expenses"] = Expense.objects.filter( - camp=self.camp, approved__isnull=True - ).prefetch_related( - "creditor", - "user", - "responsible_team", - ) - return context - - -class ExpenseDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): - model = Expense - template_name = "expense_detail_backoffice.html" - fields = ["notes"] - - def form_valid(self, form): - """ - We have two submit buttons in this form, Approve and Reject - """ - expense = form.save() - if "approve" in form.data: - # approve button was pressed - expense.approve(self.request) - elif "reject" in form.data: - # reject button was pressed - expense.reject(self.request) - else: - messages.error(self.request, "Unknown submit action") - return redirect( - reverse("backoffice:expense_list", kwargs={"camp_slug": self.camp.slug}) - ) - - -###################################### -# REIMBURSEMENTS - - -class ReimbursementListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): - model = Reimbursement - template_name = "reimbursement_list_backoffice.html" - - -class ReimbursementDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView): - model = Reimbursement - template_name = "reimbursement_detail_backoffice.html" - - -class ReimbursementCreateUserSelectView( - CampViewMixin, EconomyTeamPermissionMixin, ListView -): - template_name = "reimbursement_create_userselect.html" - - def get_queryset(self): - queryset = User.objects.filter( - id__in=Expense.objects.filter( - camp=self.camp, - reimbursement__isnull=True, - paid_by_bornhack=False, - approved=True, - ) - .values_list("user", flat=True) - .distinct() - ) - return queryset - - -class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateView): - model = Reimbursement - template_name = "reimbursement_create.html" - fields = ["notes", "paid"] - - def dispatch(self, request, *args, **kwargs): - """ Get the user from kwargs """ - self.reimbursement_user = get_object_or_404(User, pk=kwargs["user_id"]) - - # get response now so we have self.camp available below - response = super().dispatch(request, *args, **kwargs) - - # return the response - return response - - def get(self, request, *args, **kwargs): - # does this user have any approved and un-reimbursed expenses? - if not self.reimbursement_user.expenses.filter( - reimbursement__isnull=True, approved=True, paid_by_bornhack=False - ): - messages.error( - request, "This user has no approved and unreimbursed expenses!" - ) - return redirect( - reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug}) - ) - return super().get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["expenses"] = Expense.objects.filter( - user=self.reimbursement_user, - approved=True, - reimbursement__isnull=True, - paid_by_bornhack=False, - ) - context["total_amount"] = context["expenses"].aggregate(Sum("amount")) - context["reimbursement_user"] = self.reimbursement_user - return context - - def form_valid(self, form): - """ - Set user and camp for the Reimbursement before saving - """ - # get the expenses for this user - expenses = Expense.objects.filter( - user=self.reimbursement_user, - approved=True, - reimbursement__isnull=True, - paid_by_bornhack=False, - ) - if not expenses: - messages.error(self.request, "No expenses found") - return redirect( - reverse( - "backoffice:reimbursement_list", - kwargs={"camp_slug": self.camp.slug}, - ) - ) - - # get the Economy team for this camp - try: - economyteam = Team.objects.get( - camp=self.camp, name=settings.ECONOMYTEAM_NAME - ) - except Team.DoesNotExist: - messages.error(self.request, "No economy team found") - return redirect( - reverse( - "backoffice:reimbursement_list", - kwargs={"camp_slug": self.camp.slug}, - ) - ) - - # create reimbursement in database - reimbursement = form.save(commit=False) - reimbursement.reimbursement_user = self.reimbursement_user - reimbursement.user = self.request.user - reimbursement.camp = self.camp - reimbursement.save() - - # add all expenses to reimbursement - for expense in expenses: - expense.reimbursement = reimbursement - expense.save() - - # create expense for this reimbursement - expense = Expense() - expense.camp = self.camp - expense.user = self.request.user - expense.amount = reimbursement.amount - expense.description = "Payment of reimbursement %s to %s" % ( - reimbursement.pk, - reimbursement.reimbursement_user, - ) - expense.paid_by_bornhack = True - expense.responsible_team = economyteam - expense.approved = True - expense.reimbursement = reimbursement - expense.invoice_date = timezone.now() - expense.creditor = Credebtor.objects.get(name="Reimbursement") - expense.invoice.save( - "na.jpg", - File( - open( - os.path.join(settings.DJANGO_BASE_PATH, "static_src/img/na.jpg"), - "rb", - ) - ), - ) - expense.save() - - messages.success( - self.request, - "Reimbursement %s has been created with invoice_date %s" - % (reimbursement.pk, timezone.now()), - ) - return redirect( - reverse( - "backoffice:reimbursement_detail", - kwargs={"camp_slug": self.camp.slug, "pk": reimbursement.pk}, - ) - ) - - -class ReimbursementUpdateView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): - model = Reimbursement - template_name = "reimbursement_form.html" - fields = ["notes", "paid"] - - def get_success_url(self): - return reverse( - "backoffice:reimbursement_detail", - kwargs={"camp_slug": self.camp.slug, "pk": self.get_object().pk}, - ) - - -class ReimbursementDeleteView(CampViewMixin, EconomyTeamPermissionMixin, DeleteView): - model = Reimbursement - template_name = "reimbursement_delete.html" - fields = ["notes", "paid"] - - def dispatch(self, request, *args, **kwargs): - response = super().dispatch(request, *args, **kwargs) - if self.get_object().paid: - messages.error( - request, - "This reimbursement has already been paid so it cannot be deleted", - ) - return redirect( - reverse( - "backoffice:reimbursement_list", - kwargs={"camp_slug": self.camp.slug}, - ) - ) - return response - - -################################ -# REVENUES - - -class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): - model = Revenue - template_name = "revenue_list_backoffice.html" - - def get_queryset(self, **kwargs): - """ - Exclude unapproved revenues, they are shown seperately - """ - queryset = super().get_queryset(**kwargs) - return queryset.exclude(approved__isnull=True).prefetch_related( - "debtor", - "user", - "responsible_team", - ) - - def get_context_data(self, **kwargs): - """ - Include unapproved revenues seperately - """ - context = super().get_context_data(**kwargs) - context["unapproved_revenues"] = Revenue.objects.filter( - camp=self.camp, approved__isnull=True - ) - return context - - -class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): - model = Revenue - template_name = "revenue_detail_backoffice.html" - fields = ["notes"] - - def form_valid(self, form): - """ - We have two submit buttons in this form, Approve and Reject - """ - revenue = form.save() - if "approve" in form.data: - # approve button was pressed - revenue.approve(self.request) - elif "reject" in form.data: - # reject button was pressed - revenue.reject(self.request) - else: - messages.error(self.request, "Unknown submit action") - return redirect( - reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug}) - ) - - -def _ticket_getter_by_token(token): - for ticket_class in [ShopTicket, SponsorTicket, DiscountTicket]: - try: - return ticket_class.objects.get(token=token), False - except ticket_class.DoesNotExist: - try: - return ticket_class.objects.get(badge_token=token), True - except ticket_class.DoesNotExist: - pass - - -def _ticket_getter_by_pk(pk): - for ticket_class in [ShopTicket, SponsorTicket, DiscountTicket]: - try: - return ticket_class.objects.get(pk=pk) - except ticket_class.DoesNotExist: - pass - - -class ScanTicketsView( - LoginRequiredMixin, InfoTeamPermissionMixin, CampViewMixin, TemplateView -): - template_name = "info_desk/scan.html" - - ticket = None - order = None - order_search = False - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - if self.ticket: - context["ticket"] = self.ticket - - elif "ticket_token" in self.request.POST: - - # Slice to get rid of the first character which is a '#' - ticket_token = self.request.POST.get("ticket_token")[1:] - - ticket, is_badge = _ticket_getter_by_token(ticket_token) - - if ticket: - context["ticket"] = ticket - context["is_badge"] = is_badge - else: - messages.warning(self.request, "Ticket not found!") - - elif self.order_search: - context["order"] = self.order - - return context - - def post(self, request, **kwargs): - if "check_in_ticket_id" in request.POST: - self.ticket = self.check_in_ticket(request) - elif "badge_ticket_id" in request.POST: - self.ticket = self.hand_out_badge(request) - elif "find_order_id" in request.POST: - self.order_search = True - try: - order_id = self.request.POST.get("find_order_id") - self.order = Order.objects.get(id=order_id) - except Order.DoesNotExist: - pass - elif "mark_as_paid" in request.POST: - self.mark_order_as_paid(request) - - return super().get(request, **kwargs) - - def check_in_ticket(self, request): - check_in_ticket_id = request.POST.get("check_in_ticket_id") - ticket_to_check_in = _ticket_getter_by_pk(check_in_ticket_id) - ticket_to_check_in.used = True - ticket_to_check_in.save() - messages.info(request, "Ticket checked-in!") - return ticket_to_check_in - - def hand_out_badge(self, request): - badge_ticket_id = request.POST.get("badge_ticket_id") - ticket_to_handout_badge_for = _ticket_getter_by_pk(badge_ticket_id) - ticket_to_handout_badge_for.badge_handed_out = True - ticket_to_handout_badge_for.save() - messages.info(request, "Badge marked as handed out!") - return ticket_to_handout_badge_for - - def mark_order_as_paid(self, request): - order = Order.objects.get(id=request.POST.get("mark_as_paid")) - order.mark_as_paid() - messages.success(request, "Order #{} has been marked as paid!".format(order.id)) - - -class ShopTicketOverview(LoginRequiredMixin, CampViewMixin, ListView): - model = ShopTicket - template_name = "shop_ticket_overview.html" - context_object_name = "shop_tickets" - - def get_context_data(self, *, object_list=None, **kwargs): - kwargs["ticket_types"] = TicketType.objects.filter(camp=self.camp) - return super().get_context_data(object_list=object_list, **kwargs) - - -class BackofficeProxyView(CampViewMixin, RaisePermissionRequiredMixin, TemplateView): - """ - Show proxied stuff, only for simple HTML pages with no external content - Define URLs in settings.BACKOFFICE_PROXY_URLS as a dict of slug: (description, url) pairs - """ - - permission_required = "camps.backoffice_permission" - template_name = "backoffice_proxy.html" - - def dispatch(self, request, *args, **kwargs): - """ Perform the request and return the response if we have a slug """ - # list available stuff if we have no slug - if "proxy_slug" not in kwargs: - return super().dispatch(request, *args, **kwargs) - - # is the slug valid? - if kwargs["proxy_slug"] not in settings.BACKOFFICE_PROXY_URLS.keys(): - raise Http404 - - # perform the request - description, url = settings.BACKOFFICE_PROXY_URLS[kwargs["proxy_slug"]] - r = requests.get(url) - - # return the response, keeping the status code but no headers - return HttpResponse(r.content, status=r.status_code) - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - context["urls"] = settings.BACKOFFICE_PROXY_URLS - return context - - -################################ -# UPDATE HELD OUTGOING EMAILS - - -class OutgoingEmailMassUpdateView(CampViewMixin, OrgaTeamPermissionMixin, FormView): - """ - This view shows a list with forms to edit OutgoingEmail objects with hold=True - """ - - template_name = "outgoing_email_mass_update.html" - - def setup(self, *args, **kwargs): - """Get emails with no team and emails with a team for the current camp.""" - super().setup(*args, **kwargs) - self.queryset = OutgoingEmail.objects.filter( - hold=True, responsible_team__isnull=True - ).prefetch_related("responsible_team") | OutgoingEmail.objects.filter( - hold=True, responsible_team__camp=self.camp - ).prefetch_related( - "responsible_team" - ) - self.form_class = modelformset_factory( - OutgoingEmail, - fields=["subject", "text_template", "html_template", "hold"], - min_num=self.queryset.count(), - validate_min=True, - max_num=self.queryset.count(), - validate_max=True, - extra=0, - ) - - def get_context_data(self, *args, **kwargs): - """Include the formset in the context.""" - context = super().get_context_data(*args, **kwargs) - context["formset"] = self.form_class(queryset=self.queryset) - return context - - def form_valid(self, form): - """Show a message saying how many objects were updated.""" - form.save() - if form.changed_objects: - messages.success( - self.request, f"Updated {len(form.changed_objects)} OutgoingEmails" - ) - return redirect(self.get_success_url()) - - def get_success_url(self, *args, **kwargs): - """Return to the backoffice index.""" - return reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug}) - - -################################ -# Pos and PosReport views - - -class PosListView(CampViewMixin, RaisePermissionRequiredMixin, ListView): - """Show a list of Pos this user has access to (through team memberships).""" - - permission_required = "camps.backoffice_permission" - model = Pos - template_name = "pos_list.html" - - -class PosDetailView(PosViewMixin, DetailView): - """Show details for a Pos.""" - - model = Pos - template_name = "pos_detail.html" - slug_url_kwarg = "pos_slug" - - -class PosCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView): - """Create a new Pos (orga only).""" - - model = Pos - template_name = "pos_form.html" - fields = ["name", "team"] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"].fields["team"].queryset = Team.objects.filter(camp=self.camp) - return context - - -class PosUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView): - """Update a Pos.""" - - model = Pos - template_name = "pos_form.html" - slug_url_kwarg = "pos_slug" - fields = ["name", "team"] - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"].fields["team"].queryset = Team.objects.filter(camp=self.camp) - return context - - -class PosDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView): - model = Pos - template_name = "pos_delete.html" - slug_url_kwarg = "pos_slug" - - def delete(self, *args, **kwargs): - self.get_object().pos_reports.all().delete() - return super().delete(*args, **kwargs) - - def get_success_url(self): - messages.success( - self.request, "The Pos and all related PosReports has been deleted" - ) - return reverse("backoffice:pos_list", kwargs={"camp_slug": self.camp.slug}) - - -class PosReportCreateView(PosViewMixin, CreateView): - """Use this view to create new PosReports.""" - - model = PosReport - fields = ["date", "bank_responsible", "pos_responsible", "comments"] - template_name = "posreport_form.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"].fields["bank_responsible"].queryset = Team.objects.get( - camp=self.camp, - name="Orga", - ).approved_members.all() - context["form"].fields[ - "pos_responsible" - ].queryset = self.pos.team.responsible_members.all() - return context - - def form_valid(self, form): - """ - Set Pos before saving - """ - pr = form.save(commit=False) - pr.pos = self.pos - pr.save() - messages.success(self.request, "New PosReport created successfully!") - return redirect( - reverse( - "backoffice:posreport_detail", - kwargs={ - "camp_slug": self.camp.slug, - "pos_slug": self.pos.slug, - "posreport_uuid": pr.uuid, - }, - ) - ) - - -class PosReportUpdateView(PosViewMixin, UpdateView): - """Use this view to update PosReports.""" - - model = PosReport - fields = [ - "date", - "bank_responsible", - "pos_responsible", - "hax_sold_izettle", - "hax_sold_website", - "dkk_sales_izettle", - "comments", - ] - template_name = "posreport_form.html" - pk_url_kwarg = "posreport_uuid" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"].fields["bank_responsible"].queryset = Team.objects.get( - camp=self.camp, - name="Orga", - ).approved_members.all() - context["form"].fields[ - "pos_responsible" - ].queryset = self.pos.team.responsible_members.all() - return context - - -class PosReportDetailView(PosViewMixin, DetailView): - """Show details for a PosReport.""" - - model = PosReport - template_name = "posreport_detail.html" - pk_url_kwarg = "posreport_uuid" - - -class PosReportBankCountStartView(PosViewMixin, UpdateView): - """The bank responsible for a PosReport uses this view to add day-start HAX and DKK counts to a PosReport.""" - - model = PosReport - template_name = "posreport_form.html" - fields = [ - "bank_count_dkk_start", - "bank_count_hax5_start", - "bank_count_hax10_start", - "bank_count_hax20_start", - "bank_count_hax50_start", - "bank_count_hax100_start", - ] - pk_url_kwarg = "posreport_uuid" - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - if self.request.user != self.get_object().bank_responsible: - raise PermissionDenied("Only the bank responsible can do this") - - -class PosReportBankCountEndView(PosViewMixin, UpdateView): - """The bank responsible for a PosReport uses this view to add day-end HAX and DKK counts to a PosReport.""" - - model = PosReport - template_name = "posreport_form.html" - fields = [ - "bank_count_dkk_end", - "bank_count_hax5_end", - "bank_count_hax10_end", - "bank_count_hax20_end", - "bank_count_hax50_end", - "bank_count_hax100_end", - ] - pk_url_kwarg = "posreport_uuid" - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - if self.request.user != self.get_object().bank_responsible: - raise PermissionDenied("Only the bank responsible can do this") - - -class PosReportPosCountStartView(PosViewMixin, UpdateView): - """The Pos responsible for a PosReport uses this view to add day-start HAX and DKK counts to a PosReport.""" - - model = PosReport - template_name = "posreport_form.html" - fields = [ - "pos_count_dkk_start", - "pos_count_hax5_start", - "pos_count_hax10_start", - "pos_count_hax20_start", - "pos_count_hax50_start", - "pos_count_hax100_start", - ] - pk_url_kwarg = "posreport_uuid" - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - if self.request.user != self.get_object().pos_responsible: - raise PermissionDenied("Only the Pos responsible can do this") - - -class PosReportPosCountEndView(PosViewMixin, UpdateView): - """The Pos responsible for a PosReport uses this view to add day-end HAX and DKK counts to a PosReport.""" - - model = PosReport - template_name = "posreport_form.html" - fields = [ - "pos_count_dkk_end", - "pos_count_hax5_end", - "pos_count_hax10_end", - "pos_count_hax20_end", - "pos_count_hax50_end", - "pos_count_hax100_end", - "pos_json", - ] - pk_url_kwarg = "posreport_uuid" - - def setup(self, *args, **kwargs): - super().setup(*args, **kwargs) - if self.request.user != self.get_object().pos_responsible: - raise PermissionDenied("Only the pos responsible can do this") - - -################################ -# Secret Token views - - -class TokenListView(CampViewMixin, RaisePermissionRequiredMixin, ListView): - """Show a list of secret tokens for this camp""" - - permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] - model = Token - template_name = "token_list.html" - - -class TokenDetailView(CampViewMixin, RaisePermissionRequiredMixin, DetailView): - """Show details for a token.""" - - permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] - model = Token - template_name = "token_detail.html" - - -class TokenCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): - """Create a new Token.""" - - permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] - model = Token - template_name = "token_form.html" - fields = ["token", "category", "description"] - - def form_valid(self, form): - token = form.save(commit=False) - token.camp = self.camp - token.save() - return redirect( - reverse( - "backoffice:token_detail", - kwargs={"camp_slug": self.camp.slug, "pk": token.id}, - ) - ) - - -class TokenUpdateView(CampViewMixin, RaisePermissionRequiredMixin, UpdateView): - """Update a token.""" - - permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] - model = Token - template_name = "token_form.html" - fields = ["token", "category", "description"] - - -class TokenDeleteView(CampViewMixin, RaisePermissionRequiredMixin, DeleteView): - permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] - model = Token - template_name = "token_delete.html" - - def delete(self, *args, **kwargs): - self.get_object().tokenfind_set.all().delete() - return super().delete(*args, **kwargs) - - def get_success_url(self): - messages.success( - self.request, "The Token and all related TokenFinds has been deleted" - ) - return reverse("backoffice:token_list", kwargs={"camp_slug": self.camp.slug}) - - -class TokenStatsView(CampViewMixin, RaisePermissionRequiredMixin, ListView): - """Show stats for token finds for this camp""" - - permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] - model = User - template_name = "token_stats.html" - - def get_queryset(self, **kwargs): - tokenusers = ( - TokenFind.objects.filter(token__camp=self.camp) - .distinct("user") - .values_list("user", flat=True) - ) - return ( - User.objects.filter(id__in=tokenusers) - .annotate( - token_find_count=Count( - "token_finds", filter=Q(token_finds__token__camp=self.camp) - ) - ) - .exclude(token_find_count=0) - ) diff --git a/src/backoffice/views/__init__.py b/src/backoffice/views/__init__.py new file mode 100644 index 00000000..bc0c5f78 --- /dev/null +++ b/src/backoffice/views/__init__.py @@ -0,0 +1,10 @@ +"""Backoffice views.py was split into multiple files in July 2021 /tyk.""" +from .backoffice import * # noqa +from .content import * # noqa +from .economy import * # noqa +from .facilities import * # noqa +from .game import * # noqa +from .infodesk import * # noqa +from .orga import * # noqa +from .pos import * # noqa +from .program import * # noqa diff --git a/src/backoffice/views/backoffice.py b/src/backoffice/views/backoffice.py new file mode 100644 index 00000000..ddf9bc43 --- /dev/null +++ b/src/backoffice/views/backoffice.py @@ -0,0 +1,73 @@ +import logging + +import requests +from camps.mixins import CampViewMixin +from django.conf import settings +from django.http import Http404, HttpResponse +from django.views.generic import TemplateView +from facilities.models import ( + FacilityFeedback, +) +from teams.models import Team +from utils.models import OutgoingEmail + +from ..mixins import ( + RaisePermissionRequiredMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + +class BackofficeIndexView(CampViewMixin, RaisePermissionRequiredMixin, TemplateView): + """ + The Backoffice index view only requires camps.backoffice_permission so we use RaisePermissionRequiredMixin directly + """ + + permission_required = "camps.backoffice_permission" + template_name = "index.html" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["facilityfeedback_teams"] = Team.objects.filter( + id__in=set( + FacilityFeedback.objects.filter( + facility__facility_type__responsible_team__camp=self.camp, + handled=False, + ).values_list( + "facility__facility_type__responsible_team__id", flat=True + ) + ) + ) + context["held_email_count"] = OutgoingEmail.objects.filter(hold=True, responsible_team__isnull=True).count() + OutgoingEmail.objects.filter(hold=True, responsible_team__camp=self.camp).count() + return context + + +class BackofficeProxyView(CampViewMixin, RaisePermissionRequiredMixin, TemplateView): + """ + Show proxied stuff, only for simple HTML pages with no external content + Define URLs in settings.BACKOFFICE_PROXY_URLS as a dict of slug: (description, url) pairs + """ + + permission_required = "camps.backoffice_permission" + template_name = "backoffice_proxy.html" + + def dispatch(self, request, *args, **kwargs): + """ Perform the request and return the response if we have a slug """ + # list available stuff if we have no slug + if "proxy_slug" not in kwargs: + return super().dispatch(request, *args, **kwargs) + + # is the slug valid? + if kwargs["proxy_slug"] not in settings.BACKOFFICE_PROXY_URLS.keys(): + raise Http404 + + # perform the request + description, url = settings.BACKOFFICE_PROXY_URLS[kwargs["proxy_slug"]] + r = requests.get(url) + + # return the response, keeping the status code but no headers + return HttpResponse(r.content, status=r.status_code) + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["urls"] = settings.BACKOFFICE_PROXY_URLS + return context diff --git a/src/backoffice/views/content.py b/src/backoffice/views/content.py new file mode 100644 index 00000000..b592bfca --- /dev/null +++ b/src/backoffice/views/content.py @@ -0,0 +1,134 @@ +import logging + +from camps.mixins import CampViewMixin +from django.contrib import messages +from django.forms import modelformset_factory +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic.edit import FormView +from program.models import ( + Event, + EventFeedback, + Url, + UrlType, +) + +from ..forms import ( + AddRecordingForm, +) +from ..mixins import ( + ContentTeamPermissionMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class ApproveFeedbackView(CampViewMixin, ContentTeamPermissionMixin, FormView): + """ + This view shows a list of EventFeedback objects which are pending approval. + """ + + model = EventFeedback + template_name = "approve_feedback.html" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.queryset = EventFeedback.objects.filter( + event__track__camp=self.camp, approved__isnull=True + ) + + self.form_class = modelformset_factory( + EventFeedback, + fields=("approved",), + min_num=self.queryset.count(), + validate_min=True, + max_num=self.queryset.count(), + validate_max=True, + extra=0, + ) + + def get_context_data(self, *args, **kwargs): + """ + Include the queryset used for the modelformset_factory so we have + some idea which object is which in the template + Why the hell do the forms in the formset not include the object? + """ + context = super().get_context_data(*args, **kwargs) + context["event_feedback_list"] = self.queryset + context["formset"] = self.form_class(queryset=self.queryset) + return context + + def form_valid(self, form): + form.save() + if form.changed_objects: + messages.success( + self.request, f"Updated {len(form.changed_objects)} EventFeedbacks" + ) + return redirect(self.get_success_url()) + + def get_success_url(self, *args, **kwargs): + return reverse( + "backoffice:approve_event_feedback", kwargs={"camp_slug": self.camp.slug} + ) + + +class AddRecordingView(CampViewMixin, ContentTeamPermissionMixin, FormView): + """ + This view shows a list of events that is set to be recorded, but without a recording URL attached. + """ + + model = Event + template_name = "add_recording.html" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.queryset = Event.objects.filter( + track__camp=self.camp, video_recording=True + ).exclude( + urls__url_type__name="Recording" + ) + + self.form_class = modelformset_factory( + Event, + form=AddRecordingForm, + min_num=self.queryset.count(), + validate_min=True, + max_num=self.queryset.count(), + validate_max=True, + extra=0, + ) + + def get_context_data(self, *args, **kwargs): + """ + Include the queryset used for the modelformset_factory so we have + some idea which object is which in the template + Why the hell do the forms in the formset not include the object? + """ + context = super().get_context_data(*args, **kwargs) + context["event_list"] = self.queryset + context["formset"] = self.form_class(queryset=self.queryset) + return context + + def form_valid(self, form): + form.save() + + for event_data in form.cleaned_data: + if event_data['recording_url']: + url = event_data['recording_url'] + if not event_data['id'].urls.filter(url=url).exists(): + recording_url = Url() + recording_url.event = event_data['id'] + recording_url.url = url + recording_url.url_type = UrlType.objects.get(name="Recording") + recording_url.save() + + if form.changed_objects: + messages.success( + self.request, f"Updated {len(form.changed_objects)} Event" + ) + return redirect(self.get_success_url()) + + def get_success_url(self, *args, **kwargs): + return reverse( + "backoffice:add_eventrecording", kwargs={"camp_slug": self.camp.slug} + ) diff --git a/src/backoffice/views/economy.py b/src/backoffice/views/economy.py new file mode 100644 index 00000000..b6c5cdb7 --- /dev/null +++ b/src/backoffice/views/economy.py @@ -0,0 +1,452 @@ +import logging +import os + +from camps.mixins import CampViewMixin +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.models import User +from django.core.files import File +from django.db.models import Count, Q, Sum +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.utils import timezone +from django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, DeleteView, UpdateView +from economy.models import ( + Chain, + Credebtor, + Expense, + Reimbursement, + Revenue, +) +from teams.models import Team + +from ..mixins import ( + EconomyTeamPermissionMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +################################ +# CHAINS & CREDEBTORS + + +class ChainListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): + model = Chain + template_name = "chain_list_backoffice.html" + + def get_queryset(self, *args, **kwargs): + """Annotate the total count and amount for expenses and revenues for all credebtors in each chain.""" + qs = Chain.objects.annotate( + camp_expenses_amount=Sum( + "credebtors__expenses__amount", + filter=Q(credebtors__expenses__camp=self.camp), + distinct=True, + ), + camp_expenses_count=Count( + "credebtors__expenses", + filter=Q(credebtors__expenses__camp=self.camp), + distinct=True, + ), + camp_revenues_amount=Sum( + "credebtors__revenues__amount", + filter=Q(credebtors__revenues__camp=self.camp), + distinct=True, + ), + camp_revenues_count=Count( + "credebtors__revenues", + filter=Q(credebtors__revenues__camp=self.camp), + distinct=True, + ), + ) + return qs + + +class ChainDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView): + model = Chain + template_name = "chain_detail_backoffice.html" + slug_url_kwarg = "chain_slug" + + def get_queryset(self, *args, **kwargs): + """Annotate the Chain object with the camp filtered expense and revenue info.""" + qs = super().get_queryset(*args, **kwargs) + qs = qs.annotate( + camp_expenses_amount=Sum( + "credebtors__expenses__amount", + filter=Q(credebtors__expenses__camp=self.camp), + distinct=True, + ), + camp_expenses_count=Count( + "credebtors__expenses", + filter=Q(credebtors__expenses__camp=self.camp), + distinct=True, + ), + camp_revenues_amount=Sum( + "credebtors__revenues__amount", + filter=Q(credebtors__revenues__camp=self.camp), + distinct=True, + ), + camp_revenues_count=Count( + "credebtors__revenues", + filter=Q(credebtors__revenues__camp=self.camp), + distinct=True, + ), + ) + return qs + + def get_context_data(self, *args, **kwargs): + """Add credebtors, expenses and revenues to the context in camp-filtered versions.""" + context = super().get_context_data(*args, **kwargs) + + # include credebtors as a seperate queryset with annotations for total number and + # amount of expenses and revenues + context["credebtors"] = Credebtor.objects.filter( + chain=self.get_object() + ).annotate( + camp_expenses_amount=Sum( + "expenses__amount", filter=Q(expenses__camp=self.camp), distinct=True + ), + camp_expenses_count=Count( + "expenses", filter=Q(expenses__camp=self.camp), distinct=True + ), + camp_revenues_amount=Sum( + "revenues__amount", filter=Q(revenues__camp=self.camp), distinct=True + ), + camp_revenues_count=Count( + "revenues", filter=Q(revenues__camp=self.camp), distinct=True + ), + ) + + # Include expenses and revenues for the Chain in context as seperate querysets, + # since accessing them through the relatedmanager returns for all camps + context["expenses"] = Expense.objects.filter( + camp=self.camp, creditor__chain=self.get_object() + ).prefetch_related("responsible_team", "user", "creditor") + context["revenues"] = Revenue.objects.filter( + camp=self.camp, debtor__chain=self.get_object() + ).prefetch_related("responsible_team", "user", "debtor") + return context + + +class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView): + model = Credebtor + template_name = "credebtor_detail_backoffice.html" + slug_url_kwarg = "credebtor_slug" + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["expenses"] = ( + self.get_object() + .expenses.filter(camp=self.camp) + .prefetch_related("responsible_team", "user", "creditor") + ) + context["revenues"] = ( + self.get_object() + .revenues.filter(camp=self.camp) + .prefetch_related("responsible_team", "user", "debtor") + ) + return context + + +################################ +# EXPENSES + + +class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): + model = Expense + template_name = "expense_list_backoffice.html" + + def get_queryset(self, **kwargs): + """ + Exclude unapproved expenses, they are shown seperately + """ + queryset = super().get_queryset(**kwargs) + return queryset.exclude(approved__isnull=True).prefetch_related( + "creditor", + "user", + "responsible_team", + ) + + def get_context_data(self, **kwargs): + """ + Include unapproved expenses seperately + """ + context = super().get_context_data(**kwargs) + context["unapproved_expenses"] = Expense.objects.filter( + camp=self.camp, approved__isnull=True + ).prefetch_related( + "creditor", + "user", + "responsible_team", + ) + return context + + +class ExpenseDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): + model = Expense + template_name = "expense_detail_backoffice.html" + fields = ["notes"] + + def form_valid(self, form): + """ + We have two submit buttons in this form, Approve and Reject + """ + expense = form.save() + if "approve" in form.data: + # approve button was pressed + expense.approve(self.request) + elif "reject" in form.data: + # reject button was pressed + expense.reject(self.request) + else: + messages.error(self.request, "Unknown submit action") + return redirect( + reverse("backoffice:expense_list", kwargs={"camp_slug": self.camp.slug}) + ) + + +###################################### +# REIMBURSEMENTS + + +class ReimbursementListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): + model = Reimbursement + template_name = "reimbursement_list_backoffice.html" + + +class ReimbursementDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView): + model = Reimbursement + template_name = "reimbursement_detail_backoffice.html" + + +class ReimbursementCreateUserSelectView( + CampViewMixin, EconomyTeamPermissionMixin, ListView +): + template_name = "reimbursement_create_userselect.html" + + def get_queryset(self): + queryset = User.objects.filter( + id__in=Expense.objects.filter( + camp=self.camp, + reimbursement__isnull=True, + paid_by_bornhack=False, + approved=True, + ) + .values_list("user", flat=True) + .distinct() + ) + return queryset + + +class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateView): + model = Reimbursement + template_name = "reimbursement_create.html" + fields = ["notes", "paid"] + + def dispatch(self, request, *args, **kwargs): + """ Get the user from kwargs """ + self.reimbursement_user = get_object_or_404(User, pk=kwargs["user_id"]) + + # get response now so we have self.camp available below + response = super().dispatch(request, *args, **kwargs) + + # return the response + return response + + def get(self, request, *args, **kwargs): + # does this user have any approved and un-reimbursed expenses? + if not self.reimbursement_user.expenses.filter( + reimbursement__isnull=True, approved=True, paid_by_bornhack=False + ): + messages.error( + request, "This user has no approved and unreimbursed expenses!" + ) + return redirect( + reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug}) + ) + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["expenses"] = Expense.objects.filter( + user=self.reimbursement_user, + approved=True, + reimbursement__isnull=True, + paid_by_bornhack=False, + ) + context["total_amount"] = context["expenses"].aggregate(Sum("amount")) + context["reimbursement_user"] = self.reimbursement_user + return context + + def form_valid(self, form): + """ + Set user and camp for the Reimbursement before saving + """ + # get the expenses for this user + expenses = Expense.objects.filter( + user=self.reimbursement_user, + approved=True, + reimbursement__isnull=True, + paid_by_bornhack=False, + ) + if not expenses: + messages.error(self.request, "No expenses found") + return redirect( + reverse( + "backoffice:reimbursement_list", + kwargs={"camp_slug": self.camp.slug}, + ) + ) + + # get the Economy team for this camp + try: + economyteam = Team.objects.get( + camp=self.camp, name=settings.ECONOMYTEAM_NAME + ) + except Team.DoesNotExist: + messages.error(self.request, "No economy team found") + return redirect( + reverse( + "backoffice:reimbursement_list", + kwargs={"camp_slug": self.camp.slug}, + ) + ) + + # create reimbursement in database + reimbursement = form.save(commit=False) + reimbursement.reimbursement_user = self.reimbursement_user + reimbursement.user = self.request.user + reimbursement.camp = self.camp + reimbursement.save() + + # add all expenses to reimbursement + for expense in expenses: + expense.reimbursement = reimbursement + expense.save() + + # create expense for this reimbursement + expense = Expense() + expense.camp = self.camp + expense.user = self.request.user + expense.amount = reimbursement.amount + expense.description = "Payment of reimbursement %s to %s" % ( + reimbursement.pk, + reimbursement.reimbursement_user, + ) + expense.paid_by_bornhack = True + expense.responsible_team = economyteam + expense.approved = True + expense.reimbursement = reimbursement + expense.invoice_date = timezone.now() + expense.creditor = Credebtor.objects.get(name="Reimbursement") + expense.invoice.save( + "na.jpg", + File( + open( + os.path.join(settings.DJANGO_BASE_PATH, "static_src/img/na.jpg"), + "rb", + ) + ), + ) + expense.save() + + messages.success( + self.request, + "Reimbursement %s has been created with invoice_date %s" + % (reimbursement.pk, timezone.now()), + ) + return redirect( + reverse( + "backoffice:reimbursement_detail", + kwargs={"camp_slug": self.camp.slug, "pk": reimbursement.pk}, + ) + ) + + +class ReimbursementUpdateView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): + model = Reimbursement + template_name = "reimbursement_form.html" + fields = ["notes", "paid"] + + def get_success_url(self): + return reverse( + "backoffice:reimbursement_detail", + kwargs={"camp_slug": self.camp.slug, "pk": self.get_object().pk}, + ) + + +class ReimbursementDeleteView(CampViewMixin, EconomyTeamPermissionMixin, DeleteView): + model = Reimbursement + template_name = "reimbursement_delete.html" + fields = ["notes", "paid"] + + def dispatch(self, request, *args, **kwargs): + response = super().dispatch(request, *args, **kwargs) + if self.get_object().paid: + messages.error( + request, + "This reimbursement has already been paid so it cannot be deleted", + ) + return redirect( + reverse( + "backoffice:reimbursement_list", + kwargs={"camp_slug": self.camp.slug}, + ) + ) + return response + + +################################ +# REVENUES + + +class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView): + model = Revenue + template_name = "revenue_list_backoffice.html" + + def get_queryset(self, **kwargs): + """ + Exclude unapproved revenues, they are shown seperately + """ + queryset = super().get_queryset(**kwargs) + return queryset.exclude(approved__isnull=True).prefetch_related( + "debtor", + "user", + "responsible_team", + ) + + def get_context_data(self, **kwargs): + """ + Include unapproved revenues seperately + """ + context = super().get_context_data(**kwargs) + context["unapproved_revenues"] = Revenue.objects.filter( + camp=self.camp, approved__isnull=True + ) + return context + + +class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView): + model = Revenue + template_name = "revenue_detail_backoffice.html" + fields = ["notes"] + + def form_valid(self, form): + """ + We have two submit buttons in this form, Approve and Reject + """ + revenue = form.save() + if "approve" in form.data: + # approve button was pressed + revenue.approve(self.request) + elif "reject" in form.data: + # reject button was pressed + revenue.reject(self.request) + else: + messages.error(self.request, "Unknown submit action") + return redirect( + reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug}) + ) + + diff --git a/src/backoffice/views/facilities.py b/src/backoffice/views/facilities.py new file mode 100644 index 00000000..774e2de1 --- /dev/null +++ b/src/backoffice/views/facilities.py @@ -0,0 +1,324 @@ +import logging + +from camps.mixins import CampViewMixin +from django.conf import settings +from django.contrib import messages +from django.core.exceptions import PermissionDenied +from django.forms import modelformset_factory +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView +from facilities.models import ( + Facility, + FacilityFeedback, + FacilityOpeningHours, + FacilityType, +) +from leaflet.forms.widgets import LeafletWidget +from teams.models import Team + +from ..mixins import ( + OrgaTeamPermissionMixin, + RaisePermissionRequiredMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class FacilityTypeListView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + model = FacilityType + template_name = "facility_type_list_backoffice.html" + context_object_name = "facility_type_list" + + +class FacilityTypeCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView): + model = FacilityType + template_name = "facility_type_form.html" + fields = [ + "name", + "description", + "icon", + "marker", + "responsible_team", + "quickfeedback_options", + ] + + def get_context_data(self, **kwargs): + """ + Do not show teams that are not part of the current camp in the dropdown + """ + context = super().get_context_data(**kwargs) + context["form"].fields["responsible_team"].queryset = Team.objects.filter( + camp=self.camp + ) + return context + + def get_success_url(self): + return reverse( + "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug} + ) + + +class FacilityTypeUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView): + model = FacilityType + template_name = "facility_type_form.html" + fields = [ + "name", + "description", + "icon", + "marker", + "responsible_team", + "quickfeedback_options", + ] + + def get_context_data(self, **kwargs): + """ + Do not show teams that are not part of the current camp in the dropdown + """ + context = super().get_context_data(**kwargs) + context["form"].fields["responsible_team"].queryset = Team.objects.filter( + camp=self.camp + ) + return context + + def get_success_url(self): + return reverse( + "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug} + ) + + +class FacilityTypeDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView): + model = FacilityType + template_name = "facility_type_delete.html" + context_object_name = "facility_type" + + def delete(self, *args, **kwargs): + for facility in self.get_object().facilities.all(): + facility.feedbacks.all().delete() + facility.opening_hours.all().delete() + facility.delete() + return super().delete(*args, **kwargs) + + def get_success_url(self): + messages.success(self.request, "The FacilityType has been deleted") + return reverse( + "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug} + ) + + +class FacilityListView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + model = Facility + template_name = "facility_list_backoffice.html" + + +class FacilityDetailView(CampViewMixin, OrgaTeamPermissionMixin, DetailView): + model = Facility + template_name = "facility_detail_backoffice.html" + pk_url_kwarg = "facility_uuid" + + def get_queryset(self, *args, **kwargs): + qs = super().get_queryset(*args, **kwargs) + return qs.prefetch_related("opening_hours") + + +class FacilityCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView): + model = Facility + template_name = "facility_form.html" + fields = ["facility_type", "name", "description", "location"] + + def get_form(self, *args, **kwargs): + form = super().get_form(*args, **kwargs) + form.fields["location"].widget = LeafletWidget( + attrs={ + "display_raw": "true", + } + ) + return form + + def get_context_data(self, **kwargs): + """ + Do not show types that are not part of the current camp in the dropdown + """ + context = super().get_context_data(**kwargs) + context["form"].fields["facility_type"].queryset = FacilityType.objects.filter( + responsible_team__camp=self.camp + ) + return context + + def get_success_url(self): + messages.success(self.request, "The Facility has been created") + return reverse("backoffice:facility_list", kwargs={"camp_slug": self.camp.slug}) + + +class FacilityUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView): + model = Facility + template_name = "facility_form.html" + pk_url_kwarg = "facility_uuid" + fields = ["facility_type", "name", "description", "location"] + + def get_form(self, *args, **kwargs): + form = super().get_form(*args, **kwargs) + form.fields["location"].widget = LeafletWidget( + attrs={ + "display_raw": "true", + } + ) + return form + + def get_success_url(self): + messages.success(self.request, "The Facility has been updated") + return reverse( + "backoffice:facility_detail", + kwargs={ + "camp_slug": self.camp.slug, + "facility_uuid": self.get_object().uuid, + }, + ) + + +class FacilityDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView): + model = Facility + template_name = "facility_delete.html" + pk_url_kwarg = "facility_uuid" + + def delete(self, *args, **kwargs): + self.get_object().feedbacks.all().delete() + self.get_object().opening_hours.all().delete() + return super().delete(*args, **kwargs) + + def get_success_url(self): + messages.success(self.request, "The Facility has been deleted") + return reverse("backoffice:facility_list", kwargs={"camp_slug": self.camp.slug}) + + +class FacilityFeedbackView(CampViewMixin, RaisePermissionRequiredMixin, FormView): + template_name = "facilityfeedback_backoffice.html" + + def get_permission_required(self): + """ + This view requires two permissions, camps.backoffice_permission and + the permission_set for the team in question. + """ + if not self.team.permission_set: + raise PermissionDenied("No permissions set defined for this team") + return ["camps.backoffice_permission", self.team.permission_set] + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.team = get_object_or_404( + Team, camp=self.camp, slug=self.kwargs["team_slug"] + ) + self.queryset = FacilityFeedback.objects.filter( + facility__facility_type__responsible_team=self.team, handled=False + ) + self.form_class = modelformset_factory( + FacilityFeedback, + fields=("handled",), + min_num=self.queryset.count(), + validate_min=True, + max_num=self.queryset.count(), + validate_max=True, + extra=0, + ) + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["team"] = self.team + context["feedback_list"] = self.queryset + context["formset"] = self.form_class(queryset=self.queryset) + return context + + def form_valid(self, form): + form.save() + if form.changed_objects: + messages.success( + self.request, + f"Marked {len(form.changed_objects)} FacilityFeedbacks as handled!", + ) + return redirect(self.get_success_url()) + + def get_success_url(self, *args, **kwargs): + return reverse( + "backoffice:facilityfeedback", + kwargs={"camp_slug": self.camp.slug, "team_slug": self.team.slug}, + ) + + +class FacilityMixin(CampViewMixin): + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.facility = get_object_or_404(Facility, uuid=kwargs["facility_uuid"]) + + 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. + """ + form = super().get_form(*args, **kwargs) + form.fields["when"].widget.widgets[0].attrs = { + "placeholder": f"Open Date and Time (YYYY-MM-DD HH:MM). Active time zone is {settings.TIME_ZONE}.", + } + form.fields["when"].widget.widgets[1].attrs = { + "placeholder": f"Close Date and Time (YYYY-MM-DD HH:MM). Active time zone is {settings.TIME_ZONE}.", + } + return form + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["facility"] = self.facility + return context + + +class FacilityOpeningHoursCreateView( + FacilityMixin, OrgaTeamPermissionMixin, CreateView +): + model = FacilityOpeningHours + template_name = "facility_opening_hours_form.html" + fields = ["when", "notes"] + + def form_valid(self, form): + """ + Set facility before saving + """ + hours = form.save(commit=False) + hours.facility = self.facility + hours.save() + messages.success(self.request, "New opening hours created successfully!") + return redirect( + reverse( + "backoffice:facility_detail", + kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk}, + ) + ) + + +class FacilityOpeningHoursUpdateView( + FacilityMixin, OrgaTeamPermissionMixin, UpdateView +): + model = FacilityOpeningHours + template_name = "facility_opening_hours_form.html" + fields = ["when", "notes"] + + def get_success_url(self): + messages.success(self.request, "Opening hours have been updated successfully") + return reverse( + "backoffice:facility_detail", + kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk}, + ) + + +class FacilityOpeningHoursDeleteView( + FacilityMixin, OrgaTeamPermissionMixin, DeleteView +): + model = FacilityOpeningHours + template_name = "facility_opening_hours_delete.html" + + def get_success_url(self): + messages.success(self.request, "Opening hours have been deleted successfully") + return reverse( + "backoffice:facility_detail", + kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk}, + ) + + diff --git a/src/backoffice/views/game.py b/src/backoffice/views/game.py new file mode 100644 index 00000000..08a7ed8d --- /dev/null +++ b/src/backoffice/views/game.py @@ -0,0 +1,106 @@ +import logging + +from camps.mixins import CampViewMixin +from django.contrib import messages +from django.contrib.auth.models import User +from django.db.models import Count, Q +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, DeleteView, UpdateView +from tokens.models import Token, TokenFind + +from ..mixins import ( + RaisePermissionRequiredMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +################################ +# Secret Token views + + +class TokenListView(CampViewMixin, RaisePermissionRequiredMixin, ListView): + """Show a list of secret tokens for this camp""" + + permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] + model = Token + template_name = "token_list.html" + + +class TokenDetailView(CampViewMixin, RaisePermissionRequiredMixin, DetailView): + """Show details for a token.""" + + permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] + model = Token + template_name = "token_detail.html" + + +class TokenCreateView(CampViewMixin, RaisePermissionRequiredMixin, CreateView): + """Create a new Token.""" + + permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] + model = Token + template_name = "token_form.html" + fields = ["token", "category", "description"] + + def form_valid(self, form): + token = form.save(commit=False) + token.camp = self.camp + token.save() + return redirect( + reverse( + "backoffice:token_detail", + kwargs={"camp_slug": self.camp.slug, "pk": token.id}, + ) + ) + + +class TokenUpdateView(CampViewMixin, RaisePermissionRequiredMixin, UpdateView): + """Update a token.""" + + permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] + model = Token + template_name = "token_form.html" + fields = ["token", "category", "description"] + + +class TokenDeleteView(CampViewMixin, RaisePermissionRequiredMixin, DeleteView): + permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] + model = Token + template_name = "token_delete.html" + + def delete(self, *args, **kwargs): + self.get_object().tokenfind_set.all().delete() + return super().delete(*args, **kwargs) + + def get_success_url(self): + messages.success( + self.request, "The Token and all related TokenFinds has been deleted" + ) + return reverse("backoffice:token_list", kwargs={"camp_slug": self.camp.slug}) + + +class TokenStatsView(CampViewMixin, RaisePermissionRequiredMixin, ListView): + """Show stats for token finds for this camp""" + + permission_required = ["camps.backoffice_permission", "camps.gameteam_permission"] + model = User + template_name = "token_stats.html" + + def get_queryset(self, **kwargs): + tokenusers = ( + TokenFind.objects.filter(token__camp=self.camp) + .distinct("user") + .values_list("user", flat=True) + ) + return ( + User.objects.filter(id__in=tokenusers) + .annotate( + token_find_count=Count( + "token_finds", filter=Q(token_finds__token__camp=self.camp) + ) + ) + .exclude(token_find_count=0) + ) diff --git a/src/backoffice/views/infodesk.py b/src/backoffice/views/infodesk.py new file mode 100644 index 00000000..0dfbb8e2 --- /dev/null +++ b/src/backoffice/views/infodesk.py @@ -0,0 +1,150 @@ +import logging +from itertools import chain + +from camps.mixins import CampViewMixin +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import ListView, TemplateView +from shop.models import Order, OrderProductRelation +from tickets.models import DiscountTicket, ShopTicket, SponsorTicket, TicketType + +from ..mixins import ( + InfoTeamPermissionMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView): + template_name = "product_handout.html" + + def get_queryset(self, **kwargs): + return OrderProductRelation.objects.filter( + ticket_generated=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + ).order_by("order") + + +class BadgeHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView): + template_name = "badge_handout.html" + context_object_name = "tickets" + + def get_queryset(self, **kwargs): + shoptickets = ShopTicket.objects.filter(badge_ticket_generated=False) + sponsortickets = SponsorTicket.objects.filter(badge_ticket_generated=False) + discounttickets = DiscountTicket.objects.filter(badge_ticket_generated=False) + return list(chain(shoptickets, sponsortickets, discounttickets)) + + +class TicketCheckinView(CampViewMixin, InfoTeamPermissionMixin, ListView): + template_name = "ticket_checkin.html" + context_object_name = "tickets" + + def get_queryset(self, **kwargs): + shoptickets = ShopTicket.objects.filter(used=False) + sponsortickets = SponsorTicket.objects.filter(used=False) + discounttickets = DiscountTicket.objects.filter(used=False) + return list(chain(shoptickets, sponsortickets, discounttickets)) + + +def _ticket_getter_by_token(token): + for ticket_class in [ShopTicket, SponsorTicket, DiscountTicket]: + try: + return ticket_class.objects.get(token=token), False + except ticket_class.DoesNotExist: + try: + return ticket_class.objects.get(badge_token=token), True + except ticket_class.DoesNotExist: + pass + + +def _ticket_getter_by_pk(pk): + for ticket_class in [ShopTicket, SponsorTicket, DiscountTicket]: + try: + return ticket_class.objects.get(pk=pk) + except ticket_class.DoesNotExist: + pass + + +class ScanTicketsView( + LoginRequiredMixin, InfoTeamPermissionMixin, CampViewMixin, TemplateView +): + template_name = "info_desk/scan.html" + + ticket = None + order = None + order_search = False + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + if self.ticket: + context["ticket"] = self.ticket + + elif "ticket_token" in self.request.POST: + + # Slice to get rid of the first character which is a '#' + ticket_token = self.request.POST.get("ticket_token")[1:] + + ticket, is_badge = _ticket_getter_by_token(ticket_token) + + if ticket: + context["ticket"] = ticket + context["is_badge"] = is_badge + else: + messages.warning(self.request, "Ticket not found!") + + elif self.order_search: + context["order"] = self.order + + return context + + def post(self, request, **kwargs): + if "check_in_ticket_id" in request.POST: + self.ticket = self.check_in_ticket(request) + elif "badge_ticket_id" in request.POST: + self.ticket = self.hand_out_badge(request) + elif "find_order_id" in request.POST: + self.order_search = True + try: + order_id = self.request.POST.get("find_order_id") + self.order = Order.objects.get(id=order_id) + except Order.DoesNotExist: + pass + elif "mark_as_paid" in request.POST: + self.mark_order_as_paid(request) + + return super().get(request, **kwargs) + + def check_in_ticket(self, request): + check_in_ticket_id = request.POST.get("check_in_ticket_id") + ticket_to_check_in = _ticket_getter_by_pk(check_in_ticket_id) + ticket_to_check_in.used = True + ticket_to_check_in.save() + messages.info(request, "Ticket checked-in!") + return ticket_to_check_in + + def hand_out_badge(self, request): + badge_ticket_id = request.POST.get("badge_ticket_id") + ticket_to_handout_badge_for = _ticket_getter_by_pk(badge_ticket_id) + ticket_to_handout_badge_for.badge_handed_out = True + ticket_to_handout_badge_for.save() + messages.info(request, "Badge marked as handed out!") + return ticket_to_handout_badge_for + + def mark_order_as_paid(self, request): + order = Order.objects.get(id=request.POST.get("mark_as_paid")) + order.mark_as_paid() + messages.success(request, "Order #{} has been marked as paid!".format(order.id)) + + +class ShopTicketOverview(LoginRequiredMixin, InfoTeamPermissionMixin, CampViewMixin, ListView): + model = ShopTicket + template_name = "shop_ticket_overview.html" + context_object_name = "shop_tickets" + + def get_context_data(self, *, object_list=None, **kwargs): + kwargs["ticket_types"] = TicketType.objects.filter(camp=self.camp) + return super().get_context_data(object_list=object_list, **kwargs) diff --git a/src/backoffice/views/orga.py b/src/backoffice/views/orga.py new file mode 100644 index 00000000..2be27180 --- /dev/null +++ b/src/backoffice/views/orga.py @@ -0,0 +1,175 @@ +import logging + +from camps.mixins import CampViewMixin +from django.contrib import messages +from django.forms import modelformset_factory +from django.shortcuts import redirect +from django.urls import reverse +from django.utils import timezone +from django.views.generic import ListView, TemplateView +from django.views.generic.edit import FormView +from profiles.models import Profile +from shop.models import OrderProductRelation +from utils.models import OutgoingEmail + +from ..mixins import ( + OrgaTeamPermissionMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class ApproveNamesView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + template_name = "approve_public_credit_names.html" + context_object_name = "profiles" + + def get_queryset(self, **kwargs): + return Profile.objects.filter(public_credit_name_approved=False).exclude( + public_credit_name="" + ) + +################################ +# MERCHANDISE VIEWS + + +class MerchandiseOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + template_name = "orders_merchandise.html" + + def get_queryset(self, **kwargs): + camp_prefix = "BornHack {}".format(timezone.now().year) + + return ( + OrderProductRelation.objects.filter( + order__refunded=False, + order__cancelled=False, + product__category__name="Merchandise", + ) + .filter(product__name__startswith=camp_prefix) + .order_by("order") + ) + + +class MerchandiseToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView): + template_name = "merchandise_to_order.html" + + def get_context_data(self, **kwargs): + camp_prefix = "BornHack {}".format(timezone.now().year) + + order_relations = OrderProductRelation.objects.filter( + order__refunded=False, + order__cancelled=False, + product__category__name="Merchandise", + ).filter(product__name__startswith=camp_prefix) + + merchandise_orders = {} + for relation in order_relations: + try: + quantity = merchandise_orders[relation.product.name] + relation.quantity + merchandise_orders[relation.product.name] = quantity + except KeyError: + merchandise_orders[relation.product.name] = relation.quantity + + context = super().get_context_data(**kwargs) + context["merchandise"] = merchandise_orders + return context + + +################################ +# VILLAGE VIEWS + + +class VillageOrdersView(CampViewMixin, OrgaTeamPermissionMixin, ListView): + template_name = "orders_village.html" + + def get_queryset(self, **kwargs): + camp_prefix = "BornHack {}".format(timezone.now().year) + + return ( + OrderProductRelation.objects.filter( + ticket_generated=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + product__category__name="Villages", + ) + .filter(product__name__startswith=camp_prefix) + .order_by("order") + ) + + +class VillageToOrderView(CampViewMixin, OrgaTeamPermissionMixin, TemplateView): + template_name = "village_to_order.html" + + def get_context_data(self, **kwargs): + camp_prefix = "BornHack {}".format(timezone.now().year) + + order_relations = OrderProductRelation.objects.filter( + ticket_generated=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + product__category__name="Villages", + ).filter(product__name__startswith=camp_prefix) + + village_orders = {} + for relation in order_relations: + try: + quantity = village_orders[relation.product.name] + relation.quantity + village_orders[relation.product.name] = quantity + except KeyError: + village_orders[relation.product.name] = relation.quantity + + context = super().get_context_data(**kwargs) + context["village"] = village_orders + return context + + +######################################### +# UPDATE AND RELEASE HELD OUTGOING EMAILS + + +class OutgoingEmailMassUpdateView(CampViewMixin, OrgaTeamPermissionMixin, FormView): + """ + This view shows a list with forms to edit OutgoingEmail objects with hold=True + """ + + template_name = "outgoing_email_mass_update.html" + + def setup(self, *args, **kwargs): + """Get emails with no team and emails with a team for the current camp.""" + super().setup(*args, **kwargs) + self.queryset = OutgoingEmail.objects.filter( + hold=True, responsible_team__isnull=True + ).prefetch_related("responsible_team") | OutgoingEmail.objects.filter( + hold=True, responsible_team__camp=self.camp + ).prefetch_related( + "responsible_team" + ) + self.form_class = modelformset_factory( + OutgoingEmail, + fields=["subject", "text_template", "html_template", "hold"], + min_num=self.queryset.count(), + validate_min=True, + max_num=self.queryset.count(), + validate_max=True, + extra=0, + ) + + def get_context_data(self, *args, **kwargs): + """Include the formset in the context.""" + context = super().get_context_data(*args, **kwargs) + context["formset"] = self.form_class(queryset=self.queryset) + return context + + def form_valid(self, form): + """Show a message saying how many objects were updated.""" + form.save() + if form.changed_objects: + messages.success( + self.request, f"Updated {len(form.changed_objects)} OutgoingEmails" + ) + return redirect(self.get_success_url()) + + def get_success_url(self, *args, **kwargs): + """Return to the backoffice index.""" + return reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug}) diff --git a/src/backoffice/views/pos.py b/src/backoffice/views/pos.py new file mode 100644 index 00000000..bdbfab8a --- /dev/null +++ b/src/backoffice/views/pos.py @@ -0,0 +1,240 @@ +import logging + +from camps.mixins import CampViewMixin +from django.contrib import messages +from django.core.exceptions import PermissionDenied +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, DeleteView, UpdateView +from economy.models import ( + Pos, + PosReport, +) +from teams.models import Team + +from ..mixins import ( + OrgaTeamPermissionMixin, + PosViewMixin, + RaisePermissionRequiredMixin, +) + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class PosListView(CampViewMixin, RaisePermissionRequiredMixin, ListView): + """Show a list of Pos this user has access to (through team memberships).""" + + permission_required = "camps.backoffice_permission" + model = Pos + template_name = "pos_list.html" + + +class PosDetailView(PosViewMixin, DetailView): + """Show details for a Pos.""" + + model = Pos + template_name = "pos_detail.html" + slug_url_kwarg = "pos_slug" + + +class PosCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView): + """Create a new Pos (orga only).""" + + model = Pos + template_name = "pos_form.html" + fields = ["name", "team"] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"].fields["team"].queryset = Team.objects.filter(camp=self.camp) + return context + + +class PosUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView): + """Update a Pos.""" + + model = Pos + template_name = "pos_form.html" + slug_url_kwarg = "pos_slug" + fields = ["name", "team"] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"].fields["team"].queryset = Team.objects.filter(camp=self.camp) + return context + + +class PosDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView): + model = Pos + template_name = "pos_delete.html" + slug_url_kwarg = "pos_slug" + + def delete(self, *args, **kwargs): + self.get_object().pos_reports.all().delete() + return super().delete(*args, **kwargs) + + def get_success_url(self): + messages.success( + self.request, "The Pos and all related PosReports has been deleted" + ) + return reverse("backoffice:pos_list", kwargs={"camp_slug": self.camp.slug}) + + +class PosReportCreateView(PosViewMixin, CreateView): + """Use this view to create new PosReports.""" + + model = PosReport + fields = ["date", "bank_responsible", "pos_responsible", "comments"] + template_name = "posreport_form.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"].fields["bank_responsible"].queryset = Team.objects.get( + camp=self.camp, + name="Orga", + ).approved_members.all() + context["form"].fields[ + "pos_responsible" + ].queryset = self.pos.team.responsible_members.all() + return context + + def form_valid(self, form): + """ + Set Pos before saving + """ + pr = form.save(commit=False) + pr.pos = self.pos + pr.save() + messages.success(self.request, "New PosReport created successfully!") + return redirect( + reverse( + "backoffice:posreport_detail", + kwargs={ + "camp_slug": self.camp.slug, + "pos_slug": self.pos.slug, + "posreport_uuid": pr.uuid, + }, + ) + ) + + +class PosReportUpdateView(PosViewMixin, UpdateView): + """Use this view to update PosReports.""" + + model = PosReport + fields = [ + "date", + "bank_responsible", + "pos_responsible", + "hax_sold_izettle", + "hax_sold_website", + "dkk_sales_izettle", + "comments", + ] + template_name = "posreport_form.html" + pk_url_kwarg = "posreport_uuid" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"].fields["bank_responsible"].queryset = Team.objects.get( + camp=self.camp, + name="Orga", + ).approved_members.all() + context["form"].fields[ + "pos_responsible" + ].queryset = self.pos.team.responsible_members.all() + return context + + +class PosReportDetailView(PosViewMixin, DetailView): + """Show details for a PosReport.""" + + model = PosReport + template_name = "posreport_detail.html" + pk_url_kwarg = "posreport_uuid" + + +class PosReportBankCountStartView(PosViewMixin, UpdateView): + """The bank responsible for a PosReport uses this view to add day-start HAX and DKK counts to a PosReport.""" + + model = PosReport + template_name = "posreport_form.html" + fields = [ + "bank_count_dkk_start", + "bank_count_hax5_start", + "bank_count_hax10_start", + "bank_count_hax20_start", + "bank_count_hax50_start", + "bank_count_hax100_start", + ] + pk_url_kwarg = "posreport_uuid" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + if self.request.user != self.get_object().bank_responsible: + raise PermissionDenied("Only the bank responsible can do this") + + +class PosReportBankCountEndView(PosViewMixin, UpdateView): + """The bank responsible for a PosReport uses this view to add day-end HAX and DKK counts to a PosReport.""" + + model = PosReport + template_name = "posreport_form.html" + fields = [ + "bank_count_dkk_end", + "bank_count_hax5_end", + "bank_count_hax10_end", + "bank_count_hax20_end", + "bank_count_hax50_end", + "bank_count_hax100_end", + ] + pk_url_kwarg = "posreport_uuid" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + if self.request.user != self.get_object().bank_responsible: + raise PermissionDenied("Only the bank responsible can do this") + + +class PosReportPosCountStartView(PosViewMixin, UpdateView): + """The Pos responsible for a PosReport uses this view to add day-start HAX and DKK counts to a PosReport.""" + + model = PosReport + template_name = "posreport_form.html" + fields = [ + "pos_count_dkk_start", + "pos_count_hax5_start", + "pos_count_hax10_start", + "pos_count_hax20_start", + "pos_count_hax50_start", + "pos_count_hax100_start", + ] + pk_url_kwarg = "posreport_uuid" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + if self.request.user != self.get_object().pos_responsible: + raise PermissionDenied("Only the Pos responsible can do this") + + +class PosReportPosCountEndView(PosViewMixin, UpdateView): + """The Pos responsible for a PosReport uses this view to add day-end HAX and DKK counts to a PosReport.""" + + model = PosReport + template_name = "posreport_form.html" + fields = [ + "pos_count_dkk_end", + "pos_count_hax5_end", + "pos_count_hax10_end", + "pos_count_hax20_end", + "pos_count_hax50_end", + "pos_count_hax100_end", + "pos_json", + ] + pk_url_kwarg = "posreport_uuid" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + if self.request.user != self.get_object().pos_responsible: + raise PermissionDenied("Only the pos responsible can do this") diff --git a/src/backoffice/views/program.py b/src/backoffice/views/program.py new file mode 100644 index 00000000..2285d4b9 --- /dev/null +++ b/src/backoffice/views/program.py @@ -0,0 +1,934 @@ +import logging + +from camps.mixins import CampViewMixin +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 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