Skip to content
Snippets Groups Projects
Commit fb30c875 authored by Carlos Vega's avatar Carlos Vega
Browse files

Fix office availability to avoid non-overlapping events and ensure overlaps stay within bounds

parent 7536a235
No related branches found
No related tags found
1 merge request!183Hotfix/office availability
Pipeline #7301 failed
......@@ -187,12 +187,28 @@ class Worker(models.Model):
office_availability = OfficeAvailability('{} {}'.format(self.first_name, self.last_name), start=start_date, end=end_date)
#Appointments
subject_appointments = AppointmentTypeLink.objects.filter(worker=self.id, date_when__gte=start_date, date_when__lte=end_date)
general_appointments = Appointment.objects.filter(worker_assigned=self.id, datetime_when__gte=start_date, datetime_when__lte=end_date)
#Subject Appointments
old_events = Q(date_when__gt=start_date) & Q(date_when__gt=end_date)
future_events = Q(date_when__lt=start_date) & Q(date_when__lt=end_date)
non_overlap_events = old_events | future_events
overlap_events = ~non_overlap_events
query = Q(worker=self.id) & overlap_events
subject_appointments = AppointmentTypeLink.objects.filter(query)
#General Appointments
old_events = Q(datetime_when__gt=start_date) & Q(datetime_when__gt=end_date)
future_events = Q(datetime_when__lt=start_date) & Q(datetime_when__lt=end_date)
non_overlap_events = old_events | future_events
overlap_events = ~non_overlap_events
query = Q(worker_assigned=self.id) & overlap_events
general_appointments = Appointment.objects.filter(query)
#Holidays and extra availabilities.
holidays_and_extra_availabilities = self.holiday_set.filter(datetime_start__gte=start_date, datetime_end__lt=end_date).order_by('-datetime_start')
old_events = Q(datetime_start__gt=start_date) & Q(datetime_start__gt=end_date)
future_events = Q(datetime_end__lt=start_date) & Q(datetime_end__lt=end_date)
non_overlap_events = old_events | future_events
overlap_events = ~non_overlap_events
holidays_and_extra_availabilities = self.holiday_set.filter(overlap_events).order_by('-datetime_start')
#Availability
weekdays = get_weekdays_in_period(start_date, end_date)
......
......@@ -44,7 +44,7 @@ class OfficeAvailability(object):
self.office_end = office_end
self.minimum_slot = minimum_slot
self.range = pd.date_range(start=self.start, end=self.end, freq=self.minimum_slot)
logger.debug('Min index: {}. Max index: {}'.format(self.start, self.end))
logger.debug(u'Name: {}. Min index: {}. Max index: {}'.format(self.name, self.start, self.end))
self.availability = pd.Series(index=self.range, data=0) # initialize range at 0
def _get_duration(self):
......@@ -73,6 +73,27 @@ class OfficeAvailability(object):
range = range.to_series().between_time(self.office_start, self.office_end).index
self.availability[range] = 0
def _ensure_dates_are_in_bounds(self, given_start, given_end):
'''
given_start and given_end should not be a string but if so, they must comply with pd.Timestamp requirements
'''
# sort dates to ensure start < end
start, end = sorted([pd.Timestamp(given_start), pd.Timestamp(given_end)])
if start < self.availability.index.min():
start = self.availability.index.min()
# check if end is in bounds
if end > self.availability.index.max():
end = self.availability.index.max()
# this could only happen if both start and end dates are higher or lower than index.max/min since start and end dates are sorted
# this means that the two dates are out of bounds and then its time range doesn't overlap with the self.availability
if start > self.availability.index.max() or end < self.availability.index.min():
raise ValueError
return start, end
def consider_this(self, appointment_availability_or_holiday, only_working_hours=False):
'''
:appointment_availability_or_holiday can be an object from the following classes: Availability, Holiday, Appointment, AppointmentTypeLink.
......@@ -106,19 +127,35 @@ class OfficeAvailability(object):
elif isinstance(appointment_availability_or_holiday, Holiday):
start = appointment_availability_or_holiday.datetime_start
end = appointment_availability_or_holiday.datetime_end
# ensure the start and end dates are in the same range to avoid memory issues (for example, someone asking from 1960 to 2120 creating a huge pd.Range)
logger.debug('Considering {} from {} to {}'.format('Extra Availability' if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA else 'Holiday', start, end))
try:
start, end = self._ensure_dates_are_in_bounds(start, end)
except ValueError:
logger.debug('Holiday range does not overlap the availability range. Ignoring Holiday.')
return
portion = self.availability[pd.date_range(start=start, end=end, freq=self.minimum_slot)] #select the specific range
set_to = 1 if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA else 0
elif isinstance(appointment_availability_or_holiday, Appointment):
start = appointment_availability_or_holiday.datetime_when
end = start + datetime.timedelta(minutes=appointment_availability_or_holiday.length)
logger.debug('Considering General Appointment from {} to {}'.format(start, end))
try:
start, end = self._ensure_dates_are_in_bounds(start, end)
except ValueError:
logger.debug('Appointment range does not overlap the availability range. Ignoring Appointment.')
return
portion = self.availability[pd.date_range(start=start, end=end, freq=self.minimum_slot)] #select the specific range
set_to = 0
elif isinstance(appointment_availability_or_holiday, AppointmentTypeLink):
start = appointment_availability_or_holiday.date_when
end = start + datetime.timedelta(minutes=appointment_availability_or_holiday.appointment_type.default_duration)
logger.debug('Considering Subject Appointment from {} to {}'.format(start, end))
try:
start, end = self._ensure_dates_are_in_bounds(start, end)
except ValueError:
logger.debug('AppointmentTypeLink range does not overlap the availability range. Ignoring AppointmentTypeLink.')
return
portion = self.availability[pd.date_range(start=start, end=end, freq=self.minimum_slot)] #select the specific range
set_to = 0
else:
......
......@@ -108,6 +108,76 @@ class OfficeAvailabilityTest(TestCase):
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 59.900166389351085) # ((6*60)/(10*60.0+1))*100 # the border minute is 0 then no +1
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 24.98265093684941)
def test_availability_cases2(self):
#
today = timezone.now()
start_date = datetime.datetime(today.year, today.month, today.day, tzinfo=today.tzinfo) #today midnight
end_date = start_date + datetime.timedelta(days=1)
first_name = u'âêîôûŵŷäëïöüẅÿà'
last_name = u'èìòùẁỳáéíóúẃýćńóśźżąę'
office_availability = OfficeAvailability(u'{} {}'.format(first_name, last_name),
start=start_date, end=end_date, office_start='8:00', office_end='18:00')
#no availabilties added yet
self.assertEqual(office_availability.is_available(), False)
self.assertEqual(office_availability.is_available(only_working_hours=True), False)
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 0.0)
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 0.0)
#add availabilty from 8:00 to 18:00
weekday = list(get_weekdays_in_period(start_date, end_date))[0]
availability = create_availability(available_from='8:00', available_till='18:00', day_number=weekday)
office_availability.consider_this(availability)
self.assertEqual(office_availability.is_available(only_working_hours=True), True)
self.assertEqual(office_availability.is_available(), False) #less than 50%
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 100.0)
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 41.70714781401804) # ((10*(60)+1)/(24*60.0+1))*100 That +1 is the zero minute
#add holiday (leave) from 30 days ago up to next 90 days starting with today in the middle
start_date = datetime.datetime(today.year, today.month, today.day, 16, 00, tzinfo=today.tzinfo)
end_date = start_date + datetime.timedelta(days=90)
start_date = start_date - datetime.timedelta(days=30)
holiday = Holiday(person=create_worker(), datetime_start=start_date, datetime_end=end_date, kind=AVAILABILITY_HOLIDAY)
office_availability.consider_this(holiday)
self.assertEqual(office_availability.is_available(only_working_hours=True), False)
self.assertEqual(office_availability.is_available(), False) #less than 50%
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 0) # ((8*60)/(10*60.0+1))*100 # the bordwer minute is 0, then no +1
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 0) # ((8*60)/(24*60.0+1))*100
def test_availability_cases3(self):
#
today = timezone.now()
start_date = datetime.datetime(today.year, today.month, today.day, tzinfo=today.tzinfo) #today midnight
end_date = start_date + datetime.timedelta(days=1)
first_name = u'âêîôûŵŷäëïöüẅÿà'
last_name = u'èìòùẁỳáéíóúẃýćńóśźżąę'
office_availability = OfficeAvailability(u'{} {}'.format(first_name, last_name),
start=start_date, end=end_date, office_start='8:00', office_end='18:00')
#no availabilties added yet
self.assertEqual(office_availability.is_available(), False)
self.assertEqual(office_availability.is_available(only_working_hours=True), False)
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 0.0)
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 0.0)
#add availabilty from 8:00 to 18:00
weekday = list(get_weekdays_in_period(start_date, end_date))[0]
availability = create_availability(available_from='8:00', available_till='18:00', day_number=weekday)
office_availability.consider_this(availability)
self.assertEqual(office_availability.is_available(only_working_hours=True), True)
self.assertEqual(office_availability.is_available(), False) #less than 50%
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 100.0)
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 41.70714781401804) # ((10*(60)+1)/(24*60.0+1))*100 That +1 is the zero minute
#add holiday (leave) from 90 days ago up to 60 days ago but without overlapping with today's date
start_date = datetime.datetime(today.year, today.month, today.day, 16, 00, tzinfo=today.tzinfo) - datetime.timedelta(days=90)
end_date = start_date + datetime.timedelta(days=30)
holiday = Holiday(person=create_worker(), datetime_start=start_date, datetime_end=end_date, kind=AVAILABILITY_HOLIDAY)
office_availability.consider_this(holiday)
self.assertEqual(office_availability.is_available(only_working_hours=True), True)
self.assertEqual(office_availability.is_available(), False) #less than 50%
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=True), 100.0) # ((8*60)/(10*60.0+1))*100 # the bordwer minute is 0, then no +1
self.assertEqual(office_availability.get_availability_percentage(only_working_hours=False), 41.70714781401804) # ((8*60)/(24*60.0+1))*100
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment