def availability_submit(request, roster): try: roster = Roster.objects.get(name = roster) except Roster.DoesNotExist: return notification(request, 'Er is geen rooster genaamd \'%s\' gevonden' % roster) if not roster.state == 1: return notification(request, 'Je kunt nog niet, of niet meer, je beschikbaarheid doorgeven') if not RosterWorker.objects.filter(roster = roster, user = request.user): return notification(request, 'Je bent niet uitgenodigd voor dit rooster') if request.POST['shifts']: shift_pks = map(int, request.POST['shifts'].split(';')) else: shift_pks = [] year = int(request.POST['year']) week = int(request.POST['week']) monday = week_start_date(year, week) day = datetime.timedelta(days = 1) weekslots = TimeSlot.objects.filter(roster = roster, start__gt = monday, end__lt = monday + 7 * day) Availability.objects.filter(user = request.user, timeslot__pk__in = map(lambda sl: sl.pk, weekslots)).delete() for shift_pk in shift_pks: shift = TimeSlot.objects.get(pk = shift_pk) Availability(user = request.user, timeslot = shift).save() return redirect(to = reverse('availability', kwargs = {'roster': roster.name, 'year': year, 'week': week}))
def availability_copy(request, roster, year, week): try: roster = Roster.objects.get(name = roster) except Roster.DoesNotExist: return notification(request, 'Er is geen rooster genaamd \'%s\' gevonden' % roster) if not roster.state == 1: return notification(request, 'Je kunt nog niet, of niet meer, je beschikbaarheid doorgeven') year = int(year) week = int(week) if year < roster.start.year or (week < roster.start.isocalendar()[1] and year == roster.start.year) \ or year > roster.end.year or (week > roster.end.isocalendar()[1] and year == roster.end.year): return notification('Geen valide week') monday = week_start_date(year, week) day = datetime.timedelta(days = 1) weekslots = TimeSlot.objects.filter(roster = roster, start__gt = monday, end__lt = monday + 7 * day) weekavailabilities = Availability.objects.filter(user = request.user, timeslot__pk__in = map(lambda sl: sl.pk, weekslots)) weeks = 0 * day while monday + weeks >= datetime.datetime.combine(roster.start, datetime.time()): weeks -= 7 * day while monday + weeks <= datetime.datetime.combine(roster.end, datetime.time()): if not ((monday + weeks).isocalendar()[0] == year and (monday + weeks).isocalendar()[1] == week): weekslots = TimeSlot.objects.filter(roster = roster, start__gt = monday + weeks, end__lt = monday + weeks + 7 * day) Availability.objects.filter(user = request.user, timeslot__pk__in = map(lambda sl: sl.pk, weekslots)).delete() weeks += 7 * day for weekav in weekavailabilities: weeks = 0 * day while weekav.timeslot.start + weeks >= datetime.datetime.combine(roster.start, datetime.time()): weeks -= 7 * day while weekav.timeslot.end + weeks <= datetime.datetime.combine(roster.end, datetime.time()): ts_match = TimeSlot.objects.filter(roster = roster, start = weekav.timeslot.start + weeks, end = weekav.timeslot.end + weeks) if ts_match: if not Availability.objects.filter(user = request.user, timeslot = ts_match): Availability(user = request.user, timeslot = ts_match[0]).save() weeks += 7 * day return redirect(to = reverse('availability', kwargs = {'roster': roster.name, 'year': year, 'week': week}))
def availability(request, roster, year = None, week = None): try: roster = Roster.objects.get(name = roster) except Roster.DoesNotExist: return notification(request, 'Er is geen rooster genaamd \'%s\' gevonden' % roster) if not roster.state == 1: return notification(request, 'Je kunt nog niet, of niet meer, je beschikbaarheid doorgeven') if not RosterWorker.objects.filter(roster = roster, user = request.user): return notification(request, 'Je bent niet uitgenodigd voor dit rooster') if year == None or week == None: return redirect(to = reverse('availability', kwargs={'roster': roster.name, 'year': roster.start.year, 'week': roster.start.isocalendar()[1]})) else: year = int(year) week = int(week) if year < roster.start.year or (week < roster.start.isocalendar()[1] and year == roster.start.year): return redirect(to = reverse('availability', kwargs={'roster': roster.name, 'year': roster.start.year, 'week': roster.start.isocalendar()[1]})) if year > roster.end.year or (week > roster.end.isocalendar()[1] and year == roster.end.year): return redirect(to = reverse('availability', kwargs={'roster': roster.name, 'year': roster.end.year, 'week': roster.end.isocalendar()[1]})) monday = week_start_date(year, week) day = datetime.timedelta(days = 1) schedule = { 'monday': {'date': monday.strftime('%a %d %b'), 'name': 'monday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday, end__lt = monday + day)}, 'tuesday': {'date': (monday + day).strftime('%a %d %b'), 'name': 'tuesday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + day, end__lt = monday + 2 * day)}, 'wednesday': {'date': (monday + 2 * day).strftime('%a %d %b'), 'name': 'wednesday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 2 * day, end__lt = monday + 3 * day)}, 'thursday': {'date': (monday + 3 * day).strftime('%a %d %b'), 'name': 'thursday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 3 * day, end__lt = monday + 4 * day)}, 'friday': {'date': (monday + 4 * day).strftime('%a %d %b'), 'name': 'friday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 4 * day, end__lt = monday + 5 * day)}, 'saturday': {'date': (monday + 5 * day).strftime('%a %d %b'), 'name': 'saturday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 5 * day, end__lt = monday + 6 * day)}, 'sunday': {'date': (monday + 6 * day).strftime('%a %d %b'), 'name': 'sunday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 6 * day, end__lt = monday + 7 * day)}, } for schedule_day in schedule.values(): for timeslot in schedule_day['timeslots']: if Availability.objects.filter(user = request.user, timeslot = timeslot).count(): timeslot.available = True else: timeslot.available = False (next_year, next_week) = (monday + 7 * day).isocalendar()[0:2] (prev_year, prev_week) = (monday - 7 * day).isocalendar()[0:2] if prev_year < roster.start.year or (prev_week < roster.start.isocalendar()[1] and prev_year == roster.start.year): (prev_year, prev_week) = (None, None) if next_year > roster.end.year or (next_week > roster.end.isocalendar()[1] and next_year == roster.end.year): (next_year, next_week) = (None, None) ''' Get jump links to all the weeks ''' start_monday = week_start_date(roster.start.year, roster.start.isocalendar()[1]) end_monday = week_start_date(roster.end.year, roster.end.isocalendar()[1]) oneweek = datetime.timedelta(days = 7) mondays = [] day_k = start_monday while day_k <= end_monday: mondays.append({'name': day_k.strftime('%d %b'), 'is_this_week': monday == day_k, 'year': day_k.isocalendar()[0], 'week': day_k.isocalendar()[1]}) day_k += oneweek return render(request, 'availability.html', { 'roster': roster, 'schedule': schedule, 'year': year, 'prev_year': prev_year, 'next_year': next_year, 'week': week, 'prev_week': prev_week, 'next_week': next_week, 'mondays': mondays, })
def final_roster(request, roster, year = None, week = None): try: roster = Roster.objects.get(name = roster) except Roster.DoesNotExist: return notification(request, 'Er is geen rooster genaamd \'%s\' gevonden' % roster) if not roster.state == 4 and not (roster.state == 3 and request.user.is_staff): return redirect(to = reverse('availability', kwargs = {'roster': roster.name})) if year == None or week == None: if datetime.date.today() < roster.start: year = roster.start.year week = roster.start.isocalendar()[1] elif datetime.date.today() > roster.end: year = roster.end.year week = roster.end.isocalendar()[1] else: year = datetime.date.today().year week = datetime.date.today().isocalendar()[1] return redirect(to = reverse('final_roster', kwargs={'roster': roster.name, 'year': year, 'week': week})) else: year = int(year) week = int(week) if year < roster.start.year or (week < roster.start.isocalendar()[1] and year == roster.start.year): return redirect(to = reverse('final_roster', kwargs={'roster': roster.name, 'year': roster.start.year, 'week': roster.start.isocalendar()[1]})) if year > roster.end.year or (week > roster.end.isocalendar()[1] and year == roster.end.year): return redirect(to = reverse('final_roster', kwargs={'roster': roster.name, 'year': roster.end.year, 'week': roster.end.isocalendar()[1]})) monday = week_start_date(year, week) day = datetime.timedelta(days = 1) schedule = { 'monday': {'date': monday.strftime('%a %d %b'), 'name': 'monday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday, end__lt = monday + day)}, 'tuesday': {'date': (monday + day).strftime('%a %d %b'), 'name': 'tuesday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + day, end__lt = monday + 2 * day)}, 'wednesday': {'date': (monday + 2 * day).strftime('%a %d %b'), 'name': 'wednesday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 2 * day, end__lt = monday + 3 * day)}, 'thursday': {'date': (monday + 3 * day).strftime('%a %d %b'), 'name': 'thursday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 3 * day, end__lt = monday + 4 * day)}, 'friday': {'date': (monday + 4 * day).strftime('%a %d %b'), 'name': 'friday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 4 * day, end__lt = monday + 5 * day)}, 'saturday': {'date': (monday + 5 * day).strftime('%a %d %b'), 'name': 'saturday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 5 * day, end__lt = monday + 6 * day)}, 'sunday': {'date': (monday + 6 * day).strftime('%a %d %b'), 'name': 'sunday', 'timeslots': TimeSlot.objects.filter(roster = roster, start__gt = monday + 6 * day, end__lt = monday + 7 *day)}, } (next_year, next_week) = (monday + 7 * day).isocalendar()[0:2] (prev_year, prev_week) = (monday - 7 * day).isocalendar()[0:2] if prev_year < roster.start.year or (prev_week < roster.start.isocalendar()[1] and prev_year == roster.start.year): (prev_year, prev_week) = (None, None) if next_year > roster.end.year or (next_week > roster.end.isocalendar()[1] and next_year == roster.end.year): (next_year, next_week) = (None, None) urls = {} urls['all'] = 'http://' + request.get_host() + reverse('ical_all') urls['trade'] = 'http://' + request.get_host() + reverse('ical_trade') if request.user.is_authenticated(): urls['own'] = 'http://' + request.get_host() + reverse('ical_own', kwargs = {'user': request.user.username}) urls['available'] = 'http://' + request.get_host() + reverse('ical_available', kwargs = {'user': request.user.username}) user = AnonymousUser() if request.user.is_authenticated(): if RosterWorker.objects.filter(user = request.user, roster = roster) or request.user.is_staff: user = request.user ''' Get jump links to all the weeks ''' start_monday = week_start_date(roster.start.year, roster.start.isocalendar()[1]) end_monday = week_start_date(roster.end.year, roster.end.isocalendar()[1]) oneweek = datetime.timedelta(days = 7) mondays = [] day_k = start_monday while day_k <= end_monday: mondays.append({'name': day_k.strftime('%d %b'), 'is_this_week': monday == day_k, 'year': day_k.isocalendar()[0], 'week': day_k.isocalendar()[1]}) day_k += oneweek return render(request, 'final_roster.html', { 'user': user, 'roster': roster, 'schedule': schedule, 'year': year, 'prev_year': prev_year, 'next_year': next_year, 'week': week, 'prev_week': prev_week, 'next_week': next_week, 'urls': urls, 'mondays': mondays, })
def all_rosters_txt(request, year = None, week = None): rosters = Roster.objects.filter(state = 4).order_by('start') if not rosters: return notification(request, 'geen roosters gevonden') if year == None or week == None: year = datetime.date.today().year week = datetime.date.today().isocalendar()[1] return redirect(to = reverse('all_rosters_txt', kwargs={'year': year, 'week': week})) year = int(year) week = int(week) monday = week_start_date(year, week) day = datetime.timedelta(days = 1) schedule = [ {'date': monday.strftime('%a %d %b'), 'name': 'monday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__gt = monday, end__lt = monday + day)}, {'date': (monday + day).strftime('%a %d %b'), 'name': 'tuesday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__gt = monday + day, end__lt = monday + 2 * day)}, {'date': (monday + 2 * day).strftime('%a %d %b'), 'name': 'wednesday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__gt = monday + 2 * day, end__lt = monday + 3 * day)}, {'date': (monday + 3 * day).strftime('%a %d %b'), 'name': 'thursday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__gt = monday + 3 * day, end__lt = monday + 4 * day)}, {'date': (monday + 4 * day).strftime('%a %d %b'), 'name': 'friday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__gt = monday + 4 * day, end__lt = monday + 5 * day)}, {'date': (monday + 5 * day).strftime('%a %d %b'), 'name': 'saturday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__gt = monday + 5 * day, end__lt = monday + 6 * day)}, {'date': (monday + 6 * day).strftime('%a %d %b'), 'name': 'sunday', 'timeslots': TimeSlot.objects.filter(roster__in = rosters, start__qt = monday + 6 * day, end__lt = monday + 7 * day)}, ] (next_year, next_week) = (monday + 7 * day).isocalendar()[0:2] (prev_year, prev_week) = (monday - 7 * day).isocalendar()[0:2] #if prev_year < rosters[0].start.year or (prev_week < rosters[0].start.isocalendar()[1] and prev_year == rosters[0].start.year): # (prev_year, prev_week) = (None, None) #if next_year > rosters[0].end.year or (next_week > rosters[0].end.isocalendar()[1] and next_year == rosters[0].end.year): # (next_year, next_week) = (None, None) urls = {} urls['all'] = 'http://' + request.get_host() + reverse('ical_all') urls['trade'] = 'http://' + request.get_host() + reverse('ical_trade') if request.user.is_authenticated(): urls['own'] = 'http://' + request.get_host() + reverse('ical_own', kwargs = {'user': request.user.username}) urls['available'] = 'http://' + request.get_host() + reverse('ical_available', kwargs = {'user': request.user.username}) user = AnonymousUser() if request.user.is_authenticated(): if RosterWorker.objects.filter(user = request.user, roster__in = rosters): user = request.user ''' Get jump links to all the weeks ''' #week_start_date(roster.start.year, roster.start.isocalendar()[1]) #end_monday = week_start_date(roster.end.year, roster.end.isocalendar()[1]) oneweek = datetime.timedelta(days = 7) start_monday = monday - 5 * oneweek end_monday = monday + 5 * oneweek mondays = [] day_k = start_monday while day_k <= end_monday: mondays.append({'name': day_k.strftime('%d %b'), 'is_this_week': monday == day_k, 'year': day_k.isocalendar()[0], 'week': day_k.isocalendar()[1]}) day_k += oneweek return render(request, 'all_rosters_txt.html', { 'user': user, #'roster': roster, 'schedule_vals': schedule, 'year': year, 'prev_year': prev_year, 'next_year': next_year, 'week': week, 'prev_week': prev_week, 'next_week': next_week, 'urls': urls, 'mondays': mondays, })
def handle(self, *args, **options): """ Input arguments """ t_init = time.time() try: roster = Roster.objects.get(pk=int(args[0])) except IndexError: print 'Please provide a roster ID' show_rosters() return except ValueError: print 'Please provide an integer roster ID\n' show_rosters() return except Roster.DoesNotExist: print 'Roster with ID %s not found\n' % args[0] show_rosters() return N = options['steps'] check_consistency = options['check'] alpha = options['alpha'] if roster.state < 1: print 'This roster is is not ready to distribute yet, complete other steps first' return elif roster.state == 3: if options['force']: roster.state = 2 roster.save() else: print 'This roster has already been distributed; use -f to force redistribution' return elif roster.state > 3: print 'This roster has already been distributed' return elif roster.state == 1: roster.state = 2 roster.save() print '* %s *' % roster.name.upper() ''' Create the availability data structure ''' t_load = time.time() slotmap = {ts.pk: ts for ts in TimeSlot.objects.filter(roster=roster)} availabilities_qs = Availability.objects.filter( timeslot__roster=roster) t_assign = time.time() print 'TIME query data: %.3fs' % (t_assign - t_load) A, D = {}, {} degeneracy, duration, similar = {}, {}, {} slots = [] total_hours = 0 for slot in slotmap.values(): slots.append(slot) D[slot.pk] = slot.degeneracy * [None] A[slot.pk] = [] degeneracy[slot.pk] = slot.degeneracy duration[slot.pk] = to_hours(slot.duration) similar[slot.pk] = [] total_hours += slot.degeneracy * to_hours(slot.duration) ''' Find similar shifts (exactly a week difference) ''' monday = week_start_date(slot.start.isocalendar()[0], slot.start.isocalendar()[1]) day = datetime.timedelta(days=1) weeks = 0 * day while monday + weeks >= datetime.datetime.combine( roster.start, datetime.time()): weeks -= 7 * day while monday + weeks <= datetime.datetime.combine( roster.end, datetime.time()): if not ((monday + weeks).isocalendar()[0] == slot.start.isocalendar()[0] and (monday + weeks).isocalendar()[1] == slot.start.isocalendar()[1]): sim_slot = TimeSlot.objects.filter(roster=roster).filter( roster=slot.roster, start=slot.start + weeks, end=slot.end + weeks) if sim_slot: similar[slot.pk].append(sim_slot[0]) weeks += 7 * day for availability in availabilities_qs: ''' There should be no duplicates but it happens, somehow. Don't know where they come from, but I can't block them at database level because sqlite seems to throw random errors for unique_together... ''' if availability.user.pk not in map_user_pk( A[availability.timeslot.pk]): A[availability.timeslot.pk].append(availability) else: print 'There was a duplicate Availability; #%d removed' % availability.pk Availability.objects.get(pk=availability.pk).delete() if not N: N = 10 * len(slots) ''' Calculate the hours ''' """ extra hours for flexibility are calculated as: alpha * sqrt(user_available_hours / per_person_expected) [but never negative] """ t_hours = time.time() print 'TIME assign data: %.3fs' % (t_hours - t_assign) workers = RosterWorker.objects.filter(roster=roster) current_hours = {} extra_hours = {} flexibility = {} expected_hours = float(total_hours) / len(workers) available_hours = {worker.user.pk: 0 for worker in workers} for availability_list in A.values(): for availability in availability_list: available_hours[availability.user.pk] += to_hours( availability.timeslot.duration) for worker in workers: extra_hours[worker.user.pk] = worker.extra flexibility[worker.user.pk] = sqrt( available_hours[worker.user.pk] / expected_hours) extra_hours[worker.user.pk] += alpha * flexibility[worker.user.pk] current_hours[worker.user.pk] = 0 ''' Monte Carlo (with only favourable steps though) ''' t_monte = time.time() no_change_steps = 0 counter = 500 * len(slots) while no_change_steps < N and counter > 0: updated = False counter -= 1 ''' Trivial slots are removed; if there are no slots left we quit ''' if not len(slots): print 'DETERMINISTIC / TRIVIAL' break slot = weighted_choice(slots, degeneracy.values()) if len(A[slot.pk]) <= degeneracy[slot.pk]: ''' No competition for this position ''' for deg in range(degeneracy[slot.pk]): min_av = select_fewest_hours( set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) ''' Note that one of the positions might have been filled through the recursion mechanism ''' if min_av and not D[slot.pk][deg]: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) slots.remove(slot) degeneracy.pop(slot.pk) else: deg = random.randint(0, len(D[slot.pk]) - 1) if D[slot.pk][deg] == None: ''' No current worker, so no comparison ''' worker = random.choice(A[slot.pk]) (D, current_hours, updated) = assign(worker, A, D, slot, deg, current_hours) elif len(A[slot.pk]) > 1: ''' There is a selected worker, and at least one alternative ''' worker = random.choice(A[slot.pk]) while worker.pk == D[slot.pk][deg].pk: worker = random.choice(A[slot.pk]) (D, current_hours, updated) = switch(slot, deg, worker, A, D, duration, similar, current_hours, extra_hours, recursive=True) ''' Count the steps without change (the while loop quits after N of them) The only way assign is called multiple times is when there is one succesful switch, in which case True is returned anyway ''' no_change_steps += 1 if updated: no_change_steps = 0 ''' Prevent unnecessary blanks (generally none are found) ''' t_post = time.time() print 'TIME monte carlo: %.3fs' % (t_post - t_monte) for slot in slots: for deg in range(degeneracy[slot.pk]): if D[slot.pk][deg] == None: ''' There is an empty slot; find the candidate with fewest hours ''' min_av = select_fewest_hours( set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) if min_av: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) ''' Consistency checks ''' t_check = time.time() print 'TIME post check: %.3fs' % (t_check - t_post) if check_consistency: all_slot_check = 0 for slot in slots: all_slot_check += to_hours(slot.duration) * slot.degeneracy ''' Check the degeneracy values ''' assert len(D[slot.pk]) == slot.degeneracy ''' Check that there aren't more shifts occupied than there are available people ''' assert len(filter(lambda x: x, D[slot.pk])) <= len(A[slot.pk]) ''' Check that a person doesn't have two shifts in one slot ''' assert len(map_user_pk(D[slot.pk])) == len( set(map_user_pk(D[slot.pk]))) ''' Check that the total hours of all slots does not exceed the assigned hours ''' all_assign_check = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity: all_assign_check += to_hours(avity.timeslot.duration) ''' Check that all assigned shifts belong to someone that is available ''' assert avity.pk in map_pk(A[slot_pk]) else: ''' If the slot is empty, then no one is available that isn't already working ''' if len( set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))): print '>> %s | %s (%d)' % (set(map_pk( A[slot_pk])), set(map_pk( D[slot_pk])), len(D[slot_pk])) print map_user_pk(A[slot_pk]) assert len( set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))) == 0 ''' check sum of hours (bugged end 2013) and empty slots ''' current_hours_check = {} for rw in workers: current_hours_check[rw.user.pk] = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity is None: ''' check that, for every empty slot, there are fewer availabilities than places ''' assert len(A[slot_pk]) <= [ slot.degeneracy for slot in slotmap.values() if slot.pk == slot_pk ][0] else: current_hours_check[avity.user.pk] += to_hours( avity.timeslot.duration) ''' check that the total hours are the same ''' assert sum(current_hours.values()) == sum( current_hours_check.values()) assert len(current_hours) == len(current_hours_check) ''' check that the hours for individual users are the same ''' for user_pk in current_hours.keys(): assert current_hours[user_pk] == current_hours_check[user_pk] ''' Check that the hours of assigned shifts match the total of current_hours ''' assert all_assign_check == sum(current_hours.values()) ''' Store result ''' t_store = time.time() if check_consistency: print 'TIME checks: %.3fs' % (t_store - t_check) else: print 'TIME checks: off' roster = Roster.objects.get(pk=roster.pk) if not roster.state == 2: print 'The state of the roster has changed! There may be a concurrent process. Mission aborted; no changes will be made.' return Assignment.objects.filter(timeslot__in=slotmap.values()).delete() batch = [] empty_slots = [] for key, availability_list in D.items(): for deg, availability in enumerate(availability_list): if availability is None: empty_slots.append(slotmap[key]) else: batch.append( Assignment(user=availability.user, timeslot=availability.timeslot, note='shift %d' % deg)) Assignment.objects.bulk_create(batch) roster.state = 3 roster.save() ''' Process the result ''' t_result = time.time() print 'TIME store result: %.3fs' % (t_result - t_store) print 'TIME other steps: %.3fs' % (t_result - t_init - (t_check - t_monte) - (t_hours - t_assign) - (t_assign - t_load) - (t_result - t_store) - (t_store - t_check)) print 'TIME total time: %.3fs' % (t_result - t_init) print 'remaining hours: %d' % (total_hours - sum(current_hours.values())) for empty_slot in empty_slots: print '\t %s (%.1fh)' % (empty_slot, to_hours(empty_slot.duration)) print 'with alpha = %.3f max extra hours is %.1fh' % ( alpha, alpha * (max(flexibility.values()) - min(flexibility.values()))) print ' user\tfinal\textra (goal)' for user_pk in current_hours.keys(): print ' %-20s\t%d\t%.1f' % (unicode(get_user_model().objects.get( pk=user_pk))[:20], int( current_hours[user_pk]), extra_hours[user_pk]) if not counter: print '(Monte Carlo reached iteration limit)'
def handle(self, *args, **options): """ Input arguments """ t_init = time.time() try: roster = Roster.objects.get(pk = int(args[0])) except IndexError: print 'Please provide a roster ID' show_rosters() return except ValueError: print 'Please provide an integer roster ID\n' show_rosters() return except Roster.DoesNotExist: print 'Roster with ID %s not found\n' % args[0] show_rosters() return N = options['steps'] check_consistency = options['check'] alpha = options['alpha'] if roster.state < 1: print 'This roster is is not ready to distribute yet, complete other steps first' return elif roster.state == 3: if options['force']: roster.state = 2 roster.save() else: print 'This roster has already been distributed; use -f to force redistribution' return elif roster.state > 3: print 'This roster has already been distributed' return elif roster.state == 1: roster.state = 2 roster.save() print '* %s *' % roster.name.upper() ''' Create the availability data structure ''' t_load = time.time() slotmap = {ts.pk: ts for ts in TimeSlot.objects.filter(roster = roster)} availabilities_qs = Availability.objects.filter(timeslot__roster = roster) t_assign = time.time() print 'TIME query data: %.3fs' % (t_assign - t_load) A, D = {}, {} degeneracy, duration, similar = {}, {}, {} slots = [] total_hours = 0 for slot in slotmap.values(): slots.append(slot) D[slot.pk] = slot.degeneracy * [None] A[slot.pk] = [] degeneracy[slot.pk] = slot.degeneracy duration[slot.pk] = to_hours(slot.duration) similar[slot.pk] = [] total_hours += slot.degeneracy * to_hours(slot.duration) ''' Find similar shifts (exactly a week difference) ''' monday = week_start_date(slot.start.isocalendar()[0], slot.start.isocalendar()[1]) day = datetime.timedelta(days = 1) weeks = 0 * day while monday + weeks >= datetime.datetime.combine(roster.start, datetime.time()): weeks -= 7 * day while monday + weeks <= datetime.datetime.combine(roster.end, datetime.time()): if not ((monday + weeks).isocalendar()[0] == slot.start.isocalendar()[0] and (monday + weeks).isocalendar()[1] == slot.start.isocalendar()[1]): sim_slot = TimeSlot.objects.filter(roster = roster).filter(roster = slot.roster, start = slot.start + weeks, end = slot.end + weeks) if sim_slot: similar[slot.pk].append(sim_slot[0]) weeks += 7 * day for availability in availabilities_qs: ''' There should be no duplicates but it happens, somehow. Don't know where they come from, but I can't block them at database level because sqlite seems to throw random errors for unique_together... ''' if availability.user.pk not in map_user_pk(A[availability.timeslot.pk]): A[availability.timeslot.pk].append(availability) else: print 'There was a duplicate Availability; #%d removed' % availability.pk Availability.objects.get(pk = availability.pk).delete() if not N: N = 10 * len(slots) ''' Calculate the hours ''' """ extra hours for flexibility are calculated as: alpha * sqrt(user_available_hours / per_person_expected) [but never negative] """ t_hours = time.time() print 'TIME assign data: %.3fs' % (t_hours - t_assign) workers = RosterWorker.objects.filter(roster = roster) current_hours = {} extra_hours = {} flexibility = {} expected_hours = float(total_hours) / len(workers) available_hours = {worker.user.pk: 0 for worker in workers} for availability_list in A.values(): for availability in availability_list: available_hours[availability.user.pk] += to_hours(availability.timeslot.duration) for worker in workers: extra_hours[worker.user.pk] = worker.extra flexibility[worker.user.pk] = sqrt(available_hours[worker.user.pk] / expected_hours) extra_hours[worker.user.pk] += alpha * flexibility[worker.user.pk] current_hours[worker.user.pk] = 0 ''' Monte Carlo (with only favourable steps though) ''' t_monte = time.time() no_change_steps = 0 counter = 500 * len(slots) while no_change_steps < N and counter> 0: updated = False counter -= 1 ''' Trivial slots are removed; if there are no slots left we quit ''' if not len(slots): print 'DETERMINISTIC / TRIVIAL' break slot = weighted_choice(slots, degeneracy.values()) if len(A[slot.pk]) <= degeneracy[slot.pk]: ''' No competition for this position ''' for deg in range(degeneracy[slot.pk]): min_av = select_fewest_hours(set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) ''' Note that one of the positions might have been filled through the recursion mechanism ''' if min_av and not D[slot.pk][deg]: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) slots.remove(slot) degeneracy.pop(slot.pk) else: deg = random.randint(0, len(D[slot.pk]) - 1) if D[slot.pk][deg] == None: ''' No current worker, so no comparison ''' worker = random.choice(A[slot.pk]) (D, current_hours, updated) = assign(worker, A, D, slot, deg, current_hours) elif len(A[slot.pk]) > 1: ''' There is a selected worker, and at least one alternative ''' worker = random.choice(A[slot.pk]) while worker.pk == D[slot.pk][deg].pk: worker = random.choice(A[slot.pk]) (D, current_hours, updated) = switch(slot, deg, worker, A, D, duration, similar, current_hours, extra_hours, recursive = True) ''' Count the steps without change (the while loop quits after N of them) The only way assign is called multiple times is when there is one succesful switch, in which case True is returned anyway ''' no_change_steps += 1 if updated: no_change_steps = 0 ''' Prevent unnecessary blanks (generally none are found) ''' t_post = time.time() print 'TIME monte carlo: %.3fs' % (t_post - t_monte) for slot in slots: for deg in range(degeneracy[slot.pk]): if D[slot.pk][deg] == None: ''' There is an empty slot; find the candidate with fewest hours ''' min_av = select_fewest_hours(set(A[slot.pk]) - set(D[slot.pk]), current_hours, extra_hours) if min_av: (D, current_hours, updated) = assign(min_av, A, D, slot, deg, current_hours) ''' Consistency checks ''' t_check = time.time() print 'TIME post check: %.3fs' % (t_check - t_post) if check_consistency: all_slot_check = 0 for slot in slots: all_slot_check += to_hours(slot.duration) * slot.degeneracy ''' Check the degeneracy values ''' assert len(D[slot.pk]) == slot.degeneracy ''' Check that there aren't more shifts occupied than there are available people ''' assert len(filter(lambda x: x, D[slot.pk])) <= len(A[slot.pk]) ''' Check that a person doesn't have two shifts in one slot ''' assert len(map_user_pk(D[slot.pk])) == len(set(map_user_pk(D[slot.pk]))) ''' Check that the total hours of all slots does not exceed the assigned hours ''' all_assign_check = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity: all_assign_check += to_hours(avity.timeslot.duration) ''' Check that all assigned shifts belong to someone that is available ''' assert avity.pk in map_pk(A[slot_pk]) else: ''' If the slot is empty, then no one is available that isn't already working ''' if len(set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))): print '>> %s | %s (%d)' % (set(map_pk(A[slot_pk])), set(map_pk(D[slot_pk])), len(D[slot_pk])) print map_user_pk(A[slot_pk]) assert len(set(map_pk(A[slot_pk])) - set(map_pk(D[slot_pk]))) == 0 ''' check sum of hours (bugged end 2013) and empty slots ''' current_hours_check = {} for rw in workers: current_hours_check[rw.user.pk] = 0 for slot_pk, avity_list in D.items(): for deg, avity in enumerate(avity_list): if avity is None: ''' check that, for every empty slot, there are fewer availabilities than places ''' assert len(A[slot_pk]) <= [slot.degeneracy for slot in slotmap.values() if slot.pk == slot_pk][0] else: current_hours_check[avity.user.pk] += to_hours(avity.timeslot.duration) ''' check that the total hours are the same ''' assert sum(current_hours.values()) == sum(current_hours_check.values()) assert len(current_hours) == len(current_hours_check) ''' check that the hours for individual users are the same ''' for user_pk in current_hours.keys(): assert current_hours[user_pk] == current_hours_check[user_pk] ''' Check that the hours of assigned shifts match the total of current_hours ''' assert all_assign_check == sum(current_hours.values()) ''' Store result ''' t_store = time.time() if check_consistency: print 'TIME checks: %.3fs' % (t_store - t_check) else: print 'TIME checks: off' roster = Roster.objects.get(pk = roster.pk) if not roster.state == 2: print 'The state of the roster has changed! There may be a concurrent process. Mission aborted; no changes will be made.' return Assignment.objects.filter(timeslot__in = slotmap.values()).delete() batch = [] empty_slots = [] for key, availability_list in D.items(): for deg, availability in enumerate(availability_list): if availability is None: empty_slots.append(slotmap[key]) else: batch.append(Assignment(user = availability.user, timeslot = availability.timeslot, note = 'shift %d' % deg)) Assignment.objects.bulk_create(batch) roster.state = 3 roster.save() ''' Process the result ''' t_result = time.time() print 'TIME store result: %.3fs' % (t_result - t_store) print 'TIME other steps: %.3fs' % (t_result - t_init - (t_check - t_monte) - (t_hours - t_assign) - (t_assign - t_load) - (t_result - t_store) - (t_store - t_check)) print 'TIME total time: %.3fs' % (t_result - t_init) print 'remaining hours: %d' % (total_hours - sum(current_hours.values())) for empty_slot in empty_slots: print '\t %s (%.1fh)' % (empty_slot, to_hours(empty_slot.duration)) print 'with alpha = %.3f max extra hours is %.1fh' % (alpha, alpha * (max(flexibility.values()) - min(flexibility.values()))) print ' user\tfinal\textra (goal)' for user_pk in current_hours.keys(): print ' %-20s\t%d\t%.1f' % (unicode(get_user_model().objects.get(pk = user_pk))[:20], int(current_hours[user_pk]), extra_hours[user_pk]) if not counter: print '(Monte Carlo reached iteration limit)'