Skip to content

Commit

Permalink
Merge branch 'release/0.3.87' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
erikvw committed Jan 11, 2025
2 parents 706d45b + 7b3713f commit 9f89380
Show file tree
Hide file tree
Showing 15 changed files with 744 additions and 90 deletions.
83 changes: 64 additions & 19 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,33 @@ edc-consent

Add classes for the Informed Consent form and process.

Installation
============

Concepts
========

In the EDC, the ICF is a model to be completed by each participant that is to follow the data collection schedule linked to the consent.

* Version:
consents have a version number. A version has a start and end date and link to a data collection schedule (visit schedule.schedule)
* Extend existing version:
an existing consent version's data collection schedule may be extended, e.g. from 12 to 24 months for participants that have completed the model defined for the extension (cdef.extended_by).
* Version updates previous version:
For changes to the protocol that affect the data collection schedule, the consent version can be bumped up. For example, bump v1 to v2. Once participants reach a report data after the v1 end data, data collection will be blocked unless v2 is completed.


Features
========

* base class for an informed consent document
* data for models that require consent cannot be add until the consent is added
* consents have a version number and validity period
* maximum number of consented subjects can be controlled.
* data collection is only allowed within the validity period of the consent per consented participant
* data for models that require consent are tagged with the consent version


Usage
=====

Declare the consent model:

Expand Down Expand Up @@ -81,7 +106,8 @@ on the ``start`` date, ``end`` date, ``version`` and ``model`` for now.
age_min=16,
age_is_adult=18,
age_max=64,
gender=[MALE, FEMALE])
gender=[MALE, FEMALE],
extended_by=None)
site_consents.register(consent_v1)
Expand Down Expand Up @@ -153,7 +179,8 @@ Add a second ``ConsentDefinition`` to ``your consents.py`` for version 2:
age_min=16,
age_is_adult=18,
age_max=64,
gender=[MALE, FEMALE])
gender=[MALE, FEMALE],
extended_by=None)
site_consents.register(consent_v1)
site_consents.register(consent_v2)
Expand Down Expand Up @@ -227,31 +254,49 @@ As the trial continues past 2016/10/15, there will three categories of subjects:
If the report date is after 2016/10/15, data entry for "Subjects who completed version 1 only"
will be blocked until the version 2 consent is submitted.
Extending followup for an existing version
==========================================
After a protocol amendment, you may need to extend the number of timepoints for participants who agree to the extension.
This is usually done by setting a new consent version with a start date that corresponds with the implementation date of
the protocol amendment. However, if the amendment is implemented where some agree and others do not, a new version may
not suffice.
For example, suppose at 30 months into a 36 month study, the study receives approval to extend the study
to 48 months. All participants will be given a choice to complete at 36 months post-enrollment, as originally agreed,
or extend to 48 months post-enrollment. The consent extension model captures their intention and the EDC will either
allow or disallow timepoints after 36 months accordingly.
This is managed by the ``ConsentExtensionDefinition`` class where the additional timepoints are
listed.
Features
========
.. code-block:: python
* base class for an informed consent document
* data for models that require consent cannot be add until the consent is added
* consents have a version number and validity period
* maximum number of consented subjects can be controlled.
* data collection is only allowed within the validity period of the consent per consented participant
* data for models that require consent are tagged with the consent version
"""timpoints 15-18 represent 39m, 42m, 45m, 48m"""
consent_v1_ext = ConsentDefinitionExtension(
"meta_consent.subjectconsentv1ext",
version="1.1",
start=datetime(2024, 12, 16, tzinfo=ZoneInfo("UTC")),
extends=consent_v1,
timepoints=[15, 16, 17, 18],
)
TODO
====
Important:
The schedule definition must be changed in code in the ``visit_schedule`` module to include all 18 timepoints (0m-48m).
The ``ConsentExtensionDefinition`` will remove ``Visit`` instances from the ``VisitCollection`` for the given subject
if necessary.
- link subject type to the consent model. e.g. maternal, infant, adult, etc.
- version at model field level (e.g. a new consent period adds additional questions to a form)
- allow a different subject's consent to cover for another, for example mother and infant.
Usage
=====
The ``ConsentExtensionDefinition`` links to a model to be completed by the participant.
* If the model instance does not exist, the additional timepoints are truncated from the participant's schedule.
* If the model instance exists but field ``agrees_to_extension`` != ``YES``, the additional timepoints are truncated from the participant's schedule.
* If the model instance exists and field ``agrees_to_extension`` == ``YES``, the additional timepoints are NOT truncated from the participant's schedule.
ModelForm
=========
Declare the ModelForm:
Expand Down
21 changes: 19 additions & 2 deletions consent_app/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.db import models
from django.db.models import PROTECT
from django.db.models import PROTECT, Manager
from edc_constants.choices import GENDER_UNDETERMINED
from edc_constants.constants import FEMALE
from edc_identifier.model_mixins import NonUniqueSubjectIdentifierModelMixin
Expand All @@ -18,7 +18,11 @@
VulnerabilityFieldsMixin,
)
from edc_consent.managers import ConsentObjectsByCdefManager, CurrentSiteByCdefManager
from edc_consent.model_mixins import ConsentModelMixin, RequiresConsentFieldsModelMixin
from edc_consent.model_mixins import (
ConsentExtensionModelMixin,
ConsentModelMixin,
RequiresConsentFieldsModelMixin,
)


class SubjectScreening(SiteModelMixin, BaseUuidModel):
Expand Down Expand Up @@ -78,6 +82,19 @@ class Meta:
proxy = True


class SubjectConsentV1Ext(ConsentExtensionModelMixin, SiteModelMixin, BaseUuidModel):

subject_consent = models.ForeignKey(SubjectConsentV1, on_delete=models.PROTECT)

on_site = CurrentSiteManager()
history = HistoricalRecords()
objects = Manager()

class Meta(ConsentExtensionModelMixin.Meta, BaseUuidModel.Meta):
verbose_name = "Subject Consent Extension V1.1"
verbose_name_plural = "Subject Consent Extension V1.1"


class SubjectConsentUgV1(SubjectConsent):
on_site = CurrentSiteByCdefManager()
objects = ConsentObjectsByCdefManager()
Expand Down
33 changes: 32 additions & 1 deletion consent_app/visit_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


def get_visit_schedule(
consent_definition: ConsentDefinition | list[ConsentDefinition],
consent_definition: ConsentDefinition | list[ConsentDefinition], extend: bool | None = None
) -> VisitSchedule:
crfs = CrfCollection(Crf(show_order=1, model="consent_app.crfone", required=True))

Expand All @@ -30,6 +30,34 @@ def get_visit_schedule(
facility_name="5-day-clinic",
)

visit1010 = Visit(
code="1010",
timepoint=1,
rbase=relativedelta(months=1),
rlower=relativedelta(days=0),
rupper=relativedelta(days=6),
requisitions=None,
crfs=crfs,
requisitions_unscheduled=None,
crfs_unscheduled=None,
allow_unscheduled=False,
facility_name="5-day-clinic",
)

visit1020 = Visit(
code="1020",
timepoint=2,
rbase=relativedelta(months=2),
rlower=relativedelta(days=0),
rupper=relativedelta(days=6),
requisitions=None,
crfs=crfs,
requisitions_unscheduled=None,
crfs_unscheduled=None,
allow_unscheduled=False,
facility_name="5-day-clinic",
)

schedule = Schedule(
name="schedule1",
onschedule_model="edc_appointment.onscheduleone",
Expand All @@ -46,6 +74,9 @@ def get_visit_schedule(
)

schedule.add_visit(visit)
if extend:
schedule.add_visit(visit1010)
schedule.add_visit(visit1020)

visit_schedule.add_schedule(schedule)
return visit_schedule
21 changes: 15 additions & 6 deletions edc_consent/consent_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from edc_model.models import BaseUuidModel
from edc_screening.model_mixins import EligibilityModelMixin, ScreeningModelMixin

from .consent_definition_extension import ConsentDefinitionExtension
from .stubs import ConsentLikeModel

class SubjectScreening(ScreeningModelMixin, EligibilityModelMixin, BaseUuidModel): ...
Expand All @@ -40,7 +41,7 @@ class ConsentDefinition:
end: datetime = field(default=ResearchProtocolConfig().study_close_datetime, compare=False)
version: str = field(default="1", compare=False)
updates: ConsentDefinition = field(default=None, compare=False)
end_extends_on_update: bool = field(default=False, compare=False)
extends: ConsentDefinition = field(default=None, compare=False)
screening_model: list[str] = field(default_factory=list, compare=False)
age_min: int = field(default=18, compare=False)
age_max: int = field(default=110, compare=False)
Expand All @@ -50,10 +51,12 @@ class ConsentDefinition:
country: str | None = field(default=None, compare=False)
validate_duration_overlap_by_model: bool | None = field(default=True, compare=False)
subject_type: str = field(default="subject", compare=False)
timepoints: list[int] | None = field(default_factory=list, compare=False)

name: str = field(init=False, compare=False)
# set updated_by when the cdef is registered, see site_consents
updated_by: ConsentDefinition = field(default=None, compare=False, init=False)
extended_by: ConsentDefinitionExtension = field(default=None, compare=False, init=False)
_model: str = field(init=False, compare=False)
sort_index: str = field(init=False)

Expand All @@ -68,12 +71,16 @@ def __post_init__(self):
raise ConsentDefinitionError(f"Invalid gender. Got {self.gender}.")
if not self.start.tzinfo:
raise ConsentDefinitionError(f"Naive datetime not allowed. Got {self.start}.")
elif str(self.start.tzinfo) != "UTC":
raise ConsentDefinitionError(f"Start date must be UTC. Got {self.start}.")
elif str(self.start.tzinfo).upper() != "UTC":
raise ConsentDefinitionError(
f"Start date must be UTC. Got {self.start} / {self.start.tzinfo}."
)
if not self.end.tzinfo:
raise ConsentDefinitionError(f"Naive datetime not allowed Got {self.end}.")
elif str(self.end.tzinfo) != "UTC":
raise ConsentDefinitionError(f"End date must be UTC. Got {self.end}.")
elif str(self.end.tzinfo).upper() != "UTC":
raise ConsentDefinitionError(
f"End date must be UTC. Got {self.end} / {self.start.tzinfo}."
)
self.check_date_within_study_period()

@property
Expand Down Expand Up @@ -128,7 +135,9 @@ def get_consent_for(
raise_if_not_consented = (
True if raise_if_not_consented is None else raise_if_not_consented
)
opts = dict(subject_identifier=subject_identifier, version=self.version)
opts: dict[str, str | int] = dict(
subject_identifier=subject_identifier, version=self.version
)
if site_id:
opts.update(site_id=site_id)
try:
Expand Down
Loading

0 comments on commit 9f89380

Please sign in to comment.