diff --git a/smash/db_scripts/create_dummy_data.py b/smash/db_scripts/create_dummy_data.py index 5f9482d80aa2aeaaa34050963f2809dcfe6f1d28..b6a8ab5324685770bd4c06b381cf03df16cdce2b 100644 --- a/smash/db_scripts/create_dummy_data.py +++ b/smash/db_scripts/create_dummy_data.py @@ -432,7 +432,18 @@ 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] + if virus_test_1 is None: + 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] + elif virus_test_1 == True: + virus_test_1_updated = fake.date_between(start_date='-30d', end_date='-4d') + else: + virus_test_1_updated = 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, defaults={'default_location': default_location, 'type': type, 'screening_number': screening_number, 'study': study}) diff --git a/smash/web/admin.py b/smash/web/admin.py index 91037bb878f4658e2f3142ee72a6b1878ddff938..7d7611b9d71d2767d962fbd1fdd258bb5dbdda0e 100644 --- a/smash/web/admin.py +++ b/smash/web/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from models import StudySubject, Item, Room, AppointmentType, Language, Location, Worker, FlyingTeam, Availability, \ - Holiday, Visit, Appointment + Holiday, Visit, Appointment, StudyColumns, StudySubjectList, StudyVisitList, VisitColumns, SubjectColumns class LanguageAdmin(admin.ModelAdmin): @@ -21,3 +21,8 @@ admin.site.register(FlyingTeam) admin.site.register(Availability) admin.site.register(Holiday) admin.site.register(Appointment) +admin.site.register(SubjectColumns) +admin.site.register(StudyColumns) +admin.site.register(StudySubjectList) +admin.site.register(StudyVisitList) +admin.site.register(VisitColumns) diff --git a/smash/web/api_views/appointment.py b/smash/web/api_views/appointment.py index 23547b625a624aed307e05739f268b97a1e953cf..8a60dc5949e089291bb09a22f18c1fbf600fb8e5 100644 --- a/smash/web/api_views/appointment.py +++ b/smash/web/api_views/appointment.py @@ -5,6 +5,7 @@ from django.http import JsonResponse from django.urls import reverse from django.utils import timezone +from web.templatetags.filters import display_visit_number from web.api_views.serialization_utils import serialize_datetime, location_to_str, flying_team_to_str, add_column, \ bool_to_yes_no, get_filters_for_data_table_request from web.models import Appointment, Study, SubjectColumns, AppointmentColumns, AppointmentList, StudyColumns @@ -175,7 +176,7 @@ def serialize_appointment(appointment): nd_number = screening_number = phone_numbers = appointment_type_names = None status = appointment.status if appointment.visit is not None: - title = "Visit " + str(appointment.visit.visit_number) + title = "Visit {}".format(display_visit_number(appointment.visit.visit_number)) study_subject = appointment.visit.subject subject_string = study_subject.subject.last_name + " " + study_subject.subject.first_name first_name = study_subject.subject.first_name diff --git a/smash/web/api_views/serialization_utils.py b/smash/web/api_views/serialization_utils.py index 5f6cd27120fd039257378a8b3b83cedcc55b1d8e..7df976992e9a300b7472cb68df560fc52ab4dc53 100644 --- a/smash/web/api_views/serialization_utils.py +++ b/smash/web/api_views/serialization_utils.py @@ -19,6 +19,17 @@ def bool_to_yes_no_null(val): 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 cd7b5ac5dd59429c1457ea3fd30d5f90a4b87c0a..05a96565baf77d46c1c9cf9158f3a107fa9aaffe 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -4,11 +4,13 @@ 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.models import ConfigurationItem +from distutils.util import strtobool from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \ - serialize_date, serialize_datetime, get_filters_for_data_table_request, bool_to_yes_no_null + serialize_date, serialize_datetime, get_filters_for_data_table_request, virus_test_to_str from web.models import StudySubject, Visit, Appointment, Subject, SubjectColumns, StudyColumns, Study, ContactAttempt -from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID +from web.models.constants import SUBJECT_TYPE_CHOICES, GLOBAL_STUDY_ID, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO from web.models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ StudySubjectList, SUBJECT_LIST_VOUCHER_EXPIRY from web.views import e500_error @@ -78,23 +80,34 @@ def get_subject_columns(request, subject_list_type): 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) + 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 = range(0, 5) + visit_numbers = range(0, study.visits_to_show_in_subject_list + 0) + else: + virus_visit_numbers = range(1, 5+1) + visit_numbers = 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 date".format(virus_visit_number), + "virus_test_{}_updated".format(one_based_idx), + study_subject_columns, + "virus_test_date", 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): - visit_key = "visit_" + str(visit_number) - add_column(result, "Visit " + str(visit_number), visit_key, None, "visit_filter", - visible_param=study_subject_list.visits) + + for one_based_idx, visit_number in enumerate(visit_numbers, 1): + visit_key = "visit_{}".format(one_based_idx) #always starts in 1 + add_column(result, "Visit {}".format(visit_number), visit_key, None, + "visit_filter", visible_param=study_subject_list.visits) return JsonResponse({"columns": result}) @@ -308,27 +321,37 @@ def get_subjects_filtered(subjects_to_be_filtered, filters): result = result.filter(endpoint_reached=(value == "true")) elif column == "virus_test_1": if value == "null": - result = result.filter(virus_test_1__isnull=True) + result = result.filter(virus_test_1__isnull=True, virus_test_1_updated__isnull=True) + elif value == "inconclusive": + result = result.filter(virus_test_1__isnull=True, virus_test_1_updated__isnull=False) 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) + result = result.filter(virus_test_2__isnull=True, virus_test_2_updated__isnull=True) + elif value == "inconclusive": + result = result.filter(virus_test_2__isnull=True, virus_test_2_updated__isnull=False) 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) + result = result.filter(virus_test_3__isnull=True, virus_test_3_updated__isnull=True) + elif value == "inconclusive": + result = result.filter(virus_test_3__isnull=True, virus_test_3_updated__isnull=False) 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) + result = result.filter(virus_test_4__isnull=True, virus_test_4_updated__isnull=True) + elif value == "inconclusive": + result = result.filter(virus_test_4__isnull=True, virus_test_4_updated__isnull=False) 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) + result = result.filter(virus_test_5__isnull=True, virus_test_5_updated__isnull=True) + elif value == "inconclusive": + result = result.filter(virus_test_5__isnull=True, virus_test_5_updated__isnull=False) else: result = result.filter(virus_test_5=(value == "true")) elif column == "brain_donation_agreement": @@ -496,11 +519,11 @@ 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": virus_test_to_str(study_subject.virus_test_1, study_subject.virus_test_1_updated), + "virus_test_2": virus_test_to_str(study_subject.virus_test_2, study_subject.virus_test_2_updated), + "virus_test_3": virus_test_to_str(study_subject.virus_test_3, study_subject.virus_test_3_updated), + "virus_test_4": virus_test_to_str(study_subject.virus_test_4, study_subject.virus_test_4_updated), + "virus_test_5": virus_test_to_str(study_subject.virus_test_5, study_subject.virus_test_5_updated), "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, diff --git a/smash/web/api_views/visit.py b/smash/web/api_views/visit.py index 59f0eb977ae48eca6fee42d9231f2dee8de4097c..14c7021ffa5a1e07ab9aebad6d6dad88eebdb213 100644 --- a/smash/web/api_views/visit.py +++ b/smash/web/api_views/visit.py @@ -2,13 +2,16 @@ import logging from django.db.models import Q from django.http import JsonResponse +from web.models import ConfigurationItem +from distutils.util import strtobool from web.api_views.serialization_utils import bool_to_yes_no, flying_team_to_str, location_to_str, add_column, \ serialize_date, get_filters_for_data_table_request from web.models import AppointmentType, Appointment from web.models import SubjectColumns +from web.templatetags.filters import display_visit_number from web.models import Visit, Study, VisitColumns, StudyVisitList, StudyColumns -from web.models.constants import GLOBAL_STUDY_ID +from web.models.constants import GLOBAL_STUDY_ID, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO from web.models.study_visit_list import VISIT_LIST_GENERIC, VISIT_LIST_EXCEEDED_TIME, VISIT_LIST_UNFINISHED, \ VISIT_LIST_MISSING_APPOINTMENTS, VISIT_LIST_APPROACHING_WITHOUT_APPOINTMENTS, \ VISIT_LIST_APPROACHING_FOR_MAIL_CONTACT @@ -44,7 +47,14 @@ def get_visit_columns(request, visit_list_type): add_column(result, "Visit ends", "datetime_end", visit_columns, None) add_column(result, "Finished", "is_finished", visit_columns, "yes_no_filter") add_column(result, "Post mail sent", "post_mail_sent", visit_columns, "yes_no_filter") - add_column(result, "Visit number", "visit_number", visit_columns, "integer_filter") + + 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): + add_column(result, "Visit number", "display_visit_number", None, "from_zero_integer_filter") + else: + add_column(result, "Visit number", "visit_number", visit_columns, "integer_filter") + add_column(result, "Appointments in progress", "visible_appointment_types_in_progress", visit_list, "appointment_type_filter", sortable=False) add_column(result, "Done appointments", "visible_appointment_types_done", visit_list, "appointment_type_filter", @@ -102,6 +112,8 @@ def get_visits_order(visits_to_be_ordered, order_column, order_direction): result = visits_to_be_ordered.order_by(order_direction + 'post_mail_sent') elif order_column == "visit_number": result = visits_to_be_ordered.order_by(order_direction + 'visit_number') + elif order_column == "display_visit_number": + result = visits_to_be_ordered.order_by(order_direction + 'visit_number') else: logger.warn("Unknown sort column: " + str(order_column)) return result @@ -153,6 +165,8 @@ def get_visits_filtered(visits_to_be_filtered, filters): result = result.filter(post_mail_sent=(value == "true")) elif column == "visit_number": result = result.filter(visit_number=int(value)) + elif column == "display_visit_number": + result = result.filter(visit_number=int(value)+1) elif column == "visible_appointment_types_in_progress": result = filter_by_appointment_in_progress(result, value) elif column == "visible_appointment_types_done": @@ -246,4 +260,9 @@ def serialize_visit(visit): "visible_appointment_types": appointment_types_to_str(appointment_types), } + 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): + result["display_visit_number"] = display_visit_number(visit.visit_number) + return result diff --git a/smash/web/forms/forms.py b/smash/web/forms/forms.py index acead32df6e5e1401a5346ecbec59be00aac8d8d..805f2e72684e1274bc064633bfa71bafd15a0b7d 100644 --- a/smash/web/forms/forms.py +++ b/smash/web/forms/forms.py @@ -7,8 +7,10 @@ from django.utils.dates import MONTHS from web.models import Appointment, AppointmentType, AppointmentTypeLink, \ Availability, ContactAttempt, FlyingTeam, Holiday, Item, \ - StudySubject, Room, Worker, Visit, VoucherType, VoucherTypePrice -from web.models.constants import SUBJECT_TYPE_CHOICES + StudySubject, Room, Worker, Visit, VoucherType, VoucherTypePrice, ConfigurationItem +from web.models.constants import SUBJECT_TYPE_CHOICES, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO +from distutils.util import strtobool +from web.templatetags.filters import display_visit_number """ Possible redundancy, but if need arises, contents of forms can be easily customized @@ -127,6 +129,16 @@ class StatisticsForm(Form): choices = [(-1, "all")] choices.extend(SUBJECT_TYPE_CHOICES.items()) self.fields['subject_type'] = forms.ChoiceField(choices=choices, initial="-1") + 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): + new_choices = [] + for value, label in visit_choices: + if int(value) > 0: + label = display_visit_number(label) + new_choices.append((value, label)) + visit_choices = new_choices + self.fields['visit'] = forms.ChoiceField(choices=visit_choices, initial="-1") diff --git a/smash/web/forms/study_subject_forms.py b/smash/web/forms/study_subject_forms.py index 1f75992f6793598ef7d2c760af61af98ab90ab09..bf3a78441409eab3d978470843fc0a2df64cbffc 100644 --- a/smash/web/forms/study_subject_forms.py +++ b/smash/web/forms/study_subject_forms.py @@ -1,12 +1,13 @@ import logging -import re +from distutils.util import strtobool from django import forms from django.forms import ModelForm from web.forms.forms import DATETIMEPICKER_DATE_ATTRS, get_worker_from_args +from web.models import ConfigurationItem from web.models import StudySubject, Study, StudyColumns, VoucherType, Worker -from web.models.constants import SCREENING_NUMBER_PREFIXES_FOR_TYPE +from web.models.constants import SCREENING_NUMBER_PREFIXES_FOR_TYPE, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO from web.models.worker_study_role import WORKER_HEALTH_PARTNER from web.widgets.secure_file_widget import SecuredFileWidget @@ -14,18 +15,45 @@ logger = logging.getLogger(__name__) class StudySubjectForm(ModelForm): - datetime_contact_reminder = forms.DateTimeField(label="Contact on", widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), required=False) + datetime_contact_reminder = forms.DateTimeField(label="Contact on", + widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), + required=False) referral_letter = forms.FileField(label='Referral letter', widget=SecuredFileWidget(), required=False) - voucher_types = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, queryset=VoucherType.objects.all()) + voucher_types = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, + queryset=VoucherType.objects.all()) def __init__(self, *args, **kwargs): super(StudySubjectForm, self).__init__(*args, **kwargs) 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 = 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 date'.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" -class StudySubjectAddForm(StudySubjectForm): +class StudySubjectAddForm(StudySubjectForm): class Meta: model = StudySubject fields = '__all__' @@ -36,16 +64,8 @@ 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 + for visit_number in range(1, 6): + disable_virus_test_field(self, visit_number) prepare_study_subject_fields(fields=self.fields, study=self.study) @@ -104,7 +124,6 @@ def get_new_screening_number(screening_number_prefix): class StudySubjectDetailForm(StudySubjectForm): - class Meta: model = StudySubject fields = '__all__' @@ -139,18 +158,6 @@ 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): @@ -163,6 +170,14 @@ class StudySubjectEditForm(StudySubjectForm): fields = '__all__' +def disable_virus_test_field(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.attrs['readonly'] = True + self.fields[test_result_column_name].widget.attrs['disabled'] = True + self.fields[test_date_column_name].widget.attrs['readonly'] = True + + def get_study_from_args(kwargs): study = kwargs.pop('study', None) if study is None: @@ -238,7 +253,8 @@ def validate_subject_nd_number(self, cleaned_data): 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") - #else: #empty nd_number is valid + # else: #empty nd_number is valid + def validate_subject_resign_reason(self, cleaned_data): if self.study.columns.resigned and self.study.columns.resign_reason: diff --git a/smash/web/migrations/0166_auto_20200423_1457.py b/smash/web/migrations/0166_auto_20200423_1457.py new file mode 100644 index 0000000000000000000000000000000000000000..093f4bb6dd5e998fa900385b6ddd749f6fea3936 --- /dev/null +++ b/smash/web/migrations/0166_auto_20200423_1457.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-23 14:57 +from __future__ import unicode_literals + +from web.models.constants import VISIT_SHOW_VISIT_NUMBER_FROM_ZERO +from django.db import migrations + + +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, VISIT_SHOW_VISIT_NUMBER_FROM_ZERO, False, + "Should visit numbers be shown starting in 0") + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0165_configurationitem_email_virus'), + ] + + operations = [ + migrations.RunPython(configuration_items), + ] \ No newline at end of file diff --git a/smash/web/migrations/0167_auto_20200428_1110.py b/smash/web/migrations/0167_auto_20200428_1110.py new file mode 100644 index 0000000000000000000000000000000000000000..ea95418d62f1f5c1e0f7261d61771386f094910a --- /dev/null +++ b/smash/web/migrations/0167_auto_20200428_1110.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2020-04-28 11:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0166_auto_20200423_1457'), + ] + + operations = [ + migrations.AlterField( + model_name='subjectcolumns', + name='next_of_keen_address', + field=models.BooleanField(default=False, max_length=1, verbose_name=b'Next of kin address'), + ), + migrations.AlterField( + model_name='subjectcolumns', + name='next_of_keen_name', + field=models.BooleanField(default=False, max_length=1, verbose_name=b'Next of kin'), + ), + migrations.AlterField( + model_name='subjectcolumns', + name='next_of_keen_phone', + field=models.BooleanField(default=False, max_length=1, verbose_name=b'Next of kin phone'), + ), + migrations.AlterField( + model_name='subject', + name='social_security_number', + field=models.CharField(blank=True, max_length=50, verbose_name=b'Social security number'), + ), + ] diff --git a/smash/web/models/constants.py b/smash/web/models/constants.py index 361c158e015311d495803b3073978541e978f0c6..51e7a70bcb8dc2f46892003cbe9fff9f67cba961 100644 --- a/smash/web/models/constants.py +++ b/smash/web/models/constants.py @@ -43,6 +43,8 @@ CONTACT_TYPES_CHOICES = ( (CONTACT_TYPES_SMS, 'SMS'), ) +VISIT_SHOW_VISIT_NUMBER_FROM_ZERO = "VISIT_SHOW_VISIT_NUMBER_FROM_ZERO" + CANCELLED_APPOINTMENT_COLOR_CONFIGURATION_TYPE = "CANCELLED_APPOINTMENT_COLOR" NO_SHOW_APPOINTMENT_COLOR_CONFIGURATION_TYPE = "NO_SHOW_APPOINTMENT_COLOR" diff --git a/smash/web/models/subject.py b/smash/web/models/subject.py index 0f7c3c35441f692e44b17f8df61202b742b69356..ed90375f22a2cf53a3d70c7bea19fa00c21d1588 100644 --- a/smash/web/models/subject.py +++ b/smash/web/models/subject.py @@ -34,7 +34,7 @@ class Subject(models.Model): ) social_security_number = models.CharField(max_length=50, - verbose_name='Social security_number', + verbose_name='Social security number', blank=True, ) diff --git a/smash/web/models/subject_columns.py b/smash/web/models/subject_columns.py index 04a1869b0787f66eb61e8c76bd858419db16798f..82c4bf3b20df029fcc8e03397f671ef3d303f3df 100644 --- a/smash/web/models/subject_columns.py +++ b/smash/web/models/subject_columns.py @@ -86,15 +86,15 @@ class SubjectColumns(models.Model): next_of_keen_name = models.BooleanField(max_length=1, default=False, - verbose_name='Next of keen', + verbose_name='Next of kin', ) next_of_keen_phone = models.BooleanField(max_length=1, default=False, - verbose_name='Next of keen phone', + verbose_name='Next of kin phone', ) next_of_keen_address = models.BooleanField(max_length=1, default=False, - verbose_name='Next of keen address', + verbose_name='Next of kin address', ) diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index fecda37d4b7560d1b16fd9ee768119189ea9138c..e49418ae8988a3bb99154b18b14fb6a3863f184a 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -303,6 +303,23 @@ function createTable(params) { $(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="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>'); + }); + + //make columns of virus test date wider + $(tableElement).find('tfoot div[name="virus_test_date"]').each(function () { + $(this).css('width', '100px'); + $(this).text(''); + }); + + //replace the hyphen with non breaking space hyphen to ensure the column names are more readable + $(tableElement).find('th:contains("RT-PCR")').each(function(){ + non_breaking_hyphen = $.parseHTML('‑'); + var text = $(this).text().replace('-', $(non_breaking_hyphen).text()); + $(this).text(text); + }); + $(tableElement).find('tfoot div[name="integer_filter"]').each(function () { var options = '<option value selected="selected">---</option>'; for (var i = 1; i <= 8; i++) { @@ -311,6 +328,14 @@ function createTable(params) { $(this).html('<select style="width:60px" >' + options + '</select>'); }); + $(tableElement).find('tfoot div[name="from_zero_integer_filter"]').each(function () { + var options = '<option value selected="selected">---</option>'; + for (var i = 0; i < 8; i++) { + options += '<option value="' + i + '">' + i + '</option>'; + } + $(this).html('<select style="width:60px" >' + options + '</select>'); + }); + $(tableElement).find('tfoot div[name="voucher_status_filter"]').each(function () { $(this).html('<select style="width:60px" ><option value selected="selected">---</option>' + '<option value="NEW">NEW</option>' + diff --git a/smash/web/templates/subjects/visit_details.html b/smash/web/templates/subjects/visit_details.html index d7d2a88a324566d3769bed54f6a17ce17c510145..7dc6a54968d2f62947b2b46c53a19db1448937df 100644 --- a/smash/web/templates/subjects/visit_details.html +++ b/smash/web/templates/subjects/visit_details.html @@ -10,7 +10,7 @@ {% block ui_active_tab %}'subjects'{% endblock ui_active_tab %} {% block page_header %}List of visits{% endblock page_header %} -{% block page_description %}{% endblock page_description %} +{% block page_description %}Latest visit first{% endblock page_description %} {% block title %}{{ block.super }} - List of subject's visits {% endblock %} @@ -36,25 +36,25 @@ {% for element in display %} <div class="box box-widget widget-user-2"> <div class="widget-user-header bg-green"> - <h3 class="widget-user-username">{{ element.4 }}</h3> + <h3 class="widget-user-username">Visit {{ element.visit_number | display_visit_number }}</h3> <h5 class="widget-user-desc"> - {% if element.2 %}(Finished) + {% if element.finished %}(Finished) {% else %}(Not finished) {% endif %} - <a href="{% url 'web.views.visit_details' element.3 %}"><font color="#D3D3D3">Details + <a href="{% url 'web.views.visit_details' element.visit_id %}"><font color="#D3D3D3">Details >>></font></a> </h5> </div> <div class="box-footer"> - {% if not element.2 %} + {% if not element.finished %} <div> - <a href="{% url 'web.views.appointment_add' element.3 %}" class="btn btn-app"> + <a href="{% url 'web.views.appointment_add' element.visit_id %}" class="btn btn-app"> <i class="fa fa-plus"></i> Add new appointment </a> </div> {% endif %} - {% for field in element.0 %} + {% for field in element.visit_form %} {% if not field|is_checkbox %} <div class="col-md-6 form-group {% if field.errors %}has-error{% endif %} {% if field|is_checkbox %}multi-checkboxes{% endif %}"> <label class="col-sm-4 control-label">{{ field.label }}</label> @@ -67,7 +67,7 @@ <span class="help-block">{{ field.errors }}</span>{% endif %} </div> {% else %} - {% include 'includes/visit_appointment_types_field.html' with field=field appointments=element.1 readonly="true" %} + {% include 'includes/visit_appointment_types_field.html' with field=field appointments=element.appointments readonly="true" %} {% endif %} {% endfor %} </div> @@ -79,7 +79,7 @@ <h3 class="widget-user-username">Visit's appointments</h3> </div> <div class="box-footer"> - {% if element.1 %} + {% if element.appointments %} <table id="table" class="table table-bordered table-striped"> <thead> <tr> @@ -93,7 +93,7 @@ </tr> </thead> <tbody> - {% for app in element.1 %} + {% for app in element.appointments %} <tr> <td>{{ forloop.counter }}</td> <td style="background-color:{{ app.color }} !important"> diff --git a/smash/web/templates/visits/details.html b/smash/web/templates/visits/details.html index 1b5d9f6ee51346092cfb3d77091ab85766754280..6935b7d64949765f1b0b35e436c4640ff5f8f88e 100644 --- a/smash/web/templates/visits/details.html +++ b/smash/web/templates/visits/details.html @@ -11,10 +11,10 @@ {% endblock styles %} {% block ui_active_tab %}'visits'{% endblock ui_active_tab %} -{% block page_header %}Details of the visit ({{ visit.visit_number }}) {% endblock page_header %} +{% block page_header %}Details of the visit ({{ visit.visit_number |Â display_visit_number }}) {% endblock page_header %} {% block page_description %}{% endblock page_description %} -{% block title %}{{ block.super }} - Details of visit ({{ visit.visit_number }}) {% endblock %} +{% block title %}{{ block.super }} - Details of visit ({{ visit.visit_number |Â display_visit_number }}) {% endblock %} {% block breadcrumb %} {% include "subjects/breadcrumb.html" %} diff --git a/smash/web/templatetags/filters.py b/smash/web/templatetags/filters.py index ea2f0e67d5cab17040141248e6c7ffab7766852d..218e34bc50f142b794a28ba7cf932b9de12c600a 100644 --- a/smash/web/templatetags/filters.py +++ b/smash/web/templatetags/filters.py @@ -3,6 +3,9 @@ from django import template from django.forms import CheckboxSelectMultiple, CheckboxInput from django.utils.safestring import mark_safe import datetime +from web.models import ConfigurationItem +from web.models.constants import VISIT_SHOW_VISIT_NUMBER_FROM_ZERO +from distutils.util import strtobool register = template.Library() @@ -46,3 +49,13 @@ def render_appointments(statistics, appointment_type): def timestamp(value): epoch = datetime.datetime.utcfromtimestamp(0) return (value.replace(tzinfo=None) - epoch).total_seconds() + + +@register.filter(name='display_visit_number') +def display_visit_number(visit_number): + 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): + return (visit_number - 1) + else: + return visit_number \ No newline at end of file diff --git a/smash/web/views/export.py b/smash/web/views/export.py index 1cb6271207a31855a0199c62945cd654e336849a..9130b0fa15804a1dcbaf7bd983e3f5060c0ef3c3 100644 --- a/smash/web/views/export.py +++ b/smash/web/views/export.py @@ -7,7 +7,11 @@ from django.http import HttpResponse from notifications import get_today_midnight_date from web.decorators import PermissionDecorator from . import e500_error, wrap_response -from ..models import Subject, StudySubject, Appointment +from ..models import Subject, StudySubject, Appointment, ConfigurationItem +from web.models.constants import VISIT_SHOW_VISIT_NUMBER_FROM_ZERO +from distutils.util import strtobool +from web.templatetags.filters import display_visit_number +import re @PermissionDecorator('export_subjects', 'subject') @@ -86,12 +90,23 @@ def filter_fields_from_selected_fields(fields, selected_fields): def get_default_subject_fields(): + visit_from_zero = ConfigurationItem.objects.get(type=VISIT_SHOW_VISIT_NUMBER_FROM_ZERO).value + visit_from_zero = strtobool(visit_from_zero) subject_fields = [] for field in Subject._meta.fields: if field.name.upper() != "ID": 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) return subject_fields diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index ef65d5f486e0aa018a762fa8deb0091a09fdf72a..1583412ae89e5cb740e9a6fc3aab8581fdf71ee1 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -152,12 +152,14 @@ def subject_visit_details(request, id): visits_data = [] allow_add_visit = True for visit in visits: - appointments = visit.appointment_set.all() - finished = visit.is_finished - visit_id = visit.id - visit_title = "Visit " + str(visit.visit_number) - visit_form = VisitDetailForm(instance=visit) - visits_data.append((visit_form, appointments, finished, visit_id, visit_title)) + data = { + 'visit_form': VisitDetailForm(instance=visit), + 'appointments': visit.appointment_set.all(), + 'finished': visit.is_finished, + 'visit_id': visit.id, + 'visit_number': visit.visit_number + } + visits_data.append(data) if not visit.is_finished: allow_add_visit = False if not study_subject_to_be_viewed.can_schedule():