From ac565e8c2dc8bfd5e2a4d2045301c0ef9f879ab5 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 5 Jul 2021 18:14:31 +0200 Subject: [PATCH 01/11] add subject pseudonyms view --- .../subjects/subject_pseudonyms.html | 25 +++++++++++++++++++ castellum/subjects/urls.py | 2 ++ castellum/subjects/views.py | 21 ++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 castellum/subjects/templates/subjects/subject_pseudonyms.html diff --git a/castellum/subjects/templates/subjects/subject_pseudonyms.html b/castellum/subjects/templates/subjects/subject_pseudonyms.html new file mode 100644 index 000000000..300205979 --- /dev/null +++ b/castellum/subjects/templates/subjects/subject_pseudonyms.html @@ -0,0 +1,25 @@ +{% extends "subjects/subject_base.html" %} +{% load i18n utils %} + +{% block title %}{% translate 'General pseudonyms' %} · {{ block.super }}{% endblock %} + +{% block content %} + + + + + + + + + + {% for domain, pseudonym in pseudonyms %} + + + {% endfor %} + +
{% translate 'Domain key' %}{% translate 'Domain name' %}{% translate 'Pseudonym' %}
{{ domain|display:'key' }} + {{ domain|display:'name' }} + {{ pseudonym }} +
+{% endblock %} diff --git a/castellum/subjects/urls.py b/castellum/subjects/urls.py index 4999de5c1..d6d0b9021 100644 --- a/castellum/subjects/urls.py +++ b/castellum/subjects/urls.py @@ -41,6 +41,7 @@ from .views import ParticipationRecruitView from .views import SubjectDeleteView from .views import SubjectDetailView from .views import SubjectExportView +from .views import SubjectPseudonymsView from .views import SubjectSearchView app_name = 'subjects' @@ -67,6 +68,7 @@ urlpatterns = [ ), path('maintenance/notes/', MaintenanceNotesView.as_view(), name='maintenance-notes'), path('/', SubjectDetailView.as_view(), name='detail'), + path('/pseudonyms/', SubjectPseudonymsView.as_view(), name='pseudonyms'), path('/delete/', SubjectDeleteView.as_view(), name='delete'), path('/export/', SubjectExportView.as_view(), name='export'), path('/contact/', ContactUpdateView.as_view(), name='contact'), diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index b9a60b313..c40d309de 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -54,6 +54,8 @@ from castellum.contacts.forms import SearchForm from castellum.contacts.mixins import BaseContactUpdateView from castellum.contacts.models import Address from castellum.contacts.models import Contact +from castellum.pseudonyms.helpers import get_pseudonym +from castellum.pseudonyms.models import Domain from castellum.recruitment import filter_queries from castellum.recruitment.mixins import BaseAttributesUpdateView from castellum.recruitment.models import AttributeDescription @@ -209,6 +211,25 @@ class SubjectDetailView(SubjectMixin, PermissionRequiredMixin, DetailView): return context +class SubjectPseudonymsView(SubjectMixin, PermissionRequiredMixin, DetailView): + model = Subject + template_name = 'subjects/subject_pseudonyms.html' + tab = 'detail' + + def has_permission(self): + return ( + self.request.user.has_perm('subjects.export_subject') + or self.request.user.has_perm('subjects.delete_subject') + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['pseudonyms'] = [ + (d, get_pseudonym(self.subject, d.key)) for d in Domain.objects.filter(object_id=None) + ] + return context + + class SubjectDeleteView(SubjectMixin, PermissionRequiredMixin, DeleteView): model = Subject permission_required = 'subjects.delete_subject' -- GitLab From 26b094200697715ab05f54aa402b4437941e07f3 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 2 Aug 2021 11:40:33 +0200 Subject: [PATCH 02/11] monitor pseudonym access --- .../subjects/subject_pseudonyms.html | 8 ++++-- castellum/subjects/urls.py | 2 ++ castellum/subjects/views.py | 25 ++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/castellum/subjects/templates/subjects/subject_pseudonyms.html b/castellum/subjects/templates/subjects/subject_pseudonyms.html index 300205979..cc653013a 100644 --- a/castellum/subjects/templates/subjects/subject_pseudonyms.html +++ b/castellum/subjects/templates/subjects/subject_pseudonyms.html @@ -13,11 +13,15 @@ - {% for domain, pseudonym in pseudonyms %} + {% for domain in domains %} {{ domain|display:'key' }} {{ domain|display:'name' }} - {{ pseudonym }} + + + {% translate 'get pseudonym' %} + + {% endfor %} diff --git a/castellum/subjects/urls.py b/castellum/subjects/urls.py index d6d0b9021..74472dd9a 100644 --- a/castellum/subjects/urls.py +++ b/castellum/subjects/urls.py @@ -42,6 +42,7 @@ from .views import SubjectDeleteView from .views import SubjectDetailView from .views import SubjectExportView from .views import SubjectPseudonymsView +from .views import SubjectPseudonymView from .views import SubjectSearchView app_name = 'subjects' @@ -69,6 +70,7 @@ urlpatterns = [ path('maintenance/notes/', MaintenanceNotesView.as_view(), name='maintenance-notes'), path('/', SubjectDetailView.as_view(), name='detail'), path('/pseudonyms/', SubjectPseudonymsView.as_view(), name='pseudonyms'), + path('/pseudonyms//', SubjectPseudonymView.as_view(), name='pseudonym'), path('/delete/', SubjectDeleteView.as_view(), name='delete'), path('/export/', SubjectExportView.as_view(), name='export'), path('/contact/', ContactUpdateView.as_view(), name='contact'), diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index c40d309de..c6e8d9ac9 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -224,12 +224,31 @@ class SubjectPseudonymsView(SubjectMixin, PermissionRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['pseudonyms'] = [ - (d, get_pseudonym(self.subject, d.key)) for d in Domain.objects.filter(object_id=None) - ] + context['domains'] = Domain.objects.filter(object_id=None) return context +class SubjectPseudonymView(SubjectMixin, PermissionRequiredMixin, View): + def get_object(self): + return get_object_or_404(Subject, slug=self.kwargs['slug']) + + def has_permission(self): + return ( + self.request.user.has_perm('subjects.export_subject') + or self.request.user.has_perm('subjects.delete_subject') + ) + + def get(self, request, *args, **kwargs): + domain = get_object_or_404(Domain, key=kwargs['domain'], object_id=None) + pseudonym = get_pseudonym(self.subject, domain.key) + + monitoring_logger.info('Pseudonym access: domain {} by {}'.format( + domain.key, self.request.user.pk + )) + + return HttpResponse(pseudonym) + + class SubjectDeleteView(SubjectMixin, PermissionRequiredMixin, DeleteView): model = Subject permission_required = 'subjects.delete_subject' -- GitLab From ab99b04f62607190b67325ac471363832ada15f9 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 2 Aug 2021 10:50:48 +0200 Subject: [PATCH 03/11] refactor subject_confirm_delete --- .../subjects/subject_confirm_delete.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/castellum/subjects/templates/subjects/subject_confirm_delete.html b/castellum/subjects/templates/subjects/subject_confirm_delete.html index 08e11fe8f..8d5a6b796 100644 --- a/castellum/subjects/templates/subjects/subject_confirm_delete.html +++ b/castellum/subjects/templates/subjects/subject_confirm_delete.html @@ -5,16 +5,25 @@ {% block content %}
+ {% csrf_token %} + {% if is_last_guardian %}

{% translate "This subject is the only guardian of another subject. Deletion means that this subject's ward becomes unreachable!" %}

{% endif %} + {% if has_participations %}

{% translate "This subject cannot be deleted because there still is data about them in studies." %}

+ {% else %} +

+ {% translate "This subject does not have any data in studies that would block deletion." %} +

+ {% endif %} + {% if has_participations %}

{% translate "Please delete all of these study participations:" %}

@@ -47,14 +56,9 @@
- {% else %} - {% csrf_token %} -

- {% translate "This subject does not have any data in studies that would block deletion." %} -

-

{% translate "Are you sure you want to permanently delete this subject and all related data?" %}

{% endif %} +

{% translate "Are you sure you want to permanently delete this subject and all related data?" %}

{% translate 'Cancel' %} -- GitLab From 6559b3e2f583be2a54573d60a5cc9451b85e3f61 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 2 Aug 2021 10:51:30 +0200 Subject: [PATCH 04/11] link to independent pseudonyms from subject delete and export --- .../subjects/subject_confirm_delete.html | 29 +++++++++++++++++-- .../templates/subjects/subject_export.html | 14 +++++++-- castellum/subjects/views.py | 4 +++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/castellum/subjects/templates/subjects/subject_confirm_delete.html b/castellum/subjects/templates/subjects/subject_confirm_delete.html index 8d5a6b796..6253ae391 100644 --- a/castellum/subjects/templates/subjects/subject_confirm_delete.html +++ b/castellum/subjects/templates/subjects/subject_confirm_delete.html @@ -17,16 +17,23 @@

{% translate "This subject cannot be deleted because there still is data about them in studies." %}

+ {% elif has_pseudonym_in_general_domain %} +

+ {% translate "This subject may still have data in general domains." %} +

{% else %}

{% translate "This subject does not have any data in studies that would block deletion." %}

{% endif %} - {% if has_participations %} + {% if has_participations or has_pseudonym_in_general_domain %}
-

{% translate "Please delete all of these study participations:" %}

+ {% if has_participations %} +

{% translate "Please delete all of these study participations:" %}

+ {% endif %} +
    {% for participation in object.participation_set.all %} {% has_perm 'studies.view_study' user participation.study as can_view_study %} @@ -53,15 +60,31 @@
{% endfor %} + {% if has_pseudonym_in_general_domain %} +
  • +
    +
    {% translate "General pseudonym domains" %}
    +
    + +
  • + {% endif %}
    {% endif %}

    {% translate "Are you sure you want to permanently delete this subject and all related data?" %}

    + {% if not has_participations and has_pseudonym_in_general_domain %} + + {% endif %}
    {% translate 'Cancel' %} - +
    {% endblock %} diff --git a/castellum/subjects/templates/subjects/subject_export.html b/castellum/subjects/templates/subjects/subject_export.html index 681640ee2..965022f5b 100644 --- a/castellum/subjects/templates/subjects/subject_export.html +++ b/castellum/subjects/templates/subjects/subject_export.html @@ -28,10 +28,10 @@ {% endif %} - {% if has_participations %} + {% if has_participations or general_domains_exist %}
    -

    {% translate 'Remember to also gather information from the following study participations:' %}

    +

    {% translate 'Remember to also gather information from the following external sources:' %}

      {% for participation in object.participation_set.all %} {% has_perm 'studies.view_study' user participation.study as can_view_study %} @@ -55,6 +55,16 @@
    {% endfor %} + {% if general_domains_exist %} +
  • +
    +
    {% translate "General pseudonym domains" %}
    +
    + +
  • + {% endif %}
    diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index c6e8d9ac9..dc448a31c 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -266,6 +266,9 @@ class SubjectDeleteView(SubjectMixin, PermissionRequiredMixin, DeleteView): context = super().get_context_data(**kwargs) context['has_participations'] = self.object.participation_set.exists() context['is_last_guardian'] = self.is_last_guardian() + context['has_pseudonym_in_general_domain'] = ( + self.subject.pseudonym_set.filter(domain__object_id=None).exists() + ) return context def get_success_url(self): @@ -307,6 +310,7 @@ class SubjectExportView(SubjectMixin, PermissionRequiredMixin, DetailView): )) context['has_participations'] = self.object.participation_set.exists() + context['general_domains_exist'] = Domain.objects.filter(object_id=None).exists() context['appointments'] = Appointment.objects.filter( participation__subject=self.object ) -- GitLab From 581f566dfb3a822fbec074af8d6aa9f96a290887 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 13 Jul 2021 17:36:52 +0200 Subject: [PATCH 05/11] do not create new pseudonyms in subject pseudonym view --- castellum/pseudonyms/helpers.py | 8 ++++++++ castellum/subjects/views.py | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/castellum/pseudonyms/helpers.py b/castellum/pseudonyms/helpers.py index c302526c6..ef12f455d 100644 --- a/castellum/pseudonyms/helpers.py +++ b/castellum/pseudonyms/helpers.py @@ -48,6 +48,14 @@ def get_pseudonym(subject, target_domain_key): return target.pseudonym +def get_pseudonym_if_exists(subject, target_domain_key): + # You should use ``get_pseudonym()`` in most cases as it does not + # leak whether a pseudony exists. + target_domain = Domain.objects.get(key=target_domain_key) + target = Pseudonym.objects.filter(subject=subject, domain=target_domain).first() + return target.pseudonym if target else None + + def delete_pseudonym(domain_key, pseudonym): p = Pseudonym.objects.get(domain__key=domain_key, pseudonym=pseudonym) p.subject = None diff --git a/castellum/subjects/views.py b/castellum/subjects/views.py index dc448a31c..dbfb52056 100644 --- a/castellum/subjects/views.py +++ b/castellum/subjects/views.py @@ -54,7 +54,7 @@ from castellum.contacts.forms import SearchForm from castellum.contacts.mixins import BaseContactUpdateView from castellum.contacts.models import Address from castellum.contacts.models import Contact -from castellum.pseudonyms.helpers import get_pseudonym +from castellum.pseudonyms.helpers import get_pseudonym_if_exists from castellum.pseudonyms.models import Domain from castellum.recruitment import filter_queries from castellum.recruitment.mixins import BaseAttributesUpdateView @@ -240,13 +240,13 @@ class SubjectPseudonymView(SubjectMixin, PermissionRequiredMixin, View): def get(self, request, *args, **kwargs): domain = get_object_or_404(Domain, key=kwargs['domain'], object_id=None) - pseudonym = get_pseudonym(self.subject, domain.key) + pseudonym = get_pseudonym_if_exists(self.subject, domain.key) monitoring_logger.info('Pseudonym access: domain {} by {}'.format( domain.key, self.request.user.pk )) - return HttpResponse(pseudonym) + return HttpResponse(pseudonym or '—') class SubjectDeleteView(SubjectMixin, PermissionRequiredMixin, DeleteView): -- GitLab From a20c94dc4f392d4eaf4e32b6d91818dbefc63489 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 7 Jun 2021 15:39:00 +0200 Subject: [PATCH 06/11] add study.general_domains --- .../migrations/0033_study_general_domains.py | 19 +++++++++++++++++++ castellum/studies/models.py | 9 +++++++++ .../studies/templates/studies/study_diff.html | 3 +++ castellum/studies/views/studies.py | 1 + 4 files changed, 32 insertions(+) create mode 100644 castellum/studies/migrations/0033_study_general_domains.py diff --git a/castellum/studies/migrations/0033_study_general_domains.py b/castellum/studies/migrations/0033_study_general_domains.py new file mode 100644 index 000000000..424942233 --- /dev/null +++ b/castellum/studies/migrations/0033_study_general_domains.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.5 on 2021-07-27 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pseudonyms', '0004_domain_name'), + ('studies', '0032_study_is_filter_trial'), + ] + + operations = [ + migrations.AddField( + model_name='study', + name='general_domains', + field=models.ManyToManyField(blank=True, help_text='These are domains that are shared among studies, e.g. for central data repositories.', limit_choices_to={'object_id': None}, to='pseudonyms.Domain', verbose_name='General pseudonym domains'), + ), + ] diff --git a/castellum/studies/models.py b/castellum/studies/models.py index ce22f4dd6..1cbfb1a47 100644 --- a/castellum/studies/models.py +++ b/castellum/studies/models.py @@ -130,6 +130,15 @@ class Study(models.Model): members = models.ManyToManyField(User, through='StudyMembership') domains = GenericRelation(Domain) + general_domains = models.ManyToManyField( + Domain, + verbose_name=_('General pseudonym domains'), + help_text=_( + 'These are domains that are shared among studies, e.g. for central data repositories.' + ), + blank=True, + limit_choices_to={'object_id': None}, + ) advanced_filtering = models.BooleanField( _('Advanced filtering'), help_text=_( diff --git a/castellum/studies/templates/studies/study_diff.html b/castellum/studies/templates/studies/study_diff.html index 4753e0129..3826b4c3f 100644 --- a/castellum/studies/templates/studies/study_diff.html +++ b/castellum/studies/templates/studies/study_diff.html @@ -55,6 +55,9 @@
    {% translate 'Domains' %}
    {% diff_get study 'domains' as items %}{% diff_list items %}
    +
    {{ study|verbose_name:'general_domains' }}
    +
    {% diff_get study 'general_domains' as items %}{% diff_list items %}
    +
    {{ study|verbose_name:'exportable_attributes' }}
    {% diff_get study 'exportable_attributes' as items %}{% diff_list items %}
    diff --git a/castellum/studies/views/studies.py b/castellum/studies/views/studies.py index 4533857db..be892ede8 100644 --- a/castellum/studies/views/studies.py +++ b/castellum/studies/views/studies.py @@ -108,6 +108,7 @@ def take_snapshot(study): study, *study.studymembership_set.all(), *study.domains.all(), + *study.general_domains.all(), *study.studytag_set.all(), *study.studysession_set.all(), *study.subjectfiltergroup_set.all(), -- GitLab From 56e554c50bc82e3222fcb79f02d246a45d5622b9 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Wed, 28 Jul 2021 17:21:12 +0200 Subject: [PATCH 07/11] form UI in separate subtab --- .../templates/studies/study_domains_base.html | 22 ++++++++++++++ .../studies/study_domains_general.html | 14 +++++++++ ..._domains.html => study_domains_study.html} | 2 +- castellum/studies/urls/__init__.py | 6 ++-- castellum/studies/views/studies.py | 30 +++++++++++++++++-- 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 castellum/studies/templates/studies/study_domains_base.html create mode 100644 castellum/studies/templates/studies/study_domains_general.html rename castellum/studies/templates/studies/{study_pseudonym_domains.html => study_domains_study.html} (97%) diff --git a/castellum/studies/templates/studies/study_domains_base.html b/castellum/studies/templates/studies/study_domains_base.html new file mode 100644 index 000000000..629c32d57 --- /dev/null +++ b/castellum/studies/templates/studies/study_domains_base.html @@ -0,0 +1,22 @@ +{% extends "studies/study_base.html" %} +{% load i18n auth bootstrap4 utils %} + +{% block container_class %}{% if general_domains_exist %}container-lg{% else %}container{% endif %}{% endblock %} + +{% block content_with_messages %} + {% if general_domains_exist %} + + {% else %} + {{ block.super }} + {% endif %} +{% endblock %} diff --git a/castellum/studies/templates/studies/study_domains_general.html b/castellum/studies/templates/studies/study_domains_general.html new file mode 100644 index 000000000..f22c6109a --- /dev/null +++ b/castellum/studies/templates/studies/study_domains_general.html @@ -0,0 +1,14 @@ +{% extends "studies/study_domains_base.html" %} +{% load i18n auth bootstrap4 utils %} + +{% block title %}{% translate "General domains" %} · {{ block.super }}{% endblock %} + +{% block content %} +
    + {% include 'utils/form_errors.html' with form=form %} + {% csrf_token %} +

    {% translate 'General Domain Pseudonyms allow you to access data about subjects without a specific study context. This allows institutes to hold stable and/or universal information on subjects in a separate domain (allowing to store the data in an external system). Please note that you should have a good reason to apply for a general domain within your study.' %}

    + {% bootstrap_field form.general_domains %} + +
    +{% endblock %} diff --git a/castellum/studies/templates/studies/study_pseudonym_domains.html b/castellum/studies/templates/studies/study_domains_study.html similarity index 97% rename from castellum/studies/templates/studies/study_pseudonym_domains.html rename to castellum/studies/templates/studies/study_domains_study.html index 24c0025aa..abdd2f942 100644 --- a/castellum/studies/templates/studies/study_pseudonym_domains.html +++ b/castellum/studies/templates/studies/study_domains_study.html @@ -1,4 +1,4 @@ -{% extends "studies/study_base.html" %} +{% extends "studies/study_domains_base.html" %} {% load i18n auth bootstrap4 utils %} {% block title %}{% translate "Pseudonym domains" %} · {{ block.super }}{% endblock %} diff --git a/castellum/studies/urls/__init__.py b/castellum/studies/urls/__init__.py index 78ce50f23..cca8c6fbf 100644 --- a/castellum/studies/urls/__init__.py +++ b/castellum/studies/urls/__init__.py @@ -29,11 +29,12 @@ from ..views.studies import StudyCreateView from ..views.studies import StudyDeleteView from ..views.studies import StudyDetailView from ..views.studies import StudyDiffView +from ..views.studies import StudyDomainsGeneralView +from ..views.studies import StudyDomainsView from ..views.studies import StudyExportView from ..views.studies import StudyFinishRecruitmentView from ..views.studies import StudyImportView from ..views.studies import StudyIndexView -from ..views.studies import StudyPseudonymDomainsView from ..views.studies import StudyStartRecruitmentView from ..views.studies import StudyUpdateView from . import members as members_urls @@ -48,7 +49,8 @@ urlpatterns = [ path('/', StudyDetailView.as_view(), name='detail'), path('/update/', StudyUpdateView.as_view(), name='update'), path('/delete/', StudyDeleteView.as_view(), name='delete'), - path('/domains/', StudyPseudonymDomainsView.as_view(), name='domains'), + path('/domains/', StudyDomainsView.as_view(), name='domains'), + path('/domains/general', StudyDomainsGeneralView.as_view(), name='domains-general'), 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 be892ede8..ddfca0ea9 100644 --- a/castellum/studies/views/studies.py +++ b/castellum/studies/views/studies.py @@ -392,13 +392,22 @@ class StudyFinishRecruitmentView(StudyMixin, PermissionRequiredMixin, View): return redirect(get_next_url(request, reverse('studies:detail', args=[self.study.pk]))) -class StudyPseudonymDomainsView(StudyMixin, PermissionRequiredMixin, TemplateView): +class StudyDomainsMixin(StudyMixin, PermissionRequiredMixin): permission_required = 'studies.change_study' - template_name = 'studies/study_pseudonym_domains.html' is_onetime_invitation = False is_filter_trial = False tab = 'domains' + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['general_domains_exist'] = Domain.objects.filter(object_id=None).exists() + return context + + +class StudyDomainsView(StudyDomainsMixin, TemplateView): + template_name = 'studies/study_domains_study.html' + subtab = 'domains-study' + def post(self, request, *args, **kwargs): if self.request.POST.get('action') == 'add': self.study.domains.create(bits=settings.CASTELLUM_STUDY_DOMAIN_BITS) @@ -409,7 +418,22 @@ class StudyPseudonymDomainsView(StudyMixin, PermissionRequiredMixin, TemplateVie if form.is_valid(): form.save() - return redirect('studies:domains', self.study.pk) + return redirect(self.request.path) + + +class StudyDomainsGeneralView(StudyDomainsMixin, UpdateView): + model = Study + fields = ['general_domains'] + template_name = 'studies/study_domains_general.html' + subtab = 'domains-general' + + def get_success_url(self): + return self.request.path + + def form_valid(self, form): + form.instance.is_filter_trial = False + messages.success(self.request, _('Data has been saved.')) + return super().form_valid(form) class StudyImportView(PermissionRequiredMixin, FormView): -- GitLab From 0ab383cbb4389cbe08b89638e03ae7670b292bb1 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 7 Jun 2021 16:01:44 +0200 Subject: [PATCH 08/11] display pseudonyms from general domains in execution --- castellum/execution/views.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/castellum/execution/views.py b/castellum/execution/views.py index 264c9fcb3..461716e7f 100644 --- a/castellum/execution/views.py +++ b/castellum/execution/views.py @@ -48,6 +48,7 @@ from castellum.pseudonyms.forms import DomainForm from castellum.pseudonyms.forms import PseudonymForm from castellum.pseudonyms.helpers import get_pseudonym from castellum.pseudonyms.helpers import get_subject +from castellum.pseudonyms.models import Domain from castellum.recruitment.attribute_exporters import get_exporter from castellum.recruitment.mixins import BaseAttributesUpdateView from castellum.recruitment.mixins import ParticipationMixin @@ -168,7 +169,10 @@ class ParticipationDetailView(ParticipationDetailMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['domains'] = self.study.domains.all() + context['domains'] = [ + *self.study.domains.all(), + *self.study.general_domains.all(), + ] return context @@ -206,7 +210,11 @@ class ParticipationPseudonymView(ParticipationDetailMixin, View): ) def get(self, request, *args, **kwargs): - domain = get_object_or_404(self.study.domains, key=kwargs['domain']) + qs = Domain.objects.filter( + models.Q(pk__in=self.study.domains.all()) + | models.Q(pk__in=self.study.general_domains.all()) + ) + domain = get_object_or_404(qs, key=kwargs['domain']) pseudonym = get_pseudonym(self.subject, domain.key) monitoring_logger.info('Pseudonym access: domain {} by {}'.format( -- GitLab From e4f258652394d60ff27b80388c2f7dc543fe2df0 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Mon, 2 Aug 2021 11:29:44 +0200 Subject: [PATCH 09/11] document general domains --- docs/{legacy_pseudonyms.md => pseudonyms.md} | 33 +++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) rename docs/{legacy_pseudonyms.md => pseudonyms.md} (51%) diff --git a/docs/legacy_pseudonyms.md b/docs/pseudonyms.md similarity index 51% rename from docs/legacy_pseudonyms.md rename to docs/pseudonyms.md index 1bb05d896..43e793f21 100644 --- a/docs/legacy_pseudonyms.md +++ b/docs/pseudonyms.md @@ -1,15 +1,32 @@ -# Legacy Pseudonyms +# Pseudonyms + +Castellum allows you to keep the subject's contact information in a +central place and use pseudonyms everywhere else. + +Subjects can have multiple pseudonyms. To know which is which, each +pseudonym is tied to a *pseudonym domain*. There can be only one +pseudonym for a subject in each domain. + +## General domains + +Most domains are linked to an object in Castellum, usually a study. But +there can also be so called *general domains* which are not linked to +anything in Castellum. These can be useful to provide pseudonyms for +external databases that are used accross studies, e.g. for blood samples +or IQ scores. + +General domains can be created in the admin UI by creating a domain that +has a blank `content_type` and `object_id`. Study coordinators can then +select which general domain they need to access in their study. + +## Legacy pseudonyms Castellum will automatically generate pseudonyms for subjects. However, when importing subjects from a different system, there may already be -existing pseudonyms that should not be lost. This document will describe +existing pseudonyms that should not be lost. This section will describe how those legacy pseudonyms can be imported into Castellum. -# Import data - -In Castellum, a subject can have multiple pseudonyms. To know which is -which, each pseudonym is tied to a *pseudonym domain*. There can be only -one pseudonym for a subject in each domain. +### Import data To import legacy pseudonyms you first need to create such a domain. After that you can create the pseudonyms themselves: @@ -29,7 +46,7 @@ After that you can create the pseudonyms themselves: pseudonym=legacy_pseudonym, ) -# Adapt validation +### Adapt validation Pseudonyms entered by users are validated. Since legacy pseudonyms will usually have a different format, you need to globally disable that -- GitLab From 9deb4516b074cc6cb9f51668516c0830731475d9 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 3 Aug 2021 15:14:47 +0200 Subject: [PATCH 10/11] copy general domains in study duplicate --- castellum/studies/views/studies.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/castellum/studies/views/studies.py b/castellum/studies/views/studies.py index ddfca0ea9..e75da6ed1 100644 --- a/castellum/studies/views/studies.py +++ b/castellum/studies/views/studies.py @@ -227,6 +227,8 @@ class StudyCreateView(PermissionRequiredMixin, CreateView): self.object.phone = self.duplicate.phone self.object.save() + self.object.general_domains.set(self.duplicate.general_domains.all()) + for session in self.duplicate.studysession_set.order_by('pk'): s = self.object.studysession_set.create( name=session.name, -- GitLab From b3cc4715efa5d7979bf6f4aabbfa27989df6e804 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Tue, 3 Aug 2021 16:27:50 +0200 Subject: [PATCH 11/11] refine general domains wording --- castellum/studies/migrations/0033_study_general_domains.py | 2 +- castellum/studies/models.py | 3 --- castellum/studies/templates/studies/study_domains_general.html | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/castellum/studies/migrations/0033_study_general_domains.py b/castellum/studies/migrations/0033_study_general_domains.py index 424942233..4371c678c 100644 --- a/castellum/studies/migrations/0033_study_general_domains.py +++ b/castellum/studies/migrations/0033_study_general_domains.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='study', name='general_domains', - field=models.ManyToManyField(blank=True, help_text='These are domains that are shared among studies, e.g. for central data repositories.', limit_choices_to={'object_id': None}, to='pseudonyms.Domain', verbose_name='General pseudonym domains'), + field=models.ManyToManyField(blank=True, limit_choices_to={'object_id': None}, to='pseudonyms.Domain', verbose_name='General pseudonym domains'), ), ] diff --git a/castellum/studies/models.py b/castellum/studies/models.py index 1cbfb1a47..54932d29b 100644 --- a/castellum/studies/models.py +++ b/castellum/studies/models.py @@ -133,9 +133,6 @@ class Study(models.Model): general_domains = models.ManyToManyField( Domain, verbose_name=_('General pseudonym domains'), - help_text=_( - 'These are domains that are shared among studies, e.g. for central data repositories.' - ), blank=True, limit_choices_to={'object_id': None}, ) diff --git a/castellum/studies/templates/studies/study_domains_general.html b/castellum/studies/templates/studies/study_domains_general.html index f22c6109a..26bb584b4 100644 --- a/castellum/studies/templates/studies/study_domains_general.html +++ b/castellum/studies/templates/studies/study_domains_general.html @@ -7,7 +7,7 @@
    {% include 'utils/form_errors.html' with form=form %} {% csrf_token %} -

    {% translate 'General Domain Pseudonyms allow you to access data about subjects without a specific study context. This allows institutes to hold stable and/or universal information on subjects in a separate domain (allowing to store the data in an external system). Please note that you should have a good reason to apply for a general domain within your study.' %}

    +

    {% translate 'General Domains allow you to access pseudonyms for data that is shared via central repositories. Please note these pseudonyms allow to identify subjects globally and therefore carry a high privacy risk.' %}

    {% bootstrap_field form.general_domains %}
    -- GitLab