shiftregister/shiftregister/team/views.py

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