diff --git a/CHANGELOG b/CHANGELOG index 770eb87d67eb48e86ce72232d866fb8c5d02aad9..939d12aeb9efb56e39f375c096b93e9bee260fb5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ smasch (1.3.1-1) stable; urgency=medium - - * dependencies: updated django related python packages to fix + + * dependencies: updated django related python packages to fix deprectation warnings * dependencies: updated python related dependencies * dependencies: updated npm packages and removed unused packages @@ -10,6 +10,11 @@ smasch (1.3.1-1) stable; urgency=medium -- Carlos Vega <carlos.vega@lih.lu> Tue, 7 Nov 2023 10:35:00 +0200 +smasch (1.3.0-2) stable; urgency=low + * small improvement: add provenance tracking option "tracked" to CustomStudySubjectField (#523) + + -- Nirmeen Sallam <nirmeen.sallam@uni.lu> Fri, 04 Sep 2023 16:00:00 +0200 + smasch (1.3.0-1) stable; urgency=medium * small improvement: enable adding phone numbers to subject columns (#519) diff --git a/smash/web/migrations/0212_customstudysubjectfield_tracked.py b/smash/web/migrations/0212_customstudysubjectfield_tracked.py new file mode 100644 index 0000000000000000000000000000000000000000..b8c60e2b7a6a898c3256b040f8c068178a7c331e --- /dev/null +++ b/smash/web/migrations/0212_customstudysubjectfield_tracked.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.4 on 2023-07-07 14:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0211_subjectexport_data'), + ] + + operations = [ + migrations.AddField( + model_name='customstudysubjectfield', + name='tracked', + field=models.BooleanField(default=False), + ), + ] diff --git a/smash/web/models/custom_data/custom_study_subject_field.py b/smash/web/models/custom_data/custom_study_subject_field.py index 6db608d73491f0504e3d590c841d50d6b8afc636..9d754d61041e4f09987faeb3d627390516414d90 100644 --- a/smash/web/models/custom_data/custom_study_subject_field.py +++ b/smash/web/models/custom_data/custom_study_subject_field.py @@ -18,6 +18,8 @@ class CustomStudySubjectField(models.Model): unique = models.BooleanField(default=False) + tracked = models.BooleanField(default=False) + study = models.ForeignKey("web.Study", verbose_name='Study', editable=False, diff --git a/smash/web/templates/includes/custom_study_subject_field_box.html b/smash/web/templates/includes/custom_study_subject_field_box.html index 23e0c461a4eaaaaa706035498bb2b5f494b8350d..fc3de49ed0697cbebe9d073932cc7e6ffffe4e1a 100644 --- a/smash/web/templates/includes/custom_study_subject_field_box.html +++ b/smash/web/templates/includes/custom_study_subject_field_box.html @@ -17,6 +17,7 @@ <th class="text-center">Default value</th> <th class="text-center">Readonly</th> <th class="text-center">Unique</th> + <th class="text-center">Tracked</th> <th class="text-center">Obligatory</th> <th class="text-center">Edit</th> <th class="text-center">Remove</th> @@ -30,6 +31,7 @@ <td class="text-center">{{ field.default_value }}</td> <td class="text-center">{% if field.readonly %}YES{% else %}NO{% endif %}</td> <td class="text-center">{% if field.unique %}YES{% else %}NO{% endif %}</td> + <td class="text-center">{% if field.tracked %}YES{% else %}NO{% endif %}</td> <td class="text-center">{% if field.obligatory %}YES{% else %}NO{% endif %}</td> <td class="text-center"><a title="edit field" href="{% url 'web.views.custom_study_subject_field_edit' field.study.id field.id %}" diff --git a/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py b/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py index 283f431e60717ae5905f2ec5b39fe840edda805d..a96a7aff4f62f6d1c046be6f825416d44df43907 100644 --- a/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py +++ b/smash/web/tests/forms/test_CustomStudySubjectFieldAddForm.py @@ -24,13 +24,15 @@ class CustomStudySubjectFieldAddFormTest(TestCase): ('select list invalid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'bla', False, 'abc;def'), ('file', CUSTOM_FIELD_TYPE_FILE, None, True), ('file invalid', CUSTOM_FIELD_TYPE_FILE, 'tmp', False), + ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True, True), ]) - def test_add_field(self, _, field_type, default_value, valid, possible_values=''): + def test_add_field(self, _, field_type, default_value, valid, possible_values='', tracked=False): sample_data = { 'default_value': default_value, 'name': '1. name', 'type': field_type, 'possible_values': possible_values, + 'tracked': tracked, } form = CustomStudySubjectFieldAddForm(sample_data, study=get_test_study()) @@ -40,5 +42,6 @@ class CustomStudySubjectFieldAddFormTest(TestCase): self.assertEqual(1, CustomStudySubjectField.objects.filter(id=field.id).count()) self.assertEqual(default_value, field.default_value) + self.assertEqual(tracked, field.tracked) else: self.assertFalse(form.is_valid()) diff --git a/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py b/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py index 7436c8c6cd8e181324174d74282220ec353e62c9..cdbd8aa67710d2f5f70f9f32299e07eeacec1792 100644 --- a/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py +++ b/smash/web/tests/forms/test_CustomStudySubjectFieldEditForm.py @@ -24,8 +24,9 @@ class CustomStudySubjectFieldEditFormTest(TestCase): ('select list invalid', CUSTOM_FIELD_TYPE_SELECT_LIST, 'bla', False, 'abc;def'), ('file', CUSTOM_FIELD_TYPE_FILE, None, True), ('file invalid', CUSTOM_FIELD_TYPE_FILE, 'tmp', False), + ('text', CUSTOM_FIELD_TYPE_TEXT, 'bla', True, True), ]) - def test_edit_field(self, _, field_type, default_value, valid, possible_values=''): + def test_edit_field(self, _, field_type, default_value, valid, possible_values='', tracked=False): field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="", type=field_type) sample_data = { @@ -33,6 +34,7 @@ class CustomStudySubjectFieldEditFormTest(TestCase): 'name': '1. name', 'type': field_type, 'possible_values': possible_values, + 'tracked': tracked, } form = CustomStudySubjectFieldEditForm(sample_data, instance=field) @@ -41,5 +43,6 @@ class CustomStudySubjectFieldEditFormTest(TestCase): form.save() self.assertEqual(default_value, field.default_value) + self.assertEqual(tracked, field.tracked) else: self.assertFalse(form.is_valid()) diff --git a/smash/web/tests/models/test_study_subject.py b/smash/web/tests/models/test_study_subject.py index 727366a45e442879f38c1d34293d55f58dc7e971..d44f5aba5d684d3d97fc6483ce482b76a314bf26 100644 --- a/smash/web/tests/models/test_study_subject.py +++ b/smash/web/tests/models/test_study_subject.py @@ -197,6 +197,33 @@ class SubjectModelTests(TestCase): self.assertEqual(0, len(subject.custom_data_values)) + def test_subject_with_custom_field_tracked(self): + CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", type=CUSTOM_FIELD_TYPE_TEXT, + tracked=True) + + subject = create_study_subject() + + self.assertEqual(1, len(subject.custom_data_values)) + value = subject.custom_data_values[0] + self.assertEqual("xyz", value.value) + self.assertTrue(value.study_subject_field.tracked) + + def test_subject_with_removed_custom_field_not_tracked(self): + field = CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", + type=CUSTOM_FIELD_TYPE_TEXT) + + subject = create_study_subject() + self.assertEqual(1, len(subject.custom_data_values)) + self.assertFalse(subject.custom_data_values[0].study_subject_field.tracked) + field.delete() + + CustomStudySubjectField.objects.create(study=get_test_study(), default_value="xyz", type=CUSTOM_FIELD_TYPE_TEXT, + tracked=False) + + subject = create_study_subject() + self.assertEqual(1, len(subject.custom_data_values)) + self.assertFalse(subject.custom_data_values[0].study_subject_field.tracked) + def test_subject_with_field_from_different_study(self): field = CustomStudySubjectField.objects.create(study=create_study("bla"), type=CUSTOM_FIELD_TYPE_TEXT) diff --git a/smash/web/tests/view/test_subjects.py b/smash/web/tests/view/test_subjects.py index ffa66975d88f3c19b684aa65b2c0fbfd6b410320..a15d3051c22ef002330c1cd467aba10daf1c5dbe 100644 --- a/smash/web/tests/view/test_subjects.py +++ b/smash/web/tests/view/test_subjects.py @@ -8,7 +8,7 @@ from django.urls import reverse from web.forms import SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm from web.models import MailTemplate, StudySubject, StudyColumns, Visit, Provenance, Subject from web.models.constants import SEX_CHOICES_MALE, COUNTRY_AFGHANISTAN_ID, COUNTRY_OTHER_ID, \ - MAIL_TEMPLATE_CONTEXT_SUBJECT, CUSTOM_FIELD_TYPE_FILE + MAIL_TEMPLATE_CONTEXT_SUBJECT, CUSTOM_FIELD_TYPE_FILE, CUSTOM_FIELD_TYPE_TEXT from web.models.custom_data import CustomStudySubjectField from web.models.custom_data.custom_study_subject_field import get_study_subject_field_id from web.tests import LoggedInTestCase @@ -411,3 +411,94 @@ class SubjectsViewTests(LoggedInTestCase): self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}), data=form_data) self.assertEqual(count + 1, StudySubject.objects.all().count()) + + def test_subjects_add_subject_with_custom_file_field_tracked(self): + field = CustomStudySubjectField.objects.create(name="test-tracked", study=self.study, + type=CUSTOM_FIELD_TYPE_TEXT, tracked=True) + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() + form_data = self.create_add_form_data_for_study_subject() + + form_data["study_subject-" + get_study_subject_field_id(field)] = "BLA" + response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + + subject = StudySubject.objects.all().order_by("-id")[0] + + self.assertTrue('BLA' in subject.get_custom_data_value(field).value) + self.assertTrue(len(subject.provenances) == 0) + + form_data["study_subject-" + get_study_subject_field_id(field)] = "BLABLA" + response = self.client.post(reverse('web.views.subject_edit', kwargs={'subject_id': subject.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + self.assertTrue('BLABLA' in subject.get_custom_data_value(field).value) + self.assertEqual("test-tracked", subject.provenances[0].modified_field) + self.assertEqual('BLA', subject.provenances[0].previous_value) + self.assertEqual('BLABLA', subject.provenances[0].new_value) + + def test_subjects_add_subject_with_multiple_custom_file_field_tracked(self): + field = CustomStudySubjectField.objects.create(name="test-tracked", study=self.study, + type=CUSTOM_FIELD_TYPE_TEXT, tracked=True) + field2 = CustomStudySubjectField.objects.create(name="test-tracked2", study=self.study, + type=CUSTOM_FIELD_TYPE_TEXT, tracked=True) + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() + form_data = self.create_add_form_data_for_study_subject() + + form_data["study_subject-" + get_study_subject_field_id(field)] = "BLA" + form_data["study_subject-" + get_study_subject_field_id(field2)] = "lorem" + response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + + subject = StudySubject.objects.all().order_by("-id")[0] + + self.assertTrue('BLA' in subject.get_custom_data_value(field).value) + self.assertTrue('lorem' in subject.get_custom_data_value(field2).value) + self.assertTrue(len(subject.provenances) == 0) + + form_data["study_subject-" + get_study_subject_field_id(field)] = "BLABLA" + form_data["study_subject-" + get_study_subject_field_id(field2)] = "loremlorem" + response = self.client.post(reverse('web.views.subject_edit', kwargs={'subject_id': subject.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + self.assertTrue('BLABLA' in subject.get_custom_data_value(field).value) + self.assertEqual("test-tracked", subject.provenances[1].modified_field) + self.assertEqual('BLA', subject.provenances[1].previous_value) + self.assertEqual('BLABLA', subject.provenances[1].new_value) + + self.assertTrue('loremlorem' in subject.get_custom_data_value(field2).value) + self.assertEqual("test-tracked2", subject.provenances[0].modified_field) + self.assertEqual('lorem', subject.provenances[0].previous_value) + self.assertEqual('loremlorem', subject.provenances[0].new_value) + + def test_subjects_add_subject_with_custom_file_field_not_tracked(self): + field = CustomStudySubjectField.objects.create(name="test-tracked", study=self.study, + type=CUSTOM_FIELD_TYPE_TEXT, tracked=False) + self.worker.roles.all()[0].permissions.add(Permission.objects.get(codename="add_subject")) + self.worker.save() + form_data = self.create_add_form_data_for_study_subject() + + form_data["study_subject-" + get_study_subject_field_id(field)] = "BLA" + response = self.client.post(reverse('web.views.subject_add', kwargs={'study_id': self.study.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + + subject = StudySubject.objects.all().order_by("-id")[0] + + self.assertTrue('BLA' in subject.get_custom_data_value(field).value) + self.assertTrue(len(subject.provenances) == 0) + + form_data["study_subject-" + get_study_subject_field_id(field)] = "BLABLA" + response = self.client.post(reverse('web.views.subject_edit', kwargs={'subject_id': subject.id}), + data=form_data) + + self.assertEqual(response.status_code, 302) + self.assertTrue(len(subject.provenances) == 0) diff --git a/smash/web/views/subject.py b/smash/web/views/subject.py index 841061253bfda93f033452f458bfe0a34165f131..f181b08d3e40c1376a29f5436a0af2b92e56c4bc 100644 --- a/smash/web/views/subject.py +++ b/smash/web/views/subject.py @@ -1,6 +1,5 @@ # coding=utf-8 import logging - from django.contrib import messages from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, get_object_or_404 @@ -14,13 +13,10 @@ from .view_utils import WrappedView, wrap_response from ..forms import VisitDetailForm, SubjectAddForm, SubjectEditForm, StudySubjectAddForm, StudySubjectEditForm from ..models import StudySubject, MailTemplate, Worker, Study, Provenance, Subject from ..models.constants import GLOBAL_STUDY_ID, FILE_STORAGE -from ..models.study_subject_list import ( - SUBJECT_LIST_GENERIC, - SUBJECT_LIST_NO_VISIT, - SUBJECT_LIST_REQUIRE_CONTACT, - SUBJECT_LIST_VOUCHER_EXPIRY, - SUBJECT_LIST_CHOICES, -) +from ..models.custom_data import CustomStudySubjectValue +from ..models.custom_data.custom_study_subject_field import get_study_subject_field_id +from ..models.study_subject_list import SUBJECT_LIST_GENERIC, SUBJECT_LIST_NO_VISIT, SUBJECT_LIST_REQUIRE_CONTACT, \ + SUBJECT_LIST_VOUCHER_EXPIRY, SUBJECT_LIST_CHOICES from ..utils import get_client_ip logger = logging.getLogger(__name__) @@ -139,22 +135,22 @@ def subject_edit(request, subject_id): old_type = study_subject.type endpoint_was_reached = study_subject.endpoint_reached ip = get_client_ip(request) - if request.method == "POST": - study_subject_form = StudySubjectEditForm( - request.POST, - request.FILES, - instance=study_subject, - was_resigned=was_resigned, - prefix="study_subject", - endpoint_was_reached=endpoint_was_reached, - ) - subject_form = SubjectEditForm( - request.POST, request.FILES, instance=study_subject.subject, was_dead=was_dead, prefix="subject" - ) + + # save previous values of custom fields + prev_value_list_custom_fields = [] + for field in CustomStudySubjectValue.objects.filter(study_subject=study_subject): + prev_value_list_custom_fields.append(field.value) + + if request.method == 'POST': + study_subject_form = StudySubjectEditForm(request.POST, request.FILES, instance=study_subject, + was_resigned=was_resigned, prefix="study_subject", + endpoint_was_reached=endpoint_was_reached) + subject_form = SubjectEditForm(request.POST, request.FILES, instance=study_subject.subject, + was_dead=was_dead, prefix="subject" + ) if study_subject_form.is_valid() and subject_form.is_valid(): study_subject_form.save() subject_form.save() - persist_custom_file_fields(request, study_subject) if "type" in study_subject_form.changed_data and old_type != study_subject_form.cleaned_data["type"]: @@ -213,6 +209,30 @@ def subject_edit(request, subject_id): ) study_subject.mark_as_resigned() p.save() + + # loop over custom fields to add Provenance to tracked custom fields + for field in CustomStudySubjectValue.objects.filter(study_subject=study_subject): + field_id = get_study_subject_field_id(field.study_subject_field) + prev_value = prev_value_list_custom_fields.pop(0) + if field_id in study_subject_form.changed_data and field.study_subject_field.tracked: + curr_value = study_subject_form.cleaned_data[field_id] + if prev_value != curr_value: + worker = Worker.get_by_user(request.user) + p = Provenance(modified_table=StudySubject._meta.db_table, + modified_table_id=study_subject.id, + modification_author=worker, + previous_value=prev_value, + new_value=curr_value, + modification_description='Worker "{}" changed study subject "{}" field "{}" ' + 'value from study "{}"' + .format(worker, study_subject.nd_number, field.study_subject_field.name, + study_subject.study), + modified_field=field.study_subject_field.name, + request_path=request.path, + request_ip_addr=ip + ) + p.save() + messages.success(request, "Modifications saved") if "_continue" in request.POST: return redirect("web.views.subject_edit", subject_id=study_subject.id)