diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index 589d19d633efa14f693aff4fbe624ec80c55c1b2..6c9bdcbf30453e4c385bf59da815289ae78c0b08 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -10,9 +10,10 @@ from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study, ContactAttempt from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ - StudySubjectList + StudySubjectList, SUBJECT_LIST_VOUCHER_EXPIRY from web.views import e500_error -from web.views.notifications import get_subjects_with_no_visit, get_subjects_with_reminder, get_today_midnight_date +from web.views.notifications import get_subjects_with_no_visit, get_subjects_with_reminder, get_today_midnight_date, \ + get_subjects_with_almost_expired_vouchers logger = logging.getLogger(__name__) @@ -89,6 +90,8 @@ def get_subjects(request, type): return get_subjects_with_no_visit(request.user) elif type == SUBJECT_LIST_REQUIRE_CONTACT: return get_subjects_with_reminder(request.user) + elif type == SUBJECT_LIST_VOUCHER_EXPIRY: + return get_subjects_with_almost_expired_vouchers(request.user) else: raise TypeError("Unknown query type: " + type) diff --git a/smash/web/migrations/0102_studynotificationparameters_subject_voucher_expiry_visible.py b/smash/web/migrations/0102_studynotificationparameters_subject_voucher_expiry_visible.py new file mode 100644 index 0000000000000000000000000000000000000000..df7f4833a1e947d2240708c52e8229396ffc09d3 --- /dev/null +++ b/smash/web/migrations/0102_studynotificationparameters_subject_voucher_expiry_visible.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-14 12:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0101_auto_20171214_1047'), + ] + + operations = [ + migrations.AddField( + model_name='studynotificationparameters', + name='subject_voucher_expiry_visible', + field=models.BooleanField(default=False, verbose_name=b'subject vouchers almost expired'), + ), + ] diff --git a/smash/web/models/notification_columns.py b/smash/web/models/notification_columns.py index aa1141734ad6b861dd3780bc39f923533c19c520..4bfeb00a4301fefe4b6a916cd5aedde8d765d74a 100644 --- a/smash/web/models/notification_columns.py +++ b/smash/web/models/notification_columns.py @@ -31,6 +31,11 @@ class StudyNotificationParameters(models.Model): verbose_name='subject without visit', ) + subject_voucher_expiry_visible = models.BooleanField( + default=False, + verbose_name='subject vouchers almost expired', + ) + unfinished_visits_visible = models.BooleanField( default=True, verbose_name='unfinished visits', diff --git a/smash/web/models/study_subject_list.py b/smash/web/models/study_subject_list.py index cd2f5bbbbfff4142032e0cd68b9624b53e0f7668..de31e3cf04711d7909c80f56019597ef59605cb3 100644 --- a/smash/web/models/study_subject_list.py +++ b/smash/web/models/study_subject_list.py @@ -6,11 +6,13 @@ from web.models import Study, SubjectColumns, StudyColumns SUBJECT_LIST_GENERIC = "GENERIC" SUBJECT_LIST_NO_VISIT = "NO_VISIT" SUBJECT_LIST_REQUIRE_CONTACT = "REQUIRE_CONTACT" +SUBJECT_LIST_VOUCHER_EXPIRY = "VOUCHER_EXPIRY" SUBJECT_LIST_CHOICES = { SUBJECT_LIST_GENERIC: 'Generic', SUBJECT_LIST_NO_VISIT: 'Subjects without visit', SUBJECT_LIST_REQUIRE_CONTACT: 'Subjects required contact', + SUBJECT_LIST_VOUCHER_EXPIRY: 'Subject with vouchers to be expired soon' } diff --git a/smash/web/tests/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py index 3b4ad50d6f1af4e47c121b1ce4e78f9e7c1a487b..4276b7346901ff26e972498352260bfb8d169f43 100644 --- a/smash/web/tests/api_views/test_subject.py +++ b/smash/web/tests/api_views/test_subject.py @@ -9,7 +9,7 @@ from web.api_views.subject import get_subjects_order, get_subjects_filtered, ser from web.models import StudySubject, Appointment, Study, Worker from web.models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES_PATIENT, SUBJECT_TYPE_CHOICES_CONTROL from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ - StudySubjectList + StudySubjectList, SUBJECT_LIST_VOUCHER_EXPIRY from web.tests import LoggedInWithWorkerTestCase from web.tests.functions import create_study_subject, create_get_suffix, create_visit, \ create_appointment, create_empty_study_columns, create_contact_attempt, create_flying_team, create_worker @@ -104,6 +104,10 @@ class TestSubjectApi(LoggedInWithWorkerTestCase): response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_GENERIC})) self.assertEqual(response.status_code, 200) + def test_subjects_voucher_almost_expired(self): + response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_VOUCHER_EXPIRY})) + self.assertEqual(response.status_code, 200) + def test_subjects_no_visit(self): response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_NO_VISIT})) self.assertEqual(response.status_code, 200) diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index 49888524124b9842bdf68786f110b3037775215b..eb9e87dd7ddca95144ef58a1337b67f141417dda 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -102,6 +102,23 @@ def create_empty_notification_parameters(): subject_require_contact_visible=False, missing_redcap_subject_visible=False, inconsistent_redcap_subject_visible=False, + subject_voucher_expiry_visible=False, + ) + + +def create_full_notification_parameters(): + return StudyNotificationParameters.objects.create( + exceeded_visits_visible=True, + unfinished_visits_visible=True, + approaching_visits_without_appointments_visible=True, + unfinished_appointments_visible=True, + visits_with_missing_appointments_visible=True, + subject_no_visits_visible=True, + approaching_visits_for_mail_contact_visible=True, + subject_require_contact_visible=True, + missing_redcap_subject_visible=True, + inconsistent_redcap_subject_visible=True, + subject_voucher_expiry_visible=True, ) diff --git a/smash/web/tests/view/test_notifications.py b/smash/web/tests/view/test_notifications.py index 1be70d94ce1c722e4c45f68a55de6aecb2eb8605..de8c59457b9d5a26106e4dafccf4c99fc899725f 100644 --- a/smash/web/tests/view/test_notifications.py +++ b/smash/web/tests/view/test_notifications.py @@ -7,7 +7,8 @@ from web.models import Appointment, Location, AppointmentTypeLink, Study, Visit from web.models.constants import GLOBAL_STUDY_ID, VOUCHER_STATUS_USED from web.tests import LoggedInTestCase from web.tests.functions import create_appointment, create_location, create_worker, create_appointment_type, \ - create_empty_notification_parameters, create_study_subject, create_visit, create_voucher + create_empty_notification_parameters, create_study_subject, create_visit, create_voucher, create_contact_attempt, \ + create_full_notification_parameters from web.views.notifications import \ get_approaching_visits_for_mail_contact, \ get_approaching_visits_for_mail_contact_count, \ @@ -23,7 +24,7 @@ from web.views.notifications import \ get_today_midnight_date, \ get_unfinished_appointments, \ get_unfinished_appointments_count, \ - get_unfinished_visits, get_exceeded_visits + get_unfinished_visits, get_exceeded_visits, get_subject_voucher_expiry_notifications_count logger = logging.getLogger(__name__) @@ -57,7 +58,6 @@ class NotificationViewTests(LoggedInTestCase): appointment.status = Appointment.APPOINTMENT_STATUS_FINISHED appointment.save() notification = get_exceeded_visit_notifications_count(self.user) - logger.debug(get_exceeded_visits(self.user).query) self.assertEquals(1, notification.count) def test_get_exceeded_visit_notifications_count_2(self): @@ -127,6 +127,17 @@ class NotificationViewTests(LoggedInTestCase): self.assertEquals(0, result[0]) self.assertEquals(0, len(result[1])) + def test_get_notifications_with_full_study_notification(self): + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + study.notification_parameters = create_full_notification_parameters() + study.save() + + create_worker(self.user) + result = get_notifications(self.user) + + self.assertEquals(0, result[0]) + self.assertTrue(len(result[1]) > 0) + def test_get_visits_without_appointments_count_2(self): appointment_type = create_appointment_type() original_notification = get_visits_without_appointments_count(self.user) @@ -493,3 +504,34 @@ class NotificationViewTests(LoggedInTestCase): self.fail("Exception expected") except TypeError: pass + + def test_get_subjects_with_expiry_vouchers(self): + original_notification = get_subject_voucher_expiry_notifications_count(self.user) + voucher = create_voucher() + voucher.expiry_date = get_today_midnight_date() + voucher.save() + + notification = get_subject_voucher_expiry_notifications_count(self.user) + self.assertEquals(original_notification.count + 1, notification.count) + + voucher.expiry_date = get_today_midnight_date() + datetime.timedelta(days=365) + voucher.save() + + notification = get_subject_voucher_expiry_notifications_count(self.user) + self.assertEquals(original_notification.count, notification.count) + + def test_get_subjects_with_expiry_vouchers_and_contact_attempt(self): + original_notification = get_subject_voucher_expiry_notifications_count(self.user) + voucher = create_voucher() + voucher.expiry_date = get_today_midnight_date() + voucher.save() + contact_attempt = create_contact_attempt(voucher.study_subject) + + notification = get_subject_voucher_expiry_notifications_count(self.user) + self.assertEquals(original_notification.count, notification.count) + + contact_attempt.datetime_when = "2011-11-11" + contact_attempt.save() + + notification = get_subject_voucher_expiry_notifications_count(self.user) + self.assertEquals(original_notification.count + 1, notification.count) diff --git a/smash/web/tests/view/test_subjects.py b/smash/web/tests/view/test_subjects.py index c84a8d6e126f08d90009851e721a4486d3e5cd4c..ac0d9beae241b0af56d2ff2dfadb5e894727c01b 100644 --- a/smash/web/tests/view/test_subjects.py +++ b/smash/web/tests/view/test_subjects.py @@ -219,6 +219,10 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase): response = self.client.get(reverse('web.views.subject_no_visits')) self.assertEqual(response.status_code, 200) + def test_render_subjects_voucher_expiry(self): + response = self.client.get(reverse('web.views.subject_voucher_expiry')) + self.assertEqual(response.status_code, 200) + def test_render_subjects_require_contact(self): self.study_subject.datetime_contact_reminder = get_today_midnight_date() + datetime.timedelta(days=-1) diff --git a/smash/web/urls.py b/smash/web/urls.py index f5da2cb56dd161f3a3f82f2e37d366d25ca9fef4..baa812987f233ae90767d6f5c139e7b33c2a49ff 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -73,6 +73,8 @@ urlpatterns = [ url(r'^subjects$', views.subject.subjects, name='web.views.subjects'), url(r'^subjects/no_visit$', views.subject.subject_no_visits, name='web.views.subject_no_visits'), url(r'^subjects/require_contact$', views.subject.subject_require_contact, name='web.views.subject_require_contact'), + url(r'^subjects/voucher_expiry', views.subject.subject_voucher_expiry, name='web.views.subject_voucher_expiry'), + url(r'^subjects/add$', views.subject.subject_add, name='web.views.subject_add'), url(r'^subjects/subject_visit_details/(?P<id>\d+)$', views.subject.subject_visit_details, name='web.views.subject_visit_details'), diff --git a/smash/web/views/notifications.py b/smash/web/views/notifications.py index 5a42783a6a7233b6b0fcf22b66ec0f8e083301a2..58e305a63de108a643a78647bfa5fd122dfb4de3 100644 --- a/smash/web/views/notifications.py +++ b/smash/web/views/notifications.py @@ -1,13 +1,15 @@ # coding=utf-8 import datetime +import logging from django.contrib.auth.models import User, AnonymousUser -from django.db.models import Count, Case, When, Q, F +from django.db.models import Count, Case, When, Q, F, Max from django.utils import timezone -from web.models import Study +from web.models import Study, Worker, StudySubject, Visit, Appointment, Location, MissingSubject, InconsistentSubject from web.models.constants import GLOBAL_STUDY_ID, VOUCHER_STATUS_NEW -from ..models import Worker, StudySubject, Visit, Appointment, Location, MissingSubject, InconsistentSubject + +logger = logging.getLogger(__name__) class NotificationCount(object): @@ -68,6 +70,15 @@ def get_subject_with_no_visit_notifications_count(user): return notification +def get_subject_voucher_expiry_notifications_count(user): + notification = NotificationCount( + title="subject vouchers almost expired", + count=get_subjects_with_almost_expired_vouchers(user).count(), + style="fa fa-users text-aqua", + type='web.views.subject_voucher_expiry') + return notification + + def get_visits_without_appointments_count(user): notification = NotificationCount( title="unfinished visits", @@ -130,6 +141,8 @@ def get_notifications(the_user): notifications.append(get_visits_with_missing_appointments_count(worker)) if study.notification_parameters.subject_no_visits_visible: notifications.append(get_subject_with_no_visit_notifications_count(worker)) + if study.notification_parameters.subject_voucher_expiry_visible: + notifications.append(get_subject_voucher_expiry_notifications_count(worker)) if study.notification_parameters.approaching_visits_for_mail_contact_visible: notifications.append(get_approaching_visits_for_mail_contact_count(worker)) if study.notification_parameters.subject_require_contact_visible: @@ -157,6 +170,19 @@ def get_subjects_with_no_visit(user): return result +def get_subjects_with_almost_expired_vouchers(user): + notification_min_date = get_today_midnight_date() + datetime.timedelta(days=14) + contact_attempt_min_date = get_today_midnight_date() - datetime.timedelta(days=14) + result = StudySubject.objects.filter( + subject__dead=False, + resigned=False, + default_location__in=get_filter_locations(user), + ).annotate(last_contact=Max(Case(When(contactattempt__success=True, then="contactattempt__datetime_when")))).filter( + Q(vouchers__status=VOUCHER_STATUS_NEW) & Q(vouchers__expiry_date__lte=notification_min_date)).filter( + Q(last_contact__lt=contact_attempt_min_date) | Q(last_contact__isnull=True)) + return result + + def get_subjects_with_reminder(user): tomorrow = datetime.datetime.now() + datetime.timedelta(hours=1) diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index f1c9566a8be16acd252dc146bfcf3c06be5165b9..d891d0d65be6a3c07243f40d4704ee0c071d2103 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -5,11 +5,12 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, get_object_or_404 -from ..models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT from . import wrap_response from ..forms import VisitDetailForm, SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm from ..models import StudySubject, MailTemplate, Worker, Study from ..models.constants import GLOBAL_STUDY_ID +from ..models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ + SUBJECT_LIST_VOUCHER_EXPIRY logger = logging.getLogger(__name__) @@ -54,6 +55,13 @@ def subject_no_visits(request): return wrap_response(request, 'subjects/index.html', context) +def subject_voucher_expiry(request): + context = { + 'list_type': SUBJECT_LIST_VOUCHER_EXPIRY, + } + return wrap_response(request, 'subjects/index.html', context) + + def subject_require_contact(request): context = { 'list_type': SUBJECT_LIST_REQUIRE_CONTACT,