Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/all attachment kinds #753

Merged
merged 26 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4cca9c1
feat: create all InformationLetter kinds
EdoStorm96 Oct 24, 2024
9183175
feat: add correct informationletter in checker
EdoStorm96 Oct 24, 2024
29a9d1c
feat: check if Study has AV registration
EdoStorm96 Oct 24, 2024
d3717ce
fix: bug, where legal basis can be 0 ... woopsie
EdoStorm96 Oct 24, 2024
6654c4a
feature: add all possible kinds
EdoStorm96 Nov 7, 2024
ba2ddfd
feature: add all slots, through checkers
EdoStorm96 Nov 7, 2024
4b27c6d
feat: implement navigation buttons for ProposalsAttachmentsView
EdoStorm96 Nov 7, 2024
fc848fc
feature: a lotta translations
EdoStorm96 Nov 7, 2024
0f52a65
Merge branch 'feature/attachments-4' into feature/all-attachment-kinds
EdoStorm96 Nov 7, 2024
489bb64
style: black sabbath
EdoStorm96 Nov 7, 2024
8562556
Merge remote-tracking branch 'origin/feature/attachments-4' into feat…
miggol Nov 12, 2024
91d56d0
fix: Make get_kind_from_str resilient to unknown kinds
miggol Nov 13, 2024
004491a
feat: Rename OtherProposalAttachment
miggol Nov 13, 2024
9a94224
feat: Optionality groups for attachment slots
miggol Nov 13, 2024
465d10f
feat: Render help texts next to slots
miggol Nov 13, 2024
661b418
style: Black and djlint
miggol Nov 13, 2024
a6b3dc5
Merge pull request #755 from DH-IT-Portal-Development/feature/all_kin…
EdoStorm96 Nov 13, 2024
a95755e
fix: logic of slots
EdoStorm96 Nov 13, 2024
4920ba3
feature: implement match + match_and_set
EdoStorm96 Nov 13, 2024
db9ea29
feat: has_adults() for Study
EdoStorm96 Nov 13, 2024
4028328
feat: base ConsentForm on whether Study.has_adults()
EdoStorm96 Nov 13, 2024
cbb0059
style: black betty
EdoStorm96 Nov 13, 2024
2314349
fix: Give match_and_set() a return value
miggol Nov 14, 2024
2d1e3fc
fix: potential TypeError: addition of list and set
miggol Nov 14, 2024
63315bf
Merge branch 'feature/attachments-4' into feature/all-attachment-kinds
miggol Nov 20, 2024
159699c
merge: merge translations and do a style pass
miggol Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 184 additions & 9 deletions attachments/kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,198 @@ class StudyAttachmentKind(AttachmentKind):
attachment_class = StudyAttachment


class InformationLetter(StudyAttachmentKind):
#####################
# Information letters
#####################


class InformationLetterAnonymous(StudyAttachmentKind):

db_name = "information_letter_anonymous"
name = _("Informatiebrief anoniem onderzoek")
description = _(
"Je verzamelt en verwerkt de gegevens van je deelnemers"
" anoniem. Je moet je deelnemers dan wél informeren op"
" ethische gronden (zodat ze kunnen beslissen of ze"
" vrijwillig willen meedoen), maar omdat de AVG niet"
" van toepassing is op anonieme gegevens, hoef je geen"
" informatie te verstrekken over de verwerking van"
" persoonsgegevens."
)
desiredness = desiredness.REQUIRED


class InformationLetterPublicInterest(StudyAttachmentKind):

db_name = "information_letter_public_interest"
name = _("Informatiebrief algemeen belang")
description = _(
"Je kiest ervoor om de verwerking van persoonsgegevens te"
" baseren op het algemeen belang. In beginsel is dit"
" de standaardwerkwijze. \n"
"Let op! Voor bepaalde aspecten van je onderzoek kan het"
" desondanks nodig zijn om toestemming te vragen."
)
desiredness = desiredness.REQUIRED


db_name = "information_letter"
name = _("Informatiebrief")
description = _("Omschrijving informatiebrief")
class InformationLetterConsent(StudyAttachmentKind):

db_name = "information_letter_consent"
name = _("Informatiebrief toestemming")
description = _(
"Er zijn redenen om de verwerking van persoonsgegevens"
" te baseren op toestemming van de deelnemers."
" Bijvoorbeeld als er gevoelige data wordt verzameld of"
" er met minderjarigen wordt gewerkt."
)
desiredness = desiredness.REQUIRED


class ConsentForm(AttachmentKind):
LEGAL_BASIS_KIND_DICT = {
Study.LegalBases.ANONYMOUS: InformationLetterAnonymous,
Study.LegalBases.CONSENT: InformationLetterConsent,
Study.LegalBases.PUBLIC_INTEREST: InformationLetterPublicInterest,
}


###############
# Consent forms
###############


class ConsentForm(StudyAttachmentKind):

db_name = "consent_form"
name = _("Toestemmingsverklaring")
description = _("Omschrijving toestemmingsverklaring")
description = _(
"Je baseert de verwerking van persoonsgegevens binnen je onderzoek op"
" de wettelijke grondslag toestemming. De deelnemers zijn volwassenen"
" (16 jaar en ouder)."
)
desiredness = desiredness.REQUIRED


class ConsentPublicInterestSpecialDetails(StudyAttachmentKind):

db_name = "consent_public_interest_special_details"
name = _("Toestemmingsverklaring algemeen belang bijzondere persoonsgegevens")
description = _(
"Je baseert de verwerking van persoonsgegevens binnen je onderzoek"
" weliswaar op de wettelijke grondslag algemeen belang, maar je"
" verzamelt zgn. bijzondere persoonsgegevens externe link en daarvoor"
" heb je toestemming nodig."
)
desiredness = desiredness.REQUIRED


class ConsentChildrenParents(StudyAttachmentKind):

db_name = "consent_children_w_parents"
name = _("Toestemmingsverklaring kinderen tot 16 jaar, ouders of voogden aanwezig")
description = _(
"Je baseert de verwerking van persoonsgegevens binnen je"
" onderzoek op de wettelijke grondslag toestemming. De"
" deelnemers zijn kinderen jonger dan 16 jaar en minimaal"
" één van de ouders of voogden is bij het onderzoek aanwezig."
)
desiredness = desiredness.REQUIRED


class ConsentChildrenNoParents(StudyAttachmentKind):

db_name = "consent_children_no_parents"
name = _("Toestemmingsverklaring kinderen tot 16 jaar, ouders of voogden afwezig")
description = _(
"Je baseert de verwerking van persoonsgegevens binnen je"
" onderzoek op de wettelijke grondslag toestemming. De"
" deelnemers zijn kinderen jonger dan 16 jaar en er zijn"
" geen ouders of voodgen aanwezig."
)
desiredness = desiredness.REQUIRED


####################
# Recordings consent
####################


class AgreementRecordingsPublicInterest(StudyAttachmentKind):

db_name = "agreement_av_recordings"
name = _("Akkoordverklaring beeld- en geluidsopnames algemeen belang")
description = _(
"Je baseert de verwerking van persoonsgegevens weliswaar op de"
" wettelijke grondslag algemeen belang, maar je maakt beeld- en/of"
" geluidsopnames en daar moeten je deelnemers op ethische gronden"
" mee instemmen. Ook voor het verdere gebruik van die opnames kun"
" je de afspraken schriftelijk vastleggen."
)
desiredness = desiredness.REQUIRED


class ScriptVerbalConsentRecordings(StudyAttachmentKind):

db_name = "script_verbal_consent_recordings"
name = _("Script voor mondelinge toestemming opnames")
description = _(
"Je interviewt deelnemers en je maakt daarvan beeld- en/of"
" geluidsopnames. De toestemming daarvoor, en de eventuele"
" toestemming en afspraken met betrekking tot andere aspecten van"
" de verwerking van persoonsgegevens, leg je vast in een aparte opname."
)
desiredness = desiredness.REQUIRED


##############
# School stuff
##############


class SchoolInformationLetter(ProposalAttachmentKind):

db_name = "school_information_letter"
name = _("Informatiebrief gatekeeper/schoolleiding")
description = _(
"Je wilt onderzoek gaan doen binnen een bepaalde instelling (bijv."
" een school) en je verzoekt de leiding van die instelling om"
" medewerking. Voor het maken van een geïnformeerde keuze moet de"
" leiding van de instelling op de hoogte zijn van de opzet van je"
" onderzoek en van allerlei praktische aspecten."
)
desiredness = desiredness.REQUIRED


class SchoolConsentForm(ProposalAttachmentKind):

db_name = "school_consent_form"
name = _("Akkoordverklaring gatekeeper/schoolleiding")
description = _(
"Je wilt onderzoek gaan doen binnen een bepaalde instelling (bijv."
" een school) en je verzoekt de leiding van die instelling om"
" medewerking. De schoolleiding moet, na goed geïnformeerd te zijn"
"toestemming geven voor het onderzoek."
)
desiredness = desiredness.REQUIRED


#############
# Other stuff
#############


class DataManagementPlan(ProposalAttachmentKind):

db_name = "dmp"
name = _("Data Management Plan")
description = _("Omschrijving DMP")
desiredness = desiredness.RECOMMENDED

def num_recommended(self):
return 1


class OtherProposalAttachment(ProposalAttachmentKind):
class OtherAttachment(ProposalAttachmentKind):

db_name = "other"
name = _("Overig bestand")
Expand All @@ -61,13 +227,22 @@ def num_suggested(self):


STUDY_ATTACHMENTS = [
InformationLetter,
InformationLetterAnonymous,
InformationLetterPublicInterest,
InformationLetterConsent,
AgreementRecordingsPublicInterest,
ScriptVerbalConsentRecordings,
ConsentPublicInterestSpecialDetails,
ConsentChildrenParents,
ConsentChildrenNoParents,
ConsentForm,
]

PROPOSAL_ATTACHMENTS = [
SchoolInformationLetter,
SchoolConsentForm,
DataManagementPlan,
OtherProposalAttachment,
OtherAttachment,
]

ATTACHMENTS = PROPOSAL_ATTACHMENTS + STUDY_ATTACHMENTS
Expand Down
12 changes: 12 additions & 0 deletions attachments/templates/attachments/optionality_group.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% load i18n %}

<div class="border-start border-4 p-2 m-0 ps-4 pe-0">
<h6 class="m-0 p-0">
{% blocktrans trimmed with count=group.count %}
Een van deze {{ count }} documenten is voldoende
{% endblocktrans %}
</h6>
{% for slot in group.members %}
{% include slot %}
{% endfor %}
</div>
42 changes: 26 additions & 16 deletions attachments/templates/attachments/slot.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
{% load i18n %}

<div class="row mt-4 mb-4 p-3 border-3 border-bottom rounded {{classes}} bg-light">
<div class="col-2 align-middle">{{ slot.desiredness }}</div>
<div class="col-7 align-middle">
<h5>{{ slot.kind.name }}</h5>
{% if slot.attachment %}
{% include slot.attachment with proposal=proposal %}
{% else %}
{% trans "Nog toe te voegen" %}
{% endif %}
{{ kind.reason }}
<div class="uu-form-row mt-4 mb-4">
<div class="">
<div class="row border-3 p-3 border-bottom rounded {{ classes }} bg-light">
<div class="col-2 align-middle">{{ slot.desiredness }}</div>
<div class="col-7 align-middle">
<h5>{{ slot.kind.name }}</h5>
{% if slot.attachment %}
{% include slot.attachment with proposal=proposal %}
{% else %}
{% trans "Nog toe te voegen" %}
{% endif %}
{{ kind.reason }}
</div>
<div class="col-3 align-middle text-end flex-end">
{% if slot.attachment %}
<a class="btn btn-primary" href="{{ slot.get_edit_url }}">{% trans "Wijzig" %}</a>
{% else %}
<a class="btn btn-primary" href="{{ slot.get_attach_url }}">{% trans "Voeg toe" %}</a>
{% endif %}
</div>
</div>
</div>
<div class="col-3 align-middle text-end flex-end">
{% if slot.attachment %}
<a class="btn btn-primary" href="{{ slot.get_edit_url }}">{% trans "Wijzig" %}</a>
{% else %}
<a class="btn btn-primary" href="{{ slot.get_attach_url }}">{% trans "Voeg toe" %}</a>
{% endif %}
<div class="uu-form-help">
<details>
<summary class="text-muted">Meer info</summary>
{{ slot.kind.description }}
</details>
</div>
</div>
81 changes: 72 additions & 9 deletions attachments/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,36 @@ def __init__(
attachment=None,
kind=None,
force_desiredness=None,
optionality_group=None,
):
self.attachment = attachment
self.attached_object = attached_object
self.kind = kind
self.force_desiredness = force_desiredness
self.optionality_group = optionality_group
if self.optionality_group:
self.optionality_group.members.add(self)

def match(self, exclude):
def match(self, exclude=[]):
"""
Tries to fill this slot with an existing attachment that is not
in the exclusion set of already matched attachments. Returns True
or False depending on if the slot was succesfully matched.
Tries to find a matching attachment for this slot. If it finds one,
it returns the attachment, otherwise it returns False.
"""
for instance in self.get_instances_for_slot():
if instance not in exclude:
self.attachment = instance
self.kind = get_kind_from_str(instance.kind)
return True
return instance
return False

def match_and_set(self, exclude):
"""
Uses self.match() to find a matching attachment. If it finds one, it
sets self.attachment and self.kind
"""
matched_attachment = self.match(exclude=exclude)
if matched_attachment:
self.attachment = matched_attachment
self.kind = get_kind_from_str(matched_attachment.kind)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Match_and_set should actually still return True, or the instance, or anything truthy if it succeeds. Otherwise stepper.attachment_slots() will not work.

Although in that specific method we do actually want to modify internal slot state.


@property
def classes(self):
if self.required:
Expand Down Expand Up @@ -141,8 +152,60 @@ def get_edit_url(
)


class OptionalityGroup(renderable):

template_name = "attachments/optionality_group.html"

def __init__(self, members=set()):
self.members = set(members)

@property
def count(
self,
):
return len(self.members)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["group"] = self
return context


def merge_groups(slots):
"""
Takes a list of slots and merges slots that belong to the same
optionality group together. This results in a mixed output list
of bare slots and optionality groups.
"""
grouped = []
for slot in slots:
if not slot.optionality_group:
# No group, so we just append it
grouped.append(slot)
continue
if slot.optionality_group not in grouped:
# We only append the group if it's not already in the
# output list to avoid duplication
grouped.append(slot.optionality_group)
# Final pass to remove single-member groups
out = []
for item in grouped:
if type(item) is OptionalityGroup:
if item.count < 2:
# If we have fewer than two members, we just append
# the members. Addition allows for the empty list edge
# case to work.
out += item.members
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an error by me. I wrote this before turning members into a set(). But we can't add sets and lists together.

continue
out.append(item)
return out


def get_kind_from_str(db_name):
from attachments.kinds import ATTACHMENTS
from attachments.kinds import ATTACHMENTS, OtherAttachment

kinds = {kind.db_name: kind for kind in ATTACHMENTS}
return kinds[db_name]
try:
return kinds[db_name]
except KeyError:
return OtherAttachment
Loading
Loading