Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • smasch/scheduling-system
1 result
Show changes
Commits on Source (63)
Showing
with 526 additions and 10 deletions
......@@ -19,4 +19,4 @@ test:
- cd smash
- python manage.py makemigrations web && python manage.py migrate
- coverage run --source web manage.py test
- coverage report -m --omit="*/test*,*/migrations*"
- coverage report -m --omit="*/test*,*/migrations*,*debug_utils*"
pandas==0.23.4
numpy==1.15.2
matplotlib==2.2.3
Django==1.11.5
gunicorn==19.6.0
Pillow==3.4.2
......
......@@ -67,6 +67,9 @@ urlpatterns = [
url(r'^availabilities/(?P<date>\d{4}-\d{2}-\d{2})/$', daily_planning.availabilities, name='web.api.availabilities'),
url(r'^events_persist$', daily_planning.events_persist, name='web.api.events_persist'),
#worker availability
url(r'^worker_availability/$', worker.get_worker_availability, name='web.api.get_worker_availability'),
# worker data
url(r'^redcap/missing_subjects/(?P<missing_subject_id>\d+):ignore$', redcap.ignore_missing_subject,
name='web.api.redcap.ignore_missing_subject'),
......
......@@ -57,6 +57,7 @@ def get_holidays(worker, date):
'link_when': start_date,
'link_who': worker.id,
'link_end': end_date,
'kind': holiday.kind
}
result.append(event)
......@@ -246,8 +247,9 @@ def events(request, date):
subject = {
'name': unicode(appointment_subject),
'id': appointment_subject.id,
'appointment_id': appointment.id,
'color': RANDOM_COLORS[i],
'start': appointment.datetime_when.replace(tzinfo=None).strftime("%H:%M:00"),
'start': appointment.datetime_when.replace(tzinfo=None).strftime("%H:%M"),
# this indicates only location of the first appointment
# (there is small chance to have two appointments in two different places at the same day)
'location': str(appointment.location),
......
import datetime
from django.http import JsonResponse
import logging
import json
from django.http import JsonResponse, HttpResponse
from django.utils import timezone
from django.shortcuts import get_object_or_404
from web.models.constants import GLOBAL_STUDY_ID
from web.api_views.daily_planning import get_workers_for_daily_planning, get_availabilities
from ..models import Worker
logger = logging.getLogger(__name__)
def specializations(request):
workers = Worker.objects.filter(specialization__isnull=False).values_list('specialization').distinct()
......@@ -21,14 +24,18 @@ def units(request):
"units": [x[0] for x in workers]
})
def workers_for_daily_planning(request):
start_date = request.GET.get('start_date')
workers = get_workers_for_daily_planning(request)
workers_list_for_json = []
if start_date is not None:
today = timezone.now()
start_date=datetime.datetime.strptime(start_date, '%Y-%m-%d').replace(tzinfo=today.tzinfo)
for worker in workers:
role = unicode(worker.roles.filter(study_id=GLOBAL_STUDY_ID)[0].role)
worker_dict_for_json = {
'id': worker.id,
'availability': worker.availability_percentage(start_date=start_date),
'title': u"{} ({})".format(unicode(worker), role[:1].upper()),
'role': role
}
......@@ -86,3 +93,31 @@ def serialize_worker(worker):
"id": worker.id,
}
return result
def get_worker_availability(request):
start_str_date = request.GET.get("start_date")
end_str_date = request.GET.get("end_date")
worker_id = request.GET.get("worker_id")
if start_str_date is None or worker_id is None:
context = {
'status': '400', 'reason': 'Either start_date, worker_id or both are invalid.'
}
response = HttpResponse(json.dumps(context), content_type='application/json')
response.status_code = 400
return response
start_date = datetime.datetime.strptime(start_str_date, "%Y-%m-%d-%H-%M").replace(tzinfo=timezone.now().tzinfo)
if end_str_date is None or end_str_date == start_str_date:
start_date = start_date.replace(hour=0, minute=0, second=0)
end_date = start_date + datetime.timedelta(days=1)
else:
end_date = datetime.datetime.strptime(end_str_date, "%Y-%m-%d-%H-%M").replace(tzinfo=timezone.now().tzinfo)
worker = get_object_or_404(Worker, id=int(worker_id))
result = {
'start_date': start_date,
'end_date': end_date,
'availability': round(worker.availability_percentage(start_date=start_date, end_date=end_date), 0)
}
return JsonResponse(result)
\ No newline at end of file
# coding=utf-8
import time
def timeit(method):
'''
Debug decorator to measure the execution time of some method or function
'''
def timed(*args, **kw):
ts = time.time()
result = method(*args, **kw)
te = time.time()
if 'log_time' in kw:
name = kw.get('log_name', method.__name__.upper())
kw['log_time'][name] = int((te - ts) * 1000)
else:
print '%r %2.2f ms' % \
(method.__name__, (te - ts) * 1000)
return result
return timed
\ No newline at end of file
......@@ -115,6 +115,10 @@ class AppointmentAddForm(AppointmentForm):
widget=forms.CheckboxSelectMultiple,
queryset=AppointmentType.objects.all(),
)
fields['worker_assigned'].widget.attrs = {'class': 'search_worker_availability'}
fields['datetime_when'].widget.attrs = {'class': 'start_date'}
fields['length'].widget.attrs = {'class': 'appointment_duration'}
self.fields = fields
self.fields['location'].queryset = get_filter_locations(self.user)
......
......@@ -123,6 +123,10 @@ class StatisticsForm(Form):
class AvailabilityAddForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AvailabilityAddForm, self).__init__(*args, **kwargs)
self.fields['person'].widget.attrs['readonly'] = True
available_from = forms.TimeField(label="Available from",
widget=forms.TimeInput(TIMEPICKER_DATE_ATTRS),
initial="8:00",
......@@ -204,6 +208,10 @@ class FlyingTeamEditForm(ModelForm):
class HolidayAddForm(ModelForm):
def __init__(self, *args, **kwargs):
super(HolidayAddForm, self).__init__(*args, **kwargs)
self.fields['person'].widget.attrs['readonly'] = True
datetime_start = forms.DateTimeField(widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS),
initial=datetime.datetime.now().replace(hour=8, minute=0),
)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-02 09:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0118_voucher_activity_type'),
]
operations = [
migrations.AlterField(
model_name='workerstudyrole',
name='role',
field=models.CharField(choices=[(b'DOCTOR', b'Doctor'), (b'NURSE', b'Nurse'), (b'PSYCHOLOGIST', b'Psychologist'), (b'TECHNICIAN', b'Technician'), (b'SECRETARY', b'Secretary'), (b'PROJECT MANAGER', b'Project Manager')], max_length=20, verbose_name=b'Role'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-15 13:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0118_voucher_activity_type'),
]
operations = [
migrations.AlterField(
model_name='studyvisitlist',
name='type',
field=models.CharField(choices=[(b'UNFINISHED', b'Unfinished visits'), (b'APPROACHING_WITHOUT_APPOINTMENTS', b'Approaching visits'), (b'APPROACHING_FOR_MAIL_CONTACT', b'Post mail for approaching visits'), (b'GENERIC', b'Generic visit list'), (b'MISSING_APPOINTMENTS', b'Visits with missing appointments'), (b'EXCEEDED_TIME', b'Exceeded visit time')], max_length=50, verbose_name=b'Type of list'),
),
migrations.AlterField(
model_name='workerstudyrole',
name='role',
field=models.CharField(choices=[(b'DOCTOR', b'Doctor'), (b'NURSE', b'Nurse'), (b'PSYCHOLOGIST', b'Psychologist'), (b'TECHNICIAN', b'Technician'), (b'SECRETARY', b'Secretary'), (b'PROJECT MANAGER', b'Project Manager')], max_length=20, verbose_name=b'Role'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-03 09:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0119_auto_20181002_0908'),
]
operations = [
migrations.AddField(
model_name='holiday',
name='kind',
field=models.CharField(choices=[(b'H', b'Holiday'), (b'X', b'Extra Availability')], default=b'H', max_length=1),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-03 12:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0120_holiday_kind'),
]
operations = [
migrations.AlterField(
model_name='holiday',
name='kind',
field=models.CharField(choices=[(b'H', b'Holiday'), (b'X', b'Extra Availability')], default=b'H', help_text=b'Defines the kind of availability. Either Holiday or Extra Availability.', max_length=1),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-10 12:29
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0121_auto_20181003_1256'),
]
operations = [
migrations.RemoveField(
model_name='worker',
name='appointments',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-17 15:32
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('web', '0119_auto_20181015_1324'),
('web', '0122_remove_worker_appointments'),
]
operations = [
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-10-17 15:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0123_merge_20181017_1532'),
]
operations = [
migrations.AlterField(
model_name='studyvisitlist',
name='type',
field=models.CharField(choices=[(b'UNFINISHED', b'unfinished visits'), (b'APPROACHING_WITHOUT_APPOINTMENTS', b'approaching visits'), (b'APPROACHING_FOR_MAIL_CONTACT', b'post mail for approaching visits'), (b'GENERIC', b'Generic visit list'), (b'MISSING_APPOINTMENTS', b'visits with missing appointments'), (b'EXCEEDED_TIME', b'exceeded visit time')], max_length=50, verbose_name=b'Type of list'),
),
]
......@@ -78,6 +78,14 @@ WEEKDAY_CHOICES = (
(SUNDAY_AS_DAY_OF_WEEK, 'SUNDAY'),
)
AVAILABILITY_HOLIDAY = 'H'
AVAILABILITY_EXTRA = 'X'
AVAILABILITY_CHOICES = (
(AVAILABILITY_HOLIDAY, 'Holiday'),
(AVAILABILITY_EXTRA, 'Extra Availability'),
)
REDCAP_TOKEN_CONFIGURATION_TYPE = "REDCAP_TOKEN_CONFIGURATION_TYPE"
REDCAP_BASE_URL_CONFIGURATION_TYPE = "REDCAP_BASE_URL_CONFIGURATION_TYPE"
......
# coding=utf-8
from django.db import models
from constants import AVAILABILITY_CHOICES, AVAILABILITY_HOLIDAY
class Holiday(models.Model):
class Meta:
......@@ -21,6 +22,8 @@ class Holiday(models.Model):
verbose_name='Comments'
)
kind = models.CharField(max_length=1, choices=AVAILABILITY_CHOICES, default=AVAILABILITY_HOLIDAY, help_text='Defines the kind of availability. Either Holiday or Extra Availability.')
def __str__(self):
return "%s %s" % (self.person.first_name, self.person.last_name)
......
......@@ -29,3 +29,19 @@ class Study(models.Model):
def __unicode__(self):
return "%s" % self.name
@property
def has_voucher_types(self):
return self.columns.voucher_types
@property
def has_vouchers(self):
return self.columns.vouchers
@staticmethod
def get_by_id(study_id):
study = Study.objects.filter(id=study_id)
if len(study) > 0:
return study[0]
else:
return None
\ No newline at end of file
......@@ -2,13 +2,23 @@
import datetime
import logging
from web.utils import get_today_midnight_date
from django.contrib.auth.models import User, AnonymousUser
from django.db import models
from web.models.constants import GLOBAL_STUDY_ID, COUNTRY_OTHER_ID
from web.models.constants import GLOBAL_STUDY_ID, COUNTRY_OTHER_ID, AVAILABILITY_HOLIDAY, AVAILABILITY_EXTRA
from web.models.worker_study_role import STUDY_ROLE_CHOICES, HEALTH_PARTNER_ROLE_CHOICES, \
VOUCHER_PARTNER_ROLE_CHOICES, WORKER_STAFF, WORKER_HEALTH_PARTNER, WORKER_VOUCHER_PARTNER, ROLE_CHOICES
from web.utils import get_weekdays_in_period
from web.officeAvailability import OfficeAvailability
from django.db.models import Q
from web.models.holiday import Holiday
from web.models.availability import Availability
from web.models.appointment import Appointment
from web.models.appointment_type_link import AppointmentTypeLink
logger = logging.getLogger(__name__)
......@@ -61,9 +71,6 @@ class Worker(models.Model):
verbose_name='Locations',
blank=True
)
appointments = models.ManyToManyField('web.Appointment', blank=True,
verbose_name='Appointments'
)
user = models.OneToOneField(User, blank=True, null=True,
verbose_name='Username'
)
......@@ -141,7 +148,8 @@ class Worker(models.Model):
def is_on_leave(self):
if len(self.holiday_set.filter(datetime_end__gt=datetime.datetime.now(),
datetime_start__lt=datetime.datetime.now())):
datetime_start__lt=datetime.datetime.now(),
kind=AVAILABILITY_HOLIDAY)):
return True
return False
......@@ -161,6 +169,48 @@ class Worker(models.Model):
else:
return False
def is_available(self, start_date=None, end_date=None):
self.availability_percentage(start_date=start_date, end_date=end_date) > 50.0
def availability_percentage(self, start_date=None, end_date=None):
'''
start_date: defaults to None and then is set to today's midnight date
end_date: defaults to None and then is set to today's midnight date + 24 hours
'''
today_midnight = get_today_midnight_date()
if start_date is None:
start_date = today_midnight
if end_date is None:
start_date = start_date.replace(hour=0, minute=0, second=0)
end_date = start_date + datetime.timedelta(days=1)
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)
#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')
#Availability
weekdays = get_weekdays_in_period(start_date, end_date)
weekdayQ = Q() #create a filter for each weekday in the selected period
for weekday in weekdays:
weekdayQ = weekdayQ | Q(day_number=weekday)
availabilities = self.availability_set.filter(person=self.id).filter(weekdayQ).order_by('day_number', 'available_from')
things = []
things.extend(availabilities)
things.extend(holidays_and_extra_availabilities)
things.extend(subject_appointments)
things.extend(general_appointments)
for thing in things:
office_availability.consider_this(thing, only_working_hours=True)
return office_availability.get_availability_percentage(only_working_hours=True)
@property
def role(self):
roles = self.roles.filter(study=GLOBAL_STUDY_ID)
......
# coding=utf-8
import datetime
from datetime import timedelta
import logging
import pandas as pd
from web.utils import get_today_midnight_date
from web.models.holiday import Holiday
from web.models.availability import Availability
from web.models.appointment import Appointment
from web.models.appointment_type_link import AppointmentTypeLink
from web.models.constants import AVAILABILITY_EXTRA, AVAILABILITY_HOLIDAY
logger = logging.getLogger(__name__)
#only for plot method
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
class OfficeAvailability(object):
'''
start: datetime-like indicating when the range starts. If none, then today midnight
end: datetime-like indicating when the range ends. If none, then tomorrow midnight
office_start: when the office hours begin
office_end: when the office hours finish
minimum_slot: frequency of the pandas series. T stands of minutes. Docs: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.date_range.html
'''
def __init__(self, name, start=None, end=None, office_start='8:00', office_end='18:00', minimum_slot='1T'):
today_midnight = get_today_midnight_date()
tomorrow_midnight = today_midnight + datetime.timedelta(days=1)
if start is None:
self.start = today_midnight
else:
self.start = start
if end is None:
self.end = tomorrow_midnight
else:
self.end = end
self.name = name
self.office_start = office_start
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))
self.availability = pd.Series(index=self.range, data=0) # initialize range at 0
def _get_duration(self):
'''
Private method. Returns the differ
'''
return self.availability.index[-1] - self.availability.index[0]
def add_availability(self, range, only_working_hours=False):
'''
Receives a pandas date_range `pd.date_range` object.
Sets the availability to one for the specific interval of the provided range.
'''
range = range.round(self.minimum_slot)
if only_working_hours:
range = range.to_series().between_time(self.office_start, self.office_end).index
self.availability[range] = 1
def remove_availability(self, range, only_working_hours=False):
'''
Receives a pandas date_range `pd.date_range` object.
Sets the availability to zero for the specific interval of the provided range.
'''
range = range.round(self.minimum_slot)
if only_working_hours:
range = range.to_series().between_time(self.office_start, self.office_end).index
self.availability[range] = 0
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.
:only_working_hours if true, only consider the defined working hours
Availability repeat every week.
Availability always refers to a moment in which the worker should be working. Never the opposite.
Holiday has higher preference because it refers to extraordinary events like extra availability or lack of availability.
Holiday modifies the status of Availability for specific periods of time.
Only_working_hours: If true changed are limited to the provided working hours.
Known Issues: If the range to be added extends beyond the limits of the given time series range, the call to self.availability[portion.index] = set_to will fail.
It fails because there are keys missing within the time series of the object.
Two solutions are possible:
- First, limit the time periods of the ranges to be considered to the object time space. (current solution)
- Second, extend the object time space.
Notwithstanding, this issue shouldn't exist because in preivous steps we should receive the availabilities queried to the limits of this objects time space.
First proposal should be the solution to consider.
'''
if isinstance(appointment_availability_or_holiday, Availability):
start = appointment_availability_or_holiday.available_from
end = appointment_availability_or_holiday.available_till
weekday = appointment_availability_or_holiday.day_number
logger.debug('Considering Availability from {} to {} for weekday {}'.format(start, end, weekday))
portion = self.availability[self.availability.index.weekday == (weekday-1)].between_time(start,end) #selects the weekdays and then the specific hours
set_to = 1
elif isinstance(appointment_availability_or_holiday, Holiday):
start = appointment_availability_or_holiday.datetime_start
end = appointment_availability_or_holiday.datetime_end
logger.debug('Considering {} from {} to {}'.format('Extra Availability' if appointment_availability_or_holiday.kind == AVAILABILITY_EXTRA else 'Holiday', start, end))
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))
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))
portion = self.availability[pd.date_range(start=start, end=end, freq=self.minimum_slot)] #select the specific range
set_to = 0
else:
logger.error('Expected Availability, Holiday, Appointment or AppointmentTypeLink objects.')
raise TypeError
if only_working_hours:
portion = portion.between_time(self.office_start, self.office_end)
#limit portion to be changed to the bounds of the object time space (solution 1 of the aforementioned problem)
portion = portion[(self.availability.index.min() <= portion.index) & (portion.index <= self.availability.index.max())]
self.availability[portion.index] = set_to
def get_availability_percentage(self, only_working_hours=False):
'''
For multiple values this is the solution: return self.availability.value_counts().div(len(s))[1] * 100
But since it's 0 or 1, this works as well and is faster: return self.availability.mean() * 100
To test it:
import pandas as pd
range = pd.date_range(start='2018-10-1', end='2018-10-2 01:00', freq='5T', closed=None)
s = pd.Series(index=range, data=0)
range2 = pd.date_range(start='2018-10-1 1:00', end='2018-10-1 2:30', freq='5T')
s[range2] = 1
print(s.value_counts().div(len(s))[1]*100) # prints 6.312292358803987
print(s.mean()*100) # prints 6.312292358803987
%timeit s.value_counts().div(len(s))[1]*100 # 504 µs ± 19.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit s.mean()*100 # 56.3 µs ± 1.66 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
'''
if only_working_hours:
availability = self.availability.between_time(self.office_start, self.office_end)
else:
availability = self.availability
return availability.mean() * 100 #better to isolate the operation in case we change it later
def is_available(self, only_working_hours=False):
'''
Returns True if on the selected period is available at least 50% of the time
Otherwise returns False
'''
return self.get_availability_percentage(only_working_hours=only_working_hours) > 50.0
def plot_availability(self):
'''
Plot availability chart.
'''
fig = plt.figure() #create new figure. This should ensure thread safe method
ax=fig.gca() #get current axes
matplotlib.rcParams['hatch.linewidth'] = 1
logger.debug('business_hours: {} {}'.format(self.office_start, self.office_end))
business_hours = self.business_hours = pd.Series(index=self.range, data=0)
mask = business_hours.between_time(self.office_start, self.office_end).index
business_hours[mask] = 1
ax = business_hours.plot(kind='area', alpha = 0.33, color='#1190D8', label='Business Hours', legend=True, ax=ax)
#calculate good xticks
hours = self._get_duration().total_seconds()/3600
n_ticks = int(hours/24)
if n_ticks == 0:
minutes = self._get_duration().total_seconds()/60
n_ticks = int(minutes/60)
if n_ticks == 0:
n_ticks = 1
xticks=self.availability.asfreq('{}T'.format(n_ticks)).index
else:
xticks=self.availability.asfreq('{}H'.format(n_ticks)).index
title = 'Availability for {} from {} to {}'.format(self.name, self.start.strftime('%Y/%m/%d %H:%M'), self.end.strftime('%Y/%m/%d %H:%M'))
ax = self.availability.plot(figsize=(16, 8), grid = True,
title=title, legend=True, label='Availability', color='#00af52',
xticks=xticks, ax=ax, yticks=[0,1])
ax.fill_between(self.availability.index, self.availability.tolist(), facecolor="none", hatch='//', edgecolor="#00af52", alpha=1, linewidth=0.5)
ax.set_axisbelow(True)
ax.yaxis.grid(color='gray', linewidth=0.5, alpha=0)
ax.xaxis.grid(color='gray', linewidth=0.5, alpha=1)
ax.set_yticklabels(['False', 'True'])
ax.set_ylabel('Is Available ?')
ax.set_xlabel('Date & Time')
fig.tight_layout()
fig.savefig('{}_{}_{}.pdf'.format(self.name, self.start.strftime('%Y%m%d%H%M'), self.end.strftime('%Y%m%d%H%M')))