first assignment prototype

pull/1/head
parent 332fb0b4cd
commit d1e06d00ec

@ -7,13 +7,29 @@ def assign_random_shifts(modeladmin, request, queryset):
member.assign_random_shifts() member.assign_random_shifts()
def clear_shifts(modeladmin, request, queryset):
for member in queryset:
member.fallback_shifts.clear()
def reshuffle_shifts(modeladmin, request, queryset):
for member in queryset:
member.fallback_shifts.clear()
for member in queryset:
member.assign_random_shifts()
class FallbackAssignmentInline(admin.TabularInline): class FallbackAssignmentInline(admin.TabularInline):
model = FallbackAssignment model = FallbackAssignment
ordering = ("shift__start_at",)
@admin.register(TeamMember) @admin.register(TeamMember)
class TeamMemberAdmin(admin.ModelAdmin): class TeamMemberAdmin(admin.ModelAdmin):
fields = ["name"] fields = ["name"]
list_display = ["name"] list_display = ["name", "shift_count"]
inlines = (FallbackAssignmentInline,) inlines = (FallbackAssignmentInline,)
actions = (assign_random_shifts,) actions = (assign_random_shifts, clear_shifts, reshuffle_shifts)
def shift_count(self, object):
return object.fallback_shifts.count()

@ -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}"

@ -4,4 +4,4 @@ from .models import Calendar
@admin.register(Calendar) @admin.register(Calendar)
class CalendarAdmin(admin.ModelAdmin): class CalendarAdmin(admin.ModelAdmin):
list_display = ("url", "has_errors") list_display = ("url", "needs_fallback", "has_errors")

Loading…
Cancel
Save