From b799344d72cc15ad3a97fde522bfc21ed4d27b70 Mon Sep 17 00:00:00 2001 From: Piotr Gawron <piotr.gawron@uni.lu> Date: Fri, 1 Dec 2017 18:04:49 +0100 Subject: [PATCH] set of columns visible in subject list is defined in database and provided by API --- smash/web/api_urls.py | 2 + smash/web/api_views/subject.py | 60 +++++++++++++- smash/web/migrations/0076_studysubjectlist.py | 57 +++++++++++++ smash/web/migrations/0077_subjectcolumns.py | 59 ++++++++++++++ smash/web/models/__init__.py | 6 +- smash/web/models/study_subject_list.py | 50 ++++++++++++ smash/web/models/subject_columns.py | 81 +++++++++++++++++++ smash/web/static/js/subject.js | 59 +++++++------- smash/web/templates/subjects/index.html | 22 ++--- smash/web/tests/api_views/test_subject.py | 30 ++++++- smash/web/tests/functions.py | 29 ++++++- smash/web/views/subject.py | 7 +- 12 files changed, 405 insertions(+), 57 deletions(-) create mode 100644 smash/web/migrations/0076_studysubjectlist.py create mode 100644 smash/web/migrations/0077_subjectcolumns.py create mode 100644 smash/web/models/study_subject_list.py create mode 100644 smash/web/models/subject_columns.py diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py index 93f32c28..b6eb1740 100644 --- a/smash/web/api_urls.py +++ b/smash/web/api_urls.py @@ -34,6 +34,8 @@ urlpatterns = [ url(r'^cities$', subject.cities, name='web.api.cities'), url(r'^referrals$', subject.referrals, name='web.api.referrals'), url(r'^subjects/(?P<type>[A-z]+)$', subject.subjects, name='web.api.subjects'), + url(r'^subjects:columns/(?P<subject_list_type>[A-z]+)$', subject.get_subject_columns, + name='web.api.subjects.columns'), url(r'^subject_types', subject.types, name='web.api.subject_types'), # locations diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index c47bd91d..170367b0 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -5,11 +5,12 @@ from django.db.models import Count, Case, When, Min from django.db.models import Q from django.http import JsonResponse -from web.models import StudySubject, Visit, Appointment, Subject -from web.models.constants import SUBJECT_TYPE_CHOICES +from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study +from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID +from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ + StudySubjectList from web.views import e500_error from web.views.notifications import get_subjects_with_no_visit, get_subjects_with_reminder, get_today_midnight_date -from web.views.subject import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT logger = logging.getLogger(__name__) @@ -30,6 +31,59 @@ def referrals(request): }) +def add_column(result, name, field_name, column_list, param, columns_used_in_study=None): + add = True + if columns_used_in_study: + add = getattr(columns_used_in_study, field_name) + if add: + if 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] + study_subject_list = StudySubjectList.objects.filter(study=study, type=subject_list_type) + if len(study_subject_list) > 0: + subject_columns = study_subject_list[0].visible_subject_columns + study_subject_columns = study_subject_list[0].visible_subject_study_columns + else: + subject_columns = SubjectColumns() + study_subject_columns = StudyColumns() + + result = [] + add_column(result, "ND", "nd_number", study_subject_columns, "string_filter", study.columns) + add_column(result, "Screening", "screening_number", study_subject_columns, "string_filter", study.columns) + 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, "Date of birth", "date_born", subject_columns, None) + add_column(result, "Location", "default_location", study_subject_columns, "location_filter", study.columns) + add_column(result, "Deceased", "dead", subject_columns, "yes_no_filter") + add_column(result, "Resigned", "resigned", study_subject_columns, "yes_no_filter", study.columns) + 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, "Visit 1", "visit_1", None, "visit_filter") + add_column(result, "Visit 2", "visit_2", None, "visit_filter") + add_column(result, "Visit 3", "visit_3", None, "visit_filter") + add_column(result, "Visit 4", "visit_4", None, "visit_filter") + add_column(result, "Visit 5", "visit_5", None, "visit_filter") + add_column(result, "Visit 6", "visit_6", None, "visit_filter") + add_column(result, "Visit 7", "visit_7", None, "visit_filter") + add_column(result, "Visit 8", "visit_8", None, "visit_filter") + + return JsonResponse({"columns": result}) + + @login_required def get_subjects(request, type): if type == SUBJECT_LIST_GENERIC: diff --git a/smash/web/migrations/0076_studysubjectlist.py b/smash/web/migrations/0076_studysubjectlist.py new file mode 100644 index 00000000..1da54803 --- /dev/null +++ b/smash/web/migrations/0076_studysubjectlist.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-01 14:51 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +# noinspection PyUnusedLocal +# noinspection PyPep8Naming +def create_default_study_columns(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. + StudyColumns = apps.get_model("web", "StudyColumns") + study_columns = StudyColumns.objects.create() + study_columns.postponed = False + study_columns.datetime_contact_reminder = False + study_columns.type = True + study_columns.default_location = True + study_columns.flying_team = False + study_columns.screening_number = True + study_columns.nd_number = True + 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 = True + study_columns.pd_in_family = False + study_columns.resigned = True + study_columns.resign_reason = False + study_columns.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0075_auto_20171201_1252'), + ] + + operations = [ + migrations.CreateModel( + name='StudySubjectList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', 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 o list')), + ('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Study')), + ('visible_columns', + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.StudyColumns')), + ], + ), + migrations.RunPython(create_default_study_columns), + migrations.RunSQL('insert into web_studysubjectlist (study_id, visible_columns_id, type) ' + + "select 1, max(id), 'GENERIC' from web_studycolumns;"), + ] diff --git a/smash/web/migrations/0077_subjectcolumns.py b/smash/web/migrations/0077_subjectcolumns.py new file mode 100644 index 00000000..1f2757b0 --- /dev/null +++ b/smash/web/migrations/0077_subjectcolumns.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-01 15:21 +from __future__ import unicode_literals + +import django +from django.db import migrations, models + + +# noinspection PyUnusedLocal +# noinspection PyPep8Naming +def create_default_subject_columns(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.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0076_studysubjectlist'), + ] + + operations = [ + migrations.CreateModel( + name='SubjectColumns', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sex', models.BooleanField(default=False, max_length=1, verbose_name=b'Sex')), + ('first_name', models.BooleanField(default=True, max_length=1, verbose_name=b'First name')), + ('last_name', models.BooleanField(default=True, max_length=1, verbose_name=b'Last name')), + ('languages', models.BooleanField(default=False, max_length=1, verbose_name=b'Known languages')), + ('default_written_communication_language', models.BooleanField(default=False, max_length=1, verbose_name=b'Default language for document generation')), + ('phone_number', models.BooleanField(default=False, max_length=1, verbose_name=b'Phone number')), + ('phone_number_2', models.BooleanField(default=False, max_length=1, verbose_name=b'Phone number 2')), + ('phone_number_3', models.BooleanField(default=False, max_length=1, verbose_name=b'Phone number 3')), + ('email', models.BooleanField(default=False, max_length=1, verbose_name=b'E-mail')), + ('date_born', models.BooleanField(default=False, max_length=1, verbose_name=b'Date of birth')), + ('address', models.BooleanField(default=False, max_length=1, verbose_name=b'Address')), + ('postal_code', models.BooleanField(default=False, max_length=1, verbose_name=b'Postal code')), + ('city', models.BooleanField(default=False, max_length=1, verbose_name=b'City')), + ('country', models.BooleanField(default=False, max_length=1, verbose_name=b'Country')), + ('dead', models.BooleanField(default=True, max_length=1, verbose_name=b'Deceased')), + ], + ), + migrations.RunPython(create_default_subject_columns), + migrations.RenameField( + model_name='studysubjectlist', + old_name='visible_columns', + new_name='visible_subject_study_columns', + ), + migrations.AddField( + model_name='studysubjectlist', + name='visible_subject_columns', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='web.SubjectColumns'), + preserve_default=False, + ), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index 08ec5231..5226d98e 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 subject_columns import SubjectColumns from study_columns import StudyColumns from study import Study from room import Room @@ -21,12 +22,13 @@ from item import Item from language import Language from subject import Subject from study_subject import StudySubject +from study_subject_list import StudySubjectList from contact_attempt import ContactAttempt from mail_template import MailTemplate from missing_subject import MissingSubject from inconsistent_subject import InconsistentSubject, InconsistentField - -__all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, Subject, StudySubject, +__all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, + Subject, StudySubject, StudySubjectList, SubjectColumns, Visit, Worker, ContactAttempt, ConfigurationItem, MailTemplate, AppointmentTypeLink, MissingSubject, InconsistentSubject, InconsistentField, Country, StudyColumns] diff --git a/smash/web/models/study_subject_list.py b/smash/web/models/study_subject_list.py new file mode 100644 index 00000000..dd8e94da --- /dev/null +++ b/smash/web/models/study_subject_list.py @@ -0,0 +1,50 @@ +# coding=utf-8 +from django.db import models + +from web.models import Study, SubjectColumns, StudyColumns + +SUBJECT_LIST_GENERIC = "GENERIC" +SUBJECT_LIST_NO_VISIT = "NO_VISIT" +SUBJECT_LIST_REQUIRE_CONTACT = "REQUIRE_CONTACT" + +SUBJECT_LIST_CHOICES = { + SUBJECT_LIST_GENERIC: 'Generic', + SUBJECT_LIST_NO_VISIT: 'Subjects without visit', + SUBJECT_LIST_REQUIRE_CONTACT: 'Subjects required contact', +} + + +class StudySubjectList(models.Model): + class Meta: + app_label = 'web' + + study = models.ForeignKey( + Study, + on_delete=models.CASCADE, + null=False, + ) + + visible_subject_study_columns = models.ForeignKey( + StudyColumns, + on_delete=models.CASCADE, + null=False, + ) + + visible_subject_columns = models.ForeignKey( + SubjectColumns, + on_delete=models.CASCADE, + null=False, + ) + + type = models.CharField(max_length=50, + choices=SUBJECT_LIST_CHOICES.items(), + verbose_name='Type o list', + null=True, + blank=True + ) + + def __str__(self): + return "%s %s" % (self.type, self.study) + + def __unicode__(self): + return "%s %s" % (self.type, self.study) diff --git a/smash/web/models/subject_columns.py b/smash/web/models/subject_columns.py new file mode 100644 index 00000000..ad1779b9 --- /dev/null +++ b/smash/web/models/subject_columns.py @@ -0,0 +1,81 @@ +# coding=utf-8 +from django.db import models + + +class SubjectColumns(models.Model): + class Meta: + app_label = 'web' + + sex = models.BooleanField(max_length=1, + default=False, + verbose_name='Sex', + ) + + first_name = models.BooleanField(max_length=1, + default=True, + verbose_name='First name' + ) + + last_name = models.BooleanField(max_length=1, + default=True, + verbose_name='Last name' + ) + + languages = models.BooleanField(max_length=1, + default=False, + verbose_name='Known languages' + ) + + default_written_communication_language = models.BooleanField(max_length=1, + default=False, + verbose_name='Default language for document generation' + ) + phone_number = models.BooleanField(max_length=1, + default=False, + verbose_name='Phone number' + ) + + phone_number_2 = models.BooleanField(max_length=1, + default=False, + verbose_name='Phone number 2' + ) + + phone_number_3 = models.BooleanField(max_length=1, + default=False, + verbose_name='Phone number 3' + ) + + email = models.BooleanField(max_length=1, + default=False, + verbose_name='E-mail' + ) + + date_born = models.BooleanField(max_length=1, + default=False, + verbose_name='Date of birth' + ) + + address = models.BooleanField(max_length=1, + default=False, + verbose_name='Address' + ) + + postal_code = models.BooleanField(max_length=1, + default=False, + verbose_name='Postal code' + ) + + city = models.BooleanField(max_length=1, + default=False, + verbose_name='City' + ) + + country = models.BooleanField(max_length=1, + default=False, + verbose_name='Country' + ) + + dead = models.BooleanField(max_length=1, + default=True, + verbose_name='Deceased', + ) diff --git a/smash/web/static/js/subject.js b/smash/web/static/js/subject.js index 1cc539c9..d6f5ac20 100644 --- a/smash/web/static/js/subject.js +++ b/smash/web/static/js/subject.js @@ -1,3 +1,10 @@ +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) { @@ -13,39 +20,31 @@ function createColumn(dataType, name, filter, visible, renderFunction) { }; } -function getColumns(type, getSubjectEditUrl) { +function getColumns(columns, getSubjectEditUrl) { var result = []; - // don't confuse end user - // result.push(createColumn("id", "Id", null, false)); - result.push(createColumn("nd_number", "ND", "string_filter", true)); - result.push(createColumn("screening_number", "Screening", "string_filter", true)); - result.push(createColumn("first_name", "First name", "string_filter", true)); - result.push(createColumn("last_name", "Last name", "string_filter", true)); - result.push(createColumn("date_born", "Date of birth", null, false)); - result.push(createColumn("default_location", "Location", "location_filter", true)); - result.push(createColumn("dead", "Deceased", "yes_no_filter", true)); - result.push(createColumn("resigned", "Resigned", "yes_no_filter", true)); - result.push(createColumn("postponed", "Postponed", "yes_no_filter", true)); - result.push(createColumn("information_sent", "Info sent", "yes_no_filter", true)); - result.push(createColumn("type", "Type", "type_filter", true)); - result.push(createColumn("id", "Edit", null, true, function (data, type, row, meta) { - var url = getSubjectEditUrl(row.id.toString()); - - return '<a href="' + url + '" type="button" class="btn btn-block btn-default">Edit</a>'; - })); - for (var i = 1; i <= 8; i++) { - var renderFunction = (function () { - var x = i; - return function (data, type, row, meta) { - return create_visit_row(row.visits[x - 1]); - }; - })(); - - result.push(createColumn("visit_" + i, "Visit " + i, "visit_filter", true, renderFunction)); - + 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) { diff --git a/smash/web/templates/subjects/index.html b/smash/web/templates/subjects/index.html index 7f97eb86..2c39cc33 100644 --- a/smash/web/templates/subjects/index.html +++ b/smash/web/templates/subjects/index.html @@ -52,16 +52,18 @@ worker_locations.push({id: location.id, name: location.name}); {% endfor %} - createSubjectsTable({ - worker_locations: worker_locations, - subject_types_url: "{% url 'web.api.subject_types' %}", - locations_url: "{% url 'web.api.locations' %}", - subjects_url: "{% url 'web.api.subjects' list_type %}", - tableElement: document.getElementById("table"), - columns: getColumns("{{ list_type }}", getSubjectEditUrl), - checkboxesElement: document.getElementById("visible-column-checkboxes") - }) - ; + $.get("{% url 'web.api.subjects.columns' list_type %}", function (data) { + createSubjectsTable({ + worker_locations: worker_locations, + subject_types_url: "{% url 'web.api.subject_types' %}", + locations_url: "{% url 'web.api.locations' %}", + subjects_url: "{% url 'web.api.subjects' list_type %}", + 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_subject.py b/smash/web/tests/api_views/test_subject.py index 672266db..a718164f 100644 --- a/smash/web/tests/api_views/test_subject.py +++ b/smash/web/tests/api_views/test_subject.py @@ -8,10 +8,12 @@ from django.test import Client from django.test import TestCase from django.urls import reverse -from web.api_views.subject import get_subjects_order, get_subjects_filtered, serialize_subject, SUBJECT_LIST_GENERIC, \ - SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT -from web.models import StudySubject, Appointment -from web.tests.functions import create_study_subject, create_worker, create_get_suffix, create_visit, create_appointment +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 +from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT +from web.tests.functions import create_study_subject, create_worker, create_get_suffix, create_visit, \ + create_appointment, create_empty_study_columns from web.views.notifications import get_today_midnight_date logger = logging.getLogger(__name__) @@ -46,6 +48,26 @@ class TestApi(TestCase): self.assertTrue(city_name in cities) + def test_get_columns(self): + response = self.client.get( + reverse('web.api.subjects.columns', kwargs={'subject_list_type': SUBJECT_LIST_GENERIC})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + self.assertTrue(len(columns) >= 20) + + def test_get_columns_when_study_has_no_data_columns(self): + study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0] + study.columns = create_empty_study_columns() + study.save() + + response = self.client.get( + reverse('web.api.subjects.columns', kwargs={'subject_list_type': SUBJECT_LIST_GENERIC})) + self.assertEqual(response.status_code, 200) + + columns = json.loads(response.content)['columns'] + self.assertTrue(len(columns) < 20) + def test_referrals(self): referral_name = "some referral" diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index 107bb0cb..29dc7123 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -23,6 +23,29 @@ def create_location(name="test"): return Location.objects.create(name=name) +def create_empty_study_columns(): + study_columns = StudyColumns.objects.create( + postponed=False, + datetime_contact_reminder=False, + type=False, + default_location=False, + flying_team=False, + screening_number=False, + nd_number=False, + mpower_id=False, + comments=False, + referral=False, + diagnosis=False, + year_of_diagnosis=False, + information_sent=False, + pd_in_family=False, + resigned=False, + resign_reason=False, + ) + + return study_columns + + def create_study(name="test"): study_columns = StudyColumns.objects.create() return Study.objects.create(name=name, columns=study_columns) @@ -59,9 +82,9 @@ def get_test_location(): def get_test_study(): - locations = Study.objects.filter(name="test-study") - if len(locations) > 0: - return locations[0] + studies = Study.objects.filter(name="test-study") + if len(studies) > 0: + return studies[0] else: return create_study("test-study") diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index e12ba130..5825db2b 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -4,15 +4,12 @@ import logging from django.contrib import messages from django.shortcuts import redirect, get_object_or_404 +from ..models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT from . import wrap_response -from ..forms import VisitDetailForm,SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm +from ..forms import VisitDetailForm, SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm from ..models import StudySubject, MailTemplate, Worker, Study from ..models.constants import GLOBAL_STUDY_ID -SUBJECT_LIST_GENERIC = "GENERIC" -SUBJECT_LIST_NO_VISIT = "NO_VISIT" -SUBJECT_LIST_REQUIRE_CONTACT = "REQUIRE_CONTACT" - logger = logging.getLogger(__name__) -- GitLab