412 lines
12 KiB
Python
412 lines
12 KiB
Python
from datetime import timedelta
|
|
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.core.paginator import Paginator
|
|
from django.db import models, transaction
|
|
from django.db.models import Case, Count, ExpressionWrapper, F, Q, When
|
|
from django.db.models.fields import DateTimeField
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.views.generic import DetailView, ListView
|
|
from django.views.generic.edit import FormMixin
|
|
|
|
from .forms import BulkMessage, HelperMessage, HelperShift
|
|
from .models import (
|
|
Helper,
|
|
IncomingMessage,
|
|
Message,
|
|
Room,
|
|
RoomViewToken,
|
|
Shift,
|
|
ShiftRegistration,
|
|
)
|
|
|
|
# Create your views here.
|
|
|
|
|
|
def index(request):
|
|
return redirect("team:shift_overview")
|
|
|
|
|
|
@login_required
|
|
def shift_overview(request):
|
|
checkin_count = Count(
|
|
Case(
|
|
When(
|
|
shiftregistration__state=ShiftRegistration.RegState.CHECKED_IN, then=1
|
|
),
|
|
output_field=models.IntegerField(),
|
|
)
|
|
)
|
|
|
|
context = {}
|
|
context["running_shifts"] = (
|
|
Shift.with_reg_count()
|
|
.prefetch_related("event__calendar")
|
|
.annotate(
|
|
checkin_count=Count(
|
|
Case(
|
|
When(
|
|
shiftregistration__state=ShiftRegistration.RegState.CHECKED_IN,
|
|
then=1,
|
|
),
|
|
output_field=models.IntegerField(),
|
|
)
|
|
),
|
|
end_at=ExpressionWrapper(
|
|
F("start_at") + F("duration"), output_field=DateTimeField()
|
|
),
|
|
)
|
|
.filter(start_at__lte=timezone.now(), end_at__gte=timezone.now(), deleted=False)
|
|
.order_by("start_at")
|
|
)
|
|
|
|
context["next_shifts"] = (
|
|
Shift.with_reg_count()
|
|
.prefetch_related("event__calendar")
|
|
.annotate(checkin_count=checkin_count)
|
|
.filter(
|
|
start_at__gt=timezone.now(),
|
|
start_at__lte=timezone.now() + timedelta(minutes=30),
|
|
deleted=False,
|
|
)
|
|
.order_by("start_at")
|
|
)
|
|
|
|
# only Postgres supports DISTINCT on specific columns, SQLite does not support aggregates on datetime fields
|
|
context["next_shifts_per_room"] = filter(
|
|
lambda x: x is not None,
|
|
(
|
|
Shift.with_reg_count()
|
|
.prefetch_related("event__calendar")
|
|
.filter(room=room, start_at__gt=timezone.now(), deleted=False)
|
|
.order_by("start_at")
|
|
.first()
|
|
for room in Room.objects.all().order_by("name")
|
|
),
|
|
)
|
|
|
|
return render(request, "shift_overview.html", context)
|
|
|
|
|
|
def add_helper_shift(self):
|
|
pass
|
|
|
|
|
|
@login_required
|
|
def shift_detail(request, pk):
|
|
shift = get_object_or_404(
|
|
Shift.with_reg_count().prefetch_related("shiftregistration_set__helper"), pk=pk
|
|
)
|
|
form = HelperShift()
|
|
if request.method == "POST":
|
|
form = HelperShift(request.POST)
|
|
if form.is_valid():
|
|
(reg, created) = ShiftRegistration.objects.get_or_create(
|
|
helper=form.cleaned_data["helper"], shift=shift
|
|
)
|
|
if created:
|
|
messages.add_message(
|
|
request,
|
|
messages.SUCCESS,
|
|
"Helfer erfolgreich zur Schicht hinzugefügt",
|
|
)
|
|
else:
|
|
messages.add_message(
|
|
request,
|
|
messages.WARNING,
|
|
"Helfer ist bereits für diese Schicht angemeldet",
|
|
)
|
|
return redirect("team:shift", pk=shift.pk)
|
|
|
|
context = {
|
|
"shift": shift,
|
|
"add_helper_form": form,
|
|
}
|
|
return render(request, "shift_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
def bulk_message(request):
|
|
form = BulkMessage()
|
|
|
|
if request.method == "POST":
|
|
form = BulkMessage(request.POST)
|
|
if form.is_valid():
|
|
helpers = Helper.objects.filter(number_validated=True)
|
|
if form.cleaned_data["checked_in_only"]:
|
|
helpers = Helper.objects.annotate(
|
|
shift_count=Count(
|
|
Case(
|
|
When(
|
|
shiftregistration__state__in=[
|
|
ShiftRegistration.RegState.CHECKED_IN,
|
|
],
|
|
then=1,
|
|
),
|
|
output_field=models.IntegerField(),
|
|
)
|
|
)
|
|
).filter(number_validated=True, shift_count__gte=1)
|
|
|
|
try:
|
|
outbox = []
|
|
for helper in helpers:
|
|
text = form.cleaned_data["message"].replace(
|
|
"$token",
|
|
f"https://helfen.kntkt.de{helper.logintoken_set.first().get_absolute_url()}",
|
|
)
|
|
text = text.replace(
|
|
"$fb",
|
|
f"https://helfen.kntkt.de/f/{helper.logintoken_set.first().id}",
|
|
)
|
|
outbox.append(Message(text=text, to=helper))
|
|
|
|
Message.objects.bulk_create(outbox)
|
|
messages.add_message(
|
|
request, messages.SUCCESS, "Massen-Nachricht erfolgreich versendet"
|
|
)
|
|
except:
|
|
messages.add_message(
|
|
request,
|
|
messages.ERROR,
|
|
"Fehler beim Versenden der Massen-Nachricht",
|
|
)
|
|
|
|
context = {
|
|
"form": form,
|
|
}
|
|
return render(request, "bulk_message.html", context)
|
|
|
|
|
|
class HelperDetail(FormMixin, LoginRequiredMixin, DetailView):
|
|
template_name = "helper_detail.html"
|
|
model = Helper
|
|
form_class = HelperMessage
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context["history"] = (
|
|
IncomingMessage.objects.filter(sender=self.object.phone)
|
|
.annotate(incoming=models.Value(True))
|
|
.values_list("content", "created_at", "read", "incoming")
|
|
.union(
|
|
self.object.message_set.annotate(
|
|
read=models.Value(True), incoming=models.Value(False)
|
|
).values_list("text", "sent_at", "read", "incoming")
|
|
)
|
|
.order_by(F("created_at").asc(nulls_last=True))
|
|
)
|
|
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
|
|
form = self.get_form()
|
|
if form.is_valid():
|
|
IncomingMessage.objects.filter(sender=self.object.phone).update(read=True)
|
|
Message(
|
|
text=form.cleaned_data["message"].replace(
|
|
"$token",
|
|
f"https://helfen.kntkt.de{self.object.logintoken_set.first().get_absolute_url()}",
|
|
),
|
|
to=self.object,
|
|
).save()
|
|
|
|
return self.render_to_response(self.get_context_data(form=form))
|
|
|
|
|
|
class ShiftList(LoginRequiredMixin, ListView):
|
|
template_name = "shift_list.html"
|
|
model = Shift
|
|
title = "Alle Schichten"
|
|
|
|
def get_queryset(self):
|
|
return Shift.with_reg_count()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["title"] = self.title
|
|
return context
|
|
|
|
def get_ordering(self):
|
|
return ("start_at", "room__name")
|
|
|
|
|
|
class FreeShiftList(ShiftList):
|
|
title = "Freie Schichten"
|
|
|
|
def get_queryset(self):
|
|
help_wanted = Q(required_helpers__gt=F("reg_count")) | Q(
|
|
required_helpers=0
|
|
) & Q(room__required_helpers__gt=F("reg_count"))
|
|
return (
|
|
Shift.with_reg_count()
|
|
.annotate(
|
|
end_at=ExpressionWrapper(
|
|
F("start_at") + F("duration"),
|
|
output_field=DateTimeField(),
|
|
)
|
|
)
|
|
.filter(
|
|
help_wanted,
|
|
end_at__gte=timezone.now(),
|
|
deleted=False,
|
|
)
|
|
.order_by("start_at", "room__name")
|
|
)
|
|
|
|
|
|
class RoomShiftList(ShiftList):
|
|
def get_context_data(self, **kwargs):
|
|
room = get_object_or_404(Room, pk=self.kwargs["pk"])
|
|
context = super().get_context_data(**kwargs)
|
|
context["title"] = f"Schichten für {room.name}"
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
room = get_object_or_404(Room, pk=self.kwargs["pk"])
|
|
help_wanted = Q(required_helpers__gt=F("reg_count")) | Q(
|
|
required_helpers=0
|
|
) & Q(room__required_helpers__gt=F("reg_count"))
|
|
return (
|
|
Shift.with_reg_count()
|
|
.filter(
|
|
deleted=False,
|
|
room=room,
|
|
)
|
|
.order_by("start_at", "room__name")
|
|
)
|
|
|
|
|
|
class CheckinList(LoginRequiredMixin, ListView):
|
|
paginate_by = 30
|
|
template_name = "checkin_list.html"
|
|
title = "Ankommende Helfer*innen"
|
|
|
|
def get_queryset(self):
|
|
return (
|
|
ShiftRegistration.objects.select_related("helper", "shift")
|
|
.prefetch_related("shift__event__calendar")
|
|
.filter(shift__deleted=False, state=ShiftRegistration.RegState.REGISTERED)
|
|
.order_by("shift__start_at", "shift__room__name")
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["page_range"] = context["paginator"].get_elided_page_range(
|
|
context["page_obj"].number
|
|
)
|
|
context["title"] = self.title
|
|
return context
|
|
|
|
|
|
@login_required
|
|
def checkin(request, pk):
|
|
reg = get_object_or_404(ShiftRegistration, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
reg.state = reg.RegState.CHECKED_IN
|
|
reg.save()
|
|
|
|
return redirect("team:shift", pk=reg.shift.pk)
|
|
|
|
return render(
|
|
request,
|
|
"csrf_protect.html",
|
|
{"action": "Als angekommen markieren", "button_text": "Angekommen", "reg": reg},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def mark_as_failed(request, pk):
|
|
reg = get_object_or_404(ShiftRegistration, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
with transaction.atomic():
|
|
reg.state = reg.RegState.FAILED
|
|
reg.save()
|
|
|
|
# TODO: Mark helper as barred from further shift registrations (and delete pending existing ones)
|
|
|
|
return redirect("team:shift", pk=reg.shift.pk)
|
|
|
|
return render(
|
|
request,
|
|
"csrf_protect.html",
|
|
{
|
|
"action": 'Schicht als "nicht angetreten" markieren',
|
|
"button_text": "Nicht angetreten",
|
|
"reg": reg,
|
|
},
|
|
)
|
|
|
|
|
|
@login_required
|
|
def delete_shiftregistration(request, pk):
|
|
reg = get_object_or_404(ShiftRegistration, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
reg.delete()
|
|
|
|
return redirect("team:shift", pk=reg.shift.pk)
|
|
|
|
return render(
|
|
request,
|
|
"csrf_protect.html",
|
|
{"action": "Helfer*in abmelden", "button_text": "Abmelden", "reg": reg},
|
|
)
|
|
|
|
|
|
class IncomingMessagesList(LoginRequiredMixin, ListView):
|
|
model = IncomingMessage
|
|
paginate_by = 10
|
|
template_name = "incoming_messages.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["num_unread"] = IncomingMessage.objects.filter(read=False).count()
|
|
context["page_range"] = context["paginator"].get_elided_page_range(
|
|
context["page_obj"].number
|
|
)
|
|
return context
|
|
|
|
def get_ordering(self):
|
|
return ("read", "-created_at")
|
|
|
|
|
|
@login_required
|
|
def incoming_message(request, pk):
|
|
message = get_object_or_404(IncomingMessage, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
message.read = True
|
|
message.save()
|
|
|
|
return render(request, "incoming_message.html", {"message": message})
|
|
|
|
|
|
@login_required
|
|
def mark_as_read(request, pk):
|
|
helper = get_object_or_404(Helper, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
IncomingMessage.objects.filter(sender=helper.phone).update(read=True)
|
|
|
|
return redirect("team:helper", pk=pk)
|
|
|
|
|
|
def room_view_token(request, token):
|
|
token = get_object_or_404(RoomViewToken, pk=token)
|
|
room = token.room
|
|
|
|
return render(
|
|
request,
|
|
"room_registrations.html",
|
|
{"room": room, "shifts": room.valid_shifts().order_by("start_at")},
|
|
)
|