diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py index 8b880d23e238e756e9d85c8bbcceea6f53675520..56f9dc4f46cbac9ca9c7ab622abd000aadbe9aa5 100644 --- a/smash/web/api_urls.py +++ b/smash/web/api_urls.py @@ -21,6 +21,8 @@ from web.api_views import worker, location, subject, appointment_type, appointme urlpatterns = [ # 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 ee9bee9746b9f3afc97f54a3b546c886883b414a..fe5a1828c450022f33ae67b95716fa5babea18eb 100644 --- a/smash/web/api_views/appointment.py +++ b/smash/web/api_views/appointment.py @@ -6,21 +6,45 @@ from django.http import JsonResponse from django.urls import reverse from django.utils import timezone -from web.api_views.serialization_utils import serialize_datetime, location_to_str, flying_team_to_str -from web.models import Appointment -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 +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_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), - ) + result = Appointment.objects.filter(location__in=get_filter_locations(request.user)) elif appointment_type == APPOINTMENT_LIST_UNFINISHED: result = get_unfinished_appointments(request.user) elif appointment_type == APPOINTMENT_LIST_APPROACHING: @@ -42,6 +66,60 @@ def get_appointments(request, appointment_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 == "default_location": + result = appointments_to_be_ordered.order_by(order_direction + 'visit__subject__default_location') + elif order_column == "flying_team": + result = appointments_to_be_ordered.order_by(order_direction + 'visit__subject__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 filter_by_appointment(appointments_to_filter, appointment_type): + result = appointments_to_filter.filter(appointment_types=appointment_type) + 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 == "default_location": + result = result.filter(visit__subject__default_location=value) + elif column == "flying_team": + result = result.filter(visit__subject__flying_team=value) + elif column == "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 appointments(request, appointment_type): # id of the query from dataTable: https://datatables.net/manual/server-side @@ -50,9 +128,21 @@ def appointments(request, appointment_type): 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") + min_date = request.GET.get("start_date", None) max_date = request.GET.get("end_date", None) + 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 + if min_date is not None: length = 1000000000 @@ -60,9 +150,11 @@ def appointments(request, appointment_type): 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() @@ -80,23 +172,27 @@ def appointments(request, appointment_type): 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(appointment_type) for appointment_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 - appointment_type = ", ".join([appointment_type.code for appointment_type in appointment.appointment_types.all()]) - time = serialize_datetime(appointment.datetime_when) + appointment_type_codes = ", ".join( + [appointment_type_codes.code for appointment_type_codes in appointment.appointment_types.all()]) until = serialize_datetime(appointment.datetime_until()) location = location_to_str(appointment.location) @@ -108,15 +204,19 @@ def serialize_appointment(appointment): "nd_number": nd_number, "screening_number": screening_number, "phone_number": phone_numbers, - "appointment_types": appointment_types, - "type": appointment_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/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/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 27c25a15abe1ae70652f65da1dd5775c6b161eba..86ad7cd8848bb9b1752d434a6c72cd799438e141 100644 --- a/smash/web/static/js/appointment.js +++ b/smash/web/static/js/appointment.js @@ -244,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 } @@ -259,7 +259,8 @@ function get_calendar_events_function(source, allow_url_redirection, day_headers 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 8b975928d7d641a20399fa98118d4ca3257c8881..0452729eb4b61a1b8c3d2942299ac2040b637240 100644 --- a/smash/web/templates/appointments/index.html +++ b/smash/web/templates/appointments/index.html @@ -133,8 +133,8 @@ 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) { var location = event.location; 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/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__)