diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aa60a1b1fc7004324d05274788d79bf77321ef40..b69183fefa265a33dae2b211e318922abc662c73 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,6 +64,14 @@ test_python_latest: - coverage run --source web manage.py test -v3 - coverage report -m --omit="*/test*,*/migrations*,*debug_utils*" +test_create_dummy_script: + <<: *test_definition + script: + - cp "local_settings_ci_sqlite.py" "smash/smash/local_settings.py" + - cd smash + - python manage.py makemigrations web && python manage.py migrate + - python db_scripts/create_dummy_data.py + build_debian: image: debian stage: build diff --git a/CHANGELOG b/CHANGELOG index 1838d9e64722131af63a626dd13e9bf74cfd5932..0d1b7957322c24e147f4675df9f056e136d09c01 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,14 @@ smasch (1.0.0~alpha.1-0) unstable; urgency=low + * backward incompatible: all ncer/pdp related fields are moved to custom + fields and removed from default setup (#345) * improvement: added views to delete StudySubject and Subject (#354) * improvement: study subject can be configured to contain custom fields (#339) * improvement: daily automatic import is available in the study configuration panel (#334) + * improvement: possibility to edit default visible columns in different + subject lists (#348) * small improvement: "next of keen" renamed to "next of kin" (#362) * small improvement: django command for creating admin in application (#347) * small improvement: django admin panel contains usable data tables (#346) diff --git a/smash/db_scripts/create_dummy_data.py b/smash/db_scripts/create_dummy_data.py index aef885576c0f122d9f64dab0fa4408b6c2b705ed..dadf16b1f1093524e1d864ddfab5edfc9ffa274f 100644 --- a/smash/db_scripts/create_dummy_data.py +++ b/smash/db_scripts/create_dummy_data.py @@ -431,15 +431,16 @@ class smashProvider(BaseProvider): screening_number = self.createSmashScreeningNumber( default_location.prefix) - virus_test_1 = choice([None, True, False], 1, p=[0.2, 0.3, 0.5])[0] + virus_test_1_result = choice([None, "Positive", "Negative", "Inconclusive"], 1, p=[0.2, 0.2, 0.3, 0.3])[0] virus_iga_1 = choice(["Borderline", "Positive", "Negative"], 1, p=[0.02, 0.03, 0.95])[0] virus_igg_1 = choice(["Borderline", "Positive", "Negative"], 1, p=[0.02, 0.03, 0.95])[0] - if virus_test_1 is None: + if virus_test_1_result is None: + virus_test_1_collection_date = '' + virus_test_1_updated = fake.date_between(start_date='-30d', end_date='-4d') + elif virus_test_1_result == "Inconclusive": virus_test_1_updated = fake.date_between(start_date='-30d', end_date='-4d') - #inconclusive results have a date - virus_test_1_updated = choice([None, virus_test_1_updated], 1, p=[0.7, 0.3])[0] virus_test_1_collection_date = choice([None, virus_test_1_updated], 1, p=[0.7, 0.3])[0] - elif virus_test_1 == True: + elif virus_test_1_result == "Positive": virus_test_1_updated = fake.date_between(start_date='-30d', end_date='-4d') virus_test_1_collection_date = fake.date_between(start_date='-30d', end_date='-4d') else: @@ -447,13 +448,13 @@ class smashProvider(BaseProvider): virus_test_1_collection_date = fake.date_between(start_date='-30d', end_date='-4d') study_subject, _ = StudySubject.objects.update_or_create(nd_number=nd_number, subject=subject, - virus_test_1=virus_test_1, virus_test_1_updated=virus_test_1_updated, - virus_test_1_collection_date=virus_test_1_collection_date, - virus_test_1_iga_status=virus_iga_1, - virus_test_1_igg_status=virus_igg_1, defaults={'default_location': default_location, 'type': type, 'screening_number': screening_number, 'study': study}) - + study_subject.set_custom_field_value('Visit 0 RT-PCR update date', virus_test_1_updated) + study_subject.set_custom_field_value('Virus 0 RT-PCR', virus_test_1_result) + study_subject.set_custom_field_value('Visit 0 RT-PCR collection date', virus_test_1_collection_date) + study_subject.set_custom_field_value('Visit 0 IgA Status', virus_iga_1) + study_subject.set_custom_field_value('Visit 0 IgG Status', virus_igg_1) self.alreadyCreatedStudySubjects.append(study_subject) return study_subject @@ -607,7 +608,7 @@ class smashProvider(BaseProvider): defaults = {'first_name': first_name, 'last_name': last_name, 'email': email, 'unit': unit, 'specialization': specialization, 'phone_number': phone_number, 'user': user} - + worker, _ = Worker.objects.update_or_create(first_name=first_name, last_name=last_name, defaults=defaults) diff --git a/smash/db_scripts/import_file.py b/smash/db_scripts/import_file.py index d8d8d18406cdf77f3120eef893e44fb2cf266907..efae7af09d72f94171747796904a68456bf075b8 100644 --- a/smash/db_scripts/import_file.py +++ b/smash/db_scripts/import_file.py @@ -504,7 +504,6 @@ def parse_row(index, row, visit_columns, appointmentTypes, voucher_types, lcsb_w 'type': SUBJECT_TYPE_CHOICES_PATIENT, 'excluded': row['EXCLUDED'], 'exclude_reason': row['REASON.1'], - 'previously_in_study': row['PDP 1.0'], 'comments': row['COMMENT'], 'default_location': location, 'flying_team': ft, diff --git a/smash/web/api_views/appointment.py b/smash/web/api_views/appointment.py index d3dcef780df268db9b5fdc4bf3a74f91e2f6a429..6100440f9e7c0ca6ead923cf10a976780f42efe9 100644 --- a/smash/web/api_views/appointment.py +++ b/smash/web/api_views/appointment.py @@ -33,7 +33,7 @@ def get_appointment_columns(request, appointment_list_type): result = [] add_column(result, "First name", "first_name", subject_columns, "string_filter") add_column(result, "Last name", "last_name", subject_columns, "string_filter") - add_column(result, "ND number", "nd_number", subject_study_columns, "string_filter") + add_column(result, "Subject number", "nd_number", subject_study_columns, "string_filter") add_column(result, "Type", "type", subject_study_columns, "type_filter") add_column(result, "Info sent", "post_mail_sent", appointment_columns, "yes_no_filter") add_column(result, "Date", "datetime_when", appointment_columns, None) diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index 0994996d0d71d93e41f7ad2361454fdc85708c09..652079a60ee5d8c944c7231c88d6b4587ec6aa52 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -34,17 +34,6 @@ def str_to_yes_no_null(val: str): return "NO" -def virus_test_to_str(test, date): - if test is None and date is not None: - return "Inconclusive" - if test is None: - return "N/A" - if test: - return "Positive" - else: - return "Negative" - - 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 8a3b3b30fb56e6afc7f9c747bd3545c8944b405a..ec39cb03d6819fc5a4451afa559f394a574b5321 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -7,8 +7,8 @@ from django.db.models import Q from django.http import JsonResponse from django.urls import reverse -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.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 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, \ @@ -54,7 +54,7 @@ def get_subject_columns(request, subject_list_type): study_subject_columns = StudyColumns() result = [] - add_column(result, "ND", "nd_number", study_subject_columns, "string_filter", study.columns) + add_column(result, "Subject number", "nd_number", study_subject_columns, "string_filter", study.columns) add_column(result, "Screening", "screening_number", study_subject_columns, "string_filter", study.columns) add_column(result, "First name", "first_name", subject_columns, "string_filter") add_column(result, "Last name", "last_name", subject_columns, "string_filter") @@ -80,8 +80,6 @@ def get_subject_columns(request, subject_list_type): add_column(result, "Next of kin", "next_of_kin_name", subject_columns, "string_filter") add_column(result, "Next of kin phone", "next_of_kin_phone", subject_columns, "string_filter") add_column(result, "Next of kin address", "next_of_kin_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, "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) @@ -94,49 +92,24 @@ def get_subject_columns(request, subject_list_type): virus_visit_numbers = list(range(1, 5 + 1)) visit_numbers = list(range(1, study.visits_to_show_in_subject_list + 1)) - for one_based_idx, virus_visit_number in enumerate(virus_visit_numbers, 1): - add_column(result, - 'Virus {} RT-PCR'.format(virus_visit_number), - 'virus_test_{}'.format(one_based_idx), # always starts in 1 - study_subject_columns, - 'yes_no_null_inconclusive_filter', study.columns) - add_column(result, - "Visit {} RT-PCR update date".format(virus_visit_number), - "virus_test_{}_updated".format(one_based_idx), - study_subject_columns, - "virus_test_date", study.columns) - add_column(result, - "Visit {} RT-PCR collection date".format(virus_visit_number), - "virus_test_{}_collection_date".format(one_based_idx), - study_subject_columns, - "virus_test_date", study.columns) - add_column(result, - "Visit {} IgA Status".format(virus_visit_number), - "virus_test_{}_iga_status".format(one_based_idx), - study_subject_columns, - 'serology_filter', study.columns) - add_column(result, - "Visit {} IgG Status".format(virus_visit_number), - "virus_test_{}_igg_status".format(one_based_idx), - study_subject_columns, - '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(): + visible = study_subject_columns.is_custom_field_visible(custom_study_subject_field) 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) + visible_param=visible) 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) + visible_param=visible) elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_INTEGER: add_column(result, custom_study_subject_field.name, @@ -144,7 +117,7 @@ def get_subject_columns(request, subject_list_type): study_subject_columns, None, sortable=False, - visible_param=False) + visible_param=visible) elif custom_study_subject_field.type == CUSTOM_FIELD_TYPE_DOUBLE: add_column(result, custom_study_subject_field.name, @@ -152,28 +125,28 @@ def get_subject_columns(request, subject_list_type): study_subject_columns, None, sortable=False, - visible_param=False) + visible_param=visible) 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) + visible_param=visible) 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) + visible_param=visible) 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) + visible_param=visible) else: raise NotImplementedError @@ -260,8 +233,6 @@ def get_subjects_order(subjects_to_be_ordered: QuerySet, order_column, order_dir result = subjects_to_be_ordered.order_by(order_direction + 'subject__social_security_number') elif order_column == "postponed": result = subjects_to_be_ordered.order_by(order_direction + 'postponed') - elif order_column == "brain_donation_agreement": - result = subjects_to_be_ordered.order_by(order_direction + 'brain_donation_agreement') elif order_column == "excluded": result = subjects_to_be_ordered.order_by(order_direction + 'excluded') elif order_column == "type": @@ -279,16 +250,6 @@ def get_subjects_order(subjects_to_be_ordered: QuerySet, order_column, order_dir 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 re.search(r'^virus_test_[1-5]$', order_column): - result = subjects_to_be_ordered.order_by(order_direction + order_column) - elif re.search(r'^virus_test_[1-5]_updated$', order_column): - result = subjects_to_be_ordered.order_by(order_direction + order_column) - elif re.search(r'^virus_test_[1-5]_collection_date', order_column): - result = subjects_to_be_ordered.order_by(order_direction + order_column) - elif re.search(r'^virus_test_[1-5]_iga_status', order_column): - 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( @@ -396,27 +357,6 @@ def get_subjects_filtered(subjects_to_be_filtered: QuerySet, filters) -> QuerySe result = result.filter(resigned=(value == "true")) elif column == "endpoint_reached": result = result.filter(endpoint_reached=(value == "true")) - elif re.search(r'^virus_test_[1-5]$', column): - visit_number = column.replace("virus_test_", "") - virus_test__isnull_param = "virus_test_{}__isnull".format(visit_number) - virus_test__updated__isnull_param = "virus_test_{}_updated__isnull".format(visit_number) - visit_test_param = "virus_test_{}".format(visit_number) - if value == "null": - result = result.filter(**{virus_test__isnull_param: True, virus_test__updated__isnull_param: True}) - elif value == "inconclusive": - result = result.filter(**{virus_test__isnull_param: True, virus_test__updated__isnull_param: False}) - else: - result = result.filter(**{visit_test_param: (value == "true")}) - elif re.search(r'^virus_test_[1-5]_iga_status$', column): - visit_number = column.replace("virus_test_", "").replace("_iga_status", "") - virus_test_iga_status_param = "virus_test_{}_iga_status".format(visit_number) - result = result.filter(**{virus_test_iga_status_param: value}) - elif re.search(r'^virus_test_[1-5]_igg_status$', column): - visit_number = column.replace("virus_test_", "").replace("_igg_status", "") - virus_test_igg_status_param = "virus_test_{}_igg_status".format(visit_number) - result = result.filter(**{virus_test_igg_status_param: value}) - elif column == "brain_donation_agreement": - result = result.filter(brain_donation_agreement=(value == "true")) elif column == "postponed": result = result.filter(postponed=(value == "true")) elif column == "excluded": @@ -529,7 +469,7 @@ def types(request): }) -def serialize_subject(study_subject): +def serialize_subject(study_subject:StudySubject): location = location_to_str(study_subject.default_location) flying_team = flying_team_to_str(study_subject.flying_team) visits = Visit.objects.filter(subject=study_subject).order_by('visit_number') @@ -608,7 +548,6 @@ def serialize_subject(study_subject): "resigned": bool_to_yes_no(study_subject.resigned), "endpoint_reached": bool_to_yes_no(study_subject.endpoint_reached), "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), "information_sent": bool_to_yes_no(study_subject.information_sent), "health_partner_first_name": health_partner_first_name, @@ -618,14 +557,6 @@ def serialize_subject(study_subject): "id": study_subject.id, "visits": serialized_visits, } - for i in range(1, 6): - result['virus_test_{}'.format(i)] = virus_test_to_str(getattr(study_subject, "virus_test_{}".format(i)), - getattr(study_subject, "virus_test_{}_updated".format(i))) - result["virus_test_{}_updated".format(i)] = getattr(study_subject, "virus_test_{}_updated".format(i)) - result["virus_test_{}_collection_date".format(i)] = getattr(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 \ diff --git a/smash/web/api_views/visit.py b/smash/web/api_views/visit.py index 276006fc18646ec03ca6052c62c0872918e2eb2a..b13b7c224f8c99c09c6bfc38ac0aef6b52a77326 100644 --- a/smash/web/api_views/visit.py +++ b/smash/web/api_views/visit.py @@ -39,7 +39,7 @@ def get_visit_columns(request, visit_list_type): result = [] add_column(result, "First name", "first_name", visit_subject_columns, "string_filter") add_column(result, "Last name", "last_name", visit_subject_columns, "string_filter") - add_column(result, "ND number", "nd_number", visit_subject_study_columns, "string_filter", study.columns) + add_column(result, "Subject number", "nd_number", visit_subject_study_columns, "string_filter", study.columns) add_column(result, "Location", "default_location", visit_subject_study_columns, "location_filter", study.columns) add_column(result, "Flying team location", "flying_team", visit_subject_study_columns, "flying_team_filter", study.columns) diff --git a/smash/web/forms/study_subject_forms.py b/smash/web/forms/study_subject_forms.py index 6211544bd6817c549a35561bb9ce1e60786a3b86..c22662cd92ab569560c54da23d79d165d8b322ed 100644 --- a/smash/web/forms/study_subject_forms.py +++ b/smash/web/forms/study_subject_forms.py @@ -1,15 +1,14 @@ 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, 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, \ +from web.models import StudySubject, Study, StudyColumns, VoucherType, Worker +from web.models.constants import SCREENING_NUMBER_PREFIXES_FOR_TYPE, 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 @@ -81,6 +80,7 @@ def create_field_for_custom_study_subject_field(study_subject_field: CustomStudy def __unicode__(self): return "%s" % self.url + initial = CustomFileField() initial.url = val @@ -119,41 +119,6 @@ class StudySubjectForm(ModelForm): self.fields['health_partner'].queryset = Worker.get_workers_by_worker_type( WORKER_HEALTH_PARTNER) - visit_from_zero = ConfigurationItem.objects.get(type=VISIT_SHOW_VISIT_NUMBER_FROM_ZERO).value - # True values are y, yes, t, true, on and 1; false values are n, no, f, false, off and 0. - if strtobool(visit_from_zero): - virus_visit_numbers = list(range(0, 5)) - for one_based_idx, virus_visit_number in enumerate(virus_visit_numbers, 1): - field = 'virus_test_{}'.format(one_based_idx) - self.fields[field].label = 'Visit {} RT-PCR'.format(virus_visit_number) - - date_field = 'virus_test_{}_updated'.format(one_based_idx) - self.fields[date_field].label = 'Visit {} RT-PCR result date'.format(virus_visit_number) - - collection_date_field = 'virus_test_{}_collection_date'.format(one_based_idx) - self.fields[collection_date_field].label = 'Visit {} RT-PCR collection date'.format(virus_visit_number) - - iga_field = 'virus_test_{}_iga_status'.format(one_based_idx) - igg_field = 'virus_test_{}_igg_status'.format(one_based_idx) - self.fields[iga_field].label = 'Visit {} IgA status'.format(virus_visit_number) - self.fields[igg_field].label = 'Visit {} IgG status'.format(virus_visit_number) - - for visit_number in range(1, 6): - disable_virus_test_field(self, visit_number) - self.update_virus_inconclusive_data(visit_number) - - def update_virus_inconclusive_data(self, visit_number): - test_result_column_name = 'virus_test_{}'.format(visit_number) - test_date_column_name = 'virus_test_{}_updated'.format(visit_number) - - self.fields[test_result_column_name].widget.choices.append(("Inc", 'Inconclusive')) - instance = getattr(self, 'instance', None) - if instance and instance.id: - test_result = getattr(instance, test_result_column_name) - test_date = getattr(instance, test_date_column_name) - 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 @@ -185,8 +150,6 @@ class StudySubjectAddForm(StudySubjectForm): self.study = get_study_from_args(kwargs) super(StudySubjectAddForm, self).__init__(*args, **kwargs) - for visit_number in range(1, 6): - disable_virus_test_field(self, visit_number) prepare_study_subject_fields(fields=self.fields, study=self.study) @@ -214,7 +177,6 @@ class StudySubjectAddForm(StudySubjectForm): cleaned_data['screening_number'] = screening_number validate_subject_screening_number(self, cleaned_data) validate_subject_nd_number(self, cleaned_data) - validate_subject_mpower_number(self, cleaned_data) return cleaned_data def get_prefix_screening_number(self): @@ -322,7 +284,6 @@ class StudySubjectEditForm(StudySubjectForm): def clean(self): 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 @@ -337,20 +298,6 @@ class StudySubjectEditForm(StudySubjectForm): fields = '__all__' -def disable_virus_test_field(self, visit_number): - test_result_column_name = 'virus_test_{}'.format(visit_number) - test_result_date_column_name = 'virus_test_{}_updated'.format(visit_number) - test_collection_date_column_name = 'virus_test_{}_collection_date'.format(visit_number) - test_iga_status_column_name = 'virus_test_{}_iga_status'.format(visit_number) - test_igg_status_column_name = 'virus_test_{}_igg_status'.format(visit_number) - self.fields[test_result_column_name].widget.attrs['readonly'] = True - self.fields[test_result_column_name].widget.attrs['disabled'] = True - self.fields[test_result_date_column_name].widget.attrs['readonly'] = True - self.fields[test_collection_date_column_name].widget.attrs['readonly'] = True - self.fields[test_iga_status_column_name].widget.attrs['readonly'] = True - self.fields[test_igg_status_column_name].widget.attrs['readonly'] = True - - def get_study_from_args(kwargs): study = kwargs.pop('study', None) if study is None: @@ -372,15 +319,10 @@ def prepare_study_subject_fields(fields, study): prepare_field(fields, study.columns, 'nd_number') prepare_field(fields, study.columns, 'datetime_contact_reminder') prepare_field(fields, study.columns, 'postponed') - prepare_field(fields, study.columns, 'brain_donation_agreement') prepare_field(fields, study.columns, 'flying_team') - prepare_field(fields, study.columns, 'mpower_id') prepare_field(fields, study.columns, 'comments') prepare_field(fields, study.columns, 'referral') - prepare_field(fields, study.columns, 'diagnosis') - prepare_field(fields, study.columns, 'year_of_diagnosis') prepare_field(fields, study.columns, 'information_sent') - prepare_field(fields, study.columns, 'pd_in_family') prepare_field(fields, study.columns, 'endpoint_reached') prepare_field(fields, study.columns, 'excluded') prepare_field(fields, study.columns, 'resigned') @@ -388,36 +330,8 @@ def prepare_study_subject_fields(fields, study): prepare_field(fields, study.columns, 'referral_letter') prepare_field(fields, study.columns, 'health_partner') prepare_field(fields, study.columns, 'health_partner_feedback_agreement') - prepare_field(fields, study.columns, 'screening') - 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') - prepare_field(fields, study.columns, 'virus_test_1_collection_date') - prepare_field(fields, study.columns, 'virus_test_2_collection_date') - prepare_field(fields, study.columns, 'virus_test_3_collection_date') - prepare_field(fields, study.columns, 'virus_test_4_collection_date') - prepare_field(fields, study.columns, 'virus_test_5_collection_date') - prepare_field(fields, study.columns, 'virus_test_1_iga_status') - prepare_field(fields, study.columns, 'virus_test_1_igg_status') - prepare_field(fields, study.columns, 'virus_test_2_iga_status') - prepare_field(fields, study.columns, 'virus_test_2_igg_status') - prepare_field(fields, study.columns, 'virus_test_3_iga_status') - prepare_field(fields, study.columns, 'virus_test_3_igg_status') - prepare_field(fields, study.columns, 'virus_test_4_iga_status') - prepare_field(fields, study.columns, 'virus_test_4_igg_status') - prepare_field(fields, study.columns, 'virus_test_5_iga_status') - prepare_field(fields, study.columns, 'virus_test_5_igg_status') - def validate_subject_screening_number(self, cleaned_data): if self.study.columns.resign_reason: @@ -440,7 +354,7 @@ def validate_subject_nd_number(self, cleaned_data): nd_number=nd_number, study=self.study) if subjects_from_db: if subjects_from_db[0].screening_number != cleaned_data.get('screening_number', ''): - self.add_error('nd_number', "ND number already in use") + self.add_error('nd_number', "Subject number already in use") # else: #empty nd_number is valid @@ -448,13 +362,3 @@ def validate_subject_resign_reason(self, cleaned_data): if self.study.columns.resigned and self.study.columns.resign_reason: if cleaned_data['resigned'] and cleaned_data['resign_reason'] == '': self.add_error('resign_reason', "Resign reason cannot be empty") - - -def validate_subject_mpower_number(self, cleaned_data): - if self.study.columns.mpower_id: - if cleaned_data['mpower_id'] != "": - subjects_from_db = StudySubject.objects.filter( - mpower_id=cleaned_data['mpower_id']) - if subjects_from_db: - if subjects_from_db[0].screening_number != cleaned_data.get('screening_number', ''): - self.add_error('mpower_id', "mPower number already in use") diff --git a/smash/web/forms/study_subject_list_form.py b/smash/web/forms/study_subject_list_form.py index 134306d364211a81989b67072de78ad0cc170064..545b53deb3c2683b186f74b2dc19b6767d56777d 100644 --- a/smash/web/forms/study_subject_list_form.py +++ b/smash/web/forms/study_subject_list_form.py @@ -1,6 +1,8 @@ +from django import forms from django.forms import ModelForm from web.models import StudySubjectList, SubjectColumns, StudyColumns +from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id class StudySubjectListEditForm(ModelForm): @@ -30,3 +32,17 @@ class StudySubjectColumnsEditForm(ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + instance: StudyColumns = kwargs.get('instance') + for visibility in instance.custom_fields_visibility: + field = visibility.study_subject_field + field_id = get_study_subject_field_id(field) + self.fields[field_id] = forms.BooleanField(label=field.name, initial=visibility.visible, required=False) + + def save(self, commit=True) -> StudyColumns: + instance = super().save(commit) + # we can add custom values only after object exists in the database + for visibility in instance.custom_fields_visibility: + field = self[get_study_subject_field_id(visibility.study_subject_field)] + visibility.visible = str(field.value()).lower() == "true" + visibility.save() + return instance diff --git a/smash/web/migrations/0187_auto_20201130_1224.py b/smash/web/migrations/0187_auto_20201130_1224.py new file mode 100644 index 0000000000000000000000000000000000000000..44d54c081f27749d1966a997b44f0149cb3117ab --- /dev/null +++ b/smash/web/migrations/0187_auto_20201130_1224.py @@ -0,0 +1,44 @@ +# Generated by Django 3.1.3 on 2020-11-30 12:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0186_auto_20201127_1003'), + ] + + operations = [ + migrations.AlterField( + model_name='studysubjectlist', + name='study', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.study'), + ), + 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')], editable=False, max_length=50, null=True, verbose_name='Type of list'), + ), + migrations.AlterField( + model_name='studysubjectlist', + name='visible_subject_columns', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.subjectcolumns'), + ), + migrations.AlterField( + model_name='studysubjectlist', + name='visible_subject_study_columns', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.studycolumns'), + ), + migrations.CreateModel( + name='CustomStudySubjectVisibility', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('visible', models.BooleanField(default=False)), + ('study_subject_field', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.customstudysubjectfield', verbose_name='Custom Field')), + ('visible_subject_study_columns', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='web.studycolumns', verbose_name='List of visible columns')), + ], + ), + + ] diff --git a/smash/web/migrations/0188_studysubject_virus_test_1_result.py b/smash/web/migrations/0188_studysubject_virus_test_1_result.py new file mode 100644 index 0000000000000000000000000000000000000000..5a7403bbaf2b91320cedfc11cf87dd4d662007db --- /dev/null +++ b/smash/web/migrations/0188_studysubject_virus_test_1_result.py @@ -0,0 +1,98 @@ +# Generated by Django 3.1.3 on 2020-12-01 07:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0187_auto_20201130_1224'), + ] + + operations = [ + migrations.AddField( + model_name='studysubject', + name='virus_test_1_result', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_2_result', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_3_result', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_4_result', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_5_result', + field=models.CharField(max_length=20, null=True), + ), + migrations.RunSQL("update web_studysubject set virus_test_1_result='Positive' " + "where virus_test_1 is not null and virus_test_1"), + migrations.RunSQL("update web_studysubject set virus_test_1_result='Negative' " + "where virus_test_1 is not null and not virus_test_1"), + migrations.RunSQL("update web_studysubject set virus_test_1_result='Inconclusive' " + "where virus_test_1 is null and virus_test_1_updated is not null"), + + migrations.RunSQL("update web_studysubject set virus_test_2_result='Positive' " + "where virus_test_2 is not null and virus_test_2"), + migrations.RunSQL("update web_studysubject set virus_test_2_result='Negative' " + "where virus_test_2 is not null and not virus_test_2"), + migrations.RunSQL("update web_studysubject set virus_test_2_result='Inconclusive' " + "where virus_test_2 is null and virus_test_2_updated is not null"), + + migrations.RunSQL("update web_studysubject set virus_test_3_result='Positive' " + "where virus_test_3 is not null and virus_test_3"), + migrations.RunSQL("update web_studysubject set virus_test_3_result='Negative' " + "where virus_test_3 is not null and not virus_test_3"), + migrations.RunSQL("update web_studysubject set virus_test_3_result='Inconclusive' " + "where virus_test_3 is null and virus_test_3_updated is not null"), + + migrations.RunSQL("update web_studysubject set virus_test_4_result='Positive' " + "where virus_test_4 is not null and virus_test_4"), + migrations.RunSQL("update web_studysubject set virus_test_4_result='Negative' " + "where virus_test_4 is not null and not virus_test_4"), + migrations.RunSQL("update web_studysubject set virus_test_4_result='Inconclusive' " + "where virus_test_4 is null and virus_test_4_updated is not null"), + + migrations.RunSQL("update web_studysubject set virus_test_5_result='Positive' " + "where virus_test_5 is not null and virus_test_5"), + migrations.RunSQL("update web_studysubject set virus_test_5_result='Negative' " + "where virus_test_5 is not null and not virus_test_5"), + migrations.RunSQL("update web_studysubject set virus_test_5_result='Inconclusive' " + "where virus_test_5 is null and virus_test_5_updated is not null"), + migrations.RenameField( + model_name='studycolumns', + old_name='virus_test_1', + new_name='virus_test_1_result', + ), + migrations.RenameField( + model_name='studycolumns', + old_name='virus_test_2', + new_name='virus_test_2_result', + ), + migrations.RenameField( + model_name='studycolumns', + old_name='virus_test_3', + new_name='virus_test_3_result', + ), + migrations.RenameField( + model_name='studycolumns', + old_name='virus_test_4', + new_name='virus_test_4_result', + ), + migrations.RenameField( + model_name='studycolumns', + old_name='virus_test_5', + new_name='virus_test_5_result', + ), + + ] diff --git a/smash/web/migrations/0189_remove_studysubject_virus_test_1.py b/smash/web/migrations/0189_remove_studysubject_virus_test_1.py new file mode 100644 index 0000000000000000000000000000000000000000..e18f54cb8db7ba0a3d6dd133526dfac6e058e906 --- /dev/null +++ b/smash/web/migrations/0189_remove_studysubject_virus_test_1.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.3 on 2020-12-01 08:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0188_studysubject_virus_test_1_result'), + ] + + operations = [ + migrations.RemoveField( + model_name='studysubject', + name='virus_test_1', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_2', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_3', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_4', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_5', + ), + ] diff --git a/smash/web/migrations/0190_remove_study_related_fields.py b/smash/web/migrations/0190_remove_study_related_fields.py new file mode 100644 index 0000000000000000000000000000000000000000..c95ddbfc084cd43f46ee672c19e653349e56c27e --- /dev/null +++ b/smash/web/migrations/0190_remove_study_related_fields.py @@ -0,0 +1,267 @@ +# Generated by Django 3.1.3 on 2020-12-01 08:03 +from django.db import migrations + +# noinspection PyUnusedLocal +from web.models.constants import CUSTOM_FIELD_TYPE_TEXT, GLOBAL_STUDY_ID, CUSTOM_FIELD_TYPE_BOOLEAN, \ + CUSTOM_FIELD_TYPE_INTEGER, CUSTOM_FIELD_TYPE_SELECT_LIST, CUSTOM_FIELD_TYPE_DATE + + +def create_custom_fields(apps, schema_editor): + # noinspection PyPep8Naming + StudySubject = apps.get_model("web", "StudySubject") + if StudySubject.objects.all().count() > 0: + # noinspection PyPep8Naming + CustomStudySubjectField = apps.get_model("web", "CustomStudySubjectField") + # noinspection PyPep8Naming + CustomStudySubjectValue = apps.get_model("web", "CustomStudySubjectValue") + + mpower_field = CustomStudySubjectField.objects.create(name="MPower ID", type=CUSTOM_FIELD_TYPE_TEXT, + study_id=GLOBAL_STUDY_ID, unique=True) + screening_field = CustomStudySubjectField.objects.create(name="Screening", type=CUSTOM_FIELD_TYPE_TEXT, + study_id=GLOBAL_STUDY_ID) + diagnosis_field = CustomStudySubjectField.objects.create(name="Diagnosis", type=CUSTOM_FIELD_TYPE_TEXT, + study_id=GLOBAL_STUDY_ID) + previously_in_study_field = CustomStudySubjectField.objects.create(name="Previously in PDP study", + type=CUSTOM_FIELD_TYPE_BOOLEAN, + study_id=GLOBAL_STUDY_ID) + pd_in_family_field = CustomStudySubjectField.objects.create(name="PD in family", + type=CUSTOM_FIELD_TYPE_BOOLEAN, + study_id=GLOBAL_STUDY_ID) + brain_donation_agreement_field = CustomStudySubjectField.objects.create(name="Brain donation agreement", + type=CUSTOM_FIELD_TYPE_BOOLEAN, + study_id=GLOBAL_STUDY_ID) + year_of_diagnosis_field = CustomStudySubjectField.objects.create(name="Year of diagnosis", + type=CUSTOM_FIELD_TYPE_INTEGER, + study_id=GLOBAL_STUDY_ID) + + virus_test_1_result_field = CustomStudySubjectField.objects.create(name="Virus 0 RT-PCR", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Inconclusive", + default="N/A", + readonly=True) + virus_test_2_result_field = CustomStudySubjectField.objects.create(name="Virus 1 RT-PCR", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Inconclusive", + default="N/A", + readonly=True) + virus_test_3_result_field = CustomStudySubjectField.objects.create(name="Virus 2 RT-PCR", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Inconclusive", + default="N/A", + readonly=True) + virus_test_4_result_field = CustomStudySubjectField.objects.create(name="Virus 3 RT-PCR", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Inconclusive", + default="N/A", + readonly=True) + virus_test_5_result_field = CustomStudySubjectField.objects.create(name="Virus 4 RT-PCR", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Inconclusive", + default="N/A", + readonly=True) + virus_test_1_updated_field = CustomStudySubjectField.objects.create(name="Visit 0 RT-PCR update date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_2_updated_field = CustomStudySubjectField.objects.create(name="Visit 1 RT-PCR update date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_3_updated_field = CustomStudySubjectField.objects.create(name="Visit 2 RT-PCR update date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_4_updated_field = CustomStudySubjectField.objects.create(name="Visit 3 RT-PCR update date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_5_updated_field = CustomStudySubjectField.objects.create(name="Visit 4 RT-PCR update date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_1_collection_date_field = CustomStudySubjectField.objects.create( + name="Visit 0 RT-PCR collection date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_2_collection_date_field = CustomStudySubjectField.objects.create( + name="Visit 1 RT-PCR collection date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_3_collection_date_field = CustomStudySubjectField.objects.create( + name="Visit 2 RT-PCR collection date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_4_collection_date_field = CustomStudySubjectField.objects.create( + name="Visit 3 RT-PCR collection date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_5_collection_date_field = CustomStudySubjectField.objects.create( + name="Visit 4 RT-PCR collection date", + type=CUSTOM_FIELD_TYPE_DATE, + study_id=GLOBAL_STUDY_ID, + default="", + readonly=True) + virus_test_1_iga_status_field = CustomStudySubjectField.objects.create( + name="Visit 0 IgA Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_2_iga_status_field = CustomStudySubjectField.objects.create( + name="Visit 1 IgA Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_3_iga_status_field = CustomStudySubjectField.objects.create( + name="Visit 2 IgA Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_4_iga_status_field = CustomStudySubjectField.objects.create( + name="Visit 3 IgA Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_5_iga_status_field = CustomStudySubjectField.objects.create( + name="Visit 4 IgA Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_1_igg_status_field = CustomStudySubjectField.objects.create( + name="Visit 0 IgG Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_2_igg_status_field = CustomStudySubjectField.objects.create( + name="Visit 1 IgG Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_3_igg_status_field = CustomStudySubjectField.objects.create( + name="Visit 2 IgG Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_4_igg_status_field = CustomStudySubjectField.objects.create( + name="Visit 3 IgG Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + virus_test_5_igg_status_field = CustomStudySubjectField.objects.create( + name="Visit 4 IgG Status", + type=CUSTOM_FIELD_TYPE_SELECT_LIST, + study_id=GLOBAL_STUDY_ID, + possible_values="N/A;Positive;Negative;Borderline", + default="N/A", + readonly=True) + + for subject in StudySubject.objects.all(): + CustomStudySubjectValue.objects.create(study_subject_field=mpower_field, value=subject.mpower_id, + study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=screening_field, value=subject.screening, + study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=diagnosis_field, value=subject.diagnosis, + study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=previously_in_study_field, + value=subject.previously_in_study, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=brain_donation_agreement_field, + value=subject.brain_donation_agreement, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=pd_in_family_field, + value=subject.pd_in_family, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=year_of_diagnosis_field, + value=subject.year_of_diagnosis, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_1_result_field, + value=subject.virus_test_1_result, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_2_result_field, + value=subject.virus_test_2_result, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_3_result_field, + value=subject.virus_test_3_result, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_4_result_field, + value=subject.virus_test_4_result, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_5_result_field, + value=subject.virus_test_5_result, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_1_updated_field, + value=subject.virus_test_1_updated, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_2_updated_field, + value=subject.virus_test_2_updated, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_3_updated_field, + value=subject.virus_test_3_updated, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_4_updated_field, + value=subject.virus_test_4_updated, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_5_updated_field, + value=subject.virus_test_5_updated, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_1_collection_date_field, + value=subject.virus_test_1_collection_date, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_2_collection_date_field, + value=subject.virus_test_2_collection_date, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_3_collection_date_field, + value=subject.virus_test_3_collection_date, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_4_collection_date_field, + value=subject.virus_test_4_collection_date, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_5_collection_date_field, + value=subject.virus_test_5_collection_date, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_1_iga_status_field, + value=subject.virus_test_1_iga_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_2_iga_status_field, + value=subject.virus_test_2_iga_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_3_iga_status_field, + value=subject.virus_test_3_iga_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_4_iga_status_field, + value=subject.virus_test_4_iga_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_5_iga_status_field, + value=subject.virus_test_5_iga_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_1_igg_status_field, + value=subject.virus_test_1_igg_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_2_igg_status_field, + value=subject.virus_test_2_igg_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_3_igg_status_field, + value=subject.virus_test_3_igg_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_4_igg_status_field, + value=subject.virus_test_4_igg_status, study_subject=subject) + CustomStudySubjectValue.objects.create(study_subject_field=virus_test_5_igg_status_field, + value=subject.virus_test_5_igg_status, study_subject=subject) + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0189_remove_studysubject_virus_test_1'), + ] + + operations = [ + migrations.RunPython(create_custom_fields), + ] diff --git a/smash/web/migrations/0191_auto_20201201_1033.py b/smash/web/migrations/0191_auto_20201201_1033.py new file mode 100644 index 0000000000000000000000000000000000000000..33d690caf0acb662ac1272ba8afa2b20272f0057 --- /dev/null +++ b/smash/web/migrations/0191_auto_20201201_1033.py @@ -0,0 +1,282 @@ +# Generated by Django 3.1.3 on 2020-12-01 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0190_remove_study_related_fields'), + ] + + operations = [ + migrations.RemoveField( + model_name='studycolumns', + name='mpower_id', + ), + migrations.RemoveField( + model_name='studycolumns', + name='diagnosis', + ), + migrations.RemoveField( + model_name='studycolumns', + name='screening', + ), + migrations.RemoveField( + model_name='studycolumns', + name='brain_donation_agreement', + ), + migrations.RemoveField( + model_name='studycolumns', + name='pd_in_family', + ), + migrations.RemoveField( + model_name='studycolumns', + name='previously_in_study', + ), + migrations.RemoveField( + model_name='studycolumns', + name='year_of_diagnosis', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_1_collection_date', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_1_iga_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_1_igg_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_1_result', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_1_updated', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_2_collection_date', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_2_iga_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_2_igg_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_2_result', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_2_updated', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_3_collection_date', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_3_iga_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_3_igg_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_3_result', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_3_updated', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_4_collection_date', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_4_iga_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_4_igg_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_4_result', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_4_updated', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_5_collection_date', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_5_iga_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_5_igg_status', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_5_result', + ), + migrations.RemoveField( + model_name='studycolumns', + name='virus_test_5_updated', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_1_collection_date', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_1_iga_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_1_igg_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_1_result', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_1_updated', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_2_collection_date', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_2_iga_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_2_igg_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_2_result', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_2_updated', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_3_collection_date', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_3_iga_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_3_igg_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_3_result', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_3_updated', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_4_collection_date', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_4_iga_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_4_igg_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_4_result', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_4_updated', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_5_collection_date', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_5_iga_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_5_igg_status', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_5_result', + ), + migrations.RemoveField( + model_name='studysubject', + name='virus_test_5_updated', + + ), + + + migrations.RemoveField( + model_name='studysubject', + name='mpower_id', + ), + migrations.RemoveField( + model_name='studysubject', + name='diagnosis', + ), + migrations.RemoveField( + model_name='studysubject', + name='screening', + ), + migrations.RemoveField( + model_name='studysubject', + name='brain_donation_agreement', + ), + migrations.RemoveField( + model_name='studysubject', + name='pd_in_family', + ), + migrations.RemoveField( + model_name='studysubject', + name='previously_in_study', + ), + migrations.RemoveField( + model_name='studysubject', + name='year_of_diagnosis', + ), + migrations.AlterField( + model_name='studycolumns', + name='nd_number', + field=models.BooleanField(default=True, verbose_name='Subject number'), + ), + migrations.AlterField( + model_name='studysubject', + name='nd_number', + field=models.CharField(blank=True, max_length=25, verbose_name='Subject number'), + ), + ] diff --git a/smash/web/migrations/0192_auto_20201207_0956.py b/smash/web/migrations/0192_auto_20201207_0956.py new file mode 100644 index 0000000000000000000000000000000000000000..63b5cd45d2fdd440f26f0dc43208c9ad7037c015 --- /dev/null +++ b/smash/web/migrations/0192_auto_20201207_0956.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2020-12-07 09:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0191_auto_20201201_1033'), + ] + + operations = [ + migrations.AlterField( + model_name='customstudysubjectfield', + name='name', + field=models.CharField(max_length=64), + ), + ] diff --git a/smash/web/models/__init__.py b/smash/web/models/__init__.py index d6a3586ca9f80e5582d53892af72e7f282765454..56d7ea62f4e53372ea16b6423f6e43ff5549f19b 100644 --- a/smash/web/models/__init__.py +++ b/smash/web/models/__init__.py @@ -41,6 +41,7 @@ from .inconsistent_subject import InconsistentSubject, InconsistentField from .privacy_notice import PrivacyNotice from .etl import VisitImportData, SubjectImportData, EtlColumnMapping +from .custom_data import CustomStudySubjectVisibility __all__ = [Study, FlyingTeam, Appointment, AppointmentType, Availability, Holiday, Item, Language, Location, Room, Subject, StudySubject, StudySubjectList, SubjectColumns, StudyNotificationParameters, diff --git a/smash/web/models/custom_data/__init__.py b/smash/web/models/custom_data/__init__.py index e9587ebe1ac196cca3e76610cce3e02bcaff672c..19012a57213da153c11126ed619e784546322728 100644 --- a/smash/web/models/custom_data/__init__.py +++ b/smash/web/models/custom_data/__init__.py @@ -1,4 +1,5 @@ from .custom_study_subject_field import CustomStudySubjectField from .custom_study_subject_value import CustomStudySubjectValue +from .custom_study_subject_visibility import CustomStudySubjectVisibility -__all__ = [CustomStudySubjectField, CustomStudySubjectValue] +__all__ = [CustomStudySubjectField, CustomStudySubjectValue, CustomStudySubjectVisibility] diff --git a/smash/web/models/custom_data/custom_study_subject_field.py b/smash/web/models/custom_data/custom_study_subject_field.py index 5e096da35e40804ca12975a303bfe153cc7e15e2..dd2d183603701a2d13765bbd07d3173d6176977e 100644 --- a/smash/web/models/custom_data/custom_study_subject_field.py +++ b/smash/web/models/custom_data/custom_study_subject_field.py @@ -6,7 +6,7 @@ from web.models.constants import CUSTOM_FIELD_TYPE class CustomStudySubjectField(models.Model): - name = models.CharField(max_length=20, null=False, blank=False) + name = models.CharField(max_length=64, 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='') diff --git a/smash/web/models/custom_data/custom_study_subject_visibility.py b/smash/web/models/custom_data/custom_study_subject_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..16aa2f8f6eaebdbbafbe68e3f28d6a49964f21bb --- /dev/null +++ b/smash/web/models/custom_data/custom_study_subject_visibility.py @@ -0,0 +1,20 @@ +# coding=utf-8 + +from django.db import models + + +class CustomStudySubjectVisibility(models.Model): + visible = models.BooleanField(default=False, null=False) + + study_subject_field = models.ForeignKey("web.CustomStudySubjectField", + verbose_name='Custom Field', + editable=False, + null=False, + on_delete=models.CASCADE + ) + visible_subject_study_columns = models.ForeignKey("web.StudyColumns", + verbose_name='List of visible columns', + editable=False, + null=False, + on_delete=models.CASCADE + ) diff --git a/smash/web/models/mail_template.py b/smash/web/models/mail_template.py index bf101409a62f905e7c1c9f658bc262460517e4a9..633fbf4dd9c07175b3040ad9db6e54831baef33a 100644 --- a/smash/web/models/mail_template.py +++ b/smash/web/models/mail_template.py @@ -277,11 +277,11 @@ class MailTemplate(models.Model): if appointment.datetime_when is not None: # TODO: 2to3 # Was: - #appointment_date_full = appointment.datetime_when.strftime(DATETIME_FORMAT).decode( + # appointment_date_full = appointment.datetime_when.strftime(DATETIME_FORMAT).decode( # date_format_encoding()) - #appointment_date_short = appointment.datetime_when.strftime(DATE_FORMAT_SHORT).decode( + # appointment_date_short = appointment.datetime_when.strftime(DATE_FORMAT_SHORT).decode( # date_format_encoding()) - #appointment_date_time = appointment.datetime_when.strftime(DATE_FORMAT_TIME).decode( + # appointment_date_time = appointment.datetime_when.strftime(DATE_FORMAT_TIME).decode( # date_format_encoding()) # Is: appointment_date_full = appointment.datetime_when.strftime(DATETIME_FORMAT) @@ -315,8 +315,8 @@ class MailTemplate(models.Model): # "##V_DATE_START_FULL##": visit.datetime_begin.strftime(DATETIME_FORMAT).decode(date_format_encoding()), # "##V_DATE_START_SHORT##": visit.datetime_begin.strftime(DATE_FORMAT_SHORT).decode( # date_format_encoding()), - #"##V_DATE_ENDS_FULL##": visit.datetime_end.strftime(DATETIME_FORMAT).decode(date_format_encoding()), - #"##V_DATE_ENDS_SHORT##": visit.datetime_end.strftime(DATE_FORMAT_SHORT).decode(date_format_encoding()), + # "##V_DATE_ENDS_FULL##": visit.datetime_end.strftime(DATETIME_FORMAT).decode(date_format_encoding()), + # "##V_DATE_ENDS_SHORT##": visit.datetime_end.strftime(DATE_FORMAT_SHORT).decode(date_format_encoding()), # Is: "##V_DATE_START_FULL##": visit.datetime_begin.strftime(DATETIME_FORMAT), "##V_DATE_START_SHORT##": visit.datetime_begin.strftime(DATE_FORMAT_SHORT), @@ -338,13 +338,13 @@ class MailTemplate(models.Model): "##S_ADDRESS##": study_subject.subject.address, "##S_CITY##": study_subject.subject.city, "##S_COUNTRY##": str(study_subject.subject.country), - "##S_DIAGNOSIS_YEAR##": str(study_subject.year_of_diagnosis), + "##S_DIAGNOSIS_YEAR##": study_subject.get_custom_field_value('Year of diagnosis'), "##S_DATE_ADDED##": date_to_str(study_subject.date_added, DATE_FORMAT_SHORT), "##S_DATE_BORN##": date_born, - "##S_DIAGNOSIS##": str(study_subject.diagnosis), + "##S_DIAGNOSIS##": study_subject.get_custom_field_value('Diagnosis'), "##S_EMAIL##": str(study_subject.subject.email), "##S_SEX##": study_subject.subject.get_sex_display(), - "##S_MPOWER_ID##": study_subject.mpower_id, + "##S_MPOWER_ID##": study_subject.get_custom_field_value('MPower ID'), "##S_ND_NUMBER##": study_subject.nd_number, "##S_PHONE_NUMBER##": str(study_subject.subject.phone_number), "##S_PHONE_NUMBER_2##": str(study_subject.subject.phone_number_2), @@ -368,26 +368,25 @@ class MailTemplate(models.Model): for i in range(1, 6): virus_test_field = "##S_VIRUS_{}_RESULT##".format(i) - virus_test_value = virus_test_to_str(getattr(study_subject, "virus_test_{}".format(i)), - getattr(study_subject, "virus_test_{}_updated".format(i))) + virus_test_value = study_subject.get_custom_field_value('Virus {} RT-PCR'.format(i)) + if virus_test_value == "" or virus_test_value is None: + virus_test_value = "N/A" virus_test_date_field = "##S_VIRUS_{}_SAMPLE_COLLECTION_DATE##".format(i) - virus_test_date_value = date_to_str(getattr(study_subject, "virus_test_{}_collection_date".format(i)), - DATE_FORMAT_SHORT) + virus_test_date_value = study_subject.get_custom_field_value( + 'Visit {} RT-PCR collection date'.format(i)) virus_iga_status_field = "##S_VIRUS_{}_IGA_STATUS##".format(i) - virus_iga_status_value = getattr(study_subject, "virus_test_{}_iga_status".format(i)) + virus_iga_status_value = study_subject.get_custom_field_value('Visit {} IgA Status'.format(i)) if virus_iga_status_value is None: virus_iga_status_value = "" virus_igg_status_field = "##S_VIRUS_{}_IGG_STATUS##".format(i) - virus_igg_status_value = getattr(study_subject, "virus_test_{}_igg_status".format(i)) + virus_igg_status_value = study_subject.get_custom_field_value('Visit {} IgG Status'.format(i)) if virus_igg_status_value is None: virus_igg_status_value = "" if virus_test_date_value != "": - if virus_test_value == "": - virus_test_value = "N/A" if virus_iga_status_value == "": virus_iga_status_value = "N/A" if virus_igg_status_value == "": @@ -428,14 +427,3 @@ class MailTemplate(models.Model): "##C_HOURS##": str(voucher.hours), } return {} - - -def virus_test_to_str(test, date): - if test is None and date is not None: - return "Inconclusive" - if test is None: - return "" - if test: - return "Positive" - else: - return "Negative" diff --git a/smash/web/models/study_columns.py b/smash/web/models/study_columns.py index 858db85af3c29b436f20478bed2dd63220ada7ce..3050cdc9d7de4fa5a816d21dc24cddd0a1c8fcb7 100644 --- a/smash/web/models/study_columns.py +++ b/smash/web/models/study_columns.py @@ -1,6 +1,8 @@ # coding=utf-8 from django.db import models +from web.models.custom_data import CustomStudySubjectField, CustomStudySubjectVisibility + class StudyColumns(models.Model): class Meta: @@ -36,11 +38,7 @@ class StudyColumns(models.Model): ) nd_number = models.BooleanField( default=True, - verbose_name='ND number', - ) - mpower_id = models.BooleanField( - default=True, - verbose_name='MPower ID' + verbose_name='Subject number', ) comments = models.BooleanField( default=True, @@ -50,23 +48,11 @@ class StudyColumns(models.Model): default=True, verbose_name='Referred by' ) - diagnosis = models.BooleanField( - default=True, - verbose_name='Diagnosis' - ) - year_of_diagnosis = models.BooleanField( - default=True, - verbose_name='Year of diagnosis (YYYY)' - ) information_sent = models.BooleanField( default=True, verbose_name='Information sent', ) - pd_in_family = models.BooleanField( - default=True, - verbose_name='PD in family', - ) resigned = models.BooleanField( default=True, verbose_name='Resigned', @@ -97,16 +83,6 @@ class StudyColumns(models.Model): verbose_name='Agrees to give information to referral' ) - screening = models.BooleanField( - default=False, - verbose_name='Screening' - ) - - previously_in_study = models.BooleanField( - default=False, - verbose_name='Previously in PDP study', - ) - voucher_types = models.BooleanField( default=False, verbose_name='Voucher types', @@ -117,108 +93,25 @@ class StudyColumns(models.Model): verbose_name='Vouchers', ) - brain_donation_agreement = models.BooleanField( - 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', - ) - virus_test_1_collection_date = models.BooleanField( - default=False, - verbose_name='Visit 1 virus collection date', - ) - virus_test_2_collection_date = models.BooleanField( - default=False, - verbose_name='Visit 2 virus collection date', - ) - virus_test_3_collection_date = models.BooleanField( - default=False, - verbose_name='Visit 3 virus collection date', - ) - virus_test_4_collection_date = models.BooleanField( - default=False, - verbose_name='Visit 4 virus collection date', - ) - virus_test_5_collection_date = models.BooleanField( - default=False, - verbose_name='Visit 5 virus collection date', - ) - virus_test_1_iga_status = models.BooleanField( - default=False, - verbose_name='Visit 1 virus IgA status', - ) - virus_test_1_igg_status = models.BooleanField( - default=False, - verbose_name='Visit 1 virus IgG status', - ) - virus_test_2_iga_status = models.BooleanField( - default=False, - verbose_name='Visit 2 virus IgA status', - ) - virus_test_2_igg_status = models.BooleanField( - default=False, - verbose_name='Visit 2 virus IgG status', - ) - virus_test_3_iga_status = models.BooleanField( - default=False, - verbose_name='Visit 3 virus IgA status', - ) - virus_test_3_igg_status = models.BooleanField( - default=False, - verbose_name='Visit 3 virus IgG status', - ) - virus_test_4_iga_status = models.BooleanField( - default=False, - verbose_name='Visit 4 virus IgA status', - ) - virus_test_4_igg_status = models.BooleanField( - default=False, - verbose_name='Visit 4 virus IgG status', - ) - virus_test_5_iga_status = models.BooleanField( - default=False, - verbose_name='Visit 5 virus IgA status', - ) - virus_test_5_igg_status = models.BooleanField( - default=False, - verbose_name='Visit 5 virus IgG status', - ) + @property + def custom_fields_visibility(self): + study = self.studysubjectlist_set.all()[0].study + values = CustomStudySubjectVisibility.objects.filter(visible_subject_study_columns=self) + fields = list(CustomStudySubjectField.objects.filter(study=study)) + for value in values: + fields.remove(value.study_subject_field) + for field in fields: + CustomStudySubjectVisibility.objects.create(visible_subject_study_columns=self, study_subject_field=field) + return CustomStudySubjectVisibility.objects.filter(visible_subject_study_columns=self) + + def set_custom_field_visibility(self, custom_study_subject_field: CustomStudySubjectField, visible: bool): + for existing_value in self.custom_fields_visibility.all(): + if existing_value.study_subject_field == custom_study_subject_field: + existing_value.visible = visible + existing_value.save() + + def is_custom_field_visible(self, field: CustomStudySubjectField) -> bool: + for existing_value in self.custom_fields_visibility.all(): + if existing_value.study_subject_field == field: + return existing_value.visible + return False diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 39c77a02dac4b98add0562649b69cb6f6f44c07e..b7b1ac009f3720589ca67a61b9aa02eb2aa4fea3 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -1,16 +1,18 @@ # coding=utf-8 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.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE from web.models.custom_data import CustomStudySubjectValue, CustomStudySubjectField +VIRUS_CHOICES = ((None, 'N/A'), ('Inconclusive', 'Inconclusive'), ('Positive', 'Positive'), ('Negative', 'Negative')) + logger = logging.getLogger(__name__) @@ -98,11 +100,7 @@ class StudySubject(models.Model): ) nd_number = models.CharField(max_length=25, blank=True, - verbose_name='ND number', - ) - mpower_id = models.CharField(max_length=20, - blank=True, - verbose_name='MPower ID' + verbose_name='Subject number', ) comments = models.TextField(max_length=2000, blank=True, @@ -135,47 +133,15 @@ class StudySubject(models.Model): default=False, ) - screening = models.CharField(max_length=1024, - null=True, - blank=True, - verbose_name='Screening' - ) - - diagnosis = models.CharField(max_length=1024, - null=True, - blank=True, - verbose_name='Diagnosis' - ) - - previously_in_study = models.BooleanField( - verbose_name='Previously in PDP study', - default=False, - ) - voucher_types = models.ManyToManyField(VoucherType, blank=True, verbose_name='Voucher types' ) - year_of_diagnosis = models.IntegerField( - null=True, - blank=True, - verbose_name='Year of diagnosis (YYYY)' - ) - information_sent = models.BooleanField( verbose_name='Information sent', default=False ) - pd_in_family = models.BooleanField( - verbose_name='PD in family', - default=None, - null=True, - ) - brain_donation_agreement = models.BooleanField( - default=False, - verbose_name='Brain donation agreement', - ) resigned = models.BooleanField( verbose_name='Resigned', @@ -205,123 +171,6 @@ class StudySubject(models.Model): verbose_name='Endpoint reached comments' ) - virus_test_1 = models.BooleanField(choices=BOOL_CHOICES_WITH_NONE, - verbose_name='Visit 1 virus result', - default=None, - null=True - ) - virus_test_2 = models.BooleanField(choices=BOOL_CHOICES_WITH_NONE, - verbose_name='Visit 2 virus result', - default=None, - null=True - ) - virus_test_3 = models.BooleanField(choices=BOOL_CHOICES_WITH_NONE, - verbose_name='Visit 3 virus result', - default=None, - null=True - ) - virus_test_4 = models.BooleanField(choices=BOOL_CHOICES_WITH_NONE, - verbose_name='Visit 4 virus result', - default=None, - null=True - ) - virus_test_5 = models.BooleanField(choices=BOOL_CHOICES_WITH_NONE, - verbose_name='Visit 5 virus result', - default=None, - null=True - ) - 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, - ) - virus_test_1_collection_date = models.DateField(verbose_name='Visit 1 virus collection date', - blank=True, - null=True, - ) - virus_test_2_collection_date = models.DateField(verbose_name='Visit 2 virus collection date', - blank=True, - null=True, - ) - virus_test_3_collection_date = models.DateField(verbose_name='Visit 3 virus collection date', - blank=True, - null=True, - ) - virus_test_4_collection_date = models.DateField(verbose_name='Visit 4 virus collection date', - blank=True, - null=True, - ) - virus_test_5_collection_date = models.DateField(verbose_name='Visit 5 virus collection date', - blank=True, - null=True, - ) - - virus_test_1_iga_status = models.CharField(max_length=255, - verbose_name='Visit 1 IgA status', - blank=True, - null=True, - ) - virus_test_1_igg_status = models.CharField(max_length=255, - verbose_name='Visit 1 IgG status', - blank=True, - null=True, - ) - virus_test_2_iga_status = models.CharField(max_length=255, - verbose_name='Visit 2 IgA status', - blank=True, - null=True, - ) - virus_test_2_igg_status = models.CharField(max_length=255, - verbose_name='Visit 2 IgG status', - blank=True, - null=True, - ) - virus_test_3_iga_status = models.CharField(max_length=255, - verbose_name='Visit 3 IgA status', - blank=True, - null=True, - ) - virus_test_3_igg_status = models.CharField(max_length=255, - verbose_name='Visit 3 IgG status', - blank=True, - null=True, - ) - virus_test_4_iga_status = models.CharField(max_length=255, - verbose_name='Visit 4 IgA status', - blank=True, - null=True, - ) - virus_test_4_igg_status = models.CharField(max_length=255, - verbose_name='Visit 4 IgG status', - blank=True, - null=True, - ) - virus_test_5_iga_status = models.CharField(max_length=255, - verbose_name='Visit 5 IgA status', - blank=True, - null=True, - ) - virus_test_5_igg_status = models.CharField(max_length=255, - verbose_name='Visit 5 IgG status', - blank=True, - null=True, - ) - def sort_matched_screening_first(self, pattern, reverse=False): if self.screening_number is None: return None @@ -403,12 +252,24 @@ class StudySubject(models.Model): def __str__(self): return "%s %s" % (self.subject.first_name, self.subject.last_name) - def get_custom_data_value(self, custom_field) -> Optional[CustomStudySubjectValue]: + def get_custom_data_value(self, custom_field: CustomStudySubjectField) -> Optional[CustomStudySubjectValue]: for value in self.custom_data_values: if value.study_subject_field == custom_field: return value return None + def get_custom_field_value(self, param: str) -> str: + for value in self.custom_data_values.all(): + if value.study_subject_field.name == param: + return value.value + return '' + + def set_custom_field_value(self, param: str, value: str): + for custom_value in self.custom_data_values.all(): + if custom_value.study_subject_field.name == param: + custom_value.value = value + custom_value.save() + # SIGNALS @receiver(post_save, sender=StudySubject) diff --git a/smash/web/redcap_connector.py b/smash/web/redcap_connector.py index 70ae1d34e475aca3d2ab02faa0a0383fa9c5e8b3..7ccd582dd254dd3011da247b9098c4d6f48c772b 100644 --- a/smash/web/redcap_connector.py +++ b/smash/web/redcap_connector.py @@ -1,25 +1,26 @@ # coding=utf-8 -import io import datetime +import io import json import logging +from typing import Optional, List import certifi import pycurl import timeout_decorator from django.forms.models import model_to_dict from django_cron import CronJobBase, Schedule - from six import ensure_str from web.models import ConfigurationItem, StudySubject, Language, AppointmentType, Appointment, Visit, Study, \ Provenance, Worker, User from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, \ - 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, \ - GLOBAL_STUDY_ID, RED_CAP_SAMPLE_DATE_FIELD_TYPE, RED_CAP_KIT_ID_FIELD_TYPE, RED_CAP_IGA_STATUS_FIELD_TYPE, \ - RED_CAP_IGG_STATUS_FIELD_TYPE, IMPORTER_USER, IMPORT_APPOINTMENT_TYPE + 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, GLOBAL_STUDY_ID, RED_CAP_SAMPLE_DATE_FIELD_TYPE, \ + RED_CAP_KIT_ID_FIELD_TYPE, RED_CAP_IGA_STATUS_FIELD_TYPE, RED_CAP_IGG_STATUS_FIELD_TYPE, IMPORTER_USER, \ + IMPORT_APPOINTMENT_TYPE from web.models.inconsistent_subject import InconsistentField, InconsistentSubject from web.models.missing_subject import MissingSubject @@ -46,7 +47,6 @@ class RedcapSubject(object): class RedcapVisit(object): virus = None - virus_inconclusive = False visit_number = 0 virus_collection_date = None iga_status = None @@ -69,12 +69,12 @@ def different_string(string1, string2): return s1.strip() != s2.strip() -def date_equals(date1, date2): - if date1 is None and date2 is None: +def date_equals(date1: str, date2: datetime) -> bool: + if (date1 is None or date1 == '') and date2 is None: return True - if date1 is None or date2 is None: + if date1 is None or date1 == '' or date2 is None: return False - return date1.strftime("%Y-%m-%d") == date2.strftime("%Y-%m-%d") + return date1 == date2.strftime("%Y-%m-%d") class RedcapConnector(object): @@ -225,15 +225,18 @@ class RedcapConnector(object): 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) + 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() - if smasch_appointment.visit.is_finished != True: - description = '{} changed from "{}" to "{}"'.format('is_finished', - smasch_appointment.visit.is_finished, - True) + if not smasch_appointment.visit.is_finished: + description = '{} changed from "{}" to "{}"'.format( + 'is_finished', + smasch_appointment.visit.is_finished, + True) p = Provenance(modified_table=Visit._meta.db_table, modified_table_id=smasch_appointment.visit.id, modification_author=self.importer_user, @@ -241,91 +244,53 @@ class RedcapConnector(object): new_value=True, modification_description=description, modified_field='is_finished') + p.save() smasch_appointment.visit.is_finished = True smasch_appointment.visit.save() - if visit.virus is not None or visit.virus_inconclusive: - changes = [] - if visit.visit_number == 1 and subject.virus_test_1 != visit.virus: - changes.extend([('virus_test_1', visit.virus), - ('virus_test_1_updated', datetime.datetime.now())]) - if visit.visit_number == 2 and subject.virus_test_2 != visit.virus: - changes.extend([('virus_test_2', visit.virus), - ('virus_test_2_updated', datetime.datetime.now())]) - if visit.visit_number == 3 and subject.virus_test_3 != visit.virus: - changes.extend([('virus_test_3', visit.virus), - ('virus_test_3_updated', datetime.datetime.now())]) - if visit.visit_number == 4 and subject.virus_test_4 != visit.virus: - changes.extend([('virus_test_4', visit.virus), - ('virus_test_4_updated', datetime.datetime.now())]) - if visit.visit_number == 5 and subject.virus_test_5 != visit.virus: - changes.extend([('virus_test_5', visit.virus), - ('virus_test_5_updated', datetime.datetime.now())]) - if visit.visit_number == 1 and subject.virus_test_1_updated is None and visit.virus_inconclusive: - changes.extend([('virus_test_1_updated', datetime.datetime.now())]) - if visit.visit_number == 2 and subject.virus_test_2_updated is None and visit.virus_inconclusive: - changes.extend([('virus_test_2_updated', datetime.datetime.now())]) - if visit.visit_number == 3 and subject.virus_test_3_updated is None and visit.virus_inconclusive: - changes.extend([('virus_test_3_updated', datetime.datetime.now())]) - if visit.visit_number == 4 and subject.virus_test_4_updated is None and visit.virus_inconclusive: - changes.extend([('virus_test_4_updated', datetime.datetime.now())]) - if visit.visit_number == 5 and subject.virus_test_5_updated is None and visit.virus_inconclusive: - changes.extend([('virus_test_5_updated', datetime.datetime.now())]) - - if visit.visit_number == 1 and not date_equals(subject.virus_test_1_collection_date, - visit.virus_collection_date): - changes.extend([('virus_test_1_collection_date', visit.virus_collection_date)]) - if visit.visit_number == 2 and not date_equals(subject.virus_test_2_collection_date, - visit.virus_collection_date): - changes.extend([('virus_test_2_collection_date', visit.virus_collection_date)]) - if visit.visit_number == 3 and not date_equals(subject.virus_test_3_collection_date, - visit.virus_collection_date): - changes.extend([('virus_test_3_collection_date', visit.virus_collection_date)]) - if visit.visit_number == 4 and not date_equals(subject.virus_test_4_collection_date, - visit.virus_collection_date): - changes.extend([('virus_test_4_collection_date', visit.virus_collection_date)]) - if visit.visit_number == 5 and not date_equals(subject.virus_test_5_collection_date, - visit.virus_collection_date): - changes.extend([('virus_test_5_collection_date', visit.virus_collection_date)]) - - if visit.visit_number == 1 and subject.virus_test_1_iga_status != visit.iga_status: - changes.extend([('virus_test_1_iga_status', visit.iga_status)]) - if visit.visit_number == 1 and subject.virus_test_1_igg_status != visit.igg_status: - changes.extend([('virus_test_1_igg_status', visit.igg_status)]) - if visit.visit_number == 2 and subject.virus_test_2_iga_status != visit.iga_status: - changes.extend([('virus_test_2_iga_status', visit.iga_status)]) - if visit.visit_number == 2 and subject.virus_test_2_igg_status != visit.igg_status: - changes.extend([('virus_test_2_igg_status', visit.igg_status)]) - if visit.visit_number == 3 and subject.virus_test_3_iga_status != visit.iga_status: - changes.extend([('virus_test_3_iga_status', visit.iga_status)]) - if visit.visit_number == 3 and subject.virus_test_3_igg_status != visit.igg_status: - changes.extend([('virus_test_3_igg_status', visit.igg_status)]) - if visit.visit_number == 4 and subject.virus_test_4_iga_status != visit.iga_status: - changes.extend([('virus_test_4_iga_status', visit.iga_status)]) - if visit.visit_number == 4 and subject.virus_test_4_igg_status != visit.igg_status: - changes.extend([('virus_test_4_igg_status', visit.igg_status)]) - if visit.visit_number == 5 and subject.virus_test_5_iga_status != visit.iga_status: - changes.extend([('virus_test_5_iga_status', visit.iga_status)]) - if visit.visit_number == 5 and subject.virus_test_5_igg_status != visit.igg_status: - changes.extend([('virus_test_5_igg_status', visit.igg_status)]) - - # - if len(changes) > 0: - for field, new_value in changes: - old_value = getattr(subject, field) - description = '{} changed from "{}" to "{}"'.format(field, old_value, new_value) - p = Provenance(modified_table=StudySubject._meta.db_table, - modified_table_id=subject.id, - modification_author=self.importer_user, - previous_value=old_value, - new_value=new_value, - modification_description=description, - modified_field=field) - setattr(subject, field, new_value) - p.save() - subject.save() + self.update_data_from_redcap(subject, visit) return result + def update_data_from_redcap(self, subject: StudySubject, visit: RedcapVisit) -> List[Provenance]: + result = [] + if visit.virus is not None: + changes = [] + for i in range(1, 6): + if visit.visit_number == i: + result_label = "Virus {} RT-PCR".format(i - 1) + updated_label = "Visit {} RT-PCR update date".format(i - 1) + collect_label = "Visit {} RT-PCR collection date".format(i - 1) + iga_label = "Visit {} IgA Status".format(i - 1) + igg_label = "Visit {} IgG Status".format(i - 1) + if subject.get_custom_field_value(result_label) != visit.virus: + changes.extend([(result_label, visit.virus), + (updated_label, datetime.datetime.now().strftime("%Y-%m-%d"))]) + + if not date_equals(subject.get_custom_field_value(collect_label), visit.virus_collection_date): + changes.extend([(collect_label, visit.virus_collection_date.strftime("%Y-%m-%d"))]) + + if subject.get_custom_field_value(iga_label) != visit.iga_status: + changes.extend([(iga_label, visit.iga_status)]) + if subject.get_custom_field_value(igg_label) != visit.igg_status: + changes.extend([(igg_label, visit.igg_status)]) + + if len(changes) > 0: + for field, new_value in changes: + old_value = subject.get_custom_field_value(field) + description = '{} changed from "{}" to "{}"'.format(field, old_value, new_value) + p = Provenance(modified_table=StudySubject._meta.db_table, + modified_table_id=subject.id, + modification_author=self.importer_user, + previous_value=old_value, + new_value=new_value, + modification_description=description, + modified_field=field) + subject.set_custom_field_value(field, new_value) + p.save() + result.append(p) + subject.save() + return result + @staticmethod def check_sex_consistency(red_cap_subject, study_subject): if study_subject.subject.sex != red_cap_subject.sex: @@ -350,9 +315,11 @@ class RedcapConnector(object): return InconsistentField.create("dead", str(study_subject.subject.dead), str(red_cap_subject.dead)) @staticmethod - def check_mpower_id_consistency(red_cap_subject, study_subject): - if different_string(study_subject.mpower_id, red_cap_subject.mpower_id): - return InconsistentField.create("mpower id", study_subject.mpower_id, red_cap_subject.mpower_id) + def check_mpower_id_consistency(red_cap_subject: RedcapSubject, study_subject: StudySubject) \ + -> Optional[InconsistentField]: + if different_string(study_subject.get_custom_field_value('MPower ID'), red_cap_subject.mpower_id): + return InconsistentField.create("mpower id", study_subject.get_custom_field_value('MPower ID'), + red_cap_subject.mpower_id) @staticmethod def check_languages_consistency(red_cap_subject, study_subject): @@ -374,7 +341,8 @@ class RedcapConnector(object): return InconsistentField.create("languages", subject_languages, red_cap_subject_languages) @staticmethod - def create_inconsistency_subject(red_cap_subject, study_subject, url): + def create_inconsistency_subject(red_cap_subject: RedcapSubject, study_subject: StudySubject, + url: str) -> InconsistentSubject: # func dict field_checks = { 'sex': RedcapConnector.check_sex_consistency, @@ -435,11 +403,11 @@ class RedcapConnector(object): visit.visit_number = 1 if self.virus_field != "": if row.get(self.virus_field) == "Negative": - visit.virus = False + visit.virus = "Negative" elif row.get(self.virus_field) == "Positive": - visit.virus = True + visit.virus = "Positive" elif row.get(self.virus_field) == "Inconclusive": - visit.virus_inconclusive = True + visit.virus = "Inconclusive" if self.sample_date_field != "": date_str = row.get(self.sample_date_field) if date_str is not None and date_str != "" and date_str != "Not done" and date_str != "Not known": @@ -474,17 +442,18 @@ class RedcapConnector(object): visit.visit_number = i + self.study.redcap_first_visit_number + 1 if self.virus_field != "": if row.get(self.virus_field) == "Negative": - visit.virus = False + visit.virus = "Negative" elif row.get(self.virus_field) == "Positive": - visit.virus = True + visit.virus = "Positive" elif row.get(self.virus_field) == "Inconclusive": - visit.virus_inconclusive = True + visit.virus_inconclusive = "Inconclusive" if self.sample_date_field != "": date_str = row.get(self.sample_date_field) if date_str is not None and date_str != "" and date_str != "Not done" and date_str != "Not known": try: - visit.virus_collection_date = datetime.datetime.strptime(row.get(self.sample_date_field), - "%Y-%m-%d") + visit.virus_collection_date = datetime.datetime.strptime( + row.get(self.sample_date_field), + "%Y-%m-%d") except ValueError: logger.warning("Invalid date: " + row.get(self.sample_date_field)) visit.virus_collection_date = None @@ -566,7 +535,7 @@ class RedcapConnector(object): def execute_query(self, query_data, is_json=True): buf = io.BytesIO() - + curl_connection = pycurl.Curl() curl_connection.setopt(pycurl.CAINFO, certifi.where()) curl_connection.setopt(curl_connection.URL, self.base_url + "/api/") @@ -574,13 +543,13 @@ class RedcapConnector(object): curl_connection.setopt(curl_connection.WRITEFUNCTION, buf.write) curl_connection.perform() curl_connection.close() - + if is_json: val = buf.getvalue() data = json.loads(val) else: data = buf.getvalue() - + buf.close() return data diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index a7a3dcba9c3c820b7337fd3d5ec3451fdf77d8a7..408d2335df9751b63a12085aedc5e8e70d810ad4 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -314,7 +314,7 @@ function createTable(params) { $(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="yes_no_null_inconclusive_filter"]').each(function () { - $(this).html('<select style="width:100px" ><option value selected="selected">---</option><option value="true">Positive</option><option value="false">Negative</option><option value="null">N/A</option><option value="inconclusive">Inconclusive</option></select>'); + $(this).html('<select style="width:100px" ><option value selected="selected">---</option><option value="Positive">Positive</option><option value="Negative">Negative</option><option value="null">N/A</option><option value="Inconclusive">Inconclusive</option></select>'); }); $(tableElement).find('tfoot div[name="serology_filter"]').each(function () { $(this).html('<select style="width:100px" ><option value selected="selected">---</option><option value="Positive">Positive</option><option value="Negative">Negative</option><option value="Borderline">Borderline</option></select>'); diff --git a/smash/web/tests/api_views/test_appointment.py b/smash/web/tests/api_views/test_appointment.py index 899575b6696ba54a412df6a107cd277c86d1a207..c24aefe71e4cd50b61ea78f50dbe800392cf6777 100644 --- a/smash/web/tests/api_views/test_appointment.py +++ b/smash/web/tests/api_views/test_appointment.py @@ -16,7 +16,7 @@ from web.views.notifications import get_today_midnight_date class TestAppointmentApi(LoggedInWithWorkerTestCase): def setUp(self): - super(TestAppointmentApi, self).setUp() + super().setUp() self.study_subject = create_study_subject() def test_appointments_valid(self): diff --git a/smash/web/tests/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py index 9ecdc98169c49f37ccd62eb893bdd55c3bc1a57c..caccfdc9827ce01817eea6b7733cd40475e2a32f 100644 --- a/smash/web/tests/api_views/test_subject.py +++ b/smash/web/tests/api_views/test_subject.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) class TestSubjectApi(LoggedInWithWorkerTestCase): def setUp(self): - super(TestSubjectApi, self).setUp() + super().setUp() self.study_subject = create_study_subject() self.warning_counter = MsgCounterHandler() logging.getLogger('').addHandler(self.warning_counter) @@ -700,24 +700,6 @@ class TestSubjectApi(LoggedInWithWorkerTestCase): get_subjects_order(StudySubject.objects.none(), order, "asc") self.assertEqual(self.get_warning_count(), 0, msg="Sorting by \"" + order + "\" does not work") - def test_subjects_filter_virus_test(self): - study_subject_positive = create_study_subject(2) - study_subject_positive.virus_test_1 = True - study_subject_positive.save() - - study_subject_negative = create_study_subject(2) - study_subject_negative.virus_test_1 = False - study_subject_negative.save() - - study_subject_inconclusive = create_study_subject(2) - study_subject_inconclusive.virus_test_1 = None - study_subject_inconclusive.virus_test_1_updated = timezone.now() - study_subject_inconclusive.save() - - 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'), diff --git a/smash/web/tests/forms/test_StudySubjectAddForm.py b/smash/web/tests/forms/test_StudySubjectAddForm.py index 78cce7aa90f727965d703b68a33a0044c93c6393..58cecf19c55a5ae1a6e96c0323bbc4cbe07c143e 100644 --- a/smash/web/tests/forms/test_StudySubjectAddForm.py +++ b/smash/web/tests/forms/test_StudySubjectAddForm.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) class StudySubjectAddFormTests(LoggedInWithWorkerTestCase): def setUp(self): - super(StudySubjectAddFormTests, self).setUp() + super().setUp() location = self.worker.locations.all()[0] self.subject = create_subject() @@ -74,10 +74,8 @@ class StudySubjectAddFormTests(LoggedInWithWorkerTestCase): form_data['screening_number'] = "123" form = StudySubjectAddForm(data=form_data, user=self.user, study=self.study) - form.is_valid() - form.instance.subject_id = self.subject.id self.assertTrue(form.is_valid()) - self.assertIsNone(form.fields['year_of_diagnosis'].initial) + form.instance.subject_id = self.subject.id form.save() form2 = StudySubjectAddForm(data=form_data, user=self.user, study=self.study) @@ -101,22 +99,6 @@ class StudySubjectAddFormTests(LoggedInWithWorkerTestCase): self.assertFalse(validation_status) self.assertTrue("nd_number" in form2.errors) - def test_invalid_mpower_id(self): - form_data = self.sample_data - form_data['mpower_id'] = "123" - - form = StudySubjectAddForm(data=form_data, user=self.user, study=self.study) - form.is_valid() - self.assertTrue(form.is_valid()) - form.instance.subject_id = self.subject.id - form.save() - - form_data['screening_number'] = "2" - form2 = StudySubjectAddForm(data=form_data, user=self.user, study=self.study) - validation_status = form2.is_valid() - self.assertFalse(validation_status) - self.assertTrue("mpower_id" in form2.errors) - def test_get_new_screening_number(self): prefix = "X-" subject = create_study_subject() diff --git a/smash/web/tests/forms/test_StudySubjectEditForm.py b/smash/web/tests/forms/test_StudySubjectEditForm.py index 670bf37cc5f78d54670f0c934b596cda84eac4d1..f68632dc87130d311c3cb76a0a49211dd52c33f6 100644 --- a/smash/web/tests/forms/test_StudySubjectEditForm.py +++ b/smash/web/tests/forms/test_StudySubjectEditForm.py @@ -16,8 +16,9 @@ logger = logging.getLogger(__name__) class StudySubjectEditFormTests(LoggedInWithWorkerTestCase): def setUp(self): - super(StudySubjectEditFormTests, self).setUp() + super().setUp() self.study_subject = create_study_subject() + self.study = get_test_study() location = self.worker.locations.all()[0] self.sample_data = { @@ -100,21 +101,6 @@ class StudySubjectEditFormTests(LoggedInWithWorkerTestCase): 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" - self.study_subject.screening_number = "002" - self.study_subject.save() - - 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, 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) diff --git a/smash/web/tests/forms/test_subject_forms.py b/smash/web/tests/forms/test_subject_forms.py index fe109455c9595a3b3ee475a27ff23509e41a2e1b..9b45ee08278d984c84868700a1ac987e9e14084a 100644 --- a/smash/web/tests/forms/test_subject_forms.py +++ b/smash/web/tests/forms/test_subject_forms.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) class StudySubjectAddFormTests(LoggedInWithWorkerTestCase): def setUp(self): - super(StudySubjectAddFormTests, self).setUp() + super().setUp() self.subject = create_subject() def test_is_valid_social_security_number_too_short(self): diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index e34f1e3af6c3fb36acb2af10cda12cab667a5894..a4831422fe449fcf1ae7f111f790c096bd42f64f 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -51,13 +51,9 @@ def create_empty_study_columns(): flying_team=False, screening_number=False, nd_number=False, - mpower_id=False, comments=False, referral=False, - diagnosis=False, - year_of_diagnosis=False, information_sent=False, - pd_in_family=False, resigned=False, endpoint_reached=False, excluded=False, diff --git a/smash/web/tests/test_RedcapConnector.py b/smash/web/tests/test_RedcapConnector.py index 557ec07bffd3fc493a6319d07fb07debee37d79c..43d088ac760755e587cd72945bb18ec69fec30f4 100644 --- a/smash/web/tests/test_RedcapConnector.py +++ b/smash/web/tests/test_RedcapConnector.py @@ -1,16 +1,17 @@ # coding=utf-8 import logging -import unittest from django.test import TestCase -from .functions import create_study_subject, prepare_test_redcap_connection -from web.models import Language +from web.models import Language, StudySubject from web.models.inconsistent_subject import InconsistentSubject from web.models.missing_subject import MissingSubject -from web.redcap_connector import RedcapConnector, RedcapSubject, different_string +from web.redcap_connector import RedcapConnector, RedcapSubject, different_string, RedcapVisit from web.views.notifications import get_today_midnight_date +from .functions import create_study_subject, prepare_test_redcap_connection, get_test_study +from ..models.constants import CUSTOM_FIELD_TYPE_TEXT, CUSTOM_FIELD_TYPE_DATE +from ..models.custom_data import CustomStudySubjectField logger = logging.getLogger(__name__) @@ -120,9 +121,11 @@ class TestRedcapConnector(TestCase): def test_create_inconsistent_data_for_mpower_id(self): prepare_test_redcap_connection() subject = create_study_subject() + field = CustomStudySubjectField.objects.create(name="MPower ID", type=CUSTOM_FIELD_TYPE_TEXT, + study=subject.study, unique=True) redcap_subject = self.create_redcap_subject_from_smash_subject(subject) - subject.mpower_id = "105" + subject.set_custom_data_value(field, "105") self.check_single_inconsistency(redcap_subject, subject) @@ -139,11 +142,11 @@ class TestRedcapConnector(TestCase): self.assertIsNone(result) @staticmethod - def create_redcap_subject_from_smash_subject(study_subject): + def create_redcap_subject_from_smash_subject(study_subject: StudySubject) -> RedcapSubject: redcap_subject = RedcapSubject() for language in study_subject.subject.languages.all(): redcap_subject.add_language(language) - redcap_subject.mpower_id = study_subject.mpower_id + redcap_subject.mpower_id = study_subject.get_custom_field_value('MPower ID') redcap_subject.dead = study_subject.subject.dead redcap_subject.date_born = study_subject.subject.date_born redcap_subject.nd_number = study_subject.nd_number @@ -220,3 +223,26 @@ class TestRedcapConnector(TestCase): self.assertFalse(different_string("", None)) self.assertFalse(different_string(" abc", "abc ")) self.assertTrue(different_string("y", "x")) + + def test_update_data_from_redcap(self): + redcap_connection = RedcapConnector() + + collect_field = CustomStudySubjectField.objects.create(study=get_test_study(), + default_value='', + name='Visit 0 RT-PCR update date', + type=CUSTOM_FIELD_TYPE_DATE) + status_field = CustomStudySubjectField.objects.create(study=get_test_study(), + default_value='', + name='Virus 0 RT-PCR', + type=CUSTOM_FIELD_TYPE_TEXT) + + subject = create_study_subject() + redcap_visit = RedcapVisit() + redcap_visit.visit_number = 1 + redcap_visit.virus = "Inconclusive" + + provenances = redcap_connection.update_data_from_redcap(subject, redcap_visit) + self.assertTrue(len(provenances) > 0) + + self.assertEquals(subject.get_custom_data_value(status_field).value, 'Inconclusive') + self.assertTrue(subject.get_custom_data_value(collect_field).value != '') diff --git a/smash/web/tests/view/test_mail.py b/smash/web/tests/view/test_mail.py index 1f11340a796aedc4e9c0eebf7662ddd50d3cce33..c90273c97b3921d82fb3dc3a7d214d5ec22980dc 100644 --- a/smash/web/tests/view/test_mail.py +++ b/smash/web/tests/view/test_mail.py @@ -1,31 +1,38 @@ +import datetime import logging -from django.urls import reverse - -from web.models import MailTemplate -from web.models.constants import MAIL_TEMPLATE_CONTEXT_VOUCHER +from web.models.constants import CUSTOM_FIELD_TYPE_DATE, GLOBAL_STUDY_ID, CUSTOM_FIELD_TYPE_TEXT +from web.models.custom_data import CustomStudySubjectField from web.tests import LoggedInTestCase -from web.tests.functions import create_voucher, get_resource_path +from web.tests.functions import create_study_subject +from web.views.virus_mail import KitRequestEmailSendJob, count_subjects logger = logging.getLogger(__name__) -class MailTests(LoggedInTestCase): - def test_generate_vouchers(self): - voucher = create_voucher() - MailTemplate(name="name", language=None, - context=MAIL_TEMPLATE_CONTEXT_VOUCHER, - template_file=get_resource_path('upcoming_appointment_FR.docx')).save() - - page = reverse('web.views.mail_template_generate_for_vouchers') + "?voucher_id=" + str(voucher.id) - response = self.client.get(page) - self.assertEqual(response.status_code, 200) - - def test_list_mail_templates(self): - self.login_as_admin() - response = self.client.get(reverse("web.views.mail_templates")) - self.assertEqual(response.status_code, 200) - - def test_list_mail_templates_without_permission(self): - response = self.client.get(reverse("web.views.mail_templates")) - self.assertEqual(response.status_code, 302) +class VirusMailTests(LoggedInTestCase): + def test_send_email(self): + job = KitRequestEmailSendJob() + result = job.do() + self.assertEqual("mail sent", result) + + def test_empty_count_subjects(self): + result = count_subjects(datetime.datetime.now() - datetime.timedelta(days=1), "Positive") + self.assertEquals(0, result) + + def test_count_subjects(self): + create_study_subject() + study_subject = create_study_subject() + collect_field = CustomStudySubjectField.objects.create(study_id=GLOBAL_STUDY_ID, + default_value='', + name='Visit 0 RT-PCR collection date', + type=CUSTOM_FIELD_TYPE_DATE) + status_field = CustomStudySubjectField.objects.create(study_id=GLOBAL_STUDY_ID, + default_value='', + name='Virus 0 RT-PCR', + type=CUSTOM_FIELD_TYPE_TEXT) + study_subject.set_custom_data_value(collect_field, datetime.datetime.now().strftime("%Y-%m-%d")) + study_subject.set_custom_data_value(status_field, "Positive") + + result = count_subjects(datetime.datetime.now(), "Positive") + self.assertEquals(1, result) diff --git a/smash/web/tests/view/test_study_subject_list.py b/smash/web/tests/view/test_study_subject_list.py new file mode 100644 index 0000000000000000000000000000000000000000..8f0d1154d0cab2b0ec932ca1ed334a7001ace6d7 --- /dev/null +++ b/smash/web/tests/view/test_study_subject_list.py @@ -0,0 +1,58 @@ +import logging + +from django.urls import reverse + +from web.forms.study_subject_list_form import StudySubjectListEditForm, StudySubjectColumnsEditForm, \ + SubjectColumnsEditForm +from web.models import StudySubjectList, StudyColumns, SubjectColumns +from web.models.study_subject_list import SUBJECT_LIST_GENERIC +from web.tests import LoggedInWithWorkerTestCase +from web.tests.functions import get_test_study, format_form_field + +logger = logging.getLogger(__name__) + + +class StudySubjectListViewTests(LoggedInWithWorkerTestCase): + def setUp(self): + super().setUp() + self.study = get_test_study() + visible_subject_study_columns = StudyColumns.objects.create() + visible_subject_columns = SubjectColumns.objects.create() + self.list = StudySubjectList.objects.create(study=self.study, type=SUBJECT_LIST_GENERIC, + visible_subject_columns=visible_subject_columns, + visible_subject_study_columns=visible_subject_study_columns + ) + + def test_render_edit(self): + self.login_as_admin() + response = self.client.get(reverse('web.views.study_subject_list_edit', + kwargs={'study_id': self.study.id, 'study_subject_list_id': self.list.id})) + self.assertEqual(response.status_code, 200) + + def test_save_study(self): + self.login_as_admin() + form_data = self.create_edit_form_data_for_study() + + response = self.client.post(reverse('web.views.study_subject_list_edit', + kwargs={'study_id': self.study.id, 'study_subject_list_id': self.list.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + print(response['Location']) + self.assertFalse("subject_list" in response['Location']) + + def create_edit_form_data_for_study(self): + list_form = StudySubjectListEditForm(instance=self.list, prefix="list") + study_subject_columns_form = StudySubjectColumnsEditForm(instance=self.list.visible_subject_study_columns, + prefix="study_subject") + subject_columns_form = SubjectColumnsEditForm(instance=self.list.visible_subject_columns, + prefix="subject") + + form_data = {} + for key, value in list(list_form.initial.items()): + form_data['list-{}'.format(key)] = format_form_field(value) + for key, value in list(study_subject_columns_form.initial.items()): + form_data['study_subject-{}'.format(key)] = format_form_field(value) + for key, value in list(subject_columns_form.initial.items()): + form_data['subject_columns_form-{}'.format(key)] = format_form_field(value) + return form_data diff --git a/smash/web/tests/view/test_subjects.py b/smash/web/tests/view/test_subjects.py index 9551f3e7ed64921324fd712cca08a5634e9b06b1..c7cf50653eb0743465fdb64b200cb2737594a94f 100644 --- a/smash/web/tests/view/test_subjects.py +++ b/smash/web/tests/view/test_subjects.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) class SubjectsViewTests(LoggedInWithWorkerTestCase): def setUp(self): - super(SubjectsViewTests, self).setUp() + super().setUp() self.study_subject = create_study_subject() self.study = get_test_study() diff --git a/smash/web/tests/view/test_virus_mail.py b/smash/web/tests/view/test_virus_mail.py new file mode 100644 index 0000000000000000000000000000000000000000..1f11340a796aedc4e9c0eebf7662ddd50d3cce33 --- /dev/null +++ b/smash/web/tests/view/test_virus_mail.py @@ -0,0 +1,31 @@ +import logging + +from django.urls import reverse + +from web.models import MailTemplate +from web.models.constants import MAIL_TEMPLATE_CONTEXT_VOUCHER +from web.tests import LoggedInTestCase +from web.tests.functions import create_voucher, get_resource_path + +logger = logging.getLogger(__name__) + + +class MailTests(LoggedInTestCase): + def test_generate_vouchers(self): + voucher = create_voucher() + MailTemplate(name="name", language=None, + context=MAIL_TEMPLATE_CONTEXT_VOUCHER, + template_file=get_resource_path('upcoming_appointment_FR.docx')).save() + + page = reverse('web.views.mail_template_generate_for_vouchers') + "?voucher_id=" + str(voucher.id) + response = self.client.get(page) + self.assertEqual(response.status_code, 200) + + def test_list_mail_templates(self): + self.login_as_admin() + response = self.client.get(reverse("web.views.mail_templates")) + self.assertEqual(response.status_code, 200) + + def test_list_mail_templates_without_permission(self): + response = self.client.get(reverse("web.views.mail_templates")) + self.assertEqual(response.status_code, 302) diff --git a/smash/web/views/export.py b/smash/web/views/export.py index 8d69c70ada59bbc7fe88fac8e546a19afe98a2bb..193a35b9cf521ef6111775325d15a362e13e8f19 100644 --- a/smash/web/views/export.py +++ b/smash/web/views/export.py @@ -95,7 +95,7 @@ APPOINTMENT_TYPE_FIELD = CustomField({ }) STUDY_SUBJECT_FIELDS = [CustomField({ 'name': 'nd_number', - 'verbose_name': 'ND number' + 'verbose_name': 'Subject number' })] SUBJECT_FIELDS = [CustomField({ @@ -128,15 +128,6 @@ def get_default_subject_fields(study: Study): subject_fields.append(field) for field in StudySubject._meta.fields: if field.name.upper() != "ID" and field.name.upper() != "SUBJECT": - if visit_from_zero: - match = re.match(r'^virus_test_(\d*)$', field.name) - if match: - number = int(match.groups()[0]) - field.verbose_name = 'Virus {} RT-PCR'.format(display_visit_number(number)) - match = re.match(r'^virus_test_(\d*)_updated$', field.name) - if match: - number = int(match.groups()[0]) - 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(): diff --git a/smash/web/views/virus_mail.py b/smash/web/views/virus_mail.py index 9b6099cc04263b73261772d4e28f37317ad8f232..caf1e69eefc711657d427b2a2d3ed66ab9f88ea4 100644 --- a/smash/web/views/virus_mail.py +++ b/smash/web/views/virus_mail.py @@ -3,51 +3,50 @@ import datetime import logging import timeout_decorator +from django.db.models import When, Case, Min from django_cron import CronJobBase, Schedule from web.models import ConfigurationItem from web.models.constants import VIRUS_EMAIL_HOUR_CONFIGURATION_TYPE, \ - CRON_JOB_TIMEOUT + CRON_JOB_TIMEOUT, GLOBAL_STUDY_ID from ..models import StudySubject, Worker +from ..models.custom_data import CustomStudySubjectField from ..smash_email import EmailSender logger = logging.getLogger(__name__) +def count_subjects(date_when: datetime, status: str) -> int: + result = 0 + str_date = date_when.strftime("%Y-%m-%d") + for i in range(0, 10): + fields = CustomStudySubjectField.objects.filter(study_id=GLOBAL_STUDY_ID, + name='Visit {} RT-PCR collection date'.format(i)).all() + collect_field = None + if len(fields) > 0: + collect_field = fields[0] + fields = CustomStudySubjectField.objects.filter(study_id=GLOBAL_STUDY_ID, + name__contains='Virus {} RT-PCR'.format(i)).all() + status_field = None + if len(fields) > 0: + status_field = fields[0] + if collect_field is not None and status_field is not None: + print('exist') + result += StudySubject.objects.annotate( + custom_field_update_value=Min(Case(When(customstudysubjectvalue__study_subject_field=collect_field, + then='customstudysubjectvalue__value')))).annotate( + custom_field_status_value=Min(Case(When(customstudysubjectvalue__study_subject_field=status_field, + then='customstudysubjectvalue__value')))).filter( + custom_field_update_value=str_date, custom_field_status_value=status).count() + return result + + def get_subject_statistics(): date_from = datetime.datetime.now() - datetime.timedelta(days=1) - subjects_positive_count = StudySubject.objects.filter(virus_test_1_updated__gt=date_from, virus_test_1=True).count() - subjects_positive_count += StudySubject.objects.filter(virus_test_2_updated__gt=date_from, - virus_test_2=True).count() - subjects_positive_count += StudySubject.objects.filter(virus_test_3_updated__gt=date_from, - virus_test_3=True).count() - subjects_positive_count += StudySubject.objects.filter(virus_test_4_updated__gt=date_from, - virus_test_4=True).count() - subjects_positive_count += StudySubject.objects.filter(virus_test_5_updated__gt=date_from, - virus_test_5=True).count() - - subjects_negative_count = StudySubject.objects.filter(virus_test_1_updated__gt=date_from, - virus_test_1=False).count() - subjects_negative_count += StudySubject.objects.filter(virus_test_2_updated__gt=date_from, - virus_test_2=False).count() - subjects_negative_count += StudySubject.objects.filter(virus_test_3_updated__gt=date_from, - virus_test_3=False).count() - subjects_negative_count += StudySubject.objects.filter(virus_test_4_updated__gt=date_from, - virus_test_4=False).count() - subjects_negative_count += StudySubject.objects.filter(virus_test_5_updated__gt=date_from, - virus_test_5=False).count() - - subjects_inconclusive_count = StudySubject.objects.filter(virus_test_1_updated__gt=date_from, - virus_test_1__isnull=True).count() - subjects_inconclusive_count += StudySubject.objects.filter(virus_test_2_updated__gt=date_from, - virus_test_2__isnull=True).count() - subjects_inconclusive_count += StudySubject.objects.filter(virus_test_3_updated__gt=date_from, - virus_test_3__isnull=True).count() - subjects_inconclusive_count += StudySubject.objects.filter(virus_test_4_updated__gt=date_from, - virus_test_4__isnull=True).count() - subjects_inconclusive_count += StudySubject.objects.filter(virus_test_5_updated__gt=date_from, - virus_test_5__isnull=True).count() + subjects_positive_count = count_subjects(date_from, "Positive") + subjects_negative_count = count_subjects(date_from, "Negative") + subjects_inconclusive_count = count_subjects(date_from, "Inconclusive") return {"Positive": subjects_positive_count, "Negative": subjects_negative_count, "Inconclusive": subjects_inconclusive_count, @@ -87,11 +86,11 @@ def create_statistic_email_content(data, title): email_body += "</table>" - #SARS-COV2 Virus  | Number of Donors - #Inconclusive  | 5 - #Positive  | 15 - #Negative  | 45 - #Total  | 65 + # SARS-COV2 Virus  | Number of Donors + # Inconclusive  | 5 + # Positive  | 15 + # Negative  | 45 + # Total  | 65 return email_body @@ -117,12 +116,12 @@ class KitRequestEmailSendJob(CronJobBase): # noinspection PyBroadException try: - times = ConfigurationItem.objects.get( - type=VIRUS_EMAIL_HOUR_CONFIGURATION_TYPE).value.split(";") + times = ConfigurationItem.objects.get(type=VIRUS_EMAIL_HOUR_CONFIGURATION_TYPE).value.split(";") for entry in times: - # TODO it's a hack assuming that we are in CEST - text = str((int(entry.split(":")[0]) + 22) % 24) + ":" + entry.split(":")[1] - RUN_AT.append(text) + if entry != '': + # TODO it's a hack assuming that we are in CEST + text = str((int(entry.split(":")[0]) + 22) % 24) + ":" + entry.split(":")[1] + RUN_AT.append(text) except BaseException as e: logger.debug("Cannot fetch data about email hour") schedule = Schedule(run_at_times=RUN_AT)