|
|
@ -1,4 +1,7 @@
|
|
|
|
from shiftregister.importer.models import *
|
|
|
|
from shiftregister.importer.models import *
|
|
|
|
|
|
|
|
from django.db.models import Max, Sum
|
|
|
|
|
|
|
|
from django.db.models import Count, Exists, OuterRef, Subquery, Func
|
|
|
|
|
|
|
|
import math
|
|
|
|
|
|
|
|
|
|
|
|
night_shift_query = Q(start_at__hour__gte=21) | Q(start_at__hour__lte=10)
|
|
|
|
night_shift_query = Q(start_at__hour__gte=21) | Q(start_at__hour__lte=10)
|
|
|
|
|
|
|
|
|
|
|
@ -8,24 +11,104 @@ class TeamMember(models.Model):
|
|
|
|
fallback_shifts = models.ManyToManyField(Shift, through="FallbackAssignment")
|
|
|
|
fallback_shifts = models.ManyToManyField(Shift, through="FallbackAssignment")
|
|
|
|
|
|
|
|
|
|
|
|
def assign_random_shifts(self):
|
|
|
|
def assign_random_shifts(self):
|
|
|
|
events_bucket_1 = Event.objects.filter(
|
|
|
|
if self.fallback_shifts.count() != 0:
|
|
|
|
~night_shift_query, deleted=False, calendar__needs_fallback=True
|
|
|
|
return
|
|
|
|
|
|
|
|
selector1 = (~night_shift_query) & Q(
|
|
|
|
|
|
|
|
deleted=False, calendar__needs_fallback=True
|
|
|
|
)
|
|
|
|
)
|
|
|
|
events_bucket_2 = Event.objects.filter(
|
|
|
|
selector2 = night_shift_query & Q(deleted=False, calendar__needs_fallback=True)
|
|
|
|
night_shift_query, deleted=False, calendar__needs_fallback=True
|
|
|
|
self._assign_from_bucket(selector1)
|
|
|
|
|
|
|
|
self._assign_from_bucket(selector2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assign_from_bucket(self, bucket_selector):
|
|
|
|
|
|
|
|
free_bucket = (
|
|
|
|
|
|
|
|
Event.objects.filter(bucket_selector)
|
|
|
|
|
|
|
|
.annotate(
|
|
|
|
|
|
|
|
fallback_count=Count("fallbackassignment"),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.filter(fallback_count__lt=F("required_helpers"))
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
total_slot_count = Event.objects.filter(bucket_selector).aggregate(
|
|
|
|
|
|
|
|
sum=Sum("required_helpers")
|
|
|
|
|
|
|
|
)["sum"]
|
|
|
|
|
|
|
|
free_slot_count = free_bucket.annotate(
|
|
|
|
|
|
|
|
needed_helpers=F("required_helpers") - F("fallback_count")
|
|
|
|
|
|
|
|
).aggregate(sum=Sum("needed_helpers"))["sum"]
|
|
|
|
|
|
|
|
active_team_members = (
|
|
|
|
|
|
|
|
TeamMember.objects.filter(~Q(fallback_shifts=None)).count() + 1
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self._assign_from_bucket(events_bucket_1)
|
|
|
|
|
|
|
|
self._assign_from_bucket(events_bucket_2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assign_from_bucket(self, bucket):
|
|
|
|
|
|
|
|
quota = global_preferences["helper__fallback_quota"]
|
|
|
|
quota = global_preferences["helper__fallback_quota"]
|
|
|
|
number_of_team_members = TeamMember.objects.count()
|
|
|
|
number_of_team_members = TeamMember.objects.count()
|
|
|
|
max_shifts_per_member = bucket.count() / (number_of_team_members * quota)
|
|
|
|
max_shifts_per_member = math.ceil(
|
|
|
|
|
|
|
|
total_slot_count / max((number_of_team_members * quota), 1)
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
shifts_per_member = math.ceil(total_slot_count / (active_team_members))
|
|
|
|
|
|
|
|
shift_count = min(max_shifts_per_member, shifts_per_member)
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
|
|
f"total:{total_slot_count} max:{max_shifts_per_member} calc:{shifts_per_member} chosen:{shift_count} calc:{active_team_members*shift_count} free `before:{free_slot_count} {self.name}"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
# easy part: enough free shifts for everyone:
|
|
|
|
|
|
|
|
shifts = free_bucket.order_by("?")[:shift_count]
|
|
|
|
|
|
|
|
self.fallback_shifts.set(shifts)
|
|
|
|
|
|
|
|
# there is a chance that even if qota*teammembers team members are activatet, there are still unasigned shifts
|
|
|
|
|
|
|
|
# this happens if there are shifts with multiple people left, as we can not assign multiple slots for
|
|
|
|
|
|
|
|
# the same shift to one member.
|
|
|
|
|
|
|
|
# for now we will just reduce the quota a bit to calculate for these cases.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# if len(shifts) >= (shift_count - 1):
|
|
|
|
|
|
|
|
# return
|
|
|
|
|
|
|
|
shifts_needed = shift_count - len(shifts)
|
|
|
|
|
|
|
|
# this is not done very often so we can do this kinda inefficient but readable and maintainable:
|
|
|
|
|
|
|
|
# for each missing shift, get the team member who has the most shifts in our bucket and take one of them at random
|
|
|
|
|
|
|
|
# but also take care to not take any slots for shifts we already have...
|
|
|
|
|
|
|
|
|
|
|
|
assigned_shifts = self.fallback_shifts.filter(night_shift_query)
|
|
|
|
while shifts_needed > 0:
|
|
|
|
|
|
|
|
# this is a bit more complex and uses subqueries and id lists because we want to reuse the query selector for events.
|
|
|
|
|
|
|
|
# maybe there is a good way to transform q-expressions to add prefixes to fields but for now this has to work
|
|
|
|
|
|
|
|
canidate_shift_ids = Event.objects.filter(bucket_selector).values(
|
|
|
|
|
|
|
|
"shift_ptr_id"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
relevant_assignments = FallbackAssignment.objects.filter(
|
|
|
|
|
|
|
|
shift_id__in=canidate_shift_ids
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# for events in events:
|
|
|
|
# get teammembers sorted by the most shifts in the relevant bucket
|
|
|
|
# self.fallback_shifts.add(events)
|
|
|
|
sorted_members = (
|
|
|
|
|
|
|
|
TeamMember.objects.annotate(
|
|
|
|
|
|
|
|
relevant_fallback_count=Count(
|
|
|
|
|
|
|
|
"fallback_shifts",
|
|
|
|
|
|
|
|
distinct=True,
|
|
|
|
|
|
|
|
filter=Q(fallback_shifts__id__in=canidate_shift_ids),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.exclude(pk=self.pk)
|
|
|
|
|
|
|
|
.order_by("-relevant_fallback_count", "?")
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
assignment = False
|
|
|
|
|
|
|
|
for member in sorted_members:
|
|
|
|
|
|
|
|
# now get all their assignments in the relevant bucket but exclude the ones where we already have a slot in the same shift...
|
|
|
|
|
|
|
|
assignment = (
|
|
|
|
|
|
|
|
FallbackAssignment.objects.filter(
|
|
|
|
|
|
|
|
team_member_id=member.id, shift_id__in=canidate_shift_ids
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.exclude(
|
|
|
|
|
|
|
|
shift_id__in=FallbackAssignment.objects.filter(
|
|
|
|
|
|
|
|
team_member_id=self.pk
|
|
|
|
|
|
|
|
).values("shift_id")
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.order_by("?")
|
|
|
|
|
|
|
|
.first()
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if assignment:
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
if not assignment:
|
|
|
|
|
|
|
|
print("could not find any matching assignments to take away")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
shifts_needed -= 1
|
|
|
|
|
|
|
|
self.fallback_shifts.add(assignment.shift)
|
|
|
|
|
|
|
|
assignment.delete()
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
def __str__(self):
|
|
|
|
return f"{self.name}"
|
|
|
|
return f"{self.name}"
|
|
|
|