clinicedc 2.0.23__py3-none-any.whl → 2.0.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of clinicedc might be problematic. Click here for more details.
- {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/METADATA +1 -1
- {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/RECORD +48 -48
- edc_action_item/site_action_items.py +2 -2
- edc_appointment/view_utils/appointment_button.py +1 -1
- edc_consent/modeladmin_mixins/consent_model_admin_mixin.py +14 -12
- edc_consent/models/__init__.py +2 -2
- edc_consent/models/signals.py +26 -28
- edc_dashboard/view_mixins/message_view_mixin.py +3 -4
- edc_identifier/admin.py +3 -2
- edc_identifier/identifier.py +5 -5
- edc_identifier/model_mixins.py +1 -1
- edc_identifier/models.py +7 -6
- edc_identifier/research_identifier.py +1 -1
- edc_identifier/short_identifier.py +1 -1
- edc_identifier/simple_identifier.py +15 -5
- edc_identifier/subject_identifier.py +2 -2
- edc_identifier/utils.py +13 -14
- edc_listboard/view_mixins/search_listboard_view_mixin.py +6 -7
- edc_metadata/view_mixins/metadata_view_mixin.py +7 -4
- edc_randomization/randomization_list_importer.py +25 -21
- edc_randomization/randomization_list_verifier.py +31 -26
- edc_randomization/randomizer.py +1 -1
- edc_randomization/site_randomizers.py +25 -8
- edc_review_dashboard/middleware.py +4 -4
- edc_review_dashboard/views/subject_review_listboard_view.py +3 -3
- edc_screening/age_evaluator.py +1 -1
- edc_screening/eligibility.py +5 -5
- edc_screening/exceptions.py +3 -3
- edc_screening/gender_evaluator.py +1 -1
- edc_screening/modelform_mixins.py +1 -1
- edc_screening/screening_eligibility.py +30 -35
- edc_screening/utils.py +10 -12
- edc_sites/admin/list_filters.py +2 -2
- edc_sites/admin/site_model_admin_mixin.py +8 -8
- edc_sites/exceptions.py +1 -1
- edc_sites/forms.py +3 -1
- edc_sites/management/commands/sync_sites.py +11 -17
- edc_sites/models/site_profile.py +6 -4
- edc_sites/post_migrate_signals.py +2 -2
- edc_sites/site.py +8 -8
- edc_sites/utils/add_or_update_django_sites.py +3 -3
- edc_sites/utils/valid_site_for_subject_or_raise.py +7 -4
- edc_sites/view_mixins.py +2 -2
- edc_subject_dashboard/view_utils/subject_screening_button.py +5 -3
- edc_view_utils/dashboard_model_button.py +3 -3
- edc_visit_schedule/site_visit_schedules.py +2 -1
- {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/WHEEL +0 -0
- {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,6 +8,8 @@ from django.core.exceptions import ObjectDoesNotExist
|
|
|
8
8
|
from django.db import models
|
|
9
9
|
from django.utils import timezone
|
|
10
10
|
|
|
11
|
+
from edc_constants.constants import NULL_STRING
|
|
12
|
+
|
|
11
13
|
from .utils import convert_to_human_readable
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
@@ -22,10 +24,13 @@ class IdentifierError(Exception):
|
|
|
22
24
|
pass
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
IDENTIFER_PREFIX_LENGTH = 2
|
|
28
|
+
|
|
29
|
+
|
|
25
30
|
class SimpleIdentifier:
|
|
26
31
|
random_string_length: int = 5
|
|
27
32
|
template: str = "{device_id}{random_string}"
|
|
28
|
-
identifier_prefix: str =
|
|
33
|
+
identifier_prefix: str = NULL_STRING
|
|
29
34
|
|
|
30
35
|
def __init__(
|
|
31
36
|
self,
|
|
@@ -90,7 +95,7 @@ class SimpleSequentialIdentifier:
|
|
|
90
95
|
random_number: int = choice(range(1000, 9999)) # nosec B311
|
|
91
96
|
sequence: str = f"{sequence}{random_number}"
|
|
92
97
|
chk: int = int(sequence) % 11
|
|
93
|
-
self.identifier: str = f"{self.prefix or
|
|
98
|
+
self.identifier: str = f"{self.prefix or NULL_STRING}{sequence}{chk}"
|
|
94
99
|
|
|
95
100
|
def __str__(self) -> str:
|
|
96
101
|
return self.identifier
|
|
@@ -111,12 +116,13 @@ class SimpleUniqueIdentifier:
|
|
|
111
116
|
model: str = "edc_identifier.identifiermodel"
|
|
112
117
|
template: str = "{device_id}{random_string}"
|
|
113
118
|
identifier_prefix: str | None = None
|
|
119
|
+
identifier_prefix_length: int = 2
|
|
114
120
|
identifier_cls = SimpleIdentifier
|
|
115
121
|
make_human_readable: bool | None = None
|
|
116
122
|
|
|
117
123
|
def __init__(
|
|
118
124
|
self,
|
|
119
|
-
model: str = None,
|
|
125
|
+
model: str | None = None,
|
|
120
126
|
identifier_attr: str | None = None,
|
|
121
127
|
identifier_type: str | None = None,
|
|
122
128
|
identifier_prefix: str | None = None,
|
|
@@ -140,9 +146,13 @@ class SimpleUniqueIdentifier:
|
|
|
140
146
|
self.identifier_attr = identifier_attr or self.identifier_attr
|
|
141
147
|
self.identifier_type = identifier_type or self.identifier_type
|
|
142
148
|
self.identifier_prefix = identifier_prefix or self.identifier_prefix
|
|
143
|
-
if
|
|
149
|
+
if (
|
|
150
|
+
self.identifier_prefix
|
|
151
|
+
and len(self.identifier_prefix) != self.identifier_prefix_length
|
|
152
|
+
):
|
|
144
153
|
raise IdentifierError(
|
|
145
|
-
f"Expected identifier_prefix of length=
|
|
154
|
+
f"Expected identifier_prefix of length={self.identifier_prefix_length}. "
|
|
155
|
+
f"Got {len(identifier_prefix)}"
|
|
146
156
|
)
|
|
147
157
|
self.make_human_readable = make_human_readable or self.make_human_readable
|
|
148
158
|
self.device_id = django_apps.get_app_config("edc_device").device_id
|
|
@@ -10,8 +10,8 @@ class SubjectIdentifier(ResearchIdentifier):
|
|
|
10
10
|
label: str = "subjectidentifier"
|
|
11
11
|
padding: int = 4
|
|
12
12
|
|
|
13
|
-
def __init__(self, last_name: str = None, **kwargs):
|
|
14
|
-
self.last_name = last_name
|
|
13
|
+
def __init__(self, last_name: str | None = None, **kwargs):
|
|
14
|
+
self.last_name = last_name or ""
|
|
15
15
|
super().__init__(**kwargs)
|
|
16
16
|
|
|
17
17
|
def pre_identifier(self) -> None:
|
edc_identifier/utils.py
CHANGED
|
@@ -14,23 +14,22 @@ def is_subject_identifier_or_raise(subject_identifier, reference_obj=None, raise
|
|
|
14
14
|
* If `subject_identifier` is None, does nothing, unless
|
|
15
15
|
`raise_on_none` is `True`.
|
|
16
16
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
17
|
+
valid_subject_identifier = subject_identifier and re.match(
|
|
18
|
+
ResearchProtocolConfig().subject_identifier_pattern, subject_identifier or ""
|
|
19
|
+
)
|
|
20
|
+
if not valid_subject_identifier or (not subject_identifier and raise_on_none):
|
|
21
|
+
reference_msg = ""
|
|
22
|
+
if reference_obj:
|
|
23
|
+
reference_msg = f"See {reference_obj!r}. "
|
|
24
|
+
raise SubjectIdentifierError(
|
|
25
|
+
f"Invalid format for subject identifier. {reference_msg}"
|
|
26
|
+
f"Got `{subject_identifier or ''}`. "
|
|
27
|
+
f"Expected pattern `{ResearchProtocolConfig().subject_identifier_pattern}`"
|
|
28
|
+
)
|
|
30
29
|
return subject_identifier
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
def get_human_phrase(no_hyphen: bool = None) -> str:
|
|
32
|
+
def get_human_phrase(no_hyphen: bool | None = None) -> str:
|
|
34
33
|
"""Returns 6 digits split by a '-', e.g. DEC-96E.
|
|
35
34
|
|
|
36
35
|
There are 213,127,200 permutations from an unambiguous alphabet.
|
|
@@ -13,12 +13,12 @@ from edc_model_admin.utils import add_to_messages_once
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class SearchListboardMixin:
|
|
16
|
-
search_fields =
|
|
16
|
+
search_fields = ("slug",)
|
|
17
17
|
|
|
18
18
|
default_querystring_attrs: str = "q"
|
|
19
19
|
alternate_search_attr: str = "subject_identifier"
|
|
20
20
|
default_lookup = "icontains"
|
|
21
|
-
operators:
|
|
21
|
+
operators: tuple[str, ...] = (
|
|
22
22
|
"exact",
|
|
23
23
|
"iexact",
|
|
24
24
|
"contains",
|
|
@@ -33,7 +33,7 @@ class SearchListboardMixin:
|
|
|
33
33
|
"endswith",
|
|
34
34
|
"istartswith",
|
|
35
35
|
"iendswith",
|
|
36
|
-
|
|
36
|
+
)
|
|
37
37
|
|
|
38
38
|
def __init__(self, **kwargs):
|
|
39
39
|
self._search_term = None
|
|
@@ -66,12 +66,11 @@ class SearchListboardMixin:
|
|
|
66
66
|
|
|
67
67
|
@property
|
|
68
68
|
def search_term(self) -> str | None:
|
|
69
|
-
if not self._search_term:
|
|
70
|
-
|
|
71
|
-
self._search_term = escape(search_term).strip()
|
|
69
|
+
if (not self._search_term) and (search_term := self.raw_search_term):
|
|
70
|
+
self._search_term = escape(search_term).strip()
|
|
72
71
|
return self._search_term
|
|
73
72
|
|
|
74
|
-
def get_search_fields(self) ->
|
|
73
|
+
def get_search_fields(self) -> tuple[str, ...]:
|
|
75
74
|
"""Override to add additional search fields"""
|
|
76
75
|
return self.search_fields
|
|
77
76
|
|
|
@@ -16,16 +16,19 @@ class MetadataViewError(Exception):
|
|
|
16
16
|
|
|
17
17
|
class MetadataViewMixin:
|
|
18
18
|
panel_model: str = "edc_lab.panel"
|
|
19
|
-
metadata_show_status:
|
|
19
|
+
metadata_show_status: tuple[str] = (REQUIRED, KEYED)
|
|
20
20
|
|
|
21
21
|
def get_context_data(self, **kwargs) -> dict:
|
|
22
22
|
if self.appointment:
|
|
23
23
|
# always refresh metadata / run rules
|
|
24
24
|
refresh_metadata_for_timepoint(self.appointment, allow_create=True)
|
|
25
25
|
referer = self.request.headers.get("Referer")
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (
|
|
27
|
+
referer
|
|
28
|
+
and "subject_review_listboard" in referer
|
|
29
|
+
and self.appointment.related_visit
|
|
30
|
+
):
|
|
31
|
+
update_appt_status_for_timepoint(self.appointment.related_visit)
|
|
29
32
|
crf_qs = self.get_crf_metadata()
|
|
30
33
|
requisition_qs = self.get_requisition_metadata()
|
|
31
34
|
kwargs.update(crfs=crf_qs, requisitions=requisition_qs)
|
|
@@ -55,31 +55,32 @@ class RandomizationListImporter:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
Format:
|
|
58
|
-
sid,assignment,
|
|
59
|
-
1,single_dose
|
|
60
|
-
2,two_doses
|
|
58
|
+
sid,site_name, assignment, description, orig_site, orig_allocation, orig_desc
|
|
59
|
+
1,gaborone,intervention,single_dose
|
|
60
|
+
2,gaborone,control,two_doses
|
|
61
61
|
...
|
|
62
62
|
"""
|
|
63
63
|
|
|
64
|
-
required_csv_fieldnames =
|
|
64
|
+
required_csv_fieldnames = ("sid", "assignment", "site_name", "description")
|
|
65
65
|
verifier_cls = RandomizationListVerifier
|
|
66
66
|
|
|
67
67
|
def __init__(
|
|
68
68
|
self,
|
|
69
69
|
randomizer_model_cls=None,
|
|
70
|
-
randomizer_name: str = None,
|
|
71
|
-
randomizationlist_path: Path | str = None,
|
|
72
|
-
assignment_map: dict[str, int] = None,
|
|
73
|
-
verbose: bool = None,
|
|
74
|
-
overwrite: bool = None,
|
|
75
|
-
add: bool = None,
|
|
76
|
-
dryrun: bool = None,
|
|
77
|
-
username: str = None,
|
|
78
|
-
revision: str = None,
|
|
79
|
-
sid_count_for_tests: int = None,
|
|
80
|
-
extra_csv_fieldnames:
|
|
81
|
-
**kwargs,
|
|
70
|
+
randomizer_name: str | None = None,
|
|
71
|
+
randomizationlist_path: Path | str | None = None,
|
|
72
|
+
assignment_map: dict[str, int] | None = None,
|
|
73
|
+
verbose: bool | None = None,
|
|
74
|
+
overwrite: bool | None = None,
|
|
75
|
+
add: bool | None = None,
|
|
76
|
+
dryrun: bool | None = None,
|
|
77
|
+
username: str | None = None,
|
|
78
|
+
revision: str | None = None,
|
|
79
|
+
sid_count_for_tests: int | None = None,
|
|
80
|
+
extra_csv_fieldnames: tuple[str] | None = None,
|
|
81
|
+
**kwargs, # noqa: ARG002
|
|
82
82
|
):
|
|
83
|
+
extra_csv_fieldnames = extra_csv_fieldnames or ()
|
|
83
84
|
self.verify_messages: str | None = None
|
|
84
85
|
self.add = add
|
|
85
86
|
self.overwrite = overwrite
|
|
@@ -92,7 +93,7 @@ class RandomizationListImporter:
|
|
|
92
93
|
self.randomizer_name = randomizer_name
|
|
93
94
|
self.assignment_map = assignment_map
|
|
94
95
|
self.randomizationlist_path: Path = Path(randomizationlist_path).expanduser()
|
|
95
|
-
self.required_csv_fieldnames.
|
|
96
|
+
self.required_csv_fieldnames = (*self.required_csv_fieldnames, *extra_csv_fieldnames)
|
|
96
97
|
|
|
97
98
|
if self.dryrun:
|
|
98
99
|
sys.stdout.write(
|
|
@@ -223,12 +224,15 @@ class RandomizationListImporter:
|
|
|
223
224
|
sid_count = self.sid_count_for_tests
|
|
224
225
|
else:
|
|
225
226
|
sid_count = len(self.get_sid_list())
|
|
226
|
-
with self.randomizationlist_path.open(mode="r") as
|
|
227
|
-
reader = csv.DictReader(
|
|
228
|
-
for row in
|
|
227
|
+
with self.randomizationlist_path.open(mode="r") as f:
|
|
228
|
+
reader = csv.DictReader(f)
|
|
229
|
+
all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
|
|
230
|
+
sorted_rows = sorted(
|
|
231
|
+
all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
|
|
232
|
+
)
|
|
233
|
+
for row in tqdm(sorted_rows, total=sid_count):
|
|
229
234
|
if self.sid_count_for_tests and len(objs) == self.sid_count_for_tests:
|
|
230
235
|
break
|
|
231
|
-
row = {k: v.strip() for k, v in row.items()}
|
|
232
236
|
try:
|
|
233
237
|
self.randomizer_model_cls.objects.get(sid=row["sid"])
|
|
234
238
|
except ObjectDoesNotExist:
|
|
@@ -25,13 +25,13 @@ class RandomizationListVerifier:
|
|
|
25
25
|
def __init__(
|
|
26
26
|
self,
|
|
27
27
|
randomizer_name=None,
|
|
28
|
-
randomizationlist_path: Path | str = None,
|
|
28
|
+
randomizationlist_path: Path | str | None = None,
|
|
29
29
|
randomizer_model_cls=None,
|
|
30
30
|
assignment_map=None,
|
|
31
31
|
fieldnames=None,
|
|
32
32
|
sid_count_for_tests=None,
|
|
33
|
-
required_csv_fieldnames:
|
|
34
|
-
**kwargs,
|
|
33
|
+
required_csv_fieldnames: tuple[str, ...] | None = None,
|
|
34
|
+
**kwargs, # noqa: ARG002
|
|
35
35
|
):
|
|
36
36
|
self.count: int = 0
|
|
37
37
|
self.messages: list[str] = []
|
|
@@ -68,33 +68,38 @@ class RandomizationListVerifier:
|
|
|
68
68
|
)
|
|
69
69
|
elif message := self.verify():
|
|
70
70
|
self.messages.append(message)
|
|
71
|
-
if self.messages
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
raise RandomizationListError(", ".join(self.messages))
|
|
71
|
+
if self.messages and (
|
|
72
|
+
"migrate" not in sys.argv
|
|
73
|
+
and "makemigrations" not in sys.argv
|
|
74
|
+
and "import_randomization_list" not in sys.argv
|
|
75
|
+
):
|
|
76
|
+
raise RandomizationListError(", ".join(self.messages))
|
|
78
77
|
|
|
79
78
|
def verify(self) -> str | None:
|
|
80
79
|
message = None
|
|
80
|
+
|
|
81
81
|
with self.randomizationlist_path.open(mode="r") as f:
|
|
82
82
|
reader = csv.DictReader(f)
|
|
83
|
-
for
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
83
|
+
all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
|
|
84
|
+
sorted_rows = sorted(
|
|
85
|
+
all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
for index, row in enumerate(sorted_rows, start=1):
|
|
89
|
+
sys.stdout.write(f"Index: {index}, SID: {row.get('sid')}, Row: {row}\n")
|
|
90
|
+
message = self.inspect_row(index - 1, row)
|
|
91
|
+
if message:
|
|
92
|
+
break
|
|
93
|
+
if self.sid_count_for_tests and index == self.sid_count_for_tests:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
if not message and self.count != index:
|
|
97
|
+
message = (
|
|
98
|
+
f"Randomization list count is off. Expected {index} (CSV). "
|
|
99
|
+
f"Got {self.count} (model_cls). See file "
|
|
100
|
+
f"{self.randomizationlist_path}. "
|
|
101
|
+
f"Resolve this issue before using the system."
|
|
102
|
+
)
|
|
98
103
|
return message
|
|
99
104
|
|
|
100
105
|
def inspect_row(self, index: int, row) -> str | None:
|
|
@@ -103,7 +108,7 @@ class RandomizationListVerifier:
|
|
|
103
108
|
Note:Index is zero-based
|
|
104
109
|
"""
|
|
105
110
|
message = None
|
|
106
|
-
obj1 = self.randomizer_model_cls.objects.all().order_by("sid")[index]
|
|
111
|
+
obj1 = self.randomizer_model_cls.objects.all().order_by("site_name", "sid")[index]
|
|
107
112
|
try:
|
|
108
113
|
obj2 = self.randomizer_model_cls.objects.get(sid=row["sid"])
|
|
109
114
|
except ObjectDoesNotExist:
|
edc_randomization/randomizer.py
CHANGED
|
@@ -101,7 +101,7 @@ class Randomizer:
|
|
|
101
101
|
)
|
|
102
102
|
filename: str = "randomization_list.csv"
|
|
103
103
|
randomizationlist_folder: Path | str = get_randomization_list_path()
|
|
104
|
-
extra_csv_fieldnames:
|
|
104
|
+
extra_csv_fieldnames: tuple[str] | None = None
|
|
105
105
|
trial_is_blinded: bool = True
|
|
106
106
|
importer_cls: Any = RandomizationListImporter
|
|
107
107
|
apps = None # if not using django_apps
|
|
@@ -28,7 +28,22 @@ class SiteRandomizerError(Exception):
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class SiteRandomizers:
|
|
31
|
-
"""Main controller of :class:`SiteRandomizers` objects.
|
|
31
|
+
"""Main controller of :class:`SiteRandomizers` objects.
|
|
32
|
+
|
|
33
|
+
To rescan the CSV file for randomizer additional slots:
|
|
34
|
+
|
|
35
|
+
from edc_randomization.site_randomizers import site_randomizers
|
|
36
|
+
# assuming the randimizer is "default"
|
|
37
|
+
site_randomizers.get("default").import_list(add=True,dryrun=True)
|
|
38
|
+
|
|
39
|
+
If all OK:
|
|
40
|
+
|
|
41
|
+
# assuming the randimizer is "default"
|
|
42
|
+
site_randomizers.get("default").import_list(add=True,dryrun=False)
|
|
43
|
+
|
|
44
|
+
See
|
|
45
|
+
|
|
46
|
+
"""
|
|
32
47
|
|
|
33
48
|
def __init__(self):
|
|
34
49
|
self._registry = {}
|
|
@@ -57,12 +72,12 @@ class SiteRandomizers:
|
|
|
57
72
|
def get(self, name):
|
|
58
73
|
try:
|
|
59
74
|
return self._registry[str(name)]
|
|
60
|
-
except KeyError:
|
|
75
|
+
except KeyError as e:
|
|
61
76
|
raise NotRegistered(
|
|
62
77
|
f"A Randomizer class by this name is not registered. "
|
|
63
78
|
f"Expected one of {list(self._registry.keys())}. "
|
|
64
79
|
f"Got '{name}'. See site_randomizer."
|
|
65
|
-
)
|
|
80
|
+
) from e
|
|
66
81
|
|
|
67
82
|
def get_by_model(self, model=None):
|
|
68
83
|
"""Returns the randomizer class for this model label_lower.
|
|
@@ -75,10 +90,12 @@ class SiteRandomizers:
|
|
|
75
90
|
return None
|
|
76
91
|
|
|
77
92
|
def get_as_choices(self):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
return tuple(
|
|
94
|
+
[
|
|
95
|
+
(randomizer_cls.name, randomizer_cls.name)
|
|
96
|
+
for randomizer_cls in self._registry.values()
|
|
97
|
+
]
|
|
98
|
+
)
|
|
82
99
|
|
|
83
100
|
def randomize(
|
|
84
101
|
self,
|
|
@@ -115,7 +132,7 @@ class SiteRandomizers:
|
|
|
115
132
|
before_import_registry = copy.copy(site_randomizers._registry)
|
|
116
133
|
import_module(f"{app}.{module_name}")
|
|
117
134
|
if verbose:
|
|
118
|
-
sys.stdout.write(" * registered randomizer from
|
|
135
|
+
sys.stdout.write(f" * registered randomizer from '{app}'\n")
|
|
119
136
|
except Exception as e:
|
|
120
137
|
if f"No module named '{app}.{module_name}'" not in str(e):
|
|
121
138
|
raise
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
1
3
|
from django.conf import settings
|
|
2
4
|
|
|
3
5
|
from .dashboard_templates import dashboard_templates
|
|
@@ -10,12 +12,10 @@ class DashboardMiddleware:
|
|
|
10
12
|
def __call__(self, request):
|
|
11
13
|
return self.get_response(request)
|
|
12
14
|
|
|
13
|
-
def process_view(self, request, *args) -> None:
|
|
15
|
+
def process_view(self, request, *args) -> None: # noqa: ARG002
|
|
14
16
|
template_data = dashboard_templates
|
|
15
|
-
|
|
17
|
+
with contextlib.suppress(AttributeError):
|
|
16
18
|
template_data.update(settings.REVIEW_DASHBOARD_BASE_TEMPLATES)
|
|
17
|
-
except AttributeError:
|
|
18
|
-
pass
|
|
19
19
|
request.template_data.update(**template_data)
|
|
20
20
|
|
|
21
21
|
def process_template_response(self, request, response):
|
|
@@ -49,15 +49,15 @@ class SubjectReviewListboardView(
|
|
|
49
49
|
listboard_view_permission_codename = "edc_review_dashboard.view_subject_review_listboard"
|
|
50
50
|
|
|
51
51
|
navbar_selected_item = "subject_review"
|
|
52
|
-
ordering =
|
|
52
|
+
ordering = ("subject_identifier", "visit_code", "visit_code_sequence")
|
|
53
53
|
paginate_by = 25
|
|
54
54
|
search_form_url = "subject_review_listboard_url"
|
|
55
|
-
search_fields =
|
|
55
|
+
search_fields = (
|
|
56
56
|
"subject_identifier",
|
|
57
57
|
"visit_code",
|
|
58
58
|
"user_created",
|
|
59
59
|
"user_modified",
|
|
60
|
-
|
|
60
|
+
)
|
|
61
61
|
|
|
62
62
|
# attr to call SubjectReviewListboardView.urls in urls.py
|
|
63
63
|
urlconfig_getattr = "review_listboard_urls"
|
edc_screening/age_evaluator.py
CHANGED
|
@@ -25,7 +25,7 @@ class AgeEvaluator(ReportableAgeEvaluator):
|
|
|
25
25
|
self.reasons_ineligible = "Age unknown"
|
|
26
26
|
return eligible
|
|
27
27
|
|
|
28
|
-
def in_bounds_or_raise(self, age: int = None, **kwargs):
|
|
28
|
+
def in_bounds_or_raise(self, age: int | None = None, **kwargs): # noqa: ARG002
|
|
29
29
|
self.reasons_ineligible = ""
|
|
30
30
|
dob = localtime(timezone.now() - relativedelta(years=age)).date()
|
|
31
31
|
age_units = "years"
|
edc_screening/eligibility.py
CHANGED
|
@@ -18,14 +18,14 @@ class Eligibility:
|
|
|
18
18
|
# default to eligible if >=18
|
|
19
19
|
age_evaluator = AgeEvaluator(age_lower=18, age_lower_inclusive=True)
|
|
20
20
|
|
|
21
|
-
custom_reasons_dict: dict = {}
|
|
21
|
+
custom_reasons_dict: dict = {} # noqa: RUF012
|
|
22
22
|
|
|
23
23
|
def __init__(
|
|
24
24
|
self,
|
|
25
|
-
age: int = None,
|
|
26
|
-
gender: str = None,
|
|
27
|
-
pregnant: bool = None,
|
|
28
|
-
breast_feeding: bool = None,
|
|
25
|
+
age: int | None = None,
|
|
26
|
+
gender: str | None = None,
|
|
27
|
+
pregnant: bool | None = None,
|
|
28
|
+
breast_feeding: bool | None = None,
|
|
29
29
|
**additional_criteria,
|
|
30
30
|
) -> None:
|
|
31
31
|
self.criteria = dict(**additional_criteria)
|
edc_screening/exceptions.py
CHANGED
|
@@ -17,15 +17,15 @@ class ScreeningEligibilityCleanedDataKeyError(Exception):
|
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class ScreeningEligibilityInvalidCombination(Exception):
|
|
20
|
+
class ScreeningEligibilityInvalidCombination(Exception): # noqa: N818
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class RequiredFieldValueMissing(Exception):
|
|
24
|
+
class RequiredFieldValueMissing(Exception): # noqa: N818
|
|
25
25
|
pass
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class InvalidScreeningIdentifierFormat(Exception):
|
|
28
|
+
class InvalidScreeningIdentifierFormat(Exception): # noqa: N818
|
|
29
29
|
def __init__(self, *args, **kwargs):
|
|
30
30
|
self.code = INVALID_SCREENING_IDENTIFIER
|
|
31
31
|
super().__init__(*args, **kwargs)
|
|
@@ -27,7 +27,7 @@ class AlreadyConsentedFormMixin:
|
|
|
27
27
|
raise forms.ValidationError(self.already_consented_validation_message(url))
|
|
28
28
|
return cleaned_data
|
|
29
29
|
|
|
30
|
-
def already_consented_validation_url(self, cleaned_data: dict | None = None) -> str:
|
|
30
|
+
def already_consented_validation_url(self, cleaned_data: dict | None = None) -> str: # noqa: ARG002
|
|
31
31
|
url_name = url_names.get("subject_dashboard_url")
|
|
32
32
|
return reverse(
|
|
33
33
|
url_name,
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from django.utils.html import format_html
|
|
6
5
|
from django.utils.safestring import mark_safe
|
|
7
6
|
|
|
8
7
|
from edc_constants.constants import NO, PENDING, TBD, YES
|
|
@@ -33,7 +32,7 @@ class ScreeningEligibility:
|
|
|
33
32
|
eligible_fld_name: str = "eligible"
|
|
34
33
|
eligible_value_default: str = TBD
|
|
35
34
|
default_display_label = TBD
|
|
36
|
-
eligible_values_list: list =
|
|
35
|
+
eligible_values_list: list = (YES, NO, TBD)
|
|
37
36
|
ineligible_display_label: str = "INELIGIBLE"
|
|
38
37
|
is_eligible_value: str = YES
|
|
39
38
|
is_ineligible_value: str = NO
|
|
@@ -44,9 +43,9 @@ class ScreeningEligibility:
|
|
|
44
43
|
def __init__(
|
|
45
44
|
self,
|
|
46
45
|
model_obj: SubjectScreeningModel | EligibilityModelMixin = None,
|
|
47
|
-
cleaned_data: dict = None,
|
|
46
|
+
cleaned_data: dict | None = None,
|
|
48
47
|
eligible_value_default: str | None = None,
|
|
49
|
-
eligible_values_list:
|
|
48
|
+
eligible_values_list: tuple[str, ...] | None = None,
|
|
50
49
|
is_eligible_value: str | None = None,
|
|
51
50
|
is_ineligible_value: str | None = None,
|
|
52
51
|
eligible_display_label: str | None = None,
|
|
@@ -126,7 +125,7 @@ class ScreeningEligibility:
|
|
|
126
125
|
@property
|
|
127
126
|
def is_eligible(self) -> bool:
|
|
128
127
|
"""Returns True if eligible else False"""
|
|
129
|
-
return
|
|
128
|
+
return self.eligible == self.is_eligible_value
|
|
130
129
|
|
|
131
130
|
def _assess_eligibility(self) -> None:
|
|
132
131
|
self.set_fld_attrs_on_self()
|
|
@@ -136,30 +135,29 @@ class ScreeningEligibility:
|
|
|
136
135
|
self.reasons_ineligible.update(**missing_data)
|
|
137
136
|
self.eligible = self.eligible_value_default # probably TBD
|
|
138
137
|
for fldattr, fc in self.get_required_fields().items():
|
|
139
|
-
if fldattr not in missing_data:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
self.eligible = self.is_ineligible_value # probably NO
|
|
138
|
+
if fldattr not in missing_data and fc and fc.value:
|
|
139
|
+
msg = fc.msg if fc.msg else fldattr.title().replace("_", " ")
|
|
140
|
+
is_callable = False
|
|
141
|
+
try:
|
|
142
|
+
value = fc.value(getattr(self, fldattr))
|
|
143
|
+
except TypeError:
|
|
144
|
+
value = fc.value
|
|
145
|
+
else:
|
|
146
|
+
is_callable = True
|
|
147
|
+
if (
|
|
148
|
+
(isinstance(value, str) and getattr(self, fldattr) != value)
|
|
149
|
+
or (
|
|
150
|
+
isinstance(value, (list, tuple))
|
|
151
|
+
and getattr(self, fldattr) not in value
|
|
152
|
+
)
|
|
153
|
+
or (
|
|
154
|
+
isinstance(value, range)
|
|
155
|
+
and not (min(value) <= getattr(self, fldattr) <= max(value))
|
|
156
|
+
)
|
|
157
|
+
or (is_callable and value is False)
|
|
158
|
+
):
|
|
159
|
+
self.reasons_ineligible.update({fldattr: msg})
|
|
160
|
+
self.eligible = self.is_ineligible_value # probably NO
|
|
163
161
|
if self.is_eligible:
|
|
164
162
|
if self.is_eligible and not self.get_required_fields():
|
|
165
163
|
self.eligible = self.eligible_value_default
|
|
@@ -194,7 +192,7 @@ class ScreeningEligibility:
|
|
|
194
192
|
"does not exist on class. "
|
|
195
193
|
f"See {self.__class__.__name__}. "
|
|
196
194
|
f"Got {e}"
|
|
197
|
-
)
|
|
195
|
+
) from e
|
|
198
196
|
if self.model_obj:
|
|
199
197
|
try:
|
|
200
198
|
value = (
|
|
@@ -207,7 +205,7 @@ class ScreeningEligibility:
|
|
|
207
205
|
"Attribute does not exist on model. "
|
|
208
206
|
f"See {self.model_obj.__class__.__name__}. "
|
|
209
207
|
f"Got {e}"
|
|
210
|
-
)
|
|
208
|
+
) from e
|
|
211
209
|
else:
|
|
212
210
|
value = self.cleaned_data.get(fldattr)
|
|
213
211
|
setattr(self, fldattr, value)
|
|
@@ -230,10 +228,7 @@ class ScreeningEligibility:
|
|
|
230
228
|
|
|
231
229
|
def formatted_reasons_ineligible(self) -> str:
|
|
232
230
|
str_values = "<BR>".join([x for x in self.reasons_ineligible.values() if x])
|
|
233
|
-
return
|
|
234
|
-
"{}",
|
|
235
|
-
mark_safe(str_values), # nosec B703 B308
|
|
236
|
-
)
|
|
231
|
+
return mark_safe(str_values) # noqa: S308
|
|
237
232
|
|
|
238
233
|
@property
|
|
239
234
|
def display_label(self) -> str:
|