diff --git a/smash/web/forms/study_forms.py b/smash/web/forms/study_forms.py index 916f2316a4ff0c3f41a9f75840ea383a4d668bdb..6e827cf3282821cd482ae0f933e6cf1754a5d903 100644 --- a/smash/web/forms/study_forms.py +++ b/smash/web/forms/study_forms.py @@ -15,7 +15,9 @@ class StudyEditForm(ModelForm): nd_number_study_subject_regex = self.cleaned_data.get( 'nd_number_study_subject_regex') - if StudySubject.check_nd_number_regex(nd_number_study_subject_regex) == False: + instance = getattr(self, 'instance', None) + + if StudySubject.check_nd_number_regex(nd_number_study_subject_regex, instance) == False: raise ValidationError( 'Please enter a valid nd_number_study_subject_regex regex.') diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 8994caff22f7f5833f03626c76baf08253c833fb..0f0c1ddd7859934cee742395134ffb572ec6d2f0 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -208,8 +208,8 @@ class StudySubject(models.Model): return matches + sorted(reminder, reverse=reverse) @staticmethod - def check_nd_number_regex(regex_str): - nd_numbers = StudySubject.objects.all().values_list('nd_number', flat=True) + def check_nd_number_regex(regex_str, study): + nd_numbers = StudySubject.objects.filter(study=study).exclude(nd_number__isnull=True).exclude(nd_number__exact='').all().values_list('nd_number', flat=True) regex = re.compile(regex_str) for nd_number in nd_numbers: if regex.match(nd_number) is None: diff --git a/smash/web/models/visit.py b/smash/web/models/visit.py index 47ac3d8a7287d4bc57395a91e265a1b89f49a69f..c44dc7093ba1c7e265f8bdf962333f46568c5344 100644 --- a/smash/web/models/visit.py +++ b/smash/web/models/visit.py @@ -4,6 +4,7 @@ import datetime from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver +from django.db import transaction from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES_CONTROL @@ -82,16 +83,18 @@ class Visit(models.Model): datetime.timedelta(days=93) ) - @receiver(post_save, sender=Visit) -def update_visit_number(sender, instance, **kwargs): +def check_visit_number(sender, instance, **kwargs): + # no other solution to ensure the visit_number is in cronological order than to sort the whole list if there are future visits visit = instance - if visit.subject is not None: - count = Visit.objects.filter(subject=visit.subject).filter( - datetime_begin__lte=visit.datetime_begin).count() - if count != visit.visit_number: - visit.visit_number = count - visit.save() - subject_visits = Visit.objects.filter(subject=visit.subject).all() - for subject_visit in subject_visits: - update_visit_number(sender, subject_visit) + if visit.subject is not None: #not sure if select_for_update has an effect, the tests work as well without it + visits_before = Visit.objects.select_for_update().filter(subject=visit.subject).filter(datetime_begin__lt=visit.datetime_begin).count() + # we need to sort the future visits respect to this one, if any + visits = Visit.objects.select_for_update().filter(subject=visit.subject).filter(datetime_begin__gte=visit.datetime_begin).order_by('datetime_begin','datetime_end') + with transaction.atomic(): #not sure if it has an effect, the tests work as well without it + for i, v in enumerate(visits): + if v.visit_number != (visits_before + i + 1): + visit_number = (visits_before + i + 1) + Visit.objects.filter(id=v.id).update(visit_number=visit_number) # does not rise post_save, we avoid recursion + if v.id == visit.id: + visit.visit_number = v.visit_number diff --git a/smash/web/models/worker.py b/smash/web/models/worker.py index 51eb71c9422360f2774d0d362dac77ac7190eb9a..e663853509ff97088272a6e9c7cef3debecf3958 100644 --- a/smash/web/models/worker.py +++ b/smash/web/models/worker.py @@ -154,6 +154,15 @@ class Worker(models.Model): return True return False + def current_leave_details(self): + holidays = self.holiday_set.filter(datetime_end__gt=datetime.datetime.now(), + datetime_start__lt=datetime.datetime.now(), + kind=AVAILABILITY_HOLIDAY).order_by('-datetime_end') + if len(holidays) > 0: + return holidays[0] + else: + return None + def disable(self): if self.user is not None: self.user.is_active = False diff --git a/smash/web/static/js/daily_planning.js b/smash/web/static/js/daily_planning.js index 2bb9341335a17d58920a792e01b5a56c0834dcad..1c02d38cb2652d73ebe3cee1c847d4bb2cb7e873 100644 --- a/smash/web/static/js/daily_planning.js +++ b/smash/web/static/js/daily_planning.js @@ -47,12 +47,12 @@ function add_event(event, color, subjectId, locationId, boxBody) { if (event_title === undefined || event_title === "") { event_title = event.title; } - eventElement.data('event', { + + event_data = { appointment_start: event.appointment_start, appointment_end: event.appointment_end, title: event_title, stick: true, - color: color + " !important", duration: event.duration, original_duration: event.duration, subject: event.subject, @@ -70,8 +70,13 @@ function add_event(event, color, subjectId, locationId, boxBody) { end: $.fullCalendar.moment(event.appointment_end) }, borderColor: borderColor + } - }); + if(color != undefined){ + event_data['color'] = color + " !important"; + } + + eventElement.data('event', event_data); eventElement.draggable({ zIndex: 999, revert: true, @@ -227,6 +232,10 @@ function get_subjects_events(day) { } function remove_event(event) { + if(event.className.includes("background-event")){ //avoid removing availabilities + return; + } + $('#calendar').fullCalendar('removeEvents', event.id); var selector; if (event.subject_id !== undefined) { @@ -239,7 +248,10 @@ function remove_event(event) { event.duration = event.original_duration; event.removed = true; //remove !important - event.color = event.color.substring(0, 7); + if(event.color != undefined){ + event.color = event.color.substring(0, 7); + } + if (event.link_id !== undefined) { eventsCleared.push(event.link_id); } else { @@ -248,148 +260,146 @@ function remove_event(event) { add_event(event, event.color, event.subject_id, event.location_id, boxBody); } -$(document).ready(function () { - $('#calendar').fullCalendar({ +function addDailyPlanningCalendar(calendar_selector, replace_all, calendar_dict_props){ + + var customButtons = { + datePickerButton: { + text: 'select', + click: function () { + var $btnCustom = $('.fc-datePickerButton-button'); + if ($(".calendar-datepicker").length > 0) { + $(".calendar-datepicker").remove(); + } + else { + $btnCustom.after('<div class="calendar-datepicker"/>'); + $(".calendar-datepicker").datepicker().on('changeDate', function (ev) { + $(calendar_selector).fullCalendar('gotoDate', ev.date); + $(".calendar-datepicker").remove(); + }); + } + } + }, + save: { + text: 'Save', + click: function () { + calendarEvents = $(calendar_selector).fullCalendar('clientEvents'); + eventsToPersist = []; + var saveButton = $(".fc-save-button"); + var currentBorder = saveButton.css('border-color'); + $.each(calendarEvents, function (i, calendar_event) { + if (calendar_event.rendering !== "background") { + eventsToPersist.push({ + 'link_id': calendar_event.link_id, + 'appointment_id': calendar_event.appointment_id, + 'link_who': parseInt(calendar_event.resourceId), + 'start': calendar_event.start.format() + }); + if (calendar_event.link_id !== undefined) { + var index = eventsCleared.indexOf(calendar_event.link_id); + if (index > -1) { + eventsCleared.splice(index, 1); + } + } else { + var index = appointmentsCleared.indexOf(calendar_event.appointment_id); + if (index > -1) { + appointmentsCleared.splice(index, 1); + } + } + } + }); + $.post({ + url: events_url, + data: { + events_to_persist: JSON.stringify(eventsToPersist), + events_to_clear: JSON.stringify(eventsCleared), + appointments_to_clear: JSON.stringify(appointmentsCleared) + }, + dataType: "json" + }).done(function (data) { + + saveButton.css('border-color', 'green'); + setTimeout(function () { + saveButton.css('border-color', currentBorder); + }, 200); + }).error(function (data) { + + console.log(data); + saveButton.css('border-color', 'red'); + showErrorInfo("There was an unexpected problem with saving data. " + "Please contact administrators."); + setTimeout(function () { + saveButton.delay(200).css('border-color', currentBorder); + }, 200); + }); + } + }, + clear: { + text: 'Clear', + click: function () { + calendarEvents = $(calendar_selector).fullCalendar('clientEvents'); + $.each(calendarEvents, function (i, calendar_event) { + remove_event(calendar_event); + }); + } + }, + toPdf: { + text: 'PDF', + click: function () { + var srcEl = document.getElementById("calendar"); + var parent = srcEl.parentNode; + var container = document.createElement("div"); + container.style.backgroundColor = "#FFFFFF"; + container.style.width = "3840px"; + document.body.appendChild(container); + container.appendChild(srcEl); + + var pdf = new jsPDF('l', 'mm', [1485, 270]); + pdf.addHTML(container, function () { + pdf.save('daily-planning.pdf'); + parent.appendChild(srcEl); + document.body.removeChild(container); + }); + } + } + }; + + var default_properties = { schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives', defaultView: 'agendaDay', eventDurationEditable: false, eventStartEditable: true, editable: true, selectable: true, - eventOverlap: function (stillEvent, movingEvent) { - if (stillEvent.rendering === "background") { - return true; - } - return false; - }, + droppable: true, weekends: false, + selectHelper: true, + groupByResource: true, + displayEventTime: true, scrollTime: '08:00', slotDuration: '00:30', snapDuration: '00:05', - displayEventTime: true, - resourceOrder: 'role', - resourceGroupField: 'role', - dragRevertDuration: 0, minTime: "08:00:00", maxTime: "19:00:00", - groupByResource: true, - height: "auto", - customButtons: { - datePickerButton: { - text: 'select', - click: function () { - var $btnCustom = $('.fc-datePickerButton-button'); - if ($(".calendar-datepicker").length > 0) { - $(".calendar-datepicker").remove(); - } - else { - $btnCustom.after('<div class="calendar-datepicker"/>'); - $(".calendar-datepicker").datepicker().on('changeDate', function (ev) { - $('#calendar').fullCalendar('gotoDate', ev.date); - $(".calendar-datepicker").remove(); - }); - } - } - }, - save: { - text: 'Save', - click: function () { - calendarEvents = $('#calendar').fullCalendar('clientEvents'); - eventsToPersist = []; - var saveButton = $(".fc-save-button"); - var currentBorder = saveButton.css('border-color'); - $.each(calendarEvents, function (i, calendar_event) { - if (calendar_event.rendering !== "background") { - eventsToPersist.push({ - 'link_id': calendar_event.link_id, - 'appointment_id': calendar_event.appointment_id, - 'link_who': parseInt(calendar_event.resourceId), - 'start': calendar_event.start.format() - }); - if (calendar_event.link_id !== undefined) { - var index = eventsCleared.indexOf(calendar_event.link_id); - if (index > -1) { - eventsCleared.splice(index, 1); - } - } else { - var index = appointmentsCleared.indexOf(calendar_event.appointment_id); - if (index > -1) { - appointmentsCleared.splice(index, 1); - } - } - } - }); - $.post({ - url: events_url, - data: { - events_to_persist: JSON.stringify(eventsToPersist), - events_to_clear: JSON.stringify(eventsCleared), - appointments_to_clear: JSON.stringify(appointmentsCleared) - }, - dataType: "json" - }).done(function (data) { - - saveButton.css('border-color', 'green'); - setTimeout(function () { - saveButton.css('border-color', currentBorder); - }, 200); - }).error(function (data) { - - console.log(data); - saveButton.css('border-color', 'red'); - showErrorInfo("There was an unexpected problem with saving data. " + - "Please contact administrators."); - setTimeout(function () { - saveButton.delay(200).css('border-color', currentBorder); - }, 200); - }); - } - }, - clear: { - text: 'Clear', - click: function () { - calendarEvents = $('#calendar').fullCalendar('clientEvents'); - $.each(calendarEvents, function (i, calendar_event) { - remove_event(calendar_event); - }); - } - }, - toPdf: { - text: 'PDF', - click: function () { - var srcEl = document.getElementById("calendar"); - var parent = srcEl.parentNode; - var container = document.createElement("div"); - container.style.backgroundColor = "#FFFFFF"; - container.style.width = "3840px"; - document.body.appendChild(container); - container.appendChild(srcEl); - - var pdf = new jsPDF('l', 'mm', [1485, 270]); - pdf.addHTML(container, function () { - pdf.save('daily-planning.pdf'); - parent.appendChild(srcEl); - document.body.removeChild(container); - }); - } - } - - }, - viewRender: function (view, element) { - var date = view.start.format('YYYY-MM-DD'); - $('#calendar').fullCalendar('removeEvents'); - get_subjects_events(date); - }, businessHours: { start: '08:00', end: '19:00' }, + dragRevertDuration: 0, + height: "auto", + customButtons: customButtons, header: { left: 'prev,next today', center: 'title, datePickerButton', right: 'save, clear, toPdf' }, - droppable: true, + // VIEW + viewRender: function (view, element) { + var date = view.start.format('YYYY-MM-DD'); + $(calendar_selector).fullCalendar('removeEvents'); + get_subjects_events(date); + }, + // RESOURCES + resourceOrder: 'role', + resourceGroupField: 'role', resourceAreaWidth: '15%', resourceLabelText: 'Workers', refetchResourcesOnNavigate: true, @@ -421,7 +431,7 @@ $(document).ready(function () { }, resources: function(callback){ setTimeout(function(){ - var view = $('#calendar').fullCalendar('getView'); + var view = $(calendar_selector).fullCalendar('getView'); $.ajax({ url: resources_url, type: 'GET', @@ -431,13 +441,14 @@ $(document).ready(function () { } }).then(function(resources){ //Filter out roles - var checked_roles = $('.role_list_item > input:checked').map( (i,e) => e.value).toArray(); - resources = resources.filter(resource => checked_roles.includes(resource.role)); - callback(resources) + if($('.role_list_item > input').length > 0){ + var checked_roles = $('.role_list_item > input:checked').map( (i,e) => e.value).toArray(); + resources = resources.filter(resource => checked_roles.includes(resource.role)); + } + callback(resources); }); }, 0); }, - events: [], eventRender: function (event, element) { if (event.rendering !== 'background') { var content = @@ -457,6 +468,13 @@ $(document).ready(function () { } else { } }, + // EVENTS + eventOverlap: function (stillEvent, movingEvent) { + if (stillEvent.rendering === "background") { + return true; + } + return false; + }, selectAllow: function (selectInfo) { return false; }, @@ -471,7 +489,6 @@ $(document).ready(function () { eventDragStart: function (event, jsEvent, view) { $('.popover').popover('hide'); }, - selectHelper: true, drop: function (date, jsEvent, ui, resourceId) { $(this).remove(); }, @@ -482,9 +499,20 @@ $(document).ready(function () { }; setTimeout(function() {resizeCalendarColumns()}, 100); } - }); -}) -; + }; + + // REPLACE DEFAULT PROPERTIES + if(replace_all){ + default_properties = calendar_dict_props; + }else{ + for (var key in calendar_dict_props) { + default_properties[key] = calendar_dict_props[key]; + } + } + + $(calendar_selector).fullCalendar(default_properties); + +} //RESIZE COLUMNS AND ENABLE HORIZONTAL SCROLL function resizeCalendarColumns(){ diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index e389daee42ad83181e835a655e285564f0fd49de..7dd4a0ef7b7ecf6a8f0045b6c9948bc150a92f46 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -277,7 +277,7 @@ desired effect {% block footer %} <!-- To the right --> <div class="pull-right hidden-xs"> - Version: <strong>0.12.0</strong> (6 Nov 2018) + Version: <strong>0.12.1</strong> (13 Nov 2018) </div> <!-- Default to the left --> @@ -386,6 +386,10 @@ desired effect var activate = function (page_to_activate) { var $e = $(".sidebar-menu li[data-desc='" + page_to_activate + "']"); $e.addClass("active"); + if($($e).parents('li[data-desc]').length > 0){ //if there is a parent, it should also be active + $($e).parents('li[data-desc]').addClass("active"); + } + }; activate({% block ui_active_tab %}{% endblock ui_active_tab %}); diff --git a/smash/web/templates/appointments/add.html b/smash/web/templates/appointments/add.html index bf87695b3f1f00caf74b6db18777f2ec762445be..9fc04985e5e152a2daf9db4790479f9bff949f36 100644 --- a/smash/web/templates/appointments/add.html +++ b/smash/web/templates/appointments/add.html @@ -4,12 +4,16 @@ {% block styles %} {{ block.super }} + <!-- DataTables --> <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> <!-- fullCalendar 2.2.5--> <link rel="stylesheet" href="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar.min.css' %}"> <link rel="stylesheet" href="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar.print.css' %}" media="print"> + <link rel="stylesheet" href="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar_custom.print.css' %}" media="print" /> + <link rel="stylesheet" href="{% static 'fullcalendar-scheduler/scheduler.min.css' %}"> + <link rel="stylesheet" href="{% static 'css/daily_planning.css' %}"> {% include "includes/datetimepicker.css.html" %} <link rel="stylesheet" href="{% static 'css/appointment.css' %}"> @@ -18,6 +22,17 @@ text-align: center !important; margin: 0; } + .tooltip_image{ + width: 20px !important; + margin: 5px; + } + .subject_title{ + padding: 0 !important; + } + .subject_title + .tooltip > .tooltip-inner, .column_title + .tooltip > .tooltip-inner { + background-color: #fafafa; + border: solid 1px #ccc; + } </style> {% endblock styles %} @@ -47,49 +62,61 @@ New appointment for visit from {{visit_start}} to {{visit_end}} <form method="post" action="" class="form-horizontal"> {% csrf_token %} - <div class="box-body"> - <div class="col-sm-6"> - {% for field in form %} - <div class="form-group {% if field.errors %}has-error{% endif %} {% if field|is_checkbox %}multi-checkboxes{% endif %}"> - <label class="col-sm-4 control-label"> - {{ field.label }} - </label> - - <div class="col-sm-8"> - {{ field|add_class:'form-control' }} + <div class="row"> + <div class="box-body"> + <div class="col-md-6"> + {% for field in form %} + <div class="form-group {% if field.errors %}has-error{% endif %} {% if field|is_checkbox %}multi-checkboxes{% endif %}"> + <label class="col-sm-4 control-label"> + {{ field.label }} + </label> + + <div class="col-sm-8"> + {{ field|add_class:'form-control' }} + </div> + + {% if field.errors %} + <span class="help-block"> + {{ field.errors }} + </span> + {% endif %} + </div> + {% endfor %} + </div> + <div class="col-md-6"> + <div class="box box-primary"> + <div class="box-body no-padding"> + <div id="side_calendar"></div> </div> - - {% if field.errors %} - <span class="help-block"> - {{ field.errors }} - </span> - {% endif %} - </div> - {% endfor %} - </div> - <div class="col-md-6"> - <div class="box box-primary"> - <div class="box-body no-padding"> - <div id="calendar"></div> </div> </div> - </div> - </div><!-- /.box-body --> - - - <div class="box-footer"> - <div class="col-sm-6"> - <button type="submit" class="btn btn-block btn-success">Add</button> - </div> - <div class="col-sm-6"> - <a href="{% url 'web.views.visits' %}" class="btn btn-block btn-default">Cancel</a> - </div> - </div><!-- /.box-footer --> + </div><!-- /.box-body --> + </div> + <div class="row"> + <div class="box-footer"> + <div class="col-sm-6"> + <button type="submit" class="btn btn-block btn-success">Add</button> + </div> + <div class="col-sm-6"> + <a href="{% url 'web.views.visits' %}" class="btn btn-block btn-default">Cancel</a> + </div> + </div><!-- /.box-footer --> + </div> </form> </div> + <div class="row"> + <div class="col-md-12"> + <div class="box box-primary"> + <div class="box-body no-padding"> + <div id="calendar"></div> + </div> + </div> + </div> + </div> + {% endblock %} @@ -101,8 +128,9 @@ New appointment for visit from {{visit_start}} to {{visit_end}} <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script> <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> <script src="{% static 'AdminLTE/plugins/moment.js/moment.min.js' %}"></script> - <script src="{% static 'AdminLTE/plugins/fullcalendar/fullcalendar.min.js' %}"></script> <script src="{% static 'js/appointment.js' %}"></script> + <script src="{% static 'fullcalendar-scheduler/lib/fullcalendar.min.js' %}"></script> + <script src="{% static 'fullcalendar-scheduler/scheduler.min.js' %}"></script> <script> $(function () { $('#table').DataTable({ @@ -113,14 +141,16 @@ New appointment for visit from {{visit_start}} to {{visit_end}} "info": true, "autoWidth": false }); - $('#calendar').fullCalendar({ - defaultDate: moment('{{visit_start}}'), + //SIDE CALENDAR + addDailyPlanningCalendar('#side_calendar', true, { + schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives', + defaultDate: moment.max(moment('{{visit_start}}'), moment()), + editable: false, header: { left: 'prev,next today', center: 'title', right: 'month,agendaWeek' }, - editable: false, dayClick: function (date, jsEvent, view) { var dateString = date.format(); if (dateString.indexOf("T") >= 0) { @@ -129,6 +159,7 @@ New appointment for visit from {{visit_start}} to {{visit_end}} dateString = dateString + " 09:00"; } document.getElementById("id_datetime_when").value = dateString; + $('#id_datetime_when').change(); getWorkerAvailability(); }, eventClick: function (calEvent, jsEvent, view) { @@ -214,7 +245,35 @@ New appointment for visit from {{visit_start}} to {{visit_end}} } $('select.search_worker_availability, input[name="datetime_when"], input[name="length"]').on("change", getWorkerAvailability); + + $("#id_datetime_when").on("change paste keyup", function() { + var date = $("#id_datetime_when").val(); + date = moment(date); + $('#calendar').fullCalendar('gotoDate', date); + }); + + var resources_url = '{% url 'web.api.workers.daily_planning' %}'; + var events_url = '{% url 'web.api.events_persist' %}'; + + $(document).ready(function () { + addDailyPlanningCalendar('#calendar', false, { + eventDurationEditable: false, + eventStartEditable: false, + editable: false, + selectable: false, + droppable: false, + customButtons: {}, + header: { + left: '', + center: 'title', + right: '' + } + }); + }); + </script> {% include "includes/datetimepicker.js.html" %} + <script src="{% static 'js/daily_planning.js' %}"></script> + {% endblock scripts %} diff --git a/smash/web/templates/configuration/breadcrumb.html b/smash/web/templates/configuration/breadcrumb.html index 236d1bc335add0ec4427809a9c3c7e94a6490f26..737ff03042e6505dee855e4cd14da66fc9a5937e 100644 --- a/smash/web/templates/configuration/breadcrumb.html +++ b/smash/web/templates/configuration/breadcrumb.html @@ -1,4 +1,5 @@ <li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +<li>Configuration</li> <li class="active"> - <a href="{% url 'web.views.configuration' %}">Configuration</a> + <a href="{% url 'web.views.configuration' %}">General</a> </li> diff --git a/smash/web/templates/configuration/index.html b/smash/web/templates/configuration/index.html index 7330cbc831b41c140a70a67eeeeb3aa1354305fc..d5580458239aeb03c3cd24323d5be81f70cfc513 100644 --- a/smash/web/templates/configuration/index.html +++ b/smash/web/templates/configuration/index.html @@ -9,7 +9,7 @@ {% endblock styles %} -{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block ui_active_tab %}'general_conf'{% endblock ui_active_tab %} {% block page_header %}Configuration{% endblock page_header %} {% block page_description %}{% endblock page_description %} diff --git a/smash/web/templates/daily_planning.html b/smash/web/templates/daily_planning.html index 63b552701589c09f7ce5b04d23c9e827277bc131..4d6cc2058df16128406951a76d9223d55730fbeb 100644 --- a/smash/web/templates/daily_planning.html +++ b/smash/web/templates/daily_planning.html @@ -85,6 +85,10 @@ $('#calendar').fullCalendar('refetchResources'); } + $(document).ready(function () { + addDailyPlanningCalendar('#calendar', false, {}); + }); + </script> {% include "includes/datepicker.js.html" %} <script src="{% static 'js/daily_planning.js' %}"></script> diff --git a/smash/web/templates/doctors/breadcrumb.html b/smash/web/templates/doctors/breadcrumb.html index 6d111dd545adc0d422fe4dfae996671af3c8f657..391f87d887db27dadfd5b2a7f390967d920769fd 100644 --- a/smash/web/templates/doctors/breadcrumb.html +++ b/smash/web/templates/doctors/breadcrumb.html @@ -1,2 +1,7 @@ <li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +{% if worker_type == 'HEALTH_PARTNER' %} +<li>Configuration</li> +{% elif worker_type == 'VOUCHER_PARTNER' %} +<li>Configuration</li> +{% endif %} <li class="active"><a href="{% url 'web.views.workers' %}">Workers</a></li> \ No newline at end of file diff --git a/smash/web/templates/doctors/index.html b/smash/web/templates/doctors/index.html index 1accfd94d238279684ff6d4fe11d6085b05976b8..e1b0fef9d130e9837600cea02f05ca0e849de70d 100644 --- a/smash/web/templates/doctors/index.html +++ b/smash/web/templates/doctors/index.html @@ -5,10 +5,34 @@ {{ block.super }} <!-- DataTables --> <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> + <style type="text/css"> + .no_leave{ + height: 100%; + line-height: 2.5; + text-align: center; + background-color: #00a65b; + } + .on_leave{ + height: 100%; + line-height: 2.5; + text-align: center; + background-color: #dd4b39; + } + </style> {% endblock styles %} -{% block ui_active_tab %}'workers'{% endblock ui_active_tab %} +{% block ui_active_tab %} +{% if worker_type == 'STAFF' %} +'workers' +{% elif worker_type == 'HEALTH_PARTNER' %} +'health_partners' +{% elif worker_type == 'VOUCHER_PARTNER' %} +'voucher_partners' +{% endif %} +{% endblock ui_active_tab %} + {% block page_header %} + {% if worker_type == 'STAFF' %} Workers {% elif worker_type == 'HEALTH_PARTNER' %} @@ -16,6 +40,7 @@ {% elif worker_type == 'VOUCHER_PARTNER' %} Voucher Partners {% endif %} + {% endblock page_header %} {% block breadcrumb %} {% include "doctors/breadcrumb.html" %} @@ -53,7 +78,7 @@ <th>Languages</th> <th>Details</th> {% if worker_type == 'STAFF' %} - <th>On leave</th> + <th>At work</th> <th>Disabled</th> {% endif %} </tr> @@ -82,9 +107,9 @@ {% if worker_type == 'STAFF' %} <td> {% if worker.is_on_leave %} - <button type="button" class="btn btn-block btn-danger">YES</button> + <div title="Worker is on leave until: {{ worker.current_leave_details.datetime_end }}" class="on_leave">✘</div> {% else %} - <button type="button" class="btn btn-block btn-success">NO</button> + <div title="Worker is not on leave" class="no_leave">✔</div> {% endif %} </td> <td> @@ -120,5 +145,7 @@ "autoWidth": false }); }); + + $('.no_leave, .on_leave').tooltip(); </script> {% endblock scripts %} diff --git a/smash/web/templates/languages/breadcrumb.html b/smash/web/templates/languages/breadcrumb.html index b3194bfb0a32794c726a75682425d7ea4b17f30d..6e78554facad42d8816fb4f9dd84e0a8f5dd3535 100644 --- a/smash/web/templates/languages/breadcrumb.html +++ b/smash/web/templates/languages/breadcrumb.html @@ -1,2 +1,5 @@ <li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> -<li class="active"><a href="{% url 'web.views.languages' %}">Languages</a></li> \ No newline at end of file +<li>Configuration</li> +<li class="active"> + <a href="{% url 'web.views.languages' %}">Languages</a> +</li> \ No newline at end of file diff --git a/smash/web/templates/languages/list.html b/smash/web/templates/languages/list.html index eeb9eb68a3fff742857578282926129f210be213..33a5e5178c7c4b9355776bb5f588e0d2b76c64a3 100644 --- a/smash/web/templates/languages/list.html +++ b/smash/web/templates/languages/list.html @@ -7,7 +7,7 @@ <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> {% endblock styles %} -{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block ui_active_tab %}'languages'{% endblock ui_active_tab %} {% block page_header %}Languages{% endblock page_header %} {% block page_description %}{% endblock page_description %} diff --git a/smash/web/templates/study/breadcrumb.html b/smash/web/templates/study/breadcrumb.html new file mode 100644 index 0000000000000000000000000000000000000000..f6052818f23962928e0f9d250f8b163cbb5c7747 --- /dev/null +++ b/smash/web/templates/study/breadcrumb.html @@ -0,0 +1,3 @@ +<li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +<li>Configuration</li> +<li class="active"><a href="{% url 'web.views.edit_study' study_id %}">Study</a></li> \ No newline at end of file diff --git a/smash/web/templates/study/edit.html b/smash/web/templates/study/edit.html index e1e8d594d914cae156ec0bbac6bb0b134b9e488b..760f930b8d2338ac28c97dc5a9a3aa825bdee8e2 100644 --- a/smash/web/templates/study/edit.html +++ b/smash/web/templates/study/edit.html @@ -16,14 +16,14 @@ {% include "includes/datetimepicker.css.html" %} {% endblock styles %} -{% block ui_active_tab %}'subjects'{% endblock ui_active_tab %} +{% block ui_active_tab %}'study_conf'{% endblock ui_active_tab %} {% block page_header %}Edit subject{% endblock page_header %} {% block page_description %}{% endblock page_description %} {% block title %}{{ block.super }} - Edit subject information{% endblock %} {% block breadcrumb %} - {% include "subjects/breadcrumb.html" %} + {% include "study/breadcrumb.html" %} {% endblock breadcrumb %} {% block maincontent %} diff --git a/smash/web/templates/voucher_types/breadcrumb.html b/smash/web/templates/voucher_types/breadcrumb.html index 844971e607ceace28009bea2b53bf6ccd423ba0d..d3fe1508ae40cd59514f2c828129472bb89bd6b1 100644 --- a/smash/web/templates/voucher_types/breadcrumb.html +++ b/smash/web/templates/voucher_types/breadcrumb.html @@ -1,2 +1,3 @@ <li><a href="{% url 'web.views.appointments' %}"><i class="fa fa-dashboard"></i> Dashboard</a></li> +<li>Configuration</li> <li class="active"><a href="{% url 'web.views.voucher_types' %}">Voucher types</a></li> \ No newline at end of file diff --git a/smash/web/templates/voucher_types/list.html b/smash/web/templates/voucher_types/list.html index dec5cde7cb9c8db00538232581c1ebc6d11bb42a..22ae47336625cf36d56ebf8d41793e45e0956cb2 100644 --- a/smash/web/templates/voucher_types/list.html +++ b/smash/web/templates/voucher_types/list.html @@ -7,7 +7,7 @@ <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> {% endblock styles %} -{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %} +{% block ui_active_tab %}'voucher_types'{% endblock ui_active_tab %} {% block page_header %}Voucher types{% endblock page_header %} {% block page_description %}{% endblock page_description %} diff --git a/smash/web/tests/forms/test_study_forms.py b/smash/web/tests/forms/test_study_forms.py index a150a83c181cee62dcf894648ed7d5bb797a0279..6893bce69694866e8da3838acf419365776663a8 100644 --- a/smash/web/tests/forms/test_study_forms.py +++ b/smash/web/tests/forms/test_study_forms.py @@ -1,6 +1,6 @@ from django.test import TestCase from django.forms import ValidationError -from web.tests.functions import create_study_subject +from web.tests.functions import get_test_study, create_study_subject from web.forms.study_forms import StudyEditForm from web.models.study import Study from web.models.study_subject import StudySubject @@ -12,6 +12,7 @@ class StudyFormTests(TestCase): StudySubject.objects.all().delete() create_study_subject(nd_number='ND0001') form = StudyEditForm() + form.instance = get_test_study() # set default regex nd_number_study_subject_regex_default = Study._meta.get_field( 'nd_number_study_subject_regex').get_default() @@ -21,6 +22,7 @@ class StudyFormTests(TestCase): == nd_number_study_subject_regex_default) # test wrong regex form = StudyEditForm() + form.instance = get_test_study() nd_number_study_subject_regex_default = r'^nd\d{5}$' form.cleaned_data = { 'nd_number_study_subject_regex': nd_number_study_subject_regex_default} @@ -32,6 +34,7 @@ class StudyFormTests(TestCase): # this will add a studysubject with a NDnumber create_study_subject(nd_number='nd00001') form = StudyEditForm() + form.instance = get_test_study() # test new regex nd_number_study_subject_regex_default = r'^nd\d{5}$' form.cleaned_data = { diff --git a/smash/web/tests/models/test_study_subject.py b/smash/web/tests/models/test_study_subject.py index a3506d3be75b9b4ebbf35414b4ee763628300d25..324702f6dcde4d9be69974cdb77bf03f145122e1 100644 --- a/smash/web/tests/models/test_study_subject.py +++ b/smash/web/tests/models/test_study_subject.py @@ -3,7 +3,7 @@ from django.test import TestCase from web.models import Appointment from web.models import Visit from web.models import StudySubject, Study -from web.tests.functions import create_study_subject, create_appointment, create_study_subject_with_multiple_screening_numbers +from web.tests.functions import get_test_study, create_study_subject, create_appointment, create_study_subject_with_multiple_screening_numbers from web.tests.functions import create_visit @@ -72,24 +72,24 @@ class SubjectModelTests(TestCase): Appointment.APPOINTMENT_STATUS_CANCELLED, appointment_status) def test_check_nd_number_regex(self): + study = get_test_study() + # delete everything StudySubject.objects.all().delete() # get default regex nd_number_study_subject_regex_default = Study._meta.get_field( 'nd_number_study_subject_regex').get_default() - self.assertTrue(StudySubject.check_nd_number_regex( - nd_number_study_subject_regex_default)) + + self.assertTrue(StudySubject.check_nd_number_regex(nd_number_study_subject_regex_default, study)) # this will add a studysubject with a NDnumber study_subject = create_study_subject(nd_number='ND0001') - self.assertTrue(StudySubject.check_nd_number_regex( - nd_number_study_subject_regex_default)) + self.assertTrue(StudySubject.check_nd_number_regex(nd_number_study_subject_regex_default, study)) # delete everything StudySubject.objects.all().delete() # this will add a studysubject with a NDnumber create_study_subject(nd_number='ND00001') - self.assertFalse(StudySubject.check_nd_number_regex( - nd_number_study_subject_regex_default)) + self.assertFalse(StudySubject.check_nd_number_regex(nd_number_study_subject_regex_default, study)) def test_sort_matched_screening_first(self): diff --git a/smash/web/tests/models/test_visit.py b/smash/web/tests/models/test_visit.py index a247d83886c073d3d40d4318949b98817dc7924a..5ccc870c2f3f68fbc92dc2cbf5defdcc26792906 100644 --- a/smash/web/tests/models/test_visit.py +++ b/smash/web/tests/models/test_visit.py @@ -5,9 +5,52 @@ from django.test import TestCase from web.models import Visit from web.models.constants import SUBJECT_TYPE_CHOICES_PATIENT from web.tests.functions import create_study_subject, create_visit +from web.utils import get_today_midnight_date +import logging +logger = logging.getLogger(__name__) class VisitModelTests(TestCase): + def test_so_called_no_concurrency(self): + subject = create_study_subject() + Visit.objects.filter(subject=subject).all().delete() + visit1 = create_visit(subject=subject) + visit1.datetime_end = get_today_midnight_date() + datetime.timedelta(days=1) + visit1.save() + self.assertEquals(1, visit1.visit_number) + visit2 = create_visit(subject=subject) + visit2.datetime_end = get_today_midnight_date() + datetime.timedelta(days=2) + visit2.save() + visit2.refresh_from_db() + self.assertEquals(2, visit2.visit_number) + + def test_socalled_concurrency(self): + subject = create_study_subject() + Visit.objects.filter(subject=subject).all().delete() + visit1 = create_visit(subject=subject) #post save will be raised + visit2 = create_visit(subject=subject) #post save will be raised + + visit1.datetime_end = get_today_midnight_date() + datetime.timedelta(days=1) + visit2.datetime_end = get_today_midnight_date() + datetime.timedelta(days=2) + + visit1.save() #post save will be raised + visit2.save() #post save will be raised + + visit1.refresh_from_db() + visit2.refresh_from_db() + self.assertEquals(1, visit1.visit_number) + self.assertEquals(2, visit2.visit_number) + + datetime_begin = get_today_midnight_date() - datetime.timedelta(days=60) + datetime_end = get_today_midnight_date() - datetime.timedelta(days=55) + visit3 = create_visit(subject=subject, datetime_begin=datetime_begin, datetime_end=datetime_end) #post save will be raised + visit1.refresh_from_db() + visit2.refresh_from_db() + visit3.refresh_from_db() + self.assertEquals(1, visit3.visit_number) + self.assertEquals(2, visit1.visit_number) + self.assertEquals(3, visit2.visit_number) + def test_mark_as_finished(self): subject = create_study_subject() visit = create_visit(subject) diff --git a/smash/web/tests/models/test_worker.py b/smash/web/tests/models/test_worker.py index 338ef668e0ca0b9befe465f28e17884ba7613f98..97c0ac5092bbbf483c14fb919fbe944eae5ac5ef 100644 --- a/smash/web/tests/models/test_worker.py +++ b/smash/web/tests/models/test_worker.py @@ -61,6 +61,16 @@ class WorkerModelTests(TestCase): self.assertTrue(worker.is_on_leave()) + def test_current_leave_details(self): + worker = create_worker() + self.assertEqual(worker.current_leave_details(), None) + h = Holiday(person=worker, + datetime_start=get_today_midnight_date() + datetime.timedelta(days=-2), + datetime_end=get_today_midnight_date() + datetime.timedelta(days=2)) + h.save() + + self.assertEqual(worker.current_leave_details(), h) + def test_get_by_user_for_anonymous(self): self.assertIsNone(Worker.get_by_user(AnonymousUser()))