clinicedc 2.0.24__py3-none-any.whl → 2.0.26__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.26.dist-info}/METADATA +1 -1
- {clinicedc-2.0.24.dist-info → clinicedc-2.0.26.dist-info}/RECORD +58 -57
- 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_navbar/navbar.py +2 -2
- edc_randomization/admin.py +3 -1
- edc_randomization/apps.py +8 -11
- edc_randomization/auth_objects.py +9 -0
- edc_randomization/blinding.py +6 -12
- edc_randomization/decorators.py +2 -3
- edc_randomization/exceptions.py +73 -0
- edc_randomization/model_mixins.py +2 -3
- edc_randomization/randomization_list_importer.py +35 -36
- edc_randomization/randomization_list_verifier.py +19 -21
- edc_randomization/randomizer.py +12 -28
- edc_randomization/site_randomizers.py +2 -16
- edc_randomization/system_checks.py +1 -1
- edc_randomization/utils.py +3 -10
- 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.26.dist-info}/WHEEL +0 -0
- {clinicedc-2.0.24.dist-info → clinicedc-2.0.26.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,17 +6,12 @@ from django.core.exceptions import ObjectDoesNotExist
|
|
|
6
6
|
from django.core.management.color import color_style
|
|
7
7
|
from django.db.utils import OperationalError, ProgrammingError
|
|
8
8
|
|
|
9
|
+
from .exceptions import InvalidAssignment, RandomizationListError
|
|
9
10
|
from .site_randomizers import site_randomizers
|
|
10
11
|
|
|
11
12
|
style = color_style()
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
class RandomizationListError(Exception):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class InvalidAssignment(Exception):
|
|
19
|
-
pass
|
|
14
|
+
__all__ = ["RandomizationListVerifier"]
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
class RandomizationListVerifier:
|
|
@@ -25,12 +20,12 @@ class RandomizationListVerifier:
|
|
|
25
20
|
def __init__(
|
|
26
21
|
self,
|
|
27
22
|
randomizer_name=None,
|
|
28
|
-
randomizationlist_path: Path | str = None,
|
|
23
|
+
randomizationlist_path: Path | str | None = None,
|
|
29
24
|
randomizer_model_cls=None,
|
|
30
25
|
assignment_map=None,
|
|
31
26
|
fieldnames=None,
|
|
32
27
|
sid_count_for_tests=None,
|
|
33
|
-
required_csv_fieldnames:
|
|
28
|
+
required_csv_fieldnames: tuple[str, ...] | None = None,
|
|
34
29
|
**kwargs, # noqa: ARG002
|
|
35
30
|
):
|
|
36
31
|
self.count: int = 0
|
|
@@ -78,28 +73,31 @@ class RandomizationListVerifier:
|
|
|
78
73
|
def verify(self) -> str | None:
|
|
79
74
|
message = None
|
|
80
75
|
|
|
76
|
+
# read and sort from CSV file
|
|
81
77
|
with self.randomizationlist_path.open(mode="r") as f:
|
|
82
78
|
reader = csv.DictReader(f)
|
|
83
79
|
all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
|
|
84
80
|
sorted_rows = sorted(
|
|
85
|
-
all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid",
|
|
81
|
+
all_rows, key=lambda row: (row.get("site_name", ""), int(row.get("sid", 0)))
|
|
86
82
|
)
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
84
|
+
# compare sorted CSV length with DB
|
|
85
|
+
if len(sorted_rows) != self.randomizer_model_cls.objects.all().count():
|
|
86
|
+
expected_cnt = len(sorted_rows)
|
|
87
|
+
actual_cnt = self.randomizer_model_cls.objects.all().count()
|
|
97
88
|
message = (
|
|
98
|
-
f"Randomization list count is off. Expected {
|
|
99
|
-
f"Got {
|
|
89
|
+
f"Randomization list count is off. Expected {expected_cnt} (CSV). "
|
|
90
|
+
f"Got {actual_cnt} (model_cls). See file "
|
|
100
91
|
f"{self.randomizationlist_path}. "
|
|
101
92
|
f"Resolve this issue before using the system."
|
|
102
93
|
)
|
|
94
|
+
if not message:
|
|
95
|
+
# compare sorted CSV data to DB
|
|
96
|
+
for index, row in enumerate(sorted_rows, start=1):
|
|
97
|
+
sys.stdout.write(f"Index: {index}, SID: {row.get('sid')}, Row: {row}\n")
|
|
98
|
+
message = self.inspect_row(index - 1, row)
|
|
99
|
+
if message:
|
|
100
|
+
break
|
|
103
101
|
return message
|
|
104
102
|
|
|
105
103
|
def inspect_row(self, index: int, row) -> str | None:
|
edc_randomization/randomizer.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
|
|
9
9
|
from django.apps import apps as django_apps
|
|
10
10
|
from django.conf import settings
|
|
11
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
11
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
12
12
|
from django.db.models import Q
|
|
13
13
|
|
|
14
14
|
from edc_registration.utils import get_registered_subject_model_cls
|
|
@@ -18,38 +18,22 @@ from .constants import (
|
|
|
18
18
|
DEFAULT_ASSIGNMENT_MAP,
|
|
19
19
|
RANDOMIZED,
|
|
20
20
|
)
|
|
21
|
-
from .
|
|
21
|
+
from .exceptions import (
|
|
22
|
+
AllocationError,
|
|
23
|
+
AlreadyRandomized,
|
|
24
|
+
InvalidAssignmentDescriptionMap,
|
|
25
|
+
RandomizationError,
|
|
22
26
|
RandomizationListAlreadyImported,
|
|
23
|
-
|
|
27
|
+
RandomizationListFileNotFound,
|
|
24
28
|
)
|
|
29
|
+
from .randomization_list_importer import RandomizationListImporter
|
|
25
30
|
from .utils import get_randomization_list_path
|
|
26
31
|
|
|
27
32
|
if TYPE_CHECKING:
|
|
28
33
|
from edc_registration.models import RegisteredSubject
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class RandomizationListFileNotFound(Exception): # noqa: N818
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class RandomizationListNotLoaded(Exception): # noqa: N818
|
|
40
|
-
pass
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class RandomizationError(Exception):
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class AlreadyRandomized(ValidationError): # noqa: N818
|
|
48
|
-
pass
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class AllocationError(Exception):
|
|
52
|
-
pass
|
|
36
|
+
__all__ = ["Randomizer"]
|
|
53
37
|
|
|
54
38
|
|
|
55
39
|
class Randomizer:
|
|
@@ -61,8 +45,8 @@ class Randomizer:
|
|
|
61
45
|
`site_randomizer` by default. To prevent registration set
|
|
62
46
|
settings.EDC_RANDOMIZATION_REGISTER_DEFAULT_RANDOMIZER=False.
|
|
63
47
|
|
|
64
|
-
assignment_map: {<assignment:str
|
|
65
|
-
assignment_description_map: {<assignment:str
|
|
48
|
+
assignment_map: {<assignment:str>: <allocation:int>, ...}
|
|
49
|
+
assignment_description_map: {<assignment:str>: <description:str>, ...}
|
|
66
50
|
|
|
67
51
|
|
|
68
52
|
Usage:
|
|
@@ -101,7 +85,7 @@ class Randomizer:
|
|
|
101
85
|
)
|
|
102
86
|
filename: str = "randomization_list.csv"
|
|
103
87
|
randomizationlist_folder: Path | str = get_randomization_list_path()
|
|
104
|
-
extra_csv_fieldnames:
|
|
88
|
+
extra_csv_fieldnames: tuple[str] | None = None
|
|
105
89
|
trial_is_blinded: bool = True
|
|
106
90
|
importer_cls: Any = RandomizationListImporter
|
|
107
91
|
apps = None # if not using django_apps
|
|
@@ -7,26 +7,12 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
from django.apps import apps as django_apps
|
|
8
8
|
from django.utils.module_loading import import_module, module_has_submodule
|
|
9
9
|
|
|
10
|
+
from .exceptions import AlreadyRegistered, NotRegistered, RegistryNotLoaded
|
|
11
|
+
|
|
10
12
|
if TYPE_CHECKING:
|
|
11
13
|
from edc_randomization.randomizer import Randomizer
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
class RegistryNotLoaded(Exception):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class NotRegistered(Exception):
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AlreadyRegistered(Exception):
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class SiteRandomizerError(Exception):
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
|
|
30
16
|
class SiteRandomizers:
|
|
31
17
|
"""Main controller of :class:`SiteRandomizers` objects.
|
|
32
18
|
|
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
|
-
from django.core.checks import Error, Warning
|
|
6
|
+
from django.core.checks import Error, Warning # noqa: A004
|
|
7
7
|
from django.core.exceptions import ImproperlyConfigured
|
|
8
8
|
from django.core.management import color_style
|
|
9
9
|
|
edc_randomization/utils.py
CHANGED
|
@@ -14,17 +14,10 @@ from django_pandas.io import read_frame
|
|
|
14
14
|
from edc_pdutils.constants import SYSTEM_COLUMNS
|
|
15
15
|
from edc_sites.site import sites
|
|
16
16
|
|
|
17
|
+
from .exceptions import RandomizationListExporterError, SubjectNotRandomization
|
|
17
18
|
from .site_randomizers import site_randomizers
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class RandomizationListExporterError(Exception):
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class SubjectNotRandomization(Exception): # noqa: N818
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
21
|
def get_randomization_list_path() -> Path:
|
|
29
22
|
return Path(
|
|
30
23
|
getattr(
|
|
@@ -149,7 +142,7 @@ def generate_fake_randomization_list(
|
|
|
149
142
|
|
|
150
143
|
def export_randomization_list(
|
|
151
144
|
randomizer_name: str, path: str | None = None, username: str | None = None
|
|
152
|
-
):
|
|
145
|
+
) -> Path:
|
|
153
146
|
randomizer_cls = site_randomizers.get(randomizer_name)
|
|
154
147
|
|
|
155
148
|
try:
|
|
@@ -184,4 +177,4 @@ def export_randomization_list(
|
|
|
184
177
|
)
|
|
185
178
|
df.to_csv(**opts)
|
|
186
179
|
sys.stdout.write(f"{filename!s}\n")
|
|
187
|
-
return filename
|
|
180
|
+
return Path(filename)
|
|
@@ -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
|
@@ -4,6 +4,8 @@ import sys
|
|
|
4
4
|
from django.conf import settings
|
|
5
5
|
from django.core.management.base import BaseCommand
|
|
6
6
|
from django.core.management.color import color_style
|
|
7
|
+
from multisite.models import Alias
|
|
8
|
+
from multisite.utils import create_or_sync_canonical_from_all_sites
|
|
7
9
|
|
|
8
10
|
from edc_sites.site import sites as site_sites
|
|
9
11
|
from edc_sites.utils import add_or_update_django_sites
|
|
@@ -15,7 +17,6 @@ class Command(BaseCommand):
|
|
|
15
17
|
help = "Add / Update django Site model after changes to edc_sites"
|
|
16
18
|
|
|
17
19
|
def add_arguments(self, parser):
|
|
18
|
-
|
|
19
20
|
parser.add_argument(
|
|
20
21
|
"--ping-hosts",
|
|
21
22
|
default=False,
|
|
@@ -32,26 +33,19 @@ class Command(BaseCommand):
|
|
|
32
33
|
help="Suggest ALLOWED_HOSTS",
|
|
33
34
|
)
|
|
34
35
|
|
|
35
|
-
def handle(self, *args, **options) -> None:
|
|
36
|
+
def handle(self, *args, **options) -> None: # noqa: ARG002
|
|
36
37
|
sys.stdout.write("\n\n")
|
|
37
38
|
sys.stdout.write(" Edc Sites : Adding / Updating sites ... \n")
|
|
38
39
|
add_or_update_django_sites(verbose=True)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
or "multisite.apps.AppConfig" in settings.INSTALLED_APPS
|
|
42
|
-
):
|
|
43
|
-
from multisite.models import Alias
|
|
44
|
-
from multisite.utils import create_or_sync_canonical_from_all_sites
|
|
45
|
-
|
|
46
|
-
sys.stdout.write("\n Multisite. \n")
|
|
47
|
-
create_or_sync_canonical_from_all_sites(verbose=True)
|
|
40
|
+
sys.stdout.write("\n Multisite. \n")
|
|
41
|
+
create_or_sync_canonical_from_all_sites(verbose=True)
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
sys.stdout.write(" multisite.Alias \n")
|
|
44
|
+
for obj in Alias.objects.all():
|
|
45
|
+
sys.stdout.write(
|
|
46
|
+
f" - Site model: {obj.site.id}: {obj.domain} "
|
|
47
|
+
f"is_canonical={obj.is_canonical}.\n"
|
|
48
|
+
)
|
|
55
49
|
arg = [arg for arg in sys.argv if arg.startswith("--settings")]
|
|
56
50
|
sys.stdout.write(f"\n Settings: reading from {arg}")
|
|
57
51
|
sys.stdout.write("\n\n Current value of settings.ALLOWED_HOSTS\n")
|
edc_sites/models/site_profile.py
CHANGED
|
@@ -5,17 +5,19 @@ import json
|
|
|
5
5
|
from django.contrib.sites.models import Site
|
|
6
6
|
from django.db import models
|
|
7
7
|
|
|
8
|
+
from edc_constants.constants import NULL_STRING
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class SiteProfile(models.Model):
|
|
10
12
|
id = models.BigAutoField(primary_key=True)
|
|
11
13
|
|
|
12
|
-
country = models.CharField(max_length=250, default=
|
|
14
|
+
country = models.CharField(max_length=250, default=NULL_STRING)
|
|
13
15
|
|
|
14
|
-
country_code = models.CharField(max_length=15, default=
|
|
16
|
+
country_code = models.CharField(max_length=15, default=NULL_STRING)
|
|
15
17
|
|
|
16
|
-
languages = models.TextField(default=
|
|
18
|
+
languages = models.TextField(default=NULL_STRING)
|
|
17
19
|
|
|
18
|
-
title = models.CharField(max_length=250, default=
|
|
20
|
+
title = models.CharField(max_length=250, default=NULL_STRING)
|
|
19
21
|
|
|
20
22
|
site = models.OneToOneField(Site, on_delete=models.PROTECT)
|
|
21
23
|
|