diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py index 08be1500d63dfb7898b9a940659f217ece424973..56f9dc4f46cbac9ca9c7ab622abd000aadbe9aa5 100644 --- a/smash/web/api_urls.py +++ b/smash/web/api_urls.py @@ -20,7 +20,9 @@ from web.api_views import worker, location, subject, appointment_type, appointme urlpatterns = [ # appointments - url(r'^appointments/(?P<type>[A-z]+)$', appointment.appointments, name='web.api.appointments'), + url(r'^appointments/(?P<appointment_type>[A-z]+)$', appointment.appointments, name='web.api.appointments'), + url(r'^appointments:columns/(?P<appointment_list_type>[A-z]+)$', appointment.get_appointment_columns, + name='web.api.appointments.columns'), # appointment types url(r'^appointment_types$', appointment_type.appointment_types, name='web.api.appointment_types'), diff --git a/smash/web/api_views/appointment.py b/smash/web/api_views/appointment.py index 331c331c50b6cecc4f17070235448222c07d7dda..2ef29f8d941f9333ba3fa9790bafddf4a043742c 100644 --- a/smash/web/api_views/appointment.py +++ b/smash/web/api_views/appointment.py @@ -6,31 +6,55 @@ from django.http import JsonResponse from django.urls import reverse from django.utils import timezone -from web.models import Appointment -from web.views import e500_error -from web.views.appointment import APPOINTMENT_LIST_GENERIC, APPOINTMENT_LIST_UNFINISHED, APPOINTMENT_LIST_APPROACHING -from web.views.notifications import get_filter_locations, \ - get_today_midnight_date, \ - get_unfinished_appointments +from web.api_views.serialization_utils import serialize_datetime, location_to_str, flying_team_to_str, add_column, \ + bool_to_yes_no, get_filters_for_data_table_request +from web.models import Appointment, Study, SubjectColumns, AppointmentColumns, AppointmentList +from web.models.appointment_list import APPOINTMENT_LIST_GENERIC, APPOINTMENT_LIST_UNFINISHED, \ + APPOINTMENT_LIST_APPROACHING +from web.models.constants import GLOBAL_STUDY_ID +from web.views.notifications import get_filter_locations, get_today_midnight_date, get_unfinished_appointments logger = logging.getLogger(__name__) @login_required -def get_appointments(request, type, min_date, max_date): - if type == APPOINTMENT_LIST_GENERIC: - result = Appointment.objects.filter(location__in=get_filter_locations(request.user), - ) - elif type == APPOINTMENT_LIST_UNFINISHED: +def get_appointment_columns(request, appointment_list_type): + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + appointment_lists = AppointmentList.objects.filter(study=study, type=appointment_list_type) + if len(appointment_lists) > 0: + appointment_list = appointment_lists[0] + subject_columns = appointment_list.visible_subject_columns + appointment_columns = appointment_list.visible_appointment_columns + else: + subject_columns = SubjectColumns() + appointment_columns = AppointmentColumns() + + result = [] + add_column(result, "First name", "first_name", subject_columns, "string_filter") + add_column(result, "Last name", "last_name", subject_columns, "string_filter") + add_column(result, "Info sent", "post_mail_sent", appointment_columns, "yes_no_filter") + add_column(result, "Date", "datetime_when", appointment_columns, None) + add_column(result, "Appointment types", "appointment_types", appointment_columns, "appointment_type_filter", + sortable=False) + add_column(result, "Edit", "edit", None, None, sortable=False) + + return JsonResponse({"columns": result}) + + +@login_required +def get_appointments(request, appointment_type, min_date, max_date): + if appointment_type == APPOINTMENT_LIST_GENERIC: + result = Appointment.objects.filter(location__in=get_filter_locations(request.user)) + elif appointment_type == APPOINTMENT_LIST_UNFINISHED: result = get_unfinished_appointments(request.user) - elif type == APPOINTMENT_LIST_APPROACHING: + elif appointment_type == APPOINTMENT_LIST_APPROACHING: result = Appointment.objects.filter( datetime_when__gt=get_today_midnight_date(), location__in=get_filter_locations(request.user), status=Appointment.APPOINTMENT_STATUS_SCHEDULED ).order_by("datetime_when") else: - raise TypeError("Unknown query type: " + type) + raise TypeError("Unknown query type: " + appointment_type) if min_date is not None: min_date = datetime.strptime(min_date, "%Y-%m-%d").replace(tzinfo=timezone.now().tzinfo) @@ -42,74 +66,126 @@ def get_appointments(request, type, min_date, max_date): return result.order_by("datetime_when") +def get_appointments_order(appointments_to_be_ordered, order_column, order_direction): + result = appointments_to_be_ordered + if order_direction == "asc": + order_direction = "" + else: + order_direction = "-" + if order_column == "first_name": + result = appointments_to_be_ordered.order_by(order_direction + 'visit__subject__subject__first_name') + elif order_column == "last_name": + result = appointments_to_be_ordered.order_by(order_direction + 'visit__subject__subject__last_name') + elif order_column == "location": + result = appointments_to_be_ordered.order_by(order_direction + 'location') + elif order_column == "flying_team": + result = appointments_to_be_ordered.order_by(order_direction + 'flying_team') + elif order_column == "post_mail_sent": + result = appointments_to_be_ordered.order_by(order_direction + 'post_mail_sent') + elif order_column == "datetime_when": + result = appointments_to_be_ordered.order_by(order_direction + 'datetime_when') + else: + logger.warn("Unknown sort column: " + str(order_column)) + return result + + +def get_appointments_filtered(appointments_to_be_filtered, filters): + result = appointments_to_be_filtered + for row in filters: + column = row[0] + value = row[1] + if column == "first_name": + result = result.filter(visit__subject__subject__first_name__icontains=value) + elif column == "last_name": + result = result.filter(visit__subject__subject__last_name__icontains=value) + elif column == "location": + result = result.filter(location=value) + elif column == "flying_team": + result = result.filter(flying_team=value) + elif column == "appointment_types": + result = result.filter(appointment_types=value) + else: + message = "UNKNOWN filter: " + if column is None: + message += "[None]" + else: + message += str(column) + logger.warn(message) + + return result + + @login_required -def appointments(request, type): - try: - # id of the query from dataTable: https://datatables.net/manual/server-side - draw = int(request.GET.get("draw", "-1")) +def appointments(request, appointment_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")) + start = int(request.GET.get("start", "0")) + length = int(request.GET.get("length", "10")) - min_date = request.GET.get("start_date", None) - max_date = request.GET.get("end_date", None) + 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") - if min_date is not None: - length = 1000000000 + min_date = request.GET.get("start_date", None) + max_date = request.GET.get("end_date", None) - all_appointments = get_appointments(request, type, min_date, max_date) + filters = get_filters_for_data_table_request(request) - count = all_appointments.count() + if min_date is not None: + length = 1000000000 + + all_appointments = get_appointments(request, appointment_type, min_date, max_date) + + count = all_appointments.count() - sliced_subjects = all_appointments[start:(start + length)] + sorted_appointments = get_appointments_order(all_appointments, order_column, order_dir) + filtered_appointments = get_appointments_filtered(sorted_appointments, filters) + sliced_appointments = filtered_appointments[start:(start + length)] - result_appointments = sliced_subjects + result_appointments = sliced_appointments - count_filtered = all_appointments.count() + count_filtered = all_appointments.count() - data = [] - for appointment in result_appointments: - data.append(serialize_appointment(appointment)) + data = [] + for appointment in result_appointments: + data.append(serialize_appointment(appointment)) - return JsonResponse({ - "draw": draw, - "recordsTotal": count, - "recordsFiltered": count_filtered, - "data": data, - }) - except: - logger.exception("Problem with getting appointments") - return e500_error(request) + return JsonResponse({ + "draw": draw, + "recordsTotal": count, + "recordsFiltered": count_filtered, + "data": data, + }) def serialize_appointment(appointment): subject_string = "" - nd_number = screening_number = phone_numbers = appointment_types = None + first_name = "" + last_name = "" + nd_number = screening_number = phone_numbers = appointment_type_names = None if appointment.visit is not None: title = "Visit " + str(appointment.visit.visit_number) study_subject = appointment.visit.subject subject_string = study_subject.subject.last_name + " " + study_subject.subject.first_name + first_name = study_subject.subject.first_name + last_name = study_subject.subject.last_name nd_number = study_subject.nd_number screening_number = study_subject.screening_number phone_numbers = ", ".join(filter(None, [study_subject.subject.phone_number, study_subject.subject.phone_number_2, study_subject.subject.phone_number_3])) - appointment_types = ", ".join([unicode(type) for type in appointment.appointment_types.all()]) + appointment_type_names = ", ".join( + [unicode(appointment_type_codes) for appointment_type_codes in appointment.appointment_types.all()]) else: title = appointment.comment - type = ", ".join([type.code for type in appointment.appointment_types.all()]) - time = "" - if appointment.datetime_when is not None: - time = appointment.datetime_when.strftime('%Y-%m-%d %H:%M') - until = "" - if appointment.datetime_when is not None: - until = appointment.datetime_until().strftime('%Y-%m-%d %H:%M') + appointment_type_codes = ", ".join( + [appointment_type_codes.code for appointment_type_codes in appointment.appointment_types.all()]) + until = serialize_datetime(appointment.datetime_until()) - if appointment.flying_team is None: - location = unicode(appointment.location) - else: - location = unicode(appointment.location) + " (" + unicode(appointment.flying_team) + ")" + location = location_to_str(appointment.location) + flying_team = flying_team_to_str(appointment.flying_team) result = { "subject": subject_string, @@ -117,14 +193,19 @@ def serialize_appointment(appointment): "nd_number": nd_number, "screening_number": screening_number, "phone_number": phone_numbers, - "appointment_types": appointment_types, - "type": type, - "datetime_when": time, + "appointment_type_names": appointment_type_names, "datetime_until": until, "comment": appointment.comment, "color": appointment.color(), "id": appointment.id, + + "first_name": first_name, + "last_name": last_name, "location": location, + "flying_team": flying_team, + "post_mail_sent": bool_to_yes_no(appointment.post_mail_sent), + "datetime_when": serialize_datetime(appointment.datetime_when), + "appointment_types": appointment_type_codes, "url": reverse('web.views.appointment_edit', kwargs={'id': str(appointment.id)}) } return result diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index e1e96dbcfc8944eeff773fbd95016f76f5ef2db1..c1e857ed889a0973ead5f9dd0a8c5e644c32527d 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -59,3 +59,14 @@ def add_column(result, name, field_name, column_list, param, columns_used_in_stu "visible": visible, "sortable": sortable }) + + +def get_filters_for_data_table_request(request): + 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 + return filters diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index 9d2d67b3ac12f5afcc6fa32eaca82f4e1f57a0ce..ac069955aae0681027e921742dbf110f7b85c4d5 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -6,7 +6,7 @@ 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 + serialize_date, serialize_datetime, get_filters_for_data_table_request 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, \ @@ -260,13 +260,7 @@ def subjects(request, type): 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 + filters = get_filters_for_data_table_request(request) all_subjects = get_subjects(request, type) diff --git a/smash/web/api_views/visit.py b/smash/web/api_views/visit.py index ba2258b9f19eae217529aefc70ec98b99521deab..8a7a75b7805504a5ca33158dd9689e6c804f30d5 100644 --- a/smash/web/api_views/visit.py +++ b/smash/web/api_views/visit.py @@ -5,7 +5,7 @@ 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_date, get_filters_for_data_table_request from web.models import AppointmentType, Appointment from web.models import SubjectColumns from web.models import Visit, Study, VisitColumns, StudyVisitList, StudyColumns @@ -182,13 +182,7 @@ def visits(request, visit_list_type): 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 + filters = get_filters_for_data_table_request(request) all_visits = get_visits(request, visit_list_type) diff --git a/smash/web/migrations/0088_appointmentcolumns_appointmentlist.py b/smash/web/migrations/0088_appointmentcolumns_appointmentlist.py new file mode 100644 index 0000000000000000000000000000000000000000..3473fdc6d0f2a643a4bfe2f4e22c8c3e089bfe85 --- /dev/null +++ b/smash/web/migrations/0088_appointmentcolumns_appointmentlist.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-07 13:07 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0087_approaching_visit_wihout_appointment_list'), + ] + + operations = [ + migrations.CreateModel( + name='AppointmentColumns', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('flying_team', models.BooleanField(default=False, verbose_name=b'Flying team')), + ('worker_assigned', models.BooleanField(default=False, verbose_name=b'Worker conducting the assessment')), + ('appointment_types', models.BooleanField(default=True, verbose_name=b'Appointment types')), + ('room', models.BooleanField(default=False, verbose_name=b'Room')), + ('location', models.BooleanField(default=False, verbose_name=b'Location')), + ('comment', models.BooleanField(default=False, verbose_name=b'Comment')), + ('datetime_when', models.BooleanField(default=True, verbose_name=b'Comment')), + ('length', models.BooleanField(default=False, verbose_name=b'Appointment length')), + ('status', models.BooleanField(default=False, verbose_name=b'Status')), + ('post_mail_sent', models.BooleanField(default=False, verbose_name=b'Post mail sent')), + ], + ), + migrations.CreateModel( + name='AppointmentList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[(b'GENERIC', b'Generic'), (b'UNFINISHED', b'Unfinished'), (b'APPROACHING', b'Approaching')], max_length=50, verbose_name=b'Type of list')), + ('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Study')), + ('visible_appointment_columns', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.AppointmentColumns')), + ('visible_study_subject_columns', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.StudyColumns')), + ('visible_subject_columns', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.SubjectColumns')), + ('visible_visit_columns', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.VisitColumns')), + ], + ), + ] diff --git a/smash/web/migrations/0089_unfinshed_appointment_list.py b/smash/web/migrations/0089_unfinshed_appointment_list.py new file mode 100644 index 0000000000000000000000000000000000000000..dabf6f9fa77d3e3451c41225cb9dfaf64df310a5 --- /dev/null +++ b/smash/web/migrations/0089_unfinshed_appointment_list.py @@ -0,0 +1,98 @@ +# -*- 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 + + +# noinspection PyUnusedLocal +# noinspection PyPep8Naming +def create_default_columns_for_APPOINTMENT_LIST_UNFINISHED(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 = True + visit_columns.datetime_end = False + 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() + + AppointmentColumns = apps.get_model("web", "AppointmentColumns") + appointment_columns = AppointmentColumns.objects.create() + appointment_columns.flying_team = False + appointment_columns.worker_assigned = False + appointment_columns.appointment_types = True + appointment_columns.room = False + appointment_columns.location = False + appointment_columns.comment = False + appointment_columns.datetime_when = True + appointment_columns.length = False + appointment_columns.status = False + appointment_columns.post_mail_sent = False + appointment_columns.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0088_appointmentcolumns_appointmentlist'), + ] + + operations = [ + migrations.RunPython(create_default_columns_for_APPOINTMENT_LIST_UNFINISHED), + migrations.RunSQL('INSERT INTO web_appointmentlist (' + + 'study_id, ' + + 'visible_visit_columns_id, ' + + 'visible_subject_columns_id, ' + + 'visible_study_subject_columns_id, ' + + 'visible_appointment_columns_id, ' + + 'type) ' + + "SELECT " + + "1, " + + "max(web_visitcolumns.id), " + + "max(web_subjectcolumns.id), " + + "max(web_studycolumns.id), " + + "max(web_appointmentcolumns.id), " + + "'UNFINISHED' FROM web_visitcolumns, web_studycolumns, web_subjectcolumns, web_appointmentcolumns;"), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index 746b15bf599f4f08e2605b8274ddc97137b609d8..d9ee4dab7c6289bcac5c3daa75f84d463f9fde83 100644 --- a/smash/web/models/__init__.py +++ b/smash/web/models/__init__.py @@ -8,6 +8,7 @@ from flying_team import FlyingTeam from location import Location from appointment_type_link import AppointmentTypeLink from country import Country +from appointment_columns import AppointmentColumns from subject_columns import SubjectColumns from study_columns import StudyColumns from visit_columns import VisitColumns @@ -26,6 +27,7 @@ from subject import Subject from study_subject import StudySubject from study_subject_list import StudySubjectList from study_visit_list import StudyVisitList +from appointment_list import AppointmentList from contact_attempt import ContactAttempt from mail_template import MailTemplate from missing_subject import MissingSubject @@ -33,5 +35,6 @@ 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, VisitColumns, StudyVisitList] + AppointmentList, AppointmentColumns, Visit, Worker, ContactAttempt, ConfigurationItem, MailTemplate, + AppointmentTypeLink, + MissingSubject, InconsistentSubject, InconsistentField, Country, StudyColumns, VisitColumns, StudyVisitList] diff --git a/smash/web/models/appointment_columns.py b/smash/web/models/appointment_columns.py new file mode 100644 index 0000000000000000000000000000000000000000..4a65c60b9347ed2612036e6516a33e48c0fc8c28 --- /dev/null +++ b/smash/web/models/appointment_columns.py @@ -0,0 +1,47 @@ +# coding=utf-8 +from django.db import models + + +class AppointmentColumns(models.Model): + class Meta: + app_label = 'web' + + flying_team = models.BooleanField(default=False, + verbose_name='Flying team', + ) + + worker_assigned = models.BooleanField(default=False, + verbose_name='Worker conducting the assessment', + ) + + appointment_types = models.BooleanField(default=True, + verbose_name='Appointment types', + ) + + room = models.BooleanField(default=False, + verbose_name='Room', + ) + + location = models.BooleanField(default=False, + verbose_name='Location', + ) + + comment = models.BooleanField(default=False, + verbose_name='Comment', + ) + + datetime_when = models.BooleanField(default=True, + verbose_name='Comment', + ) + + length = models.BooleanField(default=False, + verbose_name='Appointment length', + ) + + status = models.BooleanField(default=False, + verbose_name='Status', + ) + + post_mail_sent = models.BooleanField(default=False, + verbose_name='Post mail sent', + ) diff --git a/smash/web/models/appointment_list.py b/smash/web/models/appointment_list.py new file mode 100644 index 0000000000000000000000000000000000000000..e04142320afcd4dddbd82ab6b4cf1dbc9efcb61c --- /dev/null +++ b/smash/web/models/appointment_list.py @@ -0,0 +1,52 @@ +# coding=utf-8 +from django.db import models + +from web.models import Study, SubjectColumns, VisitColumns, AppointmentColumns, StudyColumns + +APPOINTMENT_LIST_GENERIC = "GENERIC" +APPOINTMENT_LIST_UNFINISHED = "UNFINISHED" +APPOINTMENT_LIST_APPROACHING = "APPROACHING" + +APPOINTMENT_LIST_CHOICES = { + APPOINTMENT_LIST_GENERIC: 'Generic', + APPOINTMENT_LIST_UNFINISHED: 'Unfinished', + APPOINTMENT_LIST_APPROACHING: 'Approaching', +} + + +class AppointmentList(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_columns = models.ForeignKey( + AppointmentColumns, + on_delete=models.CASCADE, + null=False, + ) + + type = models.CharField(max_length=50, + choices=APPOINTMENT_LIST_CHOICES.items(), + verbose_name='Type of list', + ) diff --git a/smash/web/static/js/appointment.js b/smash/web/static/js/appointment.js index a4d594bab159699c5a2c247589176075e5f788ad..86ad7cd8848bb9b1752d434a6c72cd799438e141 100644 --- a/smash/web/static/js/appointment.js +++ b/smash/web/static/js/appointment.js @@ -208,7 +208,6 @@ function get_calendar_events_function(source, allow_url_redirection, day_headers } return function (start, end, timezone, callback) { if (day_headers !== undefined) { - console.log("XXXX"); $.ajax({ data: { start_date: start.format(), @@ -245,7 +244,7 @@ function get_calendar_events_function(source, allow_url_redirection, day_headers const entry = doc.data[i]; var title = entry.subject; if (title !== "") { - title += " (" + entry.nd_number + "); type: " + entry.type; + title += " (" + entry.nd_number + "); type: " + entry.appointment_types; } else { title = entry.title } @@ -257,9 +256,11 @@ function get_calendar_events_function(source, allow_url_redirection, day_headers color: entry.color, nd_number: entry.nd_number, location: entry.location, + flying_team: entry.flying_team, screening_number: entry.screening_number, phone_number: entry.phone_number, - appointment_types: entry.appointment_types + appointment_types: entry.appointment_types, + appointment_type_names: entry.appointment_type_names }; if (allow_url_redirection) { event["url"] = entry.url; @@ -271,3 +272,7 @@ function get_calendar_events_function(source, allow_url_redirection, day_headers }); } } + +function createAppointmentsTable(params) { + return createTable(params); +} diff --git a/smash/web/templates/appointments/index.html b/smash/web/templates/appointments/index.html index 81a642dc496a82ee4af2cf402e88b9ce640c42c2..0452729eb4b61a1b8c3d2942299ac2040b637240 100644 --- a/smash/web/templates/appointments/index.html +++ b/smash/web/templates/appointments/index.html @@ -133,11 +133,15 @@ if (event.nd_number) { content += '<li>ND number: ' + event.nd_number + '</li>' } - if (event.appointment_types) { - content += '<li>Appointment types: ' + event.appointment_types + '</li>' + if (event.appointment_type_names) { + content += '<li>Appointment types: ' + event.appointment_type_names + '</li>' } if (event.location) { - content += '<li>Location: ' + event.location + '</li>' + var location = event.location; + if (event.flying_team) { + location += " (" + event.flying_team + ")"; + } + content += '<li>Location: ' + location + '</li>' } content += "</ul>"; $(element).popover({ diff --git a/smash/web/templates/appointments/list.html b/smash/web/templates/appointments/list.html index b20d99c7b0dfcf3dee0d1eb9609ae3902b84faf0..24de957c72f74339e13d3d31e08ca4acdd50ce42 100644 --- a/smash/web/templates/appointments/list.html +++ b/smash/web/templates/appointments/list.html @@ -19,25 +19,12 @@ {% endblock breadcrumb %} {% block maincontent %} - <div class="row"> - <div class="col-md-16"> - <table id="table" class="table table-bordered table-striped"> - <thead> - <tr> - <th>Subject</th> - <th>Visit</th> - <th>Type</th> - <th>Date</th> - <th>Details</th> - <th>Edit</th> - </tr> - </thead> - <tbody> - </tbody> - <tfoot style="display: table-header-group;"/> - </table> - - </div> + <div class="box-body"> + <table id="table" class="table table-bordered table-striped table-responsive"> + </table> + </div> + <h3>Visible columns</h3> + <div id="visible-column-checkboxes" style="display:table; width:100%"> </div> {% endblock maincontent %} @@ -46,53 +33,32 @@ <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 'js/appointment.js' %}"></script> <script> - $(function () { - var table = $('#table').DataTable({ - serverSide: true, - processing: true, - ordering: false, - ajax: "{% url 'web.api.appointments' list_type %}", - columns: [ - {"data": "subject"}, - {"data": "title"}, - {"data": "type"}, - {"data": "datetime_when"}, - {"data": "comment"}, - {"data": null}, - ], - columnDefs: [{ - "targets": 5, - "data": "id", - "defaultContent": '<a href="#" type="button" class="btn btn-block btn-default">Edit</a>' - }, -{# {#} -{# render: function (data, type, row) {#} -{# var date = new Date(data);#} -{# var mm = date.getMonth() + 1; // getMonth() is zero-based#} -{# var dd = date.getDate();#} -{# var hour = date.getHours();#} -{# var minute = date.getMinutes();#} -{##} -{# return [date.getFullYear(),#} -{# (mm > 9 ? '' : '0') + mm,#} -{# (dd > 9 ? '' : '0') + dd#} -{# ].join('-') + " " + hour + ":" + minute;#} -{# },#} -{# targets: 3#} -{# }#} - ] - }); + function getSubjectEditUrl(id) { + return "{% url 'web.views.appointment_edit' 1234567 %}".replace(/1234567/, id); + } - $('#table tbody').on('click', 'a', function () { - var data = table.row($(this).parents('tr')).data(); - var url = "{% url 'web.views.appointment_edit' 12345 %}".replace(/12345/, data.id.toString()); - window.location.href = url; - }); + var worker_locations = []; + {% for location in worker.locations.all %} + worker_locations.push({id: location.id, name: location.name}); + {% endfor %} - $('#table_filter').css("display", "none"); + $.get("{% url 'web.api.appointments.columns' list_type %}", function (data) { + createAppointmentsTable({ + worker_locations: worker_locations, + appointment_types_url: "{% url 'web.api.appointment_types' %}", + subject_types_url: "{% url 'web.api.subject_types' %}", + locations_url: "{% url 'web.api.locations' %}", + subjects_url: "{% url 'web.api.appointments' list_type %}", + flying_teams_url: "{% url 'web.api.flying_teams' %}", + tableElement: document.getElementById("table"), + columns: getColumns(data.columns, getSubjectEditUrl), + checkboxesElement: document.getElementById("visible-column-checkboxes") + }) }); + </script> {% endblock scripts %} diff --git a/smash/web/tests/api_views/test_appointment.py b/smash/web/tests/api_views/test_appointment.py index 5074b3ddb061aa046631ae32b2b6b8f8135c23ab..6e9bed327b4708bf3ab160b5bcfa097becc0c287 100644 --- a/smash/web/tests/api_views/test_appointment.py +++ b/smash/web/tests/api_views/test_appointment.py @@ -1,33 +1,23 @@ # coding=utf-8 import datetime +import json -from django.contrib.auth.models import User -from django.test import Client -from django.test import TestCase from django.urls import reverse -from web.models import AppointmentTypeLink -from web.tests.functions import create_study_subject, create_worker, create_visit, create_appointment, \ - create_appointment_type, create_get_suffix, create_flying_team +from web.api_views.appointment import get_appointments_filtered, get_appointments_order +from web.models import AppointmentTypeLink, AppointmentList, Appointment +from web.tests import LoggedInWithWorkerTestCase +from web.tests.functions import create_study_subject, create_visit, create_appointment, \ + create_appointment_type, create_get_suffix, create_flying_team, create_location from web.views.appointment import APPOINTMENT_LIST_GENERIC, APPOINTMENT_LIST_APPROACHING, APPOINTMENT_LIST_UNFINISHED from web.views.notifications import get_today_midnight_date -class TestAppointmentApi(TestCase): +class TestAppointmentApi(LoggedInWithWorkerTestCase): def setUp(self): + super(TestAppointmentApi, 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_appointments_invalid(self): - response = self.client.get(reverse('web.api.appointments', kwargs={'type': "bla"})) - self.assertEqual(response.status_code, 500) def test_appointments_valid(self): place = "Some new flying team location" @@ -45,7 +35,7 @@ class TestAppointmentApi(TestCase): appointment2.save() AppointmentTypeLink.objects.create(appointment=appointment2, appointment_type=appointment_type) - url = reverse('web.api.appointments', kwargs={'type': APPOINTMENT_LIST_GENERIC}) + url = reverse('web.api.appointments', kwargs={'appointment_type': APPOINTMENT_LIST_GENERIC}) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -59,7 +49,7 @@ class TestAppointmentApi(TestCase): appointment.flying_team = create_flying_team(place=place) appointment.save() - url = reverse('web.api.appointments', kwargs={'type': APPOINTMENT_LIST_GENERIC}) + url = reverse('web.api.appointments', kwargs={'appointment_type': APPOINTMENT_LIST_GENERIC}) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -72,7 +62,7 @@ class TestAppointmentApi(TestCase): visit = create_visit(self.study_subject) create_appointment(visit, get_today_midnight_date() + datetime.timedelta(days=2)) - url = reverse('web.api.appointments', kwargs={'type': APPOINTMENT_LIST_APPROACHING}) + url = reverse('web.api.appointments', kwargs={'appointment_type': APPOINTMENT_LIST_APPROACHING}) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -85,7 +75,7 @@ class TestAppointmentApi(TestCase): visit = create_visit(self.study_subject) create_appointment(visit, get_today_midnight_date() + datetime.timedelta(days=-12)) - url = reverse('web.api.appointments', kwargs={'type': APPOINTMENT_LIST_UNFINISHED}) + url = reverse('web.api.appointments', kwargs={'appointment_type': APPOINTMENT_LIST_UNFINISHED}) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -106,7 +96,7 @@ class TestAppointmentApi(TestCase): "end_date": (get_today_midnight_date() + datetime.timedelta(days=3)).strftime("%Y-%m-%d"), } url = ("%s" + create_get_suffix(params)) % reverse('web.api.appointments', - kwargs={'type': APPOINTMENT_LIST_GENERIC}) + kwargs={'appointment_type': APPOINTMENT_LIST_GENERIC}) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -114,8 +104,187 @@ class TestAppointmentApi(TestCase): params["start_date"] = (get_today_midnight_date() + datetime.timedelta(days=-2)).strftime("%Y-%m-%d") url = ("%s" + create_get_suffix(params)) % reverse('web.api.appointments', - kwargs={'type': APPOINTMENT_LIST_GENERIC}) + kwargs={'appointment_type': APPOINTMENT_LIST_GENERIC}) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertTrue(name in response.content) + + def test_get_columns(self): + response = self.client.get( + reverse('web.api.appointments.columns', kwargs={'appointment_list_type': APPOINTMENT_LIST_GENERIC})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + visible_columns = 0 + for column in columns: + if column["visible"]: + visible_columns += 1 + self.assertTrue(visible_columns > 0) + + def test_get_columns_for_require_contact(self): + response = self.client.get( + reverse('web.api.appointments.columns', kwargs={'appointment_list_type': APPOINTMENT_LIST_UNFINISHED})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + visible_columns = 0 + for column in columns: + if column["visible"]: + visible_columns += 1 + self.assertTrue(visible_columns > 0) + + def test_get_columns_when_no_list_is_available(self): + AppointmentList.objects.all().delete() + response = self.client.get( + reverse('web.api.appointments.columns', kwargs={'appointment_list_type': APPOINTMENT_LIST_GENERIC})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + self.assertTrue(len(columns) > 0) + + def check_appointment_filtered(self, filters, result): + appointments = get_appointments_filtered(Appointment.objects.all(), filters) + self.assertEqual(len(result), appointments.count()) + for index in range(len(result)): + self.assertEqual(result[index], appointments[index]) + + def check_appointment_ordered(self, order, result): + appointments = get_appointments_order(Appointment.objects.all(), order, "asc") + self.assertEqual(len(result), appointments.count()) + for index in range(len(result)): + self.assertEqual(result[index], appointments[index]) + + appointments = get_appointments_order(Appointment.objects.all(), order, "desc") + length = len(result) + self.assertEqual(length, appointments.count()) + for index in range(length): + self.assertEqual(result[length - index - 1], appointments[index]) + + def test_appointment_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() + + appointment = create_appointment(create_visit(subject)) + appointment2 = create_appointment(create_visit(subject2)) + + self.check_appointment_ordered("first_name", [appointment, appointment2]) + + def test_appointment_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() + + appointment = create_appointment(create_visit(subject)) + appointment2 = create_appointment(create_visit(subject2)) + + self.check_appointment_ordered("last_name", [appointment, appointment2]) + + def test_appointment_sort_default_location(self): + subject = self.study_subject + subject2 = create_study_subject(2) + + appointment = create_appointment(create_visit(subject)) + appointment.location = create_location(name="x") + appointment.save() + appointment2 = create_appointment(create_visit(subject2)) + appointment2.location = create_location(name="y") + appointment2.save() + + self.check_appointment_ordered("location", [appointment, appointment2]) + + def test_appointment_sort_flying_team(self): + subject = self.study_subject + subject2 = create_study_subject(2) + + appointment = create_appointment(create_visit(subject)) + appointment.flying_team = create_flying_team(place="x") + appointment.save() + appointment2 = create_appointment(create_visit(subject2)) + appointment2.flying_team = create_flying_team(place="y") + appointment2.save() + + self.check_appointment_ordered("flying_team", [appointment, appointment2]) + + def test_appointment_sort_post_mail_sent(self): + appointment = create_appointment(create_visit(self.study_subject)) + + self.check_appointment_ordered("post_mail_sent", [appointment]) + + def test_appointment_sort_datetime_when(self): + subject = self.study_subject + subject2 = create_study_subject(2) + + appointment = create_appointment(create_visit(subject)) + appointment.datetime_when = "2017-10-10" + appointment.save() + appointment2 = create_appointment(create_visit(subject2)) + appointment2.datetime_when = "2017-10-12" + appointment2.save() + + self.check_appointment_ordered("datetime_when", [appointment, appointment2]) + + def test_appointment_sort_unknown(self): + appointment = create_appointment(create_visit(self.study_subject)) + + self.check_appointment_ordered("some_unknown", [appointment]) + self.check_appointment_ordered("", [appointment]) + self.check_appointment_ordered(None, [appointment]) + + def test_subjects_filter_first_name(self): + subject = self.study_subject.subject + subject.first_name = "QQ" + subject.save() + + appointment = create_appointment(create_visit(self.study_subject)) + + self.check_appointment_filtered([["first_name", "QQ"]], [appointment]) + self.check_appointment_filtered([["first_name", "PP"]], []) + + def test_subjects_filter_last_name(self): + subject = self.study_subject.subject + subject.last_name = "QQ" + subject.save() + + appointment = create_appointment(create_visit(self.study_subject)) + + self.check_appointment_filtered([["last_name", "QQ"]], [appointment]) + self.check_appointment_filtered([["last_name", "PP"]], []) + + def test_subjects_filter_location(self): + appointment = create_appointment(create_visit(self.study_subject)) + + self.check_appointment_filtered([["location", str(appointment.location.id)]], [appointment]) + self.check_appointment_filtered([["location", "-1"]], []) + + def test_subjects_filter_flying_team(self): + appointment = create_appointment(create_visit(self.study_subject)) + appointment.flying_team = create_flying_team() + appointment.save() + + self.check_appointment_filtered([["flying_team", str(appointment.flying_team.id)]], [appointment]) + self.check_appointment_filtered([["flying_team", "-1"]], []) + + def test_subjects_filter_appointment_types(self): + appointment = create_appointment(create_visit(self.study_subject)) + appointment_type = create_appointment_type() + AppointmentTypeLink.objects.create(appointment_type=appointment_type, appointment=appointment) + + self.check_appointment_filtered([["appointment_types", str(appointment_type.id)]], [appointment]) + self.check_appointment_filtered([["appointment_types", "-1"]], []) + + def test_appointment_filter_unknown(self): + appointment = create_appointment(create_visit(self.study_subject)) + + self.check_appointment_filtered([["some_unknown", "unk"]], [appointment]) + self.check_appointment_filtered([["", ""]], [appointment]) + self.check_appointment_filtered([[None, None]], [appointment]) diff --git a/smash/web/tests/view/test_notifications.py b/smash/web/tests/view/test_notifications.py index 677ce30bec0e8d5481fcba8972406ec861b06ee9..e02b515089640176d13e3b57c13c0b7a902a64b8 100644 --- a/smash/web/tests/view/test_notifications.py +++ b/smash/web/tests/view/test_notifications.py @@ -286,6 +286,16 @@ class NotificationViewTests(LoggedInTestCase): notification = get_unfinished_appointments_count(self.user) self.assertEquals(original_notification.count + 1, notification.count) + def test_get_unfinished_appointments_count_for_general_appointments(self): + appointment = create_appointment() + appointment.visit = None + appointment.datetime_when = "2011-01-01" + appointment.status = Appointment.APPOINTMENT_STATUS_SCHEDULED + appointment.save() + + notification = get_unfinished_appointments_count(self.user) + self.assertEquals(0, notification.count) + def test_get_unfinished_appointments_count_2(self): original_notification = get_unfinished_appointments_count(self.user) subject = create_study_subject() diff --git a/smash/web/views/appointment.py b/smash/web/views/appointment.py index ff4afa90d3a537f54d1eb140df02a61de0faccda..6d64acd468487ffc1a569241661c309d5b12f5ae 100644 --- a/smash/web/views/appointment.py +++ b/smash/web/views/appointment.py @@ -6,15 +6,13 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404, redirect +from web.models.appointment_list import APPOINTMENT_LIST_APPROACHING, APPOINTMENT_LIST_GENERIC, \ + APPOINTMENT_LIST_UNFINISHED from . import wrap_response from ..forms import AppointmentDetailForm, AppointmentAddForm, AppointmentEditForm, SubjectEditForm, \ StudySubjectEditForm from ..models import Appointment, StudySubject, MailTemplate -APPOINTMENT_LIST_GENERIC = "GENERIC" -APPOINTMENT_LIST_UNFINISHED = "UNFINISHED" -APPOINTMENT_LIST_APPROACHING = "APPROACHING" - logger = logging.getLogger(__name__) diff --git a/smash/web/views/notifications.py b/smash/web/views/notifications.py index 4d251909544bc61cacaade3a0a309e9b4caa39b8..2ef58548948192fb1b4fb4d606858c8eba81a85c 100644 --- a/smash/web/views/notifications.py +++ b/smash/web/views/notifications.py @@ -235,7 +235,7 @@ def get_unfinished_appointments(user): datetime_when__lt=get_today_midnight_date(), status=Appointment.APPOINTMENT_STATUS_SCHEDULED, location__in=get_filter_locations(user), - ).order_by('datetime_when') + ).exclude(visit__isnull=True).order_by('datetime_when') def waiting_for_appointment(visit):