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