Skip to content

Commit 4670bea

Browse files
committed
Merge branch 'release/0.3.64' into main
2 parents 7379c95 + 6fd4a68 commit 4670bea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+942
-741
lines changed

edc_consent/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
from importlib.metadata import version
22

33
__version__ = version("edc_consent")
4+
__all__ = [
5+
"site_consents",
6+
"ConsentDefinitionDoesNotExist",
7+
"ConsentDefinitionError",
8+
"ConsentError",
9+
"ConsentVersionSequenceError",
10+
"NotConsentedError",
11+
]
412

5-
from .exceptions import ConsentDefinitionDoesNotExist, NotConsentedError
6-
from .model_wrappers import ConsentModelWrapperMixin
13+
from .exceptions import (
14+
ConsentDefinitionDoesNotExist,
15+
ConsentDefinitionError,
16+
ConsentError,
17+
ConsentVersionSequenceError,
18+
NotConsentedError,
19+
)
720
from .site_consents import site_consents

edc_consent/auth_objects.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
1-
from .utils import get_consent_model_name, get_reconsent_model_name
1+
from .exceptions import SiteConsentError
2+
from .site_consents import site_consents
23

3-
consent_codenames = []
4-
models = [get_consent_model_name()]
5-
if get_reconsent_model_name():
6-
models.append(get_reconsent_model_name())
7-
for model in models:
8-
for action in ["view_", "add_", "change_", "delete_", "view_historical"]:
9-
consent_codenames.append(f".{action}".join(model.split(".")))
10-
11-
navbar_codenames = [
12-
"edc_consent.nav_consent",
13-
]
4+
navbar_codenames = ["edc_consent.nav_consent"]
145

156
navbar_tuples = []
167
for codename in navbar_codenames:
178
navbar_tuples.append((codename, f"Can access {codename.split('.')[1]}"))
189

10+
consent_codenames = []
11+
try:
12+
cdefs = site_consents.get_consent_definitions()
13+
except SiteConsentError:
14+
site_consents.autodiscover()
15+
try:
16+
cdefs = site_consents.get_consent_definitions()
17+
except SiteConsentError:
18+
pass
19+
else:
20+
models = [cdef.model for cdef in cdefs]
21+
22+
# TODO: handle reconsent model in auth, should come from
23+
# get_consent_definitions.
24+
# if get_reconsent_model_name():
25+
# models.append(get_reconsent_model_name())
26+
models = list(set(models))
27+
for model in models:
28+
for action in ["view_", "add_", "change_", "delete_", "view_historical"]:
29+
consent_codenames.append(f".{action}".join(model.split(".")))
30+
31+
1932
consent_codenames.extend(navbar_codenames)
2033
consent_codenames.sort()

edc_consent/consent_definition.py

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,27 @@
55
from typing import TYPE_CHECKING, Type
66

77
from django.apps import apps as django_apps
8+
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
89
from edc_constants.constants import FEMALE, MALE
10+
from edc_identifier.model_mixins import NonUniqueSubjectIdentifierModelMixin
911
from edc_protocol import Protocol
10-
from edc_utils import floor_secs, formatted_datetime
12+
from edc_sites import site_sites
13+
from edc_utils import floor_secs, formatted_date, formatted_datetime
1114
from edc_utils.date import ceil_datetime, floor_datetime, to_local, to_utc
1215

13-
from .exceptions import ConsentDefinitionError
16+
from .exceptions import (
17+
ConsentDefinitionError,
18+
ConsentDefinitionValidityPeriodError,
19+
ConsentVersionSequenceError,
20+
NotConsentedError,
21+
)
1422

1523
if TYPE_CHECKING:
1624
from .model_mixins import ConsentModelMixin
1725

26+
class ConsentLikeModel(NonUniqueSubjectIdentifierModelMixin, ConsentModelMixin):
27+
...
28+
1829

1930
@dataclass(order=True)
2031
class ConsentDefinition:
@@ -24,20 +35,24 @@ class ConsentDefinition:
2435

2536
model: str = field(compare=False)
2637
_ = KW_ONLY
27-
start: datetime = field(compare=False)
28-
end: datetime = field(compare=False)
29-
age_min: int = field(compare=False)
30-
age_max: int = field(compare=False)
31-
age_is_adult: int | None = field(compare=False)
32-
name: str = field(init=False, compare=True)
38+
start: datetime = field(default=Protocol().study_open_datetime, compare=False)
39+
end: datetime = field(default=Protocol().study_close_datetime, compare=False)
40+
age_min: int = field(default=18, compare=False)
41+
age_max: int = field(default=110, compare=False)
42+
age_is_adult: int = field(default=18, compare=False)
3343
version: str = field(default="1", compare=False)
34-
gender: list[str] = field(default_factory=list, compare=False)
44+
gender: list[str] | None = field(default_factory=list, compare=False)
45+
updates_versions: list[str] | None = field(default_factory=list, compare=False)
3546
subject_type: str = field(default="subject", compare=False)
36-
updates_versions: list[str] = field(default_factory=list, compare=False)
37-
proxy_models: list[str] = field(default_factory=list, compare=False)
47+
site_ids: list[int] = field(default_factory=list, compare=False)
48+
country: str | None = field(default=None, compare=False)
49+
name: str = field(init=False, compare=True)
50+
sort_index: str = field(init=False)
3851

3952
def __post_init__(self):
4053
self.name = f"{self.model}-{self.version}"
54+
self.sort_index = self.name
55+
self.gender = [MALE, FEMALE] if not self.gender else self.gender
4156
if MALE not in self.gender and FEMALE not in self.gender:
4257
raise ConsentDefinitionError(f"Invalid gender. Got {self.gender}.")
4358
if not self.start.tzinfo:
@@ -46,29 +61,66 @@ def __post_init__(self):
4661
raise ConsentDefinitionError(f"Naive datetime not allowed Got {self.end}.")
4762
self.check_date_within_study_period()
4863

64+
@property
65+
def sites(self):
66+
if not site_sites.loaded:
67+
raise ConsentDefinitionError(
68+
"No registered sites found or edc_sites.sites not loaded yet. "
69+
"Perhaps place `edc_sites` before `edc_consent` "
70+
"in INSTALLED_APPS."
71+
)
72+
if self.country:
73+
sites = site_sites.get_by_country(self.country, aslist=True)
74+
elif self.site_ids:
75+
sites = [s for s in site_sites.all(aslist=True) if s.site_id in self.site_ids]
76+
else:
77+
sites = [s for s in site_sites.all(aslist=True)]
78+
return sites
79+
4980
def get_consent_for(
5081
self, subject_identifier: str = None, report_datetime: datetime | None = None
51-
) -> ConsentModelMixin:
82+
) -> ConsentLikeModel:
5283
opts: dict[str, str | datetime] = dict(
5384
subject_identifier=subject_identifier,
5485
version=self.version,
5586
)
5687
if report_datetime:
5788
opts.update(consent_datetime__lte=to_utc(report_datetime))
58-
return self.model_cls.objects.get(**opts)
89+
try:
90+
consent = self.model_cls.objects.get(**opts)
91+
except ObjectDoesNotExist:
92+
dte = formatted_date(report_datetime)
93+
raise NotConsentedError(
94+
f"Consent not found. Has subject '{subject_identifier}' "
95+
f"completed version '{self.version}' of consent "
96+
f"'{self.model_cls._meta.verbose_name}' on or after '{dte}'?"
97+
)
98+
return consent
5999

60100
@property
61-
def model_cls(self) -> Type[ConsentModelMixin]:
101+
def model_cls(self) -> Type[ConsentLikeModel]:
62102
return django_apps.get_model(self.model)
63103

64104
@property
65105
def display_name(self) -> str:
66106
return (
67107
f"{self.model_cls._meta.verbose_name} v{self.version} valid "
68-
f"from {formatted_datetime(to_local(self.start))} to "
69-
f"{formatted_datetime(to_local(self.end))}"
108+
f"from {formatted_date(to_local(self.start))} to "
109+
f"{formatted_date(to_local(self.end))}"
70110
)
71111

112+
def valid_for_datetime_or_raise(self, report_datetime: datetime) -> None:
113+
if not (
114+
floor_secs(floor_datetime(self.start))
115+
<= floor_secs(floor_datetime(report_datetime))
116+
<= floor_secs(floor_datetime(self.end))
117+
):
118+
date_string = formatted_date(report_datetime)
119+
raise ConsentDefinitionValidityPeriodError(
120+
"Date does not fall within the validity period."
121+
f"See {self.name}. Got {date_string}. "
122+
)
123+
72124
def check_date_within_study_period(self) -> None:
73125
"""Raises if the date is not within the opening and closing
74126
dates of the protocol.
@@ -79,11 +131,54 @@ def check_date_within_study_period(self) -> None:
79131
for index, attr in enumerate(["start", "end"]):
80132
if not (
81133
floor_secs(floor_datetime(study_open_datetime))
82-
<= floor_secs(getattr(self, attr))
134+
<= floor_secs(floor_datetime(getattr(self, attr)))
83135
<= floor_secs(ceil_datetime(study_close_datetime))
84136
):
85137
date_string = formatted_datetime(getattr(self, attr))
86138
raise ConsentDefinitionError(
87139
f"Invalid {attr} date. Cannot be before study start date. "
88140
f"See {self}. Got {date_string}."
89141
)
142+
143+
def update_previous_consent(self, obj: ConsentLikeModel) -> None:
144+
if self.updates_versions:
145+
previous_consent = self.get_previous_consent(
146+
subject_identifier=obj.subject_identifier,
147+
)
148+
previous_consent.subject_identifier_as_pk = obj.subject_identifier_as_pk
149+
previous_consent.subject_identifier_aka = obj.subject_identifier_aka
150+
previous_consent.save(
151+
update_fields=["subject_identifier_as_pk", "subject_identifier_aka"]
152+
)
153+
154+
def get_previous_consent(
155+
self, subject_identifier: str, version: str = None
156+
) -> ConsentLikeModel | None:
157+
"""Returns the previous consent or raises if it does
158+
not exist or is out of sequence with the current.
159+
"""
160+
if version in self.updates_versions:
161+
raise ConsentVersionSequenceError(f"Invalid consent version. Got {version}.")
162+
opts = dict(
163+
subject_identifier=subject_identifier,
164+
model_name=self.model,
165+
version__in=self.updates_versions,
166+
)
167+
opts = {k: v for k, v in opts.items() if v is not None}
168+
try:
169+
previous_consent = self.model_cls.objects.get(**opts)
170+
except ObjectDoesNotExist:
171+
if not self.updates_versions:
172+
previous_consent = None
173+
else:
174+
updates_versions = ", ".join(self.updates_versions)
175+
raise ConsentVersionSequenceError(
176+
f"Failed to update previous version. A previous consent "
177+
f"with version in {updates_versions} for {subject_identifier} "
178+
f"was not found. Consent version '{self.version}' is "
179+
f"configured to update a previous version. "
180+
f"See consent definition `{self.name}`."
181+
)
182+
except MultipleObjectsReturned:
183+
previous_consent = self.model_cls.objects.filter(**opts).order_by("-version")[0]
184+
return previous_consent

edc_consent/consent_helper.py

Lines changed: 0 additions & 88 deletions
This file was deleted.

edc_consent/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,15 @@ class ConsentDefinitionError(Exception):
1616

1717
class ConsentDefinitionDoesNotExist(Exception):
1818
pass
19+
20+
21+
class ConsentDefinitionValidityPeriodError(Exception):
22+
pass
23+
24+
25+
class AlreadyRegistered(Exception):
26+
pass
27+
28+
29+
class SiteConsentError(Exception):
30+
pass

edc_consent/field_mixins/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,17 @@
77
from .site_fields_mixin import SiteFieldsMixin
88
from .verification_fields_mixin import VerificationFieldsMixin
99
from .vulnerability_fields_mixin import VulnerabilityFieldsMixin
10+
11+
__all__ = [
12+
"CitizenFieldsMixin",
13+
"FullNamePersonalFieldsMixin",
14+
"IdentityFieldsMixin",
15+
"IdentityFieldsMixinError",
16+
"PersonalFieldsMixin",
17+
"ReviewFieldsMixin",
18+
"SampleCollectionFieldsMixin",
19+
"ScoredReviewFieldsMixin",
20+
"SiteFieldsMixin",
21+
"VerificationFieldsMixin",
22+
"VulnerabilityFieldsMixin",
23+
]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from .subject_consent_form_validator import SubjectConsentFormValidatorMixin
2+
3+
__all__ = ["SubjectConsentFormValidatorMixin"]

0 commit comments

Comments
 (0)