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