diff --git a/CHANGELOG b/CHANGELOG
index 5de78527ff78208648d5195df7235c19f91ad0ba..88dca169f76a71f2a612d3afaacf89b134e9dd01 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,7 @@
 smasch (1.0.0~alpha.1-0) unstable; urgency=low
 
+  * improvement: study subject can be configured to contain custom fields
+    (#339)
   * small improvement: django command for creating admin in application (#347)
   * small improvement: django admin panel contains usable data tables (#346)
   * small improvement: possibility to unfinish visit (#351)
diff --git a/debian-files/smasch.py b/debian-files/smasch.py
index 357f4639499f4143ff9a72365c523bb38a4159ec..145ee8a0e23eb346738f4264d152675d2ea8fa32 100644
--- a/debian-files/smasch.py
+++ b/debian-files/smasch.py
@@ -16,6 +16,7 @@ DATABASES = {
 
 STATIC_ROOT = '/usr/lib/smasch/data/static'
 MEDIA_ROOT = '/usr/lib/smasch/data/media'
+UPLOAD_ROOT = '/usr/lib/smasch/data/upload'
 
 ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
 
diff --git a/requirements-dev.txt b/requirements-dev.txt
index d3241ad8559f7901ec9798eab57ec077aaca301b..8bfe3d4fe71b7aa07a14da2e5763ff2ca97750fc 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,3 +1,4 @@
 coverage==5.3
 django-debug-toolbar==1.11
 mockito==1.2.2
+parameterized==0.7.4
diff --git a/smash/web/api_urls.py b/smash/web/api_urls.py
index 7dae498a26d5e610164cf4a544429a2f9dde03a7..11a02b7b4c553ecb41c5a1272502ab437bf93e3d 100644
--- a/smash/web/api_urls.py
+++ b/smash/web/api_urls.py
@@ -37,7 +37,7 @@ urlpatterns = [
     url(r'^referrals$', subject.referrals, name='web.api.referrals'),
     url(r'^export_log$', provenance.ExportLog.as_view(), name='web.api.export_log'),
     url(r'^provenances$', provenance.CompleteLog.as_view(), name='web.api.provenances'),
-    url(r'^subjects/(?P<type>[A-z]+)$', subject.subjects, name='web.api.subjects'),
+    url(r'^subjects/(?P<subject_list_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'),
diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py
index 5cf59a19882a3472600b97c581a686e48bc99ac8..0994996d0d71d93e41f7ad2361454fdc85708c09 100644
--- a/smash/web/api_views/serialization_utils.py
+++ b/smash/web/api_views/serialization_utils.py
@@ -3,14 +3,14 @@ import logging
 logger = logging.getLogger(__name__)
 
 
-def bool_to_yes_no(val):
+def bool_to_yes_no(val: bool):
     if val:
         return "YES"
     else:
         return "NO"
 
 
-def bool_to_yes_no_null(val):
+def bool_to_yes_no_null(val: bool):
     if val is None:
         return "N/A"
     if val:
@@ -19,6 +19,21 @@ def bool_to_yes_no_null(val):
         return "NO"
 
 
+def str_to_yes_no(val: str):
+    if val.lower() == 'true':
+        return "YES"
+    else:
+        return "NO"
+
+def str_to_yes_no_null(val: str):
+    if val is None:
+        return None
+    if val.lower() == 'true':
+        return "YES"
+    else:
+        return "NO"
+
+
 def virus_test_to_str(test, date):
     if test is None and date is not None:
         return "Inconclusive"
@@ -60,7 +75,7 @@ def serialize_datetime(date):
     return result
 
 
-def add_column(result, name, field_name, column_list, param, columns_used_in_study=None, visible_param=None,
+def add_column(result, name, field_name, column_list, param_filter, columns_used_in_study=None, visible_param=None,
                sortable=True, add_param=True):
     add = add_param
     if columns_used_in_study:
@@ -75,7 +90,7 @@ def add_column(result, name, field_name, column_list, param, columns_used_in_stu
         result.append({
             "type": field_name,
             "name": name,
-            "filter": param,
+            "filter": param_filter,
             "visible": visible,
             "sortable": sortable
         })
diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py
index 946001644eefa285c19517d3ce72b04d915b8407..7e3ce4e70a1baa14c8471139864cc64fc27c50de 100644
--- a/smash/web/api_views/subject.py
+++ b/smash/web/api_views/subject.py
@@ -1,17 +1,20 @@
 import logging
 import re
+from distutils.util import strtobool
 
-from django.db.models import Count, Case, When, Min, Max
+from django.db.models import Count, Case, When, Min, Max, QuerySet
 from django.db.models import Q
 from django.http import JsonResponse
 from django.urls import reverse
-from web.models import ConfigurationItem
-from distutils.util import strtobool
 
-from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \
-    serialize_date, serialize_datetime, get_filters_for_data_table_request, virus_test_to_str
-from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study, ContactAttempt
-from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO
+from web.api_views.serialization_utils import str_to_yes_no_null, bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \
+    serialize_date, serialize_datetime, get_filters_for_data_table_request, virus_test_to_str, str_to_yes_no
+from web.models import ConfigurationItem, StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, \
+    Study, ContactAttempt
+from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO, \
+    CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, CUSTOM_FIELD_TYPE_DOUBLE, \
+    CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id, CustomStudySubjectField
 from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \
     StudySubjectList, SUBJECT_LIST_VOUCHER_EXPIRY
 from web.views import e500_error
@@ -37,6 +40,7 @@ def referrals(request):
     })
 
 
+# noinspection PyUnusedLocal
 def get_subject_columns(request, subject_list_type):
     study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0]
     study_subject_lists = StudySubjectList.objects.filter(study=study, type=subject_list_type)
@@ -118,6 +122,61 @@ def get_subject_columns(request, subject_list_type):
                    'serology_filter', study.columns)
 
     add_column(result, "Type", "type", study_subject_columns, "type_filter", study.columns)
+    for custom_study_subject_field in study.customstudysubjectfield_set.all():
+        if custom_study_subject_field.type == CUSTOM_FIELD_TYPE_TEXT:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       "string_filter",
+                       visible_param=False)
+        elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_BOOLEAN:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       "yes_no_filter",
+                       visible_param=False)
+        elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_INTEGER:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       None,
+                       sortable=False,
+                       visible_param=False)
+        elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_DOUBLE:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       None,
+                       sortable=False,
+                       visible_param=False)
+        elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_DATE:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       None,
+                       visible_param=False)
+        elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_SELECT_LIST:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       'select_filter:' + custom_study_subject_field.possible_values,
+                       visible_param=False)
+        elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_FILE:
+            add_column(result,
+                       custom_study_subject_field.name,
+                       get_study_subject_field_id(custom_study_subject_field),
+                       study_subject_columns,
+                       'select_filter:N/A;Available',
+                       visible_param=False)
+        else:
+            raise NotImplementedError
+
     add_column(result, "Edit", "edit", None, None, sortable=False)
 
     for one_based_idx, visit_number in enumerate(visit_numbers, 1):
@@ -128,17 +187,17 @@ def get_subject_columns(request, subject_list_type):
     return JsonResponse({"columns": result})
 
 
-def get_subjects(request, type):
-    if type == SUBJECT_LIST_GENERIC:
+def get_subjects(request, list_type):
+    if list_type == SUBJECT_LIST_GENERIC:
         return StudySubject.objects.all()
-    elif type == SUBJECT_LIST_NO_VISIT:
+    elif list_type == SUBJECT_LIST_NO_VISIT:
         return get_subjects_with_no_visit(request.user)
-    elif type == SUBJECT_LIST_REQUIRE_CONTACT:
+    elif list_type == SUBJECT_LIST_REQUIRE_CONTACT:
         return get_subjects_with_reminder(request.user)
-    elif type == SUBJECT_LIST_VOUCHER_EXPIRY:
+    elif list_type == SUBJECT_LIST_VOUCHER_EXPIRY:
         return get_subjects_with_almost_expired_vouchers(request.user)
     else:
-        raise TypeError("Unknown query type: " + type)
+        raise TypeError("Unknown query type: " + list_type)
 
 
 def order_by_visit(subjects_to_be_ordered, order_direction, visit_number):
@@ -147,14 +206,16 @@ def order_by_visit(subjects_to_be_ordered, order_direction, visit_number):
         order_direction + 'sort_visit_date')
 
 
-def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, column_filters={}):
+def get_subjects_order(subjects_to_be_ordered: QuerySet, order_column, order_direction, column_filters=None):
+    if column_filters is None:
+        column_filters = {}
     result = subjects_to_be_ordered
     if order_direction == "asc":
         order_direction = ""
     else:
         order_direction = "-"
     if order_column is None:
-        logger.warn("Column cannot be null")
+        logger.warning("Column cannot be null")
     elif order_column == "first_name":
         result = subjects_to_be_ordered.order_by(order_direction + 'subject__first_name')
     elif order_column == "last_name":
@@ -228,8 +289,14 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co
         result = subjects_to_be_ordered.order_by(order_direction + order_column)
     elif re.search(r'^virus_test_[1-5]_igg_status', order_column):
         result = subjects_to_be_ordered.order_by(order_direction + order_column)
+    elif re.search(r'^custom_field-[0-9]+$', order_column):
+        field_id = int(order_column.replace("custom_field-", ""))
+        result = subjects_to_be_ordered.annotate(
+            custom_field_value=Min(Case(When(customstudysubjectvalue__study_subject_field__id=field_id,
+                                             then='customstudysubjectvalue__value')))).order_by(
+            order_direction + 'custom_field_value')
     else:
-        logger.warn("Unknown sort column: " + str(order_column))
+        logger.warning("Unknown sort column: " + str(order_column))
     return result
 
 
@@ -298,13 +365,13 @@ def filter_by_visit(result, visit_number, visit_type):
     return result
 
 
-def get_subjects_filtered(subjects_to_be_filtered, filters):
+def get_subjects_filtered(subjects_to_be_filtered: QuerySet, filters) -> QuerySet:
     result = subjects_to_be_filtered
     for row in filters:
         column = row[0]
         value = row[1]
         if column is None:
-            logger.warn("Filter column cannot be null")
+            logger.warning("Filter column cannot be null")
         elif column == "first_name":
             result = result.filter(subject__first_name__icontains=value)
         elif column == "last_name":
@@ -371,6 +438,33 @@ def get_subjects_filtered(subjects_to_be_filtered, filters):
         elif str(column).startswith("visit_"):
             visit_number = get_visit_number_from_visit_x_string(column)
             result = filter_by_visit(result, visit_number, value)
+        elif re.search(r'^custom_field-[0-9]+$', column):
+            field_id = int(column.replace("custom_field-", ""))
+            field = CustomStudySubjectField.objects.get(pk=field_id)
+            if field.type == CUSTOM_FIELD_TYPE_TEXT:
+                result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                       customstudysubjectvalue__value__icontains=value)
+            elif field.type == CUSTOM_FIELD_TYPE_BOOLEAN:
+                if value.lower() == 'true' or value.lower() == 'false':
+                    result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                           customstudysubjectvalue__value__icontains=value)
+                else:
+                    result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                           customstudysubjectvalue__value='')
+            elif field.type == CUSTOM_FIELD_TYPE_INTEGER or field.type == CUSTOM_FIELD_TYPE_DOUBLE:
+                result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                       customstudysubjectvalue__value=value)
+            elif field.type == CUSTOM_FIELD_TYPE_DATE:
+                result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                       customstudysubjectvalue__value=value)
+            elif field.type == CUSTOM_FIELD_TYPE_INTEGER or field.type == CUSTOM_FIELD_TYPE_SELECT_LIST:
+                result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                       customstudysubjectvalue__value=value)
+            elif field.type == CUSTOM_FIELD_TYPE_FILE:
+                result = result.filter(customstudysubjectvalue__study_subject_field__id=field_id,
+                                       customstudysubjectvalue__value__isnull=(value == 'N/A'))
+            else:
+                raise NotImplementedError
         elif column == "":
             pass
         else:
@@ -379,11 +473,11 @@ def get_subjects_filtered(subjects_to_be_filtered, filters):
                 message += "[None]"
             else:
                 message += str(column)
-            logger.warn(message)
+            logger.warning(message)
     return result
 
 
-def subjects(request, type):
+def subjects(request, subject_list_type):
     try:
         # id of the query from dataTable: https://datatables.net/manual/server-side
         draw = int(request.GET.get("draw", "-1"))
@@ -396,7 +490,7 @@ def subjects(request, type):
 
         filters = get_filters_for_data_table_request(request)
 
-        all_subjects = get_subjects(request, type)
+        all_subjects = get_subjects(request, subject_list_type)
 
         count = all_subjects.count()
 
@@ -532,4 +626,28 @@ def serialize_subject(study_subject):
                                                                     "virus_test_{}_collection_date".format(i))
         result["virus_test_{}_iga_status".format(i)] = getattr(study_subject, "virus_test_{}_iga_status".format(i))
         result["virus_test_{}_igg_status".format(i)] = getattr(study_subject, "virus_test_{}_igg_status".format(i))
+
+    for field_value in study_subject.custom_data_values:
+        if field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_TEXT \
+                or field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_INTEGER \
+                or field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_DOUBLE:
+            val = field_value.value
+            if val is None:
+                val = ''
+            result[get_study_subject_field_id(field_value.study_subject_field)] = val
+        elif field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_BOOLEAN:
+            result[get_study_subject_field_id(field_value.study_subject_field)] = str_to_yes_no_null(field_value.value)
+        elif field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_DATE:
+            result[get_study_subject_field_id(field_value.study_subject_field)] = field_value.value
+        elif field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_SELECT_LIST:
+            result[get_study_subject_field_id(field_value.study_subject_field)] = field_value.value
+        elif field_value.study_subject_field.type == CUSTOM_FIELD_TYPE_FILE:
+            if field_value.value is None:
+                result[get_study_subject_field_id(field_value.study_subject_field)] = ''
+            else:
+                result[get_study_subject_field_id(field_value.study_subject_field)] = reverse(
+                    'web.views.uploaded_files') + '?file=' + str(field_value.value)
+        else:
+            raise NotImplementedError
+
     return result
diff --git a/smash/web/forms/custom_study_subject_field_forms.py b/smash/web/forms/custom_study_subject_field_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..a986a466a46ea78785d57f3306c695d781e02887
--- /dev/null
+++ b/smash/web/forms/custom_study_subject_field_forms.py
@@ -0,0 +1,72 @@
+import re
+
+from django import forms
+
+from web.models.constants import CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, CUSTOM_FIELD_TYPE_DOUBLE, \
+    CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField
+
+
+class CustomStudySubjectFieldForm(forms.ModelForm):
+    possible_values = forms.CharField(label='Possible values',
+                                      widget=forms.TextInput(attrs={'placeholder': "values separated by ';'"}),
+                                      required=False)
+
+    def __init__(self, *args, **kwargs):
+        super(CustomStudySubjectFieldForm, self).__init__(*args, **kwargs)
+
+    def clean(self):
+        cleaned_data = super(CustomStudySubjectFieldForm, self).clean()
+
+        if cleaned_data['default_value'] != '' \
+                and cleaned_data['default_value'] is not None:
+            if cleaned_data['type'] == CUSTOM_FIELD_TYPE_BOOLEAN \
+                    and cleaned_data['default_value'].lower() != 'false' \
+                    and cleaned_data['default_value'].lower() != 'true':
+                self.add_error('default_value',
+                               "Default value can be one of the following: 'true', 'false', ''.")
+            elif cleaned_data['type'] == CUSTOM_FIELD_TYPE_INTEGER \
+                    and re.match(r"[-+]?\d+$", cleaned_data['default_value']) is None:
+                self.add_error('default_value',
+                               "Default value must be integer (or empty).")
+            elif cleaned_data['type'] == CUSTOM_FIELD_TYPE_DOUBLE \
+                    and re.match(r"[-+]?\d+?\.\d+?$", cleaned_data['default_value']) is None:
+                self.add_error('default_value',
+                               "Default value must be numeric (or empty).")
+            elif cleaned_data['type'] == CUSTOM_FIELD_TYPE_DATE \
+                    and re.match(r"\d\d\d\d-\d\d-\d\d?$", cleaned_data['default_value']) is None:
+                self.add_error('default_value',
+                               "Default value must be in format YYYY-MM-DD (or empty).")
+            elif cleaned_data['type'] == CUSTOM_FIELD_TYPE_SELECT_LIST \
+                    and not cleaned_data['default_value'] in cleaned_data['possible_values'].split(';'):
+                self.add_error('default_value',
+                               "Default value must be listed in possible values (semicolon separated list)")
+            elif cleaned_data['type'] == CUSTOM_FIELD_TYPE_FILE \
+                    and cleaned_data['default_value'] is not None \
+                    and cleaned_data['default_value'] != '':
+                self.add_error('default_value',
+                               "Default value is not available for files")
+        return cleaned_data
+
+
+class CustomStudySubjectFieldAddForm(CustomStudySubjectFieldForm):
+    class Meta:
+        model = CustomStudySubjectField
+        fields = '__all__'
+
+    def __init__(self, *args, **kwargs):
+        self.study = kwargs.pop('study', None)
+        super(CustomStudySubjectFieldForm, self).__init__(*args, **kwargs)
+
+    def save(self, commit=True) -> CustomStudySubjectField:
+        self.instance.study_id = self.study.id
+        return super(CustomStudySubjectFieldAddForm, self).save(commit)
+
+
+class CustomStudySubjectFieldEditForm(CustomStudySubjectFieldForm):
+    class Meta:
+        model = CustomStudySubjectField
+        fields = '__all__'
+
+    def __init__(self, *args, **kwargs):
+        super(CustomStudySubjectFieldForm, self).__init__(*args, **kwargs)
diff --git a/smash/web/forms/study_subject_forms.py b/smash/web/forms/study_subject_forms.py
index bce5929d844935d572b90776d8ae5d28d8212e69..6211544bd6817c549a35561bb9ce1e60786a3b86 100644
--- a/smash/web/forms/study_subject_forms.py
+++ b/smash/web/forms/study_subject_forms.py
@@ -1,19 +1,101 @@
+import datetime
 import logging
-
+import re
 from distutils.util import strtobool
+
 from django import forms
 from django.forms import ModelForm
 
-from web.forms.forms import DATETIMEPICKER_DATE_ATTRS, get_worker_from_args
-from web.models import ConfigurationItem
-from web.models import StudySubject, Study, StudyColumns, VoucherType, Worker
-from web.models.constants import SCREENING_NUMBER_PREFIXES_FOR_TYPE, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO
+from web.forms.forms import DATETIMEPICKER_DATE_ATTRS, get_worker_from_args, DATEPICKER_DATE_ATTRS
+from web.models import ConfigurationItem, StudySubject, Study, StudyColumns, VoucherType, Worker
+from web.models.constants import SCREENING_NUMBER_PREFIXES_FOR_TYPE, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO, \
+    CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, CUSTOM_FIELD_TYPE_DOUBLE, \
+    CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField, CustomStudySubjectValue
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id
 from web.models.worker_study_role import WORKER_HEALTH_PARTNER
 from web.widgets.secure_file_widget import SecuredFileWidget
 
 logger = logging.getLogger(__name__)
 
 
+def get_custom_select_choices(possible_values):
+    result = list()
+    index = 1
+    result.append(('0', '---'))
+    for value in possible_values.split(";"):
+        result.append((str(index), value))
+        index += 1
+    return result
+
+
+def create_field_for_custom_study_subject_field(study_subject_field: CustomStudySubjectField,
+                                                field_value: CustomStudySubjectValue = None) -> forms.Field:
+    val = study_subject_field.default_value
+    if field_value is not None:
+        val = field_value.value
+    if study_subject_field.type == CUSTOM_FIELD_TYPE_TEXT:
+        field = forms.CharField(label=study_subject_field.name, initial=val,
+                                required=study_subject_field.required, disabled=study_subject_field.readonly)
+    elif study_subject_field.type == CUSTOM_FIELD_TYPE_BOOLEAN:
+        initial = False
+        if val is not None and val.lower() == 'true':
+            initial = True
+        field = forms.BooleanField(label=study_subject_field.name, initial=initial,
+                                   required=study_subject_field.required, disabled=study_subject_field.readonly)
+    elif study_subject_field.type == CUSTOM_FIELD_TYPE_INTEGER:
+        initial = None
+        if val is not None and re.match(r"[-+]?\d+$", val) is not None:
+            initial = int(val)
+        field = forms.IntegerField(label=study_subject_field.name, initial=initial,
+                                   required=study_subject_field.required, disabled=study_subject_field.readonly)
+    elif study_subject_field.type == CUSTOM_FIELD_TYPE_DOUBLE:
+        initial = None
+        if val is not None and re.match(r"[-+]?\d+?\.\d+?$", val) is not None:
+            initial = float(val)
+        field = forms.FloatField(label=study_subject_field.name, initial=initial,
+                                 required=study_subject_field.required, disabled=study_subject_field.readonly)
+    elif study_subject_field.type == CUSTOM_FIELD_TYPE_DATE:
+        initial = None
+        if val is not None and val != '':
+            initial = datetime.datetime.strptime(val, '%Y-%m-%d')
+        field = forms.DateTimeField(label=study_subject_field.name, initial=initial,
+                                    required=study_subject_field.required, disabled=study_subject_field.readonly,
+                                    widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"))
+    elif study_subject_field.type == CUSTOM_FIELD_TYPE_SELECT_LIST:
+        initial = '0'
+        for v, k in get_custom_select_choices(study_subject_field.possible_values):
+            if k == val:
+                initial = v
+        field = forms.ChoiceField(label=study_subject_field.name, initial=initial,
+                                  required=study_subject_field.required, disabled=study_subject_field.readonly,
+                                  choices=get_custom_select_choices(study_subject_field.possible_values))
+    elif study_subject_field.type == CUSTOM_FIELD_TYPE_FILE:
+        initial = None
+        if val is not None:
+            class CustomFileField:
+                url = ''
+
+                def __str__(self):
+                    return "%s" % self.url
+
+                def __unicode__(self):
+                    return "%s" % self.url
+            initial = CustomFileField()
+            initial.url = val
+
+        field = forms.FileField(
+            label=study_subject_field.name,
+            required=study_subject_field.required,
+            disabled=study_subject_field.readonly,
+            initial=initial,
+            widget=SecuredFileWidget(attrs={'multiple': True})
+        )
+    else:
+        raise NotImplementedError
+    return field
+
+
 class StudySubjectForm(ModelForm):
     datetime_contact_reminder = forms.DateTimeField(label="Contact on",
                                                     widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS),
@@ -24,6 +106,16 @@ class StudySubjectForm(ModelForm):
 
     def __init__(self, *args, **kwargs):
         super(StudySubjectForm, self).__init__(*args, **kwargs)
+        instance = kwargs.get('instance')
+        if instance:
+            for value in instance.custom_data_values:
+                field_id = get_study_subject_field_id(value.study_subject_field)
+                self.fields[field_id] = create_field_for_custom_study_subject_field(value.study_subject_field, value)
+        else:
+            for field_type in CustomStudySubjectField.objects.filter(study=self.study):
+                field_id = get_study_subject_field_id(field_type)
+                self.fields[field_id] = create_field_for_custom_study_subject_field(field_type)
+
         self.fields['health_partner'].queryset = Worker.get_workers_by_worker_type(
             WORKER_HEALTH_PARTNER)
 
@@ -62,6 +154,25 @@ class StudySubjectForm(ModelForm):
             if test_result is None and test_date is not None:
                 self.initial[test_result_column_name] = "Inc"
 
+    def clean(self):
+        cleaned_data = super(StudySubjectForm, self).clean()
+        subject_id = -1
+        if getattr(self, 'instance', None) is not None:
+            subject_id = getattr(self, 'instance', None).id
+        for field_type in CustomStudySubjectField.objects.filter(study=self.study):
+            if field_type.unique:
+                field_id = get_study_subject_field_id(field_type)
+                value = cleaned_data[field_id]
+                if value is not None and value != "":
+                    count = StudySubject.objects.filter(customstudysubjectvalue__study_subject_field=field_type,
+                                                        customstudysubjectvalue__value=value,
+                                                        study=self.study) \
+                        .exclude(id=subject_id) \
+                        .count()
+                    if count > 0:
+                        self.add_error(field_id, "Value must be unique within the study")
+        return cleaned_data
+
 
 class StudySubjectAddForm(StudySubjectForm):
     class Meta:
@@ -79,9 +190,14 @@ class StudySubjectAddForm(StudySubjectForm):
 
         prepare_study_subject_fields(fields=self.fields, study=self.study)
 
-    def save(self, commit=True):
+    def save(self, commit=True) -> StudySubject:
         self.instance.study_id = self.study.id
-        return super(StudySubjectAddForm, self).save(commit)
+        instance = super(StudySubjectAddForm, self).save(commit)
+        # we can add custom values only after object exists in the database
+        for field_type in CustomStudySubjectField.objects.filter(study=self.study):
+            self.instance.set_custom_data_value(field_type, get_study_subject_field_value(field_type, self[
+                get_study_subject_field_id(field_type)]))
+        return instance
 
     def build_screening_number(self, cleaned_data):
         screening_number = cleaned_data.get('screening_number', None)
@@ -154,6 +270,39 @@ def get_study_from_study_subject_instance(study_subject):
         return Study(columns=StudyColumns())
 
 
+def get_study_subject_field_value(field_type: CustomStudySubjectField, field: forms.BoundField):
+    if field_type.type == CUSTOM_FIELD_TYPE_TEXT:
+        return field.value()
+    elif field_type.type == CUSTOM_FIELD_TYPE_BOOLEAN:
+        return str(field.value())
+    elif field_type.type == CUSTOM_FIELD_TYPE_INTEGER:
+        if field.value() is None:
+            return None
+        return str(field.value())
+    elif field_type.type == CUSTOM_FIELD_TYPE_DOUBLE:
+        if field.value() is None:
+            return None
+        return str(field.value())
+    elif field_type.type == CUSTOM_FIELD_TYPE_DATE:
+        if field.value() is None:
+            return None
+        return field.value()
+    elif field_type.type == CUSTOM_FIELD_TYPE_SELECT_LIST:
+        if field.value() == '0':
+            return ''
+        if field.value() is None:
+            return None
+        for v, k in get_custom_select_choices(field_type.possible_values):
+            if v == field.value():
+                return k
+        return None
+    elif field_type.type == CUSTOM_FIELD_TYPE_FILE:
+        # this must be handled in view
+        return None
+    else:
+        raise NotImplementedError
+
+
 class StudySubjectEditForm(StudySubjectForm):
 
     def __init__(self, *args, **kwargs):
@@ -171,9 +320,17 @@ class StudySubjectEditForm(StudySubjectForm):
         prepare_study_subject_fields(fields=self.fields, study=self.study)
 
     def clean(self):
-        validate_subject_nd_number(self, self.cleaned_data)
-        validate_subject_mpower_number(self, self.cleaned_data)
-        validate_subject_resign_reason(self, self.cleaned_data)
+        cleaned_data = super(StudySubjectEditForm, self).clean()
+        validate_subject_nd_number(self, cleaned_data)
+        validate_subject_mpower_number(self, cleaned_data)
+        validate_subject_resign_reason(self, cleaned_data)
+        return cleaned_data
+
+    def save(self, commit=True) -> StudySubject:
+        for field_type in CustomStudySubjectField.objects.filter(study=self.study):
+            self.instance.set_custom_data_value(field_type, get_study_subject_field_value(field_type, self[
+                get_study_subject_field_id(field_type)]))
+        return super(StudySubjectForm, self).save(commit)
 
     class Meta:
         model = StudySubject
diff --git a/smash/web/forms/subject_forms.py b/smash/web/forms/subject_forms.py
index e60082bc21ccfc1253e8108370fdb6fb192d34bd..38555a3459b80225c49143904ca6bbe39d7732d5 100644
--- a/smash/web/forms/subject_forms.py
+++ b/smash/web/forms/subject_forms.py
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
 
 
 def validate_subject_country(self, cleaned_data):
-    if cleaned_data['country'].id == COUNTRY_OTHER_ID:
+    if 'country' not in cleaned_data or cleaned_data['country'].id == COUNTRY_OTHER_ID:
         self.add_error('country', "Select valid country")
 
 
@@ -20,6 +20,7 @@ def validate_social_security_number(self, number):
     if not is_valid_social_security_number(number):
         self.add_error('social_security_number', "Social security number is invalid")
 
+
 FIELD_ORDER = ["first_name", "last_name", "sex", "date_born", "social_security_number",
                "default_written_communication_language", "languages", "phone_number", "phone_number_2",
                "phone_number_3", "address", "city", "postal_code", "country"]
@@ -27,7 +28,7 @@ FIELD_ORDER = ["first_name", "last_name", "sex", "date_born", "social_security_n
 
 class SubjectAddForm(ModelForm):
     date_born = forms.DateField(label="Date of birth",
-                                widget=forms.DateInput( DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False)
+                                widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False)
 
     field_order = FIELD_ORDER
 
diff --git a/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py b/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py
new file mode 100644
index 0000000000000000000000000000000000000000..25e16fa4fc684c640e69cfe713f22b31f1c26229
--- /dev/null
+++ b/smash/web/migrations/0177_customstudysubjectfield_customstudysubjectvalue.py
@@ -0,0 +1,37 @@
+# Generated by Django 2.0.13 on 2020-11-06 08:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('web', '0176_configurationitem_local_setting_clean'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CustomStudySubjectField',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=20)),
+                ('type', models.CharField(choices=[('TEXT', 'Text'), ('BOOL', 'Boolean (True/False)'), ('INTEGER', 'Integer'), ('DOUBLE', 'Double (real number)'), ('DATE', 'Date'), ('SELECT_LIST', 'Select list')], max_length=20)),
+                ('possible_values', models.CharField(blank=True, default='', max_length=1024, null=True)),
+                ('default_value', models.CharField(blank=True, max_length=20, null=True)),
+                ('readonly', models.BooleanField(default=False)),
+                ('required', models.BooleanField(default=False)),
+                ('unique', models.BooleanField(default=False)),
+                ('study', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.Study', verbose_name='Study')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='CustomStudySubjectValue',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('value', models.CharField(blank=True, max_length=2048, null=True)),
+                ('study_subject', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.StudySubject', verbose_name='Study')),
+                ('study_subject_field', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.CustomStudySubjectField', verbose_name='Custom Field')),
+            ],
+        ),
+    ]
diff --git a/smash/web/migrations/0178_auto_20201116_1250.py b/smash/web/migrations/0178_auto_20201116_1250.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f94b8c9e8b6a8846490aa674d4156a807ac137d
--- /dev/null
+++ b/smash/web/migrations/0178_auto_20201116_1250.py
@@ -0,0 +1,1356 @@
+# Generated by Django 2.0.13 on 2020-11-16 12:50
+
+import django.core.files.storage
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('web', '0177_customstudysubjectfield_customstudysubjectvalue'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='appointment',
+            name='appointment_types',
+            field=models.ManyToManyField(blank=True, related_name='new_appointment', through='web.AppointmentTypeLink', to='web.AppointmentType', verbose_name='Appointment types'),
+        ),
+        migrations.AlterField(
+            model_name='appointment',
+            name='post_mail_sent',
+            field=models.BooleanField(default=False, verbose_name='Post mail sent'),
+        ),
+        migrations.AlterField(
+            model_name='appointment',
+            name='status',
+            field=models.CharField(choices=[('SCHEDULED', 'Scheduled'), ('FINISHED', 'Finished'), ('CANCELLED', 'Cancelled'), ('NO_SHOW', 'No Show')], default='SCHEDULED', max_length=20, verbose_name='Status'),
+        ),
+        migrations.AlterField(
+            model_name='appointment',
+            name='visit',
+            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Visit', verbose_name='Visit ID'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='appointment_types',
+            field=models.BooleanField(default=True, verbose_name='Appointment types'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='comment',
+            field=models.BooleanField(default=False, verbose_name='Comment'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='datetime_when',
+            field=models.BooleanField(default=True, verbose_name='Comment'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='flying_team',
+            field=models.BooleanField(default=False, verbose_name='Flying team'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='length',
+            field=models.BooleanField(default=False, verbose_name='Appointment length'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='location',
+            field=models.BooleanField(default=False, verbose_name='Location'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='post_mail_sent',
+            field=models.BooleanField(default=False, verbose_name='Post mail sent'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='room',
+            field=models.BooleanField(default=False, verbose_name='Room'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='status',
+            field=models.BooleanField(default=False, verbose_name='Status'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentcolumns',
+            name='worker_assigned',
+            field=models.BooleanField(default=False, verbose_name='Worker conducting the assessment'),
+        ),
+        migrations.AlterField(
+            model_name='appointmentlist',
+            name='type',
+            field=models.CharField(choices=[('GENERIC', 'Generic'), ('UNFINISHED', 'Unfinished'), ('APPROACHING', 'Approaching')], max_length=50, verbose_name='Type of list'),
+        ),
+        migrations.AlterField(
+            model_name='appointmenttype',
+            name='calendar_font_color',
+            field=models.CharField(default='#00000', max_length=2000, verbose_name='Calendar font color'),
+        ),
+        migrations.AlterField(
+            model_name='availability',
+            name='available_from',
+            field=models.TimeField(verbose_name='Available from'),
+        ),
+        migrations.AlterField(
+            model_name='availability',
+            name='available_till',
+            field=models.TimeField(verbose_name='Available until'),
+        ),
+        migrations.AlterField(
+            model_name='availability',
+            name='day_number',
+            field=models.IntegerField(choices=[(1, 'MONDAY'), (2, 'TUESDAY'), (3, 'WEDNESDAY'), (4, 'THURSDAY'), (5, 'FRIDAY'), (6, 'SATURDAY'), (7, 'SUNDAY')], verbose_name='Day of the week'),
+        ),
+        migrations.AlterField(
+            model_name='availability',
+            name='person',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name='Worker'),
+        ),
+        migrations.AlterField(
+            model_name='configurationitem',
+            name='name',
+            field=models.CharField(editable=False, max_length=255, verbose_name='Name'),
+        ),
+        migrations.AlterField(
+            model_name='configurationitem',
+            name='type',
+            field=models.CharField(editable=False, max_length=50, verbose_name='Type'),
+        ),
+        migrations.AlterField(
+            model_name='configurationitem',
+            name='value',
+            field=models.CharField(max_length=1024, verbose_name='Value'),
+        ),
+        migrations.AlterField(
+            model_name='contactattempt',
+            name='datetime_when',
+            field=models.DateTimeField(help_text='When did the contact occurred?', verbose_name='When'),
+        ),
+        migrations.AlterField(
+            model_name='contactattempt',
+            name='subject',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.StudySubject', verbose_name='Subject'),
+        ),
+        migrations.AlterField(
+            model_name='contactattempt',
+            name='type',
+            field=models.CharField(choices=[('E', 'Email'), ('M', 'Post mail'), ('F', 'Face to face'), ('X', 'Fax'), ('P', 'Phone'), ('S', 'SMS')], default='P', max_length=2),
+        ),
+        migrations.AlterField(
+            model_name='contactattempt',
+            name='worker',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name='Worker'),
+        ),
+        migrations.AlterField(
+            model_name='customstudysubjectfield',
+            name='type',
+            field=models.CharField(choices=[('TEXT', 'Text'), ('BOOL', 'Boolean (True/False)'), ('INTEGER', 'Integer'), ('DOUBLE', 'Double (real number)'), ('DATE', 'Date'), ('SELECT_LIST', 'Select list'), ('FILE', 'File')], max_length=20),
+        ),
+        migrations.AlterField(
+            model_name='holiday',
+            name='info',
+            field=models.TextField(blank=True, max_length=2000, verbose_name='Comments'),
+        ),
+        migrations.AlterField(
+            model_name='holiday',
+            name='kind',
+            field=models.CharField(choices=[('H', 'Holiday'), ('X', 'Extra Availability')], default='H', help_text='Defines the kind of availability. Either Holiday or Extra Availability.', max_length=1),
+        ),
+        migrations.AlterField(
+            model_name='inconsistentfield',
+            name='inconsistent_subject',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.InconsistentSubject', verbose_name='Invalid fields'),
+        ),
+        migrations.AlterField(
+            model_name='inconsistentfield',
+            name='name',
+            field=models.CharField(max_length=255, verbose_name='Field name'),
+        ),
+        migrations.AlterField(
+            model_name='inconsistentfield',
+            name='redcap_value',
+            field=models.CharField(max_length=255, verbose_name='RED Cap value'),
+        ),
+        migrations.AlterField(
+            model_name='inconsistentfield',
+            name='smash_value',
+            field=models.CharField(max_length=255, verbose_name='Smash value'),
+        ),
+        migrations.AlterField(
+            model_name='inconsistentsubject',
+            name='redcap_url',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='URL to RED Cap subject'),
+        ),
+        migrations.AlterField(
+            model_name='inconsistentsubject',
+            name='subject',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.StudySubject', verbose_name='Subject'),
+        ),
+        migrations.AlterField(
+            model_name='language',
+            name='image',
+            field=models.ImageField(upload_to=''),
+        ),
+        migrations.AlterField(
+            model_name='language',
+            name='locale',
+            field=models.CharField(choices=[('af_ZA', 'af_ZA'), ('am_ET', 'am_ET'), ('ar_AE', 'ar_AE'), ('ar_BH', 'ar_BH'), ('ar_DZ', 'ar_DZ'), ('ar_EG', 'ar_EG'), ('ar_IQ', 'ar_IQ'), ('ar_JO', 'ar_JO'), ('ar_KW', 'ar_KW'), ('ar_LB', 'ar_LB'), ('ar_LY', 'ar_LY'), ('ar_MA', 'ar_MA'), ('ar_OM', 'ar_OM'), ('ar_QA', 'ar_QA'), ('ar_SA', 'ar_SA'), ('ar_SY', 'ar_SY'), ('ar_TN', 'ar_TN'), ('ar_YE', 'ar_YE'), ('arn_CL', 'arn_CL'), ('as_IN', 'as_IN'), ('az_AZ', 'az_AZ'), ('az_AZ', 'az_AZ'), ('ba_RU', 'ba_RU'), ('be_BY', 'be_BY'), ('bg_BG', 'bg_BG'), ('bn_IN', 'bn_IN'), ('bo_BT', 'bo_BT'), ('bo_CN', 'bo_CN'), ('br_FR', 'br_FR'), ('bs_BA', 'bs_BA'), ('bs_BA', 'bs_BA'), ('ca_ES', 'ca_ES'), ('co_FR', 'co_FR'), ('cs_CZ', 'cs_CZ'), ('cy_GB', 'cy_GB'), ('da_DK', 'da_DK'), ('de_AT', 'de_AT'), ('de_CH', 'de_CH'), ('de_DE', 'de_DE'), ('de_LI', 'de_LI'), ('de_LU', 'de_LU'), ('div_MV', 'div_MV'), ('dsb_DE', 'dsb_DE'), ('el_GR', 'el_GR'), ('en_AU', 'en_AU'), ('en_BZ', 'en_BZ'), ('en_CA', 'en_CA'), ('en_CB', 'en_CB'), ('en_GB', 'en_GB'), ('en_IE', 'en_IE'), ('en_IN', 'en_IN'), ('en_IN', 'en_IN'), ('en_JA', 'en_JA'), ('en_MY', 'en_MY'), ('en_NZ', 'en_NZ'), ('en_PH', 'en_PH'), ('en_TT', 'en_TT'), ('en_US', 'en_US'), ('en_ZA', 'en_ZA'), ('en_ZW', 'en_ZW'), ('es_AR', 'es_AR'), ('es_BO', 'es_BO'), ('es_CL', 'es_CL'), ('es_CO', 'es_CO'), ('es_CR', 'es_CR'), ('es_DO', 'es_DO'), ('es_EC', 'es_EC'), ('es_ES', 'es_ES'), ('es_ES', 'es_ES'), ('es_GT', 'es_GT'), ('es_HN', 'es_HN'), ('es_MX', 'es_MX'), ('es_NI', 'es_NI'), ('es_PA', 'es_PA'), ('es_PE', 'es_PE'), ('es_PR', 'es_PR'), ('es_PY', 'es_PY'), ('es_SV', 'es_SV'), ('es_UR', 'es_UR'), ('es_US', 'es_US'), ('es_VE', 'es_VE'), ('et_EE', 'et_EE'), ('eu_ES', 'eu_ES'), ('fa_IR', 'fa_IR'), ('fi_FI', 'fi_FI'), ('fil_PH', 'fil_PH'), ('fo_FO', 'fo_FO'), ('fr_BE', 'fr_BE'), ('fr_CA', 'fr_CA'), ('fr_CH', 'fr_CH'), ('fr_FR', 'fr_FR'), ('fr_LU', 'fr_LU'), ('fr_MC', 'fr_MC'), ('fy_NL', 'fy_NL'), ('ga_IE', 'ga_IE'), ('gbz_AF', 'gbz_AF'), ('gl_ES', 'gl_ES'), ('gsw_FR', 'gsw_FR'), ('gu_IN', 'gu_IN'), ('ha_NG', 'ha_NG'), ('he_IL', 'he_IL'), ('hi_IN', 'hi_IN'), ('hr_BA', 'hr_BA'), ('hr_HR', 'hr_HR'), ('hu_HU', 'hu_HU'), ('hy_AM', 'hy_AM'), ('id_ID', 'id_ID'), ('ii_CN', 'ii_CN'), ('is_IS', 'is_IS'), ('it_CH', 'it_CH'), ('it_IT', 'it_IT'), ('iu_CA', 'iu_CA'), ('iu_CA', 'iu_CA'), ('ja_JP', 'ja_JP'), ('ka_GE', 'ka_GE'), ('kh_KH', 'kh_KH'), ('kk_KZ', 'kk_KZ'), ('kl_GL', 'kl_GL'), ('kn_IN', 'kn_IN'), ('ko_KR', 'ko_KR'), ('kok_IN', 'kok_IN'), ('ky_KG', 'ky_KG'), ('lb_LU', 'lb_LU'), ('lo_LA', 'lo_LA'), ('lt_LT', 'lt_LT'), ('lv_LV', 'lv_LV'), ('mi_NZ', 'mi_NZ'), ('mk_MK', 'mk_MK'), ('ml_IN', 'ml_IN'), ('mn_CN', 'mn_CN'), ('mn_MN', 'mn_MN'), ('moh_CA', 'moh_CA'), ('mr_IN', 'mr_IN'), ('ms_BN', 'ms_BN'), ('ms_MY', 'ms_MY'), ('mt_MT', 'mt_MT'), ('nb_NO', 'nb_NO'), ('ne_NP', 'ne_NP'), ('nl_BE', 'nl_BE'), ('nl_NL', 'nl_NL'), ('nn_NO', 'nn_NO'), ('ns_ZA', 'ns_ZA'), ('oc_FR', 'oc_FR'), ('or_IN', 'or_IN'), ('pa_IN', 'pa_IN'), ('pl_PL', 'pl_PL'), ('ps_AF', 'ps_AF'), ('pt_BR', 'pt_BR'), ('pt_PT', 'pt_PT'), ('qut_GT', 'qut_GT'), ('quz_BO', 'quz_BO'), ('quz_EC', 'quz_EC'), ('quz_PE', 'quz_PE'), ('rm_CH', 'rm_CH'), ('ro_RO', 'ro_RO'), ('ru_RU', 'ru_RU'), ('rw_RW', 'rw_RW'), ('sa_IN', 'sa_IN'), ('sah_RU', 'sah_RU'), ('se_FI', 'se_FI'), ('se_NO', 'se_NO'), ('se_SE', 'se_SE'), ('si_LK', 'si_LK'), ('sk_SK', 'sk_SK'), ('sl_SI', 'sl_SI'), ('sma_NO', 'sma_NO'), ('sma_SE', 'sma_SE'), ('smj_NO', 'smj_NO'), ('smj_SE', 'smj_SE'), ('smn_FI', 'smn_FI'), ('sms_FI', 'sms_FI'), ('sq_AL', 'sq_AL'), ('sr_BA', 'sr_BA'), ('sr_BA', 'sr_BA'), ('sr_SP', 'sr_SP'), ('sr_SP', 'sr_SP'), ('sv_FI', 'sv_FI'), ('sv_SE', 'sv_SE'), ('sw_KE', 'sw_KE'), ('syr_SY', 'syr_SY'), ('ta_IN', 'ta_IN'), ('te_IN', 'te_IN'), ('tg_TJ', 'tg_TJ'), ('th_TH', 'th_TH'), ('tk_TM', 'tk_TM'), ('tmz_DZ', 'tmz_DZ'), ('tn_ZA', 'tn_ZA'), ('tr_TR', 'tr_TR'), ('tt_RU', 'tt_RU'), ('ug_CN', 'ug_CN'), ('uk_UA', 'uk_UA'), ('ur_IN', 'ur_IN'), ('ur_PK', 'ur_PK'), ('uz_UZ', 'uz_UZ'), ('uz_UZ', 'uz_UZ'), ('vi_VN', 'vi_VN'), ('wen_DE', 'wen_DE'), ('wo_SN', 'wo_SN'), ('xh_ZA', 'xh_ZA'), ('yo_NG', 'yo_NG'), ('zh_CHS', 'zh_CHS'), ('zh_CHT', 'zh_CHT'), ('zh_CN', 'zh_CN'), ('zh_HK', 'zh_HK'), ('zh_MO', 'zh_MO'), ('zh_SG', 'zh_SG'), ('zh_TW', 'zh_TW'), ('zu_ZA', 'zu_ZA')], default='fr_FR', max_length=10),
+        ),
+        migrations.AlterField(
+            model_name='language',
+            name='windows_locale_name',
+            field=models.CharField(choices=[('af_ZA', 'af_ZA'), ('am_ET', 'am_ET'), ('ar_AE', 'ar_AE'), ('ar_BH', 'ar_BH'), ('ar_DZ', 'ar_DZ'), ('ar_EG', 'ar_EG'), ('ar_IQ', 'ar_IQ'), ('ar_JO', 'ar_JO'), ('ar_KW', 'ar_KW'), ('ar_LB', 'ar_LB'), ('ar_LY', 'ar_LY'), ('ar_MA', 'ar_MA'), ('ar_OM', 'ar_OM'), ('ar_QA', 'ar_QA'), ('ar_SA', 'ar_SA'), ('ar_SY', 'ar_SY'), ('ar_TN', 'ar_TN'), ('ar_YE', 'ar_YE'), ('arn_CL', 'arn_CL'), ('as_IN', 'as_IN'), ('az_AZ', 'az_AZ'), ('az_AZ', 'az_AZ'), ('ba_RU', 'ba_RU'), ('be_BY', 'be_BY'), ('bg_BG', 'bg_BG'), ('bn_IN', 'bn_IN'), ('bo_BT', 'bo_BT'), ('bo_CN', 'bo_CN'), ('br_FR', 'br_FR'), ('bs_BA', 'bs_BA'), ('bs_BA', 'bs_BA'), ('ca_ES', 'ca_ES'), ('co_FR', 'co_FR'), ('cs_CZ', 'cs_CZ'), ('cy_GB', 'cy_GB'), ('da_DK', 'da_DK'), ('de_AT', 'de_AT'), ('de_CH', 'de_CH'), ('de_DE', 'de_DE'), ('de_LI', 'de_LI'), ('de_LU', 'de_LU'), ('div_MV', 'div_MV'), ('dsb_DE', 'dsb_DE'), ('el_GR', 'el_GR'), ('en_AU', 'en_AU'), ('en_BZ', 'en_BZ'), ('en_CA', 'en_CA'), ('en_CB', 'en_CB'), ('en_GB', 'en_GB'), ('en_IE', 'en_IE'), ('en_IN', 'en_IN'), ('en_IN', 'en_IN'), ('en_JA', 'en_JA'), ('en_MY', 'en_MY'), ('en_NZ', 'en_NZ'), ('en_PH', 'en_PH'), ('en_TT', 'en_TT'), ('en_US', 'en_US'), ('en_ZA', 'en_ZA'), ('en_ZW', 'en_ZW'), ('es_AR', 'es_AR'), ('es_BO', 'es_BO'), ('es_CL', 'es_CL'), ('es_CO', 'es_CO'), ('es_CR', 'es_CR'), ('es_DO', 'es_DO'), ('es_EC', 'es_EC'), ('es_ES', 'es_ES'), ('es_ES', 'es_ES'), ('es_GT', 'es_GT'), ('es_HN', 'es_HN'), ('es_MX', 'es_MX'), ('es_NI', 'es_NI'), ('es_PA', 'es_PA'), ('es_PE', 'es_PE'), ('es_PR', 'es_PR'), ('es_PY', 'es_PY'), ('es_SV', 'es_SV'), ('es_UR', 'es_UR'), ('es_US', 'es_US'), ('es_VE', 'es_VE'), ('et_EE', 'et_EE'), ('eu_ES', 'eu_ES'), ('fa_IR', 'fa_IR'), ('fi_FI', 'fi_FI'), ('fil_PH', 'fil_PH'), ('fo_FO', 'fo_FO'), ('fr_BE', 'fr_BE'), ('fr_CA', 'fr_CA'), ('fr_CH', 'fr_CH'), ('fr_FR', 'fr_FR'), ('fr_LU', 'fr_LU'), ('fr_MC', 'fr_MC'), ('fy_NL', 'fy_NL'), ('ga_IE', 'ga_IE'), ('gbz_AF', 'gbz_AF'), ('gl_ES', 'gl_ES'), ('gsw_FR', 'gsw_FR'), ('gu_IN', 'gu_IN'), ('ha_NG', 'ha_NG'), ('he_IL', 'he_IL'), ('hi_IN', 'hi_IN'), ('hr_BA', 'hr_BA'), ('hr_HR', 'hr_HR'), ('hu_HU', 'hu_HU'), ('hy_AM', 'hy_AM'), ('id_ID', 'id_ID'), ('ii_CN', 'ii_CN'), ('is_IS', 'is_IS'), ('it_CH', 'it_CH'), ('it_IT', 'it_IT'), ('iu_CA', 'iu_CA'), ('iu_CA', 'iu_CA'), ('ja_JP', 'ja_JP'), ('ka_GE', 'ka_GE'), ('kh_KH', 'kh_KH'), ('kk_KZ', 'kk_KZ'), ('kl_GL', 'kl_GL'), ('kn_IN', 'kn_IN'), ('ko_KR', 'ko_KR'), ('kok_IN', 'kok_IN'), ('ky_KG', 'ky_KG'), ('lb_LU', 'lb_LU'), ('lo_LA', 'lo_LA'), ('lt_LT', 'lt_LT'), ('lv_LV', 'lv_LV'), ('mi_NZ', 'mi_NZ'), ('mk_MK', 'mk_MK'), ('ml_IN', 'ml_IN'), ('mn_CN', 'mn_CN'), ('mn_MN', 'mn_MN'), ('moh_CA', 'moh_CA'), ('mr_IN', 'mr_IN'), ('ms_BN', 'ms_BN'), ('ms_MY', 'ms_MY'), ('mt_MT', 'mt_MT'), ('nb_NO', 'nb_NO'), ('ne_NP', 'ne_NP'), ('nl_BE', 'nl_BE'), ('nl_NL', 'nl_NL'), ('nn_NO', 'nn_NO'), ('ns_ZA', 'ns_ZA'), ('oc_FR', 'oc_FR'), ('or_IN', 'or_IN'), ('pa_IN', 'pa_IN'), ('pl_PL', 'pl_PL'), ('ps_AF', 'ps_AF'), ('pt_BR', 'pt_BR'), ('pt_PT', 'pt_PT'), ('qut_GT', 'qut_GT'), ('quz_BO', 'quz_BO'), ('quz_EC', 'quz_EC'), ('quz_PE', 'quz_PE'), ('rm_CH', 'rm_CH'), ('ro_RO', 'ro_RO'), ('ru_RU', 'ru_RU'), ('rw_RW', 'rw_RW'), ('sa_IN', 'sa_IN'), ('sah_RU', 'sah_RU'), ('se_FI', 'se_FI'), ('se_NO', 'se_NO'), ('se_SE', 'se_SE'), ('si_LK', 'si_LK'), ('sk_SK', 'sk_SK'), ('sl_SI', 'sl_SI'), ('sma_NO', 'sma_NO'), ('sma_SE', 'sma_SE'), ('smj_NO', 'smj_NO'), ('smj_SE', 'smj_SE'), ('smn_FI', 'smn_FI'), ('sms_FI', 'sms_FI'), ('sq_AL', 'sq_AL'), ('sr_BA', 'sr_BA'), ('sr_BA', 'sr_BA'), ('sr_SP', 'sr_SP'), ('sr_SP', 'sr_SP'), ('sv_FI', 'sv_FI'), ('sv_SE', 'sv_SE'), ('sw_KE', 'sw_KE'), ('syr_SY', 'syr_SY'), ('ta_IN', 'ta_IN'), ('te_IN', 'te_IN'), ('tg_TJ', 'tg_TJ'), ('th_TH', 'th_TH'), ('tk_TM', 'tk_TM'), ('tmz_DZ', 'tmz_DZ'), ('tn_ZA', 'tn_ZA'), ('tr_TR', 'tr_TR'), ('tt_RU', 'tt_RU'), ('ug_CN', 'ug_CN'), ('uk_UA', 'uk_UA'), ('ur_IN', 'ur_IN'), ('ur_PK', 'ur_PK'), ('uz_UZ', 'uz_UZ'), ('uz_UZ', 'uz_UZ'), ('vi_VN', 'vi_VN'), ('wen_DE', 'wen_DE'), ('wo_SN', 'wo_SN'), ('xh_ZA', 'xh_ZA'), ('yo_NG', 'yo_NG'), ('zh_CHS', 'zh_CHS'), ('zh_CHT', 'zh_CHT'), ('zh_CN', 'zh_CN'), ('zh_HK', 'zh_HK'), ('zh_MO', 'zh_MO'), ('zh_SG', 'zh_SG'), ('zh_TW', 'zh_TW'), ('zu_ZA', 'zu_ZA')], default='French', max_length=10),
+        ),
+        migrations.AlterField(
+            model_name='location',
+            name='color',
+            field=models.CharField(blank=True, default='', max_length=20, verbose_name='Calendar appointment color'),
+        ),
+        migrations.AlterField(
+            model_name='mailtemplate',
+            name='context',
+            field=models.CharField(choices=[('A', 'Appointment'), ('S', 'Subject'), ('V', 'Visit'), ('C', 'Voucher')], max_length=1),
+        ),
+        migrations.AlterField(
+            model_name='mailtemplate',
+            name='template_file',
+            field=models.FileField(upload_to='templates/'),
+        ),
+        migrations.AlterField(
+            model_name='missingsubject',
+            name='ignore',
+            field=models.BooleanField(default=False, verbose_name='Ignore missing subject'),
+        ),
+        migrations.AlterField(
+            model_name='missingsubject',
+            name='redcap_id',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='RED Cap id'),
+        ),
+        migrations.AlterField(
+            model_name='missingsubject',
+            name='redcap_url',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='URL to RED Cap subject'),
+        ),
+        migrations.AlterField(
+            model_name='missingsubject',
+            name='subject',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.StudySubject', verbose_name='Subject'),
+        ),
+        migrations.AlterField(
+            model_name='provenance',
+            name='modification_author',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name='Worker who modified the row'),
+        ),
+        migrations.AlterField(
+            model_name='provenance',
+            name='modification_date',
+            field=models.DateTimeField(auto_now_add=True, verbose_name='Modified on'),
+        ),
+        migrations.AlterField(
+            model_name='provenance',
+            name='modification_description',
+            field=models.CharField(max_length=20480, verbose_name='Description'),
+        ),
+        migrations.AlterField(
+            model_name='provenance',
+            name='modified_field',
+            field=models.CharField(blank='', max_length=1024, verbose_name='Modified field'),
+        ),
+        migrations.AlterField(
+            model_name='provenance',
+            name='new_value',
+            field=models.CharField(blank=True, max_length=2048, null=True, verbose_name='New Value'),
+        ),
+        migrations.AlterField(
+            model_name='provenance',
+            name='previous_value',
+            field=models.CharField(blank=True, max_length=2048, null=True, verbose_name='Previous Value'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='auto_create_follow_up',
+            field=models.BooleanField(default=True, verbose_name='Auto create follow up visit'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='default_delta_time_for_control_follow_up',
+            field=models.IntegerField(default=4, help_text='Time difference between visits used to automatically create follow up visits', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Time difference between control visits'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='default_delta_time_for_follow_up_units',
+            field=models.CharField(choices=[('years', 'Years'), ('days', 'Days')], default='years', help_text='Units for the number of days between visits for both patients and controls', max_length=10, verbose_name='Units for the follow up incrementals'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='default_delta_time_for_patient_follow_up',
+            field=models.IntegerField(default=1, help_text='Time difference between visits used to automatically create follow up visits', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Time difference between patient visits'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='default_visit_duration_in_months',
+            field=models.IntegerField(default=6, help_text='Duration of the visit, this is, the time interval, in months, when the appointments may take place', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Duration of the visits in months'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='default_voucher_expiration_in_months',
+            field=models.IntegerField(default=3, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Duration of the vouchers in months'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='name',
+            field=models.CharField(max_length=255, verbose_name='Name'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='nd_number_study_subject_regex',
+            field=models.CharField(default='^ND\\d{4}$', help_text='Defines the regex to check the ID used for each study subject. Keep in mind that this regex should be valid for all previous study subjects in the database.', max_length=255, verbose_name='Study Subject ND Number Regex'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='redcap_first_visit_number',
+            field=models.IntegerField(default=1, verbose_name='Number of the first visit in redcap system'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='sample_mail_statistics',
+            field=models.BooleanField(default=False, verbose_name='Email with sample collections should use statistics'),
+        ),
+        migrations.AlterField(
+            model_name='study',
+            name='visits_to_show_in_subject_list',
+            field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)], verbose_name='Number of visits to show in the subject list'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='brain_donation_agreement',
+            field=models.BooleanField(default=False, verbose_name='Brain donation agreement'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='comments',
+            field=models.BooleanField(default=True, verbose_name='Comments'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='datetime_contact_reminder',
+            field=models.BooleanField(default=True, verbose_name='Please make a contact on'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='default_location',
+            field=models.BooleanField(default=True, verbose_name='Default appointment location'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='diagnosis',
+            field=models.BooleanField(default=True, verbose_name='Diagnosis'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='endpoint_reached',
+            field=models.BooleanField(default=True, verbose_name='Endpoint reached'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='excluded',
+            field=models.BooleanField(default=False, verbose_name='Excluded'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='flying_team',
+            field=models.BooleanField(default=True, verbose_name='Default flying team location (if applicable)'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='health_partner',
+            field=models.BooleanField(default=False, verbose_name='Health partner'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='health_partner_feedback_agreement',
+            field=models.BooleanField(default=False, verbose_name='Agrees to give information to referral'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='information_sent',
+            field=models.BooleanField(default=True, verbose_name='Information sent'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='mpower_id',
+            field=models.BooleanField(default=True, verbose_name='MPower ID'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='nd_number',
+            field=models.BooleanField(default=True, verbose_name='ND number'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='pd_in_family',
+            field=models.BooleanField(default=True, verbose_name='PD in family'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='postponed',
+            field=models.BooleanField(default=True, verbose_name='Postponed'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='previously_in_study',
+            field=models.BooleanField(default=False, verbose_name='Previously in PDP study'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='referral',
+            field=models.BooleanField(default=True, verbose_name='Referred by'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='referral_letter',
+            field=models.BooleanField(default=False, verbose_name='Referral letter'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='resign_reason',
+            field=models.BooleanField(default=True, verbose_name='Endpoint reached comments'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='resigned',
+            field=models.BooleanField(default=True, verbose_name='Resigned'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='screening',
+            field=models.BooleanField(default=False, verbose_name='Screening'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='screening_number',
+            field=models.BooleanField(default=True, verbose_name='Screening number'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='type',
+            field=models.BooleanField(default=True, verbose_name='Type'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_1',
+            field=models.BooleanField(default=False, verbose_name='Visit 1 virus results'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_1_collection_date',
+            field=models.BooleanField(default=False, verbose_name='Visit 1 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_1_iga_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 1 virus IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_1_igg_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 1 virus IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_1_updated',
+            field=models.BooleanField(default=False, verbose_name='Visit 1 virus results date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_2',
+            field=models.BooleanField(default=False, verbose_name='Visit 2 virus results'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_2_collection_date',
+            field=models.BooleanField(default=False, verbose_name='Visit 2 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_2_iga_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 2 virus IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_2_igg_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 2 virus IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_2_updated',
+            field=models.BooleanField(default=False, verbose_name='Visit 2 virus results date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_3',
+            field=models.BooleanField(default=False, verbose_name='Visit 3 virus results'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_3_collection_date',
+            field=models.BooleanField(default=False, verbose_name='Visit 3 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_3_iga_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 3 virus IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_3_igg_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 3 virus IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_3_updated',
+            field=models.BooleanField(default=False, verbose_name='Visit 3 virus results date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_4',
+            field=models.BooleanField(default=False, verbose_name='Visit 4 virus results'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_4_collection_date',
+            field=models.BooleanField(default=False, verbose_name='Visit 4 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_4_iga_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 4 virus IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_4_igg_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 4 virus IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_4_updated',
+            field=models.BooleanField(default=False, verbose_name='Visit 4 virus results date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_5',
+            field=models.BooleanField(default=False, verbose_name='Visit 5 virus results'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_5_collection_date',
+            field=models.BooleanField(default=False, verbose_name='Visit 5 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_5_iga_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 5 virus IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_5_igg_status',
+            field=models.BooleanField(default=False, verbose_name='Visit 5 virus IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='virus_test_5_updated',
+            field=models.BooleanField(default=False, verbose_name='Visit 5 virus results date'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='voucher_types',
+            field=models.BooleanField(default=False, verbose_name='Voucher types'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='vouchers',
+            field=models.BooleanField(default=False, verbose_name='Vouchers'),
+        ),
+        migrations.AlterField(
+            model_name='studycolumns',
+            name='year_of_diagnosis',
+            field=models.BooleanField(default=True, verbose_name='Year of diagnosis (YYYY)'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='approaching_visits_for_mail_contact_visible',
+            field=models.BooleanField(default=True, verbose_name='post mail for approaching visits'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='approaching_visits_without_appointments_visible',
+            field=models.BooleanField(default=True, verbose_name='approaching visits'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='exceeded_visits_visible',
+            field=models.BooleanField(default=True, verbose_name='exceeded visit time'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='inconsistent_redcap_subject_visible',
+            field=models.BooleanField(default=True, verbose_name='inconsistent RED Cap subject'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='missing_redcap_subject_visible',
+            field=models.BooleanField(default=True, verbose_name='missing RED Cap subject'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='subject_no_visits_visible',
+            field=models.BooleanField(default=True, verbose_name='subject without visit'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='subject_require_contact_visible',
+            field=models.BooleanField(default=True, verbose_name='subject required contact'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='subject_voucher_expiry_visible',
+            field=models.BooleanField(default=False, verbose_name='subject vouchers almost expired'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='unfinished_appointments_visible',
+            field=models.BooleanField(default=True, verbose_name='unfinished appointments'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='unfinished_visits_visible',
+            field=models.BooleanField(default=True, verbose_name='unfinished visits'),
+        ),
+        migrations.AlterField(
+            model_name='studynotificationparameters',
+            name='visits_with_missing_appointments_visible',
+            field=models.BooleanField(default=True, verbose_name='visits with missing appointments'),
+        ),
+        migrations.AlterField(
+            model_name='studyredcapcolumns',
+            name='date_born',
+            field=models.BooleanField(default=True, verbose_name='Date of birth'),
+        ),
+        migrations.AlterField(
+            model_name='studyredcapcolumns',
+            name='dead',
+            field=models.BooleanField(default=True, verbose_name='Dead'),
+        ),
+        migrations.AlterField(
+            model_name='studyredcapcolumns',
+            name='languages',
+            field=models.BooleanField(default=True, verbose_name='Languages'),
+        ),
+        migrations.AlterField(
+            model_name='studyredcapcolumns',
+            name='mpower_id',
+            field=models.BooleanField(default=True, verbose_name='MPower ID'),
+        ),
+        migrations.AlterField(
+            model_name='studyredcapcolumns',
+            name='sex',
+            field=models.BooleanField(default=True, verbose_name='Sex'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='brain_donation_agreement',
+            field=models.BooleanField(default=False, verbose_name='Brain donation agreement'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='date_added',
+            field=models.DateField(auto_now_add=True, verbose_name='Added on'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='datetime_contact_reminder',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Please make a contact on'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='default_location',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Location', verbose_name='Default appointment location'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='diagnosis',
+            field=models.CharField(blank=True, max_length=1024, null=True, verbose_name='Diagnosis'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='endpoint_reached',
+            field=models.BooleanField(default=False, verbose_name='Endpoint Reached'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='endpoint_reached_reason',
+            field=models.TextField(blank=True, max_length=2000, verbose_name='Endpoint reached comments'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='exclude_reason',
+            field=models.TextField(blank=True, max_length=2000, verbose_name='Exclude reason'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='excluded',
+            field=models.BooleanField(default=False, verbose_name='Excluded'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='flying_team',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.FlyingTeam', verbose_name='Default flying team location (if applicable)'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='health_partner',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='web.Worker', verbose_name='Health partner'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='health_partner_feedback_agreement',
+            field=models.BooleanField(default=False, verbose_name='Agrees to give information to referral'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='information_sent',
+            field=models.BooleanField(default=False, verbose_name='Information sent'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='nd_number',
+            field=models.CharField(blank=True, max_length=25, verbose_name='ND number'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='pd_in_family',
+            field=models.NullBooleanField(default=None, verbose_name='PD in family'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='previously_in_study',
+            field=models.BooleanField(default=False, verbose_name='Previously in PDP study'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='referral_letter',
+            field=models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(location='~/tmp/upload'), upload_to='referral_letters', verbose_name='Referral letter'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='resign_reason',
+            field=models.TextField(blank=True, max_length=2000, verbose_name='Resign reason'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='resigned',
+            field=models.BooleanField(default=False, verbose_name='Resigned'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='screening',
+            field=models.CharField(blank=True, max_length=1024, null=True, verbose_name='Screening'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='screening_number',
+            field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Screening number'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='study',
+            field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.Study', verbose_name='Study'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='subject',
+            field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.Subject', verbose_name='Subject'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='type',
+            field=models.CharField(blank=True, choices=[('C', 'CONTROL'), ('P', 'PATIENT')], max_length=1, null=True, verbose_name='Type'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_1',
+            field=models.NullBooleanField(choices=[(True, 'Yes'), (False, 'No'), (None, 'N/A')], default=None, verbose_name='Visit 1 virus result'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_1_collection_date',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 1 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_1_iga_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 1 IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_1_igg_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 1 IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_1_updated',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 1 virus result date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_2',
+            field=models.NullBooleanField(choices=[(True, 'Yes'), (False, 'No'), (None, 'N/A')], default=None, verbose_name='Visit 2 virus result'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_2_collection_date',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 2 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_2_iga_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 2 IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_2_igg_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 2 IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_2_updated',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 2 virus result date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_3',
+            field=models.NullBooleanField(choices=[(True, 'Yes'), (False, 'No'), (None, 'N/A')], default=None, verbose_name='Visit 3 virus result'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_3_collection_date',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 3 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_3_iga_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 3 IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_3_igg_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 3 IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_3_updated',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 3 virus result date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_4',
+            field=models.NullBooleanField(choices=[(True, 'Yes'), (False, 'No'), (None, 'N/A')], default=None, verbose_name='Visit 4 virus result'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_4_collection_date',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 4 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_4_iga_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 4 IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_4_igg_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 4 IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_4_updated',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 4 virus result date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_5',
+            field=models.NullBooleanField(choices=[(True, 'Yes'), (False, 'No'), (None, 'N/A')], default=None, verbose_name='Visit 5 virus result'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_5_collection_date',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 5 virus collection date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_5_iga_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 5 IgA status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_5_igg_status',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Visit 5 IgG status'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='virus_test_5_updated',
+            field=models.DateField(blank=True, null=True, verbose_name='Visit 5 virus result date'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='voucher_types',
+            field=models.ManyToManyField(blank=True, to='web.VoucherType', verbose_name='Voucher types'),
+        ),
+        migrations.AlterField(
+            model_name='studysubject',
+            name='year_of_diagnosis',
+            field=models.IntegerField(blank=True, null=True, verbose_name='Year of diagnosis (YYYY)'),
+        ),
+        migrations.AlterField(
+            model_name='studysubjectlist',
+            name='last_contact_attempt',
+            field=models.BooleanField(default=False, verbose_name='Last contact attempt'),
+        ),
+        migrations.AlterField(
+            model_name='studysubjectlist',
+            name='type',
+            field=models.CharField(blank=True, choices=[('GENERIC', 'Generic subject list'), ('NO_VISIT', 'Subjects without visit'), ('REQUIRE_CONTACT', 'Subjects required contact'), ('VOUCHER_EXPIRY', 'Subject with vouchers to be expired soon')], max_length=50, null=True, verbose_name='Type of list'),
+        ),
+        migrations.AlterField(
+            model_name='studysubjectlist',
+            name='visits',
+            field=models.BooleanField(default=True, verbose_name='Visits summary'),
+        ),
+        migrations.AlterField(
+            model_name='studyvisitlist',
+            name='type',
+            field=models.CharField(choices=[('GENERIC', 'Generic visit list'), ('EXCEEDED_TIME', 'Exceeded visit time'), ('UNFINISHED', 'Unfinished visits'), ('MISSING_APPOINTMENTS', 'Visits with missing appointments'), ('APPROACHING_WITHOUT_APPOINTMENTS', 'Approaching visits'), ('APPROACHING_FOR_MAIL_CONTACT', 'Post mail for approaching visits')], max_length=50, verbose_name='Type of list'),
+        ),
+        migrations.AlterField(
+            model_name='studyvisitlist',
+            name='visible_appointment_types_done',
+            field=models.BooleanField(default=False, verbose_name='Done appointments'),
+        ),
+        migrations.AlterField(
+            model_name='studyvisitlist',
+            name='visible_appointment_types_in_progress',
+            field=models.BooleanField(default=False, verbose_name='Appointments in progress'),
+        ),
+        migrations.AlterField(
+            model_name='studyvisitlist',
+            name='visible_appointment_types_missing',
+            field=models.BooleanField(default=False, verbose_name='Missing appointments'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='address',
+            field=models.CharField(blank=True, max_length=255, verbose_name='Address'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='city',
+            field=models.CharField(blank=True, max_length=50, verbose_name='City'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='country',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='web.Country', verbose_name='Country'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='date_born',
+            field=models.DateField(blank=True, null=True, verbose_name='Date of birth (YYYY-MM-DD)'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='dead',
+            field=models.BooleanField(default=False, verbose_name='Deceased'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='default_written_communication_language',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subjects_written_communication', to='web.Language', verbose_name='Default language for document generation'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='email',
+            field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='first_name',
+            field=models.CharField(max_length=50, verbose_name='First name'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='languages',
+            field=models.ManyToManyField(blank=True, to='web.Language', verbose_name='Known languages'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='last_name',
+            field=models.CharField(max_length=50, verbose_name='Last name'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='next_of_keen_address',
+            field=models.TextField(blank=True, max_length=2000, verbose_name='Next of keen address'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='next_of_keen_name',
+            field=models.CharField(blank=True, max_length=255, verbose_name='Next of keen'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='next_of_keen_phone',
+            field=models.CharField(blank=True, max_length=50, verbose_name='Next of keen phone'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='phone_number',
+            field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Phone number'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='phone_number_2',
+            field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Phone number 2'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='phone_number_3',
+            field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Phone number 3'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='postal_code',
+            field=models.CharField(blank=True, max_length=7, verbose_name='Postal code'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='sex',
+            field=models.CharField(choices=[('M', 'Male'), ('F', 'Female')], max_length=1, verbose_name='Sex'),
+        ),
+        migrations.AlterField(
+            model_name='subject',
+            name='social_security_number',
+            field=models.CharField(blank=True, max_length=50, verbose_name='Social security number'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='address',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Address'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='city',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='City'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='country',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Country'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='date_born',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Date of birth'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='dead',
+            field=models.BooleanField(default=True, max_length=1, verbose_name='Deceased'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='default_written_communication_language',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Default language for document generation'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='email',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='E-mail'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='first_name',
+            field=models.BooleanField(default=True, max_length=1, verbose_name='First name'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='languages',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Known languages'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='last_name',
+            field=models.BooleanField(default=True, max_length=1, verbose_name='Last name'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='next_of_keen_address',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Next of kin address'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='next_of_keen_name',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Next of kin'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='next_of_keen_phone',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Next of kin phone'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='phone_number',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Phone number'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='phone_number_2',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Phone number 2'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='phone_number_3',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Phone number 3'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='postal_code',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Postal code'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='sex',
+            field=models.BooleanField(default=False, max_length=1, verbose_name='Sex'),
+        ),
+        migrations.AlterField(
+            model_name='subjectcolumns',
+            name='social_security_number',
+            field=models.BooleanField(default=False, verbose_name='Social security_number'),
+        ),
+        migrations.AlterField(
+            model_name='visit',
+            name='visit_number',
+            field=models.IntegerField(default=1, verbose_name='Visit number'),
+        ),
+        migrations.AlterField(
+            model_name='visitcolumns',
+            name='datetime_begin',
+            field=models.BooleanField(choices=[(True, 'Yes'), (False, 'No')], default=True, verbose_name='Visit starts date'),
+        ),
+        migrations.AlterField(
+            model_name='visitcolumns',
+            name='datetime_end',
+            field=models.BooleanField(choices=[(True, 'Yes'), (False, 'No')], default=True, verbose_name='Visit ends date'),
+        ),
+        migrations.AlterField(
+            model_name='visitcolumns',
+            name='is_finished',
+            field=models.BooleanField(choices=[(True, 'Yes'), (False, 'No')], default=True, verbose_name='Is finished'),
+        ),
+        migrations.AlterField(
+            model_name='visitcolumns',
+            name='post_mail_sent',
+            field=models.BooleanField(choices=[(True, 'Yes'), (False, 'No')], default=True, verbose_name='Post mail sent'),
+        ),
+        migrations.AlterField(
+            model_name='visitcolumns',
+            name='visible_appointment_types',
+            field=models.BooleanField(default=False, verbose_name='All appointments'),
+        ),
+        migrations.AlterField(
+            model_name='visitcolumns',
+            name='visit_number',
+            field=models.BooleanField(choices=[(True, 'Yes'), (False, 'No')], default=True, verbose_name='Visit number'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='activity_type',
+            field=models.CharField(blank=True, default='', max_length=40, verbose_name='Activity type'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='expiry_date',
+            field=models.DateField(verbose_name='Expiry date'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='feedback',
+            field=models.TextField(blank=True, max_length=2000, verbose_name='Feedback'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='hours',
+            field=models.IntegerField(default=0, verbose_name='Hours'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='issue_date',
+            field=models.DateField(verbose_name='Issue date'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='issue_worker',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issued_vouchers', to='web.Worker', verbose_name='Issued by'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='number',
+            field=models.CharField(max_length=50, unique=True, verbose_name='Number'),
+        ),
+        migrations.AlterField(
+            model_name='voucher',
+            name='status',
+            field=models.CharField(choices=[('NEW', 'New'), ('IN_USE', 'In use'), ('USED', 'Used'), ('EXPIRED', 'Expired'), ('REMOVED', 'Removed')], default='NEW', max_length=20, verbose_name='Status'),
+        ),
+        migrations.AlterField(
+            model_name='voucherpartnersession',
+            name='date',
+            field=models.DateTimeField(verbose_name='Issue date'),
+        ),
+        migrations.AlterField(
+            model_name='voucherpartnersession',
+            name='length',
+            field=models.IntegerField(verbose_name='Length (minutes)'),
+        ),
+        migrations.AlterField(
+            model_name='vouchertype',
+            name='code',
+            field=models.CharField(max_length=20, verbose_name='Code'),
+        ),
+        migrations.AlterField(
+            model_name='vouchertype',
+            name='description',
+            field=models.CharField(blank=True, max_length=1024, verbose_name='Description'),
+        ),
+        migrations.AlterField(
+            model_name='vouchertypeprice',
+            name='end_date',
+            field=models.DateField(verbose_name='End date'),
+        ),
+        migrations.AlterField(
+            model_name='vouchertypeprice',
+            name='price',
+            field=models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Price'),
+        ),
+        migrations.AlterField(
+            model_name='vouchertypeprice',
+            name='start_date',
+            field=models.DateField(verbose_name='Start date'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='address',
+            field=models.CharField(blank=True, max_length=255, verbose_name='Address'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='city',
+            field=models.CharField(blank=True, max_length=50, verbose_name='City'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='comment',
+            field=models.TextField(blank=True, max_length=1024, null=True, verbose_name='Comment'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='country',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='web.Country', verbose_name='Country'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='email',
+            field=models.EmailField(blank=True, max_length=254, verbose_name='E-mail'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='fax_number',
+            field=models.CharField(blank=True, max_length=20, verbose_name='Fax number'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='first_name',
+            field=models.CharField(blank=True, max_length=50, verbose_name='First name'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='languages',
+            field=models.ManyToManyField(blank=True, to='web.Language', verbose_name='Known languages'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='last_name',
+            field=models.CharField(blank=True, max_length=50, verbose_name='Last name'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='locations',
+            field=models.ManyToManyField(blank=True, to='web.Location', verbose_name='Locations'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='name',
+            field=models.CharField(blank=True, default='', max_length=50, verbose_name='Name'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='phone_number',
+            field=models.CharField(blank=True, max_length=20, verbose_name='Phone number'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='phone_number_2',
+            field=models.CharField(blank=True, max_length=20, verbose_name='Phone number 2'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='postal_code',
+            field=models.CharField(blank=True, max_length=7, verbose_name='Postal code'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='specialization',
+            field=models.CharField(blank=True, max_length=20, verbose_name='Specialization'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='unit',
+            field=models.CharField(blank=True, max_length=50, verbose_name='Unit'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='voucher_partner_code',
+            field=models.CharField(blank=True, max_length=10, verbose_name='Code'),
+        ),
+        migrations.AlterField(
+            model_name='worker',
+            name='voucher_types',
+            field=models.ManyToManyField(blank=True, to='web.VoucherType', verbose_name='Voucher types'),
+        ),
+        migrations.AlterField(
+            model_name='workerstudyrole',
+            name='name',
+            field=models.CharField(choices=[('DOCTOR', 'Doctor'), ('NURSE', 'Nurse'), ('PSYCHOLOGIST', 'Psychologist'), ('TECHNICIAN', 'Technician'), ('SECRETARY', 'Secretary'), ('PROJECT MANAGER', 'Project Manager')], max_length=20, verbose_name='Role'),
+        ),
+        migrations.AlterField(
+            model_name='workerstudyrole',
+            name='permissions',
+            field=models.ManyToManyField(blank=True, to='auth.Permission', verbose_name='Worker Study Permissions'),
+        ),
+    ]
diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py
index f061f3061fe03099a3355dd489092b1cb96f3fcf..34f44a4886090fcfb416ce73e20e7ab3ecb06970 100644
--- a/smash/web/models/constants.py
+++ b/smash/web/models/constants.py
@@ -2,6 +2,7 @@
 import locale
 
 from django.core.files.storage import FileSystemStorage
+from django.conf import settings
 
 BOOL_CHOICES = ((True, 'Yes'), (False, 'No'))
 SEX_CHOICES_MALE = 'M'
@@ -154,4 +155,21 @@ VOUCHER_STATUS_CHOICES = (
     (VOUCHER_STATUS_REMOVED, 'Removed'),
 )
 
-FILE_STORAGE = FileSystemStorage(location='uploads')
+FILE_STORAGE = FileSystemStorage(location=settings.UPLOAD_ROOT)
+
+CUSTOM_FIELD_TYPE_TEXT = "TEXT"
+CUSTOM_FIELD_TYPE_BOOLEAN = "BOOL"
+CUSTOM_FIELD_TYPE_INTEGER = "INTEGER"
+CUSTOM_FIELD_TYPE_DOUBLE = "DOUBLE"
+CUSTOM_FIELD_TYPE_DATE = "DATE"
+CUSTOM_FIELD_TYPE_SELECT_LIST = "SELECT_LIST"
+CUSTOM_FIELD_TYPE_FILE = "FILE"
+CUSTOM_FIELD_TYPE = (
+    (CUSTOM_FIELD_TYPE_TEXT, "Text"),
+    (CUSTOM_FIELD_TYPE_BOOLEAN, "Boolean (True/False)"),
+    (CUSTOM_FIELD_TYPE_INTEGER, "Integer"),
+    (CUSTOM_FIELD_TYPE_DOUBLE, "Double (real number)"),
+    (CUSTOM_FIELD_TYPE_DATE, "Date"),
+    (CUSTOM_FIELD_TYPE_SELECT_LIST, "Select list"),
+    (CUSTOM_FIELD_TYPE_FILE, "File"),
+)
diff --git a/smash/web/models/custom_data/__init__.py b/smash/web/models/custom_data/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9587ebe1ac196cca3e76610cce3e02bcaff672c
--- /dev/null
+++ b/smash/web/models/custom_data/__init__.py
@@ -0,0 +1,4 @@
+from .custom_study_subject_field import CustomStudySubjectField
+from .custom_study_subject_value import CustomStudySubjectValue
+
+__all__ = [CustomStudySubjectField, CustomStudySubjectValue]
diff --git a/smash/web/models/custom_data/custom_study_subject_field.py b/smash/web/models/custom_data/custom_study_subject_field.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e096da35e40804ca12975a303bfe153cc7e15e2
--- /dev/null
+++ b/smash/web/models/custom_data/custom_study_subject_field.py
@@ -0,0 +1,30 @@
+# coding=utf-8
+
+from django.db import models
+
+from web.models.constants import CUSTOM_FIELD_TYPE
+
+
+class CustomStudySubjectField(models.Model):
+    name = models.CharField(max_length=20, null=False, blank=False)
+    type = models.CharField(max_length=20, choices=CUSTOM_FIELD_TYPE, null=False, blank=False)
+
+    possible_values = models.CharField(max_length=1024, null=True, blank=True, default='')
+
+    default_value = models.CharField(max_length=20, null=True, blank=True)
+    readonly = models.BooleanField(default=False)
+
+    required = models.BooleanField(default=False)
+
+    unique = models.BooleanField(default=False)
+
+    study = models.ForeignKey("web.Study",
+                              verbose_name='Study',
+                              editable=False,
+                              null=False,
+                              on_delete=models.CASCADE
+                              )
+
+
+def get_study_subject_field_id(study_subject_field: CustomStudySubjectField) -> str:
+    return "custom_field-" + str(study_subject_field.id)
diff --git a/smash/web/models/custom_data/custom_study_subject_value.py b/smash/web/models/custom_data/custom_study_subject_value.py
new file mode 100644
index 0000000000000000000000000000000000000000..6398dfe615cdbcb1e5d07ae20a179f5348e46830
--- /dev/null
+++ b/smash/web/models/custom_data/custom_study_subject_value.py
@@ -0,0 +1,20 @@
+# coding=utf-8
+
+from django.db import models
+
+
+class CustomStudySubjectValue(models.Model):
+    value = models.CharField(max_length=2048, null=True, blank=True)
+
+    study_subject_field = models.ForeignKey("web.CustomStudySubjectField",
+                                            verbose_name='Custom Field',
+                                            editable=False,
+                                            null=False,
+                                            on_delete=models.CASCADE
+                                            )
+    study_subject = models.ForeignKey("web.StudySubject",
+                                      verbose_name='Study',
+                                      editable=False,
+                                      null=False,
+                                      on_delete=models.CASCADE
+                                      )
diff --git a/smash/web/models/study.py b/smash/web/models/study.py
index 05e8706d90e9bd7c8a54ceb81cba1d576bdfadbe..f61834f6aacb44a17ec824b47ca7a0a672991630 100644
--- a/smash/web/models/study.py
+++ b/smash/web/models/study.py
@@ -1,10 +1,10 @@
 # coding=utf-8
-from django.db import models
+import re
 
-from web.models import StudyColumns, StudyNotificationParameters, StudyRedCapColumns
 from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
 
-import re
+from web.models import StudyColumns, StudyNotificationParameters, StudyRedCapColumns
 
 FOLLOW_UP_INCREMENT_IN_YEARS = 'years'
 FOLLOW_UP_INCREMENT_IN_DAYS = 'days'
@@ -13,8 +13,8 @@ FOLLOW_UP_INCREMENT_UNIT_CHOICE = {
     FOLLOW_UP_INCREMENT_IN_DAYS: 'Days'
 }
 
-class Study(models.Model):
 
+class Study(models.Model):
     class Meta:
         app_label = 'web'
 
@@ -87,12 +87,12 @@ class Study(models.Model):
     )
 
     default_delta_time_for_follow_up_units = models.CharField(max_length=10,
-        choices=list(FOLLOW_UP_INCREMENT_UNIT_CHOICE.items()),
-        verbose_name='Units for the follow up incrementals',
-        help_text='Units for the number of days between visits for both patients and controls',
-        default=FOLLOW_UP_INCREMENT_IN_YEARS,
-        blank=False
-    )
+                                                              choices=list(FOLLOW_UP_INCREMENT_UNIT_CHOICE.items()),
+                                                              verbose_name='Units for the follow up incrementals',
+                                                              help_text='Units for the number of days between visits for both patients and controls',
+                                                              default=FOLLOW_UP_INCREMENT_IN_YEARS,
+                                                              blank=False
+                                                              )
 
     study_privacy_notice = models.ForeignKey("web.PrivacyNotice",
                               verbose_name='Study Privacy Note',
diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py
index 95cf4c5e8208253695e2a43e60f7bb8f48266640..3a438d81f07cbcc439dc591c2b938ca054ed907d 100644
--- a/smash/web/models/study_subject.py
+++ b/smash/web/models/study_subject.py
@@ -2,12 +2,14 @@
 import logging
 import re
 
+from typing import Optional
 from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 
 from web.models import VoucherType, Appointment, Location, Visit, Provenance
 from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE, BOOL_CHOICES_WITH_NONE
+from web.models.custom_data import CustomStudySubjectValue, CustomStudySubjectField
 
 logger = logging.getLogger(__name__)
 
@@ -369,9 +371,38 @@ class StudySubject(models.Model):
         else:
             return 'Normal'
 
+    @property
+    def custom_data_values(self):
+        values = CustomStudySubjectValue.objects.filter(study_subject=self)
+        fields = list(CustomStudySubjectField.objects.filter(study=self.study))
+        for value in values:
+            fields.remove(value.study_subject_field)
+        for field in fields:
+            CustomStudySubjectValue.objects.create(study_subject=self, value=field.default_value,
+                                                   study_subject_field=field)
+        return CustomStudySubjectValue.objects.filter(study_subject=self)
+
+    def set_custom_data_value(self, custom_study_subject_field: CustomStudySubjectField, value: str):
+        found = False
+        for existing_value in self.customstudysubjectvalue_set.all():
+            if existing_value.study_subject_field == custom_study_subject_field:
+                found = True
+                existing_value.value = value
+                existing_value.save()
+        if not found:
+            self.customstudysubjectvalue_set.add(
+                CustomStudySubjectValue.objects.create(study_subject=self, value=value,
+                                                       study_subject_field=custom_study_subject_field))
+
     def __str__(self):
         return "%s %s" % (self.subject.first_name, self.subject.last_name)
 
+    def get_custom_data_value(self, custom_field) -> Optional[CustomStudySubjectValue]:
+        for value in self.custom_data_values:
+            if value.study_subject_field == custom_field:
+                return value
+        return None
+
 
 # SIGNALS
 @receiver(post_save, sender=StudySubject)
diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js
index 4737d47311ea8a2fadf9346de3fb557ec7ceeaeb..a7a3dcba9c3c820b7337fd3d5ec3451fdf77d8a7 100644
--- a/smash/web/static/js/smash.js
+++ b/smash/web/static/js/smash.js
@@ -105,16 +105,16 @@ function createColumn(dataType, name, filter, visible, sortable, renderFunction)
 We use an auxiliary function to create the function due to the lack of block scope in JS.
 https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example
 */
-function createRenderFunction(columnRow){
-    return function(data, type, row, meta){
+function createRenderFunction(columnRow) {
+    return function (data, type, row, meta) {
         /*
         Fancy highlighting for the column matches.
         */
-        if(columnRow.filter == 'string_filter'){
+        if (columnRow.filter == 'string_filter') {
             //obtain the input element by its placeholdername which matches the column name: e.g.: <input type="text" style="width:80px" placeholder="First name">
             filter_value = $(`input[placeholder="${columnRow.name}"]`).val();
             //if there is any filter, we highlight the matching part
-            if(filter_value != undefined && filter_value.length > 0){
+            if (filter_value != undefined && filter_value.length > 0) {
                 //global and case insensitive replacement
                 return data.replace(RegExp(filter_value, 'gi'), `<span class="highlight_match">${filter_value}</span>`);
             }
@@ -128,7 +128,7 @@ function getColumns(columns, getSubjectEditUrl) {
     var result = [];
     for (var i = 0; i < columns.length; i++) {
         var columnRow = columns[i];
-        if (columnRow.type === "address"){
+        if (columnRow.type === "address") {
 
             var renderFunction = (function () {
                 return function (data, type, row) {
@@ -186,7 +186,17 @@ function createFilter(columnsDefinition) {
         var column = columnsDefinition[i];
         var element = document.createElement("th");
         if (column.filter !== null) {
-            element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>";
+            if (column.filter.indexOf('select_filter:') === 0) {
+                var options = column.filter.replace('select_filter:', '').split(";");
+                var content = '<select style="width:60px" ><option value selected="selected">---</option>';
+                for (var j = 0; j < options.length; j++) {
+                    content += '<option value="' + options[j] + '">' + options[j] + '</option>';
+                }
+                content += '</select>';
+                element.innerHTML = content;
+            } else {
+                element.innerHTML = "<div name='" + column.filter + "'>" + column.name + "</div>";
+            }
         }
         footerRow.appendChild(element);
     }
@@ -222,9 +232,9 @@ function create_visit_row(visit) {
             text = `<span title="Appointments are taking place.">IN PROGRESS</span>`;
 
         }
-        
+
         var start_date = moment(visit.datetime_start);
-        var end_date   = moment(visit.datetime_end);
+        var end_date = moment(visit.datetime_end);
 
         text += ` <br/>
             <span data-html="true" title="From: ${start_date.format('ddd Do MMMM YYYY')} </br> To: ${end_date.format('ddd Do MMMM YYYY')}">
@@ -234,14 +244,14 @@ function create_visit_row(visit) {
             <span data-html="true" title="From: ${start_date.format('ddd Do MMMM YYYY')} </br> To: ${end_date.format('ddd Do MMMM YYYY')}">
             To: ${end_date.format('D MMM. YYYY')}
             </span>`
- 
+
         text += `<br/><span data-html="true" title="Visit details<br/>Appointment Types:<br/><div class='appointment_type_list'>${visit.appointment_types.join('<br/>')}</div>">
                     <a href="${visit.edit_visit_url}"><i class="fa fa-list" aria-hidden="true"></i></a>
                 </span>`;
 
-        if(!visit.is_finished){
+        if (!visit.is_finished) {
             text += `<span title="Add new appointment to visit"><a href="${visit.add_appointment_url}"><i class="fa fa-plus-square-o" aria-hidden="true"></i></a></span>`;
-        }else{
+        } else {
             text += `<span title="Visit is marked as finished" ><i class="fa fa-check-circle" aria-hidden="true"></i></span>`;
         }
 
@@ -317,7 +327,7 @@ function createTable(params) {
     });
 
     //replace the hyphen with non breaking space hyphen to ensure the column names are more readable
-    $(tableElement).find('th:contains("RT-PCR")').each(function(){
+    $(tableElement).find('th:contains("RT-PCR")').each(function () {
         non_breaking_hyphen = $.parseHTML('&#8209;');
         var text = $(this).text().replace('-', $(non_breaking_hyphen).text());
         $(this).text(text);
@@ -464,17 +474,22 @@ function createTable(params) {
             columnDefs: columnDefs,
             order: [[0, 'desc']],
             buttons: [
-                { extend: 'excel', 
-                  exportOptions: {
-                    columns: ':visible',
-                    modifier: { page: 'all', search: 'applied', order: 'applied' }
-                  },
-                  text: `<span class='excel-export-button'><i class="fa fa-file-excel-o" aria-hidden="true"></i> Export selection to excel</span>`
+                {
+                    extend: 'excel',
+                    exportOptions: {
+                        columns: ':visible',
+                        modifier: {page: 'all', search: 'applied', order: 'applied'}
+                    },
+                    text: `<span class='excel-export-button'><i class="fa fa-file-excel-o" aria-hidden="true"></i> Export selection to excel</span>`
                 }
             ],
             dom: dom_settings, //see docs: https://datatables.net/reference/option/dom
-            initComplete: function(settings, json) {
-                $('.excel-export-button').parents('button').tooltip({container: 'body', title: `Show <b>All</b> entries to consider all rows instead of current page. Filters and sorting will prevail.`, html: true});
+            initComplete: function (settings, json) {
+                $('.excel-export-button').parents('button').tooltip({
+                    container: 'body',
+                    title: `Show <b>All</b> entries to consider all rows instead of current page. Filters and sorting will prevail.`,
+                    html: true
+                });
             }
         });
 
@@ -502,7 +517,7 @@ function createTable(params) {
         var column = table.column($(this).attr('data-column'));
         // Toggle the visibility
         column.visible(visible);
-    }); 
+    });
 }
 
 //------------------------------------------------------
diff --git a/smash/web/templates/custom_study_subject_field/add.html b/smash/web/templates/custom_study_subject_field/add.html
new file mode 100644
index 0000000000000000000000000000000000000000..b61d7bffecd820ac8c78d7754b94445c80798c9a
--- /dev/null
+++ b/smash/web/templates/custom_study_subject_field/add.html
@@ -0,0 +1,9 @@
+{% extends "custom_study_subject_field/add_edit.html" %}
+
+{% block page_header %}Add custom study subject field{% endblock page_header %}
+
+{% block title %}{{ block.super }} - Add custom study subject field{% endblock %}
+
+{% block form-title %}Enter custom study subject field details{% endblock %}
+
+{% block save-button %}Add{% endblock %}
diff --git a/smash/web/templates/custom_study_subject_field/add_edit.html b/smash/web/templates/custom_study_subject_field/add_edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..c7d6fafaa29d65a9377c33362fdf9774ff46a2f8
--- /dev/null
+++ b/smash/web/templates/custom_study_subject_field/add_edit.html
@@ -0,0 +1,105 @@
+{% extends "_base.html" %}
+{% load static %}
+{% load filters %}
+
+{% block styles %}
+    {{ block.super }}
+    <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/>
+
+    {% include "includes/datepicker.css.html" %}
+{% endblock styles %}
+
+{% block ui_active_tab %}'configuration'{% endblock ui_active_tab %}
+
+{% block page_description %}{% endblock page_description %}
+
+{% block breadcrumb %}
+    {% include "study/breadcrumb.html" %}
+{% endblock breadcrumb %}
+
+{% block maincontent %}
+
+    {% block content %}
+        <div class="row">
+            <div class="col-md-12">
+                <div class="box box-success">
+                    <div class="box-header with-border">
+                        <h3 class="box-title">{% block form-title %}Enter custom study subject field
+                            details{% endblock %}</h3>
+                    </div>
+
+
+                    <form method="post" action="" class="form-horizontal">
+                        {% csrf_token %}
+
+                        <div class="box-body">
+                            {% for field in form %}
+                                <div class="form-group {% if field.errors %}has-error{% endif %}">
+                                    <label class="col-sm-4  col-lg-offset-1 col-lg-2 control-label">
+                                        {{ field.label }}
+                                    </label>
+
+                                    <div class="col-sm-8 col-lg-4">
+                                        {{ field|add_class:'form-control' }}
+                                    </div>
+
+                                    {% if field.errors %}
+                                        <span class="help-block">
+  							                {{ field.errors }}
+  						                </span>
+                                    {% endif %}
+                                </div>
+                            {% endfor %}
+                        </div><!-- /.box-body -->
+                        <div class="box-footer">
+                            <div class="col-sm-6">
+                                <button type="submit" class="btn btn-block btn-success">{% block save-button %}
+                                    Add{% endblock %}
+                                </button>
+                            </div>
+                            <div class="col-sm-6">
+                                <a href="{% url 'web.views.edit_study' study_id %}"
+                                   class="btn btn-block btn-default">Cancel</a>
+                            </div>
+                        </div><!-- /.box-footer -->
+                    </form>
+                </div>
+
+            </div>
+        </div>
+
+    {% endblock %}
+
+
+{% endblock maincontent %}
+
+{% block scripts %}
+    {{ block.super }}
+
+    <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script>
+
+    {% include "includes/datetimepicker.js.html" %}
+
+    <script>
+        var hide_fields = function () {
+            if ($('#id_type').val() === 'SELECT_LIST') {
+                $('#id_possible_values').parent().parent().show();
+            } else {
+                $('#id_possible_values').parent().parent().hide();
+            }
+            if ($('#id_type').val() === 'FILE') {
+                $('#id_default_value').parent().parent().hide();
+                $('#id_unique').parent().parent().hide();
+            } else {
+                $('#id_default_value').parent().parent().show();
+                $('#id_unique').parent().parent().show();
+            }
+        }
+        hide_fields();
+        $('#id_type').on('change', function () {
+            hide_fields();
+        });
+
+    </script>
+
+{% endblock scripts %}
diff --git a/smash/web/templates/custom_study_subject_field/edit.html b/smash/web/templates/custom_study_subject_field/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..653879e55187cab20a865c8ad1a562b5f3200241
--- /dev/null
+++ b/smash/web/templates/custom_study_subject_field/edit.html
@@ -0,0 +1,9 @@
+{% extends "custom_study_subject_field/add_edit.html" %}
+
+{% block page_header %}Edit custom study subject field{% endblock page_header %}
+
+{% block title %}{{ block.super }} - Edit custom study subject field{% endblock %}
+
+{% block form-title %}Enter custom study subject field details{% endblock %}
+
+{% block save-button %}Save{% endblock %}
diff --git a/smash/web/templates/export/index.html b/smash/web/templates/export/index.html
index 4e99d70b88409d4c55f35182c106db6c0c3f3cc9..114714cb2c09b785129ceaed0d7a1c34a6ad6a37 100644
--- a/smash/web/templates/export/index.html
+++ b/smash/web/templates/export/index.html
@@ -70,10 +70,10 @@
             </ul>
         </div>
         <ul>
-            <li><a onclick="addFields(this, 'subject_fields')" href="{% url 'web.views.export_to_excel' 'subjects' %}"><i class="fa fa-file-excel-o"></i> XLS -
+            <li><a onclick="addFields(this, 'subject_fields')" href="{% url 'web.views.export_to_excel' study_id 'subjects' %}"><i class="fa fa-file-excel-o"></i> XLS -
                 Excel</a>
             </li>
-            <li><a onclick="addFields(this, 'subject_fields')" href="{% url 'web.views.export_to_csv' 'subjects' %}"><i class="fa fa-file-text-o"></i> CSV -
+            <li><a onclick="addFields(this, 'subject_fields')" href="{% url 'web.views.export_to_csv' study_id 'subjects' %}"><i class="fa fa-file-text-o"></i> CSV -
                 Text based</a></li>
         </ul>
         <h3>Appointments</h3>
@@ -89,9 +89,9 @@
             </ul>
         </div>
         <ul>
-            <li><a onclick="addFields(this, 'appointment_fields')" href="{% url 'web.views.export_to_excel' 'appointments' %}"><i class="fa fa-file-excel-o"></i> XLS -
+            <li><a onclick="addFields(this, 'appointment_fields')" href="{% url 'web.views.export_to_excel' study_id 'appointments' %}"><i class="fa fa-file-excel-o"></i> XLS -
                 Excel</a></li>
-            <li><a onclick="addFields(this, 'appointment_fields')" href="{% url 'web.views.export_to_csv' 'appointments' %}"><i class="fa fa-file-text-o"></i> CSV -
+            <li><a onclick="addFields(this, 'appointment_fields')" href="{% url 'web.views.export_to_csv' study_id 'appointments' %}"><i class="fa fa-file-text-o"></i> CSV -
                 Text based</a></li>
         </ul>
 
diff --git a/smash/web/templates/includes/custom_study_subject_field_box.html b/smash/web/templates/includes/custom_study_subject_field_box.html
new file mode 100644
index 0000000000000000000000000000000000000000..23e0c461a4eaaaaa706035498bb2b5f494b8350d
--- /dev/null
+++ b/smash/web/templates/includes/custom_study_subject_field_box.html
@@ -0,0 +1,52 @@
+<div class="row">
+    <div class="col-lg-12">
+        <div class="box box-success">
+            <div class="box-header with-border">
+                <h3>Custom study subject fields <a title="add a new field"
+                                                   id="add-study-subject-field"
+                                                   href="{% url 'web.views.custom_study_subject_field_add' study.id %}"
+                                                   class="text-primary"
+                ><i class="fa fa-plus-circle text-success"></i></a></h3>
+            </div>
+            <div class="box-body">
+                <table class="table table-bordered table-striped">
+                    <thead>
+                    <tr>
+                        <th class="text-center">Name</th>
+                        <th class="text-center">Type</th>
+                        <th class="text-center">Default value</th>
+                        <th class="text-center">Readonly</th>
+                        <th class="text-center">Unique</th>
+                        <th class="text-center">Obligatory</th>
+                        <th class="text-center">Edit</th>
+                        <th class="text-center">Remove</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    {% for field in fields %}
+                        <tr>
+                            <td class="text-center">{{ field.name }}</td>
+                            <td class="text-center">{{ field.type }}</td>
+                            <td class="text-center">{{ field.default_value }}</td>
+                            <td class="text-center">{% if field.readonly %}YES{% else %}NO{% endif %}</td>
+                            <td class="text-center">{% if field.unique %}YES{% else %}NO{% endif %}</td>
+                            <td class="text-center">{% if field.obligatory %}YES{% else %}NO{% endif %}</td>
+                            <td class="text-center"><a title="edit field"
+                                                       href="{% url 'web.views.custom_study_subject_field_edit' field.study.id field.id %}"
+                                                       type="button"
+                                                       class="btn btn-block btn-default"
+                            >EDIT</a></td>
+                            <td class="text-center"><a title="remove field"
+                                                       href="{% url 'web.views.custom_study_subject_field_delete' field.study.id field.id %}"
+                                                       type="button"
+                                                       class="btn btn-block btn-danger"
+                            >REMOVE</a></td>
+                        </tr>
+                    {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+
+    </div>
+</div>
diff --git a/smash/web/templates/sidebar.html b/smash/web/templates/sidebar.html
index a575319957dc7bab0a8d2cf03b4204c9796d53b3..bea2f85b80f6a78b75564abe3becd7a04f0a7427 100644
--- a/smash/web/templates/sidebar.html
+++ b/smash/web/templates/sidebar.html
@@ -26,12 +26,12 @@
     {% endif %}
 
     {% if "change_worker" in permissions %}
-    <li data-desc="workers">
-        <a href="{% url 'web.views.workers' %}">
-            <i class="fa fa-user-md"></i>
-            <span>Worker</span>
-        </a>
-    </li>
+        <li data-desc="workers">
+            <a href="{% url 'web.views.workers' %}">
+                <i class="fa fa-user-md"></i>
+                <span>Worker</span>
+            </a>
+        </li>
     {% endif %}
 
     {% if equipment_perms %}
@@ -85,57 +85,59 @@
 
     {% if  "export_subjects" in permissions %}
         <li data-desc="export">
-            <a href="{% url 'web.views.export' %}">
+            <a href="{% url 'web.views.export' study_id %}">
                 <i class="fa fa-file-excel-o"></i>
                 <span>Export</span>
             </a>
         </li>
     {% endif %}
 
-    {% if study.has_vouchers and "change_voucher" in permissions%}
-    <li data-desc="vouchers">
-        <a href="{% url 'web.views.vouchers' %}">
-            <i class="fa fa-user-md"></i>
-            <span>Vouchers</span>
-        </a>
-    </li>
+    {% if study.has_vouchers and "change_voucher" in permissions %}
+        <li data-desc="vouchers">
+            <a href="{% url 'web.views.vouchers' %}">
+                <i class="fa fa-user-md"></i>
+                <span>Vouchers</span>
+            </a>
+        </li>
     {% endif %}
 
     {% if conf_perms %}
 
-    <li data-desc="configuration" class="treeview">
-        <a href="#">
-            <i class="fa fa-wrench"></i> <span>Configuration</span>
-            <span class="pull-right-container">
+        <li data-desc="configuration" class="treeview">
+            <a href="#">
+                <i class="fa fa-wrench"></i> <span>Configuration</span>
+                <span class="pull-right-container">
               <i class="fa fa-angle-left pull-right"></i>
             </span>
-        </a>
-        <ul class="treeview-menu">
-            {% if "change_configurationitem" in permissions %}
-            <li data-desc="settings"><a href="{% url 'web.views.configuration' %}">General</a></li>
-            {% endif %}
-
-            {% if "change_language" in permissions %}
-            <li data-desc="languages"><a href="{% url 'web.views.languages' %}">Languages</a></li>
-            {% endif %}
-
-            {% if study.has_voucher_types and "change_vouchertype" in permissions %}
-            <li data-desc="voucher_types"><a href="{% url 'web.views.voucher_types' %}">Voucher types</a></li>
-            {% endif %}
-
-            {% if study.has_vouchers and "change_worker" in permissions %}
-            <li data-desc="voucher_partner"><a href="{% url 'web.views.workers' 'VOUCHER_PARTNER' %}">Voucher partners</a></li>
-            {% endif %}
-
-            {% if "change_worker" in permissions %}
-            <li data-desc="health_partner"><a href="{% url 'web.views.workers' 'HEALTH_PARTNER' %}">Health partners</a></li>
-            {% endif %}
-
-            {% if "change_study" in permissions %}
-            <li data-desc="study_conf"><a href="{% url 'web.views.edit_study' study_id %}">Study</a></li>
-            {% endif %}
-        </ul>
-    </li>
+            </a>
+            <ul class="treeview-menu">
+                {% if "change_configurationitem" in permissions %}
+                    <li data-desc="settings"><a href="{% url 'web.views.configuration' %}">General</a></li>
+                {% endif %}
+
+                {% if "change_language" in permissions %}
+                    <li data-desc="languages"><a href="{% url 'web.views.languages' %}">Languages</a></li>
+                {% endif %}
+
+                {% if study.has_voucher_types and "change_vouchertype" in permissions %}
+                    <li data-desc="voucher_types"><a href="{% url 'web.views.voucher_types' %}">Voucher types</a></li>
+                {% endif %}
+
+                {% if study.has_vouchers and "change_worker" in permissions %}
+                    <li data-desc="voucher_partner"><a href="{% url 'web.views.workers' 'VOUCHER_PARTNER' %}">Voucher
+                        partners</a></li>
+                {% endif %}
+
+                {% if "change_worker" in permissions %}
+                    <li data-desc="health_partner"><a href="{% url 'web.views.workers' 'HEALTH_PARTNER' %}">Health
+                        partners</a></li>
+                {% endif %}
+
+                {% if "change_study" in permissions %}
+                    <li data-desc="study_conf"><a href="{% url 'web.views.edit_study' study_id %}">Study</a></li>
+                {% endif %}
+            </ul>
+        </li>
 
     {% endif %}
 
diff --git a/smash/web/templates/study/edit.html b/smash/web/templates/study/edit.html
index 8e20e1e1ace1e7d4ea94f0dae72c9f53dd812247..23fb256da409bae69b027de9d8fcbd93f3bf1dd9 100644
--- a/smash/web/templates/study/edit.html
+++ b/smash/web/templates/study/edit.html
@@ -56,7 +56,8 @@
                                         <label for="{{ field.id_for_label }}" class="col-sm-4 control-label">
                                             {{ field.label }}
                                             {% if field.help_text %}
-                                                <i class="fa fa-info-circle" aria-hidden="true" data-toggle="tooltip" data-placement="bottom" title="{{field.help_text}}"></i>
+                                                <i class="fa fa-info-circle" aria-hidden="true" data-toggle="tooltip"
+                                                   data-placement="bottom" title="{{ field.help_text }}"></i>
                                             {% endif %}
                                         </label>
                                         
@@ -117,6 +118,8 @@
                             </div>
                         </div><!-- /.box-body -->
 
+                        {% include 'includes/custom_study_subject_field_box.html' with study=study fields=study.customstudysubjectfield_set.all %}
+
                         <div class="box-header with-border">
                             <h3>Available subject study data</h3>
                         </div>
diff --git a/smash/web/templates/subjects/index.html b/smash/web/templates/subjects/index.html
index 29a4d2308145826a49e5d49091eb949a3157b524..acd65e2701d66b6b21151692ddc55cea85393689 100644
--- a/smash/web/templates/subjects/index.html
+++ b/smash/web/templates/subjects/index.html
@@ -9,35 +9,42 @@
         .box-body {
             overflow-x: scroll;
         }
+
         .highlight_match {
             font-weight: bolder;
             border-bottom: black 1px solid;
         }
 
-        .dataTables_length{
+        .dataTables_length {
             display: inline-block;
         }
-        .dt-buttons{
+
+        .dt-buttons {
             margin-left: 10px;
         }
-        .visit_row{
+
+        .visit_row {
             font-size: 10pt;
             max-width: 22ch;
             min-width: 18ch;
             text-align: center;
         }
-        .visit_row > span{
+
+        .visit_row > span {
             padding-right: 2px;
             padding-left: 2px;
         }
-        .visit_row > span > a{
+
+        .visit_row > span > a {
             color: inherit;
         }
-        .appointment_type_list{
+
+        .appointment_type_list {
             margin-top: 10px;
             text-align: left;
         }
-        td.subject-address{
+
+        td.subject-address {
             word-break: break-word;
         }
     </style>
@@ -45,10 +52,10 @@
 {% endblock styles %}
 
 {% block ui_active_tab %}'subjects'{% endblock ui_active_tab %}
-{% block page_header %}{{  list_description }}{% endblock page_header %}
+{% block page_header %}{{ list_description }}{% endblock page_header %}
 {% block page_description %}{% endblock page_description %}
 
-{% block title %}{{ block.super }} - {{  list_description }}{% endblock %}
+{% block title %}{{ block.super }} - {{ list_description }}{% endblock %}
 
 {% block breadcrumb %}
     {% include "subjects/breadcrumb.html" %}
@@ -57,7 +64,7 @@
 {% block maincontent %}
 
     <div>
-        <a href="{% url 'web.views.subject_add' %}" class="btn btn-app"
+        <a href="{% url 'web.views.subject_add' study_id %}" class="btn btn-app"
                 {% if not "add_subject" in permissions %}
            disabled
                 {% endif %}
@@ -80,14 +87,22 @@
     {{ block.super }}
 
     <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/jquery.dataTables.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/dataTables.buttons.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/buttons.bootstrap.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/buttons.colVis.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/buttons.html5.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/JSZip/jszip.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/pdfmake/pdfmake.min.js' %}"></script>
-    <script type="text/javascript" src="{% static 'AdminLTE/plugins/datatables/extensions/pdfmake/vfs_fonts.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/dataTables.buttons.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/buttons.bootstrap.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/buttons.colVis.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/Buttons/js/buttons.html5.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/JSZip/jszip.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/pdfmake/pdfmake.min.js' %}"></script>
+    <script type="text/javascript"
+            src="{% static 'AdminLTE/plugins/datatables/extensions/pdfmake/vfs_fonts.js' %}"></script>
     <script type="text/javascript" src="{% static 'js/subject.js' %}"></script>
 
     <script>
diff --git a/smash/web/tests/api_views/test_serialization_utils.py b/smash/web/tests/api_views/test_serialization_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa556639930051bd06636dd2a4af2b5e0dbf0fe8
--- /dev/null
+++ b/smash/web/tests/api_views/test_serialization_utils.py
@@ -0,0 +1,33 @@
+import logging
+
+from django.core.files.images import ImageFile
+from django.test import TestCase
+
+from web.api_views.serialization_utils import *
+from web.tests.functions import get_resource_path
+
+logger = logging.getLogger(__name__)
+
+
+class SerializationUtilsTests(TestCase):
+    def test_str_to_yes_no(self):
+        self.assertEqual(str_to_yes_no('true'), 'YES')
+        with self.assertRaises(AttributeError):
+            str_to_yes_no(None)
+        self.assertEqual(str_to_yes_no('potato'), 'NO')
+        self.assertEqual(str_to_yes_no('false'), 'NO')
+
+    def test_str_to_yes_no_null(self):
+        self.assertEqual(str_to_yes_no_null('true'), 'YES')
+        self.assertEqual(str_to_yes_no_null(None), None)
+        self.assertEqual(str_to_yes_no_null('potato'), 'NO')
+        self.assertEqual(str_to_yes_no_null('false'), 'NO')
+
+    def test_bool_to_yes_no_null(self):
+        self.assertEqual(bool_to_yes_no_null(True), 'YES')
+        self.assertEqual(bool_to_yes_no_null(None), 'N/A')
+        self.assertEqual(bool_to_yes_no_null(False), 'NO')
+
+    def test_bool_to_yes_no(self):
+        self.assertEqual(bool_to_yes_no(True), 'YES')
+        self.assertEqual(bool_to_yes_no(False), 'NO')
diff --git a/smash/web/tests/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py
index bbfecae04b530cfa628872ba14132ca5feec0268..7f5c992faac5dd1844a2e6c62c0f8687dc91c098 100644
--- a/smash/web/tests/api_views/test_subject.py
+++ b/smash/web/tests/api_views/test_subject.py
@@ -4,18 +4,24 @@ import json
 import logging
 
 from django.urls import reverse
+from parameterized import parameterized
 from django.utils import timezone
 from six import ensure_str
 
 from web.api_views.subject import get_subjects_order, get_subjects_filtered, serialize_subject, get_subject_columns
 from web.importer.warning_counter import MsgCounterHandler
 from web.models import StudySubject, Appointment, Study, Worker, SubjectColumns, StudyColumns
-from web.models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES_PATIENT, SUBJECT_TYPE_CHOICES_CONTROL
+from web.models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES_PATIENT, SUBJECT_TYPE_CHOICES_CONTROL, \
+    CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, CUSTOM_FIELD_TYPE_DOUBLE, \
+    CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id
 from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \
     StudySubjectList, SUBJECT_LIST_VOUCHER_EXPIRY
 from web.tests import LoggedInWithWorkerTestCase
 from web.tests.functions import create_study_subject, create_get_suffix, create_visit, \
-    create_appointment, create_empty_study_columns, create_contact_attempt, create_flying_team, create_worker
+    create_appointment, create_empty_study_columns, create_contact_attempt, create_flying_team, create_worker, \
+    get_test_study
 from web.views.notifications import get_today_midnight_date
 
 logger = logging.getLogger(__name__)
@@ -114,31 +120,33 @@ class TestSubjectApi(LoggedInWithWorkerTestCase):
         self.assertTrue(referral_name in referrals)
 
     def test_subjects_general(self):
-        response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_GENERIC}))
+        response = self.client.get(reverse('web.api.subjects', kwargs={'subject_list_type': SUBJECT_LIST_GENERIC}))
         self.assertEqual(response.status_code, 200)
 
     def test_subjects_general_with_special_characters(self):
         contact_attempt = create_contact_attempt(subject=self.study_subject)
         contact_attempt.worker.first_name = "à special character"
         contact_attempt.worker.save()
-        response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_GENERIC}))
+        response = self.client.get(reverse('web.api.subjects', kwargs={'subject_list_type': SUBJECT_LIST_GENERIC}))
         self.assertEqual(response.status_code, 200)
         logger.debug(response.content)
 
     def test_subjects_voucher_almost_expired(self):
-        response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_VOUCHER_EXPIRY}))
+        response = self.client.get(
+            reverse('web.api.subjects', kwargs={'subject_list_type': SUBJECT_LIST_VOUCHER_EXPIRY}))
         self.assertEqual(response.status_code, 200)
 
     def test_subjects_no_visit(self):
-        response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_NO_VISIT}))
+        response = self.client.get(reverse('web.api.subjects', kwargs={'subject_list_type': SUBJECT_LIST_NO_VISIT}))
         self.assertEqual(response.status_code, 200)
 
     def test_subjects_require_contact(self):
-        response = self.client.get(reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_REQUIRE_CONTACT}))
+        response = self.client.get(
+            reverse('web.api.subjects', kwargs={'subject_list_type': SUBJECT_LIST_REQUIRE_CONTACT}))
         self.assertEqual(response.status_code, 200)
 
     def test_subjects_invalid(self):
-        response = self.client.get(reverse('web.api.subjects', kwargs={'type': "bla"}))
+        response = self.client.get(reverse('web.api.subjects', kwargs={'subject_list_type': "bla"}))
         self.assertEqual(response.status_code, 500)
 
     def test_subjects_general_search(self):
@@ -150,13 +158,15 @@ class TestSubjectApi(LoggedInWithWorkerTestCase):
             "columns[0][search][value]": "another_name",
             "columns[0][data]": "first_name"
         }
-        url = ("%s" + create_get_suffix(params)) % reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_GENERIC})
+        url = ("%s" + create_get_suffix(params)) % reverse('web.api.subjects',
+                                                           kwargs={'subject_list_type': SUBJECT_LIST_GENERIC})
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
         self.assertFalse(name.encode('utf8') in response.content)
 
         params["columns[0][search][value]"] = name
-        url = ("%s" + create_get_suffix(params)) % reverse('web.api.subjects', kwargs={'type': SUBJECT_LIST_GENERIC})
+        url = ("%s" + create_get_suffix(params)) % reverse('web.api.subjects',
+                                                           kwargs={'subject_list_type': SUBJECT_LIST_GENERIC})
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(name.encode('utf8') in response.content)
@@ -577,8 +587,8 @@ class TestSubjectApi(LoggedInWithWorkerTestCase):
         appointment.status = Appointment.APPOINTMENT_STATUS_FINISHED
         appointment.save()
 
-        visit = create_visit(subject2, datetime_begin=get_today_midnight_date() + datetime.timedelta(days=31),
-                             datetime_end=get_today_midnight_date() + datetime.timedelta(days=61))
+        create_visit(subject2, datetime_begin=get_today_midnight_date() + datetime.timedelta(days=31),
+                     datetime_end=get_today_midnight_date() + datetime.timedelta(days=61))
 
         self.check_subject_ordered("visit_1", [subject, subject2])
 
@@ -664,21 +674,21 @@ class TestSubjectApi(LoggedInWithWorkerTestCase):
 
     def test_get_subject_order_for_columns(self):
         study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0]
-        list = StudySubjectList.objects.create(study=study, type="custom",
-                                               visible_subject_columns=SubjectColumns.objects.create(),
-                                               visible_subject_study_columns=StudyColumns.objects.create())
-        for key in vars(list.visible_subject_columns):
-            if getattr(list.visible_subject_columns, key) == False:
-                setattr(list.visible_subject_columns, key, True)
-        list.visible_subject_columns.save()
-
-        for key in vars(list.visible_subject_study_columns):
-            if getattr(list.visible_subject_study_columns, key) == False:
-                setattr(list.visible_subject_study_columns, key, True)
-        list.visible_subject_study_columns.save()
+        subject_list = StudySubjectList.objects.create(study=study, type="custom",
+                                                       visible_subject_columns=SubjectColumns.objects.create(),
+                                                       visible_subject_study_columns=StudyColumns.objects.create())
+        for key in vars(subject_list.visible_subject_columns):
+            if not getattr(subject_list.visible_subject_columns, key):
+                setattr(subject_list.visible_subject_columns, key, True)
+        subject_list.visible_subject_columns.save()
+
+        for key in vars(subject_list.visible_subject_study_columns):
+            if not getattr(subject_list.visible_subject_study_columns, key):
+                setattr(subject_list.visible_subject_study_columns, key, True)
+        subject_list.visible_subject_study_columns.save()
 
         for key in vars(study.columns):
-            if getattr(study.columns, key) == False:
+            if not getattr(study.columns, key):
                 setattr(study.columns, key, True)
         study.columns.save()
 
@@ -708,3 +718,99 @@ class TestSubjectApi(LoggedInWithWorkerTestCase):
         self.check_subject_filtered([["virus_test_1", "true"]], [study_subject_positive])
         self.check_subject_filtered([["virus_test_1", "false"]], [study_subject_negative])
         self.check_subject_filtered([["virus_test_1", "inconclusive"]], [study_subject_inconclusive])
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, '103'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, '205.5'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-05'),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, 'BLA'),
+    ])
+    def test_subjects_filter_by_custom_field(self, _, field_type, value):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="", type=field_type)
+        study_subject = create_study_subject(2)
+        study_subject.set_custom_data_value(field, value)
+
+        self.check_subject_filtered([[get_study_subject_field_id(field), value]], [study_subject])
+        self.check_subject_filtered([[get_study_subject_field_id(field), value + "-bla"]], [])
+
+    def test_subjects_filter_by_custom_file_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="",
+                                                       type=CUSTOM_FIELD_TYPE_FILE)
+        study_subject = create_study_subject(2)
+        study_subject.set_custom_data_value(field, 'test_file.txt')
+
+        self.check_subject_filtered([[get_study_subject_field_id(field), 'Available']], [study_subject])
+        self.check_subject_filtered([[get_study_subject_field_id(field), "N/A"]], [])
+
+    def test_subjects_filter_by_two_custom_fields(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz",
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+        field2 = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz",
+                                                        type=CUSTOM_FIELD_TYPE_TEXT)
+        study_subject = create_study_subject(2)
+        study_subject.set_custom_data_value(field, "bla")
+        study_subject.set_custom_data_value(field2, "ab")
+
+        self.check_subject_filtered([[get_study_subject_field_id(field), "bla"],
+                                     [get_study_subject_field_id(field2), "ab"]], [study_subject])
+        self.check_subject_filtered([[get_study_subject_field_id(field), "bla"],
+                                     [get_study_subject_field_id(field2), "abcs"]], [])
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', 'cla', 'dla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, '', 'False', 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, '106', '107', '201'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, '206.12', '300.3', '400'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-05', '2021-01-05', '2021-01-06',),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, 'BLA', 'BLA2', 'BLA3'),
+        ('file list', CUSTOM_FIELD_TYPE_FILE, 'file1.txt', 'file2.txt', 'file3.txt'),
+    ])
+    def test_subjects_sort_by_custom_fields(self, _, field_type, value1, value2, value3):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="", type=field_type)
+        study_subject = self.study_subject
+        study_subject2 = create_study_subject(3)
+        study_subject3 = create_study_subject(4)
+        study_subject.set_custom_data_value(field, value1)
+        study_subject2.set_custom_data_value(field, value3)
+        study_subject3.set_custom_data_value(field, value2)
+
+        self.check_subject_ordered(get_study_subject_field_id(field), [study_subject, study_subject3, study_subject2])
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, '340'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, '540.8'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-07'),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, 'BLA'),
+        ('file', CUSTOM_FIELD_TYPE_FILE, 'test_file.txt'),
+    ])
+    def test_subjects_columns_for_custom(self, _, field_type, value):
+        CustomStudySubjectField.objects.create(study=Study.objects.filter(id=GLOBAL_STUDY_ID)[0], default_value=value,
+                                               type=field_type)
+        response = self.client.get(
+            reverse('web.api.subjects.columns', kwargs={'subject_list_type': SUBJECT_LIST_GENERIC}))
+        self.assertEqual(response.status_code, 200)
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, '341'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, '541.54'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-07'),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, 'BLA'),
+        ('file', CUSTOM_FIELD_TYPE_FILE, 'test_file.txt'),
+    ])
+    def test_serialize_subject_with_custom_field(self, _, field_type, value):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(),
+                                                       default_value='',
+                                                       type=field_type)
+
+        study_subject = self.study_subject
+        study_subject.set_custom_data_value(field, value)
+
+        subject_json = serialize_subject(study_subject)
+        self.assertIsNotNone(subject_json[get_study_subject_field_id(field)])
+        self.assertTrue(subject_json[get_study_subject_field_id(field)] != '')
diff --git a/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py b/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py
new file mode 100644
index 0000000000000000000000000000000000000000..e238aa3a5f2eff643e2b91eee188e54c37ead811
--- /dev/null
+++ b/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py
@@ -0,0 +1,44 @@
+from django.test import TestCase
+from parameterized import parameterized
+
+from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldAddForm
+from web.models.constants import CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, \
+    CUSTOM_FIELD_TYPE_DOUBLE, CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField
+from web.tests.functions import get_test_study
+
+
+class CustomStudySubjectFieldAddFormTest(TestCase):
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True),
+        ('bool valid', CUSTOM_FIELD_TYPE_BOOLEAN, 'True', True),
+        ('bool invalid', CUSTOM_FIELD_TYPE_BOOLEAN, 'bla', False),
+        ('int valid', CUSTOM_FIELD_TYPE_INTEGER, '102', True),
+        ('int invalid', CUSTOM_FIELD_TYPE_INTEGER, 'bla', False),
+        ('double valid', CUSTOM_FIELD_TYPE_DOUBLE, '202.25', True),
+        ('double invalid', CUSTOM_FIELD_TYPE_DOUBLE, 'bla', False),
+        ('date valid', CUSTOM_FIELD_TYPE_DATE, '2021-01-20', True),
+        ('date invalid', CUSTOM_FIELD_TYPE_DATE, 'bla', False),
+        ('select list valid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'abc', True, 'abc;def;xyz'),
+        ('select list invalid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'bla', False, 'abc;def'),
+        ('file', CUSTOM_FIELD_TYPE_FILE, None, True),
+        ('file invalid', CUSTOM_FIELD_TYPE_FILE, 'tmp', False),
+    ])
+    def test_add_field(self, _, field_type, default_value, valid, possible_values=''):
+        sample_data = {
+            'default_value': default_value,
+            'name': '1. name',
+            'type': field_type,
+            'possible_values': possible_values,
+        }
+
+        form = CustomStudySubjectFieldAddForm(sample_data, study=get_test_study())
+        if valid:
+            self.assertTrue(form.is_valid())
+            field = form.save()
+
+            self.assertEquals(1, CustomStudySubjectField.objects.filter(id=field.id).count())
+            self.assertEquals(default_value, field.default_value)
+        else:
+            self.assertFalse(form.is_valid())
diff --git a/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py b/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a4bd25fef20f3ebbd368660e839db3b1cdc93bf
--- /dev/null
+++ b/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py
@@ -0,0 +1,45 @@
+from django.test import TestCase
+from parameterized import parameterized
+
+from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldEditForm
+from web.models.constants import CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, \
+    CUSTOM_FIELD_TYPE_DOUBLE, CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField
+from web.tests.functions import get_test_study
+
+
+class CustomStudySubjectFieldEditFormTest(TestCase):
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True),
+        ('bool valid', CUSTOM_FIELD_TYPE_BOOLEAN, 'True', True),
+        ('bool invalid', CUSTOM_FIELD_TYPE_BOOLEAN, 'bla', False),
+        ('int valid', CUSTOM_FIELD_TYPE_INTEGER, '911', True),
+        ('int invalid', CUSTOM_FIELD_TYPE_INTEGER, 'bla', False),
+        ('double valid', CUSTOM_FIELD_TYPE_DOUBLE, '821.45', True),
+        ('double invalid', CUSTOM_FIELD_TYPE_DOUBLE, 'bla', False),
+        ('date valid', CUSTOM_FIELD_TYPE_DATE, '2020-10-04', True),
+        ('date invalid', CUSTOM_FIELD_TYPE_DATE, 'bla', False),
+        ('select list valid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'abc', True, 'abc;def;xyz'),
+        ('select list invalid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'bla', False, 'abc;def'),
+        ('file', CUSTOM_FIELD_TYPE_FILE, None, True),
+        ('file invalid', CUSTOM_FIELD_TYPE_FILE, 'tmp', False),
+    ])
+    def test_edit_field(self, _, field_type, default_value, valid, possible_values=''):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="", type=field_type)
+
+        sample_data = {
+            'default_value': default_value,
+            'name': '1. name',
+            'type': field_type,
+            'possible_values': possible_values,
+        }
+
+        form = CustomStudySubjectFieldEditForm(sample_data, instance=field)
+        if valid:
+            self.assertTrue(form.is_valid())
+            form.save()
+
+            self.assertEquals(default_value, field.default_value)
+        else:
+            self.assertFalse(form.is_valid())
diff --git a/smash/web/tests/forms/test_study_forms.py b/smash/web/tests/forms/test_StudyEditForm.py
similarity index 90%
rename from smash/web/tests/forms/test_study_forms.py
rename to smash/web/tests/forms/test_StudyEditForm.py
index 0fe2bd2cd5f0b9fce56a6f11feef5e9bb471fd49..cde133cd73d78c771803af408844866a676f310b 100644
--- a/smash/web/tests/forms/test_study_forms.py
+++ b/smash/web/tests/forms/test_StudyEditForm.py
@@ -1,14 +1,15 @@
 from django.test import TestCase
-from django.forms import ValidationError
-from web.tests.functions import get_test_study, create_study_subject
+
 from web.forms.study_forms import StudyEditForm
 from web.models.study import Study
 from web.models.study_subject import StudySubject
+from web.tests.functions import get_test_study, create_study_subject
+
 
-class StudyFormTests(TestCase):
+class StudyEditFormTests(TestCase):
 
     def test_study_default_regex(self):
-        # this will add a studysubject with a NDnumber
+        # this will add a StudySubject with a ND number
         StudySubject.objects.all().delete()
         create_study_subject(nd_number='ND0001')
         form = StudyEditForm()
@@ -30,7 +31,7 @@ class StudyFormTests(TestCase):
 
     def test_study_other_regex(self):
         StudySubject.objects.all().delete()
-        # this will add a studysubject with a NDnumber
+        # this will add a StudySubject with a ND number
         create_study_subject(nd_number='nd00001')
         form = StudyEditForm()
         form.instance = get_test_study()
diff --git a/smash/web/tests/forms/test_StudySubjectAddForm.py b/smash/web/tests/forms/test_StudySubjectAddForm.py
index 29b4a7b7715b80db243e0a3b30053f1a7f3ba7b7..62ea530a82927bf964586ae771b651822e3a5f1e 100644
--- a/smash/web/tests/forms/test_StudySubjectAddForm.py
+++ b/smash/web/tests/forms/test_StudySubjectAddForm.py
@@ -1,8 +1,15 @@
+import datetime
 import logging
 
-from web.forms.study_subject_forms import get_new_screening_number
+from django.core.files.uploadedfile import SimpleUploadedFile
+from parameterized import parameterized
+
 from web.forms import StudySubjectAddForm
-from web.models.constants import SUBJECT_TYPE_CHOICES_CONTROL
+from web.forms.study_subject_forms import get_new_screening_number, get_study_subject_field_id
+from web.models.constants import SUBJECT_TYPE_CHOICES_CONTROL, CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, \
+    CUSTOM_FIELD_TYPE_INTEGER, CUSTOM_FIELD_TYPE_DOUBLE, CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, \
+    CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField, CustomStudySubjectValue
 from web.tests import LoggedInWithWorkerTestCase
 from web.tests.functions import create_study_subject, create_subject, get_test_study, create_empty_study
 
@@ -28,6 +35,29 @@ class StudySubjectAddFormTests(LoggedInWithWorkerTestCase):
         form.is_valid()
         self.assertTrue(form.is_valid())
 
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', 'bla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, True, 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, 104, '104'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, 201.25, '201.25'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-03', '2020-11-03'),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, '1', 'BLA', 'BLA;BLA-BLA'),
+    ])
+    def test_add_with_custom_field(self, _, field_type, value, expected_serialized_value, possible_values=''):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="",
+                                                       type=field_type, possible_values=possible_values)
+        count = CustomStudySubjectValue.objects.filter(study_subject_field=field).count()
+
+        self.sample_data[get_study_subject_field_id(field)] = value
+
+        form = StudySubjectAddForm(data=self.sample_data, user=self.user, study=self.study)
+        form.instance.subject_id = create_subject().id
+        self.assertTrue(form.is_valid())
+        subject = form.save()
+
+        self.assertEqual(count + 1, CustomStudySubjectValue.objects.filter(study_subject_field=field).count())
+        self.assertEqual(expected_serialized_value, subject.get_custom_data_value(field).value)
+
     def test_validation_for_study_without_columns(self):
         form = StudySubjectAddForm(data=self.sample_data, user=self.user, study=create_empty_study())
         self.assertTrue(form.is_valid())
@@ -151,3 +181,26 @@ class StudySubjectAddFormTests(LoggedInWithWorkerTestCase):
 
         new_screening_number = get_new_screening_number(prefix)
         self.assertEqual(prefix + "001", new_screening_number)
+
+    def test_form_with_readonly_custom_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", readonly=True,
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+
+        form = StudySubjectAddForm(user=self.user, study=self.study)
+
+        self.assertTrue(form.fields[get_study_subject_field_id(field)].disabled)
+
+    def test_form_with_unique_custom_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", unique=True,
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+
+        study_subject = create_study_subject()
+        study_subject.set_custom_data_value(field, "bla")
+
+        self.sample_data[get_study_subject_field_id(field)] = "bla2"
+        form = StudySubjectAddForm(data=self.sample_data, user=self.user, study=self.study)
+        self.assertTrue(form.is_valid())
+
+        self.sample_data[get_study_subject_field_id(field)] = "bla"
+        form = StudySubjectAddForm(data=self.sample_data, user=self.user, study=self.study)
+        self.assertFalse(form.is_valid())
diff --git a/smash/web/tests/forms/test_StudySubjectEditForm.py b/smash/web/tests/forms/test_StudySubjectEditForm.py
index da51f0a6a8d77fbdf15b5b35a6805ee1a6fbfa77..3e0a4676323068c713a08f74ef3eb4d1dbd5a175 100644
--- a/smash/web/tests/forms/test_StudySubjectEditForm.py
+++ b/smash/web/tests/forms/test_StudySubjectEditForm.py
@@ -1,9 +1,15 @@
 import logging
 
+from parameterized import parameterized
+
 from web.forms import StudySubjectEditForm
 from web.models import StudySubject
+from web.models.constants import CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_BOOLEAN, CUSTOM_FIELD_TYPE_INTEGER, \
+    CUSTOM_FIELD_TYPE_DOUBLE, CUSTOM_FIELD_TYPE_DATE, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id
 from web.tests import LoggedInWithWorkerTestCase
-from web.tests.functions import create_study_subject, create_empty_study
+from web.tests.functions import create_study_subject, create_empty_study, get_test_study
 
 logger = logging.getLogger(__name__)
 
@@ -27,7 +33,7 @@ class StudySubjectEditFormTests(LoggedInWithWorkerTestCase):
         StudySubject.objects.all().delete()
 
     def test_validation(self):
-        edit_form = StudySubjectEditForm(self.sample_data)
+        edit_form = StudySubjectEditForm(self.sample_data, instance=self.study_subject)
         save_status = edit_form.is_valid()
         logger.debug(edit_form.errors)
         self.assertTrue(save_status)
@@ -36,7 +42,7 @@ class StudySubjectEditFormTests(LoggedInWithWorkerTestCase):
         self.study_subject.study = create_empty_study()
         self.study_subject.save()
 
-        edit_form = StudySubjectEditForm(self.sample_data)
+        edit_form = StudySubjectEditForm(self.sample_data, instance=self.study_subject)
         save_status = edit_form.is_valid()
         self.assertTrue(save_status)
 
@@ -53,6 +59,47 @@ class StudySubjectEditFormTests(LoggedInWithWorkerTestCase):
         self.assertTrue("nd_number" in edit_form.errors)
         self.assertFalse(save_status)
 
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', 'bla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, True, 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, 10, '10'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, 30.5, '30.5'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-05', '2020-11-05'),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, '1', 'BLA', 'BLA;BLA-BLA'),
+        ('select list empty value', CUSTOM_FIELD_TYPE_SELECT_LIST, '0', '', 'BLA;BLA-BLA'),
+    ])
+    def test_edit_with_custom_field(self, _, field_type, value, expected_serialized_value, possible_values=''):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="",
+                                                       type=field_type, possible_values=possible_values)
+
+        self.assertEqual(field.default_value, self.study_subject.get_custom_data_value(field).value)
+
+        self.sample_data[get_study_subject_field_id(field)] = value
+
+        edit_form = StudySubjectEditForm(self.sample_data, instance=self.study_subject)
+        self.assertTrue(edit_form.is_valid())
+        subject = edit_form.save()
+
+        self.assertEqual(expected_serialized_value, subject.get_custom_data_value(field).value)
+
+    @parameterized.expand([
+        ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', 'bla'),
+        ('bool', CUSTOM_FIELD_TYPE_BOOLEAN, True, 'True'),
+        ('int', CUSTOM_FIELD_TYPE_INTEGER, 10, '10'),
+        ('double', CUSTOM_FIELD_TYPE_DOUBLE, 30.5, '30.5'),
+        ('date', CUSTOM_FIELD_TYPE_DATE, '2020-11-05', '2020-11-05'),
+        ('select list', CUSTOM_FIELD_TYPE_SELECT_LIST, '1', 'BLA', 'BLA;BLA-BLA'),
+        ('select list empty value', CUSTOM_FIELD_TYPE_SELECT_LIST, '0', '', 'BLA;BLA-BLA'),
+        ('file', CUSTOM_FIELD_TYPE_FILE, 'bla.txt', 'bla.txt'),
+    ])
+    def test_initial_edit_with_custom_field(self, _, field_type, expected_initial_value, value, possible_values=''):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value=value,
+                                                       type=field_type, possible_values=possible_values)
+
+        form = StudySubjectEditForm(instance=self.study_subject)
+
+        self.assertTrue(form[get_study_subject_field_id(field)].initial, expected_initial_value)
+
     def test_invalid_mpower_id_edit(self):
         self.study_subject.mpower_id = "mpower_002"
         self.study_subject.nd_number = "ND0002"
@@ -62,8 +109,31 @@ class StudySubjectEditFormTests(LoggedInWithWorkerTestCase):
         self.sample_data['mpower_id'] = "mpower_002"
         self.sample_data['nd_number'] = "ND0001"
         self.sample_data['screening_number'] = "001"
-        edit_form = StudySubjectEditForm(self.sample_data)
+        edit_form = StudySubjectEditForm(self.sample_data, instance=self.study_subject)
 
         save_status = edit_form.is_valid()
         self.assertTrue("mpower_id" in edit_form.errors)
         self.assertFalse(save_status)
+
+    def test_form_with_readonly_custom_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", readonly=True,
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+
+        form = StudySubjectEditForm(instance=self.study_subject)
+
+        self.assertTrue(form.fields[get_study_subject_field_id(field)].disabled)
+
+    def test_form_with_unique_custom_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", unique=True,
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+
+        study_subject2 = create_study_subject()
+        study_subject2.set_custom_data_value(field, "bla")
+
+        self.sample_data[get_study_subject_field_id(field)] = "bla2"
+        form = StudySubjectEditForm(self.sample_data, instance=self.study_subject)
+        self.assertTrue(form.is_valid())
+
+        self.sample_data[get_study_subject_field_id(field)] = "bla"
+        form = StudySubjectEditForm(self.sample_data, instance=self.study_subject)
+        self.assertFalse(form.is_valid())
diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py
index fd899d8f80dd75eb34defca69fb04c0654560169..26f7ebb241e39e206bd7e7dad27a5fce53ab51ad 100644
--- a/smash/web/tests/functions.py
+++ b/smash/web/tests/functions.py
@@ -2,6 +2,7 @@ import datetime
 import logging
 import os
 
+from django.contrib.auth.models import Permission
 from django.contrib.auth.models import User
 from django.utils.timezone import make_aware, is_aware
 
@@ -14,7 +15,6 @@ from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, REDCAP_BASE_UR
 from web.models.worker_study_role import ROLE_CHOICES_DOCTOR, WORKER_VOUCHER_PARTNER
 from web.redcap_connector import RedcapSubject
 from web.views.notifications import get_today_midnight_date
-from django.contrib.auth.models import Permission
 
 logger = logging.getLogger(__name__)
 
@@ -70,7 +70,8 @@ def create_study(name="test"):
     study_columns = StudyColumns.objects.create()
     notification_parameters = StudyNotificationParameters.objects.create()
     redcap_columns = StudyRedCapColumns.objects.create()
-    return Study.objects.create(name=name, columns=study_columns, notification_parameters=notification_parameters, redcap_columns=redcap_columns)
+    return Study.objects.create(name=name, columns=study_columns, notification_parameters=notification_parameters,
+                                redcap_columns=redcap_columns)
 
 
 TEST_ID_COUNTER = 0
@@ -191,7 +192,7 @@ def create_subject():
     )
 
 
-def create_study_subject(subject_id=1, subject=None, nd_number='ND0001'):
+def create_study_subject(subject_id=1, subject=None, nd_number='ND0001') -> StudySubject:
     if subject is None:
         subject = create_subject()
     study_subject = StudySubject.objects.create(
@@ -201,7 +202,7 @@ def create_study_subject(subject_id=1, subject=None, nd_number='ND0001'):
         study=get_test_study(),
         subject=subject
     )
-    if nd_number is not None: #null value in column "nd_number" violates not-null constraint
+    if nd_number is not None:  # null value in column "nd_number" violates not-null constraint
         study_subject.nd_number = nd_number
         study_subject.save()
 
@@ -242,12 +243,14 @@ def create_user(username=None, password=None):
     create_worker(user)
     return user
 
+
 def add_permissions_to_worker(worker, codenames):
-    roles  = WorkerStudyRole.objects.filter(worker=worker, study_id=GLOBAL_STUDY_ID)
-    perms  = Permission.objects.filter(codename__in=codenames).all()
+    roles = WorkerStudyRole.objects.filter(worker=worker, study_id=GLOBAL_STUDY_ID)
+    perms = Permission.objects.filter(codename__in=codenames).all()
     roles[0].permissions.set(perms)
     roles[0].save()
 
+
 def create_worker(user=None, with_test_location=False):
     worker = Worker.objects.create(
         first_name='piotr',
@@ -321,7 +324,7 @@ def create_appointment(visit=None, when=None, length=30):
             make_aware(when_datetime)
     else:
         when_datetime = when
-    
+
     return Appointment.objects.create(
         visit=visit,
         length=length,
@@ -427,6 +430,5 @@ def datetimeify_date(date, timezone=datetime.timezone.utc):
         """
 
     actual_type = str(type(date))
-    raise TypeError("Date should be either a subclass of 'datetime.date', string or bytes! But is: {} instead".format(actual_type))
-
-
+    raise TypeError(
+        "Date should be either a subclass of 'datetime.date', string or bytes! But is: {} instead".format(actual_type))
diff --git a/smash/web/tests/models/test_study_subject.py b/smash/web/tests/models/test_study_subject.py
index 343aa758cfaabb4bd57fd67f60322fe1d53af74b..11878f57384cd028d4bcb9412e06330be9d34db8 100644
--- a/smash/web/tests/models/test_study_subject.py
+++ b/smash/web/tests/models/test_study_subject.py
@@ -1,10 +1,11 @@
 from django.test import TestCase
 
-from web.models import Appointment
-from web.models import Visit
-from web.models import StudySubject, Study
-from web.tests.functions import get_test_study, create_study_subject, create_appointment, create_study_subject_with_multiple_screening_numbers
-from web.tests.functions import create_visit
+from web.models import Appointment, Visit, StudySubject, Study
+from web.models.constants import CUSTOM_FIELD_TYPE_TEXT
+from web.models.custom_data import CustomStudySubjectField
+from web.tests.functions import create_visit, create_study
+from web.tests.functions import get_test_study, create_study_subject, create_appointment, \
+    create_study_subject_with_multiple_screening_numbers
 
 
 class SubjectModelTests(TestCase):
@@ -123,7 +124,6 @@ class SubjectModelTests(TestCase):
         self.assertFalse(StudySubject.check_nd_number_regex(nd_number_study_subject_regex_default, study))
 
     def test_sort_matched_screening_first(self):
-
         def create_result(phrase, subject_id=1):
             phrase = phrase.format(subject_id=subject_id)
             phrase = phrase.split(';')
@@ -152,7 +152,6 @@ class SubjectModelTests(TestCase):
             'E-00{subject_id}; L-00{subject_id}', subject_id=1))
 
     def test_sort_matched_screening_first_bad_number(self):
-
         subject_id = 2
         screening_number = 'L-0/0{}; E-00{}; F-0/1{}'.format(subject_id, subject_id, subject_id)
 
@@ -164,10 +163,53 @@ class SubjectModelTests(TestCase):
         self.assertEqual(subject.sort_matched_screening_first('L-'), [('L', '0/02'), ('E', subject_id), ('F', '0/12')])
 
     def test_sort_matched_screening_first_bad_format(self):
-
         screening_number = 'potato; tomato'
         subject_id = 3
         subject = create_study_subject_with_multiple_screening_numbers(
             subject_id=subject_id, screening_number=screening_number)
 
         self.assertEqual(subject.sort_matched_screening_first('L-'), [('potato', None), ('tomato', None)])
+
+    def test_subject_with_custom_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz",
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+
+        subject = create_study_subject()
+
+        self.assertEqual(1, len(subject.custom_data_values))
+        value = subject.custom_data_values[0]
+        self.assertEqual("xyz", value.value)
+        self.assertEqual(subject, value.study_subject)
+        self.assertEqual(field, value.study_subject_field)
+
+    def test_subject_with_removed_custom_field(self):
+        field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz",
+                                                       type=CUSTOM_FIELD_TYPE_TEXT)
+
+        subject = create_study_subject()
+        self.assertEqual(1, len(subject.custom_data_values))
+        field.delete()
+
+        self.assertEqual(0, len(subject.custom_data_values))
+
+    def test_subject_with_field_from_different_study(self):
+        field = CustomStudySubjectField.objects.create(study=create_study('bla'), type=CUSTOM_FIELD_TYPE_TEXT)
+
+        subject = create_study_subject()
+        self.assertIsNone(subject.get_custom_data_value(field))
+
+    def test_status(self):
+        subject = create_study_subject()
+        # noinspection PySetFunctionToLiteral
+        statuses = set([subject.status, ])
+        subject.endpoint_reached = True
+        statuses.add(subject.status)
+        subject.excluded = True
+        statuses.add(subject.status)
+        subject.resigned = True
+        statuses.add(subject.status)
+        subject.subject.dead = True
+        statuses.add(subject.status)
+
+        print(statuses)
+        self.assertEqual(5, len(statuses))
diff --git a/smash/web/tests/view/test_custom_study_subject_field.py b/smash/web/tests/view/test_custom_study_subject_field.py
new file mode 100644
index 0000000000000000000000000000000000000000..72bb61dceeee9c4311ef3f2374451e4269b6b81d
--- /dev/null
+++ b/smash/web/tests/view/test_custom_study_subject_field.py
@@ -0,0 +1,73 @@
+import logging
+
+from django.urls import reverse
+
+from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldEditForm
+from web.models.constants import CUSTOM_FIELD_TYPE_TEXT
+from web.models.custom_data import CustomStudySubjectField
+from web.tests import LoggedInWithWorkerTestCase
+from web.tests.functions import get_test_study, format_form_field
+
+logger = logging.getLogger(__name__)
+
+
+class CustomStudySubjectFieldViewTests(LoggedInWithWorkerTestCase):
+    def setUp(self):
+        super(CustomStudySubjectFieldViewTests, self).setUp()
+        self.study = get_test_study()
+
+    def test_render_add(self):
+        self.login_as_admin()
+        response = self.client.get(
+            reverse('web.views.custom_study_subject_field_add', kwargs={'study_id': self.study.id}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_edit(self):
+        self.login_as_admin()
+        field = CustomStudySubjectField.objects.create(study=self.study, type=CUSTOM_FIELD_TYPE_TEXT)
+        response = self.client.get(
+            reverse('web.views.custom_study_subject_field_edit',
+                    kwargs={'study_id': self.study.id, 'field_id': field.id}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_save_edit_field(self):
+        self.login_as_admin()
+        field = CustomStudySubjectField.objects.create(study=self.study, name='HW', type=CUSTOM_FIELD_TYPE_TEXT)
+        form_data = CustomStudySubjectFieldViewTests.create_edit_form_data_for_study(field)
+        form_data['name'] = 'test'
+        response = self.client.post(
+            reverse('web.views.custom_study_subject_field_edit',
+                    kwargs={'study_id': self.study.id, 'field_id': field.id}), data=form_data)
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue("study" in response['Location'])
+        field = CustomStudySubjectField.objects.get(id=field.id)
+        self.assertEqual('test', field.name)
+
+    def test_save_add_field(self):
+        self.login_as_admin()
+        field = CustomStudySubjectField.objects.create(study=self.study, name='HW', type=CUSTOM_FIELD_TYPE_TEXT)
+        count = CustomStudySubjectField.objects.all().count()
+        form_data = CustomStudySubjectFieldViewTests.create_edit_form_data_for_study(field)
+        response = self.client.post(
+            reverse('web.views.custom_study_subject_field_add', kwargs={'study_id': self.study.id}), data=form_data)
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(count + 1, CustomStudySubjectField.objects.all().count())
+
+    def test_delete_field(self):
+        self.login_as_admin()
+        field = CustomStudySubjectField.objects.create(study=self.study, name='HW', type=CUSTOM_FIELD_TYPE_TEXT)
+        response = self.client.post(
+            reverse('web.views.custom_study_subject_field_delete',
+                    kwargs={'study_id': self.study.id, 'field_id': field.id}))
+        self.assertEqual(response.status_code, 302)
+        count = CustomStudySubjectField.objects.filter(id=field.id).count()
+        self.assertEqual(0, count)
+
+    @staticmethod
+    def create_edit_form_data_for_study(field: CustomStudySubjectField):
+        study_form = CustomStudySubjectFieldEditForm(instance=field)
+
+        form_data = {}
+        for key, value in list(study_form.initial.items()):
+            form_data[key] = format_form_field(value)
+        return form_data
diff --git a/smash/web/tests/view/test_export.py b/smash/web/tests/view/test_export.py
index 3f6a5c99b7fc12060eb0aad8ca95a92460d424de..c367eb1d2df89ee577b1d2eeb8ef302093e6d2ac 100644
--- a/smash/web/tests/view/test_export.py
+++ b/smash/web/tests/view/test_export.py
@@ -2,50 +2,59 @@
 from django.urls import reverse
 
 from web.models import Appointment, AppointmentTypeLink
+from web.models.constants import CUSTOM_FIELD_TYPE_TEXT
+from web.models.custom_data import CustomStudySubjectField
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id
 from web.tests import LoggedInTestCase
-from web.tests.functions import create_study_subject, create_appointment, create_visit, create_appointment_type
-from web.views.export import subject_to_row_for_fields, DROP_OUT_FIELD
+from web.tests.functions import create_study_subject, create_appointment, create_visit, create_appointment_type, \
+    get_test_study
+from web.views.export import subject_to_row_for_fields, DROP_OUT_FIELD, get_subjects_as_array
 
 
 class TestExportView(LoggedInTestCase):
     def test_export_subjects_to_csv(self):
         self.login_as_admin()
         create_study_subject()
-        response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "subjects"}))
+        response = self.client.get(
+            reverse('web.views.export_to_csv', kwargs={'study_id': get_test_study().id, 'data_type': "subjects"}))
         self.assertEqual(response.status_code, 200)
 
     def test_export_subjects_to_csv_without_permission(self):
-        response = self.client.get(reverse("web.views.mail_templates"))
+        self.client.get(reverse("web.views.mail_templates"))
         create_study_subject()
-        response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "subjects"}))
+        response = self.client.get(
+            reverse('web.views.export_to_csv', kwargs={'study_id': get_test_study().id, 'data_type': "subjects"}))
         self.assertEqual(response.status_code, 302)
 
     def test_render_export(self):
         self.login_as_admin()
         create_study_subject()
-        response = self.client.get(reverse('web.views.export'))
+        response = self.client.get(reverse('web.views.export', kwargs={'study_id': get_test_study().id}))
         self.assertEqual(response.status_code, 200)
 
     def test_render_export_without_permission(self):
         create_study_subject()
-        response = self.client.get(reverse('web.views.export'))
+        response = self.client.get(reverse('web.views.export', kwargs={'study_id': get_test_study().id}))
         self.assertEqual(response.status_code, 302)
 
     def test_export_appointments_to_csv(self):
         self.login_as_admin()
         create_appointment()
-        response = self.client.get(reverse('web.views.export_to_csv', kwargs={'data_type': "appointments"}))
+        response = self.client.get(
+            reverse('web.views.export_to_csv', kwargs={'study_id': get_test_study().id, 'data_type': "appointments"}))
         self.assertEqual(response.status_code, 200)
 
     def test_export_subjects_to_excel(self):
         self.login_as_admin()
         create_study_subject()
-        response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "subjects"}))
+        response = self.client.get(
+            reverse('web.views.export_to_excel', kwargs={'study_id': get_test_study().id, 'data_type': "subjects"}))
         self.assertEqual(response.status_code, 200)
 
     def test_export_subjects_to_excel_without_permission(self):
         create_study_subject()
-        response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "subjects"}))
+        response = self.client.get(
+            reverse('web.views.export_to_excel', kwargs={'study_id': get_test_study().id, 'data_type': "subjects"}))
         self.assertEqual(response.status_code, 302)
 
     def test_export_appointments_to_excel(self):
@@ -55,7 +64,8 @@ class TestExportView(LoggedInTestCase):
         appointment.save()
         AppointmentTypeLink.objects.create(appointment=appointment, appointment_type=create_appointment_type())
 
-        response = self.client.get(reverse('web.views.export_to_excel', kwargs={'data_type': "appointments"}))
+        response = self.client.get(
+            reverse('web.views.export_to_excel', kwargs={'study_id': get_test_study().id, 'data_type': "appointments"}))
         self.assertEqual(response.status_code, 200)
 
     def test_subject_to_row_for_fields_when_not_resigned(self):
@@ -86,3 +96,11 @@ class TestExportView(LoggedInTestCase):
 
         result = subject_to_row_for_fields(subject, [DROP_OUT_FIELD])
         self.assertTrue(result[0])
+
+    def test_subject_with_custom_field(self):
+        subject = create_study_subject()
+        field = CustomStudySubjectField.objects.create(study=subject.study, name='HW', type=CUSTOM_FIELD_TYPE_TEXT)
+        subject.set_custom_data_value(field, 'bbb')
+
+        subjects = get_subjects_as_array(get_test_study(), get_study_subject_field_id(field))
+        self.assertEqual('bbb', subjects[1][0])
diff --git a/smash/web/tests/view/test_study.py b/smash/web/tests/view/test_study.py
index e1965334f0a2cbdebd6cfeeb339927da3a317029..d71abed6d62d3720cb0811246620b26279e4a1ee 100644
--- a/smash/web/tests/view/test_study.py
+++ b/smash/web/tests/view/test_study.py
@@ -52,7 +52,7 @@ class StudyViewTests(LoggedInWithWorkerTestCase):
 
         self.assertEqual(response.status_code, 302)
 
-    def test_save_study_without_changing_page(self):
+    def test_save_study_and_continue_without_changing_page(self):
         self.login_as_admin()
         form_data = self.create_edit_form_data_for_study()
         form_data['_continue'] = True
@@ -63,6 +63,16 @@ class StudyViewTests(LoggedInWithWorkerTestCase):
         self.assertEqual(response.status_code, 302)
         self.assertTrue("edit" in response['Location'])
 
+    def test_save_study_without_changing_page(self):
+        self.login_as_admin()
+        form_data = self.create_edit_form_data_for_study()
+
+        response = self.client.post(
+            reverse('web.views.edit_study', kwargs={'study_id': GLOBAL_STUDY_ID}), data=form_data)
+
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue("appointments" in response['Location'])
+
     def create_edit_form_data_for_study(self):
         study_form = StudyEditForm(instance=self.study, prefix="study")
         notifications_form = StudyNotificationParametersEditForm(instance=self.study.notification_parameters,
diff --git a/smash/web/tests/view/test_subjects.py b/smash/web/tests/view/test_subjects.py
index a6332603515f633cafd66bedd86bf6a7b6a9b905..c0fb25330b8b12e4b1fff4cb35f176a1d3ac2a4a 100644
--- a/smash/web/tests/view/test_subjects.py
+++ b/smash/web/tests/view/test_subjects.py
@@ -6,9 +6,11 @@ from django.core.files.uploadedfile import SimpleUploadedFile
 from django.urls import reverse
 
 from web.forms import SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm
-from web.models import MailTemplate, StudySubject, StudyColumns, Visit
+from web.models import MailTemplate, StudySubject, StudyColumns, Visit, Provenance
 from web.models.constants import SEX_CHOICES_MALE, SUBJECT_TYPE_CHOICES_CONTROL, SUBJECT_TYPE_CHOICES_PATIENT, \
-    COUNTRY_AFGHANISTAN_ID, COUNTRY_OTHER_ID, MAIL_TEMPLATE_CONTEXT_SUBJECT
+    COUNTRY_AFGHANISTAN_ID, COUNTRY_OTHER_ID, MAIL_TEMPLATE_CONTEXT_SUBJECT, CUSTOM_FIELD_TYPE_FILE
+from web.models.custom_data import CustomStudySubjectField
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id
 from web.tests import LoggedInWithWorkerTestCase
 from web.tests.functions import create_study_subject, create_visit, create_appointment, get_test_location, \
     create_language, get_resource_path, get_test_study, format_form_field
@@ -27,7 +29,7 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject"))
         self.worker.save()
 
-        response = self.client.get(reverse('web.views.subject_add'))
+        response = self.client.get(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}))
         self.assertEqual(response.status_code, 200)
 
     def test_render_subject_edit(self):
@@ -138,7 +140,7 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         self.assertEqual(response.status_code, 302)
         self.assertTrue("edit" in response.url)
 
-    def create_edit_form_data_for_study_subject(self, instance=None):
+    def create_edit_form_data_for_study_subject(self, instance: StudySubject = None):
         if instance is None:
             instance = self.study_subject
         form_study_subject = StudySubjectEditForm(instance=instance, prefix="study_subject")
@@ -160,6 +162,7 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         for key, value in list(form_subject.initial.items()):
             form_data['subject-{}'.format(key)] = format_form_field(value)
         self.add_valid_form_data_for_subject_add(form_data)
+        form_data["study_subject-default_location"] = get_test_location().id
         return form_data
 
     def test_subjects_add_2(self):
@@ -168,8 +171,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         form_data = self.create_add_form_data_for_study_subject()
 
         form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_CONTROL
-        form_data["study_subject-default_location"] = get_test_location().id
-        response = self.client.post(reverse('web.views.subject_add'), data=form_data)
+        response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}),
+                                    data=form_data)
         self.assertEqual(response.status_code, 302)
         response = self.client.get(response.url)
         self.assertContains(response, "Subject created")
@@ -187,9 +190,9 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         form_data = self.create_add_form_data_for_study_subject()
 
         form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_CONTROL
-        form_data["study_subject-default_location"] = get_test_location().id
         form_data["study_subject-referral_letter"] = SimpleUploadedFile("file.txt", b"file_content")
-        response = self.client.post(reverse('web.views.subject_add'), data=form_data)
+        response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}),
+                                    data=form_data)
         self.assertEqual(response.status_code, 302)
         response = self.client.get(response.url)
         self.assertContains(response, "Subject created")
@@ -223,8 +226,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         self.worker.save()
         form_data = self.create_add_form_data_for_study_subject()
 
-        form_data["study_subject-default_location"] = get_test_location().id
-        response = self.client.post(reverse('web.views.subject_add'), data=form_data)
+        response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}),
+                                    data=form_data)
         self.assertEqual(response.status_code, 302)
         response = self.client.get(response.url)
         self.assertContains(response, "Subject created")
@@ -234,15 +237,31 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
                          "prefix should start by P" +
                          " as default location prefix is not defined and subject type is patient")
 
+    def test_subjects_add_subject_with_custom_file_field(self):
+        field = CustomStudySubjectField.objects.create(study=self.study, type=CUSTOM_FIELD_TYPE_FILE)
+        self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject"))
+        self.worker.save()
+        form_data = self.create_add_form_data_for_study_subject()
+
+        form_data["study_subject-" + get_study_subject_field_id(field)] = SimpleUploadedFile("my-custom-file-name.txt",
+                                                                                             b"file_content")
+        response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}),
+                                    data=form_data)
+
+        self.assertEqual(response.status_code, 302)
+
+        subject = StudySubject.objects.all().order_by("-id")[0]
+        self.assertTrue('my-custom-file-name' in subject.get_custom_data_value(field).value)
+
     def test_subjects_add_invalid(self):
         self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject"))
         self.worker.save()
         form_data = self.create_add_form_data_for_study_subject()
         form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_CONTROL
-        form_data["study_subject-default_location"] = get_test_location().id
 
         form_data["subject-country"] = COUNTRY_OTHER_ID
-        response = self.client.post(reverse('web.views.subject_add'), data=form_data)
+        response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}),
+                                    data=form_data)
 
         self.assertTrue("Invalid data".encode('utf8') in response.content)
 
@@ -257,7 +276,8 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
         location.prefix = 'X'
         location.save()
         form_data["study_subject-default_location"] = get_test_location().id
-        response = self.client.post(reverse('web.views.subject_add'), data=form_data)
+        response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}),
+                                    data=form_data)
         self.assertEqual(response.status_code, 302)
         response = self.client.get(response.url)
         self.assertContains(response, "Subject created")
@@ -283,3 +303,15 @@ class SubjectsViewTests(LoggedInWithWorkerTestCase):
 
         response = self.client.get(reverse('web.views.subject_require_contact'))
         self.assertEqual(response.status_code, 200)
+
+    def test_save_subject_edit_when_type_changed(self):
+        form_data = self.create_edit_form_data_for_study_subject(self.study_subject)
+
+        count = Provenance.objects.all().count()
+        form_data["study_subject-type"] = SUBJECT_TYPE_CHOICES_PATIENT
+        response = self.client.post(
+            reverse('web.views.subject_edit', kwargs={'id': self.study_subject.id}), data=form_data)
+
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(count + 1, Provenance.objects.all().count())
diff --git a/smash/web/urls.py b/smash/web/urls.py
index 05889ef12e873bbdeb356aaecf299862dbd9ae4a..1bf145b3ba0de4ca48b4b0279be0c5b829a034d6 100644
--- a/smash/web/urls.py
+++ b/smash/web/urls.py
@@ -18,7 +18,6 @@ from django.conf.urls import include
 from django.conf.urls import url
 from django.contrib.auth.views import logout
 from django.views.defaults import page_not_found
-from django.views.generic import TemplateView
 
 from web import views
 from web.views.daily_planning import TemplateDailyPlannerView
@@ -35,10 +34,14 @@ urlpatterns = [
 
     url(r'^change_password/$', views.password.change_password, name='change_password'),
 
-    url(r'^appointment_types/$', views.appointment_type.AppointmentTypeListView.as_view(), name='web.views.appointment_types'),
-    url(r'^appointment_types/add$', views.appointment_type.AppointmentTypeCreateView.as_view(), name='web.views.appointment_type_add'),
-    url(r'^appointment_types/(?P<pk>\d+)/edit$', views.appointment_type.AppointmentTypeEditView.as_view(), name='web.views.appointment_type_edit'),
-    url(r'^appointment_types/(?P<pk>\d+)/delete$', views.appointment_type.AppointmentTypeDeleteView.as_view(), name='web.views.appointment_type_delete'),
+    url(r'^appointment_types/$', views.appointment_type.AppointmentTypeListView.as_view(),
+        name='web.views.appointment_types'),
+    url(r'^appointment_types/add$', views.appointment_type.AppointmentTypeCreateView.as_view(),
+        name='web.views.appointment_type_add'),
+    url(r'^appointment_types/(?P<pk>\d+)/edit$', views.appointment_type.AppointmentTypeEditView.as_view(),
+        name='web.views.appointment_type_edit'),
+    url(r'^appointment_types/(?P<pk>\d+)/delete$', views.appointment_type.AppointmentTypeDeleteView.as_view(),
+        name='web.views.appointment_type_delete'),
 
     ####################
     #   APPOINTMENTS   #
@@ -83,7 +86,7 @@ urlpatterns = [
     url(r'^subjects/require_contact$', views.subject.subject_require_contact, name='web.views.subject_require_contact'),
     url(r'^subjects/voucher_expiry', views.subject.subject_voucher_expiry, name='web.views.subject_voucher_expiry'),
 
-    url(r'^subjects/add$', views.subject.subject_add, name='web.views.subject_add'),
+    url(r'^study/(?P<study_id>\d+)/subjects/add$', views.subject.subject_add, name='web.views.subject_add'),
     url(r'^subjects/subject_visit_details/(?P<id>\d+)$', views.subject.subject_visit_details,
         name='web.views.subject_visit_details'),
     url(r'^subjects/edit/(?P<id>\d+)$', views.subject.subject_edit, name='web.views.subject_edit'),
@@ -181,7 +184,8 @@ urlpatterns = [
     url(r'^mail_templates/add$', views.mails.mail_template_add, name='web.views.mail_template_add'),
     url(r'^mail_templates/(?P<pk>\d+)/edit$', views.mails.mail_template_edit, name='web.views.mail_template_edit'),
 
-    url(r'^mail_templates/(?P<pk>\d+)/delete$', views.mails.MailTemplatesDeleteView.as_view(), name='web.views.mail_template_delete'),
+    url(r'^mail_templates/(?P<pk>\d+)/delete$', views.mails.MailTemplatesDeleteView.as_view(),
+        name='web.views.mail_template_delete'),
     url(r'^mail_templates/(?P<mail_template_id>\d+)/generate/(?P<instance_id>\d+)$', views.mails.generate,
         name="web.views.mail_template_generate"),
     url(r'^mail_templates/print_vouchers$', views.mails.generate_for_vouchers,
@@ -253,13 +257,20 @@ urlpatterns = [
 
     url(r'^study/(?P<study_id>\d+)/edit', views.study.study_edit, name='web.views.edit_study'),
 
+    url(r'^study/(?P<study_id>\d+)/custom_study_subject_field/add', views.study.custom_study_subject_field_add,
+        name='web.views.custom_study_subject_field_add'),
+    url(r'^study/(?P<study_id>\d+)/custom_study_subject_field/(?P<field_id>\d+)/edit',
+        views.study.custom_study_subject_field_edit, name='web.views.custom_study_subject_field_edit'),
+    url(r'^study/(?P<study_id>\d+)/custom_study_subject_field/(?P<field_id>\d+)/delete',
+        views.study.custom_study_subject_field_delete, name='web.views.custom_study_subject_field_delete'),
+
     ####################
     #       EXPORT     #
     ####################
 
-    url(r'^export$', views.export.export, name='web.views.export'),
-    url(r'^export/csv/(?P<data_type>[A-z]+)$', views.export.export_to_csv, name='web.views.export_to_csv'),
-    url(r'^export/xls/(?P<data_type>[A-z]+)$', views.export.export_to_excel, name='web.views.export_to_excel'),
+    url(r'^study/(?P<study_id>\d+)export$', views.export.export, name='web.views.export'),
+    url(r'^study/(?P<study_id>\d+)export/csv/(?P<data_type>[A-z]+)$', views.export.export_to_csv, name='web.views.export_to_csv'),
+    url(r'^study/(?P<study_id>\d+)export/xls/(?P<data_type>[A-z]+)$', views.export.export_to_excel, name='web.views.export_to_excel'),
 
     ####################
     #       CONFIGURATION     #
diff --git a/smash/web/views/export.py b/smash/web/views/export.py
index f6cd6b738cf9de931e7bf59acee72a5ddf1526d0..8d69c70ada59bbc7fe88fac8e546a19afe98a2bb 100644
--- a/smash/web/views/export.py
+++ b/smash/web/views/export.py
@@ -1,36 +1,42 @@
 # coding=utf-8
 import csv
+import re
+from distutils.util import strtobool
 
 import django_excel as excel
 from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
+
 from ..utils import get_client_ip
 from .notifications import get_today_midnight_date
 from web.decorators import PermissionDecorator
-from . import e500_error, wrap_response
-from ..models import Subject, StudySubject, Appointment, ConfigurationItem, Worker, Provenance, Visit
+from web.models import Subject, StudySubject, Appointment, ConfigurationItem, Study, Worker, Provenance, Visit
 from web.models.constants import VISIT_SHOW_VISIT_NUMBER_FROM_ZERO
-from distutils.util import strtobool
+from web.models.custom_data import CustomStudySubjectField
+from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id
 from web.templatetags.filters import display_visit_number
-import re
+from . import e500_error, wrap_response
+from .notifications import get_today_midnight_date
 
 
 @PermissionDecorator('export_subjects', 'subject')
-def export_to_csv(request, data_type="subjects"):
+def export_to_csv(request, study_id, data_type="subjects"):
+    study = get_object_or_404(Study, id=study_id)
     # Create the HttpResponse object with the appropriate CSV header.
     selected_fields = request.GET.get('fields', None)
-    response = HttpResponse(content_type='text/csv')
+    response = HttpResponse(content_type='text/csv; charset=utf-8')
     response['Content-Disposition'] = 'attachment; filename="' + data_type + '-' + get_today_midnight_date().strftime(
         "%Y-%m-%d") + '.csv"'
 
     if data_type == "subjects":
-        data = get_subjects_as_array(selected_fields=selected_fields)
+        data = get_subjects_as_array(study, selected_fields=selected_fields)
     elif data_type == "appointments":
         data = get_appointments_as_array(selected_fields=selected_fields)
     else:
         return e500_error(request)
     writer = csv.writer(response, quotechar=str('"'), quoting=csv.QUOTE_ALL)
     for row in data:
-        writer.writerow([s.encode("utf-8") for s in row])
+        writer.writerow(row)
 
     worker = Worker.get_by_user(request.user)
     ip = get_client_ip(request)
@@ -46,11 +52,12 @@ def export_to_csv(request, data_type="subjects"):
 
 
 @PermissionDecorator('export_subjects', 'subject')
-def export_to_excel(request, data_type="subjects"):
+def export_to_excel(request, study_id, data_type="subjects"):
+    study = get_object_or_404(Study, id=study_id)
     selected_fields = request.GET.get('fields', None)
     filename = data_type + '-' + get_today_midnight_date().strftime("%Y-%m-%d") + ".xls"
     if data_type == "subjects":
-        data = get_subjects_as_array(selected_fields=selected_fields)
+        data = get_subjects_as_array(study, selected_fields=selected_fields)
     elif data_type == "appointments":
         data = get_appointments_as_array(selected_fields=selected_fields)
     else:
@@ -73,6 +80,9 @@ def export_to_excel(request, data_type="subjects"):
 
 
 class CustomField:
+    name = ''
+    verbose_name = ''
+
     def __init__(self, dictionary):
         for k, v in list(dictionary.items()):
             setattr(self, k, v)
@@ -109,7 +119,7 @@ def filter_fields_from_selected_fields(fields, selected_fields):
     return [field for field in fields if field.name in selected_fields]
 
 
-def get_default_subject_fields():
+def get_default_subject_fields(study: Study):
     visit_from_zero = ConfigurationItem.objects.get(type=VISIT_SHOW_VISIT_NUMBER_FROM_ZERO).value
     visit_from_zero = strtobool(visit_from_zero)
     subject_fields = []
@@ -129,12 +139,15 @@ def get_default_subject_fields():
                     field.verbose_name = 'Virus {} RT-PCR date'.format(display_visit_number(number))
             subject_fields.append(field)
     subject_fields.append(DROP_OUT_FIELD)
+    for custom_field in CustomStudySubjectField.objects.filter(study=study).all():
+        subject_fields.append(
+            CustomField({'verbose_name': custom_field.name, 'name': get_study_subject_field_id(custom_field)}))
     return subject_fields
 
 
-def get_subjects_as_array(selected_fields=None):
+def get_subjects_as_array(study: Study, selected_fields: str = None):
     result = []
-    subject_fields = get_default_subject_fields()
+    subject_fields = get_default_subject_fields(study)
     subject_fields = filter_fields_from_selected_fields(subject_fields, selected_fields)
 
     field_names = [field.verbose_name for field in subject_fields]  # faster than loop
@@ -147,9 +160,18 @@ def get_subjects_as_array(selected_fields=None):
     return result
 
 
-def subject_to_row_for_fields(study_subject, subject_fields):
+def subject_to_row_for_fields(study_subject: StudySubject, subject_fields):
     row = []
+    custom_fields = CustomStudySubjectField.objects.filter(study=study_subject.study).all()
+
     for field in subject_fields:
+        cell = None
+        for custom_field in custom_fields:
+            if get_study_subject_field_id(custom_field) == field.name:
+                val = study_subject.get_custom_data_value(custom_field)
+                if val is not None:
+                    cell = val.value
+
         if field == DROP_OUT_FIELD:
             if not study_subject.resigned:
                 cell = False
@@ -227,8 +249,10 @@ def get_appointments_as_array(selected_fields=None):
 
 
 @PermissionDecorator('export_subjects', 'subject')
-def export(request):
+def export(request, study_id):
+    study = get_object_or_404(Study, id=study_id)
     return wrap_response(request, 'export/index.html', {
-        'subject_fields': get_default_subject_fields(),
-        'appointment_fields': get_appointment_fields()[0]
+        'subject_fields': get_default_subject_fields(study),
+        'appointment_fields': get_appointment_fields()[0],
+        'study_id': study_id
     })
diff --git a/smash/web/views/study.py b/smash/web/views/study.py
index 7edc1d43b5fd319a3cd29046e8c5320c6a60ea20..7060a98284ba20e9b8f99b9795c0a31591d2311f 100644
--- a/smash/web/views/study.py
+++ b/smash/web/views/study.py
@@ -4,13 +4,17 @@ import logging
 from django.contrib import messages
 from django.shortcuts import redirect, get_object_or_404
 
-from web.forms import StudyColumnsEditForm, StudyEditForm, StudyNotificationParametersEditForm, StudyRedCapColumnsEditForm
+from web.decorators import PermissionDecorator
+from web.forms import StudyColumnsEditForm, StudyEditForm, StudyNotificationParametersEditForm, \
+    StudyRedCapColumnsEditForm
+from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldAddForm, CustomStudySubjectFieldEditForm
 from web.models import Study
+from web.models.custom_data import CustomStudySubjectField
 from web.views import wrap_response
-from web.decorators import PermissionDecorator
 
 logger = logging.getLogger(__name__)
 
+
 @PermissionDecorator('change_study', 'configuration')
 def study_edit(request, study_id):
     study = get_object_or_404(Study, id=study_id)
@@ -22,8 +26,11 @@ def study_edit(request, study_id):
         study_columns_form = StudyColumnsEditForm(request.POST, request.FILES, instance=study.columns,
                                                   prefix="columns")
         redcap_columns_form = StudyRedCapColumnsEditForm(request.POST, request.FILES, instance=study.redcap_columns,
-                                                  prefix="redcap")
-        if study_form.is_valid() and notifications_form.is_valid() and study_columns_form.is_valid() and redcap_columns_form.is_valid():
+                                                         prefix="redcap")
+        if study_form.is_valid() \
+                and notifications_form.is_valid() \
+                and study_columns_form.is_valid() \
+                and redcap_columns_form.is_valid():
             study_form.save()
             notifications_form.save()
             study_columns_form.save()
@@ -42,7 +49,7 @@ def study_edit(request, study_id):
                                                   prefix="columns")
 
         redcap_columns_form = StudyRedCapColumnsEditForm(instance=study.redcap_columns,
-                                                  prefix="redcap")
+                                                         prefix="redcap")
 
     return wrap_response(request, 'study/edit.html', {
         'study_form': study_form,
@@ -51,3 +58,48 @@ def study_edit(request, study_id):
         'study_columns_form': study_columns_form,
         'redcap_columns_form': redcap_columns_form
     })
+
+
+@PermissionDecorator('change_study', 'configuration')
+def custom_study_subject_field_add(request, study_id):
+    study = get_object_or_404(Study, id=study_id)
+    if request.method == 'POST':
+        field_form = CustomStudySubjectFieldAddForm(request.POST, study=study)
+        field_form.instance.study = study
+        if field_form.is_valid():
+            field_form.save()
+            return redirect('web.views.edit_study', study_id=study.id)
+    else:
+        field_form = CustomStudySubjectFieldAddForm(study=study)
+
+    return wrap_response(request, 'custom_study_subject_field/add.html', {
+        'form': field_form,
+        'study_id': study.id
+    })
+
+
+@PermissionDecorator('change_study', 'configuration')
+def custom_study_subject_field_edit(request, study_id, field_id):
+    study = get_object_or_404(Study, id=study_id)
+    field = get_object_or_404(CustomStudySubjectField, id=field_id)
+    if request.method == 'POST':
+        field_form = CustomStudySubjectFieldEditForm(request.POST, instance=field)
+        field_form.instance.study = study
+        if field_form.is_valid():
+            field_form.save()
+            return redirect('web.views.edit_study', study_id=study.id)
+    else:
+        field_form = CustomStudySubjectFieldEditForm(instance=field)
+
+    return wrap_response(request, 'custom_study_subject_field/edit.html', {
+        'form': field_form,
+        'study_id': study.id
+    })
+
+
+@PermissionDecorator('change_study', 'configuration')
+def custom_study_subject_field_delete(request, study_id, field_id):
+    study = get_object_or_404(Study, id=study_id)
+    field = get_object_or_404(CustomStudySubjectField, id=field_id)
+    field.delete()
+    return redirect('web.views.edit_study', study_id=study.id)
diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py
index 6922008ba35fd2140b4f9444f4c07146f4807cb5..2b0ddce56b84551a39bbfca87ad2d4bcc067c5e3 100644
--- a/smash/web/views/subject.py
+++ b/smash/web/views/subject.py
@@ -5,10 +5,11 @@ from django.contrib import messages
 from django.shortcuts import redirect, get_object_or_404
 from ..utils import get_client_ip
 from web.decorators import PermissionDecorator
+from web.models.custom_data import CustomStudySubjectField
 from . import wrap_response
 from ..forms import VisitDetailForm, SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm
 from ..models import StudySubject, MailTemplate, Worker, Study, Provenance, Subject
-from ..models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES
+from ..models.constants import GLOBAL_STUDY_ID, SUBJECT_TYPE_CHOICES, FILE_STORAGE
 from ..models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \
     SUBJECT_LIST_VOUCHER_EXPIRY, SUBJECT_LIST_CHOICES
 
@@ -20,6 +21,7 @@ def subject_list(request, subject_list_type):
         'list_type': subject_list_type,
         'worker': Worker.get_by_user(request.user),
         'list_description': SUBJECT_LIST_CHOICES[subject_list_type],
+        'study_id': GLOBAL_STUDY_ID,
     }
     return wrap_response(request, 'subjects/index.html', context)
 
@@ -29,17 +31,17 @@ def subjects(request):
 
 
 @PermissionDecorator('add_subject', 'subject')
-def subject_add(request):
-    study = Study.objects.filter(id=GLOBAL_STUDY_ID)[0]
+def subject_add(request, study_id):
+    study = get_object_or_404(Study, id=study_id)
     if request.method == 'POST':
         study_subject_form = StudySubjectAddForm(request.POST, request.FILES, prefix="study_subject", user=request.user,
                                                  study=study)
-
         subject_form = SubjectAddForm(request.POST, request.FILES, prefix="subject")
         if study_subject_form.is_valid() and subject_form.is_valid():
             subject = subject_form.save()
             study_subject_form.instance.subject_id = subject.id
-            study_subject_form.save()
+            study_subject = study_subject_form.save()
+            persist_custom_file_fields(request, study_subject)
             messages.add_message(request, messages.SUCCESS, 'Subject created')
             return redirect('web.views.subject_edit', id=study_subject_form.instance.id)
         else:
@@ -75,7 +77,7 @@ def subject_edit(request, id):
     ip = get_client_ip(request)
     if request.method == 'POST':
         study_subject_form = StudySubjectEditForm(request.POST, request.FILES, instance=study_subject,
-                                                  was_resigned=was_resigned, prefix="study_subject", 
+                                                  was_resigned=was_resigned, prefix="study_subject",
                                                   endpoint_was_reached=endpoint_was_reached)
         subject_form = SubjectEditForm(request.POST, request.FILES, instance=study_subject.subject,
                                        was_dead=was_dead, prefix="subject"
@@ -83,6 +85,9 @@ def subject_edit(request, id):
         if study_subject_form.is_valid() and subject_form.is_valid():
             study_subject_form.save()
             subject_form.save()
+
+            persist_custom_file_fields(request, study_subject)
+
             # check if subject was marked as dead or resigned
             if 'type' in study_subject_form.changed_data and old_type != study_subject_form.cleaned_data['type']:
                 worker = Worker.get_by_user(request.user)
@@ -153,6 +158,16 @@ def subject_edit(request, id):
     })
 
 
+def persist_custom_file_fields(request, study_subject):
+    for key in request.FILES:
+        if key.startswith('study_subject-custom_field-'):
+            file_folder = key.replace('study_subject-', '')
+            field_id = int(file_folder.replace("custom_field-", ""))
+            file = FILE_STORAGE.save(name=file_folder + "/" + request.FILES[key].name,
+                                     content=request.FILES[key])
+            study_subject.set_custom_data_value(CustomStudySubjectField.objects.get(pk=field_id), file)
+
+
 def subject_visit_details(request, id):
     study_subject_to_be_viewed = get_object_or_404(StudySubject, id=id)
     visits = study_subject_to_be_viewed.visit_set.order_by("-visit_number").all()
diff --git a/smash/web/views/uploaded_files.py b/smash/web/views/uploaded_files.py
index f7f5f3dfa6936bac2592fdf16829947be3297eaa..de6f7c6b1e2901385f832f13ed014c687ba983ed 100644
--- a/smash/web/views/uploaded_files.py
+++ b/smash/web/views/uploaded_files.py
@@ -18,6 +18,6 @@ def path_to_filename(path):
 def download(request):
     if request.GET and request.GET.get('file'):
         path = FILE_STORAGE.location + "/" + request.GET.get('file')
-        response = HttpResponse(FileWrapper(open(path, 'r')), content_type='application/force-download')
+        response = HttpResponse(FileWrapper(open(path, 'rb')), content_type='application/force-download')
         response['Content-Disposition'] = 'attachment; filename=%s' % path_to_filename(path)
         return response