diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index 89e2c9c19c2eb5aa958a17165fe12d440b842c58..5f6cd27120fd039257378a8b3b83cedcc55b1d8e 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -10,6 +10,15 @@ def bool_to_yes_no(val): return "NO" +def bool_to_yes_no_null(val): + if val is None: + return "N/A" + if val: + return "YES" + else: + return "NO" + + def flying_team_to_str(flying_team): result = "" if flying_team is not None: diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index c31d13903ab43b26fa7db0fd1f9c570d28e5cb5e..cd7b5ac5dd59429c1457ea3fd30d5f90a4b87c0a 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -1,12 +1,12 @@ import logging -from django.urls import reverse from django.db.models import Count, Case, When, Min, Max from django.db.models import Q from django.http import JsonResponse +from django.urls import reverse from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \ - serialize_date, serialize_datetime, get_filters_for_data_table_request + serialize_date, serialize_datetime, get_filters_for_data_table_request, bool_to_yes_no_null from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study, ContactAttempt from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ @@ -73,12 +73,25 @@ def get_subject_columns(request, subject_list_type): add_column(result, "Next of keen", "next_of_keen_name", subject_columns, "string_filter") add_column(result, "Next of keen phone", "next_of_keen_phone", subject_columns, "string_filter") add_column(result, "Next of keen address", "next_of_keen_address", subject_columns, "string_filter") - add_column(result, "Brain donation agreement", "brain_donation_agreement", study_subject_columns, "yes_no_filter", study.columns) + add_column(result, "Brain donation agreement", "brain_donation_agreement", study_subject_columns, "yes_no_filter", + study.columns) add_column(result, "Excluded", "excluded", study_subject_columns, "yes_no_filter", study.columns) add_column(result, "Info sent", "information_sent", study_subject_columns, "yes_no_filter", study.columns) + + add_column(result, "Visit 1 virus", "virus_test_1", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 1 virus date", "virus_test_1_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 2 virus", "virus_test_2", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 2 virus date", "virus_test_2_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 3 virus", "virus_test_3", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 3 virus date", "virus_test_3_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 4 virus", "virus_test_4", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 4 virus date", "virus_test_4_updated", study_subject_columns, None, study.columns) + add_column(result, "Visit 5 virus", "virus_test_5", study_subject_columns, "yes_no_null_filter", study.columns) + add_column(result, "Visit 5 virus date", "virus_test_5_updated", study_subject_columns, None, study.columns) + add_column(result, "Type", "type", study_subject_columns, "type_filter", study.columns) add_column(result, "Edit", "edit", None, None, sortable=False) - for visit_number in range(1, study.visits_to_show_in_subject_list+1): + for visit_number in range(1, study.visits_to_show_in_subject_list + 1): visit_key = "visit_" + str(visit_number) add_column(result, "Visit " + str(visit_number), visit_key, None, "visit_filter", visible_param=study_subject_list.visits) @@ -133,7 +146,8 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co else: pattern = column_filters[u'screening_number'] result = subjects_to_be_ordered.all() - result = sorted(result, key=lambda t: t.sort_matched_screening_first(pattern, reverse=order_direction == '-'), reverse = order_direction == '-' ) + result = sorted(result, key=lambda t: t.sort_matched_screening_first(pattern, reverse=order_direction == '-'), + reverse=order_direction == '-') elif order_column == "default_location": result = subjects_to_be_ordered.order_by(order_direction + 'default_location') elif order_column == "flying_team": @@ -173,6 +187,26 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, co elif str(order_column).startswith("visit_"): visit_number = get_visit_number_from_visit_x_string(order_column) result = order_by_visit(subjects_to_be_ordered, order_direction, visit_number) + elif order_column == "virus_test_1": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_1') + elif order_column == "virus_test_2": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_2') + elif order_column == "virus_test_3": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_3') + elif order_column == "virus_test_4": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_4') + elif order_column == "virus_test_5": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_5') + elif order_column == "virus_test_1_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_1_updated') + elif order_column == "virus_test_2_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_2_updated') + elif order_column == "virus_test_3_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_3_updated') + elif order_column == "virus_test_4_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_4_updated') + elif order_column == "virus_test_5_updated": + result = subjects_to_be_ordered.order_by(order_direction + 'virus_test_5_updated') else: logger.warn("Unknown sort column: " + str(order_column)) return result @@ -272,6 +306,31 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): result = result.filter(resigned=(value == "true")) elif column == "endpoint_reached": result = result.filter(endpoint_reached=(value == "true")) + elif column == "virus_test_1": + if value == "null": + result = result.filter(virus_test_1__isnull=True) + else: + result = result.filter(virus_test_1=(value == "true")) + elif column == "virus_test_2": + if value == "null": + result = result.filter(virus_test_2__isnull=True) + else: + result = result.filter(virus_test_2=(value == "true")) + elif column == "virus_test_3": + if value == "null": + result = result.filter(virus_test_3__isnull=True) + else: + result = result.filter(virus_test_3=(value == "true")) + elif column == "virus_test_4": + if value == "null": + result = result.filter(virus_test_4__isnull=True) + else: + result = result.filter(virus_test_4=(value == "true")) + elif column == "virus_test_5": + if value == "null": + result = result.filter(virus_test_5__isnull=True) + else: + result = result.filter(virus_test_5=(value == "true")) elif column == "brain_donation_agreement": result = result.filter(brain_donation_agreement=(value == "true")) elif column == "postponed": @@ -358,6 +417,7 @@ def types(request): "types": data }) + def serialize_subject(study_subject): location = location_to_str(study_subject.default_location) flying_team = flying_team_to_str(study_subject.flying_team) @@ -388,7 +448,7 @@ def serialize_subject(study_subject): status = "SHOULD_BE_IN_PROGRESS" else: status = "UPCOMING" - + appointment_types = ['{} ({})'.format(at.code, at.description) for at in visit.appointment_types.all()] if len(appointment_types) == 0: appointment_types = ['No appointment types set.'] @@ -436,6 +496,16 @@ def serialize_subject(study_subject): "dead": bool_to_yes_no(study_subject.subject.dead), "resigned": bool_to_yes_no(study_subject.resigned), "endpoint_reached": bool_to_yes_no(study_subject.endpoint_reached), + "virus_test_1": bool_to_yes_no_null(study_subject.virus_test_1), + "virus_test_2": bool_to_yes_no_null(study_subject.virus_test_2), + "virus_test_3": bool_to_yes_no_null(study_subject.virus_test_3), + "virus_test_4": bool_to_yes_no_null(study_subject.virus_test_4), + "virus_test_5": bool_to_yes_no_null(study_subject.virus_test_5), + "virus_test_1_updated": study_subject.virus_test_1_updated, + "virus_test_2_updated": study_subject.virus_test_2_updated, + "virus_test_3_updated": study_subject.virus_test_3_updated, + "virus_test_4_updated": study_subject.virus_test_4_updated, + "virus_test_5_updated": study_subject.virus_test_5_updated, "postponed": bool_to_yes_no(study_subject.postponed), "brain_donation_agreement": bool_to_yes_no(study_subject.brain_donation_agreement), "excluded": bool_to_yes_no(study_subject.excluded), @@ -447,4 +517,4 @@ def serialize_subject(study_subject): "id": study_subject.id, "visits": serialized_visits, } - return result \ No newline at end of file + return result diff --git a/smash/web/forms/study_subject_forms.py b/smash/web/forms/study_subject_forms.py index 2bc880780226734cfa1653501960eb47bc8138b0..1f75992f6793598ef7d2c760af61af98ab90ab09 100644 --- a/smash/web/forms/study_subject_forms.py +++ b/smash/web/forms/study_subject_forms.py @@ -36,6 +36,17 @@ class StudySubjectAddForm(StudySubjectForm): self.study = get_study_from_args(kwargs) super(StudySubjectAddForm, self).__init__(*args, **kwargs) + self.fields['virus_test_1'].widget.attrs['readonly'] = True + self.fields['virus_test_2'].widget.attrs['readonly'] = True + self.fields['virus_test_3'].widget.attrs['readonly'] = True + self.fields['virus_test_4'].widget.attrs['readonly'] = True + self.fields['virus_test_5'].widget.attrs['readonly'] = True + self.fields['virus_test_1_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_2_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_3_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_4_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_5_updated'].widget.attrs['readonly'] = True + prepare_study_subject_fields(fields=self.fields, study=self.study) def save(self, commit=True): @@ -128,6 +139,18 @@ class StudySubjectEditForm(StudySubjectForm): self.fields['resigned'].disabled = was_resigned self.fields['endpoint_reached'].disabled = endpoint_was_reached + + self.fields['virus_test_1'].widget.attrs['readonly'] = True + self.fields['virus_test_2'].widget.attrs['readonly'] = True + self.fields['virus_test_3'].widget.attrs['readonly'] = True + self.fields['virus_test_4'].widget.attrs['readonly'] = True + self.fields['virus_test_5'].widget.attrs['readonly'] = True + self.fields['virus_test_1_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_2_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_3_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_4_updated'].widget.attrs['readonly'] = True + self.fields['virus_test_5_updated'].widget.attrs['readonly'] = True + prepare_study_subject_fields(fields=self.fields, study=self.study) def clean(self): @@ -181,6 +204,17 @@ def prepare_study_subject_fields(fields, study): prepare_field(fields, study.columns, 'previously_in_study') prepare_field(fields, study.columns, 'voucher_types') + prepare_field(fields, study.columns, 'virus_test_1') + prepare_field(fields, study.columns, 'virus_test_2') + prepare_field(fields, study.columns, 'virus_test_3') + prepare_field(fields, study.columns, 'virus_test_4') + prepare_field(fields, study.columns, 'virus_test_5') + prepare_field(fields, study.columns, 'virus_test_1_updated') + prepare_field(fields, study.columns, 'virus_test_2_updated') + prepare_field(fields, study.columns, 'virus_test_3_updated') + prepare_field(fields, study.columns, 'virus_test_4_updated') + prepare_field(fields, study.columns, 'virus_test_5_updated') + def validate_subject_screening_number(self, cleaned_data): if self.study.columns.resign_reason: diff --git a/smash/web/importer/csv_tns_visit_import_reader.py b/smash/web/importer/csv_tns_visit_import_reader.py index 8bc4942a9639dff41e4885e24cd727543360720e..530696cb3f4c433f79ba2289496ee27065a5f77c 100644 --- a/smash/web/importer/csv_tns_visit_import_reader.py +++ b/smash/web/importer/csv_tns_visit_import_reader.py @@ -4,6 +4,7 @@ import datetime import logging import sys import traceback +import codecs import pytz from django.conf import settings @@ -38,7 +39,7 @@ class TnsCsvVisitImportReader: result = [] with open(filename) as csv_file: - reader = csv.reader(csv_file, delimiter=';') + reader = csv.reader((remove_bom(line) for line in csv_file), delimiter=';') headers = next(reader, None) for row in reader: try: @@ -48,7 +49,7 @@ class TnsCsvVisitImportReader: nd_number = data['donor_id'] study_subjects = StudySubject.objects.filter(nd_number=nd_number) if len(study_subjects) == 0: - logger.warn("Subject " + nd_number + " does not exist") + logger.debug("Subject " + nd_number + " does not exist. Creating") subject = Subject.objects.create() study_subject = StudySubject.objects.create(subject=subject, study=Study.objects.filter(id=GLOBAL_STUDY_ID)[0], @@ -77,7 +78,7 @@ class TnsCsvVisitImportReader: visits = Visit.objects.filter(subject=study_subject, visit_number=visit_number) if len(visits) > 0: - logger.warn("Visit for subject " + nd_number + " already exists. Updating") + logger.debug("Visit for subject " + nd_number + " already exists. Updating") visit = visits[0] visit.datetime_begin = date visit.datetime_end = date + datetime.timedelta(days=14) @@ -90,7 +91,7 @@ class TnsCsvVisitImportReader: appointments = Appointment.objects.filter(visit=visit, appointment_types=self.appointment_type) if len(appointments) > 0: - logger.warn("Appointment for subject " + nd_number + " already set. Updating") + logger.debug("Appointment for subject " + nd_number + " already set. Updating") appointment = appointments[0] appointment.length = 60 appointment.datetime_when = date @@ -134,12 +135,75 @@ class TnsCsvVisitImportReader: text = data.get('adressofvisit', None) if text is None: text = data['lab_id'] - if text.startswith('lab-reunis'): - text = u"Laboratoires réunis" - if text.startswith('lab-ketterthill'): - text = u"Ketterthill" - if text.startswith('lab-bionex'): - text = u"BioneXt" + + if text.startswith('lab-reunis-1'): + text='Laboratoires réunis, 23 Route de Diekirch, 6555, Bollendorf-Pont' + if text.startswith('lab-reunis-2'): + text='Laboratoires réunis, 38 Rue Hiehl, 6131, Junglinster' + if text.startswith('lab-reunis-3'): + text='Laboratoires réunis, 16 Rue de la Gare, 6117, Junglinster' + if text.startswith('lab-reunis-4'): + text='Laboratoires réunis, 456 Rue de Neudorf, 2222, Luxembourg' + if text.startswith('lab-reunis-5'): + text='Laboratoires réunis, 14 Place St Michel, 7556, Mersch' + if text.startswith('lab-reunis-6'): + text='Laboratoires réunis, 2 Avenue des Bains, 5610, Mondorf-les-Bains' + if text.startswith('lab-reunis-7'): + text='Laboratoires réunis, 239 Route d\'Arlon, 8011, Strassen' + if text.startswith('lab-reunis-8'): + text='Laboratoires réunis, 123 Route de Diekirch, 7220, Walferdange' + if text.startswith('lab-reunis-9'): + text='Laboratoires réunis, 20 Rue de Luxembourg, 4220, Esch-sur-Alzette' + if text.startswith('lab-reunis-10'): + text='Laboratoires réunis, 124 Avenue de Luxembourg, 4940, Bascharage' + if text.startswith('lab-reunis-11'): + text='Laboratoires réunis, 1 Marbuergerstrooss, 9764, Marnach' + if text.startswith('lab-reunis-12'): + text='Laboratoires réunis, 51 Avenue Lucien Salentiny, 9080, Ettelbruck' + if text.startswith('lab-reunis-13'): + text='Laboratoires réunis, 14 route de l\'Europe, 5531, Remich' + if text.startswith('lab-reunis-14'): + text='Laboratoires réunis, 27, rue Principale, 5240, Sandweiler' + if text.startswith('lab-reunis-15'): + text='Laboratoires réunis, booking by phone 780 290-1, , ' + if text.startswith('lab-bionext-16'): + text='BioneXt, 2 Rue du Chateau d\'Eau, 3364, Leudelange' + if text.startswith('lab-bionextpd-17'): + text='BioneXt, PickenDoheem' + if text.startswith('lab-ketterthill-18'): + text='Ketterthill, 11, rue Schwaarze Wee , 3474, Dudelange' + if text.startswith('lab-ketterthill-19'): + text='Ketterthill, 52, bd J.-F. Kennedy , 4170, Esch-sur-Alzette' + if text.startswith('lab-ketterthill-20'): + text='Ketterthill, 7, route de Bettembourg , 5810, Hesperange' + if text.startswith('lab-ketterthill-21'): + text='Ketterthill, Avenue des Bains (Dom. Thermal) , 5601, Mondorf-les-Bains' + if text.startswith('lab-ketterthill-22'): + text='Ketterthill, 8, avenue du Swing , 4367, Belvaux' + if text.startswith('lab-ketterthill-23'): + text='Ketterthill, 1-3, rue de la Continentale , 4917, Bascharage' + if text.startswith('lab-ketterthill-24'): + text='Ketterthill, 14, rue d\'Esch, 3920, Mondercange' + if text.startswith('lab-ketterthill-25'): + text='Ketterthill, 21, rue d\'Orval , 2270, Luxembourg' + if text.startswith('lab-ketterthill-26'): + text='Ketterthill, 24, rue Glesener , 1630, Luxembourg' + if text.startswith('lab-ketterthill-27'): + text='Ketterthill, 36, avenue Victor Hugo , 1750, Luxembourg' + if text.startswith('lab-ketterthill-28'): + text='Ketterthill, 70, rue de Luxembourg , 8140, Bridel' + if text.startswith('lab-ketterthill-29'): + text='Ketterthill, 15, rue Edward Steichen , 2540, Luxembourg' + if text.startswith('lab-ketterthill-30'): + text='Ketterthill, 29, rue Cents , 1319, Luxembourg' + if text.startswith('lab-ketterthill-31'): + text='Ketterthill, 155, rue Lucien Salentiny , 9080, Ettelbruck' + if text.startswith('lab-ketterthill-32'): + text='Ketterthill, 12, rue G.-D. Charlotte , 7520, Mersch' + if text.startswith('lab-ketterthill-33'): + text='Ketterthill, 18, rue de la Piscine , 8508, Redange-sur-Atert' + if text.startswith('lab-ketterthill-34'): + text='Ketterthill, 19, rue Grande-Duchesse Charlotte , 9515, Wiltz' locations = Location.objects.filter(name=text) if len(locations) > 0: return locations[0] @@ -159,3 +223,6 @@ class TnsCsvVisitImportReader: style = ' color="brown" ' result += "<p><font " + style + ">Number of raised warnings: <b>" + str(self.warning_count) + "</b></font></p>" return result + +def remove_bom(line): + return line[3:] if line.startswith(codecs.BOM_UTF8) else line diff --git a/smash/web/migrations/0159_configurationitem_email_items_for_redcap.py b/smash/web/migrations/0159_configurationitem_email_items_for_redcap.py new file mode 100644 index 0000000000000000000000000000000000000000..f496a5a3f92489be49b66ac9dbac156eb07e46db --- /dev/null +++ b/smash/web/migrations/0159_configurationitem_email_items_for_redcap.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-04-04 09:43 +from __future__ import unicode_literals + +from django.db import migrations + +from web.models.constants import RED_CAP_LANGUAGE_4_FIELD_TYPE, RED_CAP_LANGUAGE_3_FIELD_TYPE, \ + RED_CAP_LANGUAGE_2_FIELD_TYPE, RED_CAP_LANGUAGE_1_FIELD_TYPE, RED_CAP_MPOWER_ID_FIELD_TYPE, RED_CAP_DEAD_FIELD_TYPE, \ + RED_CAP_SEX_FIELD_TYPE, RED_CAP_DATE_BORN_FIELD_TYPE, RED_CAP_ND_NUMBER_FIELD_TYPE, RED_CAP_VIRUS_FIELD_TYPE + + +def create_item(apps, type, value, name): + # We can't import the ConfigurationItem model directly as it may be a newer + # version than this migration expects. We use the historical version. + ConfigurationItem = apps.get_model("web", "ConfigurationItem") + item = ConfigurationItem.objects.create() + item.type = type + item.value = value + item.name = name + item.save() + + +def configuration_items(apps, schema_editor): + create_item(apps, RED_CAP_LANGUAGE_4_FIELD_TYPE, "dm_language_4", + "Redcap field for language 4") + create_item(apps, RED_CAP_LANGUAGE_3_FIELD_TYPE, "dm_language_3", + "Redcap field for language 3") + create_item(apps, RED_CAP_LANGUAGE_2_FIELD_TYPE, "dm_language_2", + "Redcap field for language 2") + create_item(apps, RED_CAP_LANGUAGE_1_FIELD_TYPE, "dm_language_1", + "Redcap field for language 1") + create_item(apps, RED_CAP_MPOWER_ID_FIELD_TYPE, "dm_mpowerid", + "Redcap field for mPowerId") + create_item(apps, RED_CAP_DEAD_FIELD_TYPE, "dm_death", + "Redcap field for deceased") + create_item(apps, RED_CAP_SEX_FIELD_TYPE, "cdisc_dm_sex", + "Redcap field for sex") + create_item(apps, RED_CAP_DATE_BORN_FIELD_TYPE, "cdisc_dm_brthdtc", + "Redcap field for birth date") + create_item(apps, RED_CAP_ND_NUMBER_FIELD_TYPE, "cdisc_dm_usubjd", + "Redcap field for subject id") + create_item(apps, RED_CAP_VIRUS_FIELD_TYPE, "", + "Redcap field for virus test result") + + +class Migration(migrations.Migration): + dependencies = [ + ('web', '0158_configurationitem_email_items'), + ] + + operations = [ + migrations.RunPython(configuration_items), + ] diff --git a/smash/web/migrations/0160_auto_20200415_1101.py b/smash/web/migrations/0160_auto_20200415_1101.py new file mode 100644 index 0000000000000000000000000000000000000000..f00f970dcc844171d9c43b7743823b5e4b2772bb --- /dev/null +++ b/smash/web/migrations/0160_auto_20200415_1101.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-15 11:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0159_configurationitem_email_items_for_redcap'), + ] + + operations = [ + migrations.AddField( + model_name='studycolumns', + name='virus_test_1', + field=models.BooleanField(default=False, verbose_name=b'Visit 1 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_1_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 1 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_2', + field=models.BooleanField(default=False, verbose_name=b'Visit 2 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_2_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 2 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_3', + field=models.BooleanField(default=False, verbose_name=b'Visit 3 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_3_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 3 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_4', + field=models.BooleanField(default=False, verbose_name=b'Visit 4 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_4_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 4 virus results date'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_5', + field=models.BooleanField(default=False, verbose_name=b'Visit 5 virus results'), + ), + migrations.AddField( + model_name='studycolumns', + name='virus_test_5_updated', + field=models.BooleanField(default=False, verbose_name=b'Visit 5 virus results date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_1', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 1 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_1_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 1 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_2', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 2 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_2_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 2 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_3', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 3 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_3_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 3 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_4', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 4 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_4_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 4 virus result date'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_5', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, editable=False, verbose_name=b'Visit 5 virus result'), + ), + migrations.AddField( + model_name='studysubject', + name='virus_test_5_updated', + field=models.DateField(editable=False, null=True, verbose_name=b'Visit 5 virus result date'), + ), + ] diff --git a/smash/web/migrations/0161_auto_20200416_0736.py b/smash/web/migrations/0161_auto_20200416_0736.py new file mode 100644 index 0000000000000000000000000000000000000000..babe517464ca8315c824671d5c8d625bce3ec907 --- /dev/null +++ b/smash/web/migrations/0161_auto_20200416_0736.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-16 07:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0160_auto_20200415_1101'), + ] + + operations = [ + migrations.AddField( + model_name='study', + name='sample_mail_statistics', + field=models.BooleanField(default=False, verbose_name=b'Email with sample collections should use statistics'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_1', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, verbose_name=b'Visit 1 virus result'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_1_updated', + field=models.DateField(blank=True, null=True, verbose_name=b'Visit 1 virus result date'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_2', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, verbose_name=b'Visit 2 virus result'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_2_updated', + field=models.DateField(blank=True, null=True, verbose_name=b'Visit 2 virus result date'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_3', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, verbose_name=b'Visit 3 virus result'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_3_updated', + field=models.DateField(blank=True, null=True, verbose_name=b'Visit 3 virus result date'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_4', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, verbose_name=b'Visit 4 virus result'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_4_updated', + field=models.DateField(blank=True, null=True, verbose_name=b'Visit 4 virus result date'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_5', + field=models.NullBooleanField(choices=[(True, b'Yes'), (False, b'No'), (None, b'N/A')], default=None, verbose_name=b'Visit 5 virus result'), + ), + migrations.AlterField( + model_name='studysubject', + name='virus_test_5_updated', + field=models.DateField(blank=True, null=True, verbose_name=b'Visit 5 virus result date'), + ), + ] diff --git a/smash/web/migrations/0162_auto_20200416_1212.py b/smash/web/migrations/0162_auto_20200416_1212.py new file mode 100644 index 0000000000000000000000000000000000000000..9e2bf7c91f719ca93400c4e47b5b206036833394 --- /dev/null +++ b/smash/web/migrations/0162_auto_20200416_1212.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-16 12:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0161_auto_20200416_0736'), + ] + + operations = [ + migrations.AlterField( + model_name='configurationitem', + name='value', + field=models.CharField(max_length=1024, verbose_name=b'Value'), + ), + migrations.AlterField( + model_name='subject', + name='next_of_keen_name', + field=models.CharField(blank=True, max_length=255, verbose_name=b'Next of keen'), + ), + ] diff --git a/smash/web/migrations/0163_study_redcap_first_visit_number.py b/smash/web/migrations/0163_study_redcap_first_visit_number.py new file mode 100644 index 0000000000000000000000000000000000000000..e518d8ef98accd9b26caad3e036d12ee6147c0cb --- /dev/null +++ b/smash/web/migrations/0163_study_redcap_first_visit_number.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-16 12:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0162_auto_20200416_1212'), + ] + + operations = [ + migrations.AddField( + model_name='study', + name='redcap_first_visit_number', + field=models.IntegerField(default=1, verbose_name=b'Number of the first visit in redcap system'), + ), + ] diff --git a/smash/web/models/configuration_item.py b/smash/web/models/configuration_item.py index ed4b3e39faa17a8f0d5ea231cc178fd2a3fe4ee9..86e3d12c5649c0727bff560d8b894c42152e2b33 100644 --- a/smash/web/models/configuration_item.py +++ b/smash/web/models/configuration_item.py @@ -21,7 +21,7 @@ class ConfigurationItem(models.Model): editable=False ) - value = models.CharField(max_length=50, + value = models.CharField(max_length=1024, verbose_name='Value', ) diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py index e83ddc83fa6dc4ea020850952d9fb151f97ab3d4..b512e714efac9136e8c4d9d9bc048f77d065458f 100644 --- a/smash/web/models/constants.py +++ b/smash/web/models/constants.py @@ -10,6 +10,11 @@ SEX_CHOICES = ( (SEX_CHOICES_MALE, 'Male'), (SEX_CHOICES_FEMALE, 'Female'), ) +BOOL_CHOICES_WITH_NONE = ( + (True, 'Yes'), + (False, 'No'), + (None, 'N/A'), +) SUBJECT_TYPE_CHOICES_CONTROL = 'C' SUBJECT_TYPE_CHOICES_PATIENT = 'P' SUBJECT_TYPE_CHOICES = { @@ -47,6 +52,17 @@ KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE = "KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_ KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE = "KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE" KIT_DAILY_EMAIL_TIME_FORMAT_TYPE = "KIT_DAILY_EMAIL_TIME_FORMAT_TYPE" +RED_CAP_LANGUAGE_4_FIELD_TYPE = 'RED_CAP_LANGUAGE_4_FIELD_TYPE' +RED_CAP_LANGUAGE_3_FIELD_TYPE = 'RED_CAP_LANGUAGE_3_FIELD_TYPE' +RED_CAP_LANGUAGE_2_FIELD_TYPE = 'RED_CAP_LANGUAGE_2_FIELD_TYPE' +RED_CAP_LANGUAGE_1_FIELD_TYPE = 'RED_CAP_LANGUAGE_1_FIELD_TYPE' +RED_CAP_MPOWER_ID_FIELD_TYPE = 'RED_CAP_MPOWER_ID_FIELD_TYPE' +RED_CAP_DEAD_FIELD_TYPE = 'RED_CAP_DEAD_FIELD_TYPE' +RED_CAP_SEX_FIELD_TYPE = 'RED_CAP_SEX_FIELD_TYPE' +RED_CAP_DATE_BORN_FIELD_TYPE = 'RED_CAP_DATE_BORN_FIELD_TYPE' +RED_CAP_ND_NUMBER_FIELD_TYPE = 'RED_CAP_ND_NUMBER_FIELD_TYPE' +RED_CAP_VIRUS_FIELD_TYPE = 'RED_CAP_VIRUS_FIELD_TYPE' + MAIL_TEMPLATE_CONTEXT_SUBJECT = 'S' MAIL_TEMPLATE_CONTEXT_APPOINTMENT = 'A' MAIL_TEMPLATE_CONTEXT_VISIT = 'V' diff --git a/smash/web/models/study.py b/smash/web/models/study.py index b3baf2f706b7d6191228272d8c61e7be9bb6eb48..ed1fa433b140ad626c5746f5440a1ee6143b5ef0 100644 --- a/smash/web/models/study.py +++ b/smash/web/models/study.py @@ -43,6 +43,16 @@ class Study(models.Model): verbose_name="Auto create follow up visit" ) + redcap_first_visit_number = models.IntegerField( + default=1, + verbose_name="Number of the first visit in redcap system" + ) + + sample_mail_statistics = models.BooleanField( + default=False, + verbose_name="Email with sample collections should use statistics" + ) + visits_to_show_in_subject_list = models.IntegerField( verbose_name='Number of visits to show in the subject list', default=5, diff --git a/smash/web/models/study_columns.py b/smash/web/models/study_columns.py index 7d20b3913d40a71f72fc0b70327a20084e83a158..d2e769ca52216cf9560933ec89d9902f3092220e 100644 --- a/smash/web/models/study_columns.py +++ b/smash/web/models/study_columns.py @@ -12,69 +12,69 @@ class StudyColumns(models.Model): ) datetime_contact_reminder = models.BooleanField( - default=True, - verbose_name='Please make a contact on' - ) + default=True, + verbose_name='Please make a contact on' + ) type = models.BooleanField( - default=True, - verbose_name='Type' - ) + default=True, + verbose_name='Type' + ) default_location = models.BooleanField( - default=True, - verbose_name='Default appointment location', - ) + default=True, + verbose_name='Default appointment location', + ) flying_team = models.BooleanField( - default=True, - verbose_name='Default flying team location (if applicable)', - ) + default=True, + verbose_name='Default flying team location (if applicable)', + ) screening_number = models.BooleanField( - default=True, - verbose_name='Screening number', - ) + default=True, + verbose_name='Screening number', + ) nd_number = models.BooleanField( - default=True, - verbose_name='ND number', - ) + default=True, + verbose_name='ND number', + ) mpower_id = models.BooleanField( - default=True, - verbose_name='MPower ID' - ) + default=True, + verbose_name='MPower ID' + ) comments = models.BooleanField( - default=True, - verbose_name='Comments' - ) + default=True, + verbose_name='Comments' + ) referral = models.BooleanField( - default=True, - verbose_name='Referred by' - ) + default=True, + verbose_name='Referred by' + ) diagnosis = models.BooleanField( - default=True, - verbose_name='Diagnosis' - ) + default=True, + verbose_name='Diagnosis' + ) year_of_diagnosis = models.BooleanField( - default=True, - verbose_name='Year of diagnosis (YYYY)' - ) + default=True, + verbose_name='Year of diagnosis (YYYY)' + ) information_sent = models.BooleanField( - default=True, - verbose_name='Information sent', - ) + default=True, + verbose_name='Information sent', + ) pd_in_family = models.BooleanField( - default=True, - verbose_name='PD in family', - ) + default=True, + verbose_name='PD in family', + ) resigned = models.BooleanField( - default=True, - verbose_name='Resigned', - ) + default=True, + verbose_name='Resigned', + ) resign_reason = models.BooleanField( - default=True, - verbose_name='Resign reason' - ) + default=True, + verbose_name='Resign reason' + ) excluded = models.BooleanField(default=False, verbose_name='Excluded') @@ -83,41 +83,82 @@ class StudyColumns(models.Model): resign_reason = models.BooleanField(default=True, verbose_name='Endpoint reached comments') referral_letter = models.BooleanField( - default=False, - verbose_name='Referral letter' - ) + default=False, + verbose_name='Referral letter' + ) health_partner = models.BooleanField( - default=False, - verbose_name='Health partner' - ) + default=False, + verbose_name='Health partner' + ) health_partner_feedback_agreement = models.BooleanField( - default=False, - verbose_name='Agrees to give information to referral' - ) + default=False, + verbose_name='Agrees to give information to referral' + ) screening = models.BooleanField( - default=False, - verbose_name='Screening' - ) + default=False, + verbose_name='Screening' + ) previously_in_study = models.BooleanField( - default=False, - verbose_name='Previously in PDP study', - ) + default=False, + verbose_name='Previously in PDP study', + ) voucher_types = models.BooleanField( - default=False, - verbose_name='Voucher types', - ) + default=False, + verbose_name='Voucher types', + ) vouchers = models.BooleanField( - default=False, - verbose_name='Vouchers', - ) + default=False, + verbose_name='Vouchers', + ) brain_donation_agreement = models.BooleanField( - default=False, - verbose_name='Brain donation agreement', - ) + default=False, + verbose_name='Brain donation agreement', + ) + + virus_test_1 = models.BooleanField( + default=False, + verbose_name='Visit 1 virus results', + ) + virus_test_2 = models.BooleanField( + default=False, + verbose_name='Visit 2 virus results', + ) + virus_test_3 = models.BooleanField( + default=False, + verbose_name='Visit 3 virus results', + ) + virus_test_4 = models.BooleanField( + default=False, + verbose_name='Visit 4 virus results', + ) + virus_test_5 = models.BooleanField( + default=False, + verbose_name='Visit 5 virus results', + ) + virus_test_1_updated = models.BooleanField( + default=False, + verbose_name='Visit 1 virus results date', + ) + virus_test_2_updated = models.BooleanField( + default=False, + verbose_name='Visit 2 virus results date', + ) + virus_test_3_updated = models.BooleanField( + default=False, + verbose_name='Visit 3 virus results date', + ) + virus_test_4_updated = models.BooleanField( + default=False, + verbose_name='Visit 4 virus results date', + ) + virus_test_5_updated = models.BooleanField( + default=False, + verbose_name='Visit 5 virus results date', + ) diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 22a5b0b8fe77518abb0092587531f0b715413633..d802d8debb0e5d87dc0a54756bc9152349855cd9 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -1,23 +1,25 @@ # coding=utf-8 import logging import re -from django.db import models -from web.models import VoucherType, Appointment, Location, Visit, Provenance -from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE +from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver +from web.models import VoucherType, Appointment, Location, Visit, Provenance +from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE, BOOL_CHOICES_WITH_NONE + logger = logging.getLogger(__name__) -class StudySubject(models.Model): +class StudySubject(models.Model): class Meta: app_label = 'web' @property def provenances(self): - return Provenance.objects.filter(modified_table=StudySubject._meta.db_table, modified_table_id=self.id).order_by('-modification_date') + return Provenance.objects.filter(modified_table=StudySubject._meta.db_table, + modified_table_id=self.id).order_by('-modification_date') def finish_all_visits(self): visits = Visit.objects.filter(subject=self, is_finished=False) @@ -168,9 +170,9 @@ class StudySubject(models.Model): default=None, ) brain_donation_agreement = models.BooleanField( - default=False, - verbose_name='Brain donation agreement', - ) + default=False, + verbose_name='Brain donation agreement', + ) resigned = models.BooleanField( verbose_name='Resigned', @@ -196,42 +198,84 @@ class StudySubject(models.Model): editable=True ) endpoint_reached_reason = models.TextField(max_length=2000, - blank=True, - verbose_name='Endpoint reached comments' - ) + blank=True, + verbose_name='Endpoint reached comments' + ) + + virus_test_1 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 1 virus result', + default=None + ) + virus_test_2 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 2 virus result', + default=None + ) + virus_test_3 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 3 virus result', + default=None + ) + virus_test_4 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 4 virus result', + default=None + ) + virus_test_5 = models.NullBooleanField(choices=BOOL_CHOICES_WITH_NONE, + verbose_name='Visit 5 virus result', + default=None + ) + virus_test_1_updated = models.DateField(verbose_name='Visit 1 virus result date', + blank=True, + null=True, + ) + virus_test_2_updated = models.DateField(verbose_name='Visit 2 virus result date', + blank=True, + null=True, + ) + virus_test_3_updated = models.DateField(verbose_name='Visit 3 virus result date', + blank=True, + null=True, + ) + virus_test_4_updated = models.DateField(verbose_name='Visit 4 virus result date', + blank=True, + null=True, + ) + virus_test_5_updated = models.DateField(verbose_name='Visit 5 virus result date', + blank=True, + null=True, + ) def sort_matched_screening_first(self, pattern, reverse=False): - if self.screening_number is None: - return None - - parts = self.screening_number.split(';') - matches, reminder = [], [] - - try: - for part in parts: - chunks = part.strip().split('-') - if len(chunks) == 2: - letter, number = chunks - try: - tupl = (letter, int(number)) - except ValueError: #better than isdigit because isdigit fails with negative numbers and others - tupl = (letter, number) - else: - logger.warn('There are {} chunks in some parts of this screening_number: |{}|.'.format( - len(chunks), self.screening_number)) - tupl = (part.strip(), None) - if pattern is not None and pattern in part: - matches.append(tupl) - else: - reminder.append(tupl) - except: #if the format is not the expected format - matches = parts - - return matches + sorted(reminder, reverse=reverse) + if self.screening_number is None: + return None + + parts = self.screening_number.split(';') + matches, reminder = [], [] + + try: + for part in parts: + chunks = part.strip().split('-') + if len(chunks) == 2: + letter, number = chunks + try: + tupl = (letter, int(number)) + except ValueError: # better than isdigit because isdigit fails with negative numbers and others + tupl = (letter, number) + else: + logger.warn('There are {} chunks in some parts of this screening_number: |{}|.'.format( + len(chunks), self.screening_number)) + tupl = (part.strip(), None) + if pattern is not None and pattern in part: + matches.append(tupl) + else: + reminder.append(tupl) + except: # if the format is not the expected format + matches = parts + + return matches + sorted(reminder, reverse=reverse) @staticmethod def check_nd_number_regex(regex_str, study): - nd_numbers = StudySubject.objects.filter(study=study).exclude(nd_number__isnull=True).exclude(nd_number__exact='').all().values_list('nd_number', flat=True) + nd_numbers = StudySubject.objects.filter(study=study).exclude(nd_number__isnull=True).exclude( + nd_number__exact='').all().values_list('nd_number', flat=True) regex = re.compile(regex_str) for nd_number in nd_numbers: if regex.match(nd_number) is None: @@ -239,21 +283,20 @@ class StudySubject(models.Model): return True def can_schedule(self): - return not any([self.resigned, self.excluded, self.endpoint_reached, self.subject.dead]) + return not any([self.resigned, self.excluded, self.endpoint_reached, self.subject.dead]) @property def status(self): - if self.subject.dead: - return 'Deceased' - elif self.resigned: - return 'Resigned' - elif self.excluded: - return 'Excluded' - elif self.endpoint_reached: - return 'Endpoint Reached' - else: - return 'Normal' - + if self.subject.dead: + return 'Deceased' + elif self.resigned: + return 'Resigned' + elif self.excluded: + return 'Excluded' + elif self.endpoint_reached: + return 'Endpoint Reached' + else: + return 'Normal' def __str__(self): return "%s %s" % (self.subject.first_name, self.subject.last_name) @@ -261,12 +304,13 @@ class StudySubject(models.Model): def __unicode__(self): return "%s %s" % (self.subject.first_name, self.subject.last_name) -#SIGNALS + +# SIGNALS @receiver(post_save, sender=StudySubject) def set_as_resigned_or_excluded_or_endpoint_reached(sender, instance, **kwargs): - if instance.excluded: - instance.mark_as_excluded() - if instance.resigned: - instance.mark_as_resigned() - if instance.endpoint_reached: - instance.mark_as_endpoint_reached() \ No newline at end of file + if instance.excluded: + instance.mark_as_excluded() + if instance.resigned: + instance.mark_as_resigned() + if instance.endpoint_reached: + instance.mark_as_endpoint_reached() diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py index 918d4645513b2a690bb163d79c8b448b741dc141..0f7c3c35441f692e44b17f8df61202b742b69356 100644 --- a/smash/web/models/subject.py +++ b/smash/web/models/subject.py @@ -105,7 +105,7 @@ class Subject(models.Model): verbose_name='Country' ) - next_of_keen_name = models.CharField(max_length=50, + next_of_keen_name = models.CharField(max_length=255, blank=True, verbose_name='Next of keen' ) diff --git a/smash/web/redcap_connector.py b/smash/web/redcap_connector.py index 4e1da4fa683708eb0a6c28dc46c7ae214622f143..8329ef604bee1fbbbf48e75bd29648c149ffb350 100644 --- a/smash/web/redcap_connector.py +++ b/smash/web/redcap_connector.py @@ -1,43 +1,25 @@ # coding=utf-8 import cStringIO +import datetime import json import logging -import pycurl import certifi +import pycurl import timeout_decorator +from django.conf import settings +from django.forms.models import model_to_dict from django_cron import CronJobBase, Schedule -from django.forms.models import model_to_dict -from web.models.constants import GLOBAL_STUDY_ID -from web.models import ConfigurationItem, StudySubject, Language, Study, StudyRedCapColumns +from web.models import ConfigurationItem, StudySubject, Language, AppointmentType, Appointment, Visit, Study from web.models.constants import REDCAP_TOKEN_CONFIGURATION_TYPE, \ - REDCAP_BASE_URL_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT + REDCAP_BASE_URL_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT, RED_CAP_LANGUAGE_4_FIELD_TYPE, RED_CAP_LANGUAGE_3_FIELD_TYPE, \ + RED_CAP_LANGUAGE_2_FIELD_TYPE, RED_CAP_LANGUAGE_1_FIELD_TYPE, RED_CAP_MPOWER_ID_FIELD_TYPE, RED_CAP_DEAD_FIELD_TYPE, \ + RED_CAP_SEX_FIELD_TYPE, RED_CAP_DATE_BORN_FIELD_TYPE, RED_CAP_ND_NUMBER_FIELD_TYPE, RED_CAP_VIRUS_FIELD_TYPE, \ + GLOBAL_STUDY_ID from web.models.inconsistent_subject import InconsistentField, InconsistentSubject from web.models.missing_subject import MissingSubject -RED_CAP_LANGUAGE_4_FIELD = 'dm_language_4' - -RED_CAP_LANGUAGE_3_FIELD = 'dm_language_3' - -RED_CAP_LANGUAGE_2_FIELD = 'dm_language_2' - -RED_CAP_LANGUAGE_1_FIELD = 'dm_language_1' - -# noinspection SpellCheckingInspection -RED_CAP_MPOWER_ID_FIELD = 'dm_mpowerid' - -RED_CAP_DEAD_FIELD = 'dm_death' - -# noinspection SpellCheckingInspection -RED_CAP_SEX_FIELD = 'cdisc_dm_sex' - -# noinspection SpellCheckingInspection -RED_CAP_DATE_BORN_FIELD = 'cdisc_dm_brthdtc' - -# noinspection SpellCheckingInspection -RED_CAP_ND_NUMBER_FIELD = 'cdisc_dm_usubjd' - logger = logging.getLogger(__name__) @@ -52,12 +34,18 @@ class RedcapSubject(object): def __init__(self): self.languages = [] + self.visits = [] def add_language(self, language): if language is not None: self.languages.append(language) +class RedcapVisit(object): + virus = None + visit_number = 0 + + def different_string(string1, string2): if string1 is None: string1 = "" @@ -83,6 +71,19 @@ class RedcapConnector(object): for language in languages: self.language_by_name[language.name.lower()] = language + self.date_born_field = ConfigurationItem.objects.get(type=RED_CAP_DATE_BORN_FIELD_TYPE).value + self.sex_field = ConfigurationItem.objects.get(type=RED_CAP_SEX_FIELD_TYPE).value + self.nd_number_field = ConfigurationItem.objects.get(type=RED_CAP_ND_NUMBER_FIELD_TYPE).value + self.dead_field = ConfigurationItem.objects.get(type=RED_CAP_DEAD_FIELD_TYPE).value + self.language_1_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_1_FIELD_TYPE).value + self.language_2_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_2_FIELD_TYPE).value + self.language_3_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_3_FIELD_TYPE).value + self.language_4_field = ConfigurationItem.objects.get(type=RED_CAP_LANGUAGE_4_FIELD_TYPE).value + self.m_power_id_field = ConfigurationItem.objects.get(type=RED_CAP_MPOWER_ID_FIELD_TYPE).value + self.virus_field = ConfigurationItem.objects.get(type=RED_CAP_VIRUS_FIELD_TYPE).value + + self.study = Study.objects.get(id=GLOBAL_STUDY_ID) + def find_missing(self): pid = self.get_project_id() redcap_version = self.get_redcap_version() @@ -157,6 +158,13 @@ class RedcapConnector(object): self.add_inconsistent(inconsistent) def find_inconsistent(self): + appointment_type_code_to_finish = getattr(settings, "IMPORT_APPOINTMENT_TYPE", None) + appointment_type_to_finish = None + if appointment_type_code_to_finish is not None: + appointment_types = AppointmentType.objects.filter(code=appointment_type_code_to_finish) + if len(appointment_types) > 0: + appointment_type_to_finish = appointment_types[0] + pid = self.get_project_id() redcap_version = self.get_redcap_version() @@ -173,9 +181,41 @@ class RedcapConnector(object): if red_cap_subject is not None: url = self.create_redcap_link(pid, redcap_version, subject) - subject = self.create_inconsistency_subject(red_cap_subject, subject, url) - if subject is not None: - result.append(subject) + + inconsistent_subject = self.create_inconsistency_subject(red_cap_subject, subject, url) + if inconsistent_subject is not None: + result.append(inconsistent_subject) + if appointment_type_to_finish is not None: + for visit in red_cap_subject.visits: + smasch_visits = Visit.objects.filter(visit_number=visit.visit_number, subject=subject) + smasch_appointments = Appointment.objects.filter(visit__in=smasch_visits, + appointment_types=appointment_type_to_finish, + status=Appointment.APPOINTMENT_STATUS_SCHEDULED) + for smasch_appointment in smasch_appointments: + smasch_appointment.mark_as_finished() + smasch_appointment.visit.is_finished = True + smasch_appointment.visit.save() + if visit.virus is not None: + if visit.visit_number == 1 and subject.virus_test_1 != visit.virus: + subject.virus_test_1 = visit.virus + subject.virus_test_1_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 2 and subject.virus_test_2 != visit.virus: + subject.virus_test_2 = visit.virus + subject.virus_test_2_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 3 and subject.virus_test_3 != visit.virus: + subject.virus_test_3 = visit.virus + subject.virus_test_3_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 4 and subject.virus_test_4 != visit.virus: + subject.virus_test_4 = visit.virus + subject.virus_test_4_updated = datetime.datetime.now() + subject.save() + if visit.visit_number == 5 and subject.virus_test_5 != visit.virus: + subject.virus_test_5 = visit.virus + subject.virus_test_5_updated = datetime.datetime.now() + subject.save() return result @@ -228,7 +268,7 @@ class RedcapConnector(object): @staticmethod def create_inconsistency_subject(red_cap_subject, study_subject, url): - #func dict + # func dict field_checks = { 'sex': RedcapConnector.check_sex_consistency, 'date_born': RedcapConnector.check_birth_date_consistency, @@ -239,14 +279,14 @@ class RedcapConnector(object): fields = [] - #get fields which are true from redcap columns - fields_to_check = [k for k,v in model_to_dict(study_subject.study.redcap_columns).iteritems() if v is True] + # get fields which are true from redcap columns + fields_to_check = [k for k, v in model_to_dict(study_subject.study.redcap_columns).iteritems() if v is True] for field_to_check in fields_to_check: field = field_checks[field_to_check](red_cap_subject, study_subject) if field is not None: fields.append(field) - + result = None if len(fields) > 0: result = InconsistentSubject.create(smash_subject=study_subject, url=url, fields=fields) @@ -257,21 +297,68 @@ class RedcapConnector(object): subject.nd_number + "&page=demographics" def get_red_cap_subjects(self): - query_data = { + + query_data = self.get_subject_query_data() + data = self.execute_query(query_data) + result = [] + for row in data: + if isinstance(row, dict): + redcap_subject = RedcapSubject() + redcap_subject.nd_number = row.get(self.nd_number_field) + if self.date_born_field != "": + redcap_subject.date_born = row.get(self.date_born_field) + if self.sex_field != "": + redcap_subject.sex = row.get(self.sex_field) + if self.dead_field != "": + redcap_subject.dead = (row.get(self.dead_field).lower() == "yes") + if self.m_power_id_field != "": + redcap_subject.mpower_id = row.get(self.m_power_id_field) + if self.language_1_field != "" and row.get(self.language_1_field): + redcap_subject.add_language(self.get_language(row.get(self.language_1_field))) + if self.language_2_field != "" and row[self.language_2_field]: + redcap_subject.add_language(self.get_language(row.get(self.language_2_field))) + if self.language_3_field != "" and row[self.language_3_field]: + redcap_subject.add_language(self.get_language(row.get(self.language_3_field))) + if self.language_4_field != "" and row[self.language_4_field]: + redcap_subject.add_language(self.get_language(row.get(self.language_4_field))) + visit = RedcapVisit() + visit.visit_number = 1 + if self.virus_field != "": + if row.get(self.virus_field) == "Negative": + visit.virus = False + elif row.get(self.virus_field) == "Positive": + visit.virus = True + redcap_subject.visits.append(visit) + result.append(redcap_subject) + for i in range(1, 9): + query_data = self.get_subject_query_data() + query_data["events[0]"] = "visit_" + str(i + self.study.redcap_first_visit_number) + "_arm_1" + data = self.execute_query(query_data) + if isinstance(data, dict): + break + for row in data: + if isinstance(row, dict): + nd_number = row.get(self.nd_number_field) + for redcap_subject in result: + if redcap_subject.nd_number == nd_number: + visit = RedcapVisit() + visit.visit_number = i + if self.virus_field != "": + if row.get(self.virus_field) == "Negative": + visit.virus = False + elif row.get(self.virus_field) == "Positive": + visit.virus = True + redcap_subject.visits.append(visit) + + return result + + def get_subject_query_data(self): + result = { 'token': self.token, 'content': 'record', 'format': 'json', 'type': 'flat', - 'fields[0]': RED_CAP_DATE_BORN_FIELD, - 'fields[1]': RED_CAP_SEX_FIELD, - 'fields[2]': RED_CAP_ND_NUMBER_FIELD, - 'fields[3]': RED_CAP_DEAD_FIELD, - 'fields[4]': RED_CAP_LANGUAGE_1_FIELD, - 'fields[5]': RED_CAP_LANGUAGE_2_FIELD, - 'fields[6]': RED_CAP_LANGUAGE_3_FIELD, - 'fields[7]': RED_CAP_LANGUAGE_4_FIELD, - 'fields[8]': RED_CAP_MPOWER_ID_FIELD, - 'events[0]': 'visit_1_arm_1', + 'events[0]': 'visit_' + str(self.study.redcap_first_visit_number) + '_arm_1', 'rawOrLabel': 'label', 'rawOrLabelHeaders': 'raw', 'exportCheckboxLabel': 'false', @@ -279,24 +366,37 @@ class RedcapConnector(object): 'exportDataAccessGroups': 'false', 'returnFormat': 'json' } - data = self.execute_query(query_data) - result = [] - for row in data: - redcap_subject = RedcapSubject() - redcap_subject.nd_number = row[RED_CAP_ND_NUMBER_FIELD] - redcap_subject.date_born = row[RED_CAP_DATE_BORN_FIELD] - redcap_subject.sex = row[RED_CAP_SEX_FIELD] - redcap_subject.dead = (row[RED_CAP_DEAD_FIELD].lower() == "yes") - redcap_subject.mpower_id = row[RED_CAP_MPOWER_ID_FIELD] - if row[RED_CAP_LANGUAGE_1_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_1_FIELD])) - if row[RED_CAP_LANGUAGE_2_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_2_FIELD])) - if row[RED_CAP_LANGUAGE_3_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_3_FIELD])) - if row[RED_CAP_LANGUAGE_4_FIELD]: - redcap_subject.add_language(self.get_language(row[RED_CAP_LANGUAGE_4_FIELD])) - result.append(redcap_subject) + field_number = 0 + if self.date_born_field != "": + result['fields[' + str(field_number) + ']'] = self.date_born_field + field_number += 1 + if self.sex_field != "": + result['fields[' + str(field_number) + ']'] = self.sex_field + field_number += 1 + if self.nd_number_field != "": + result['fields[' + str(field_number) + ']'] = self.nd_number_field + field_number += 1 + if self.dead_field != "": + result['fields[' + str(field_number) + ']'] = self.dead_field + field_number += 1 + if self.language_1_field != "": + result['fields[' + str(field_number) + ']'] = self.language_1_field + field_number += 1 + if self.language_2_field != "": + result['fields[' + str(field_number) + ']'] = self.language_2_field + field_number += 1 + if self.language_3_field != "": + result['fields[' + str(field_number) + ']'] = self.language_3_field + field_number += 1 + if self.language_4_field != "": + result['fields[' + str(field_number) + ']'] = self.language_4_field + field_number += 1 + if self.m_power_id_field != "": + result['fields[' + str(field_number) + ']'] = self.m_power_id_field + field_number += 1 + if self.virus_field != "": + result['fields[' + str(field_number) + ']'] = self.virus_field + field_number += 1 return result def get_language(self, name): diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index 2f2846523db2447089e98c1c7f08de6accf1aff3..fecda37d4b7560d1b16fd9ee768119189ea9138c 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -300,6 +300,9 @@ function createTable(params) { $(tableElement).find('tfoot div[name="yes_no_filter"]').each(function () { $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option></select>'); }); + $(tableElement).find('tfoot div[name="yes_no_null_filter"]').each(function () { + $(this).html('<select style="width:60px" ><option value selected="selected">---</option><option value="true">YES</option><option value="false">NO</option><option value="null">N/A</option></select>'); + }); $(tableElement).find('tfoot div[name="integer_filter"]').each(function () { var options = '<option value selected="selected">---</option>'; for (var i = 1; i <= 8; i++) { diff --git a/smash/web/tests/importer/test_tns_csv_visit_import_reader.py b/smash/web/tests/importer/test_tns_csv_visit_import_reader.py index 351988cd9ef810eb8229c80ff4b55a60811f6f14..f8ee5ce7db59a87c6a50a68b5b37af1efbbad8b5 100644 --- a/smash/web/tests/importer/test_tns_csv_visit_import_reader.py +++ b/smash/web/tests/importer/test_tns_csv_visit_import_reader.py @@ -14,7 +14,7 @@ from web.tests.functions import get_resource_path, create_study_subject, create_ logger = logging.getLogger(__name__) -class TestTnsCsvSubjectReader(TestCase): +class TestTnsCsvVisitReader(TestCase): def setUp(self): self.warning_counter = MsgCounterHandler() logging.getLogger('').addHandler(self.warning_counter) @@ -69,7 +69,7 @@ class TestTnsCsvSubjectReader(TestCase): self.assertEqual(4, appointment.datetime_when.month) self.assertEqual(2020, appointment.datetime_when.year) - self.assertEquals(1, self.get_warnings_count()) + self.assertEquals(0, self.get_warnings_count()) def test_load_data_with_existing_visit_and_appointment(self): filename = get_resource_path('tns_vouchers_import.csv') @@ -97,7 +97,7 @@ class TestTnsCsvSubjectReader(TestCase): self.assertEqual(4, appointment.datetime_when.month) self.assertEqual(2020, appointment.datetime_when.year) - self.assertEquals(2, self.get_warnings_count()) + self.assertEquals(0, self.get_warnings_count()) def test_load_data_with_visit_and_no_previous_visits(self): filename = get_resource_path('tns_vouchers_3_import.csv') @@ -130,7 +130,7 @@ class TestTnsCsvSubjectReader(TestCase): self.assertEqual("cov-000111", visit.subject.nd_number) self.assertEqual(1, visit.visit_number) - self.assertEquals(1, self.get_warnings_count()) + self.assertEquals(0, self.get_warnings_count()) def test_load_data_with_lab_id(self): filename = get_resource_path('tns_vouchers_lab_id_import.csv') @@ -139,15 +139,15 @@ class TestTnsCsvSubjectReader(TestCase): visit = Visit.objects.filter(id=visits[0].id)[0] appointment = Appointment.objects.filter(visit=visit)[0] - self.assertEqual(u"Laboratoires réunis", appointment.location.name) + self.assertTrue(u"Laboratoires réunis" in appointment.location.name) visit = Visit.objects.filter(id=visits[1].id)[0] appointment = Appointment.objects.filter(visit=visit)[0] - self.assertEqual(u"Ketterthill", appointment.location.name) + self.assertTrue(u"Ketterthill" in appointment.location.name) visit = Visit.objects.filter(id=visits[2].id)[0] appointment = Appointment.objects.filter(visit=visit)[0] - self.assertEqual(u"BioneXt", appointment.location.name) + self.assertTrue(u"BioneXt" in appointment.location.name) self.assertEquals(3, self.get_warnings_count()) diff --git a/smash/web/views/kit.py b/smash/web/views/kit.py index 2ce7e1341d53669bc0707c70804d857c6e917e15..374285be96003b08b48a4c330701e03a362ae330 100644 --- a/smash/web/views/kit.py +++ b/smash/web/views/kit.py @@ -4,8 +4,8 @@ import locale import logging import platform import time +import traceback -import pytz import timeout_decorator from django.contrib import messages from django.utils.dateparse import parse_datetime @@ -14,10 +14,10 @@ from django_cron.models import CronJobLog from notifications import get_filter_locations, get_today_midnight_date from web.decorators import PermissionDecorator -from web.models import ConfigurationItem, Language, Worker +from web.models import ConfigurationItem, Language, Worker, Study from web.models.constants import KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE, KIT_DAILY_EMAIL_DAYS_PERIOD_TYPE, \ KIT_EMAIL_HOUR_CONFIGURATION_TYPE, KIT_DAILY_EMAIL_TIME_FORMAT_TYPE, \ - KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT + KIT_EMAIL_DAY_OF_WEEK_CONFIGURATION_TYPE, CRON_JOB_TIMEOUT, GLOBAL_STUDY_ID from . import wrap_response from ..forms import KitRequestForm from ..models import AppointmentType, Appointment @@ -88,15 +88,8 @@ def kit_requests(request): return wrap_response(request, 'equipment_and_rooms/kit_requests/kit_requests.html', get_kit_requests_data(request)) -def send_mail(data): - time_format= ConfigurationItem.objects.get(type=KIT_DAILY_EMAIL_TIME_FORMAT_TYPE).value - - end_date_str = " end of time" - if data["end_date"] is not None: - end_date_str = data["end_date"].strftime('%Y-%m-%d') - title = "Samples between " + \ - data["start_date"].strftime('%Y-%m-%d') + " and " + end_date_str - +def create_detailed_email_content(data, title): + time_format = ConfigurationItem.objects.get(type=KIT_DAILY_EMAIL_TIME_FORMAT_TYPE).value cell_style = "padding: 8px; line-height: 1.42857143; vertical-align: top; " \ "font-size: 14px; font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;" @@ -139,6 +132,84 @@ def send_mail(data): unicode(appointment.worker_assigned) + "</td>" email_body += "</tr>" email_body += "</tbody></table>" + return email_body + + +def create_statistic_email_content(data, title): + time_format = ConfigurationItem.objects.get(type=KIT_DAILY_EMAIL_TIME_FORMAT_TYPE).value + cell_style = "padding: 8px; line-height: 1.42857143; vertical-align: top; " \ + "font-size: 14px; font-family: 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;" + + email_body = "<h1>" + title + "</h1>" + + location_summary = {} + + table = {} + for appointment in data["appointments"]: + + appointment_date = appointment.datetime_when.strftime(time_format) + location = unicode(appointment.location) + if appointment.flying_team is not None: + location += " (" + unicode(appointment.flying_team) + ")" + simple_location = location.split(",")[0] + if location_summary.get(simple_location) is None: + location_summary[simple_location] = 0 + location_summary[simple_location] += 1 + + if table.get(appointment_date) is None: + table[appointment_date] = {} + if table[appointment_date].get(location) is None: + table[appointment_date][location] = 0 + table[appointment_date][location] += 1 + + email_body += 'Total number of donors scheduled: ' + str(len(data["appointments"])) + "</br></br>" + for location in location_summary: + email_body += 'Total number of donors scheduled at ' + location + ': ' + str( + location_summary[location]) + "</br>" + + email_body += "</br>" + + email_body += '<table style="border: 1px solid #f4f4f4;border-spacing: 0;border-collapse: collapse;">' \ + '<thead><tr>' \ + '<th>Date</th>' \ + '<th>Location</th>' \ + '<th>No-of-Donors</th>' \ + '</tr></thead>' + email_body += "<tbody>" + + even = True + for appointment_date in table: + for location in table[appointment_date]: + subjects = table[appointment_date][location] + row_style = "" + even = not even + if even: + row_style = ' background-color: #f9f9f9;' + email_body += "<tr style='" + row_style + "'>" + email_body += "<td style='" + cell_style + "'>" + appointment_date + "</td>" + email_body += "<td style='" + cell_style + "'>" + location + "</td>" + email_body += "<td style='" + cell_style + "'>" + str(subjects) + "</td>" + email_body += "</tr>" + email_body += "</tbody></table>" + return email_body + + +def send_mail(data): + end_date_str = " end of time" + title = None + if data["end_date"] is not None: + if (data["end_date"] - data["start_date"]).days < 2: + title = "Details of Donors scheduled for bio-sampling on " + data["start_date"].strftime('%Y-%m-%d') + end_date_str = data["end_date"].strftime('%Y-%m-%d') + if title is None: + title = "Samples between " + \ + data["start_date"].strftime('%Y-%m-%d') + " and " + end_date_str + + if Study.objects.get(id=GLOBAL_STUDY_ID).sample_mail_statistics: + email_body = create_statistic_email_content(data, title) + else: + email_body = create_detailed_email_content(data, title) + recipients = ConfigurationItem.objects.get( type=KIT_RECIPIENT_EMAIL_CONFIGURATION_TYPE).value cc_recipients = [] @@ -153,6 +224,8 @@ def kit_requests_send_mail(request, start_date, end_date=None): send_mail(data) messages.add_message(request, messages.SUCCESS, 'Mail sent') except Exception as e: + traceback.print_exc() + logger.warn('Kit Request Send Mail Failed: |{}|\n{}'.format( e.message, e.args)) messages.add_message(request, messages.ERROR, @@ -161,36 +234,34 @@ def kit_requests_send_mail(request, start_date, end_date=None): class KitRequestEmailSendJob(CronJobBase): - RUN_EVERY_MINUTES = 1 - schedule = Schedule(run_every_mins=RUN_EVERY_MINUTES) + RUN_AT = [] + + try: + times = ConfigurationItem.objects.get( + type=KIT_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) + except: + logger.error("Cannot fetch data about email hour") + schedule = Schedule(run_at_times=RUN_AT) code = 'web.kit_request_weekly_email' # a unique code @timeout_decorator.timeout(CRON_JOB_TIMEOUT) def do(self): - now = datetime.datetime.utcnow() - hour = int(ConfigurationItem.objects.get( - type=KIT_EMAIL_HOUR_CONFIGURATION_TYPE).value.split(":")[0]) - minute = int(ConfigurationItem.objects.get( - type=KIT_EMAIL_HOUR_CONFIGURATION_TYPE).value.split(":")[1]) - # check if we sent email this day already - date = now.replace(hour=hour, minute=minute) - # TODO it's a hack assuming that we are in CEST - date = pytz.utc.localize(date - datetime.timedelta(minutes=122)) jobs = CronJobLog.objects.filter( - code=KitRequestEmailSendJob.code, message="mail sent", start_time__gte=date) + code=KitRequestEmailSendJob.code, message="mail sent", start_time__gte=datetime.datetime.utcnow()) if jobs.count() == 0: - if pytz.utc.localize(datetime.datetime.utcnow()) > date: - if self.match_day_of_week(): - worker = Worker.objects.create() - data = get_kit_requests(worker) - send_mail(data) - worker.delete() - return "mail sent" - else: - return "day of week doesn't match" + if self.match_day_of_week(): + worker = Worker.objects.create() + data = get_kit_requests(worker) + send_mail(data) + worker.delete() + return "mail sent" else: - return "too early" + return "day of week doesn't match" else: return "mail already sent"