diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py index 098a31aabd9b1032c24cd7a5d8b1ff0530df86b6..08be1500d63dfb7898b9a940659f217ece424973 100644 --- a/smash/web/api_urls.py +++ b/smash/web/api_urls.py @@ -16,7 +16,7 @@ Including another URLconf from django.conf.urls import url from web.api_views import worker, location, subject, appointment_type, appointment, configuration, daily_planning, \ - redcap, flying_team + redcap, flying_team, visit urlpatterns = [ # appointments @@ -38,6 +38,10 @@ urlpatterns = [ name='web.api.subjects.columns'), url(r'^subject_types', subject.types, name='web.api.subject_types'), + # visits data + url(r'^visits/(?P<visit_list_type>[A-z]+)$', visit.visits, name='web.api.visits'), + url(r'^visits:columns/(?P<visit_list_type>[A-z]+)$', visit.get_visit_columns, name='web.api.visits.columns'), + # locations url(r'^locations$', location.locations, name='web.api.locations'), diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e1e96dbcfc8944eeff773fbd95016f76f5ef2db1 --- /dev/null +++ b/smash/web/api_views/serialization_utils.py @@ -0,0 +1,61 @@ +import logging + +logger = logging.getLogger(__name__) + + +def bool_to_yes_no(val): + if val: + return "YES" + else: + return "NO" + + +def flying_team_to_str(flying_team): + result = "" + if flying_team is not None: + result = unicode(flying_team) + return result + + +def location_to_str(location): + result = "" + if location is not None: + result = unicode(location.name) + return result + + +def serialize_date(date): + if date is not None: + result = date.strftime('%Y-%m-%d') + else: + result = "" + return result + + +def serialize_datetime(date): + if date is not None: + result = date.strftime('%Y-%m-%d %H:%M') + else: + result = "" + return result + + +def add_column(result, name, field_name, column_list, param, columns_used_in_study=None, visible_param=None, + sortable=True): + add = True + if columns_used_in_study: + add = getattr(columns_used_in_study, field_name) + if add: + if visible_param is not None: + visible = visible_param + elif column_list is None: + visible = True + else: + visible = getattr(column_list, field_name) + result.append({ + "type": field_name, + "name": name, + "filter": param, + "visible": visible, + "sortable": sortable + }) diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index ab558c01959f748304588899bebd91204d84ced0..9d2d67b3ac12f5afcc6fa32eaca82f4e1f57a0ce 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -5,6 +5,8 @@ from django.db.models import Count, Case, When, Min, Max from django.db.models import Q from django.http import JsonResponse +from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \ + serialize_date, serialize_datetime 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, \ @@ -31,25 +33,6 @@ def referrals(request): }) -def add_column(result, name, field_name, column_list, param, columns_used_in_study=None, visible_param=None): - add = True - if columns_used_in_study: - add = getattr(columns_used_in_study, field_name) - if add: - if visible_param is not None: - visible = visible_param - elif column_list is None: - visible = True - else: - visible = getattr(column_list, field_name) - result.append({ - "type": field_name, - "name": name, - "filter": param, - "visible": visible - }) - - @login_required def get_subject_columns(request, subject_list_type): study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] @@ -80,7 +63,7 @@ def get_subject_columns(request, subject_list_type): add_column(result, "Postponed", "postponed", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Info sent", "information_sent", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Type", "type", study_subject_columns, "type_filter", study.columns) - add_column(result, "Edit", "edit", None, None) + add_column(result, "Edit", "edit", None, None, sortable=False) for visit_number in range(1, 9): visit_key = "visit_" + str(visit_number) add_column(result, "Visit " + str(visit_number), visit_key, None, "visit_filter", @@ -321,20 +304,9 @@ def types(request): }) -def get_yes_no(val): - if val: - return "YES" - else: - return "NO" - - def serialize_subject(study_subject): - location = "" - if study_subject.default_location is not None: - location = study_subject.default_location.name - flying_team = "" - if study_subject.flying_team is not None: - flying_team = unicode(study_subject.flying_team) + location = location_to_str(study_subject.default_location) + flying_team = flying_team_to_str(study_subject.flying_team) visits = Visit.objects.filter(subject=study_subject).order_by('visit_number') serialized_visits = [] for visit in visits: @@ -359,17 +331,15 @@ def serialize_subject(study_subject): status = "UPCOMING" serialized_visits.append({ "status": status, - "datetime_start": visit.datetime_begin.strftime('%Y-%m-%d'), - "datetime_end": visit.datetime_end.strftime('%Y-%m-%d'), + "datetime_start": serialize_date(visit.datetime_begin), + "datetime_end": serialize_date(visit.datetime_end), }) - contact_reminder = study_subject.datetime_contact_reminder - if contact_reminder is not None: - contact_reminder = contact_reminder.strftime('%Y-%m-%d %H:%M') + contact_reminder = serialize_datetime(study_subject.datetime_contact_reminder) contact_attempts = ContactAttempt.objects.filter(subject=study_subject).order_by("-datetime_when") if len(contact_attempts) > 0: last_contact_attempt = contact_attempts[0] - last_contact_attempt_string = last_contact_attempt.datetime_when.strftime( - '%Y-%m-%d %H:%M') + "<br/>" + str(last_contact_attempt.worker) + "<br/> Success: " + get_yes_no( + last_contact_attempt_string = serialize_datetime(last_contact_attempt.datetime_when) + "<br/>" + str( + last_contact_attempt.worker) + "<br/> Success: " + bool_to_yes_no( last_contact_attempt.success) + "<br/>" + last_contact_attempt.comment else: @@ -385,10 +355,10 @@ def serialize_subject(study_subject): "referral": study_subject.referral, "default_location": location, "flying_team": flying_team, - "dead": get_yes_no(study_subject.subject.dead), - "resigned": get_yes_no(study_subject.resigned), - "postponed": get_yes_no(study_subject.postponed), - "information_sent": get_yes_no(study_subject.information_sent), + "dead": bool_to_yes_no(study_subject.subject.dead), + "resigned": bool_to_yes_no(study_subject.resigned), + "postponed": bool_to_yes_no(study_subject.postponed), + "information_sent": bool_to_yes_no(study_subject.information_sent), "type": study_subject.get_type_display(), "id": study_subject.id, "visits": serialized_visits, diff --git a/smash/web/api_views/visit.py b/smash/web/api_views/visit.py new file mode 100644 index 0000000000000000000000000000000000000000..ba2258b9f19eae217529aefc70ec98b99521deab --- /dev/null +++ b/smash/web/api_views/visit.py @@ -0,0 +1,254 @@ +import logging + +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.http import JsonResponse + +from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \ + serialize_date +from web.models import AppointmentType, Appointment +from web.models import SubjectColumns +from web.models import Visit, Study, VisitColumns, StudyVisitList, StudyColumns +from web.models.constants import GLOBAL_STUDY_ID +from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_EXCEEDED_TIME, VISIT_LIST_UNFINISHED, \ + VISIT_LIST_MISSING_APPOINTMENTS, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, \ + VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT +from web.views.notifications import get_unfinished_visits, get_active_visits_with_missing_appointments, \ + get_approaching_visits_without_appointments, get_approaching_visits_for_mail_contact, get_exceeded_visits + +logger = logging.getLogger(__name__) + + +# noinspection PyUnusedLocal +@login_required +def get_visit_columns(request, visit_list_type): + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + study_visit_lists = StudyVisitList.objects.filter(study=study, type=visit_list_type) + if len(study_visit_lists) > 0: + visit_list = study_visit_lists[0] + visit_columns = visit_list.visible_visit_columns + visit_subject_columns = visit_list.visible_subject_columns + visit_subject_study_columns = visit_list.visible_study_subject_columns + else: + visit_list = StudyVisitList() + visit_columns = VisitColumns() + visit_subject_columns = SubjectColumns() + visit_subject_study_columns = StudyColumns() + + result = [] + add_column(result, "First name", "first_name", visit_subject_columns, "string_filter") + add_column(result, "Last name", "last_name", visit_subject_columns, "string_filter") + add_column(result, "Location", "default_location", visit_subject_study_columns, "location_filter", + study.columns) + add_column(result, "Flying team location", "flying_team", visit_subject_study_columns, "flying_team_filter", + study.columns) + add_column(result, "Visit begins", "datetime_begin", visit_columns, None) + add_column(result, "Visit ends", "datetime_end", visit_columns, None) + add_column(result, "Finished", "is_finished", visit_columns, "yes_no_filter") + add_column(result, "Post mail sent", "post_mail_sent", visit_columns, "yes_no_filter") + add_column(result, "Visit number", "visit_number", visit_columns, "integer_filter") + add_column(result, "Appointments in progress", "visible_appointment_types_in_progress", visit_list, + "appointment_type_filter", sortable=False) + add_column(result, "Done appointments", "visible_appointment_types_done", visit_list, "appointment_type_filter", + sortable=False) + add_column(result, "Missing appointments", "visible_appointment_types_missing", visit_list, + "appointment_type_filter", sortable=False) + add_column(result, "All appointments", "visible_appointment_types", visit_columns, "appointment_type_filter", + sortable=False) + add_column(result, "Edit", "edit", None, None, sortable=False) + + return JsonResponse({"columns": result}) + + +# noinspection PyUnusedLocal +@login_required +def get_visits(request, visit_type): + if visit_type == VISIT_LIST_GENERIC: + return Visit.objects.all() + elif visit_type == VISIT_LIST_EXCEEDED_TIME: + return get_exceeded_visits(request.user).all() + elif visit_type == VISIT_LIST_UNFINISHED: + return get_unfinished_visits(request.user).all() + elif visit_type == VISIT_LIST_MISSING_APPOINTMENTS: + return get_active_visits_with_missing_appointments(request.user).all() + elif visit_type == VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS: + return get_approaching_visits_without_appointments(request.user).all() + elif visit_type == VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT: + return get_approaching_visits_for_mail_contact(request.user).all() + else: + raise TypeError("Unknown query type: " + visit_type) + + +def get_visits_order(visits_to_be_ordered, order_column, order_direction): + result = visits_to_be_ordered + if order_direction == "asc": + order_direction = "" + else: + order_direction = "-" + if order_column == "first_name": + result = visits_to_be_ordered.order_by(order_direction + 'subject__subject__first_name') + elif order_column == "last_name": + result = visits_to_be_ordered.order_by(order_direction + 'subject__subject__last_name') + elif order_column == "default_location": + result = visits_to_be_ordered.order_by(order_direction + 'subject__default_location') + elif order_column == "flying_team": + result = visits_to_be_ordered.order_by(order_direction + 'subject__flying_team') + elif order_column == "datetime_begin": + result = visits_to_be_ordered.order_by(order_direction + 'datetime_begin') + elif order_column == "datetime_end": + result = visits_to_be_ordered.order_by(order_direction + 'datetime_end') + elif order_column == "is_finished": + result = visits_to_be_ordered.order_by(order_direction + 'is_finished') + elif order_column == "post_mail_sent": + result = visits_to_be_ordered.order_by(order_direction + 'post_mail_sent') + elif order_column == "visit_number": + result = visits_to_be_ordered.order_by(order_direction + 'visit_number') + else: + logger.warn("Unknown sort column: " + str(order_column)) + return result + + +def filter_by_appointment_in_progress(visits_to_filter, appointment_type): + result = visits_to_filter.filter(Q(appointment__appointment_types=appointment_type) & Q( + appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED)) + return result + + +def filter_by_appointment_done(visits_to_filter, appointment_type): + result = visits_to_filter.filter(Q(appointment__appointment_types=appointment_type) & Q( + appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED)) + return result + + +def filter_by_appointment_missing(visits_to_filter, appointment_type): + result = filter_by_appointment(visits_to_filter, appointment_type).exclude( + Q(appointment__appointment_types=appointment_type), Q( + appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED) | Q( + appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED)) + return result + + +def filter_by_appointment(visits_to_filter, appointment_type): + result = visits_to_filter.filter(appointment_types=appointment_type) + return result + + +def get_visits_filtered(visits_to_be_filtered, filters): + result = visits_to_be_filtered + for row in filters: + column = row[0] + value = row[1] + if column == "first_name": + result = result.filter(subject__subject__first_name__icontains=value) + elif column == "last_name": + result = result.filter(subject__subject__last_name__icontains=value) + elif column == "flying_team": + result = result.filter(subject__flying_team=value) + elif column == "default_location": + result = result.filter(subject__default_location=value) + elif column == "is_finished": + result = result.filter(is_finished=(value == "true")) + elif column == "post_mail_sent": + result = result.filter(post_mail_sent=(value == "true")) + elif column == "visit_number": + result = result.filter(visit_number=int(value)) + elif column == "visible_appointment_types_in_progress": + result = filter_by_appointment_in_progress(result, value) + elif column == "visible_appointment_types_done": + result = filter_by_appointment_done(result, value) + elif column == "visible_appointment_types_missing": + result = filter_by_appointment_missing(result, value) + elif column == "visible_appointment_types": + result = filter_by_appointment(result, value) + else: + message = "UNKNOWN filter: " + if column is None: + message += "[None]" + else: + message += str(column) + logger.warn(message) + + return result + + +@login_required +def visits(request, visit_list_type): + # id of the query from dataTable: https://datatables.net/manual/server-side + draw = int(request.GET.get("draw", "-1")) + + start = int(request.GET.get("start", "0")) + length = int(request.GET.get("length", "10")) + order = int(request.GET.get("order[0][column]", "0")) + order_dir = request.GET.get("order[0][dir]", "asc") + order_column = request.GET.get("columns[" + str(order) + "][data]", "last_name") + + filters = [] + column_id = 0 + while request.GET.get("columns[" + str(column_id) + "][search][value]", "unknown") != "unknown": + val = request.GET.get("columns[" + str(column_id) + "][search][value]", "unknown") + if val != "": + filters.append([request.GET.get("columns[" + str(column_id) + "][data]"), val]) + column_id += 1 + + all_visits = get_visits(request, visit_list_type) + + count = all_visits.count() + + ordered_visits = get_visits_order(all_visits, order_column, order_dir) + filtered_visits = get_visits_filtered(ordered_visits, filters) + sliced_visits = filtered_visits[start:(start + length)] + + result_visits = sliced_visits + + count_filtered = filtered_visits.count() + + data = [] + for visit in result_visits: + data.append(serialize_visit(visit)) + + return JsonResponse({ + "draw": draw, + "recordsTotal": count, + "recordsFiltered": count_filtered, + "data": data, + }) + + +def appointment_types_to_str(appointment_types): + result = "" + for appointment_type in appointment_types: + result += unicode(appointment_type.code) + ", " + return result + + +def serialize_visit(visit): + appointment_types = visit.appointment_types.all() + appointment_types_in_progress = AppointmentType.objects.filter(Q(appointmenttypelink__appointment__visit=visit) & Q( + appointmenttypelink__appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED)).distinct().all() + appointment_types_done = AppointmentType.objects.filter(Q(appointmenttypelink__appointment__visit=visit) & Q( + appointmenttypelink__appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED)).distinct().all() + + appointment_types_missing = [] + for appointment_type in appointment_types: + if appointment_types_in_progress.filter(id=appointment_type.id).count() == 0 and appointment_types_done.filter( + id=appointment_type.id).count() == 0: + appointment_types_missing.append(appointment_type) + + result = { + "first_name": visit.subject.subject.first_name, + "last_name": visit.subject.subject.last_name, + "datetime_begin": serialize_date(visit.datetime_begin), + "datetime_end": serialize_date(visit.datetime_end), + "flying_team": flying_team_to_str(visit.subject.flying_team), + "default_location": location_to_str(visit.subject.default_location), + "is_finished": bool_to_yes_no(visit.is_finished), + "post_mail_sent": bool_to_yes_no(visit.post_mail_sent), + "visit_number": visit.visit_number, + "id": visit.id, + "visible_appointment_types_in_progress": appointment_types_to_str(appointment_types_in_progress), + "visible_appointment_types_done": appointment_types_to_str(appointment_types_done), + "visible_appointment_types_missing": appointment_types_to_str(appointment_types_missing), + "visible_appointment_types": appointment_types_to_str(appointment_types), + } + + return result diff --git a/smash/web/migrations/0083_auto_20171205_1251.py b/smash/web/migrations/0083_auto_20171205_1251.py new file mode 100644 index 0000000000000000000000000000000000000000..cce682bd38f6119785b96ce29c81d5ecc1abed1f --- /dev/null +++ b/smash/web/migrations/0083_auto_20171205_1251.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-05 12:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0082_studysubjectlist_visits'), + ] + + operations = [ + migrations.CreateModel( + name='StudyVisitList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[(b'GENERIC', b'Generic')], max_length=50, verbose_name=b'Type o list')), + ('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Study')), + ('visible_subject_columns', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.SubjectColumns')), + ], + ), + migrations.CreateModel( + name='VisitColumns', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datetime_begin', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Visit starts date')), + ('datetime_end', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Visit ends date')), + ('is_finished', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Is finished')), + ('post_mail_sent', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Post mail sent')), + ('visit_number', models.BooleanField(choices=[(True, b'Yes'), (False, b'No')], default=True, verbose_name=b'Visit number')), + ], + ), + migrations.AddField( + model_name='studyvisitlist', + name='visible_visit_columns', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.VisitColumns'), + ), + ] diff --git a/smash/web/migrations/0084_auto_20171205_1640.py b/smash/web/migrations/0084_auto_20171205_1640.py new file mode 100644 index 0000000000000000000000000000000000000000..10e2c9e1b343cab292e032586bb25a0f6e11796c --- /dev/null +++ b/smash/web/migrations/0084_auto_20171205_1640.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-05 16:40 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0083_auto_20171205_1251'), + ] + + operations = [ + migrations.AddField( + model_name='studyvisitlist', + name='visible_appointment_types_done', + field=models.BooleanField(default=False, verbose_name=b'Done appointments'), + ), + migrations.AddField( + model_name='studyvisitlist', + name='visible_appointment_types_in_progress', + field=models.BooleanField(default=False, verbose_name=b'Appointments in progress'), + ), + migrations.AddField( + model_name='studyvisitlist', + name='visible_appointment_types_missing', + field=models.BooleanField(default=False, verbose_name=b'Missing appointments'), + ), + migrations.AddField( + model_name='studyvisitlist', + name='visible_study_subject_columns', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='web.StudyColumns'), + preserve_default=False, + ), + migrations.AddField( + model_name='visitcolumns', + name='visible_appointment_types', + field=models.BooleanField(default=False, verbose_name=b'All appointments'), + ), + migrations.AlterField( + model_name='studyvisitlist', + name='type', + field=models.CharField(choices=[(b'UNFINISHED', b'unfinished visits'), + (b'APPROACHING_WITHOUT_APPOINTMENTS', b'approaching visits'), + (b'APPROACHING_FOR_MAIL_CONTACT', b'post mail for approaching visits'), + (b'GENERIC', b'Generic'), + (b'MISSING_APPOINTMENTS', b'visits with missing appointments'), + (b'EXCEEDED_TIME', b'exceeded visit time')], max_length=50, + verbose_name=b'Type of list'), + ), + ] diff --git a/smash/web/migrations/0085_auto_20171205_1650.py b/smash/web/migrations/0085_auto_20171205_1650.py new file mode 100644 index 0000000000000000000000000000000000000000..2ffcafec5c7f58f48eafb8b20dd9c61fa809d1cd --- /dev/null +++ b/smash/web/migrations/0085_auto_20171205_1650.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-05 16:50 +from __future__ import unicode_literals + +from django.db import migrations, models + +# noinspection PyUnusedLocal +# noinspection PyPep8Naming +def create_default_columns_for_VISIT_LIST_EXCEEDED_TIME(apps, schema_editor): + # We can't import the Study model directly as it may be a newer + # version than this migration expects. We use the historical version. + SubjectColumns = apps.get_model("web", "SubjectColumns") + subject_columns = SubjectColumns.objects.create() + subject_columns.sex = False + subject_columns.first_name = True + subject_columns.last_name = True + subject_columns.languages = False + subject_columns.default_written_communication_language = False + subject_columns.phone_number = False + subject_columns.phone_number_2 = False + subject_columns.phone_number_3 = False + subject_columns.email = False + subject_columns.date_born = False + subject_columns.address = False + subject_columns.postal_code = False + subject_columns.city = False + subject_columns.country = False + subject_columns.dead = False + subject_columns.save() + + StudyColumns = apps.get_model("web", "StudyColumns") + study_columns = StudyColumns.objects.create() + study_columns.postponed = False + study_columns.datetime_contact_reminder = False + study_columns.type = False + study_columns.default_location = True + study_columns.flying_team = True + study_columns.screening_number = False + study_columns.nd_number = False + study_columns.mpower_id = False + study_columns.comments = False + study_columns.referral = False + study_columns.diagnosis = False + study_columns.year_of_diagnosis = False + study_columns.information_sent = False + study_columns.pd_in_family = False + study_columns.resigned = False + study_columns.resign_reason = False + study_columns.save() + + VisitColumns = apps.get_model("web", "VisitColumns") + visit_columns = VisitColumns.objects.create() + visit_columns.datetime_begin = False + visit_columns.datetime_end = True + visit_columns.is_finished = False + visit_columns.post_mail_sent = False + visit_columns.visit_number = True + visit_columns.visible_appointment_types = False + visit_columns.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0084_auto_20171205_1640'), + ] + + operations = [ + migrations.AlterField( + model_name='studysubjectlist', + name='type', + field=models.CharField(blank=True, choices=[(b'GENERIC', b'Generic'), (b'NO_VISIT', b'Subjects without visit'), (b'REQUIRE_CONTACT', b'Subjects required contact')], max_length=50, null=True, verbose_name=b'Type of list'), + ), + migrations.RunPython(create_default_columns_for_VISIT_LIST_EXCEEDED_TIME), + migrations.RunSQL('INSERT INTO web_studyvisitlist (' + + 'study_id, ' + + 'visible_visit_columns_id, ' + + 'visible_subject_columns_id, ' + + 'visible_study_subject_columns_id, ' + + 'visible_appointment_types_done,' + 'visible_appointment_types_in_progress,' + 'visible_appointment_types_missing,' + 'type) ' + + "SELECT " + + "1, " + + "max(web_visitcolumns.id), " + + "max(web_subjectcolumns.id), " + + "max(web_studycolumns.id), " + + "TRUE, " + + "TRUE, " + + "TRUE, " + + "'EXCEEDED_TIME' FROM web_visitcolumns, web_studycolumns, web_subjectcolumns;"), + ] diff --git a/smash/web/migrations/0086_unfinished_visit_list.py b/smash/web/migrations/0086_unfinished_visit_list.py new file mode 100644 index 0000000000000000000000000000000000000000..9b8f79975a0b56335cc1b52f1e391cb1dce7938c --- /dev/null +++ b/smash/web/migrations/0086_unfinished_visit_list.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-05 16:50 +from __future__ import unicode_literals + +from django.db import migrations, models + +# noinspection PyUnusedLocal +# noinspection PyPep8Naming +def create_default_columns_for_VISIT_LIST_EXCEEDED_TIME(apps, schema_editor): + # We can't import the Study model directly as it may be a newer + # version than this migration expects. We use the historical version. + SubjectColumns = apps.get_model("web", "SubjectColumns") + subject_columns = SubjectColumns.objects.create() + subject_columns.sex = False + subject_columns.first_name = True + subject_columns.last_name = True + subject_columns.languages = False + subject_columns.default_written_communication_language = False + subject_columns.phone_number = False + subject_columns.phone_number_2 = False + subject_columns.phone_number_3 = False + subject_columns.email = False + subject_columns.date_born = False + subject_columns.address = False + subject_columns.postal_code = False + subject_columns.city = False + subject_columns.country = False + subject_columns.dead = False + subject_columns.save() + + StudyColumns = apps.get_model("web", "StudyColumns") + study_columns = StudyColumns.objects.create() + study_columns.postponed = False + study_columns.datetime_contact_reminder = False + study_columns.type = False + study_columns.default_location = True + study_columns.flying_team = True + study_columns.screening_number = False + study_columns.nd_number = False + study_columns.mpower_id = False + study_columns.comments = False + study_columns.referral = False + study_columns.diagnosis = False + study_columns.year_of_diagnosis = False + study_columns.information_sent = False + study_columns.pd_in_family = False + study_columns.resigned = False + study_columns.resign_reason = False + study_columns.save() + + VisitColumns = apps.get_model("web", "VisitColumns") + visit_columns = VisitColumns.objects.create() + visit_columns.datetime_begin = False + visit_columns.datetime_end = True + visit_columns.is_finished = False + visit_columns.post_mail_sent = False + visit_columns.visit_number = True + visit_columns.visible_appointment_types = False + visit_columns.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0085_auto_20171205_1650'), + ] + + operations = [ + migrations.RunPython(create_default_columns_for_VISIT_LIST_EXCEEDED_TIME), + migrations.RunSQL('INSERT INTO web_studyvisitlist (' + + 'study_id, ' + + 'visible_visit_columns_id, ' + + 'visible_subject_columns_id, ' + + 'visible_study_subject_columns_id, ' + + 'visible_appointment_types_done,' + 'visible_appointment_types_in_progress,' + 'visible_appointment_types_missing,' + 'type) ' + + "SELECT " + + "1, " + + "max(web_visitcolumns.id), " + + "max(web_subjectcolumns.id), " + + "max(web_studycolumns.id), " + + "TRUE, " + + "TRUE, " + + "TRUE, " + + "'UNFINISHED' FROM web_visitcolumns, web_studycolumns, web_subjectcolumns;"), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index 7d37f387ad3dd5da911ecc29472cb4687d0c2fb6..746b15bf599f4f08e2605b8274ddc97137b609d8 100644 --- a/smash/web/models/__init__.py +++ b/smash/web/models/__init__.py @@ -10,6 +10,7 @@ from appointment_type_link import AppointmentTypeLink from country import Country from subject_columns import SubjectColumns from study_columns import StudyColumns +from visit_columns import VisitColumns from notification_columns import StudyNotificationParameters from study import Study from room import Room @@ -24,6 +25,7 @@ from language import Language from subject import Subject from study_subject import StudySubject from study_subject_list import StudySubjectList +from study_visit_list import StudyVisitList from contact_attempt import ContactAttempt from mail_template import MailTemplate from missing_subject import MissingSubject @@ -32,4 +34,4 @@ from inconsistent_subject import InconsistentSubject, InconsistentField __all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, Subject, StudySubject, StudySubjectList, SubjectColumns, StudyNotificationParameters, Visit, Worker, ContactAttempt, ConfigurationItem, MailTemplate, AppointmentTypeLink, MissingSubject, - InconsistentSubject, InconsistentField, Country, StudyColumns] + InconsistentSubject, InconsistentField, Country, StudyColumns, VisitColumns, StudyVisitList] diff --git a/smash/web/models/study_subject_list.py b/smash/web/models/study_subject_list.py index 1b09054fd1391f85de1bbaccf8a390c63bc2a09c..cd2f5bbbbfff4142032e0cd68b9624b53e0f7668 100644 --- a/smash/web/models/study_subject_list.py +++ b/smash/web/models/study_subject_list.py @@ -48,7 +48,7 @@ class StudySubjectList(models.Model): type = models.CharField(max_length=50, choices=SUBJECT_LIST_CHOICES.items(), - verbose_name='Type o list', + verbose_name='Type of list', null=True, blank=True ) diff --git a/smash/web/models/study_visit_list.py b/smash/web/models/study_visit_list.py new file mode 100644 index 0000000000000000000000000000000000000000..e16c334ec9a97fcf953ea49fe36a454c0ccf49ba --- /dev/null +++ b/smash/web/models/study_visit_list.py @@ -0,0 +1,64 @@ +# coding=utf-8 +from django.db import models + +from web.models import Study, SubjectColumns, VisitColumns +from web.models import StudyColumns + +VISIT_LIST_GENERIC = "GENERIC" +VISIT_LIST_EXCEEDED_TIME = "EXCEEDED_TIME" +VISIT_LIST_UNFINISHED = "UNFINISHED" +VISIT_LIST_MISSING_APPOINTMENTS = "MISSING_APPOINTMENTS" +VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS = "APPROACHING_WITHOUT_APPOINTMENTS" +VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT = "APPROACHING_FOR_MAIL_CONTACT" + +VISIT_LIST_CHOICES = { + VISIT_LIST_GENERIC: 'Generic', + VISIT_LIST_EXCEEDED_TIME: 'exceeded visit time', + VISIT_LIST_UNFINISHED: 'unfinished visits', + VISIT_LIST_MISSING_APPOINTMENTS: 'visits with missing appointments', + VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS: 'approaching visits', + VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT: 'post mail for approaching visits', +} + + +class StudyVisitList(models.Model): + class Meta: + app_label = 'web' + + study = models.ForeignKey( + Study, + on_delete=models.CASCADE, + null=False, + ) + + visible_visit_columns = models.ForeignKey( + VisitColumns, + on_delete=models.CASCADE, + null=False, + ) + + visible_subject_columns = models.ForeignKey( + SubjectColumns, + on_delete=models.CASCADE, + null=False, + ) + visible_study_subject_columns = models.ForeignKey( + StudyColumns, + on_delete=models.CASCADE, + null=False, + ) + + visible_appointment_types_in_progress = models.BooleanField(default=False, + verbose_name='Appointments in progress' + ) + visible_appointment_types_done = models.BooleanField(default=False, + verbose_name='Done appointments' + ) + visible_appointment_types_missing = models.BooleanField(default=False, + verbose_name='Missing appointments' + ) + + type = models.CharField(max_length=50, + choices=VISIT_LIST_CHOICES.items(), + verbose_name='Type of list', + ) diff --git a/smash/web/models/visit_columns.py b/smash/web/models/visit_columns.py new file mode 100644 index 0000000000000000000000000000000000000000..6984c6de1ad5aef2ea64f389711d169cf1f8e076 --- /dev/null +++ b/smash/web/models/visit_columns.py @@ -0,0 +1,37 @@ +# coding=utf-8 +from django.db import models + +from web.models.constants import BOOL_CHOICES + + +class VisitColumns(models.Model): + class Meta: + app_label = 'web' + + datetime_begin = models.BooleanField(choices=BOOL_CHOICES, + verbose_name='Visit starts date', + default=True + ) + + datetime_end = models.BooleanField(choices=BOOL_CHOICES, + verbose_name='Visit ends date', + default=True + ) + + is_finished = models.BooleanField(choices=BOOL_CHOICES, + verbose_name='Is finished', + default=True + ) + + post_mail_sent = models.BooleanField(choices=BOOL_CHOICES, + verbose_name='Post mail sent', + default=True + ) + + visit_number = models.BooleanField(choices=BOOL_CHOICES, + verbose_name='Visit number', + default=True + ) + visible_appointment_types = models.BooleanField(default=False, + verbose_name='All appointments' + ) diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index 3bfb678925183ceaf0ac7f5f88e61d4700ca48ce..6da68884c152b60a3e56823dc4e808d4b604e76c 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -70,4 +70,294 @@ function showErrorInfo(content) { $(errorDialogDiv).attr("role", "dialog"); $(errorDialogDiv).addClass("modal modal-danger fade"); $(errorDialogDiv).modal("show"); -} \ No newline at end of file +} + +//------------------------------------------------------ +// common code for dynamic Data Tables +//------------------------------------------------------ + +if (!String.prototype.startsWith) { + String.prototype.startsWith = function (searchString, position) { + position = position || 0; + return this.indexOf(searchString, position) === position; + }; +} + +function createColumn(dataType, name, filter, visible, sortable, renderFunction) { + if (renderFunction === undefined) { + renderFunction = function (data, type, row, meta) { + return row[dataType]; + } + } + return { + "type": dataType, + "name": name, + "filter": filter, + "visible": visible, + "render": renderFunction, + "sortable": sortable + }; +} + +function getColumns(columns, getSubjectEditUrl) { + var result = []; + for (var i = 0; i < columns.length; i++) { + var columnRow = columns[i]; + if (columnRow.type === "edit") { + result.push(createColumn("id", columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable, function (data, type, row) { + var url = getSubjectEditUrl(row.id.toString()); + + return '<a href="' + url + '" type="button" class="btn btn-block btn-default">Edit</a>'; + })); + } else if (/^visit_[0-9]+$/.test(columnRow.type)) { + var renderFunction = (function () { + var x = columnRow.type.replace("visit_", ""); + return function (data, type, row) { + return create_visit_row(row.visits[x - 1]); + }; + })(); + + result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable, renderFunction)); + + } else { + result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable)); + } + } + return result; +} + +function createHeader(columnsDefinition) { + var header = document.createElement("thead"); + var headerRow = document.createElement("tr"); + header.appendChild(headerRow); + for (var i = 0; i < columnsDefinition.length; i++) { + var column = columnsDefinition[i]; + var element = document.createElement("th"); + element.innerHTML = column.name; + headerRow.appendChild(element); + } + return header; +} + +function createFilter(columnsDefinition) { + var footer = document.createElement("tfoot"); + footer.style.display = "table-header-group"; + var footerRow = document.createElement("tr"); + footer.appendChild(footerRow); + for (var i = 0; i < columnsDefinition.length; i++) { + var column = columnsDefinition[i]; + var element = document.createElement("th"); + if (column.filter !== null) { + element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>"; + } + footerRow.appendChild(element); + } + return footer; +} + +function create_visit_row(visit) { + var color = "white"; + var text = "---"; + if (visit !== undefined && visit !== null) { + if (visit.status === "DONE") { + color = "green"; + text = "OK"; + } else if (visit.status === "MISSED") { + color = "pink"; + text = "MISSED"; + } else if (visit.status === "UPCOMING") { + color = "#00ffff"; + text = "UPCOMING"; + } else if (visit.status === "EXCEEDED") { + color = "orange"; + text = "EXCEEDED"; + } else if (visit.status === "SHOULD_BE_IN_PROGRESS") { + color = "orange"; + text = "IN PROGRESS (NO APPOINTMENTS)"; + } else if (visit.status === "IN_PROGRESS") { + color = "lightgreen"; + text = "IN PROGRESS"; + } + text += "<br/>" + visit.datetime_start + " - " + visit.datetime_end; + } + return "<div style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>"; +} + +function createVisibilityCheckboxes(checkboxesElement, columns) { + var row = null; + for (var i = 0; i < columns.length; i++) { + if (i % 10 === 0) { + row = document.createElement("div"); + row.style.display = "table-row"; + checkboxesElement.appendChild(row); + } + var column = columns[i]; + var element = document.createElement("div"); + element.style.display = "table-cell"; + var checked = ""; + if (column.visible) { + checked = "checked"; + } + element.innerHTML = "<input type='checkbox' " + checked + " data-column='" + i + "' name='" + column.type + "'/>" + column.name; + row.appendChild(element); + } + +} + +function createTable(params) { + var tableElement = params.tableElement; + var worker_locations = params.worker_locations; + if (worker_locations === undefined) { + worker_locations = []; + } + var subject_types_url = params.subject_types_url; + var locations_url = params.locations_url; + var flying_teams_url = params.flying_teams_url; + var appointment_types_url = params.appointment_types_url; + var subjects_url = params.subjects_url; + var columnsDefinition = params.columns; + + tableElement.appendChild(createHeader(columnsDefinition)); + tableElement.appendChild(createFilter(columnsDefinition)); + tableElement.appendChild(document.createElement("tbody")); + createVisibilityCheckboxes(params.checkboxesElement, columnsDefinition); + + var table; + $(tableElement).find('tfoot div[name="string_filter"]').each(function () { + var title = $(this).text(); + $(this).html('<input type="text" style="width:80px" placeholder="' + title + '" />'); + }); + + $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () { + $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>'); + }); + $(tableElement).find('tfoot div[name="integer_filter"]').each(function () { + var options = '<option value selected="selected">---</option>'; + for (var i = 1; i <= 8; i++) { + options += '<option value="' + i + '">' + i + '</option>'; + } + $(this).html('<select style="width:60px" >' + options + '</select>'); + }); + + $(tableElement).find('tfoot div[name="visit_filter"]').each(function () { + $(this).html('<select style="width:60px" >' + + '<option value selected="selected">---</option>' + + '<option value="MISSED">MISSED</option>' + + '<option value="DONE">DONE</option>' + + '<option value="EXCEED">EXCEED</option>' + + '<option value="IN_PROGRESS">IN PROGRESS</option>' + + '<option value="SHOULD_BE_IN_PROGRESS">SHOULD BE IN PROGRESS</option>' + + '<option value="UPCOMING">UPCOMING</option>' + + '</select>'); + }); + + $(tableElement).find('tfoot div[name="location_filter"]').each(function () { + var obj = $(this); + obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); + var select = $('select', obj); + $.get(locations_url, function (data) { + $.each(data.locations, function (index, location) { + select.append('<option value="' + location.id + '">' + location.name + '</option>'); + }); + if (worker_locations.length === 1) { + select.val(worker_locations[0].id); + } + }); + }); + + $(tableElement).find('tfoot div[name="flying_team_filter"]').each(function () { + var obj = $(this); + obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); + var select = $('select', obj); + $.get(flying_teams_url, function (data) { + $.each(data.flying_teams, function (index, flying_team) { + select.append('<option value="' + flying_team.id + '">' + flying_team.name + '</option>'); + }); + if (worker_locations.length === 1) { + select.val(worker_locations[0].id); + } + }); + }); + + $(tableElement).find('tfoot div[name="appointment_type_filter"]').each(function () { + var obj = $(this); + obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); + var select = $('select', obj); + $.get(appointment_types_url, function (data) { + $.each(data.appointment_types, function (index, appointment_type) { + select.append('<option value="' + appointment_type.id + '">' + appointment_type.type + '</option>'); + }); + if (worker_locations.length === 1) { + select.val(worker_locations[0].id); + } + }); + }); + + $(tableElement).find('tfoot div[name="type_filter"]').each(function () { + var obj = $(this); + obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); + var select = $('select', obj); + $.get(subject_types_url, function (data) { + $.each(data.types, function (index, type) { + select.append('<option value="' + type.id + '">' + type.name + '</option>'); + }); + }); + }); + + + $(function () { + var columns = []; + var columnDefs = []; + for (var i = 0; i < columnsDefinition.length; i++) { + var column = columnsDefinition[i]; + columns.push({"data": column.type}); + columnDefs.push({ + "targets": i, + "render": column.render, + visible: column.visible, + orderable: column.sortable + }); + } + + + table = $('#table').DataTable({ + pageLength: 25, + serverSide: true, + processing: true, + responsive: true, + ajax: subjects_url, + columns: columns, + columnDefs: columnDefs, + order: [[0, 'desc']] + }); + + // Apply the search + table.columns().every(function () { + var that = this; + + $('input', this.footer()).on('keyup change', function () { + if (that.search() !== this.value) { + that.search(this.value).draw(); + } + }); + $('select', this.footer()).on('keyup change', function () { + if (that.search() !== this.value) { + that.search(this.value).draw(); + } + }); + }); + $('#table_filter').css("display", "null"); + }); + $('#visible-column-checkboxes input').on('click', function (e) { + var visible = $(this).is(":checked"); + + // Get the column API object + var column = table.column($(this).attr('data-column')); + // Toggle the visibility + column.visible(visible); + }); +} + +//------------------------------------------------------ +// END common code for dynamic Data Tables +//------------------------------------------------------ diff --git a/smash/web/static/js/subject.js b/smash/web/static/js/subject.js index 585d1b9f5b568e31ffa03dcc29fbe23fafcb45d1..434b796a2e48a78f93ecd710d02de1ad9f453efa 100644 --- a/smash/web/static/js/subject.js +++ b/smash/web/static/js/subject.js @@ -1,253 +1,3 @@ -if (!String.prototype.startsWith) { - String.prototype.startsWith = function (searchString, position) { - position = position || 0; - return this.indexOf(searchString, position) === position; - }; -} - -function createColumn(dataType, name, filter, visible, renderFunction) { - if (renderFunction === undefined) { - renderFunction = function (data, type, row, meta) { - return row[dataType]; - } - } - return { - "type": dataType, - "name": name, - "filter": filter, - "visible": visible, - "render": renderFunction - }; -} - -function getColumns(columns, getSubjectEditUrl) { - var result = []; - for (var i = 0; i < columns.length; i++) { - var columnRow = columns[i]; - if (columnRow.type === "edit") { - result.push(createColumn("id", columnRow.name, columnRow.filter, columnRow.visible, function (data, type, row) { - var url = getSubjectEditUrl(row.id.toString()); - - return '<a href="' + url + '" type="button" class="btn btn-block btn-default">Edit</a>'; - })); - } else if (columnRow.type.startsWith("visit")) { - var renderFunction = (function () { - var x = i; - return function (data, type, row) { - return create_visit_row(row.visits[x - 1]); - }; - })(); - - result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, renderFunction)); - - } else { - result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible)); - } - } - return result; -} - -function createHeader(columnsDefinition) { - var header = document.createElement("thead"); - var headerRow = document.createElement("tr"); - header.appendChild(headerRow); - for (var i = 0; i < columnsDefinition.length; i++) { - var column = columnsDefinition[i]; - var element = document.createElement("th"); - element.innerHTML = column.name; - headerRow.appendChild(element); - } - return header; -} - -function createFilter(columnsDefinition) { - var footer = document.createElement("tfoot"); - footer.style.display = "table-header-group"; - var footerRow = document.createElement("tr"); - footer.appendChild(footerRow); - for (var i = 0; i < columnsDefinition.length; i++) { - var column = columnsDefinition[i]; - var element = document.createElement("th"); - if (column.filter !== null) { - element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>"; - } - footerRow.appendChild(element); - } - return footer; -} - -function create_visit_row(visit) { - var color = "white"; - var text = "---"; - if (visit !== undefined && visit !== null) { - if (visit.status === "DONE") { - color = "green"; - text = "OK"; - } else if (visit.status === "MISSED") { - color = "pink"; - text = "MISSED"; - } else if (visit.status === "UPCOMING") { - color = "#00ffff"; - text = "UPCOMING"; - } else if (visit.status === "EXCEEDED") { - color = "orange"; - text = "EXCEEDED"; - } else if (visit.status === "SHOULD_BE_IN_PROGRESS") { - color = "orange"; - text = "IN PROGRESS (NO APPOINTMENTS)"; - } else if (visit.status === "IN_PROGRESS") { - color = "lightgreen"; - text = "IN PROGRESS"; - } - text += "<br/>" + visit.datetime_start + " - " + visit.datetime_end; - } - return "<div style='background-color:" + color + "';width:100%;height:100%>" + text + "</div>"; -} - -function createVisibilityCheckboxes(checkboxesElement, columns) { - var row = null; - for (var i = 0; i < columns.length; i++) { - if (i % 10 === 0) { - row = document.createElement("div"); - row.style.display = "table-row"; - checkboxesElement.appendChild(row); - } - var column = columns[i]; - var element = document.createElement("div"); - element.style.display = "table-cell"; - var checked = ""; - if (column.visible) { - checked = "checked"; - } - element.innerHTML = "<input type='checkbox' " + checked + " data-column='" + i + "' name='" + column.type + "'/>" + column.name; - row.appendChild(element); - } - -} - function createSubjectsTable(params) { - var tableElement = params.tableElement; - var worker_locations = params.worker_locations; - var subject_types_url = params.subject_types_url; - var locations_url = params.locations_url; - var flying_teams_url = params.flying_teams_url; - var subjects_url = params.subjects_url; - var columnsDefinition = params.columns; - - tableElement.appendChild(createHeader(columnsDefinition)); - tableElement.appendChild(createFilter(columnsDefinition)); - tableElement.appendChild(document.createElement("tbody")); - createVisibilityCheckboxes(params.checkboxesElement, columnsDefinition); - - var table; - $(tableElement).find('tfoot div[name="string_filter"]').each(function () { - var title = $(this).text(); - $(this).html('<input type="text" style="width:80px" placeholder="' + title + '" />'); - }); - - $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () { - $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>'); - }); - - $(tableElement).find('tfoot div[name="visit_filter"]').each(function () { - $(this).html('<select style="width:60px" >' + - '<option value selected="selected">---</option>' + - '<option value="MISSED">MISSED</option>' + - '<option value="DONE">DONE</option>' + - '<option value="EXCEED">EXCEED</option>' + - '<option value="IN_PROGRESS">IN PROGRESS</option>' + - '<option value="SHOULD_BE_IN_PROGRESS">SHOULD BE IN PROGRESS</option>' + - '<option value="UPCOMING">UPCOMING</option>' + - '</select>'); - }); - - $(tableElement).find('tfoot div[name="location_filter"]').each(function () { - var obj = $(this); - obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); - var select = $('select', obj); - $.get(locations_url, function (data) { - $.each(data.locations, function (index, location) { - select.append('<option value="' + location.id + '">' + location.name + '</option>'); - }); - if (worker_locations.length === 1) { - select.val(worker_locations[0].id); - } - }); - }); - - $(tableElement).find('tfoot div[name="flying_team_filter"]').each(function () { - var obj = $(this); - obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); - var select = $('select', obj); - $.get(flying_teams_url, function (data) { - $.each(data.flying_teams, function (index, flying_team) { - select.append('<option value="' + flying_team.id + '">' + flying_team.name + '</option>'); - }); - if (worker_locations.length === 1) { - select.val(worker_locations[0].id); - } - }); - }); - - - - $(tableElement).find('tfoot div[name="type_filter"]').each(function () { - var obj = $(this); - obj.html('<select style="width:80px"><option value selected="selected">---</option></select>'); - var select = $('select', obj); - $.get(subject_types_url, function (data) { - $.each(data.types, function (index, type) { - select.append('<option value="' + type.id + '">' + type.name + '</option>'); - }); - }); - }); - - - $(function () { - var columns = []; - var columnDefs = []; - for (var i = 0; i < columnsDefinition.length; i++) { - var column = columnsDefinition[i]; - columns.push({"data": column.type}); - columnDefs.push({"targets": i, "render": column.render, visible: column.visible}); - } - - - table = $('#table').DataTable({ - pageLength: 25, - serverSide: true, - processing: true, - responsive: true, - ajax: subjects_url, - columns: columns, - columnDefs: columnDefs, - order: [[0, 'desc']] - }); - - // Apply the search - table.columns().every(function () { - var that = this; - - $('input', this.footer()).on('keyup change', function () { - if (that.search() !== this.value) { - that.search(this.value).draw(); - } - }); - $('select', this.footer()).on('keyup change', function () { - if (that.search() !== this.value) { - that.search(this.value).draw(); - } - }); - }); - $('#table_filter').css("display", "null"); - }); - $('#visible-column-checkboxes input').on('click', function (e) { - var visible = $(this).is(":checked"); - - // Get the column API object - var column = table.column($(this).attr('data-column')); - console.log($(this).attr('data-column')); - // Toggle the visibility - column.visible(visible); - }); -} \ No newline at end of file + return createTable(params); +} diff --git a/smash/web/static/js/visit.js b/smash/web/static/js/visit.js index c4eac088fd1fe16a7bad5f2baa307481184bc58b..a2636ed219748484b648296a736608d14732589c 100644 --- a/smash/web/static/js/visit.js +++ b/smash/web/static/js/visit.js @@ -10,3 +10,8 @@ function visit_dates_behaviour(startDateInput, endDateInput) { } }); } + + +function createVisitsTable(params) { + return createTable(params); +} diff --git a/smash/web/templates/_base.html b/smash/web/templates/_base.html index 8779d279c12492c247e55b19c3e5223cc6803d8c..4f4b59bf72f636625948a85cbd018358e2857a39 100644 --- a/smash/web/templates/_base.html +++ b/smash/web/templates/_base.html @@ -186,8 +186,8 @@ desired effect </div> </div> <!-- /.row --> - {% endcomment %} </li> + {% endcomment %} <!-- Menu Footer--> <li class="user-footer"> <!--<div class="pull-left"> diff --git a/smash/web/templates/mail_templates/index.html b/smash/web/templates/mail_templates/index.html deleted file mode 100644 index 137ade1b2b90f7773f205a2a6d81981db5212e75..0000000000000000000000000000000000000000 --- a/smash/web/templates/mail_templates/index.html +++ /dev/null @@ -1,134 +0,0 @@ -{% extends "_base.html" %} -{% load static %} - -{% block styles %} - {{ block.super }} - <!-- DataTables --> - <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> -{% endblock styles %} - -{% block ui_active_tab %}'mail_templates'{% endblock ui_active_tab %} -{% block page_header %}Mail templates{% endblock page_header %} -{% block page_description %}{% endblock page_description %} - -{% block breadcrumb %} - {% include "mail_templates/breadcrumb.html" %} -{% endblock breadcrumb %} - -{% block maincontent %} - <div class="box box-danger box-solid"> - <div class="box-header with-border"> - <h3 class="box-title">Not yet implemented</h3> - - <div class="box-tools pull-right"> - <button type="button" class="btn btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button> - </div> - <!-- /.box-tools --> - </div> - <!-- /.box-header --> - <div class="box-body"> - This is only a static page to show, how the system would look like. - A comprehensive system for template editing and generation has to be developed. - </div> - <!-- /.box-body --> - </div> - - <div> - <a class="btn btn-app"> - <i class="fa fa-plus"></i> Add new letter - </a> - </div> - - <div class="box-body"> - <table id="table" class="table table-bordered table-striped"> - <thead> - <tr> - <th>No.</th> - <th>Destination</th> - <th>Language</th> - <th>Title</th> - <th>Details</th> - <th>Edit</th> - <th>Delete</th> - <th>Generate</th> - </tr> - </thead> - <tbody> - <tr> - <td>1</td> - <td>Patients</td> - <td>english</td> - <td>"Thank you" letter</td> - <td> - <button type="button" class="btn btn-block btn-default">Details</button> - </td> - <td> - <button type="button" class="btn btn-block btn-warning">Edit</button> - </td> - <td> - <button type="button" class="btn btn-block btn-danger">Delete</button> - </td> - <td> - <button type="button" class="btn btn-block btn-default">Generate</button> - </td> - </tr> - <tr> - <td>2</td> - <td>Patients</td> - <td>english</td> - <td>"Dankescreiben" letter</td> - <td> - <button type="button" class="btn btn-block btn-default">Details</button> - </td> - <td> - <button type="button" class="btn btn-block btn-warning">Edit</button> - </td> - <td> - <button type="button" class="btn btn-block btn-danger">Delete</button> - </td> - <td> - <button type="button" class="btn btn-block btn-default">Generate</button> - </td> - </tr> - <tr> - <td>3</td> - <td>Doctors</td> - <td>english</td> - <td>"New Scheduling system"</td> - <td> - <button type="button" class="btn btn-block btn-default">Details</button> - </td> - <td> - <button type="button" class="btn btn-block btn-warning">Edit</button> - </td> - <td> - <button type="button" class="btn btn-block btn-danger">Delete</button> - </td> - <td> - <button type="button" class="btn btn-block btn-default">Generate</button> - </td> - </tr> - </tbody> - </table> - </div> -{% endblock maincontent %} - -{% block scripts %} - {{ block.super }} - - <script src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script> - <script src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script> - - <script> - $(function () { - $('#table').DataTable({ - "paging": true, - "lengthChange": false, - "searching": true, - "ordering": true, - "info": true, - "autoWidth": false - }); - }); - </script> -{% endblock scripts %} diff --git a/smash/web/templates/visits/index.html b/smash/web/templates/visits/index.html index 6d82fe9098392bd9c903d3442379326a71c25378..eb25d83536ca7336b78e770abc4c2e9aae082f63 100644 --- a/smash/web/templates/visits/index.html +++ b/smash/web/templates/visits/index.html @@ -7,9 +7,6 @@ <!-- 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"> {% endblock styles %} {% block ui_active_tab %}'visits'{% endblock ui_active_tab %} @@ -23,62 +20,12 @@ {% endblock breadcrumb %} {% block maincontent %} - - <div> - <a href="{% url 'web.views.visit_add' %}" class="btn btn-app"> - <i class="fa fa-plus"></i> - Add new visit - </a> + <div class="box-body"> + <table id="table" class="table table-bordered table-striped table-responsive"> + </table> </div> - - <div> - <div> - {% if visit_list %} - <table id="visit_table" class="table table-bordered table-striped"> - <thead> - <tr> - <th>Subject name</th> - <th>Full information</th> - <th>Visit begins</th> - <th>Visit ends</th> - <th>Finished?</th> - </tr> - </thead> - <tbody> - {% for visit in visit_list %} - <tr> - <td>{{ visit.subject.first_name }} {{ visit.subject.last_name }}</td> - <td> - <a href="{% url 'web.views.visit_details' visit.id %}" type="button" - class="btn btn-block btn-default">Details</a> - </td> - <td> - {{ visit.datetime_begin }} - </td> - <td> - {{ visit.datetime_end }} - </td> - <td> - {% if visit.is_finished %} - <button type="button" class="btn btn-block btn-success">YES</button> - {% else %} - <button type="button" class="btn btn-block btn-danger">NO</button> - {% endif %} - </td> - - </tr> - {% endfor %} - </tbody> - </table> - - {% else %} - <p>No visits to display.</p> - {% endif %} - - <hr/> - - </div> - + <h3>Visible columns</h3> + <div id="visible-column-checkboxes" style="display:table; width:100%"> </div> {% endblock maincontent %} @@ -87,19 +34,22 @@ <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/visit.js' %}"></script> <script> - $(function () { - $('#planning_table, #approaching_table').DataTable({ - "paging": true, - "lengthChange": false, - "searching": true, - "ordering": true, - "info": true, - "autoWidth": false - }); + function getVisitEditUrl(id) { + return "{% url 'web.views.visit_details' 12345678 %}".replace(/12345678/, id); + } + $.get("{% url 'web.api.visits.columns' visit_list_type %}", function (data) { + createVisitsTable({ + locations_url: "{% url 'web.api.locations' %}", + flying_teams_url: "{% url 'web.api.flying_teams' %}", + appointment_types_url: "{% url 'web.api.appointment_types' %}", + subjects_url: "{% url 'web.api.visits' visit_list_type %}", + tableElement: document.getElementById("table"), + columns: getColumns(data.columns, getVisitEditUrl), + checkboxesElement: document.getElementById("visible-column-checkboxes") + }) }); </script> {% endblock scripts %} diff --git a/smash/web/tests/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py index b3da6195882493d7c95980d006977720ebf2bb12..2ed3fc0a5721f39de92e27066bbc28ce6c362882 100644 --- a/smash/web/tests/api_views/test_subject.py +++ b/smash/web/tests/api_views/test_subject.py @@ -3,33 +3,25 @@ import datetime import json import logging -from django.contrib.auth.models import User -from django.test import Client -from django.test import TestCase from django.urls import reverse +from web.tests import LoggedInWithWorkerTestCase from web.api_views.subject import get_subjects_order, get_subjects_filtered, serialize_subject from web.models import StudySubject, Appointment, Study 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 -from web.tests.functions import create_study_subject, create_worker, create_get_suffix, create_visit, \ +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 from web.views.notifications import get_today_midnight_date logger = logging.getLogger(__name__) -class TestApi(TestCase): +class TestSubjectApi(LoggedInWithWorkerTestCase): def setUp(self): + super(TestSubjectApi, self).setUp() self.study_subject = create_study_subject() - 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) - self.client.login(username=username, password=password) def test_cities(self): city_name = "some city" diff --git a/smash/web/tests/api_views/test_visit.py b/smash/web/tests/api_views/test_visit.py new file mode 100644 index 0000000000000000000000000000000000000000..2020af6324887766b1922b4aee8a1721f4dcef94 --- /dev/null +++ b/smash/web/tests/api_views/test_visit.py @@ -0,0 +1,273 @@ +# coding=utf-8 +import datetime +import json +import logging + +from django.urls import reverse + +from web.api_views.visit import get_visits_filtered, get_visits_order +from web.models import Visit, AppointmentTypeLink, Appointment +from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_EXCEEDED_TIME, VISIT_LIST_UNFINISHED, \ + VISIT_LIST_MISSING_APPOINTMENTS, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, \ + VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT +from web.tests import LoggedInWithWorkerTestCase +from web.tests.functions import create_study_subject, create_get_suffix, create_visit, create_appointment_type, \ + create_flying_team, create_appointment +from web.views.notifications import get_today_midnight_date + +logger = logging.getLogger(__name__) + + +class TestVisitApi(LoggedInWithWorkerTestCase): + def setUp(self): + super(TestVisitApi, self).setUp() + self.study_subject = create_study_subject() + self.visit = create_visit(self.study_subject) + + def test_get_columns(self): + response = self.client.get(reverse('web.api.visits.columns', kwargs={'visit_list_type': VISIT_LIST_GENERIC})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + self.assertTrue(len(columns) > 0) + + def test_get_columns_for_time_exceeded(self): + response = self.client.get( + reverse('web.api.visits.columns', kwargs={'visit_list_type': VISIT_LIST_EXCEEDED_TIME})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + self.assertTrue(len(columns) > 0) + + def test_visits_exceeded(self): + response = self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_EXCEEDED_TIME})) + self.assertEqual(response.status_code, 200) + + def test_visits_generic(self): + self.visit.appointment_types.add(create_appointment_type()) + self.visit.save() + response = self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_GENERIC})) + self.assertEqual(response.status_code, 200) + + def test_visits_unfinished(self): + response = self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_UNFINISHED})) + self.assertEqual(response.status_code, 200) + + def test_visits_missing_appointments(self): + response = self.client.get( + reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_MISSING_APPOINTMENTS})) + self.assertEqual(response.status_code, 200) + + def test_visits_approaching_without_appointments(self): + response = self.client.get( + reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS})) + self.assertEqual(response.status_code, 200) + + def test_visits_approaching_for_mail_contact(self): + response = self.client.get( + reverse('web.api.visits', kwargs={'visit_list_type': VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT})) + self.assertEqual(response.status_code, 200) + + def test_visits_invalid(self): + with self.assertRaises(TypeError): + self.client.get(reverse('web.api.visits', kwargs={'visit_list_type': "invalid_type"})) + + def test_visits_general_search(self): + name = "Piotrek" + self.study_subject.subject.first_name = name + self.study_subject.subject.save() + create_visit(self.study_subject) + + params = { + "columns[0][search][value]": "another_name", + "columns[0][data]": "first_name" + } + url = ("%s" + create_get_suffix(params)) % reverse('web.api.visits', + kwargs={'visit_list_type': VISIT_LIST_GENERIC}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertFalse(name in response.content) + + params["columns[0][search][value]"] = name + url = ("%s" + create_get_suffix(params)) % reverse('web.api.visits', + kwargs={'visit_list_type': VISIT_LIST_GENERIC}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTrue(name in response.content) + + def check_visit_filtered(self, filters, result): + subjects = get_visits_filtered(Visit.objects.all(), filters) + self.assertEqual(len(result), subjects.count()) + for index in range(len(result)): + self.assertEqual(result[index], subjects[index]) + + def check_visit_ordered(self, order, result): + visits = get_visits_order(Visit.objects.all(), order, "asc") + self.assertEqual(len(result), visits.count()) + for index in range(len(result)): + self.assertEqual(result[index], visits[index]) + + visits = get_visits_order(Visit.objects.all(), order, "desc") + length = len(result) + self.assertEqual(length, visits.count()) + for index in range(length): + self.assertEqual(result[length - index - 1], visits[index]) + + def test_visits_sort_first_name(self): + subject = self.study_subject + subject.subject.first_name = "PPP" + subject.subject.save() + + subject2 = create_study_subject(2) + subject2.subject.first_name = "QQQ" + subject2.subject.save() + + visit2 = create_visit(subject2) + + self.check_visit_ordered("first_name", [self.visit, visit2]) + + def test_visits_sort_last_name(self): + subject = self.study_subject + subject.subject.last_name = "PPP" + subject.subject.save() + + subject2 = create_study_subject(2) + subject2.subject.last_name = "QQQ" + subject2.subject.save() + + visit2 = create_visit(subject2) + + self.check_visit_ordered("last_name", [self.visit, visit2]) + + def test_visits_sort_default_location(self): + self.check_visit_ordered("default_location", [self.visit]) + + def test_visits_sort_flying_team(self): + self.check_visit_ordered("flying_team", [self.visit]) + + def test_visits_sort_is_finished(self): + self.check_visit_ordered("is_finished", [self.visit]) + + def test_visits_sort_post_mail_sent(self): + self.check_visit_ordered("post_mail_sent", [self.visit]) + + def test_visits_sort_datetime_begin(self): + visit1 = self.visit + visit2 = create_visit(self.study_subject) + visit1.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=1) + visit2.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=2) + visit1.save() + visit2.save() + + self.check_visit_ordered("datetime_begin", [visit1, visit2]) + + def test_visits_sort_datetime_end(self): + visit1 = self.visit + visit2 = create_visit(self.study_subject) + visit1.datetime_end = get_today_midnight_date() + datetime.timedelta(days=1) + visit2.datetime_end = get_today_midnight_date() + datetime.timedelta(days=2) + visit1.save() + visit2.save() + + self.check_visit_ordered("datetime_end", [visit1, visit2]) + + def test_visits_sort_visit_number(self): + visit1 = self.visit + visit2 = create_visit(self.study_subject) + + visit2.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=2) + visit2.save() + visit1.datetime_begin = get_today_midnight_date() + datetime.timedelta(days=1) + visit1.save() + + # visit_number is adjusted automatically according to start date + self.check_visit_ordered("visit_number", [visit1, visit2]) + + def test_visits_sort_unknown(self): + self.check_visit_ordered("some_unknown", [self.visit]) + self.check_visit_ordered("", [self.visit]) + self.check_visit_ordered(None, [self.visit]) + + def test_visits_filter_last_name(self): + subject = self.study_subject + subject.subject.last_name = "XXX" + subject.subject.save() + + self.check_visit_filtered([["last_name", "Q"]], []) + self.check_visit_filtered([["last_name", "xx"]], [self.visit]) + + def test_visits_filter_flying_team(self): + subject = self.study_subject + subject.flying_team = create_flying_team() + subject.save() + + self.check_visit_filtered([["flying_team", "-1"]], []) + self.check_visit_filtered([["flying_team", str(subject.flying_team.id)]], [self.visit]) + + def test_visits_filter_default_location(self): + self.check_visit_filtered([["default_location", "-1"]], []) + self.check_visit_filtered([["default_location", str(self.study_subject.default_location.id)]], [self.visit]) + + def test_visits_filter_is_finished(self): + self.check_visit_filtered([["is_finished", str(not self.visit.is_finished).lower()]], []) + self.check_visit_filtered([["is_finished", str(self.visit.is_finished).lower()]], [self.visit]) + + def test_visits_filter_post_mail_sent(self): + self.check_visit_filtered([["post_mail_sent", str(not self.visit.post_mail_sent).lower()]], []) + self.check_visit_filtered([["post_mail_sent", str(self.visit.post_mail_sent).lower()]], [self.visit]) + + def test_visits_filter_visit_number(self): + self.check_visit_filtered([["visit_number", "-1"]], []) + self.check_visit_filtered([["visit_number", str(self.visit.visit_number)]], [self.visit]) + + def test_visits_filter_visible_appointment_types(self): + appointment_type_1 = create_appointment_type() + appointment_type_2 = create_appointment_type() + self.visit.appointment_types.add(appointment_type_1) + self.visit.save() + + self.check_visit_filtered([["visible_appointment_types", str(appointment_type_2.id)]], []) + self.check_visit_filtered([["visible_appointment_types", str(appointment_type_1.id)]], [self.visit]) + + def test_visits_filter_visible_appointment_types_missing(self): + appointment_type_1 = create_appointment_type() + appointment_type_2 = create_appointment_type() + self.visit.appointment_types.add(appointment_type_1) + self.visit.save() + appointment = create_appointment(self.visit) + + AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=appointment_type_2) + + self.check_visit_filtered([["visible_appointment_types_missing", str(appointment_type_2.id)]], []) + self.check_visit_filtered([["visible_appointment_types_missing", str(appointment_type_1.id)]], [self.visit]) + + def test_visits_filter_visible_appointment_types_done(self): + appointment_type_1 = create_appointment_type() + appointment_type_2 = create_appointment_type() + self.visit.appointment_types.add(appointment_type_1) + self.visit.save() + appointment = create_appointment(self.visit) + appointment.status = Appointment.APPOINTMENT_STATUS_FINISHED + appointment.save() + + AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=appointment_type_1) + + self.check_visit_filtered([["visible_appointment_types_done", str(appointment_type_2.id)]], []) + self.check_visit_filtered([["visible_appointment_types_done", str(appointment_type_1.id)]], [self.visit]) + + def test_visits_filter_visible_appointment_types_in_progress(self): + appointment_type_1 = create_appointment_type() + appointment_type_2 = create_appointment_type() + self.visit.appointment_types.add(appointment_type_1) + self.visit.save() + appointment = create_appointment(self.visit) + + AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=appointment_type_1) + + self.check_visit_filtered([["visible_appointment_types_in_progress", str(appointment_type_2.id)]], []) + self.check_visit_filtered([["visible_appointment_types_in_progress", str(appointment_type_1.id)]], [self.visit]) + + def test_visits_filter_unknown(self): + self.check_visit_filtered([["some_unknown", "unknown data"]], [self.visit]) + self.check_visit_filtered([["", ""]], [self.visit]) + self.check_visit_filtered([[None, None]], [self.visit]) diff --git a/smash/web/tests/view/test_notifications.py b/smash/web/tests/view/test_notifications.py index d25525fa92f0d4d459b39ea8a174c8f129154221..8bc8200467724641b13de55bc0c6d33f1a914dbd 100644 --- a/smash/web/tests/view/test_notifications.py +++ b/smash/web/tests/view/test_notifications.py @@ -144,7 +144,7 @@ class NotificationViewTests(LoggedInTestCase): visit.save() visits = get_unfinished_visits(self.user) - self.assertEquals(3, len(visits)) + self.assertEquals(3, visits.count()) # check sort order self.assertTrue(visits[0].datetime_begin < visits[1].datetime_begin) diff --git a/smash/web/urls.py b/smash/web/urls.py index 6376c8d2a6300e3a9beaea96528ec2e919ec5d5d..1e97079064f8895f1a7b55dc7651153fe2ba7b72 100644 --- a/smash/web/urls.py +++ b/smash/web/urls.py @@ -133,7 +133,6 @@ urlpatterns = [ # MAIL # #################### - url(r'^mail_templates_old$', views.mails.mail_templates, name='web.views.mail_templates_old'), url(r'^mail_templates$', views.mails.MailTemplatesListView.as_view(), name='web.views.mail_templates'), url(r'^mail_templates/add$', views.mails.MailTemplatesCreateView.as_view(), name='web.views.mail_template_add'), url(r'^mail_templates/(?P<pk>\d+)/delete$', views.mails.MailTemplatesDeleteView.as_view(), diff --git a/smash/web/views/mails.py b/smash/web/views/mails.py index 8ca773008e269e26c2c9a03010ff25ad5f8df05a..78d33d4189374f9776058715650dec560b13d0d9 100644 --- a/smash/web/views/mails.py +++ b/smash/web/views/mails.py @@ -12,7 +12,7 @@ from django.views.generic import DeleteView from django.views.generic import ListView from django.views.generic import UpdateView -from . import wrap_response, WrappedView +from . import WrappedView from ..models import StudySubject, Visit, Appointment, MailTemplate from ..models.constants import MAIL_TEMPLATE_CONTEXT_SUBJECT, MAIL_TEMPLATE_CONTEXT_VISIT, \ MAIL_TEMPLATE_CONTEXT_APPOINTMENT @@ -26,10 +26,6 @@ CONTEXT_TYPES_MAPPING = { } -def mail_templates(request): - return wrap_response(request, "mail_templates/index.html", {}) - - class MailTemplatesListView(ListView, WrappedView): model = MailTemplate context_object_name = "mail_templates" diff --git a/smash/web/views/notifications.py b/smash/web/views/notifications.py index d6e8d62ff461acfbe5b7be0d758bb757adb75962..c698553b8ba8f1cb4b8e8ebedda0d4f45d0810eb 100644 --- a/smash/web/views/notifications.py +++ b/smash/web/views/notifications.py @@ -2,7 +2,7 @@ import datetime from django.contrib.auth.models import User, AnonymousUser -from django.db.models import Count, Case, When +from django.db.models import Count, Case, When, Q, F from django.utils import timezone from web.models import Study @@ -71,7 +71,7 @@ def get_subject_with_no_visit_notifications_count(user): def get_visits_without_appointments_count(user): notification = NotificationCount( title="unfinished visits", - count=len(get_unfinished_visits(user)), + count=get_unfinished_visits(user).count(), style="fa fa-user-times text-yellow", type='web.views.unfinished_visits') return notification @@ -80,7 +80,7 @@ def get_visits_without_appointments_count(user): def get_visits_with_missing_appointments_count(user): notification = NotificationCount( title="visits with missing appointments", - count=len(get_active_visits_with_missing_appointments(user)), + count=get_active_visits_with_missing_appointments(user).count(), style="fa fa-user-times text-yellow", type='web.views.visits_with_missing_appointments') return notification @@ -169,19 +169,13 @@ def get_subjects_with_reminder(user): def get_active_visits_with_missing_appointments(user): - result = [] - for visit in get_active_visits_without_appointments(user): - if waiting_for_appointment(visit): - result.append(visit) - return result + visits_without_appointments = get_active_visits_without_appointments(user) + return visits_without_appointments.filter(types_in_system_count__lt=F("types_expected_in_system_count")) def get_unfinished_visits(user): - result = [] - for visit in get_active_visits_without_appointments(user): - if not waiting_for_appointment(visit): - result.append(visit) - return result + visits_without_appointments = get_active_visits_without_appointments(user) + return visits_without_appointments.filter(types_in_system_count__gte=F("types_expected_in_system_count")) def get_approaching_visits_without_appointments(user): @@ -259,7 +253,16 @@ def get_active_visits_without_appointments(user): datetime_end__gt=today, is_finished=False, subject__default_location__in=get_filter_locations(user), - my_count=0).order_by('datetime_begin') + my_count=0).order_by('datetime_begin').annotate( + # types_in_system_count annotation counts how many different appointment types were scheduled or already + # performed + types_in_system_count=Count(Case(When( + Q(appointment__status=Appointment.APPOINTMENT_STATUS_SCHEDULED) | + Q(appointment__status=Appointment.APPOINTMENT_STATUS_FINISHED) + , then="appointment__appointment_types")), distinct=True)).annotate( + # types_expected_in_system_count annotation counts how many different appointment types should be performed in + # the visit + types_expected_in_system_count=Count('appointment_types', distinct=True)) def get_filter_locations(user): diff --git a/smash/web/views/visit.py b/smash/web/views/visit.py index 0dd67c3b3f67cabb631f9b08ee00359081b1694a..7a1e821b71fe0b36b4b49d0ac2de19eaadefa8af 100644 --- a/smash/web/views/visit.py +++ b/smash/web/views/visit.py @@ -3,9 +3,10 @@ import logging from django.shortcuts import get_object_or_404, redirect -from notifications import get_active_visits_with_missing_appointments, get_unfinished_visits, \ - get_approaching_visits_without_appointments, get_approaching_visits_for_mail_contact, get_exceeded_visits, \ - waiting_for_appointment +from notifications import waiting_for_appointment +from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_MISSING_APPOINTMENTS, \ + VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT, VISIT_LIST_EXCEEDED_TIME, \ + VISIT_LIST_UNFINISHED from . import wrap_response from ..forms import VisitDetailForm, VisitAddForm, SubjectDetailForm, StudySubjectDetailForm from ..models import Visit, Appointment, StudySubject, MailTemplate @@ -13,35 +14,32 @@ from ..models import Visit, Appointment, StudySubject, MailTemplate logger = logging.getLogger(__name__) -def visits(request): - visit_list = Visit.objects.order_by('-datetime_begin') - context = { - 'visit_list': visit_list - } +def show_visits(request, visit_list_type): + return wrap_response(request, 'visits/index.html', {'visit_list_type': visit_list_type}) - return wrap_response(request, 'visits/index.html', context) +def visits(request): + return show_visits(request, VISIT_LIST_GENERIC) -def visits_with_missing_appointments(request): - context = { - 'visit_list': get_active_visits_with_missing_appointments(request.user) - } - return wrap_response(request, 'visits/index.html', context) +def visits_with_missing_appointments(request): + return show_visits(request, VISIT_LIST_MISSING_APPOINTMENTS) def approaching_visits_without_appointments(request): - context = { - 'visit_list': get_approaching_visits_without_appointments(request.user) - } - return wrap_response(request, 'visits/index.html', context) + return show_visits(request, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS) def approaching_visits_for_mail_contact(request): - context = { - 'visit_list': get_approaching_visits_for_mail_contact(request.user) - } - return wrap_response(request, 'visits/index.html', context) + return show_visits(request, VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT) + + +def exceeded_visits(request): + return show_visits(request, VISIT_LIST_EXCEEDED_TIME) + + +def unfinished_visits(request): + return show_visits(request, VISIT_LIST_UNFINISHED) def visit_details(request, id): @@ -105,18 +103,3 @@ def visit_add(request, subject_id=-1): form = VisitAddForm(initial={'subject': subject}) return wrap_response(request, 'visits/add.html', {'form': form}) - - -def exceeded_visits(request): - context = { - 'visit_list': get_exceeded_visits(request.user) - } - return wrap_response(request, 'visits/index.html', context) - - -def unfinished_visits(request): - context = { - 'visit_list': get_unfinished_visits(request.user) - } - - return wrap_response(request, 'visits/index.html', context)