diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index 6bf6bc78d0c6b507e82f70551a361d427186a848..ad8d4fc05db75f8dfcb5e659989891dc81ae49d5 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -1,5 +1,6 @@ import logging +from django.urls import reverse from django.db.models import Count, Case, When, Min, Max from django.db.models import Q from django.http import JsonResponse @@ -322,7 +323,6 @@ def types(request): "types": data }) - def serialize_subject(study_subject): location = location_to_str(study_subject.default_location) flying_team = flying_team_to_str(study_subject.flying_team) @@ -353,10 +353,19 @@ def serialize_subject(study_subject): status = "SHOULD_BE_IN_PROGRESS" else: status = "UPCOMING" + + appointment_types = ['{} ({})'.format(at.code, at.description) for at in visit.appointment_types.all()] + if len(appointment_types) == 0: + appointment_types = ['No appointment types set.'] + serialized_visits.append({ "status": status, + "appointment_types": appointment_types, + "edit_visit_url": reverse('web.views.visit_details', args=(visit.id,)), + "add_appointment_url": reverse('web.views.appointment_add', args=(visit.id,)), "datetime_start": serialize_date(visit.datetime_begin), "datetime_end": serialize_date(visit.datetime_end), + "is_finished": visit.is_finished }) contact_reminder = serialize_datetime(study_subject.datetime_contact_reminder) contact_attempts = ContactAttempt.objects.filter(subject=study_subject).order_by("-datetime_when") diff --git a/smash/web/forms/forms.py b/smash/web/forms/forms.py index 3106fba657d935031fcf148a5cbe6c253dc6c9d2..acead32df6e5e1401a5346ecbec59be00aac8d8d 100644 --- a/smash/web/forms/forms.py +++ b/smash/web/forms/forms.py @@ -60,6 +60,14 @@ class VisitDetailForm(ModelForm): appointment_types = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, queryset=AppointmentType.objects.all()) + def __init__(self, *args, **kwargs): + super(VisitDetailForm, self).__init__(*args, **kwargs) + instance = getattr(self, 'instance', None) + if instance.is_finished: #set form as readonly + for key in self.fields.keys(): + self.fields[key].widget.attrs['readonly'] = True + + class Meta: model = Visit exclude = ['is_finished', 'visit_number'] diff --git a/smash/web/forms/study_forms.py b/smash/web/forms/study_forms.py index 6e827cf3282821cd482ae0f933e6cf1754a5d903..df1874fa316d955ecd36530e99fa7f8866866738 100644 --- a/smash/web/forms/study_forms.py +++ b/smash/web/forms/study_forms.py @@ -3,6 +3,9 @@ import logging from django.forms import ModelForm, ValidationError from web.models import Study, StudyNotificationParameters, StudyColumns, StudySubject +import datetime +from dateutil.relativedelta import relativedelta + logger = logging.getLogger(__name__) @@ -11,17 +14,35 @@ class StudyEditForm(ModelForm): def __init__(self, *args, **kwargs): super(StudyEditForm, self).__init__(*args, **kwargs) - def clean_nd_number_study_subject_regex(self): - nd_number_study_subject_regex = self.cleaned_data.get( - 'nd_number_study_subject_regex') + def clean(self): + cleaned_data = super(StudyEditForm, self).clean() + #check regex + nd_number_study_subject_regex = cleaned_data.get('nd_number_study_subject_regex') + 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.') - - return nd_number_study_subject_regex + if nd_number_study_subject_regex is None or StudySubject.check_nd_number_regex(nd_number_study_subject_regex, instance) == False: + self.add_error('nd_number_study_subject_regex', 'Please enter a valid nd_number_study_subject_regex regex.') + + #check default_visit_duration_in_months + visit_duration_in_months = cleaned_data.get('default_visit_duration_in_months') + control_follow_up = cleaned_data.get('default_delta_time_for_control_follow_up') + patient_follow_up = cleaned_data.get('default_delta_time_for_patient_follow_up') + units = cleaned_data.get('default_delta_time_for_follow_up_units') + + if None not in [visit_duration_in_months, control_follow_up, patient_follow_up, units]: + t = datetime.datetime.today() + visit_duration = relativedelta(months=int(visit_duration_in_months)) + control_delta = relativedelta(**{units: control_follow_up}) + patient_delta = relativedelta(**{units: patient_follow_up}) + + #relative time delta has no __cmp__ method, so we add them to a datetime + min_delta_time = min((t + control_delta), (t + patient_delta)) + if (t+visit_duration) > min_delta_time: + self.add_error('default_visit_duration_in_months', 'Please enter a valid "duration of the visits". It must be shorter than the time difference between patient and control visits.') + + return cleaned_data class Meta: model = Study diff --git a/smash/web/management/commands/holidays.py b/smash/web/management/commands/holidays.py index 01a33ecc137bc7c077dfc120c2107f2b399ee1ff..5b729987a2673ae3c3503e50efc1228685654e81 100644 --- a/smash/web/management/commands/holidays.py +++ b/smash/web/management/commands/holidays.py @@ -4,6 +4,7 @@ from django.core.management.base import BaseCommand from ...models import Appointment, Location, AppointmentType, AppointmentTypeLink + def get_easter_monday(easter_sunday): return next_weekday(easter_sunday, 0) diff --git a/smash/web/migrations/0132_study_default_visit_duration_in_months.py b/smash/web/migrations/0132_study_default_visit_duration_in_months.py new file mode 100644 index 0000000000000000000000000000000000000000..522e672e1041241ab5c52140cebbbc53b8be7bbc --- /dev/null +++ b/smash/web/migrations/0132_study_default_visit_duration_in_months.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2018-11-13 10:05 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0131_study_default_voucher_expiration_in_months'), + ] + + operations = [ + migrations.AddField( + model_name='study', + name='default_visit_duration_in_months', + field=models.IntegerField(default=3, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Default duration of the visits in months'), + ), + ] diff --git a/smash/web/migrations/0133_auto_20181113_1550.py b/smash/web/migrations/0133_auto_20181113_1550.py new file mode 100644 index 0000000000000000000000000000000000000000..efc19768b94521fcc1c12fcdaf030d640d94a8f7 --- /dev/null +++ b/smash/web/migrations/0133_auto_20181113_1550.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2018-11-13 15:50 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0132_study_default_visit_duration_in_months'), + ] + + operations = [ + migrations.AddField( + model_name='study', + name='default_delta_time_for_control_follow_up', + field=models.IntegerField(default=4, help_text=b'Time difference between visits used to automatically create follow up visits', validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Time difference between control visits'), + ), + migrations.AddField( + model_name='study', + name='default_delta_time_for_follow_up_units', + field=models.CharField(choices=[(b'days', b'Days'), (b'years', b'Years')], default=b'years', help_text=b'Units for the number of days between visits for both patients and controls', max_length=10, verbose_name=b'Units for the follow up incrementals'), + ), + migrations.AddField( + model_name='study', + name='default_delta_time_for_patient_follow_up', + field=models.IntegerField(default=1, help_text=b'Time difference between visits used to automatically create follow up visits', validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Time difference between patient visits'), + ), + migrations.AlterField( + model_name='study', + name='default_visit_duration_in_months', + field=models.IntegerField(default=6, help_text=b'Duration of the visit, this is, the time interval, in months, when the appointments may take place', validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Duration of the visits in months'), + ), + migrations.AlterField( + model_name='study', + name='default_voucher_expiration_in_months', + field=models.IntegerField(default=3, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Duration of the vouchers in months'), + ), + ] diff --git a/smash/web/migrations/0134_merge_20181211_1542.py b/smash/web/migrations/0134_merge_20181211_1542.py new file mode 100644 index 0000000000000000000000000000000000000000..9ea20794127870e032566c49af3e2d438e0c7acd --- /dev/null +++ b/smash/web/migrations/0134_merge_20181211_1542.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2018-12-11 15:42 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0132_workerstudyrole_permissions'), + ('web', '0133_auto_20181113_1550'), + ] + + operations = [ + ] diff --git a/smash/web/models/study.py b/smash/web/models/study.py index e89ac16fc1f491a335b770d6e953b127227771bd..b4e9dfa5a1410cfd82d4a6b2d42a915cb9f81f57 100644 --- a/smash/web/models/study.py +++ b/smash/web/models/study.py @@ -6,6 +6,12 @@ from django.core.validators import MaxValueValidator, MinValueValidator import re +FOLLOW_UP_INCREMENT_IN_YEARS = 'years' +FOLLOW_UP_INCREMENT_IN_DAYS = 'days' +FOLLOW_UP_INCREMENT_UNIT_CHOICE = { + FOLLOW_UP_INCREMENT_IN_YEARS: 'Years', + FOLLOW_UP_INCREMENT_IN_DAYS: 'Days' +} class Study(models.Model): @@ -39,11 +45,40 @@ class Study(models.Model): ) default_voucher_expiration_in_months = models.IntegerField( - verbose_name='Default duration of the vouchers in months', + verbose_name='Duration of the vouchers in months', default=3, validators=[MinValueValidator(1)] ) + default_visit_duration_in_months = models.IntegerField( + verbose_name='Duration of the visits in months', + help_text='Duration of the visit, this is, the time interval, in months, when the appointments may take place', + default=6, + validators=[MinValueValidator(1)] + ) + + default_delta_time_for_patient_follow_up = models.IntegerField( + verbose_name='Time difference between patient visits', + help_text='Time difference between visits used to automatically create follow up visits', + default=1, + validators=[MinValueValidator(1)] + ) + + default_delta_time_for_control_follow_up = models.IntegerField( + verbose_name='Time difference between control visits', + help_text='Time difference between visits used to automatically create follow up visits', + default=4, + validators=[MinValueValidator(1)] + ) + + default_delta_time_for_follow_up_units = models.CharField(max_length=10, + choices=FOLLOW_UP_INCREMENT_UNIT_CHOICE.items(), + verbose_name='Units for the follow up incrementals', + help_text='Units for the number of days between visits for both patients and controls', + default=FOLLOW_UP_INCREMENT_IN_YEARS, + blank=False + ) + def check_nd_number(self, nd_number): regex = re.compile(self.nd_number_study_subject_regex) return regex.match(nd_number) is not None diff --git a/smash/web/models/visit.py b/smash/web/models/visit.py index cb6fd2c45f20467dd351ed4849bbd3e93639c978..c2b00ebb5056edda65023becaba5a6d126959ea6 100644 --- a/smash/web/models/visit.py +++ b/smash/web/models/visit.py @@ -1,5 +1,6 @@ # coding=utf-8 import datetime +from dateutil.relativedelta import relativedelta from django.db import models from django.db.models.signals import post_save @@ -7,7 +8,10 @@ from django.dispatch import receiver from django.db import transaction from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES_CONTROL +from web.models import Study +import logging +logger = logging.getLogger(__name__) class Visit(models.Model): @@ -69,18 +73,21 @@ class Visit(models.Model): follow_up_number = Visit.objects.filter( subject=self.subject).count() + 1 - delta_days = 365 + study = self.subject.study + if self.subject.type == SUBJECT_TYPE_CHOICES_CONTROL: - delta_days = 365 * 3 + 366 + args = {study.default_delta_time_for_follow_up_units: study.default_delta_time_for_control_follow_up} + else: + args = {study.default_delta_time_for_follow_up_units: study.default_delta_time_for_patient_follow_up} + + time_to_next_visit = relativedelta(**args) * (follow_up_number - 1) #calculated from first visit - time_to_next_visit = datetime.timedelta( - days=delta_days * (follow_up_number - 1)) + logger.warn('new visit: {} {} {}'.format(args, relativedelta(**args), time_to_next_visit)) Visit.objects.create( subject=self.subject, datetime_begin=visit_started + time_to_next_visit, - datetime_end=visit_started + time_to_next_visit + - datetime.timedelta(days=93) + datetime_end=visit_started + time_to_next_visit + relativedelta(months=study.default_visit_duration_in_months) ) @receiver(post_save, sender=Visit) diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index f92df08f2378d2e88dbe5deae555202b7103d463..dcf03f52dee0cf0914ecc7c85ccb7a38d641d934 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -185,27 +185,56 @@ function create_visit_row(visit) { var text = "---"; if (visit !== undefined && visit !== null) { if (visit.status === "DONE") { - color = "green"; - text = "OK"; + color = "#6bff5a"; + text = `<span title="Visit is finished, all appointments done.">OK</span>`; + } else if (visit.status === "MISSED") { color = "pink"; - text = "MISSED"; + text = `<span title="Visit is finished, some appointments were not carried out.">MISSED</span>`; + } else if (visit.status === "UPCOMING") { color = "#00ffff"; - text = "UPCOMING"; + text = `<span title="Visit has not started yet.">UPCOMING</span>`; + } else if (visit.status === "EXCEEDED") { color = "orange"; - text = "EXCEEDED"; + text = `<span title="Visit is over and no appointments were set.">EXCEEDED</span>`; + } else if (visit.status === "SHOULD_BE_IN_PROGRESS") { color = "orange"; - text = "IN PROGRESS (NO APPOINTMENTS)"; + text = `<span title="Visit has started but no appointments have been set yet.">IN PROGRESS (NO APPOINTMENTS)</span>`; + } else if (visit.status === "IN_PROGRESS") { color = "lightgreen"; - text = "IN PROGRESS"; + text = `<span title="Appointments are taking place.">IN PROGRESS</span>`; + } - text += "<br/>" + visit.datetime_start + " - " + visit.datetime_end; + + var start_date = moment(visit.datetime_start); + var end_date = moment(visit.datetime_end); + + text += `<br/> + <span data-html="true" title="From: ${start_date.format('ddd Do MMMM YYYY')} </br> To: ${end_date.format('ddd Do MMMM YYYY')}"> + From: ${start_date.format('D MMM. YYYY')} + </span> + <br/> + <span data-html="true" title="From: ${start_date.format('ddd Do MMMM YYYY')} </br> To: ${end_date.format('ddd Do MMMM YYYY')}"> + To: ${end_date.format('D MMM. YYYY')} + </span>` + + text += `<br/><span data-html="true" title="Visit details<br/>Appointment Types:<br/><div class='appointment_type_list'>${visit.appointment_types.join('<br/>')}</div>"> + <a href="${visit.edit_visit_url}"><i class="fa fa-list" aria-hidden="true"></i></a> + </span>`; + + if(!visit.is_finished){ + text += `<span title="Add new appointment to visit"><a href="${visit.add_appointment_url}"><i class="fa fa-plus-square-o" aria-hidden="true"></i></a></span>`; + }else{ + text += `<span title="Visit is marked as finished" ><i class="fa fa-check-circle" aria-hidden="true"></i></span>`; + } + + } - return "<div style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>"; + return "<div class='visit_row' style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>"; } function createVisibilityCheckboxes(checkboxesElement, columns) { diff --git a/smash/web/static/js/visit.js b/smash/web/static/js/visit.js index a2636ed219748484b648296a736608d14732589c..1e5aa2208b0177fad7bd72613cf637f7681c6f3e 100644 --- a/smash/web/static/js/visit.js +++ b/smash/web/static/js/visit.js @@ -1,10 +1,12 @@ -function visit_dates_behaviour(startDateInput, endDateInput) { +function visit_dates_behaviour(startDateInput, endDateInput, default_visit_duration) { $(startDateInput).change(function () { var object = $(this); try { var date = new Date(object.val()); - date.setMonth(date.getMonth() + 3); - $(endDateInput).val(date.toISOString().substring(0, 10)); + date.setMonth(date.getMonth() + default_visit_duration); + if($(endDateInput).val() == ""){ + $(endDateInput).val(date.toISOString().substring(0, 10)); + } } catch (err) { //there was a problematic date to process } diff --git a/smash/web/templates/includes/visit_appointment_types_field.html b/smash/web/templates/includes/visit_appointment_types_field.html index 4e167755b38db5821992197a8d3edf9ddb12245c..8cd1998f280aaebad4286e617f260e0191f00dbd 100644 --- a/smash/web/templates/includes/visit_appointment_types_field.html +++ b/smash/web/templates/includes/visit_appointment_types_field.html @@ -13,7 +13,7 @@ background: orange; {% endif %}{% endif %}{% endfor %}{% endfor %} "></div> - <input {% if readonly %}disabled="disabled"{% endif %} + <input {% if field.field.widget.attrs.readonly %}disabled="disabled"{% endif %} {% for option in field.value %}{% if option == pk|slugify or option == pk %}checked="checked" {% endif %}{% endfor %} type="checkbox" id="id_{{ field.name }}_{{ forloop.counter0 }}" diff --git a/smash/web/templates/subjects/index.html b/smash/web/templates/subjects/index.html index 7042377035d26f30ec7a6deb59535e7e4f0ff80f..c3192e6452f79ca6e13194a6b8483ba168932326 100644 --- a/smash/web/templates/subjects/index.html +++ b/smash/web/templates/subjects/index.html @@ -3,6 +3,7 @@ {% block styles %} {{ block.super }} + <script src="{% static 'AdminLTE/plugins/moment.js/moment.min.js' %}"></script> <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> <style type="text/css"> .box-body { @@ -12,6 +13,23 @@ font-weight: bolder; border-bottom: black 1px solid; } + .visit_row{ + font-size: 10pt; + max-width: 22ch; + min-width: 18ch; + text-align: center; + } + .visit_row > span{ + padding-right: 2px; + padding-left: 2px; + } + .visit_row > span > a{ + color: inherit; + } + .appointment_type_list{ + margin-top: 10px; + text-align: left; + } </style> {% endblock styles %} @@ -72,7 +90,12 @@ columns: getColumns(data.columns, getSubjectEditUrl), checkboxesElement: document.getElementById("visible-column-checkboxes"), dom_settings: 'lrtip' // show table without search box - }) + }); + }); + + $('body').tooltip({ + selector: '.visit_row > span[title]', + trigger: 'hover' }); </script> diff --git a/smash/web/templates/visits/add.html b/smash/web/templates/visits/add.html index 749641be57b7d9c1e4160a5d7457fff0cbfa5fed..4c93ee497b56c5d68886deb070115662d1fa3e05 100644 --- a/smash/web/templates/visits/add.html +++ b/smash/web/templates/visits/add.html @@ -70,7 +70,8 @@ <script src="{% static 'js/visit.js' %}"></script> <script> - visit_dates_behaviour($("[name='datetime_begin']"),$("[name='datetime_end']")); + var default_visit_duration_in_months = parseInt("{{default_visit_duration}}"); + visit_dates_behaviour($("[name='datetime_begin']"), $("[name='datetime_end']"), default_visit_duration_in_months); </script> {% include "includes/datepicker.js.html" %} diff --git a/smash/web/templates/visits/details.html b/smash/web/templates/visits/details.html index 62069efad065578c2615da6a13a554e261b11629..a85ce75d1f33ac0ce2284b723f1fca38ef46ea1c 100644 --- a/smash/web/templates/visits/details.html +++ b/smash/web/templates/visits/details.html @@ -91,10 +91,17 @@ </div> <div> + {%if visFinished%} + <a href="{% url 'web.views.appointment_add' vid %}" class="btn btn-app" disabled> + <i class="fa fa-plus"></i> + Add new appointment + </a> + {% else %} <a href="{% url 'web.views.appointment_add' vid %}" class="btn btn-app"> <i class="fa fa-plus"></i> Add new appointment </a> + {% endif %} </div> @@ -221,7 +228,8 @@ <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> <script src="{% static 'js/visit.js' %}"></script> <script> - visit_dates_behaviour($("[name='datetime_begin']"), $("[name='datetime_end']")); + var default_visit_duration_in_months = parseInt("{{default_visit_duration}}"); + visit_dates_behaviour($("[name='datetime_begin']"), $("[name='datetime_end']"), default_visit_duration_in_months); </script> {% include "includes/datepicker.js.html" %} diff --git a/smash/web/tests/forms/test_study_forms.py b/smash/web/tests/forms/test_study_forms.py index 6893bce69694866e8da3838acf419365776663a8..0fe2bd2cd5f0b9fce56a6f11feef5e9bb471fd49 100644 --- a/smash/web/tests/forms/test_study_forms.py +++ b/smash/web/tests/forms/test_study_forms.py @@ -18,16 +18,15 @@ class StudyFormTests(TestCase): 'nd_number_study_subject_regex').get_default() form.cleaned_data = { 'nd_number_study_subject_regex': nd_number_study_subject_regex_default} - self.assertTrue(form.clean_nd_number_study_subject_regex() - == nd_number_study_subject_regex_default) + + self.assertTrue(form.clean()['nd_number_study_subject_regex'] == 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} - self.assertRaises( - ValidationError, form.clean_nd_number_study_subject_regex) + self.assertFalse(form.is_valid()) def test_study_other_regex(self): StudySubject.objects.all().delete() @@ -39,5 +38,5 @@ class StudyFormTests(TestCase): nd_number_study_subject_regex_default = r'^nd\d{5}$' form.cleaned_data = { 'nd_number_study_subject_regex': nd_number_study_subject_regex_default} - self.assertTrue(form.clean_nd_number_study_subject_regex() + self.assertTrue(form.clean()['nd_number_study_subject_regex'] == nd_number_study_subject_regex_default) diff --git a/smash/web/tests/models/test_visit.py b/smash/web/tests/models/test_visit.py index 38cb4b6c50585a8cd1b983b7aca609a3c67712ae..d9c7efb9c1c416f98e83d4fecac819d4433512e2 100644 --- a/smash/web/tests/models/test_visit.py +++ b/smash/web/tests/models/test_visit.py @@ -1,5 +1,5 @@ import datetime - +from dateutil.relativedelta import relativedelta from django.test import TestCase from web.models import Visit @@ -137,7 +137,8 @@ class VisitModelTests(TestCase): visit.mark_as_finished() - follow_up_visit = Visit.objects.filter(subject=subject).filter(visit_number=2)[0] + visit_number=2 + follow_up_visit = Visit.objects.filter(subject=subject).filter(visit_number=visit_number)[0] follow_up_visit.datetime_begin = visit.datetime_begin + datetime.timedelta(days=133) follow_up_visit.datetime_end = visit.datetime_begin + datetime.timedelta(days=170) follow_up_visit.save() @@ -147,10 +148,16 @@ class VisitModelTests(TestCase): visit_count = Visit.objects.filter(subject=subject).count() self.assertEquals(3, visit_count) - new_follow_up = Visit.objects.filter(subject=subject).filter(visit_number=3)[0] + visit_number=3 + new_follow_up = Visit.objects.filter(subject=subject).filter(visit_number=visit_number)[0] # check if follow up date is based on the first visit date - self.assertTrue(visit.datetime_begin + datetime.timedelta(days=365 * 2 - 1) < new_follow_up.datetime_begin) + study = visit.subject.study + args = {study.default_delta_time_for_follow_up_units: study.default_delta_time_for_patient_follow_up} #patient + + time_to_next_visit = relativedelta(**args) * (visit_number - 1) #calculated from first visit + + self.assertTrue(visit.datetime_begin + time_to_next_visit - datetime.timedelta(days=1) < new_follow_up.datetime_begin) def test_visit_to_string(self): visit = create_visit(create_study_subject()) diff --git a/smash/web/views/visit.py b/smash/web/views/visit.py index 96dde765288b7b54fd87468952d6a742e2fd133a..77625043f21147bc3c9b0e3cc314a9925120c2de 100644 --- a/smash/web/views/visit.py +++ b/smash/web/views/visit.py @@ -74,6 +74,7 @@ def visit_details(request, id): languages.extend(study_subject.subject.languages.all()) return wrap_response(request, 'visits/details.html', { + 'default_visit_duration' : study_subject.study.default_visit_duration_in_months, 'visit_form': visit_form, 'study_subject_form': study_subject_form, 'subject_form': subject_form, @@ -96,6 +97,7 @@ def visit_mark(request, id, as_what): def visit_add(request, subject_id=-1): if request.method == 'POST': form = VisitAddForm(request.POST, request.FILES) + args = {'form': form} if form.is_valid(): visit = form.save() return redirect('web.views.visit_details', visit.id) @@ -105,5 +107,6 @@ def visit_add(request, subject_id=-1): if len(subjects) > 0: subject = subjects[0] form = VisitAddForm(initial={'subject': subject}) + args = {'form': form, 'default_visit_duration': subject.study.default_visit_duration_in_months} - return wrap_response(request, 'visits/add.html', {'form': form}) + return wrap_response(request, 'visits/add.html', args)