first assignment prototype
This commit is contained in:
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
|
||||||
|
)
|
||||||
|
selector2 = night_shift_query & Q(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
|
||||||
)
|
)
|
||||||
events_bucket_2 = Event.objects.filter(
|
|
||||||
night_shift_query, deleted=False, calendar__needs_fallback=True
|
|
||||||
)
|
|
||||||
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.
|
||||||
|
|
||||||
assigned_shifts = self.fallback_shifts.filter(night_shift_query)
|
# 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...
|
||||||
|
|
||||||
# for events in events:
|
while shifts_needed > 0:
|
||||||
# self.fallback_shifts.add(events)
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
|
# get teammembers sorted by the most shifts in the relevant bucket
|
||||||
|
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…
Reference in New Issue