split backoffice views.py into multiple files, add autoflake to pre-commit

This commit is contained in:
Thomas Steen Rasmussen 2021-07-19 15:00:59 +02:00
parent f0f5a609b6
commit a21bc1097c
12 changed files with 2605 additions and 2467 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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}
)

View file

@ -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})
)

View file

@ -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},
)

View file

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

View file

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

View file

@ -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})

240
src/backoffice/views/pos.py Normal file
View file

@ -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")

View file

@ -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:<br>"
for v in violations:
message += v + "<br>"
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