Skip to content

Commit

Permalink
Merge branch 'release/0.3.77' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
erikvw committed Mar 27, 2024
2 parents 18db6b9 + 099f40e commit f43eb1a
Show file tree
Hide file tree
Showing 34 changed files with 1,148 additions and 519 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ repos:
- id: check-executables-have-shebangs
- id: check-merge-conflict
- id: debug-statements
- id: detect-private-key
# - id: detect-private-key

- repo: https://github.com/adrienverge/yamllint
rev: v1.34.0
Expand Down
91 changes: 40 additions & 51 deletions consent_app/baker_recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,47 @@
from faker import Faker
from model_bakery.recipe import Recipe, seq

from .models import SubjectConsent, SubjectConsentV3
from .models import (
SubjectConsent,
SubjectConsentV1,
SubjectConsentV2,
SubjectConsentV3,
SubjectConsentV4,
)

fake = Faker()

subjectconsent = Recipe(
SubjectConsent,
consent_datetime=get_utcnow,
dob=get_utcnow() - relativedelta(years=25),
first_name=fake.first_name,
last_name=fake.last_name,
# note, passes for model but won't pass validation in modelform clean()
initials="AA",
gender=MALE,
# will raise IntegrityError if multiple made without _quantity
identity=seq("12315678"),
# will raise IntegrityError if multiple made without _quantity
confirm_identity=seq("12315678"),
identity_type="passport",
is_dob_estimated="-",
language="en",
is_literate=YES,
is_incarcerated=NO,
study_questions=YES,
consent_reviewed=YES,
consent_copy=YES,
assessment_score=YES,
consent_signature=YES,
site=Site.objects.get_current(),
)

subjectconsentv3 = Recipe(
SubjectConsentV3,
consent_datetime=get_utcnow,
dob=get_utcnow() - relativedelta(years=25),
first_name=fake.first_name,
last_name=fake.last_name,
# note, passes for model but won't pass validation in modelform clean()
initials="AA",
gender=MALE,
# will raise IntegrityError if multiple made without _quantity
identity=seq("12315678"),
# will raise IntegrityError if multiple made without _quantity
confirm_identity=seq("12315678"),
identity_type="passport",
is_dob_estimated="-",
language="en",
is_literate=YES,
is_incarcerated=NO,
study_questions=YES,
consent_reviewed=YES,
consent_copy=YES,
assessment_score=YES,
consent_signature=YES,
site=Site.objects.get_current(),
)
def get_opts():
return dict(
consent_datetime=get_utcnow,
dob=get_utcnow() - relativedelta(years=25),
first_name=fake.first_name,
last_name=fake.last_name,
# note, passes for model but won't pass validation in modelform clean()
initials="AA",
gender=MALE,
# will raise IntegrityError if multiple made without _quantity
identity=seq("12315678"),
# will raise IntegrityError if multiple made without _quantity
confirm_identity=seq("12315678"),
identity_type="passport",
is_dob_estimated="-",
language="en",
is_literate=YES,
is_incarcerated=NO,
study_questions=YES,
consent_reviewed=YES,
consent_copy=YES,
assessment_score=YES,
consent_signature=YES,
site=Site.objects.get_current(),
)


subjectconsent = Recipe(SubjectConsent, **get_opts())

subjectconsentv1 = Recipe(SubjectConsentV1, **get_opts())
subjectconsentv2 = Recipe(SubjectConsentV2, **get_opts())
subjectconsentv3 = Recipe(SubjectConsentV3, **get_opts())
subjectconsentv4 = Recipe(SubjectConsentV4, **get_opts())
26 changes: 26 additions & 0 deletions consent_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ReviewFieldsMixin,
VulnerabilityFieldsMixin,
)
from edc_consent.managers import ConsentObjectsByCdefManager, CurrentSiteByCdefManager
from edc_consent.model_mixins import ConsentModelMixin, RequiresConsentFieldsModelMixin


Expand Down Expand Up @@ -70,16 +71,41 @@ 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


class SubjectConsentV4(SubjectConsent):
on_site = CurrentSiteByCdefManager()
objects = ConsentObjectsByCdefManager()

class Meta:
proxy = True

Expand Down
19 changes: 0 additions & 19 deletions edc_consent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1 @@
from importlib.metadata import version

__version__ = version("edc_consent")
__all__ = [
"site_consents",
"ConsentDefinitionDoesNotExist",
"ConsentDefinitionError",
"ConsentError",
"ConsentVersionSequenceError",
"NotConsentedError",
]

from .exceptions import (
ConsentDefinitionDoesNotExist,
ConsentDefinitionError,
ConsentError,
ConsentVersionSequenceError,
NotConsentedError,
)
from .site_consents import site_consents
2 changes: 1 addition & 1 deletion edc_consent/auth_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# error will be broadcast in system checks
pass
else:
models = [cdef.model for cdef in cdefs]
models = [cdef.proxy_model for cdef in cdefs]
models = list(set(models))
for model in models:
for action in ["view_", "add_", "change_", "delete_", "view_historical"]:
Expand Down
83 changes: 43 additions & 40 deletions edc_consent/consent_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
from edc_screening.utils import get_subject_screening_model
from edc_sites import site_sites
from edc_utils import floor_secs, formatted_date, formatted_datetime
from edc_utils.date import ceil_datetime, floor_datetime, to_local, to_utc
from edc_utils.date import ceil_datetime, floor_datetime, to_local

from .exceptions import (
ConsentDefinitionError,
ConsentDefinitionValidityPeriodError,
NotConsentedError,
)
from .managers import ConsentObjectsByCdefManager, CurrentSiteByCdefManager

if TYPE_CHECKING:
from edc_identifier.model_mixins import NonUniqueSubjectIdentifierModelMixin
Expand All @@ -37,13 +38,13 @@ class ConsentDefinition:
of a consent.
"""

model: str = field(compare=False)
proxy_model: str = field(compare=False)
_ = KW_ONLY
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)
Expand All @@ -53,22 +54,18 @@ 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)

def __post_init__(self):
self.name = f"{self.model}-{self.version}"
self.model = self.proxy_model
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:
Expand All @@ -79,6 +76,31 @@ def __post_init__(self):
raise ConsentDefinitionError(f"Naive datetime not allowed Got {self.end}.")
self.check_date_within_study_period()

@property
def model(self):
model_cls = django_apps.get_model(self._model)
if not model_cls._meta.proxy:
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
def model(self, value):
self._model = value

@property
def sites(self):
if not site_sites.loaded:
Expand All @@ -98,42 +120,23 @@ def sites(self):
def get_consent_for(
self,
subject_identifier: str = None,
report_datetime: datetime | None = None,
site_id: int | None = None,
raise_if_not_consented: bool | None = None,
) -> ConsentLikeModel | None:
"""Returns a subject consent using this consent_definition's
model_cls and version.
If it does not exist and this consent_definition updates a
previous (update_cdef), will try again with the update_cdef's
model_cls and version.
Finally, if the subject cosent does not exist raises a
NotConsentedError.
"""
consent_obj = None
raise_if_not_consented = (
True if raise_if_not_consented is None else raise_if_not_consented
)
opts: dict[str, str | datetime] = dict(
subject_identifier=subject_identifier, version=self.version
)
if report_datetime:
opts.update(consent_datetime__lte=to_utc(report_datetime))
opts = dict(subject_identifier=subject_identifier, version=self.version)
if site_id:
opts.update(site_id=site_id)
try:
consent_obj = self.model_cls.objects.get(**opts)
except ObjectDoesNotExist:
if self.update_cdef:
opts.update(version=self.update_cdef.version)
try:
consent_obj = self.update_cdef.model_cls.objects.get(**opts)
except ObjectDoesNotExist:
pass
consent_obj = None
if not consent_obj and raise_if_not_consented:
dte = formatted_date(report_datetime)
raise NotConsentedError(
f"Consent not found. Has subject '{subject_identifier}' "
f"completed version '{self.version}' of consent on or after '{dte}'?"
f"Consent not found for this version. Has subject '{subject_identifier}' "
f"completed a version '{self.version}' consent?"
)
return consent_obj

Expand Down
4 changes: 4 additions & 0 deletions edc_consent/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ class AlreadyRegistered(Exception):

class SiteConsentError(Exception):
pass


class ConsentDefinitionNotConfiguredForUpdate(Exception):
pass
12 changes: 4 additions & 8 deletions edc_consent/field_mixins/personal_fields_mixin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from django.core.validators import RegexValidator
from django.db import models
from django.utils.html import format_html
from django_crypto_fields.fields import (
EncryptedCharField,
FirstnameField,
LastnameField,
)
from django_crypto_fields.fields import EncryptedCharField
from django_crypto_fields.models import CryptoMixin
from edc_constants.choices import GENDER_UNDETERMINED
from edc_model.models import NameFieldsModelMixin
Expand All @@ -29,7 +25,7 @@ class BaseFieldsMixin(models.Model):
blank=False,
)

guardian_name = LastnameField(
guardian_name = EncryptedCharField(
verbose_name="Guardian's last and first name",
validators=[FullNameValidator()],
blank=True,
Expand All @@ -55,7 +51,7 @@ class Meta:


class PersonalFieldsMixin(CryptoMixin, BaseFieldsMixin, models.Model):
first_name = FirstnameField(
first_name = EncryptedCharField(
null=True,
blank=False,
validators=[
Expand All @@ -67,7 +63,7 @@ class PersonalFieldsMixin(CryptoMixin, BaseFieldsMixin, models.Model):
help_text="Use UPPERCASE letters only.",
)

last_name = LastnameField(
last_name = EncryptedCharField(
verbose_name="Surname",
null=True,
blank=False,
Expand Down
Loading

0 comments on commit f43eb1a

Please sign in to comment.