split backoffice views.py into multiple files, add autoflake to pre-commit
This commit is contained in:
parent
f0f5a609b6
commit
a21bc1097c
|
@ -12,3 +12,10 @@ repos:
|
||||||
rev: "v5.8.0"
|
rev: "v5.8.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: "isort"
|
- 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
10
src/backoffice/views/__init__.py
Normal file
10
src/backoffice/views/__init__.py
Normal 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
|
73
src/backoffice/views/backoffice.py
Normal file
73
src/backoffice/views/backoffice.py
Normal 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
|
134
src/backoffice/views/content.py
Normal file
134
src/backoffice/views/content.py
Normal 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}
|
||||||
|
)
|
452
src/backoffice/views/economy.py
Normal file
452
src/backoffice/views/economy.py
Normal 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})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
324
src/backoffice/views/facilities.py
Normal file
324
src/backoffice/views/facilities.py
Normal 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},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
106
src/backoffice/views/game.py
Normal file
106
src/backoffice/views/game.py
Normal 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)
|
||||||
|
)
|
150
src/backoffice/views/infodesk.py
Normal file
150
src/backoffice/views/infodesk.py
Normal 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)
|
175
src/backoffice/views/orga.py
Normal file
175
src/backoffice/views/orga.py
Normal 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
240
src/backoffice/views/pos.py
Normal 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")
|
934
src/backoffice/views/program.py
Normal file
934
src/backoffice/views/program.py
Normal 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
|
Loading…
Reference in a new issue