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
edc_identifier/models.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
from django.db.models import UniqueConstraint
|
|
3
3
|
|
|
4
|
+
from edc_constants.constants import NULL_STRING
|
|
4
5
|
from edc_model.models import BaseUuidModel
|
|
5
6
|
from edc_sites.model_mixins import SiteModelMixin
|
|
6
7
|
|
|
@@ -25,21 +26,21 @@ class IdentifierModel(SiteModelMixin, BaseUuidModel):
|
|
|
25
26
|
|
|
26
27
|
name = models.CharField(max_length=100)
|
|
27
28
|
|
|
28
|
-
subject_identifier = models.CharField(max_length=50, default=
|
|
29
|
+
subject_identifier = models.CharField(max_length=50, default=NULL_STRING)
|
|
29
30
|
|
|
30
31
|
sequence_number = models.IntegerField(default=1)
|
|
31
32
|
|
|
32
|
-
linked_identifier = models.CharField(max_length=50, default=
|
|
33
|
+
linked_identifier = models.CharField(max_length=50, default=NULL_STRING)
|
|
33
34
|
|
|
34
35
|
device_id = models.IntegerField()
|
|
35
36
|
|
|
36
|
-
protocol_number = models.CharField(max_length=25, default=
|
|
37
|
+
protocol_number = models.CharField(max_length=25, default=NULL_STRING)
|
|
37
38
|
|
|
38
|
-
model = models.CharField(max_length=100, default=
|
|
39
|
+
model = models.CharField(max_length=100, default=NULL_STRING)
|
|
39
40
|
|
|
40
|
-
identifier_type = models.CharField(max_length=100, default=
|
|
41
|
+
identifier_type = models.CharField(max_length=100, default=NULL_STRING)
|
|
41
42
|
|
|
42
|
-
identifier_prefix = models.CharField(max_length=25, default=
|
|
43
|
+
identifier_prefix = models.CharField(max_length=25, default=NULL_STRING)
|
|
43
44
|
|
|
44
45
|
objects = IdentifierModelManager()
|
|
45
46
|
|
|
@@ -21,7 +21,7 @@ class IdentifierMissingTemplateValue(Exception): # noqa: N818
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class ResearchIdentifier:
|
|
24
|
-
label: str = None # e.g. subject_identifier, plot_identifier, etc
|
|
24
|
+
label: str | None = None # e.g. subject_identifier, plot_identifier, etc
|
|
25
25
|
identifier_type: str | None = None # e.g. 'subject', 'infant', 'plot', a.k.a subject_type
|
|
26
26
|
template: str | None = None
|
|
27
27
|
padding: int = 5
|
|
@@ -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)
|
edc_navbar/navbar.py
CHANGED
|
@@ -29,7 +29,7 @@ class Navbar:
|
|
|
29
29
|
|
|
30
30
|
def get(self, name: str) -> NavbarItem | None:
|
|
31
31
|
try:
|
|
32
|
-
navbar_item =
|
|
32
|
+
navbar_item = next(nb for nb in self.navbar_items if nb.name == name)
|
|
33
33
|
except IndexError:
|
|
34
34
|
navbar_item = None
|
|
35
35
|
return navbar_item
|
|
@@ -37,7 +37,7 @@ class Navbar:
|
|
|
37
37
|
def set_active(self, name: str) -> None:
|
|
38
38
|
if name:
|
|
39
39
|
for navbar_item in self.navbar_items:
|
|
40
|
-
navbar_item.active =
|
|
40
|
+
navbar_item.active = navbar_item.name == name
|
|
41
41
|
|
|
42
42
|
def show_user_permissions(self, user: User = None) -> dict[str, dict[str, bool]]:
|
|
43
43
|
"""Returns the permissions required to access this Navbar
|
edc_randomization/admin.py
CHANGED
|
@@ -17,6 +17,8 @@ from .auth_objects import RANDO_UNBLINDED
|
|
|
17
17
|
from .blinding import user_is_blinded
|
|
18
18
|
from .site_randomizers import site_randomizers
|
|
19
19
|
|
|
20
|
+
__all__ = ["RandomizationListModelAdmin", "print_pharmacy_labels"]
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
@admin.action(permissions=["view"], description="Print labels for pharmacy")
|
|
22
24
|
def print_pharmacy_labels(modeladmin, request, queryset):
|
|
@@ -59,7 +61,7 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
|
|
|
59
61
|
|
|
60
62
|
search_fields = ("subject_identifier", "sid")
|
|
61
63
|
|
|
62
|
-
def get_fieldsets(self, request, obj=None):
|
|
64
|
+
def get_fieldsets(self, request, obj=None): # noqa: ARG002
|
|
63
65
|
return (
|
|
64
66
|
(None, {"fields": self.get_fieldnames(request)}),
|
|
65
67
|
audit_fieldset_tuple,
|
edc_randomization/apps.py
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from warnings import warn
|
|
3
|
-
|
|
4
1
|
from django.apps import AppConfig as DjangoAppConfig
|
|
5
|
-
from django.conf import settings
|
|
6
2
|
from django.core.checks.registry import register
|
|
7
3
|
|
|
8
4
|
from .system_checks import randomizationlist_check
|
|
@@ -17,10 +13,11 @@ class AppConfig(DjangoAppConfig):
|
|
|
17
13
|
def ready(self):
|
|
18
14
|
register(randomizationlist_check, deploy=True)
|
|
19
15
|
|
|
20
|
-
@property
|
|
21
|
-
def randomization_list_path(self):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
# @property
|
|
17
|
+
# def randomization_list_path(self):
|
|
18
|
+
# warn(
|
|
19
|
+
# "Use of settings.RANDOMIZATION_LIST_PATH has been deprecated. "
|
|
20
|
+
# "See site_randomizers in edc_randomization",
|
|
21
|
+
# stacklevel=2,
|
|
22
|
+
# )
|
|
23
|
+
# return os.path.join(settings.RANDOMIZATION_LIST_PATH)
|
|
@@ -2,6 +2,15 @@ import sys
|
|
|
2
2
|
|
|
3
3
|
from .site_randomizers import site_randomizers
|
|
4
4
|
|
|
5
|
+
__all__ = [
|
|
6
|
+
"RANDO_BLINDED",
|
|
7
|
+
"RANDO_UNBLINDED",
|
|
8
|
+
"get_rando_permissions_codenames",
|
|
9
|
+
"get_rando_permissions_tuples",
|
|
10
|
+
"make_randomizationlist_view_only",
|
|
11
|
+
"update_rando_group_permissions",
|
|
12
|
+
]
|
|
13
|
+
|
|
5
14
|
RANDO_UNBLINDED = "RANDO_UNBLINDED"
|
|
6
15
|
RANDO_BLINDED = "RANDO_BLINDED"
|
|
7
16
|
|
edc_randomization/blinding.py
CHANGED
|
@@ -4,7 +4,6 @@ from django import forms
|
|
|
4
4
|
from django.conf import settings
|
|
5
5
|
from django.contrib.auth import get_user_model
|
|
6
6
|
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
|
7
|
-
from django.utils.html import format_html
|
|
8
7
|
from django.utils.safestring import mark_safe
|
|
9
8
|
|
|
10
9
|
from .auth_objects import RANDO_UNBLINDED
|
|
@@ -43,13 +42,11 @@ def user_is_blinded(username) -> bool:
|
|
|
43
42
|
return blinded
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def user_is_blinded_from_request(request):
|
|
47
|
-
|
|
45
|
+
def user_is_blinded_from_request(request) -> bool:
|
|
46
|
+
return user_is_blinded(request.user.username) or (
|
|
48
47
|
not user_is_blinded(request.user.username)
|
|
49
48
|
and RANDO_UNBLINDED not in [g.name for g in request.user.groups.all()]
|
|
50
|
-
)
|
|
51
|
-
return True
|
|
52
|
-
return False
|
|
49
|
+
)
|
|
53
50
|
|
|
54
51
|
|
|
55
52
|
def raise_if_prohibited_from_unblinded_rando_group(username: str, groups: Iterable) -> None:
|
|
@@ -61,12 +58,9 @@ def raise_if_prohibited_from_unblinded_rando_group(username: str, groups: Iterab
|
|
|
61
58
|
if RANDO_UNBLINDED in [grp.name for grp in groups] and user_is_blinded(username):
|
|
62
59
|
raise forms.ValidationError(
|
|
63
60
|
{
|
|
64
|
-
"groups":
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"This user is not unblinded and may not added "
|
|
68
|
-
"to the <U>RANDO_UNBLINDED</U> group."
|
|
69
|
-
), # nosec B703 B308
|
|
61
|
+
"groups": mark_safe(
|
|
62
|
+
"This user is not unblinded and may not added "
|
|
63
|
+
"to the <U>RANDO_UNBLINDED</U> group."
|
|
70
64
|
)
|
|
71
65
|
}
|
|
72
66
|
)
|
edc_randomization/decorators.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from .exceptions import RegisterRandomizerError
|
|
3
4
|
from .randomizer import Randomizer
|
|
4
5
|
from .site_randomizers import site_randomizers
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
class RegisterRandomizerError(Exception):
|
|
8
|
-
pass
|
|
7
|
+
__all__ = ["register"]
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
def register(site=None, **kwargs) -> Any:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from django.core.exceptions import ValidationError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InvalidAssignment(Exception): # noqa: N818
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RandomizationListImportError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RandomizationListAlreadyImported(Exception): # noqa: N818
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RandomizationListError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RegistryNotLoaded(Exception): # noqa: N818
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NotRegistered(Exception): # noqa: N818
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AlreadyRegistered(Exception): # noqa: N818
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SiteRandomizerError(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RandomizationListExporterError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SubjectNotRandomization(Exception): # noqa: N818
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InvalidAssignmentDescriptionMap(Exception): # noqa: N818
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RandomizationListFileNotFound(Exception): # noqa: N818
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RandomizationListNotLoaded(Exception): # noqa: N818
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RandomizationError(Exception):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AlreadyRandomized(ValidationError): # noqa: N818
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AllocationError(Exception):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RegisterRandomizerError(Exception):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RandomizationListModelError(Exception):
|
|
73
|
+
pass
|
|
@@ -7,12 +7,11 @@ from django_crypto_fields.fields import EncryptedCharField
|
|
|
7
7
|
from edc_model.models import HistoricalRecords
|
|
8
8
|
from edc_sites.managers import CurrentSiteManager
|
|
9
9
|
|
|
10
|
+
from .exceptions import RandomizationListModelError
|
|
10
11
|
from .randomizer import RandomizationError
|
|
11
12
|
from .site_randomizers import site_randomizers
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
class RandomizationListModelError(Exception):
|
|
15
|
-
pass
|
|
14
|
+
__all__ = ["RandomizationListManager", "RandomizationListModelMixin"]
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class RandomizationListManager(models.Manager):
|
|
@@ -12,21 +12,16 @@ from tqdm import tqdm
|
|
|
12
12
|
|
|
13
13
|
from edc_sites.site import sites as site_sites
|
|
14
14
|
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
InvalidAssignment,
|
|
17
|
+
RandomizationListAlreadyImported,
|
|
18
|
+
RandomizationListImportError,
|
|
19
|
+
)
|
|
15
20
|
from .randomization_list_verifier import RandomizationListVerifier
|
|
16
21
|
|
|
17
22
|
style = color_style()
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
class RandomizationListImportError(Exception):
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class RandomizationListAlreadyImported(Exception):
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class InvalidAssignment(Exception):
|
|
29
|
-
pass
|
|
24
|
+
__all__ = ["RandomizationListImporter"]
|
|
30
25
|
|
|
31
26
|
|
|
32
27
|
class RandomizationListImporter:
|
|
@@ -67,19 +62,20 @@ class RandomizationListImporter:
|
|
|
67
62
|
def __init__(
|
|
68
63
|
self,
|
|
69
64
|
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,
|
|
65
|
+
randomizer_name: str | None = None,
|
|
66
|
+
randomizationlist_path: Path | str | None = None,
|
|
67
|
+
assignment_map: dict[str, int] | None = None,
|
|
68
|
+
verbose: bool | None = None,
|
|
69
|
+
overwrite: bool | None = None,
|
|
70
|
+
add: bool | None = None,
|
|
71
|
+
dryrun: bool | None = None,
|
|
72
|
+
username: str | None = None,
|
|
73
|
+
revision: str | None = None,
|
|
74
|
+
sid_count_for_tests: int | None = None,
|
|
75
|
+
extra_csv_fieldnames: tuple[str] | None = None,
|
|
76
|
+
**kwargs, # noqa: ARG002
|
|
82
77
|
):
|
|
78
|
+
extra_csv_fieldnames = extra_csv_fieldnames or ()
|
|
83
79
|
self.verify_messages: str | None = None
|
|
84
80
|
self.add = add
|
|
85
81
|
self.overwrite = overwrite
|
|
@@ -92,7 +88,7 @@ class RandomizationListImporter:
|
|
|
92
88
|
self.randomizer_name = randomizer_name
|
|
93
89
|
self.assignment_map = assignment_map
|
|
94
90
|
self.randomizationlist_path: Path = Path(randomizationlist_path).expanduser()
|
|
95
|
-
self.required_csv_fieldnames.
|
|
91
|
+
self.required_csv_fieldnames = (*self.required_csv_fieldnames, *extra_csv_fieldnames)
|
|
96
92
|
|
|
97
93
|
if self.dryrun:
|
|
98
94
|
sys.stdout.write(
|
|
@@ -156,7 +152,7 @@ class RandomizationListImporter:
|
|
|
156
152
|
index = 0
|
|
157
153
|
with self.randomizationlist_path.open(mode="r") as csvfile:
|
|
158
154
|
reader = csv.DictReader(csvfile)
|
|
159
|
-
for index,
|
|
155
|
+
for index, _ in enumerate(reader):
|
|
160
156
|
if index == 0:
|
|
161
157
|
continue
|
|
162
158
|
if index == 0:
|
|
@@ -178,11 +174,11 @@ class RandomizationListImporter:
|
|
|
178
174
|
elif index == 1:
|
|
179
175
|
if self.dryrun:
|
|
180
176
|
row_as_dict = {k: v for k, v in row.items()}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
sys.stdout.write(" --> First row:\n")
|
|
178
|
+
sys.stdout.write(f" --> {list(row_as_dict.keys())}\n")
|
|
179
|
+
sys.stdout.write(f" --> {list(row_as_dict.values())}\n")
|
|
184
180
|
obj = self.randomizer_model_cls(**self.get_import_options(row))
|
|
185
|
-
pprint(obj.__dict__)
|
|
181
|
+
pprint(obj.__dict__) # noqa: T203
|
|
186
182
|
else:
|
|
187
183
|
break
|
|
188
184
|
|
|
@@ -223,12 +219,15 @@ class RandomizationListImporter:
|
|
|
223
219
|
sid_count = self.sid_count_for_tests
|
|
224
220
|
else:
|
|
225
221
|
sid_count = len(self.get_sid_list())
|
|
226
|
-
with self.randomizationlist_path.open(mode="r") as
|
|
227
|
-
reader = csv.DictReader(
|
|
228
|
-
for row in
|
|
222
|
+
with self.randomizationlist_path.open(mode="r") as f:
|
|
223
|
+
reader = csv.DictReader(f)
|
|
224
|
+
all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
|
|
225
|
+
sorted_rows = sorted(
|
|
226
|
+
all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
|
|
227
|
+
)
|
|
228
|
+
for row in tqdm(sorted_rows, total=sid_count):
|
|
229
229
|
if self.sid_count_for_tests and len(objs) == self.sid_count_for_tests:
|
|
230
230
|
break
|
|
231
|
-
row = {k: v.strip() for k, v in row.items()}
|
|
232
231
|
try:
|
|
233
232
|
self.randomizer_model_cls.objects.get(sid=row["sid"])
|
|
234
233
|
except ObjectDoesNotExist:
|
|
@@ -300,7 +299,7 @@ class RandomizationListImporter:
|
|
|
300
299
|
**self.get_extra_import_options(row),
|
|
301
300
|
)
|
|
302
301
|
|
|
303
|
-
def get_extra_import_options(self, row):
|
|
302
|
+
def get_extra_import_options(self, row): # noqa: ARG002
|
|
304
303
|
return {}
|
|
305
304
|
|
|
306
305
|
@staticmethod
|
|
@@ -314,9 +313,9 @@ class RandomizationListImporter:
|
|
|
314
313
|
"""Returns the site name or raises"""
|
|
315
314
|
try:
|
|
316
315
|
site_name = self.get_site_names()[row["site_name"].lower()]
|
|
317
|
-
except KeyError:
|
|
316
|
+
except KeyError as e:
|
|
318
317
|
raise RandomizationListImportError(
|
|
319
318
|
f"Invalid site. Got {row['site_name']}. "
|
|
320
319
|
f"Expected one of {self.get_site_names().keys()}"
|
|
321
|
-
)
|
|
320
|
+
) from e
|
|
322
321
|
return site_name
|