clinicedc 2.0.24__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.24.dist-info → clinicedc-2.0.25.dist-info}/METADATA +1 -1
- {clinicedc-2.0.24.dist-info → clinicedc-2.0.25.dist-info}/RECORD +47 -47
- 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 +21 -17
- edc_randomization/randomization_list_verifier.py +2 -2
- edc_randomization/randomizer.py +1 -1
- 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.24.dist-info → clinicedc-2.0.25.dist-info}/WHEEL +0 -0
- {clinicedc-2.0.24.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)
|
|
@@ -67,19 +67,20 @@ class RandomizationListImporter:
|
|
|
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,12 +25,12 @@ 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:
|
|
33
|
+
required_csv_fieldnames: tuple[str, ...] | None = None,
|
|
34
34
|
**kwargs, # noqa: ARG002
|
|
35
35
|
):
|
|
36
36
|
self.count: int = 0
|
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
|
|
@@ -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:
|
edc_screening/utils.py
CHANGED
|
@@ -40,10 +40,8 @@ def format_reasons_ineligible(*str_values: str | None, delimiter: str | None = N
|
|
|
40
40
|
str_values = str_values or []
|
|
41
41
|
str_values = tuple(x for x in str_values if x)
|
|
42
42
|
if str_values:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
mark_safe(delimiter.join(str_values)), # noqa: S308
|
|
46
|
-
)
|
|
43
|
+
formatted_string = delimiter.join(str_values)
|
|
44
|
+
reasons = mark_safe(formatted_string) # noqa: S308
|
|
47
45
|
return reasons
|
|
48
46
|
|
|
49
47
|
|
|
@@ -120,15 +118,15 @@ def is_eligible_or_raise(
|
|
|
120
118
|
if url and url_name.endswith("changelist"):
|
|
121
119
|
url = f"{url}?q={subject_screening.screening_identifier}"
|
|
122
120
|
if not url:
|
|
123
|
-
|
|
124
|
-
"{}",
|
|
121
|
+
safe_string = mark_safe( # noqa: S308
|
|
125
122
|
"Not allowed. Subject is not eligible. "
|
|
126
|
-
f"Got {subject_screening.screening_identifier}",
|
|
123
|
+
f"Got {subject_screening.screening_identifier}.",
|
|
127
124
|
)
|
|
128
125
|
else:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
safe_string = format_html(
|
|
127
|
+
"Not allowed. Subject is not eligible. See subject "
|
|
128
|
+
'<A href="{url}">{screening_identifier}</A>',
|
|
129
|
+
url=url,
|
|
130
|
+
screening_identifier=subject_screening.screening_identifier,
|
|
133
131
|
)
|
|
134
|
-
raise forms.ValidationError(
|
|
132
|
+
raise forms.ValidationError(safe_string)
|
edc_sites/admin/list_filters.py
CHANGED
|
@@ -17,10 +17,10 @@ class SiteListFilter(SimpleListFilter):
|
|
|
17
17
|
else:
|
|
18
18
|
site_ids = sites.get_site_ids_for_user(request=request)
|
|
19
19
|
for site in Site.objects.filter(id__in=site_ids).order_by("id"):
|
|
20
|
-
names.append((site.id, f"{site.id} {sites.get(site.id).description}"))
|
|
20
|
+
names.append((site.id, f"{site.id} {sites.get(site.id).description}")) # noqa: PERF401
|
|
21
21
|
return tuple(names)
|
|
22
22
|
|
|
23
|
-
def queryset(self, request, queryset):
|
|
23
|
+
def queryset(self, request, queryset): # noqa: ARG002
|
|
24
24
|
if self.value() and self.value() != "none":
|
|
25
25
|
queryset = queryset.filter(site__id=self.value())
|
|
26
26
|
return queryset
|
|
@@ -46,7 +46,7 @@ class SiteModelAdminMixin:
|
|
|
46
46
|
]
|
|
47
47
|
return sites.get_view_only_site_ids_for_user(request=request)
|
|
48
48
|
|
|
49
|
-
def has_viewallsites_permission(self, request, obj=None) -> bool:
|
|
49
|
+
def has_viewallsites_permission(self, request, obj=None) -> bool: # noqa: ARG002
|
|
50
50
|
"""Checks if the user has the EDC custom codename
|
|
51
51
|
"viewallsites" for this model.
|
|
52
52
|
|
|
@@ -54,7 +54,7 @@ class SiteModelAdminMixin:
|
|
|
54
54
|
"""
|
|
55
55
|
opts = self.opts
|
|
56
56
|
codename_allsites = get_permission_codename("viewallsites", opts)
|
|
57
|
-
return request.user.has_perm("
|
|
57
|
+
return request.user.has_perm(f"{opts.app_label}.{codename_allsites}")
|
|
58
58
|
|
|
59
59
|
@admin.display(description="Site", ordering="site__id")
|
|
60
60
|
def site_code(self, obj=None):
|
|
@@ -75,7 +75,7 @@ class SiteModelAdminMixin:
|
|
|
75
75
|
to mulitple sites.
|
|
76
76
|
"""
|
|
77
77
|
list_filter = super().get_list_filter(request)
|
|
78
|
-
list_filter = [x for x in list_filter if x
|
|
78
|
+
list_filter = [x for x in list_filter if x not in ("site", SiteListFilter)]
|
|
79
79
|
if self.user_may_view_other_sites(request) or self.has_viewallsites_permission(
|
|
80
80
|
request
|
|
81
81
|
):
|
|
@@ -95,25 +95,25 @@ class SiteModelAdminMixin:
|
|
|
95
95
|
or self.has_viewallsites_permission(request)
|
|
96
96
|
) and "site" not in list_display:
|
|
97
97
|
list_display = tuple(list_display)
|
|
98
|
-
list_display = list_display[:pos]
|
|
98
|
+
list_display = list_display[:pos], self.site_code, list_display[pos:]
|
|
99
99
|
elif "site" in list_display:
|
|
100
100
|
list_display = tuple(
|
|
101
101
|
[x for x in list_display if x not in ["site", self.site_code]]
|
|
102
102
|
)
|
|
103
|
-
list_display = list_display[:pos]
|
|
103
|
+
list_display = list_display[:pos], self.site_code, list_display[pos:]
|
|
104
104
|
return list_display
|
|
105
105
|
|
|
106
106
|
def get_queryset(self, request) -> QuerySet:
|
|
107
107
|
"""Limit modeladmin queryset for the current site only"""
|
|
108
108
|
qs = super().get_queryset(request)
|
|
109
|
-
site_ids =
|
|
109
|
+
site_ids = (request.site.id, *self.get_view_only_site_ids_for_user(request=request))
|
|
110
110
|
try:
|
|
111
111
|
qs = qs.select_related("site").filter(site_id__in=site_ids)
|
|
112
|
-
except FieldError:
|
|
112
|
+
except FieldError as e:
|
|
113
113
|
raise SiteModeAdminMixinError(
|
|
114
114
|
f"Model missing field `site`. Model `{self.model}`. Did you mean to use "
|
|
115
115
|
f"the SiteModelAdminMixin? See `{self}`."
|
|
116
|
-
)
|
|
116
|
+
) from e
|
|
117
117
|
return qs
|
|
118
118
|
|
|
119
119
|
def get_form(self, request, obj=None, change=False, **kwargs):
|
edc_sites/exceptions.py
CHANGED
edc_sites/forms.py
CHANGED