diff --git a/castellum/execution/templates/execution/participation_detail.html b/castellum/execution/templates/execution/participation_detail.html index 93b3c7f1c1f2ddaced4b24570530b9168d4dd0ba..a48b3ae97becc86a0af571aebc7a7afb101dfe93 100644 --- a/castellum/execution/templates/execution/participation_detail.html +++ b/castellum/execution/templates/execution/participation_detail.html @@ -14,7 +14,7 @@ {% endfor %} - {% else %} + {% elif domains|length > 0 %}
{% translate 'Pseudonym' %}
{% for domain in domains %} diff --git a/castellum/execution/templates/execution/study_export.html b/castellum/execution/templates/execution/study_export.html index b78758e78bdbb256d557b4a90e2b8df27bfdb2d3..d1ff65109e4aa48032b8814381190935ab3b6ceb 100644 --- a/castellum/execution/templates/execution/study_export.html +++ b/castellum/execution/templates/execution/study_export.html @@ -5,6 +5,7 @@ {% block content %}
+ {% include 'utils/form_errors.html' with form=form %} {% bootstrap_field form.domain %}
diff --git a/castellum/execution/templates/execution/study_resolve.html b/castellum/execution/templates/execution/study_resolve.html index d9ba259f6f093635a4f8f0471bcbef32af45205a..71b27716d3b12e5ac67c41f02329e67df115ed85 100644 --- a/castellum/execution/templates/execution/study_resolve.html +++ b/castellum/execution/templates/execution/study_resolve.html @@ -6,7 +6,7 @@ {% include 'utils/form_errors.html' with form=form %} {% csrf_token %} - {% if form.domain.field.choices|length > 1 %} + {% if form.domain.field.choices|length != 1 %} {% bootstrap_field form.domain %} {% else %} {{ form.domain.as_hidden }} diff --git a/castellum/pseudonyms/forms.py b/castellum/pseudonyms/forms.py index 4f4ceaf741529b953cebe6edceb3705462d2abf1..800e2406782c72e3c104b745a608df54348c1303 100644 --- a/castellum/pseudonyms/forms.py +++ b/castellum/pseudonyms/forms.py @@ -40,7 +40,12 @@ class DomainForm(forms.Form): super().__init__(*args, **kwargs) self.fields['domain'].choices = [(d.key, str(d)) for d in context.domains.all()] - self.fields['domain'].initial = self.fields['domain'].choices[0][0] + if self.fields['domain'].choices: + self.fields['domain'].initial = self.fields['domain'].choices[0][0] + else: + self.fields['domain'].disabled = True + self.cleaned_data = {} + self.add_error(None, _('All pseudonym domains have been deleted.')) class PseudonymForm(DomainForm): diff --git a/castellum/studies/apps.py b/castellum/studies/apps.py index a55922c3a5985c3c6f28811cbac53d5321ceb4d2..4d9c19b5218297414d4b2a5977699b7a4f73f930 100644 --- a/castellum/studies/apps.py +++ b/castellum/studies/apps.py @@ -29,7 +29,6 @@ class StudiesConfig(AppConfig): name = 'castellum.studies' def ready(self): - post_save.connect(signals.create_study_domain) pre_delete.connect(signals.delete_study_domain) post_save.connect(signals.create_session_domain) diff --git a/castellum/studies/signals.py b/castellum/studies/signals.py index 97e1d7486d6df94ec7ee7859cf6835209da1d6cb..108685d2999710ebc7e8dfd2e0fcaa640a79918b 100644 --- a/castellum/studies/signals.py +++ b/castellum/studies/signals.py @@ -21,17 +21,6 @@ from django.conf import settings -def create_study_domain(sender, instance, using, **kwargs): - from castellum.pseudonyms.models import Domain - from castellum.studies.models import Study - - if sender is Study and not instance.domains.exists(): - Domain.objects.create( - bits=settings.CASTELLUM_STUDY_DOMAIN_BITS, - context=instance, - ) - - def delete_study_domain(sender, instance, using, **kwargs): from castellum.studies.models import Study diff --git a/castellum/studies/templates/studies/domain_confirm_delete.html b/castellum/studies/templates/studies/domain_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..a800809b8e6e22ea1807b03ce3028b525d3528e1 --- /dev/null +++ b/castellum/studies/templates/studies/domain_confirm_delete.html @@ -0,0 +1,22 @@ +{% extends "studies/study_domains_base.html" %} +{% load i18n auth django_bootstrap5 utils %} + +{% block title %}{% translate "Delete" %} · {% translate "Study domains" %} · {{ block.super }}{% endblock %} + +{% block content %} +
+ {% csrf_token %} +

{% blocktranslate %}Are you sure you want to permanently delete the domain "{{ object }}" and all related pseudonyms?{% endblocktranslate %}

+

{% translate "Once a pseudonym is deleted, it is no longer possible to find the corresponding contact information. Note, however, that additional steps might be necessary for full anonymization (e.g. for image data)." %}

+

{% translate "The date when a domain should be deleted is usually defined in the ethics application and the study consent form." %}

+ + +
+ {% translate 'Cancel' %} + +
+
+{% endblock %} diff --git a/castellum/studies/templates/studies/study_domains_study.html b/castellum/studies/templates/studies/study_domains_study.html index ad0e582117e05ca9783dbe7620b321fa3c4c61b8..6b1afc7acb0d921da40fd345d1b5e367054c183f 100644 --- a/castellum/studies/templates/studies/study_domains_study.html +++ b/castellum/studies/templates/studies/study_domains_study.html @@ -32,6 +32,9 @@
+
+ {% translate 'Delete' %} +
{% endfor %} diff --git a/castellum/studies/urls/__init__.py b/castellum/studies/urls/__init__.py index 243691e7132796ab35af62e190595e2faf41a1c5..d0f0669190c92c4e01df876b051e9aa4001519cc 100644 --- a/castellum/studies/urls/__init__.py +++ b/castellum/studies/urls/__init__.py @@ -30,6 +30,7 @@ from ..views.studies import StudyCreateView from ..views.studies import StudyDeleteView from ..views.studies import StudyDetailView from ..views.studies import StudyDiffView +from ..views.studies import StudyDomainDeleteView from ..views.studies import StudyDomainsGeneralView from ..views.studies import StudyDomainsView from ..views.studies import StudyExportView @@ -52,6 +53,11 @@ urlpatterns = [ path('/delete/', StudyDeleteView.as_view(), name='delete'), path('/domains/', StudyDomainsView.as_view(), name='domains'), path('/domains/general', StudyDomainsGeneralView.as_view(), name='domains-general'), + path( + '/domains//', + StudyDomainDeleteView.as_view(), + name='domain-delete', + ), path('/start/', StudyStartRecruitmentView.as_view(), name='start'), path('/finish/', StudyFinishRecruitmentView.as_view(), name='finish'), path('/mail/', OneTimeInvitationMailRecruitmentView.as_view(), name='mail'), diff --git a/castellum/studies/views/studies.py b/castellum/studies/views/studies.py index 329533c913dea63d85e5b9c7a054234715db0b5b..771aefd8f3998f74f9a6ee30e1e45c6adc57704e 100644 --- a/castellum/studies/views/studies.py +++ b/castellum/studies/views/studies.py @@ -260,6 +260,11 @@ class StudyCreateView(PermissionRequiredMixin, CreateView): for tag in self.duplicate.executiontag_set.all(): self.object.executiontag_set.create(name=tag.name) else: + Domain.objects.create( + bits=settings.CASTELLUM_STUDY_DOMAIN_BITS, + context=self.object, + ) + for name, color in settings.CASTELLUM_DEFAULT_EXECUTION_TAGS: self.object.executiontag_set.create(name=name, color=color) @@ -457,6 +462,18 @@ class StudyDomainsGeneralView(StudyDomainsMixin, UpdateView): return super().form_valid(form) +class StudyDomainDeleteView(StudyDomainsMixin, DeleteView): + model = Domain + template_name = 'studies/domain_confirm_delete.html' + subtab = 'domains-study' + + def get_object(self): + return get_object_or_404(self.study.domains.all(), key=self.kwargs['key']) + + def get_success_url(self): + return reverse('studies:domains', args=[self.study.pk]) + + class StudyImportView(PermissionRequiredMixin, FormView): permission_required = 'studies.change_study' template_name = 'studies/study_import.html' diff --git a/tests/execution/views/test_api_domains_view.py b/tests/execution/views/test_api_domains_view.py index 7b082fef758f037f3c5179e5af10870e08d723fd..bfe499ed1c3f087fcf6a6fa7417e3bda73b7ff9b 100644 --- a/tests/execution/views/test_api_domains_view.py +++ b/tests/execution/views/test_api_domains_view.py @@ -12,7 +12,7 @@ def test_apidomainsview(client, conductor, study_in_execution_status): assert response.status_code == 200 data = response.json() - assert len(data['domains']) == 3 + assert len(data['domains']) == 2 def test_no_general_domains(client, conductor, study_in_execution_status): @@ -25,7 +25,7 @@ def test_no_general_domains(client, conductor, study_in_execution_status): assert response.status_code == 200 data = response.json() - assert len(data['domains']) == 1 + assert len(data['domains']) == 0 def test_permission(client, recruiter, study_in_execution_status): diff --git a/tests/execution/views/test_export_view.py b/tests/execution/views/test_export_view.py index cace1e11c2fdc56a3b4cc8a84667770768046bf1..32a26d55c18fd13b2027d86889d5b16bb57bbb3e 100644 --- a/tests/execution/views/test_export_view.py +++ b/tests/execution/views/test_export_view.py @@ -1,4 +1,3 @@ -import pytest from model_bakery import baker from castellum.pseudonyms.models import Domain @@ -7,8 +6,9 @@ from castellum.recruitment.models import Participation from castellum.studies.models import Study -@pytest.mark.smoketest -def test_200(client, conductor, study_in_execution_status): +def test_single_domain(client, conductor, study_in_execution_status): + baker.make(Domain, context=study_in_execution_status) + client.force_login(conductor) url = '/execution/{}/export/'.format(study_in_execution_status.pk) response = client.get(url) @@ -17,17 +17,19 @@ def test_200(client, conductor, study_in_execution_status): assert response['Content-Type'] == 'application/zip' -def test_single_domain(client, conductor, study_in_execution_status): +def test_no_domain(client, conductor, study_in_execution_status): client.force_login(conductor) url = '/execution/{}/export/'.format(study_in_execution_status.pk) response = client.get(url) assert response.status_code == 200 - assert response['Content-Type'] == 'application/zip' + assert response['Content-Type'] == 'text/html; charset=utf-8' + assert b'All pseudonym domains have been deleted.' in response.content def test_multi_domain(client, conductor, study_in_execution_status): baker.make(Domain, context=study_in_execution_status) + baker.make(Domain, context=study_in_execution_status) client.force_login(conductor) url = '/execution/{}/export/'.format(study_in_execution_status.pk) @@ -39,6 +41,7 @@ def test_multi_domain(client, conductor, study_in_execution_status): def test_multi_domain_given(client, conductor, study_in_execution_status): baker.make(Participation, study=study_in_execution_status, status=Participation.INVITED) + baker.make(Domain, context=study_in_execution_status) domain2 = baker.make(Domain, context=study_in_execution_status) client.force_login(conductor) @@ -55,6 +58,7 @@ def test_multi_domain_given(client, conductor, study_in_execution_status): def test_multi_domain_given_invalid(client, conductor, study_in_execution_status): baker.make(Domain, context=study_in_execution_status) + baker.make(Domain, context=study_in_execution_status) client.force_login(conductor) url = '/execution/{}/export/?domain={}'.format(study_in_execution_status.pk, 'invalid') @@ -65,6 +69,7 @@ def test_multi_domain_given_invalid(client, conductor, study_in_execution_status def test_multi_domain_given_wrong_study(client, conductor, study_in_execution_status): study2 = baker.make(Study) + baker.make(Domain, context=study_in_execution_status) domain2 = baker.make(Domain, context=study_in_execution_status) client.force_login(conductor) diff --git a/tests/execution/views/test_pseudonyms_view.py b/tests/execution/views/test_pseudonyms_view.py index 01da3e21ef49f6b06ccbd9440d87934cc1562e63..0981f0a4136019352a7f7a20a657963c85c6ab57 100644 --- a/tests/execution/views/test_pseudonyms_view.py +++ b/tests/execution/views/test_pseudonyms_view.py @@ -28,7 +28,7 @@ def test_lists_all_pseudonyms(client, conductor, study): study.pk, participation.pk )) assert response.status_code == 200 - assert response.content.count(b'