From 0045ace7a84572243fc1e3794f8898948cff2525 Mon Sep 17 00:00:00 2001 From: erikvw Date: Thu, 21 Mar 2024 16:52:05 -0500 Subject: [PATCH] remove 'updates_by' from ConsentDefinition init, change 'updates' to only accept a ConsentDefinition --- consent_app/models.py | 19 +++ edc_consent/consent_definition.py | 35 ++-- .../consent_version_model_mixin.py | 12 +- edc_consent/site_consents.py | 71 +++++--- edc_consent/system_checks.py | 104 +++++++----- edc_consent/tests/consent_test_utils.py | 12 +- edc_consent/tests/tests/test_actions.py | 15 +- edc_consent/tests/tests/test_consent.py | 160 ++++++++++++------ .../tests/tests/test_consent_definition.py | 24 +-- edc_consent/tests/tests/test_consent_form.py | 56 +++--- edc_consent/tests/tests/test_consent_model.py | 34 ++-- .../tests/tests/test_consent_update.py | 0 12 files changed, 343 insertions(+), 199 deletions(-) delete mode 100644 edc_consent/tests/tests/test_consent_update.py diff --git a/consent_app/models.py b/consent_app/models.py index 798b959..50d849d 100644 --- a/consent_app/models.py +++ b/consent_app/models.py @@ -17,6 +17,7 @@ ReviewFieldsMixin, VulnerabilityFieldsMixin, ) +from edc_consent.managers import ConsentObjectsByCdefManager, CurrentSiteByCdefManager from edc_consent.model_mixins import ConsentModelMixin, RequiresConsentFieldsModelMixin @@ -70,16 +71,34 @@ class Meta(ConsentModelMixin.Meta): class SubjectConsentV1(SubjectConsent): + on_site = CurrentSiteByCdefManager() + objects = ConsentObjectsByCdefManager() + + class Meta: + proxy = True + + +class SubjectConsentUgV1(SubjectConsent): + on_site = CurrentSiteByCdefManager() + objects = ConsentObjectsByCdefManager() + class Meta: proxy = True class SubjectConsentV2(SubjectConsent): + + on_site = CurrentSiteByCdefManager() + objects = ConsentObjectsByCdefManager() + class Meta: proxy = True class SubjectConsentV3(SubjectConsent): + on_site = CurrentSiteByCdefManager() + objects = ConsentObjectsByCdefManager() + class Meta: proxy = True diff --git a/edc_consent/consent_definition.py b/edc_consent/consent_definition.py index 564b803..aebbaa1 100644 --- a/edc_consent/consent_definition.py +++ b/edc_consent/consent_definition.py @@ -18,6 +18,7 @@ ConsentDefinitionValidityPeriodError, NotConsentedError, ) +from .managers import ConsentObjectsByCdefManager, CurrentSiteByCdefManager if TYPE_CHECKING: from edc_identifier.model_mixins import NonUniqueSubjectIdentifierModelMixin @@ -42,8 +43,8 @@ class ConsentDefinition: start: datetime = field(default=ResearchProtocolConfig().study_open_datetime, compare=True) end: datetime = field(default=ResearchProtocolConfig().study_close_datetime, compare=False) version: str = field(default="1", compare=False) - updates: tuple[ConsentDefinition, str] = field(default=tuple, compare=False) - updated_by: str = field(default=None, compare=False) + updates: ConsentDefinition = field(default=None, compare=False) + end_extends_on_update: bool = field(default=False, compare=False) screening_model: str = field(default=None, compare=False) age_min: int = field(default=18, compare=False) age_max: int = field(default=110, compare=False) @@ -53,10 +54,10 @@ 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) + name: str = field(init=False, compare=False) - update_cdef: ConsentDefinition = field(default=None, init=False, compare=False) - update_model: str = field(default=None, init=False, compare=False) - update_version: str = field(default=None, 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) _model: str = field(init=False, compare=False) sort_index: str = field(init=False) @@ -65,12 +66,6 @@ def __post_init__(self): self.name = f"{self.proxy_model}-{self.version}" self.sort_index = self.name self.gender = [MALE, FEMALE] if not self.gender else self.gender - try: - self.update_cdef, self.update_model = self.updates - except (ValueError, TypeError): - pass - else: - self.update_version = self.update_cdef.version if not self.screening_model: self.screening_model = get_subject_screening_model() if MALE not in self.gender and FEMALE not in self.gender: @@ -88,6 +83,18 @@ def model(self): raise ConsentDefinitionError( f"Model class must be a proxy. See {self.name}. Got {model_cls}" ) + elif not isinstance(model_cls.objects, (ConsentObjectsByCdefManager,)): + raise ConsentDefinitionError( + "Incorrect 'objects' model manager for consent model. " + f"Expected {ConsentObjectsByCdefManager}. See {self.name}. " + f"Got {model_cls.objects.__class__}" + ) + elif not isinstance(model_cls.on_site, (CurrentSiteByCdefManager,)): + raise ConsentDefinitionError( + "Incorrect 'on_site' model manager for consent model. " + f"Expected {CurrentSiteByCdefManager}. See {self.name}. " + f"Got {model_cls.objects.__class__}" + ) return self._model @model.setter @@ -138,10 +145,10 @@ def get_consent_for( try: consent_obj = self.model_cls.objects.get(**opts) except ObjectDoesNotExist: - if self.update_cdef: - opts.update(version=self.update_cdef.version) + if self.updates: + opts.update(version=self.updates.version) try: - consent_obj = self.update_cdef.model_cls.objects.get(**opts) + consent_obj = self.updates.model_cls.objects.get(**opts) except ObjectDoesNotExist: pass if not consent_obj and raise_if_not_consented: diff --git a/edc_consent/model_mixins/consent_version_model_mixin.py b/edc_consent/model_mixins/consent_version_model_mixin.py index f09e356..83a2980 100644 --- a/edc_consent/model_mixins/consent_version_model_mixin.py +++ b/edc_consent/model_mixins/consent_version_model_mixin.py @@ -62,11 +62,17 @@ def consent_definition(self): report_datetime=self.consent_datetime, site=site_sites.get(site.id), ) - if self._meta.label_lower not in [cdef.model, cdef.update_model]: + if self._meta.label_lower != cdef.model: raise ConsentDefinitionModelError( f"Incorrect model for consent_definition. This model cannot be used " - f"to create or update consent version '{cdef.version}'. Expected " - f"'{cdef.model}' or '{cdef.update_model}'. Got '{self._meta.label_lower}'." + f"to 'create' consent version '{cdef.version}'. Expected " + f"'{cdef.model}'. Got '{self._meta.label_lower}'." + ) + elif cdef.updates and self._meta.label_lower != cdef.updates.updated_by.model: + raise ConsentDefinitionModelError( + f"Incorrect model to update a consent. This model cannot be used " + f"to 'update' consent version '{cdef.version}'. Expected " + f"'{cdef.updates.updated_by.model}'. Got '{self._meta.label_lower}'." ) return cdef diff --git a/edc_consent/site_consents.py b/edc_consent/site_consents.py index e6aef3f..7228783 100644 --- a/edc_consent/site_consents.py +++ b/edc_consent/site_consents.py @@ -36,14 +36,55 @@ def __init__(self): self.registry = {} self.loaded = False - def register(self, cdef: ConsentDefinition) -> None: + def register( + self, cdef: ConsentDefinition, updated_by: ConsentDefinition | None = None + ) -> None: + cdef.updated_by = updated_by if cdef.name in self.registry: raise AlreadyRegistered(f"Consent definition already registered. Got {cdef.name}.") + self.validate_period_overlap_or_raise(cdef) + self.validate_updates_or_raise(cdef) + self.registry.update({cdef.name: cdef}) + self.loaded = True + + def get_registry_display(self): + cdefs = sorted(list(self.registry.values()), key=lambda x: x.version) + return "', '".join([cdef.display_name for cdef in cdefs]) + + def get(self, name) -> ConsentDefinition: + return self.registry.get(name) + + def all(self) -> list[ConsentDefinition]: + return sorted(list(self.registry.values()), key=lambda x: x.version) + + def validate_updates_or_raise(self, cdef: ConsentDefinition) -> None: + if cdef.updates: + if cdef.updates not in self.registry.values(): + raise ConsentDefinitionError( + f"Updates unregistered consent definition. See {cdef.name}. " + f"Got {cdef.updates.name}" + ) + elif cdef.updates and cdef.updates.updated_by is None: + raise ConsentDefinitionError( + f"Cdef mismatch with consent definition configured to update another. " + f"'{cdef.name}' is configured to update " + f"'{cdef.updates.name}' but '{cdef.updates.name}' " + f"updated_by is None. " + ) + elif cdef.updates and cdef.updates.updated_by != cdef: + raise ConsentDefinitionError( + f"Cdef mismatch with consent definition configured to update another. " + f"'{cdef.name}' is configured to update " + f"'{cdef.updates.name}' but '{cdef.updates.name}' " + f"updated_by='{cdef.updates.updated_by.name}' not '{cdef.name}'. " + ) + + def validate_period_overlap_or_raise(self, cdef: ConsentDefinition): for registered_cdef in self.registry.values(): if ( cdef and cdef.validate_duration_overlap_by_model - and registered_cdef.model == cdef.model + and registered_cdef.proxy_model == cdef.proxy_model ): if ( registered_cdef.start <= cdef.start <= registered_cdef.end @@ -54,32 +95,6 @@ def register(self, cdef: ConsentDefinition) -> None: f"definition. See already registered consent {registered_cdef.name}. " f"Got {cdef.name}." ) - if cdef.update_cdef: - if cdef.update_cdef not in self.registry.values(): - raise ConsentDefinitionError( - f"Updates unregistered consent definition. See {cdef.name}. " - f"Got {cdef.update_cdef.name}" - ) - elif cdef.update_cdef.updated_by and cdef.update_cdef.updated_by != cdef.version: - raise ConsentDefinitionError( - f"Version mismatch with consent definition configured to update another. " - f"'{cdef.name}' is configured to update " - f"'{cdef.update_cdef.name}' but '{cdef.update_cdef.name}' " - f"updated_by='{cdef.update_cdef.version}' not '{cdef.version}'. " - ) - - self.registry.update({cdef.name: cdef}) - self.loaded = True - - def get_registry_display(self): - cdefs = sorted(list(self.registry.values()), key=lambda x: x.version) - return "', '".join([cdef.display_name for cdef in cdefs]) - - def get(self, name) -> ConsentDefinition: - return self.registry.get(name) - - def all(self) -> list[ConsentDefinition]: - return sorted(list(self.registry.values()), key=lambda x: x.version) def get_consent_definition( self, diff --git a/edc_consent/system_checks.py b/edc_consent/system_checks.py index efc0d66..daac90c 100644 --- a/edc_consent/system_checks.py +++ b/edc_consent/system_checks.py @@ -33,33 +33,24 @@ def check_consents_proxy_models(app_configs, **kwargs) -> list[CheckMessage]: def check_consents_versions(app_configs, **kwargs) -> list[CheckMessage]: """Expect versions to be unique across `proxy_for` model""" errors = [] - cdefs1: list[ConsentDefinition] = [cdef for cdef in site_consents.registry.values()] used = [] - for cdef1 in cdefs1: - if cdef1 in used: + for cdef in [cdef for cdef in site_consents.registry.values()]: + if cdef in used: continue - versions = [] - for cdef in site_consents.registry.values(): - if ( - cdef.model_cls._meta.proxy - and cdef.model_cls._meta.proxy_for_model - == cdef1.model_cls._meta.proxy_for_model - ): - versions.append(cdef.version) - used.append(cdef) - if versions and len(set(versions)) != len(versions): - errors.append( - Warning( - "Consent definition version in use already for model. " - f"Got {cdef.version}.", - id="edc_consent.W001", - ) - ) + err, used = inspect_others_using_same_proxy_for_model_with_duplicate_versions( + cdef, used + ) + if err: + errors.append(err) return errors def check_consents_durations(app_configs, **kwargs) -> list[CheckMessage]: - """Durations may not overlap across `proxy_for` model""" + """Durations may not overlap across `proxy_for` model + + This check needs models to be ready otherwise we would add it + to site_consents.register. + """ errors = [] found = [] cdefs: list[ConsentDefinition] = [cdef for cdef in site_consents.registry.values()] @@ -67,24 +58,55 @@ def check_consents_durations(app_configs, **kwargs) -> list[CheckMessage]: for cdef2 in cdefs: if cdef1 == cdef2: continue - if ( - cdef1.model_cls._meta.proxy - and cdef1.model_cls._meta.proxy_for_model - == cdef1.model_cls._meta.proxy_for_model - ): - if ( - cdef1.start <= cdef2.start <= cdef1.end - or cdef1.start <= cdef2.end <= cdef1.end - ): - if sorted([cdef1, cdef2], key=lambda x: x.version) in found: - continue - else: - found.append(sorted([cdef1, cdef2], key=lambda x: x.version)) - errors.append( - Warning( - "Consent definition duration overlap. " - f"Got {cdef1.name} and {cdef2.name}.", - id="edc_consent.W002", - ) - ) + err, found = inspect_possible_overlap_in_validity_period(cdef1, cdef2, found) + if err: + errors.append(err) return errors + + +def inspect_possible_overlap_in_validity_period( + cdef1, cdef2, found +) -> tuple[Warning | None, list]: + """Durations between cdef1 and cdef2 may not overlap + if they are using proxies of the same model -- `proxy_for` model. + + This is just a warning as there may be valid cases to allow this. + For example, where consent definitions are customized by site. + """ + err = None + if ( + cdef1.model_cls._meta.proxy + and cdef1.model_cls._meta.proxy_for_model == cdef1.model_cls._meta.proxy_for_model + ): + if cdef1.start <= cdef2.start <= cdef1.end or cdef1.start <= cdef2.end <= cdef1.end: + if sorted([cdef1, cdef2], key=lambda x: x.version) in found: + pass + else: + found.append(sorted([cdef1, cdef2], key=lambda x: x.version)) + err = Warning( + "Consent definition duration overlap found for same proxy_for_model. " + f"Got {cdef1.name} and {cdef2.name}.", + id="edc_consent.W002", + ) + + return err, found + + +def inspect_others_using_same_proxy_for_model_with_duplicate_versions( + cdef1: ConsentDefinition, used: list[ConsentDefinition] +) -> tuple[Warning | None, list[ConsentDefinition]]: + err = None + versions = [] + opts1 = cdef1.model_cls._meta + for cdef2 in site_consents.registry.values(): + opts2 = cdef2.model_cls._meta + if opts2.proxy and opts2.proxy_for_model == opts1.proxy_for_model: + versions.append(cdef2.version) + used.append(cdef2) + if duplicates := [v for v in versions if versions.count(v) > 1]: + err = Warning( + f"Duplicate consent definition 'version' found for same proxy_for_model. " + f"Got '{opts1.proxy_for_model._meta.label_lower}' versions {set(duplicates)}.", + id="edc_consent.W001", + ) + return err, used diff --git a/edc_consent/tests/consent_test_utils.py b/edc_consent/tests/consent_test_utils.py index c93436e..67d0306 100644 --- a/edc_consent/tests/consent_test_utils.py +++ b/edc_consent/tests/consent_test_utils.py @@ -5,7 +5,6 @@ from edc_protocol.research_protocol_config import ResearchProtocolConfig from edc_consent.consent_definition import ConsentDefinition -from edc_consent.site_consents import site_consents def consent_definition_factory( @@ -13,27 +12,26 @@ def consent_definition_factory( start: datetime = None, end: datetime = None, gender: list[str] | None = None, - updates: tuple[str, str] = None, - updated_by: str | None = None, + updates: ConsentDefinition = None, version: str | None = None, age_min: int | None = None, age_max: int | None = None, age_is_adult: int | None = None, + **kwargs, ) -> ConsentDefinition: options = dict( start=start or ResearchProtocolConfig().study_open_datetime, end=end or ResearchProtocolConfig().study_close_datetime, gender=gender or ["M", "F"], updates=updates or None, - updated_by=updated_by or None, version=version or "1", age_min=age_min or 16, age_max=age_max or 64, age_is_adult=age_is_adult or 18, ) - model = model or "consent_app.subjectconsent" + options.update(**kwargs) + model = model or "consent_app.subjectconsentv1" consent_definition = ConsentDefinition(model, **options) - site_consents.register(consent_definition) return consent_definition @@ -43,7 +41,6 @@ def consent_factory(proxy_model=None, **kwargs): end=kwargs.get("end"), gender=kwargs.get("gender", ["M", "F"]), updates=kwargs.get("updates", None), - updated_by=kwargs.get("updated_by", None), version=kwargs.get("version", "1"), age_min=kwargs.get("age_min", 16), age_max=kwargs.get("age_max", 64), @@ -53,5 +50,4 @@ def consent_factory(proxy_model=None, **kwargs): ), ) consent_definition = ConsentDefinition(proxy_model, **options) - site_consents.register(consent_definition) return consent_definition diff --git a/edc_consent/tests/tests/test_actions.py b/edc_consent/tests/tests/test_actions.py index a949b3f..29252af 100644 --- a/edc_consent/tests/tests/test_actions.py +++ b/edc_consent/tests/tests/test_actions.py @@ -10,7 +10,7 @@ from faker import Faker from model_bakery import baker -from consent_app.models import SubjectConsent +from consent_app.models import SubjectConsentV1 from edc_consent.actions import unverify_consent, verify_consent from edc_consent.site_consents import site_consents @@ -31,9 +31,10 @@ def setUp(self): site_consents.registry = {} self.study_open_datetime = ResearchProtocolConfig().study_open_datetime self.study_close_datetime = ResearchProtocolConfig().study_close_datetime - consent_definition_factory( + cdef = consent_definition_factory( start=self.study_open_datetime, end=self.study_close_datetime ) + site_consents.register(cdef) self.request = HttpRequest() user = User.objects.create(username="erikvw") self.request.user = user @@ -42,23 +43,23 @@ def setUp(self): last_name = fake.last_name() initials = first_name[0] + choice(string.ascii_uppercase) + last_name[0] baker.make_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", consent_datetime=self.study_open_datetime + relativedelta(days=1), initials=initials.upper(), ) def test_verify(self): - for consent_obj in SubjectConsent.objects.all(): + for consent_obj in SubjectConsentV1.objects.all(): verify_consent(request=self.request, consent_obj=consent_obj) - for consent_obj in SubjectConsent.objects.all(): + for consent_obj in SubjectConsentV1.objects.all(): self.assertTrue(consent_obj.is_verified) self.assertEqual(consent_obj.verified_by, "erikvw") self.assertIsNotNone(consent_obj.is_verified_datetime) def test_unverify(self): - for consent_obj in SubjectConsent.objects.all(): + for consent_obj in SubjectConsentV1.objects.all(): unverify_consent(consent_obj=consent_obj) - for consent_obj in SubjectConsent.objects.all(): + for consent_obj in SubjectConsentV1.objects.all(): self.assertFalse(consent_obj.is_verified) self.assertIsNone(consent_obj.verified_by) self.assertIsNone(consent_obj.is_verified_datetime) diff --git a/edc_consent/tests/tests/test_consent.py b/edc_consent/tests/tests/test_consent.py index fbb7f47..2a5e74b 100644 --- a/edc_consent/tests/tests/test_consent.py +++ b/edc_consent/tests/tests/test_consent.py @@ -20,6 +20,7 @@ ) from edc_consent.site_consents import site_consents +from ...consent_definition import ConsentDefinition from ..consent_test_utils import consent_definition_factory @@ -47,7 +48,7 @@ def test_raises_error_if_no_consent(self): self.assertRaises( SiteConsentError, baker.make_recipe, - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", subject_identifier=subject_identifier, consent_datetime=self.study_open_datetime, ) @@ -60,6 +61,8 @@ def test_raises_error_if_no_consent2(self): start=self.study_open_datetime, end=self.study_close_datetime, ) + site_consents.register(consent_definition) + visit_schedule = get_visit_schedule(consent_definition) schedule = visit_schedule.schedules.get("schedule1") site_visit_schedules._registry = {} @@ -78,16 +81,18 @@ def test_allows_create_if_consent(self): """Asserts can create a consent model instance if a valid consent. """ - consent_definition = consent_definition_factory( + cdef = consent_definition = consent_definition_factory( start=self.study_open_datetime, end=self.study_close_datetime, ) + site_consents.register(cdef) + visit_schedule = get_visit_schedule(consent_definition) schedule = visit_schedule.schedules.get("schedule1") site_visit_schedules._registry = {} site_visit_schedules.register(get_visit_schedule(consent_definition)) subject_consent = baker.make_recipe( - "consent_app.subjectconsent", + cdef.model, subject_identifier=self.subject_identifier, consent_datetime=self.study_open_datetime, dob=self.study_open_datetime - relativedelta(years=25), @@ -108,40 +113,46 @@ def test_allows_create_if_consent(self): self.fail("NotConsentedError unexpectedly raised") def test_cannot_create_consent_without_consent_by_datetime(self): - consent_definition_factory( + cdef = consent_definition_factory( start=self.study_open_datetime + relativedelta(days=5), end=self.study_close_datetime, version="1", ) + site_consents.register(cdef) + self.assertRaises( ConsentDefinitionDoesNotExist, baker.make_recipe, - "consent_app.subjectconsent", + cdef.model, dob=self.study_open_datetime - relativedelta(years=25), consent_datetime=self.study_open_datetime, ) def test_consent_gets_version(self): - consent_definition_factory( + cdef = consent_definition_factory( start=self.study_open_datetime, end=self.study_close_datetime, version="1.0", ) + site_consents.register(cdef) + consent = baker.make_recipe( - "consent_app.subjectconsent", + cdef.model, consent_datetime=self.study_open_datetime, dob=self.study_open_datetime - relativedelta(years=25), ) self.assertEqual(consent.version, "1.0") def test_model_gets_version(self): - consent_definition = consent_definition_factory( + cdef = consent_definition = consent_definition_factory( start=self.study_open_datetime, end=self.study_close_datetime, version="1.0", ) + site_consents.register(cdef) + subject_consent = baker.make_recipe( - "consent_app.subjectconsent", + cdef.model, subject_identifier=self.subject_identifier, consent_datetime=self.study_open_datetime, dob=self.study_open_datetime - relativedelta(years=25), @@ -167,22 +178,24 @@ def test_model_gets_version(self): self.assertEqual(crf_one.consent_version, "1.0") def test_model_consent_version_no_change(self): - consent_definition = consent_definition_factory( + cdef = consent_definition_factory( start=self.study_open_datetime, end=self.study_close_datetime, version="1.2", ) + site_consents.register(cdef) + baker.make_recipe( - "consent_app.subjectconsent", + cdef.model, subject_identifier=self.subject_identifier, consent_datetime=self.study_open_datetime, dob=self.study_open_datetime - relativedelta(years=25), ) - visit_schedule = get_visit_schedule(consent_definition) + visit_schedule = get_visit_schedule(cdef) schedule = visit_schedule.schedules.get("schedule1") site_visit_schedules._registry = {} - site_visit_schedules.register(get_visit_schedule(consent_definition)) + site_visit_schedules.register(get_visit_schedule(cdef)) subject_visit = SubjectVisit.objects.create( subject_identifier=self.subject_identifier, @@ -199,21 +212,51 @@ def test_model_consent_version_no_change(self): crf_one.save() self.assertEqual(crf_one.consent_version, "1.2") - def test_model_consent_version_changes_with_report_datetime(self): - consent_definition10 = consent_definition_factory( + def test_multiple_consents_returned(self): + cdef10 = consent_definition_factory( start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), version="1.0", ) - consent_definition11 = consent_definition_factory( + site_consents.register(cdef10) + + cdef11 = consent_definition_factory( start=self.study_open_datetime + timedelta(days=51), end=self.study_open_datetime + timedelta(days=100), version="1.1", ) + site_consents.register(cdef11) + consent_datetime = self.study_open_datetime + timedelta(days=10) + self.assertRaises( + SiteConsentError, + baker.make_recipe, + cdef10.model, + subject_identifier=self.subject_identifier, + consent_datetime=consent_datetime, + dob=self.study_open_datetime - relativedelta(years=25), + ) + + def test_model_consent_version_changes_with_report_datetime(self): + cdef10 = consent_definition_factory( + start=self.study_open_datetime, + end=self.study_open_datetime + timedelta(days=50), + version="1.0", + ) + site_consents.register(cdef10) + + cdef20 = consent_definition_factory( + model="consent_app.subjectconsentv2", + start=self.study_open_datetime + timedelta(days=51), + end=self.study_open_datetime + timedelta(days=100), + version="2.0", + ) + site_consents.register(cdef20) + + consent_datetime = self.study_open_datetime + timedelta(days=10) subject_consent = baker.make_recipe( - "consent_app.subjectconsent", + cdef10.model, subject_identifier=self.subject_identifier, consent_datetime=consent_datetime, dob=self.study_open_datetime - relativedelta(years=25), @@ -223,7 +266,7 @@ def test_model_consent_version_changes_with_report_datetime(self): self.assertEqual(subject_consent.subject_identifier, self.subject_identifier) self.assertEqual(subject_consent.consent_datetime, consent_datetime) - visit_schedule = get_visit_schedule([consent_definition10, consent_definition11]) + visit_schedule = get_visit_schedule([cdef10, cdef20]) schedule = visit_schedule.schedules.get("schedule1") site_visit_schedules._registry = {} site_visit_schedules.register(visit_schedule) @@ -242,8 +285,8 @@ def test_model_consent_version_changes_with_report_datetime(self): ) self.assertEqual(crf_one.consent_version, "1.0") consent_datetime = self.study_open_datetime + timedelta(days=60) - subject_consent = baker.make_recipe( - "consent_app.subjectconsent", + baker.make_recipe( + cdef20.model, subject_identifier=self.subject_identifier, consent_datetime=consent_datetime, dob=self.study_open_datetime - relativedelta(years=25), @@ -256,53 +299,60 @@ def test_model_consent_version_changes_with_report_datetime(self): report_datetime=consent_datetime, ) - # Mcrf_one.save() - self.assertEqual(crf_one.consent_version, "1.1") + self.assertEqual(crf_one.consent_version, "2.0") def test_consent_periods_cannot_overlap(self): - consent_definition_factory( + cdef1 = consent_definition_factory( start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), version="1.0", ) - self.assertRaises( - ConsentDefinitionError, - consent_definition_factory, - start=self.study_open_datetime + timedelta(days=25), - end=self.study_open_datetime + timedelta(days=100), + site_consents.register(cdef1) + cdef2 = consent_definition_factory( + start=self.study_open_datetime, + end=self.study_open_datetime + timedelta(days=50), version="1.1", ) + self.assertRaises(ConsentDefinitionError, site_consents.register, cdef2) def test_consent_periods_cannot_overlap2(self): - consent_definition_factory( - model="consent_app.subjectconsent", + cdef1 = consent_definition_factory( + model="consent_app.subjectconsentv1", start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), version="1.0", ) - self.assertRaises( - ConsentDefinitionError, - consent_definition_factory, - model="consent_app.subjectconsent", + + cdef2 = consent_definition_factory( + model="consent_app.subjectconsentv2", start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), - version="1.1", + version="2.0", + updates=cdef1, + validate_duration_overlap_by_model=True, ) + site_consents.register(cdef1, updated_by=cdef2) + site_consents.register(cdef2) + def test_consent_periods_can_overlap_if_different_model(self): - consent_definition_factory( - model="consent_app.subjectconsent", + cdef1 = consent_definition_factory( + model="consent_app.subjectconsentv1", start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), version="1.0", ) + + cdef2 = consent_definition_factory( + model="consent_app.subjectconsentv2", + start=self.study_open_datetime, + end=self.study_open_datetime + timedelta(days=50), + version="1.0", + ) + + site_consents.register(cdef1) try: - consent_definition_factory( - model="consent_app.subjectconsent2", - start=self.study_open_datetime, - end=self.study_open_datetime + timedelta(days=50), - version="1.0", - ) + site_consents.register(cdef2) except ConsentDefinitionError: self.fail("ConsentPeriodOverlapError unexpectedly raised") @@ -324,6 +374,7 @@ def test_consent_definition_naive_datetime_start(self): """ d = self.study_open_datetime dte = datetime(d.year, d.month, d.day, 0, 0, 0, 0) + self.assertRaises( ConsentDefinitionError, consent_definition_factory, @@ -349,27 +400,40 @@ def test_consent_definition_naive_datetime_end(self): @skip def test_consent_update_needs_previous_version(self): """Asserts that a consent type updates a previous consent.""" - consent_definition_factory( + cdef1 = consent_definition_factory( start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), version="1.0", ) - # specify updates version that does not exist, raises + + # specify updates version that is not registered + self.assertRaises( + ConsentDefinitionError, + consent_definition_factory, + start=self.study_open_datetime + timedelta(days=51), + end=self.study_open_datetime + timedelta(days=100), + version="1.1", + updates=cdef1, + ) + # specify updates garbage self.assertRaises( ConsentDefinitionError, consent_definition_factory, start=self.study_open_datetime + timedelta(days=51), end=self.study_open_datetime + timedelta(days=100), version="1.1", - update_versions=["1.2"], + updates=ConsentDefinition, ) + # specify updates version that exists, ok - consent_definition_factory( + cdef2 = consent_definition_factory( start=self.study_open_datetime + timedelta(days=51), end=self.study_open_datetime + timedelta(days=100), version="1.1", - update_versions=["1.0"], + updates=cdef1, ) + site_consents.register(cdef1, updated_by=cdef2) + site_consents.register(cdef2) @skip def test_consent_model_needs_previous_version(self): diff --git a/edc_consent/tests/tests/test_consent_definition.py b/edc_consent/tests/tests/test_consent_definition.py index 8b6998f..b79cd0e 100644 --- a/edc_consent/tests/tests/test_consent_definition.py +++ b/edc_consent/tests/tests/test_consent_definition.py @@ -34,22 +34,22 @@ def default_options(self, **kwargs): return options def test_ok(self): - ConsentDefinition("consent_app.subjectconsent", **self.default_options()) + ConsentDefinition("consent_app.subjectconsentv1", **self.default_options()) def test_cdef_name(self): - cdef1 = ConsentDefinition("consent_app.subjectconsent", **self.default_options()) - self.assertEqual(cdef1.name, "consent_app.subjectconsent-1") + cdef1 = ConsentDefinition("consent_app.subjectconsentv1", **self.default_options()) + self.assertEqual(cdef1.name, "consent_app.subjectconsentv1-1") site_consents.register(cdef1) - site_consents.get_consent_definition("consent_app.subjectconsent") - site_consents.get_consent_definition(model="consent_app.subjectconsent") + site_consents.get_consent_definition("consent_app.subjectconsentv1") + site_consents.get_consent_definition(model="consent_app.subjectconsentv1") site_consents.get_consent_definition(version="1") # add country site_consents.registry = {} cdef1 = ConsentDefinition( - "consent_app.subjectconsentug", **self.default_options(country="uganda") + "consent_app.subjectconsentugv1", **self.default_options(country="uganda") ) - self.assertEqual(cdef1.name, "consent_app.subjectconsentug-1") + self.assertEqual(cdef1.name, "consent_app.subjectconsentugv1-1") site_consents.register(cdef1) cdef2 = site_consents.get_consent_definition(country="uganda") self.assertEqual(cdef1, cdef2) @@ -57,7 +57,7 @@ def test_cdef_name(self): def test_with_country(self): site_consents.registry = {} cdef1 = ConsentDefinition( - "consent_app.subjectconsent", country="uganda", **self.default_options() + "consent_app.subjectconsentv1", country="uganda", **self.default_options() ) site_consents.register(cdef1) cdef2 = site_consents.get_consent_definition(country="uganda") @@ -66,10 +66,10 @@ def test_with_country(self): def test_with_country_raises_on_potential_duplicate(self): site_consents.registry = {} cdef1 = ConsentDefinition( - "consent_app.subjectconsent", country="uganda", **self.default_options() + "consent_app.subjectconsentv1", country="uganda", **self.default_options() ) cdef2 = ConsentDefinition( - "consent_app.subjectconsentug", country="uganda", **self.default_options() + "consent_app.subjectconsentugv1", country="uganda", **self.default_options() ) site_consents.register(cdef1) site_consents.register(cdef2) @@ -80,10 +80,10 @@ def test_with_country_raises_on_potential_duplicate(self): def test_duplicate_version(self): site_consents.registry = {} cdef1 = ConsentDefinition( - "consent_app.subjectconsent", country="uganda", **self.default_options() + "consent_app.subjectconsentv1", country="uganda", **self.default_options() ) cdef2 = ConsentDefinition( - "consent_app.subjectconsentug", country="uganda", **self.default_options() + "consent_app.subjectconsentugv1", country="uganda", **self.default_options() ) site_consents.register(cdef1) site_consents.register(cdef2) diff --git a/edc_consent/tests/tests/test_consent_form.py b/edc_consent/tests/tests/test_consent_form.py index 0fc0daf..ad612c5 100644 --- a/edc_consent/tests/tests/test_consent_form.py +++ b/edc_consent/tests/tests/test_consent_form.py @@ -14,7 +14,7 @@ from faker import Faker from model_bakery import baker -from consent_app.models import SubjectConsent, SubjectScreening +from consent_app.models import SubjectConsent, SubjectConsentV1, SubjectScreening from edc_consent.consent_definition import ConsentDefinition from edc_consent.form_validators import SubjectConsentFormValidatorMixin from edc_consent.modelform_mixins import ConsentModelFormMixin @@ -38,7 +38,7 @@ class SubjectConsentForm(ConsentModelFormMixin, FormValidatorMixin, forms.ModelF ) class Meta: - model = SubjectConsent + model = SubjectConsentV1 fields = "__all__" @@ -54,26 +54,30 @@ def setUp(self): self.study_open_datetime = ResearchProtocolConfig().study_open_datetime self.study_close_datetime = ResearchProtocolConfig().study_close_datetime - self.convent_v1 = self.consent_factory( + self.consent_v1 = self.consent_factory( start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), - model="consent_app.subjectconsent", + model="consent_app.subjectconsentv1", version="1.0", ) - self.convent_v2 = self.consent_factory( + self.consent_v2 = self.consent_factory( start=self.study_open_datetime + timedelta(days=51), end=self.study_open_datetime + timedelta(days=100), model="consent_app.subjectconsentv2", version="2.0", ) - self.convent_v3 = self.consent_factory( + self.consent_v3 = self.consent_factory( start=self.study_open_datetime + timedelta(days=101), end=self.study_open_datetime + timedelta(days=150), version="3.0", model="consent_app.subjectconsentv3", - updates=(self.convent_v2, "consent_app.subjectconsentupdatetov3"), + updates=self.consent_v2, ) + site_consents.register(self.consent_v1) + site_consents.register(self.consent_v2, updated_by=self.consent_v3) + site_consents.register(self.consent_v3) + self.dob = self.study_open_datetime - relativedelta(years=25) @staticmethod @@ -88,9 +92,8 @@ def consent_factory(**kwargs): age_max=kwargs.get("age_max", 64), age_is_adult=kwargs.get("age_is_adult", 18), ) - model = kwargs.get("model", "consent_app.subjectconsent") + model = kwargs.get("model", "consent_app.subjectconsentv1") consent_definition = ConsentDefinition(model, **options) - site_consents.register(consent_definition) return consent_definition def cleaned_data(self, **kwargs): @@ -159,7 +162,7 @@ def prepare_subject_consent( eligibility_datetime=consent_datetime, ) subject_consent = baker.prepare_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", dob=dob, consent_datetime=consent_datetime, first_name=first_name or "XXXXXX", @@ -190,7 +193,7 @@ def test_base_form_is_valid(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) self.assertTrue(form.is_valid()) @@ -209,7 +212,7 @@ def test_base_form_catches_consent_datetime_before_study_open(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertEqual(form._errors, {}) @@ -223,7 +226,7 @@ def test_base_form_catches_consent_datetime_before_study_open(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("consent_datetime", form._errors) @@ -245,7 +248,7 @@ def test_base_form_identity_mismatch(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("identity", form._errors) @@ -280,7 +283,7 @@ def test_base_form_identity_dupl(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("identity", form._errors) @@ -304,7 +307,7 @@ def test_base_form_guardian_and_dob1(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("guardian_name", form._errors) @@ -328,7 +331,7 @@ def test_base_form_guardian_and_dob2(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertEqual({}, form._errors) @@ -354,7 +357,7 @@ def test_base_form_guardian_and_dob4(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("guardian_name", form._errors) @@ -376,7 +379,7 @@ def test_base_form_catches_dob_lower(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("dob", form._errors) @@ -398,14 +401,14 @@ def test_base_form_catches_dob_upper(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("dob", form._errors) def test_base_form_catches_gender_of_consent(self): site_consents.registry = {} - self.consent_factory( + cdef = self.consent_factory( start=self.study_open_datetime, end=self.study_open_datetime + timedelta(days=50), version="1.0", @@ -413,13 +416,14 @@ def test_base_form_catches_gender_of_consent(self): first_name="ERIK", last_name="THEPLEEB", ) + site_consents.register(cdef) subject_consent = self.prepare_subject_consent(gender=MALE) opts = SubjectConsentForm._meta data = model_to_dict(subject_consent, opts.fields, opts.exclude) form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertEqual({}, form._errors) @@ -432,7 +436,7 @@ def test_base_form_catches_gender_of_consent(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertIn("gender", form._errors) @@ -462,7 +466,7 @@ def test_base_form_catches_is_literate_and_witness(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertEqual({}, form._errors) @@ -476,7 +480,7 @@ def test_raises_on_duplicate_identity1(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(site=subject_consent.site), + instance=opts.model(site=subject_consent.site), ) form.is_valid() self.assertEqual({}, form._errors) @@ -504,7 +508,7 @@ def test_current_site(self): form = SubjectConsentForm( data=data, initial=dict(screening_identifier=data.get("screening_identifier")), - instance=SubjectConsent(), + instance=SubjectConsentV1(), ) form.is_valid() self.assertEqual({}, form._errors) diff --git a/edc_consent/tests/tests/test_consent_model.py b/edc_consent/tests/tests/test_consent_model.py index 490c8c3..54cd381 100644 --- a/edc_consent/tests/tests/test_consent_model.py +++ b/edc_consent/tests/tests/test_consent_model.py @@ -43,18 +43,19 @@ def setUp(self): start=self.study_open_datetime + timedelta(days=51), end=self.study_open_datetime + timedelta(days=100), version="2.0", - updated_by="3.0", ) self.consent_v3 = consent_factory( proxy_model="consent_app.subjectconsentv3", start=self.study_open_datetime + timedelta(days=101), end=self.study_open_datetime + timedelta(days=150), version="3.0", - updates=(self.consent_v2, "consent_app.subjectconsentupdatev3"), + updates=self.consent_v2, ) + site_consents.register(self.consent_v1) + site_consents.register(self.consent_v2, updated_by=self.consent_v3) + site_consents.register(self.consent_v3) self.dob = self.study_open_datetime - relativedelta(years=25) - @tag("1") def test_encryption(self): subject_consent = baker.make_recipe( "consent_app.subjectconsentv1", @@ -101,7 +102,7 @@ def test_subject_has_current_consent(self): ) self.assertEqual(subject_consent.version, "1.0") baker.make_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv2", subject_identifier=subject_identifier, identity=identity, confirm_identity=identity, @@ -109,7 +110,7 @@ def test_subject_has_current_consent(self): dob=get_utcnow() - relativedelta(years=25), ) cdef = site_consents.get_consent_definition( - model="consent_app.subjectconsent", version="2.0" + model="consent_app.subjectconsentv2", version="2.0" ) subject_consent = cdef.get_consent_for( subject_identifier="123456789", @@ -121,7 +122,7 @@ def test_model_updates(self): subject_identifier = "123456789" identity = "987654321" consent = baker.make_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", subject_identifier=subject_identifier, identity=identity, confirm_identity=identity, @@ -130,7 +131,7 @@ def test_model_updates(self): ) self.assertEqual(consent.version, "1.0") consent = baker.make_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv2", subject_identifier=subject_identifier, identity=identity, confirm_identity=identity, @@ -152,7 +153,7 @@ def test_model_updates2(self): subject_identifier = "123456789" identity = "987654321" consent = baker.make_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", subject_identifier=subject_identifier, identity=identity, confirm_identity=identity, @@ -223,6 +224,8 @@ def test_model_updates_from_v1_to_v2(self): @tag("1") def test_v3_extends_v2_end_date_up_to_v3_consent_datetime(self): + # TODO: is this a valid test? How does it fill in data from + # the previous consent? traveller = time_machine.travel(self.study_open_datetime) traveller.start() subject_identifier = "123456789" @@ -339,10 +342,15 @@ def test_raise_with_date_past_any_consent_period(self): traveller.start() subject_identifier = "123456789" identity = "987654321" + self.assertRaises( + ConsentDefinitionDoesNotExist, + site_consents.get_consent_definition, + report_datetime=get_utcnow(), + ) self.assertRaises( ConsentDefinitionDoesNotExist, baker.make_recipe, - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", subject_identifier=subject_identifier, identity=identity, confirm_identity=identity, @@ -355,10 +363,12 @@ def test_raise_with_incorrect_model_for_cdef(self): traveller.start() subject_identifier = "123456789" identity = "987654321" + cdef = site_consents.get_consent_definition(report_datetime=get_utcnow()) + self.assertEqual(cdef.model, "consent_app.subjectconsentv3") self.assertRaises( ConsentDefinitionModelError, baker.make_recipe, - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", subject_identifier=subject_identifier, identity=identity, confirm_identity=identity, @@ -368,7 +378,7 @@ def test_raise_with_incorrect_model_for_cdef(self): def test_model_str_repr_etc(self): obj = baker.make_recipe( - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", screening_identifier="ABCDEF", subject_identifier="12345", consent_datetime=self.study_open_datetime + relativedelta(days=1), @@ -384,7 +394,7 @@ def test_checks_identity_fields_match_or_raises(self): self.assertRaises( IdentityFieldsMixinError, baker.make_recipe, - "consent_app.subjectconsent", + "consent_app.subjectconsentv1", subject_identifier="12345", consent_datetime=self.study_open_datetime + relativedelta(days=1), identity="123456789", diff --git a/edc_consent/tests/tests/test_consent_update.py b/edc_consent/tests/tests/test_consent_update.py deleted file mode 100644 index e69de29..0000000