5
5
from typing import TYPE_CHECKING , Type
6
6
7
7
from django .apps import apps as django_apps
8
+ from django .core .exceptions import MultipleObjectsReturned , ObjectDoesNotExist
8
9
from edc_constants .constants import FEMALE , MALE
10
+ from edc_identifier .model_mixins import NonUniqueSubjectIdentifierModelMixin
9
11
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
11
14
from edc_utils .date import ceil_datetime , floor_datetime , to_local , to_utc
12
15
13
- from .exceptions import ConsentDefinitionError
16
+ from .exceptions import (
17
+ ConsentDefinitionError ,
18
+ ConsentDefinitionValidityPeriodError ,
19
+ ConsentVersionSequenceError ,
20
+ NotConsentedError ,
21
+ )
14
22
15
23
if TYPE_CHECKING :
16
24
from .model_mixins import ConsentModelMixin
17
25
26
+ class ConsentLikeModel (NonUniqueSubjectIdentifierModelMixin , ConsentModelMixin ):
27
+ ...
28
+
18
29
19
30
@dataclass (order = True )
20
31
class ConsentDefinition :
@@ -24,20 +35,24 @@ class ConsentDefinition:
24
35
25
36
model : str = field (compare = False )
26
37
_ = 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 )
33
43
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 )
35
46
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 )
38
51
39
52
def __post_init__ (self ):
40
53
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
41
56
if MALE not in self .gender and FEMALE not in self .gender :
42
57
raise ConsentDefinitionError (f"Invalid gender. Got { self .gender } ." )
43
58
if not self .start .tzinfo :
@@ -46,29 +61,66 @@ def __post_init__(self):
46
61
raise ConsentDefinitionError (f"Naive datetime not allowed Got { self .end } ." )
47
62
self .check_date_within_study_period ()
48
63
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
+
49
80
def get_consent_for (
50
81
self , subject_identifier : str = None , report_datetime : datetime | None = None
51
- ) -> ConsentModelMixin :
82
+ ) -> ConsentLikeModel :
52
83
opts : dict [str , str | datetime ] = dict (
53
84
subject_identifier = subject_identifier ,
54
85
version = self .version ,
55
86
)
56
87
if report_datetime :
57
88
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
59
99
60
100
@property
61
- def model_cls (self ) -> Type [ConsentModelMixin ]:
101
+ def model_cls (self ) -> Type [ConsentLikeModel ]:
62
102
return django_apps .get_model (self .model )
63
103
64
104
@property
65
105
def display_name (self ) -> str :
66
106
return (
67
107
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 ))} "
70
110
)
71
111
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
+
72
124
def check_date_within_study_period (self ) -> None :
73
125
"""Raises if the date is not within the opening and closing
74
126
dates of the protocol.
@@ -79,11 +131,54 @@ def check_date_within_study_period(self) -> None:
79
131
for index , attr in enumerate (["start" , "end" ]):
80
132
if not (
81
133
floor_secs (floor_datetime (study_open_datetime ))
82
- <= floor_secs (getattr (self , attr ))
134
+ <= floor_secs (floor_datetime ( getattr (self , attr ) ))
83
135
<= floor_secs (ceil_datetime (study_close_datetime ))
84
136
):
85
137
date_string = formatted_datetime (getattr (self , attr ))
86
138
raise ConsentDefinitionError (
87
139
f"Invalid { attr } date. Cannot be before study start date. "
88
140
f"See { self } . Got { date_string } ."
89
141
)
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
0 commit comments