shiftregister/shiftregister/app/models.py

229 lines
7.4 KiB
Python

import secrets
from datetime import timedelta
from django.db import models
from django.db.models import Case, Count, ExpressionWrapper, F, Q, When
from django.shortcuts import reverse
from django.template import Context, Template
from django.utils import timezone
from dynamic_preferences.registries import global_preferences_registry
from phonenumber_field.modelfields import PhoneNumberField
from shiftregister.messaging import message
global_preferences = global_preferences_registry.manager()
class Room(models.Model):
name = models.CharField(max_length=200, primary_key=True)
required_helpers = models.IntegerField()
meeting_location = models.TextField(default="Infopoint")
description = models.TextField(blank=True, default="")
send_reminders = models.BooleanField(default=True)
def __str__(self):
return self.name
def valid_shifts(self):
return self.shift_set.filter(deleted=False)
class Shift(models.Model):
room = models.ForeignKey(Room, on_delete=models.RESTRICT)
start_at = models.DateTimeField(db_index=True)
duration = models.DurationField()
required_helpers = models.IntegerField(
default=0, help_text="When this is set to zero, the room value is used instead."
)
description = models.TextField(blank=True, default="")
deleted = models.BooleanField(default=False, db_index=True)
def with_reg_count():
return Shift.objects.annotate(
reg_count=Count(
"shiftregistration",
distinct=True,
filter=Q(
shiftregistration__state__in=(
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
)
),
)
).select_related("room")
def __str__(self):
return f"{self.room.name}: {self.start_at}"
def has_ended(self):
return (self.start_at + self.duration) < timezone.now()
def is_running(self):
return (self.start_at <= timezone.now()) and (not self.has_ended())
def registration_count(self):
return (
self.reg_count
if hasattr(self, "reg_count")
else self.shiftregistration_set.filter(
state__in=[
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
]
).count()
)
def valid_registrations(self):
return self.shiftregistration_set.filter(
state__in=[
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
],
)
class Helper(models.Model):
phone = PhoneNumberField(unique=True, editable=False)
name = models.CharField(max_length=200)
# change this to a generic state variable to allow for number blocking/account deactivation?
number_validated = models.BooleanField(default=False)
asta_confirmed = models.BooleanField(default=False)
def __str__(self):
return self.name
def send_confirmation(self):
(token, created) = LoginToken.objects.get_or_create(helper=self)
token.send()
return token
# current or next shift
def important_shift(self):
ret = (
ShiftRegistration.objects.annotate(
shift_end=ExpressionWrapper(
F("shift__start_at") + F("shift__duration"),
output_field=models.DateTimeField(),
)
)
.filter(
helper=self,
shift_end__gte=timezone.now(),
shift__deleted=False,
state__in=[
ShiftRegistration.RegState.REGISTERED,
ShiftRegistration.RegState.CHECKED_IN,
],
)
.order_by("shift__start_at")
.first()
)
if ret:
return ret.shift
class ShiftRegistration(models.Model):
class Meta:
unique_together = (("shift", "helper"),)
# use restrict for now as Model.delete is not called
shift = models.ForeignKey(Shift, on_delete=models.RESTRICT)
helper = models.ForeignKey(Helper, on_delete=models.CASCADE)
reminder_sent = models.BooleanField(default=False)
class RegState(models.TextChoices):
# default is registered
REGISTERED = "REG", "registriert"
CHECKED_IN = "CHECK", "checkin"
# cancel via infopoint
CANCELED = "CANCEL", "abgemeldet"
# did not attend shift
FAILED = "FAIL", "nicht angetreten"
state = models.CharField(
max_length=7,
choices=RegState.choices,
default=RegState.REGISTERED,
)
def can_cancel(self):
if self.state != self.RegState.REGISTERED:
return False
return self.shift.start_at > (
timezone.now()
+ global_preferences_registry.manager()["helper__min_cancel_time"]
)
def send_reminder(self):
url = reverse("shift", kwargs={"shiftid": self.shift.pk})
template = Template(
'Deine Schicht beginnt um {{ start_at|date:"H:i" }}, bitte komm 15 Minuten vorher an den Infopoint https://helfen.kntkt.de{{ url }} Du kommst ab jetzt ohne Anstehen aufs Gelände'
)
text = template.render(Context({"start_at": self.shift.start_at, "url": url}))
msg = Message(to=self.helper, text=text)
msg.save()
self.reminder_sent = True
self.save()
def __str__(self):
return f"{self.helper.name}: {self.shift}"
def is_pending(self):
return self.state == self.RegState.REGISTERED
def is_checked_in(self):
return self.state == self.RegState.CHECKED_IN
def is_withdrawn(self):
return self.state in (self.RegState.CANCELED, self.RegState.FAILED)
class Message(models.Model):
# remove limit and send long messages in multiple messages?
text = models.CharField(max_length=160)
to = models.ForeignKey(Helper, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
sent_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return f"{self.to.name}({self.created_at}): {self.text}"
def as_outbound(self):
return message.Message(
self.pk,
recipient=self.to.phone,
text=self.text,
type=message.MessageType.OUTBOUND,
created_at=self.created_at,
)
def gen_token():
return secrets.token_urlsafe(
15
) # returns 15 bytes Base64-encoded (times 1.333...) = 20 characters
class LoginToken(models.Model):
id = models.CharField(
max_length=20, primary_key=True, default=gen_token, editable=False
)
helper = models.ForeignKey(Helper, on_delete=models.CASCADE)
sent_at = models.DateTimeField(auto_now_add=True)
send_count = models.IntegerField(default=0)
def send(self):
text = f"Dein Registrierungslink zum Helfer*innensystem: https://helfen.kntkt.de{self.get_absolute_url()}\nWenn du dich nicht registriert hast, ignoriere diese SMS."
msg = Message(to=self.helper, text=text)
msg.save()
self.sent_at = timezone.now()
self.send_count += 1
self.save()
# import here to break import cycle
from .tasks import send_message
send_message.delay(msg.pk)
def get_absolute_url(self):
return reverse("token_login", kwargs={"token": self.id})