diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index 89e2c9c19c2eb5aa958a17165fe12d440b842c58..5f6cd27120fd039257378a8b3b83cedcc55b1d8e 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -10,6 +10,15 @@ def bool_to_yes_no(val): return "NO" +def bool_to_yes_no_null(val): + if val is None: + return "N/A" + if val: + return "YES" + else: + return "NO" + + def flying_team_to_str(flying_team): result = "" if flying_team is not None: diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index c31d13903ab43b26fa7db0fd1f9c570d28e5cb5e..cd7b5ac5dd59429c1457ea3fd30d5f90a4b87c0a 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -1,12 +1,12 @@ import logging -from django.urls import reverse from django.db.models import Count, Case, When, Min, Max from django.db.models import Q from django.http import JsonResponse +from django.urls import reverse 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 + serialize_date, serialize_datetime, get_filters_for_data_table_request, bool_to_yes_no_null from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study, ContactAttempt from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ @@ -73,12 +73,25 @@ def get_subject_columns(request, subject_list_type): add_column(result, "Next of keen", "next_of_keen_name", subject_columns, "string_filter") add_column(result, "Next of keen phone", "next_of_keen_phone", subject_columns, "string_filter") add_column(result, "Next of keen address", "next_of_keen_address", subject_columns, "string_filter") - add_column(result, "Brain donation agreement", "brain_donation_agreement", study_subject_columns, "yes_no_filter", study.columns) + add_column(result, "Brain donation agreement", "brain_donation_agreement", study_subject_columns, "yes_no_filter", + study.columns) add_column(result, "Excluded", "excluded", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Info sent", "information_sent", study_subject_columns, "yes_no_filter", study.columns) + + add_column(result, "Visit 1 virus", "virus_test_1", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 1 virus date", "virus_test_1_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 2 virus", "virus_test_2", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 2 virus date", "virus_test_2_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 3 virus", "virus_test_3", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 3 virus date", "virus_test_3_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 4 virus", "virus_test_4", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 4 virus date", "virus_test_4_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 5 virus", "virus_test_5", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 5 virus date", "virus_test_5_updated", study_subject_columns, None, study.columns) + add_column(result, "Type", "type", study_subject_columns, "type_filter", study.columns) add_column(result, "Edit", "edit", None, None, sortable=False) - for visit_number in range(1, study.visits_to_show_in_subject_list+1): + for visit_number in range(1, study.visits_to_show_in_subject_list + 1): visit_key = "visit_" + str(visit_number) add_column(result, "Visit " + str(visit_number), visit_key, None, "visit_filter", visible_param=study_subject_list.visits) @@ -133,7 +146,8 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co else: pattern = column_filters[u'screening_number'] result = subjects_to_be_ordered.all() - result = sorted(result, key=lambda t: t.sort_matched_screening_first(pattern, reverse=order_direction == '-'), reverse = order_direction == '-' ) + result = sorted(result, key=lambda t: t.sort_matched_screening_first(pattern, reverse=order_direction == '-'), + reverse=order_direction == '-') elif order_column == "default_location": result = subjects_to_be_ordered.order_by(order_direction + 'default_location') elif order_column == "flying_team": @@ -173,6 +187,26 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co elif str(order_column).startswith("visit_"): visit_number = get_visit_number_from_visit_x_string(order_column) result = order_by_visit(subjects_to_be_ordered, order_direction, visit_number) + elif order_column == "virus_test_1": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_1') + elif order_column == "virus_test_2": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_2') + elif order_column == "virus_test_3": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_3') + elif order_column == "virus_test_4": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_4') + elif order_column == "virus_test_5": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_5') + elif order_column == "virus_test_1_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_1_updated') + elif order_column == "virus_test_2_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_2_updated') + elif order_column == "virus_test_3_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_3_updated') + elif order_column == "virus_test_4_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_4_updated') + elif order_column == "virus_test_5_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_5_updated') else: logger.warn("Unknown sort column: " + str(order_column)) return result @@ -272,6 +306,31 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): result = result.filter(resigned=(value == "true")) elif column == "endpoint_reached": result = result.filter(endpoint_reached=(value == "true")) + elif column == "virus_test_1": + if value == "null": + result = result.filter(virus_test_1__isnull=True) + else: + result = result.filter(virus_test_1=(value == "true")) + elif column == "virus_test_2": + if value == "null": + result = result.filter(virus_test_2__isnull=True) + else: + result = result.filter(virus_test_2=(value == "true")) + elif column == "virus_test_3": + if value == "null": + result = result.filter(virus_test_3__isnull=True) + else: + result = result.filter(virus_test_3=(value == "true")) + elif column == "virus_test_4": + if value == "null": + result = result.filter(virus_test_4__isnull=True) + else: + result = result.filter(virus_test_4=(value == "true")) + elif column == "virus_test_5": + if value == "null": + result = result.filter(virus_test_5__isnull=True) + else: + result = result.filter(virus_test_5=(value == "true")) elif column == "brain_donation_agreement": result = result.filter(brain_donation_agreement=(value == "true")) elif column == "postponed": @@ -358,6 +417,7 @@ def types(request): "types": data }) + def serialize_subject(study_subject): location = location_to_str(study_subject.default_location) flying_team = flying_team_to_str(study_subject.flying_team) @@ -388,7 +448,7 @@ def serialize_subject(study_subject): status = "SHOULD_BE_IN_PROGRESS" else: status = "UPCOMING" - + appointment_types = ['{} ({})'.format(at.code, at.description) for at in visit.appointment_types.all()] if len(appointment_types) == 0: appointment_types = ['No appointment types set.'] @@ -436,6 +496,16 @@ def serialize_subject(study_subject): "dead": bool_to_yes_no(study_subject.subject.dead), "resigned": bool_to_yes_no(study_subject.resigned), "endpoint_reached": bool_to_yes_no(study_subject.endpoint_reached), + "virus_test_1": bool_to_yes_no_null(study_subject.virus_test_1), + "virus_test_2": bool_to_yes_no_null(study_subject.virus_test_2), + "virus_test_3": bool_to_yes_no_null(study_subject.virus_test_3), + "virus_test_4": bool_to_yes_no_null(study_subject.virus_test_4), + "virus_test_5": bool_to_yes_no_null(study_subject.virus_test_5), + "virus_test_1_updated": study_subject.virus_test_1_updated, + "virus_test_2_updated": study_subject.virus_test_2_updated, + "virus_test_3_updated": study_subject.virus_test_3_updated, + "virus_test_4_updated": study_subject.virus_test_4_updated, + "virus_test_5_updated": study_subject.virus_test_5_updated, "postponed": bool_to_yes_no(study_subject.postponed), "brain_donation_agreement": bool_to_yes_no(study_subject.brain_donation_agreement), "excluded": bool_to_yes_no(study_subject.excluded), @@ -447,4 +517,4 @@ def serialize_subject(study_subject): "id": study_subject.id, "visits": serialized_visits, } - return result \ No newline at end of file + return result diff --git a/smash/web/forms/study_subject_forms.py b/smash/web/forms/study_subject_forms.py index 2bc880780226734cfa1653501960eb47bc8138b0..1f75992f6793598ef7d2c760af61af98ab90ab09 100644 --- a/smash/web/forms/study_subject_forms.py +++ b/smash/web/forms/study_subject_forms.py @@ -36,6 +36,17 @@ class StudySubjectAddForm(StudySubjectForm): self.study = get_study_from_args(kwargs) super(StudySubjectAddForm, self).__init__(*args, **kwargs) + self.fields['virus_test_1'].widget.attrs['readonly'] = True + self.fields['virus_test_2'].widget.attrs['readonly'] = True + self.fields['virus_test_3'].widget.attrs['readonly'] = True + self.fields['virus_test_4'].widget.attrs['readonly'] = True + self.fields['virus_test_5'].widget.attrs['readonly'] = True + self.fields['virus_test_1_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_2_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_3_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_4_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_5_updated'].widget.attrs['readonly'] = True + prepare_study_subject_fields(fields=self.fields, study=self.study) def save(self, commit=True): @@ -128,6 +139,18 @@ class StudySubjectEditForm(StudySubjectForm): self.fields['resigned'].disabled = was_resigned self.fields['endpoint_reached'].disabled = endpoint_was_reached + + self.fields['virus_test_1'].widget.attrs['readonly'] = True + self.fields['virus_test_2'].widget.attrs['readonly'] = True + self.fields['virus_test_3'].widget.attrs['readonly'] = True + self.fields['virus_test_4'].widget.attrs['readonly'] = True + self.fields['virus_test_5'].widget.attrs['readonly'] = True + self.fields['virus_test_1_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_2_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_3_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_4_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_5_updated'].widget.attrs['readonly'] = True + prepare_study_subject_fields(fields=self.fields, study=self.study) def clean(self): @@ -181,6 +204,17 @@ def prepare_study_subject_fields(fields, study): prepare_field(fields, study.columns, 'previously_in_study') prepare_field(fields, study.columns, 'voucher_types') + prepare_field(fields, study.columns, 'virus_test_1') + prepare_field(fields, study.columns, 'virus_test_2') + prepare_field(fields, study.columns, 'virus_test_3') + prepare_field(fields, study.columns, 'virus_test_4') + prepare_field(fields, study.columns, 'virus_test_5') + prepare_field(fields, study.columns, 'virus_test_1_updated') + prepare_field(fields, study.columns, 'virus_test_2_updated') + prepare_field(fields, study.columns, 'virus_test_3_updated') + prepare_field(fields, study.columns, 'virus_test_4_updated') + prepare_field(fields, study.columns, 'virus_test_5_updated') + def validate_subject_screening_number(self, cleaned_data): if self.study.columns.resign_reason: diff --git a/smash/web/migrations/0159_configurationitem_email_items_for_redcap.py b/smash/web/migrations/0159_configurationitem_email_items_for_redcap.py new file mode 100644 index 0000000000000000000000000000000000000000..f496a5a3f92489be49b66ac9dbac156eb07e46db --- /dev/null +++ b/smash/web/migrations/0159_configurationitem_email_items_for_redcap.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-04-04 09:43 +from __future__ import unicode_literals + +from django.db import migrations + +from web.models.constants import RED_CAP_LANGUAGE_4_FIELD_TYPE, RED_CAP_LANGUAGE_3_FIELD_TYPE, \ + RED_CAP_LANGUAGE_2_FIELD_TYPE, RED_CAP_LANGUAGE_1_FIELD_TYPE, RED_CAP_MPOWER_ID_FIELD_TYPE, RED_CAP_DEAD_FIELD_TYPE, \ + RED_CAP_SEX_FIELD_TYPE, RED_CAP_DATE_BORN_FIELD_TYPE, RED_CAP_ND_NUMBER_FIELD_TYPE, RED_CAP_VIRUS_FIELD_TYPE + + +def create_item(apps, type, value, name): + # We can't import the ConfigurationItem model directly as it may be a newer + # version than this migration expects. We use the historical version. + ConfigurationItem = apps.get_model("web", "ConfigurationItem") + item = ConfigurationItem.objects.create() + item.type = type + item.value = value + item.name = name + item.save() + + +def configuration_items(apps, schema_editor): + create_item(apps, RED_CAP_LANGUAGE_4_FIELD_TYPE, "dm_language_4", + "Redcap field for language 4") + create_item(apps, RED_CAP_LANGUAGE_3_FIELD_TYPE, "dm_language_3", + "Redcap field for language 3") + create_item(apps, RED_CAP_LANGUAGE_2_FIELD_TYPE, "dm_language_2", + "Redcap field for language 2") + create_item(apps, RED_CAP_LANGUAGE_1_FIELD_TYPE, "dm_language_1", + "Redcap field for language 1") + create_item(apps, RED_CAP_MPOWER_ID_FIELD_TYPE, "dm_mpowerid", + "Redcap field for mPowerId") + create_item(apps, RED_CAP_DEAD_FIELD_TYPE, "dm_death", + "Redcap field for deceased") + create_item(apps, RED_CAP_SEX_FIELD_TYPE, "cdisc_dm_sex", + "Redcap field for sex") + create_item(apps, RED_CAP_DATE_BORN_FIELD_TYPE, "cdisc_dm_brthdtc", + "Redcap field for birth date") + create_item(apps, RED_CAP_ND_NUMBER_FIELD_TYPE, "cdisc_dm_usubjd", + "Redcap field for subject id") + create_item(apps, RED_CAP_VIRUS_FIELD_TYPE, "", + "Redcap field for virus test result") + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0158_configurationitem_email_items'), + ] + + operations = [ + migrations.RunPython(configuration_items), + ] diff --git a/smash/web/migrations/0160_auto_20200415_1101.py b/smash/web/migrations/0160_auto_20200415_1101.py new file mode 100644 index 0000000000000000000000000000000000000000..f00f970dcc844171d9c43b7743823b5e4b2772bb --- /dev/null +++ b/smash/web/migrations/0160_auto_20200415_1101.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-15 11:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0159_configurationitem_email_items_for_redcap'), + ] + + operations = [ + migrations.AddField( + model_name='studycolumns', + name='virus_test_1', + field=models.BooleanField(default=False, verbose_name=b'Visit 1 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_1_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 1 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_2', + field=models.BooleanField(default=False, verbose_name=b'Visit 2 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_2_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 2 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_3', + field=models.BooleanField(default=False, verbose_name=b'Visit 3 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_3_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 3 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_4', + field=models.BooleanField(default=False, verbose_name=b'Visit 4 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_4_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 4 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_5', + field=models.BooleanField(default=False, verbose_name=b'Visit 5 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_5_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 5 virus results date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_1', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 1 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_1_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 1 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_2', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 2 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_2_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 2 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_3', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 3 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_3_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 3 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_4', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 4 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_4_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 4 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_5', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 5 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_5_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 5 virus result date'), + ), + ] diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py index e83ddc83fa6dc4ea020850952d9fb151f97ab3d4..b512e714efac9136e8c4d9d9bc048f77d065458f 100644 --- a/smash/web/models/constants.py +++ b/smash/web/models/constants.py @@ -10,6 +10,11 @@ SEX_CHOICES = ( (SEX_CHOICES_MALE, 'Male'), (SEX_CHOICES_FEMALE, 'Female'), ) +BOOL_CHOICES_WITH_NONE = ( + (True, 'Yes'), + (False, 'No'), + (None, 'N/A'), +) SUBJECT_TYPE_CHOICES_CONTROL = 'C' SUBJECT_TYPE_CHOICES_PATIENT = 'P' SUBJECT_TYPE_CHOICES = { @@ -47,6 +52,17 @@ KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE = "KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_ KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE = "KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE" KIT_DAILY_EMAIL_TIME_FORMAT_TYPE = "KIT_DAILY_EMAIL_TIME_FORMAT_TYPE" +RED_CAP_LANGUAGE_4_FIELD_TYPE = 'RED_CAP_LANGUAGE_4_FIELD_TYPE' +RED_CAP_LANGUAGE_3_FIELD_TYPE = 'RED_CAP_LANGUAGE_3_FIELD_TYPE' +RED_CAP_LANGUAGE_2_FIELD_TYPE = 'RED_CAP_LANGUAGE_2_FIELD_TYPE' +RED_CAP_LANGUAGE_1_FIELD_TYPE = 'RED_CAP_LANGUAGE_1_FIELD_TYPE' +RED_CAP_MPOWER_ID_FIELD_TYPE = 'RED_CAP_MPOWER_ID_FIELD_TYPE' +RED_CAP_DEAD_FIELD_TYPE = 'RED_CAP_DEAD_FIELD_TYPE' +RED_CAP_SEX_FIELD_TYPE = 'RED_CAP_SEX_FIELD_TYPE' +RED_CAP_DATE_BORN_FIELD_TYPE = 'RED_CAP_DATE_BORN_FIELD_TYPE' +RED_CAP_ND_NUMBER_FIELD_TYPE = 'RED_CAP_ND_NUMBER_FIELD_TYPE' +RED_CAP_VIRUS_FIELD_TYPE = 'RED_CAP_VIRUS_FIELD_TYPE' + MAIL_TEMPLATE_CONTEXT_SUBJECT = 'S' MAIL_TEMPLATE_CONTEXT_APPOINTMENT = 'A' MAIL_TEMPLATE_CONTEXT_VISIT = 'V' diff --git a/smash/web/models/study_columns.py b/smash/web/models/study_columns.py index 7d20b3913d40a71f72fc0b70327a20084e83a158..d2e769ca52216cf9560933ec89d9902f3092220e 100644 --- a/smash/web/models/study_columns.py +++ b/smash/web/models/study_columns.py @@ -12,69 +12,69 @@ class StudyColumns(models.Model): ) datetime_contact_reminder = models.BooleanField( - default=True, - verbose_name='Please make a contact on' - ) + default=True, + verbose_name='Please make a contact on' + ) type = models.BooleanField( - default=True, - verbose_name='Type' - ) + default=True, + verbose_name='Type' + ) default_location = models.BooleanField( - default=True, - verbose_name='Default appointment location', - ) + default=True, + verbose_name='Default appointment location', + ) flying_team = models.BooleanField( - default=True, - verbose_name='Default flying team location (if applicable)', - ) + default=True, + verbose_name='Default flying team location (if applicable)', + ) screening_number = models.BooleanField( - default=True, - verbose_name='Screening number', - ) + default=True, + verbose_name='Screening number', + ) nd_number = models.BooleanField( - default=True, - verbose_name='ND number', - ) + default=True, + verbose_name='ND number', + ) mpower_id = models.BooleanField( - default=True, - verbose_name='MPower ID' - ) + default=True, + verbose_name='MPower ID' + ) comments = models.BooleanField( - default=True, - verbose_name='Comments' - ) + default=True, + verbose_name='Comments' + ) referral = models.BooleanField( - default=True, - verbose_name='Referred by' - ) + default=True, + verbose_name='Referred by' + ) diagnosis = models.BooleanField( - default=True, - verbose_name='Diagnosis' - ) + default=True, + verbose_name='Diagnosis' + ) year_of_diagnosis = models.BooleanField( - default=True, - verbose_name='Year of diagnosis (YYYY)' - ) + default=True, + verbose_name='Year of diagnosis (YYYY)' + ) information_sent = models.BooleanField( - default=True, - verbose_name='Information sent', - ) + default=True, + verbose_name='Information sent', + ) pd_in_family = models.BooleanField( - default=True, - verbose_name='PD in family', - ) + default=True, + verbose_name='PD in family', + ) resigned = models.BooleanField( - default=True, - verbose_name='Resigned', - ) + default=True, + verbose_name='Resigned', + ) resign_reason = models.BooleanField( - default=True, - verbose_name='Resign reason' - ) + default=True, + verbose_name='Resign reason' + ) excluded = models.BooleanField(default=False, verbose_name='Excluded') @@ -83,41 +83,82 @@ class StudyColumns(models.Model): resign_reason = models.BooleanField(default=True, verbose_name='Endpoint reached comments') referral_letter = models.BooleanField( - default=False, - verbose_name='Referral letter' - ) + default=False, + verbose_name='Referral letter' + ) health_partner = models.BooleanField( - default=False, - verbose_name='Health partner' - ) + default=False, + verbose_name='Health partner' + ) health_partner_feedback_agreement = models.BooleanField( - default=False, - verbose_name='Agrees to give information to referral' - ) + default=False, + verbose_name='Agrees to give information to referral' + ) screening = models.BooleanField( - default=False, - verbose_name='Screening' - ) + default=False, + verbose_name='Screening' + ) previously_in_study = models.BooleanField( - default=False, - verbose_name='Previously in PDP study', - ) + default=False, + verbose_name='Previously in PDP study', + ) voucher_types = models.BooleanField( - default=False, - verbose_name='Voucher types', - ) + default=False, + verbose_name='Voucher types', + ) vouchers = models.BooleanField( - default=False, - verbose_name='Vouchers', - ) + default=False, + verbose_name='Vouchers', + ) brain_donation_agreement = models.BooleanField( - default=False, - verbose_name='Brain donation agreement', - ) + default=False, + verbose_name='Brain donation agreement', + ) + + virus_test_1 = models.BooleanField( + default=False, + verbose_name='Visit 1 virus results', + ) + virus_test_2 = models.BooleanField( + default=False, + verbose_name='Visit 2 virus results', + ) + virus_test_3 = models.BooleanField( + default=False, + verbose_name='Visit 3 virus results', + ) + virus_test_4 = models.BooleanField( + default=False, + verbose_name='Visit 4 virus results', + ) + virus_test_5 = models.BooleanField( + default=False, + verbose_name='Visit 5 virus results', + ) + virus_test_1_updated = models.BooleanField( + default=False, + verbose_name='Visit 1 virus results date', + ) + virus_test_2_updated = models.BooleanField( + default=False, + verbose_name='Visit 2 virus results date', + ) + virus_test_3_updated = models.BooleanField( + default=False, + verbose_name='Visit 3 virus results date', + ) + virus_test_4_updated = models.BooleanField( + default=False, + verbose_name='Visit 4 virus results date', + ) + virus_test_5_updated = models.BooleanField( + default=False, + verbose_name='Visit 5 virus results date', + ) diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 22a5b0b8fe77518abb0092587531f0b715413633..d802d8debb0e5d87dc0a54756bc9152349855cd9 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -1,23 +1,25 @@ # coding=utf-8 import logging import re -from django.db import models -from web.models import VoucherType, Appointment, Location, Visit, Provenance -from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE +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 + logger = logging.getLogger(__name__) -class StudySubject(models.Model): +class StudySubject(models.Model): class Meta: app_label = 'web' @property def provenances(self): - return Provenance.objects.filter(modified_table=StudySubject._meta.db_table, modified_table_id=self.id).order_by('-modification_date') + return Provenance.objects.filter(modified_table=StudySubject._meta.db_table, + modified_table_id=self.id).order_by('-modification_date') def finish_all_visits(self): visits = Visit.objects.filter(subject=self, is_finished=False) @@ -168,9 +170,9 @@ class StudySubject(models.Model): default=None, ) brain_donation_agreement = models.BooleanField( - default=False, - verbose_name='Brain donation agreement', - ) + default=False, + verbose_name='Brain donation agreement', + ) resigned = models.BooleanField( verbose_name='Resigned', @@ -196,42 +198,84 @@ class StudySubject(models.Model): editable=True ) endpoint_reached_reason = models.TextField(max_length=2000, - blank=True, - verbose_name='Endpoint reached comments' - ) + blank=True, + verbose_name='Endpoint reached comments' + ) + + virus_test_1 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 1 virus result', + default=None + ) + virus_test_2 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 2 virus result', + default=None + ) + virus_test_3 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 3 virus result', + default=None + ) + virus_test_4 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 4 virus result', + default=None + ) + virus_test_5 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 5 virus result', + default=None + ) + virus_test_1_updated = models.DateField(verbose_name='Visit 1 virus result date', + blank=True, + null=True, + ) + virus_test_2_updated = models.DateField(verbose_name='Visit 2 virus result date', + blank=True, + null=True, + ) + virus_test_3_updated = models.DateField(verbose_name='Visit 3 virus result date', + blank=True, + null=True, + ) + virus_test_4_updated = models.DateField(verbose_name='Visit 4 virus result date', + blank=True, + null=True, + ) + virus_test_5_updated = models.DateField(verbose_name='Visit 5 virus result date', + blank=True, + null=True, + ) def sort_matched_screening_first(self, pattern, reverse=False): - if self.screening_number is None: - return None - - parts = self.screening_number.split(';') - matches, reminder = [], [] - - try: - for part in parts: - chunks = part.strip().split('-') - if len(chunks) == 2: - letter, number = chunks - try: - tupl = (letter, int(number)) - except ValueError: #better than isdigit because isdigit fails with negative numbers and others - tupl = (letter, number) - else: - logger.warn('There are {} chunks in some parts of this screening_number: |{}|.'.format( - len(chunks), self.screening_number)) - tupl = (part.strip(), None) - if pattern is not None and pattern in part: - matches.append(tupl) - else: - reminder.append(tupl) - except: #if the format is not the expected format - matches = parts - - return matches + sorted(reminder, reverse=reverse) + if self.screening_number is None: + return None + + parts = self.screening_number.split(';') + matches, reminder = [], [] + + try: + for part in parts: + chunks = part.strip().split('-') + if len(chunks) == 2: + letter, number = chunks + try: + tupl = (letter, int(number)) + except ValueError: # better than isdigit because isdigit fails with negative numbers and others + tupl = (letter, number) + else: + logger.warn('There are {} chunks in some parts of this screening_number: |{}|.'.format( + len(chunks), self.screening_number)) + tupl = (part.strip(), None) + if pattern is not None and pattern in part: + matches.append(tupl) + else: + reminder.append(tupl) + except: # if the format is not the expected format + matches = parts + + return matches + sorted(reminder, reverse=reverse) @staticmethod def check_nd_number_regex(regex_str, study): - nd_numbers = StudySubject.objects.filter(study=study).exclude(nd_number__isnull=True).exclude(nd_number__exact='').all().values_list('nd_number', flat=True) + nd_numbers = StudySubject.objects.filter(study=study).exclude(nd_number__isnull=True).exclude( + nd_number__exact='').all().values_list('nd_number', flat=True) regex = re.compile(regex_str) for nd_number in nd_numbers: if regex.match(nd_number) is None: @@ -239,21 +283,20 @@ class StudySubject(models.Model): return True def can_schedule(self): - return not any([self.resigned, self.excluded, self.endpoint_reached, self.subject.dead]) + return not any([self.resigned, self.excluded, self.endpoint_reached, self.subject.dead]) @property def status(self): - if self.subject.dead: - return 'Deceased' - elif self.resigned: - return 'Resigned' - elif self.excluded: - return 'Excluded' - elif self.endpoint_reached: - return 'Endpoint Reached' - else: - return 'Normal' - + if self.subject.dead: + return 'Deceased' + elif self.resigned: + return 'Resigned' + elif self.excluded: + return 'Excluded' + elif self.endpoint_reached: + return 'Endpoint Reached' + else: + return 'Normal' def __str__(self): return "%s %s" % (self.subject.first_name, self.subject.last_name) @@ -261,12 +304,13 @@ class StudySubject(models.Model): def __unicode__(self): return "%s %s" % (self.subject.first_name, self.subject.last_name) -#SIGNALS + +# SIGNALS @receiver(post_save, sender=StudySubject) def set_as_resigned_or_excluded_or_endpoint_reached(sender, instance, **kwargs): - if instance.excluded: - instance.mark_as_excluded() - if instance.resigned: - instance.mark_as_resigned() - if instance.endpoint_reached: - instance.mark_as_endpoint_reached() \ No newline at end of file + if instance.excluded: + instance.mark_as_excluded() + if instance.resigned: + instance.mark_as_resigned() + if instance.endpoint_reached: + instance.mark_as_endpoint_reached() diff --git a/smash/web/redcap_connector.py b/smash/web/redcap_connector.py index 4e1da4fa683708eb0a6c28dc46c7ae214622f143..793e0fa221ee88c8daffcd67d08c95d069bf7498 100644 --- a/smash/web/redcap_connector.py +++ b/smash/web/redcap_connector.py @@ -1,43 +1,24 @@ # coding=utf-8 import cStringIO +import datetime import json import logging -import pycurl import certifi +import pycurl import timeout_decorator +from django.conf import settings +from django.forms.models import model_to_dict from django_cron import CronJobBase, Schedule -from django.forms.models import model_to_dict -from web.models.constants import GLOBAL_STUDY_ID -from web.models import ConfigurationItem, StudySubject, Language, Study, StudyRedCapColumns +from web.models import ConfigurationItem, StudySubject, Language, AppointmentType, Appointment, Visit from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, \ - REDCAP_BASE_URL_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT + REDCAP_BASE_URL_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT, RED_CAP_LANGUAGE_4_FIELD_TYPE, RED_CAP_LANGUAGE_3_FIELD_TYPE, \ + RED_CAP_LANGUAGE_2_FIELD_TYPE, RED_CAP_LANGUAGE_1_FIELD_TYPE, RED_CAP_MPOWER_ID_FIELD_TYPE, RED_CAP_DEAD_FIELD_TYPE, \ + RED_CAP_SEX_FIELD_TYPE, RED_CAP_DATE_BORN_FIELD_TYPE, RED_CAP_ND_NUMBER_FIELD_TYPE, RED_CAP_VIRUS_FIELD_TYPE from web.models.inconsistent_subject import InconsistentField, InconsistentSubject from web.models.missing_subject import MissingSubject -RED_CAP_LANGUAGE_4_FIELD = 'dm_language_4' - -RED_CAP_LANGUAGE_3_FIELD = 'dm_language_3' - -RED_CAP_LANGUAGE_2_FIELD = 'dm_language_2' - -RED_CAP_LANGUAGE_1_FIELD = 'dm_language_1' - -# noinspection SpellCheckingInspection -RED_CAP_MPOWER_ID_FIELD = 'dm_mpowerid' - -RED_CAP_DEAD_FIELD = 'dm_death' - -# noinspection SpellCheckingInspection -RED_CAP_SEX_FIELD = 'cdisc_dm_sex' - -# noinspection SpellCheckingInspection -RED_CAP_DATE_BORN_FIELD = 'cdisc_dm_brthdtc' - -# noinspection SpellCheckingInspection -RED_CAP_ND_NUMBER_FIELD = 'cdisc_dm_usubjd' - logger = logging.getLogger(__name__) @@ -52,12 +33,18 @@ class RedcapSubject(object): def __init__(self): self.languages = [] + self.visits = [] def add_language(self, language): if language is not None: self.languages.append(language) +class RedcapVisit(object): + virus = None + visit_number = 0 + + def different_string(string1, string2): if string1 is None: string1 = "" @@ -83,6 +70,28 @@ class RedcapConnector(object): for language in languages: self.language_by_name[language.name.lower()] = language + self.date_born_field = ConfigurationItem.objects.get(type=RED_CAP_DATE_BORN_FIELD_TYPE).value + self.sex_field = ConfigurationItem.objects.get(type=RED_CAP_SEX_FIELD_TYPE).value + self.nd_number_field = ConfigurationItem.objects.get(type=RED_CAP_ND_NUMBER_FIELD_TYPE).value + self.dead_field = ConfigurationItem.objects.get(type=RED_CAP_DEAD_FIELD_TYPE).value + self.language_1_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_1_FIELD_TYPE).value + self.language_2_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_2_FIELD_TYPE).value + self.language_3_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_3_FIELD_TYPE).value + self.language_4_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_4_FIELD_TYPE).value + self.m_power_id_field = ConfigurationItem.objects.get(type=RED_CAP_MPOWER_ID_FIELD_TYPE).value + self.virus_field = ConfigurationItem.objects.get(type=RED_CAP_VIRUS_FIELD_TYPE).value + + self.date_born_field = "" + self.sex_field = "" + self.nd_number_field = "donor_id" + self.dead_field = "" + self.language_1_field = "" + self.language_2_field = "" + self.language_3_field = "" + self.language_4_field = "" + self.m_power_id_field = "" + self.virus_field = "sarscov2_status" + def find_missing(self): pid = self.get_project_id() redcap_version = self.get_redcap_version() @@ -157,6 +166,13 @@ class RedcapConnector(object): self.add_inconsistent(inconsistent) def find_inconsistent(self): + appointment_type_code_to_finish = getattr(settings, "IMPORT_APPOINTMENT_TYPE", None) + appointment_type_to_finish = None + if appointment_type_code_to_finish is not None: + appointment_types = AppointmentType.objects.filter(code=appointment_type_code_to_finish) + if len(appointment_types) > 0: + appointment_type_to_finish = appointment_types[0] + pid = self.get_project_id() redcap_version = self.get_redcap_version() @@ -173,9 +189,41 @@ class RedcapConnector(object): if red_cap_subject is not None: url = self.create_redcap_link(pid, redcap_version, subject) - subject = self.create_inconsistency_subject(red_cap_subject, subject, url) - if subject is not None: - result.append(subject) + + inconsistent_subject = self.create_inconsistency_subject(red_cap_subject, subject, url) + if inconsistent_subject is not None: + result.append(inconsistent_subject) + if appointment_type_to_finish is not None: + for visit in red_cap_subject.visits: + smasch_visits = Visit.objects.filter(visit_number=visit.visit_number, subject=subject) + smasch_appointments = Appointment.objects.filter(visit__in=smasch_visits, + appointment_types=appointment_type_to_finish, + status=Appointment.APPOINTMENT_STATUS_SCHEDULED) + for smasch_appointment in smasch_appointments: + smasch_appointment.mark_as_finished() + smasch_appointment.visit.is_finished = True + smasch_appointment.visit.save() + if visit.virus is not None: + if visit.visit_number == 1 and subject.virus_test_1 != visit.virus: + subject.virus_test_1 = visit.virus + subject.virus_test_1_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 2 and subject.virus_test_2 != visit.virus: + subject.virus_test_2 = visit.virus + subject.virus_test_2_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 3 and subject.virus_test_3 != visit.virus: + subject.virus_test_3 = visit.virus + subject.virus_test_3_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 4 and subject.virus_test_4 != visit.virus: + subject.virus_test_4 = visit.virus + subject.virus_test_4_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 5 and subject.virus_test_5 != visit.virus: + subject.virus_test_5 = visit.virus + subject.virus_test_5_updated = datetime.datetime.now() + subject.save() return result @@ -228,7 +276,7 @@ class RedcapConnector(object): @staticmethod def create_inconsistency_subject(red_cap_subject, study_subject, url): - #func dict + # func dict field_checks = { 'sex': RedcapConnector.check_sex_consistency, 'date_born': RedcapConnector.check_birth_date_consistency, @@ -239,14 +287,14 @@ class RedcapConnector(object): fields = [] - #get fields which are true from redcap columns - fields_to_check = [k for k,v in model_to_dict(study_subject.study.redcap_columns).iteritems() if v is True] + # get fields which are true from redcap columns + fields_to_check = [k for k, v in model_to_dict(study_subject.study.redcap_columns).iteritems() if v is True] for field_to_check in fields_to_check: field = field_checks[field_to_check](red_cap_subject, study_subject) if field is not None: fields.append(field) - + result = None if len(fields) > 0: result = InconsistentSubject.create(smash_subject=study_subject, url=url, fields=fields) @@ -257,20 +305,66 @@ class RedcapConnector(object): subject.nd_number + "&page=demographics" def get_red_cap_subjects(self): - query_data = { + query_data = self.get_subject_query_data() + data = self.execute_query(query_data) + result = [] + for row in data: + if isinstance(row, dict): + redcap_subject = RedcapSubject() + redcap_subject.nd_number = row.get(self.nd_number_field) + if self.date_born_field != "": + redcap_subject.date_born = row.get(self.date_born_field) + if self.sex_field != "": + redcap_subject.sex = row.get(self.sex_field) + if self.dead_field != "": + redcap_subject.dead = (row.get(self.dead_field).lower() == "yes") + if self.m_power_id_field != "": + redcap_subject.mpower_id = row.get(self.m_power_id_field) + if self.language_1_field != "" and row.get(self.language_1_field): + redcap_subject.add_language(self.get_language(row.get(self.language_1_field))) + if self.language_2_field != "" and row[self.language_2_field]: + redcap_subject.add_language(self.get_language(row.get(self.language_2_field))) + if self.language_3_field != "" and row[self.language_3_field]: + redcap_subject.add_language(self.get_language(row.get(self.language_3_field))) + if self.language_4_field != "" and row[self.language_4_field]: + redcap_subject.add_language(self.get_language(row.get(self.language_4_field))) + visit = RedcapVisit() + visit.visit_number = 1 + if self.virus_field != "": + if row.get(self.virus_field) == "Negative": + visit.virus = False + elif row.get(self.virus_field) == "Positive": + visit.virus = True + redcap_subject.visits.append(visit) + result.append(redcap_subject) + for i in range(2, 10): + query_data = self.get_subject_query_data() + query_data["events[0]"] = "visit_" + str(i) + "_arm_1" + data = self.execute_query(query_data) + if isinstance(data, dict): + break + for row in data: + if isinstance(row, dict): + nd_number = row.get(self.nd_number_field) + for redcap_subject in result: + if redcap_subject.nd_number == nd_number: + visit = RedcapVisit() + visit.visit_number = i + if self.virus_field != "": + if row.get(self.virus_field) == "Negative": + visit.virus = False + elif row.get(self.virus_field) == "Positive": + visit.virus = True + redcap_subject.visits.append(visit) + + return result + + def get_subject_query_data(self): + result = { 'token': self.token, 'content': 'record', 'format': 'json', 'type': 'flat', - 'fields[0]': RED_CAP_DATE_BORN_FIELD, - 'fields[1]': RED_CAP_SEX_FIELD, - 'fields[2]': RED_CAP_ND_NUMBER_FIELD, - 'fields[3]': RED_CAP_DEAD_FIELD, - 'fields[4]': RED_CAP_LANGUAGE_1_FIELD, - 'fields[5]': RED_CAP_LANGUAGE_2_FIELD, - 'fields[6]': RED_CAP_LANGUAGE_3_FIELD, - 'fields[7]': RED_CAP_LANGUAGE_4_FIELD, - 'fields[8]': RED_CAP_MPOWER_ID_FIELD, 'events[0]': 'visit_1_arm_1', 'rawOrLabel': 'label', 'rawOrLabelHeaders': 'raw', @@ -279,24 +373,37 @@ class RedcapConnector(object): 'exportDataAccessGroups': 'false', 'returnFormat': 'json' } - data = self.execute_query(query_data) - result = [] - for row in data: - redcap_subject = RedcapSubject() - redcap_subject.nd_number = row[RED_CAP_ND_NUMBER_FIELD] - redcap_subject.date_born = row[RED_CAP_DATE_BORN_FIELD] - redcap_subject.sex = row[RED_CAP_SEX_FIELD] - redcap_subject.dead = (row[RED_CAP_DEAD_FIELD].lower() == "yes") - redcap_subject.mpower_id = row[RED_CAP_MPOWER_ID_FIELD] - if row[RED_CAP_LANGUAGE_1_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_1_FIELD])) - if row[RED_CAP_LANGUAGE_2_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_2_FIELD])) - if row[RED_CAP_LANGUAGE_3_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_3_FIELD])) - if row[RED_CAP_LANGUAGE_4_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_4_FIELD])) - result.append(redcap_subject) + field_number = 0 + if self.date_born_field != "": + result['fields[' + str(field_number) + ']'] = self.date_born_field + field_number += 1 + if self.sex_field != "": + result['fields[' + str(field_number) + ']'] = self.sex_field + field_number += 1 + if self.nd_number_field != "": + result['fields[' + str(field_number) + ']'] = self.nd_number_field + field_number += 1 + if self.dead_field != "": + result['fields[' + str(field_number) + ']'] = self.dead_field + field_number += 1 + if self.language_1_field != "": + result['fields[' + str(field_number) + ']'] = self.language_1_field + field_number += 1 + if self.language_2_field != "": + result['fields[' + str(field_number) + ']'] = self.language_2_field + field_number += 1 + if self.language_3_field != "": + result['fields[' + str(field_number) + ']'] = self.language_3_field + field_number += 1 + if self.language_4_field != "": + result['fields[' + str(field_number) + ']'] = self.language_4_field + field_number += 1 + if self.m_power_id_field != "": + result['fields[' + str(field_number) + ']'] = self.m_power_id_field + field_number += 1 + if self.virus_field != "": + result['fields[' + str(field_number) + ']'] = self.virus_field + field_number += 1 return result def get_language(self, name): diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index 2f2846523db2447089e98c1c7f08de6accf1aff3..fecda37d4b7560d1b16fd9ee768119189ea9138c 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -300,6 +300,9 @@ function createTable(params) { $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () { $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>'); }); + $(tableElement).find('tfoot div[name="yes_no_null_filter"]').each(function () { + $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option><option value="null">N/A</option></select>'); + }); $(tableElement).find('tfoot div[name="integer_filter"]').each(function () { var options = '<option value selected="selected">---</option>'; for (var i = 1; i <= 8; i++) {