diff --git a/smash/web/api_views/daily_planning.py b/smash/web/api_views/daily_planning.py index ce152cf31c801e3c4c4bb95b0b0056f0fa7f6dd3..e6417df290437a9f77d020bfc992ef7d9eccfc8d 100644 --- a/smash/web/api_views/daily_planning.py +++ b/smash/web/api_views/daily_planning.py @@ -2,11 +2,13 @@ import datetime import json import random +from operator import itemgetter + from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import get_object_or_404 -from ..models import Appointment, AppointmentTypeLink +from ..models import Appointment, AppointmentTypeLink, Worker, Availability, Holiday from ..views.notifications import get_filter_locations # mix_color = (0, 166, 90) @@ -27,6 +29,109 @@ def build_duration(duration): return "{0:02d}:{1:02d}".format(number_of_hours, minutes) +def get_holidays(worker, date): + today_start = datetime.datetime.strptime(date, '%Y-%m-%d').replace(hour=0, minute=0) + today_end = datetime.datetime.strptime(date, '%Y-%m-%d').replace(hour=23, minute=59) + holidays = Holiday.objects.filter(person=worker, datetime_start__lte=today_end, datetime_end__gte=today_start) + result = [] + for holiday in holidays: + minutes = int((holiday.datetime_end - holiday.datetime_start).total_seconds() / 60) + # hack for time zones + start_date = datetime.datetime.combine(today_start, holiday.datetime_start.time()) + end_date = datetime.datetime.combine(today_start, holiday.datetime_end.time()) + event = { + 'duration': build_duration(minutes), + 'link_when': start_date, + 'link_who': worker.id, + 'link_end': end_date, + } + result.append(event) + + return result + + +def remove_holidays(availability, worker, date): + today_start = datetime.datetime.strptime(date, '%Y-%m-%d').replace(hour=0, minute=0) + today_end = datetime.datetime.strptime(date, '%Y-%m-%d').replace(hour=23, minute=59) + holidays_starting_before = Holiday.objects.filter(person=worker, datetime_start__lte=today_start, + datetime_end__gte=today_start) + holidays_starting_today = Holiday.objects.filter(person=worker, datetime_start__lte=today_end, + datetime_start__gte=today_start) + holidays_ending_today = Holiday.objects.filter(person=worker, datetime_end__lte=today_end, + datetime_end__gte=today_start) + result = [] + timestamps = [] + timestamps.append({ + "time": availability.available_from, + "type": "start" + }) + timestamps.append({ + "time": availability.available_till, + "type": "stop" + }) + + direction = -holidays_starting_before.count() + + for holiday in holidays_starting_today: + timestamps.append({ + "time": holiday.datetime_start.time(), + "type": "stop" + }) + for holiday in holidays_ending_today: + timestamps.append({ + "time": holiday.datetime_end.time(), + "type": "start" + }) + + start_date = None + stop_date = None + + sorted_array = sorted(timestamps, key=itemgetter('time')) + + for timestamp in sorted_array: + type = timestamp.get("type", None) + if type == "stop": + direction -= 1 + stop_date = timestamp["time"] + if direction == 0: + result.append(Availability(person=worker, day_number=availability.day_number, + available_from=start_date, available_till=stop_date)) + elif type == "start": + direction += 1 + start_date = timestamp["time"] + else: + raise TypeError("Unknown type: " + str(type)) + if direction > 0: + result.append(Availability(person=worker, day_number=availability.day_number, + available_from=start_date, available_till=today_end.time())) + + return result + + +def get_availabilities(worker, date): + result = [] + today = datetime.datetime.strptime(date, '%Y-%m-%d') + weekday = today.weekday() + 1 + availabilities = Availability.objects.filter(person=worker, day_number=weekday) + + for availability in availabilities: + availabilities_without_holidays = remove_holidays(availability, worker, date) + for availability_without_holiday in availabilities_without_holidays: + start_date = datetime.datetime.combine(today, availability_without_holiday.available_from) + end_date = datetime.datetime.combine(today, availability_without_holiday.available_till) + delta = end_date - start_date + + minutes = int(delta.total_seconds() / 60) + event = { + 'duration': build_duration(minutes), + 'link_when': start_date, + 'link_who': worker.id, + 'link_end': end_date, + } + result.append(event) + return result + + @login_required def events(request, date): appointments = Appointment.objects.filter( @@ -46,6 +151,7 @@ def events(request, date): 'events': [] } subjects[appointment_subject.id] = subject + links = AppointmentTypeLink.objects.filter(appointment=appointment).all() for j, link in enumerate(links): link_when = link.date_when @@ -69,8 +175,20 @@ def events(request, date): } subject_events = subjects[appointment_subject.id]['events'] subject_events.append(event) + + availabilities = [] + holidays = [] + workers = Worker.objects.filter(locations__in=get_filter_locations(request.user)).exclude( + role=Worker.ROLE_CHOICES_SECRETARY).distinct() + + for worker in workers: + availabilities = availabilities + get_availabilities(worker, date) + holidays = holidays + get_holidays(worker, date) + return JsonResponse({ "data": subjects.values(), + 'availabilities': availabilities, + 'holidays': holidays }) diff --git a/smash/web/models/worker.py b/smash/web/models/worker.py index e962487c74522ec5a5ccd94e7373772e14e6f51e..8c5d003691667c88b861ebfb88be49bec14610e6 100644 --- a/smash/web/models/worker.py +++ b/smash/web/models/worker.py @@ -38,8 +38,9 @@ class Worker(models.Model): ) ROLE_CHOICES_SECRETARY = "SECRETARY" + ROLE_CHOICES_DOCTOR = "DOCTOR" ROLE_CHOICES = ( - ('DOCTOR', 'Doctor'), + (ROLE_CHOICES_DOCTOR, 'Doctor'), ('NURSE', 'Nurse'), ('PSYCHOLOGIST', 'Psychologist'), ('TECHNICIAN', 'Technician'), diff --git a/smash/web/static/css/daily_planning.css b/smash/web/static/css/daily_planning.css index 6402f390b6f5fee01bcbd460029f121075caa3f0..0fef5841305f2622e97ed7c698fa8a998a790960 100644 --- a/smash/web/static/css/daily_planning.css +++ b/smash/web/static/css/daily_planning.css @@ -26,4 +26,8 @@ .fc-event { font-size: 1em !important; +} + +.background-event { + z-index: -1 !important; } \ No newline at end of file diff --git a/smash/web/static/js/daily_planning.js b/smash/web/static/js/daily_planning.js index 920909990dcf9d63544d1cc8aa0e62f1175c387e..68f6b33bb26d6543c2fdfc73e9ec566380acd278 100644 --- a/smash/web/static/js/daily_planning.js +++ b/smash/web/static/js/daily_planning.js @@ -1,6 +1,6 @@ const TIME_FORMAT = 'HH:mm'; const FLYING_TEAM_LABEL = 'Flying Team'; -const FLYTING_TEAM_BORDER_COLOR = "orange"; +const FLYING_TEAM_BORDER_COLOR = "orange"; var overlaps = (function () { function getPositions(elem) { @@ -21,6 +21,7 @@ var overlaps = (function () { return function (x, y, element) { var pos1 = [[x, x + 100], [y, y + 20]], pos2 = getPositions(element); + console.log(x, y, element); return comparePositions(pos1[0], pos2[0]) && comparePositions(pos1[1], pos2[1]); }; })(); @@ -34,9 +35,9 @@ function add_event(event, color, subjectId, boxBody) { var borderColor; var location = event.location; if (location === FLYING_TEAM_LABEL) { - eventElement.css('border', '2px solid ' + FLYTING_TEAM_BORDER_COLOR); + eventElement.css('border', '2px solid ' + FLYING_TEAM_BORDER_COLOR); location = event.flying_team_location + ' (FT)'; - borderColor = FLYTING_TEAM_BORDER_COLOR; + borderColor = FLYING_TEAM_BORDER_COLOR; } eventElement.data('event', { appointment_start: event.appointment_start, @@ -83,6 +84,26 @@ function add_event(event, color, subjectId, boxBody) { function get_subjects_events(day) { $.get('/api/events/' + day, function (data) { $("#subjects").empty(); + var availabilities = data.availabilities; + $.each(availabilities, function (index, event) { + event.backgroundColor = '#AAFFAA !important'; + event.start = $.fullCalendar.moment(event.link_when); + event.end = $.fullCalendar.moment(event.link_end); + event.rendering = 'background'; + event.resourceId = event.link_who.toString(); + event.className = 'background-event'; + $('#calendar').fullCalendar('renderEvent', event, true); + }); + var holidays = data.holidays; + $.each(holidays, function (index, event) { + event.backgroundColor = '#FFAAAA !important'; + event.start = $.fullCalendar.moment(event.link_when); + event.end = $.fullCalendar.moment(event.link_end); + event.rendering = 'background'; + event.resourceId = event.link_who.toString(); + event.className = 'background-event'; + $('#calendar').fullCalendar('renderEvent', event, true); + }); var subjects = data.data; $.each(subjects, function (index, subject) { var divSubject = $("<div class='col-md-4 col-lg-3 col-sm-12 subjects-events'/>"); @@ -119,6 +140,7 @@ function get_subjects_events(day) { }); }); } + function remove_event(event) { $('#calendar').fullCalendar('removeEvents', event.id); var selector = '#subject_' + event.subject_id; @@ -130,6 +152,7 @@ function remove_event(event) { eventsCleared.push(event.link_id); add_event(event, event.color, event.subject_id, boxBody); } + $(document).ready(function () { $('#calendar').fullCalendar({ schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives', @@ -138,7 +161,12 @@ $(document).ready(function () { eventStartEditable: true, editable: true, selectable: true, - eventOverlap: false, + eventOverlap: function (stillEvent, movingEvent) { + if (stillEvent.rendering === "background") { + return true; + } + return false; + }, weekends: false, scrollTime: '08:00', slotDuration: '00:30', @@ -223,19 +251,26 @@ $(document).ready(function () { resources: resources_url, events: [], eventRender: function (event, element) { - element.popover({ - title: event.title, - container: 'body', - placement: 'bottom', - trigger: 'click', - content: '<ul>' + - '<li>' + event.subject + '</li>' + - '<li>' + event.start.format(TIME_FORMAT) + ' - ' + event.end.format(TIME_FORMAT) + '</li>' + - '<li>Appointment starts: ' + event.constraint.start.format(TIME_FORMAT) + '</li>' + - '<li>Location: ' + (event.location !== FLYING_TEAM_LABEL ? event.location : event.flying_team_location + ' (FT)') + '</li>' + - '</ul>', - html: true - }); + if (event.rendering !== 'background') { + element.popover({ + title: event.title, + container: 'body', + placement: 'bottom', + trigger: 'click', + content: '<ul>' + + '<li>' + event.subject + '</li>' + + '<li>' + event.start.format(TIME_FORMAT) + ' - ' + event.end.format(TIME_FORMAT) + '</li>' + + '<li>Appointment starts: ' + event.constraint.start.format(TIME_FORMAT) + '</li>' + + '<li>Location: ' + (event.location !== FLYING_TEAM_LABEL ? event.location : event.flying_team_location + ' (FT)') + '</li>' + + '</ul>', + html: true + }); + } else { + + } + }, + selectAllow: function (selectInfo) { + return false; }, eventDragStop: function (event, jsEvent, ui, view) { // var x = isElemOverDiv(jsEvent.clientX, jsEvent.clientY, $('#subjects')); diff --git a/smash/web/tests/test_api_daily_planning.py b/smash/web/tests/test_api_daily_planning.py new file mode 100644 index 0000000000000000000000000000000000000000..83b8e4157d4e11d45f4e099dbabe6c6a21839964 --- /dev/null +++ b/smash/web/tests/test_api_daily_planning.py @@ -0,0 +1,142 @@ +# coding=utf-8 +import datetime +import json + +from django.contrib.auth.models import User +from django.test import Client +from django.test import TestCase +from django.urls import reverse + +from web.models import Worker, Availability, Holiday +from web.models.constants import TUESDAY_AS_DAY_OF_WEEK +from web.tests.functions import create_worker + + +class TestApi(TestCase): + def setUp(self): + self.client = Client() + username = 'piotr' + password = 'top_secret' + self.user = User.objects.create_user( + username=username, email='jacob@bla', password=password) + self.worker = create_worker(self.user, True) + self.worker.role = Worker.ROLE_CHOICES_DOCTOR + self.worker.save() + self.client.login(username=username, password=password) + + def test_empty_availabilities(self): + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(0, len(availabilities)) + + def test_nonempty_availabilities(self): + availability = Availability.objects.create(person=self.worker, day_number=TUESDAY_AS_DAY_OF_WEEK, + available_from="8:00", available_till="16:00") + availability.save() + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(1, len(availabilities)) + + def test_nonempty_availabilities_with_non_overlapping_holidays(self): + availability = Availability.objects.create(person=self.worker, day_number=TUESDAY_AS_DAY_OF_WEEK, + available_from="8:00", available_till="16:00") + availability.save() + + holiday = Holiday.objects.create(person=self.worker, + datetime_start=datetime.datetime.now().replace(day=1, hour=5), + datetime_end=datetime.datetime.now().replace(day=2, hour=20) + ) + holiday.save() + + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(1, len(availabilities)) + + def test_empty_availabilities_due_to_holidays(self): + availability = Availability.objects.create(person=self.worker, day_number=TUESDAY_AS_DAY_OF_WEEK, + available_from="8:00", available_till="16:00") + availability.save() + + holiday = Holiday.objects.create(person=self.worker, + datetime_start=datetime.datetime.now().replace(year=2017, month=9, day=5, + hour=5), + datetime_end=datetime.datetime.now().replace(year=2017, month=9, day=5, + hour=20) + ) + holiday.save() + + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(0, len(availabilities)) + + def test_empty_availabilities_due_to_holidays_2(self): + availability = Availability.objects.create(person=self.worker, day_number=TUESDAY_AS_DAY_OF_WEEK, + available_from="8:00", available_till="16:00") + availability.save() + + holiday = Holiday.objects.create(person=self.worker, + datetime_start=datetime.datetime.now().replace(year=2017, month=9, day=1, + hour=12), + datetime_end=datetime.datetime.now().replace(year=2017, month=9, day=17, + hour=12) + ) + holiday.save() + + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(0, len(availabilities)) + + def test_nonempty_availabilities_with_overlapping_holidays(self): + availability = Availability.objects.create(person=self.worker, day_number=TUESDAY_AS_DAY_OF_WEEK, + available_from="8:00", available_till="16:00") + availability.save() + + holiday = Holiday.objects.create(person=self.worker, + datetime_start=datetime.datetime.now().replace(year=2017, month=9, day=6, + hour=12), + datetime_end=datetime.datetime.now().replace(year=2017, month=9, day=6, + hour=20) + ) + holiday.save() + + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(1, len(availabilities)) + + def test_nonempty_availabilities_with_included_holidays(self): + availability = Availability.objects.create(person=self.worker, day_number=TUESDAY_AS_DAY_OF_WEEK, + available_from="8:00", available_till="16:00") + availability.save() + + holiday = Holiday.objects.create(person=self.worker, + datetime_start=datetime.datetime.now().replace(year=2017, month=9, day=5, + hour=12), + datetime_end=datetime.datetime.now().replace(year=2017, month=9, day=5, + hour=13) + ) + holiday.save() + + response = self.client.get(reverse('web.api.events', kwargs={'date': "2017-09-05"})) + self.assertEqual(response.status_code, 200) + + availabilities = json.loads(response.content)['availabilities'] + + self.assertEqual(2, len(availabilities))