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()
|
||||
|
||||
|
||||
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):
|
||||
model = FallbackAssignment
|
||||
ordering = ("shift__start_at",)
|
||||
|
||||
|
||||
@admin.register(TeamMember)
|
||||
class TeamMemberAdmin(admin.ModelAdmin):
|
||||
fields = ["name"]
|
||||
list_display = ["name"]
|
||||
list_display = ["name", "shift_count"]
|
||||
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 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)
|
||||
|
||||
|
@ -8,24 +11,104 @@ class TeamMember(models.Model):
|
|||
fallback_shifts = models.ManyToManyField(Shift, through="FallbackAssignment")
|
||||
|
||||
def assign_random_shifts(self):
|
||||
events_bucket_1 = Event.objects.filter(
|
||||
~night_shift_query, deleted=False, calendar__needs_fallback=True
|
||||
if self.fallback_shifts.count() != 0:
|
||||
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"]
|
||||
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:
|
||||
# self.fallback_shifts.add(events)
|
||||
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
|
||||
)
|
||||
|
||||
# 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):
|
||||
return f"{self.name}"
|
||||
|
|
|
@ -4,4 +4,4 @@ from .models import Calendar
|
|||
|
||||
@admin.register(Calendar)
|
||||
class CalendarAdmin(admin.ModelAdmin):
|
||||
list_display = ("url", "has_errors")
|
||||
list_display = ("url", "needs_fallback", "has_errors")
|
||||
|
|
Loading…
Reference in New Issue