clinicedc 2.0.7__py3-none-any.whl → 2.0.9__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.7.dist-info → clinicedc-2.0.9.dist-info}/METADATA +4 -3
- {clinicedc-2.0.7.dist-info → clinicedc-2.0.9.dist-info}/RECORD +136 -137
- {clinicedc-2.0.7.dist-info → clinicedc-2.0.9.dist-info}/WHEEL +1 -1
- edc_action_item/auths.py +37 -32
- edc_action_item/models/action_model_mixin.py +1 -2
- edc_action_item/models/signals.py +22 -23
- edc_action_item/site_action_items.py +5 -9
- edc_action_item/utils.py +3 -3
- edc_adverse_event/auths.py +55 -51
- edc_adverse_event/model_mixins/ae_tmg/ae_tmg_methods_model_mixin.py +2 -4
- edc_appointment/auths.py +14 -10
- edc_appointment/creators/appointment_creator.py +1 -1
- edc_appointment/creators/appointments_creator.py +1 -1
- edc_appointment/model_mixins/appointment_methods_model_mixin.py +2 -3
- edc_appointment/model_mixins/appointment_model_mixin.py +31 -28
- edc_appointment/models/appointment.py +1 -1
- edc_appointment/utils.py +19 -24
- edc_auth/auth_objects/__init__.py +2 -20
- edc_auth/auth_objects/default_groups.py +13 -11
- edc_auth/auth_objects/default_roles.py +26 -24
- edc_auth/auth_updater/auth_updater.py +13 -2
- edc_auth/auth_updater/group_updater.py +12 -10
- edc_auth/auth_updater/role_updater.py +2 -2
- edc_auth/constants.py +10 -0
- edc_auth/import_users.py +3 -3
- edc_auth/migrations/0036_alter_userprofile_alternate_email_and_more.py +88 -0
- edc_auth/models/user_profile.py +14 -11
- edc_auth/site_auths.py +80 -67
- edc_consent/auths.py +18 -12
- edc_constants/constants.py +1 -0
- edc_crf/auths.py +5 -0
- edc_dashboard/auths.py +10 -6
- edc_dashboard/url_config.py +92 -83
- edc_dashboard/url_names.py +4 -4
- edc_dashboard/view_mixins/url_request_context_mixin.py +6 -5
- edc_data_manager/admin/data_query_admin.py +12 -11
- edc_data_manager/auths.py +37 -34
- edc_data_manager/rule/query_rule_wrapper.py +7 -7
- edc_export/archive_exporter.py +3 -2
- edc_export/auths.py +32 -28
- edc_export/model_exporter/model_exporter.py +4 -1
- edc_facility/auths.py +8 -3
- edc_facility/facility.py +8 -9
- edc_form_label/custom_label_condition.py +11 -8
- edc_form_label/form_label.py +1 -1
- edc_form_runners/auths.py +11 -6
- edc_form_validators/applicable_field_validator.py +7 -6
- edc_form_validators/base_form_validator.py +8 -9
- edc_form_validators/other_specify_field_validator.py +2 -8
- edc_form_validators/required_field_validator.py +19 -16
- edc_identifier/research_identifier.py +11 -10
- edc_identifier/simple_identifier.py +8 -2
- edc_lab/auths.py +26 -23
- edc_lab/lab/aliquot_creator.py +5 -8
- edc_lab/lab/primary_aliquot.py +14 -5
- edc_lab/migrations/0038_alter_aliquot_slug_alter_box_slug_alter_boxitem_slug_and_more.py +112 -0
- edc_lab/model_mixins/requisition/requisition_model_mixin.py +6 -8
- edc_lab/models/aliquot.py +2 -2
- edc_lab/models/manifest/manifest.py +2 -2
- edc_lab/models/manifest/manifest_item.py +1 -1
- edc_lab_dashboard/auths.py +16 -11
- edc_lab_results/calculate_missing.py +8 -8
- edc_lab_results/form_validator_mixins/blood_results_form_validator_mixin.py +2 -2
- edc_lab_results/get_summary.py +26 -25
- edc_lab_results/model_mixins/blood_result_model_mixin.py +2 -0
- edc_label/auths.py +6 -1
- edc_label/label_template.py +8 -8
- edc_list_data/load_model_data.py +3 -3
- edc_list_data/post_migrate_signals.py +1 -1
- edc_list_data/preload_data.py +2 -2
- edc_list_data/row.py +1 -1
- edc_list_data/site_list_data.py +6 -5
- edc_locator/auths.py +18 -13
- edc_metadata/auths.py +11 -7
- edc_metadata/metadata/metadata.py +1 -1
- edc_metadata/metadata_rules/crf/crf_rule.py +1 -1
- edc_metadata/metadata_rules/metadata_rule_evaluator.py +5 -3
- edc_metadata/metadata_rules/rule.py +2 -3
- edc_metadata/metadata_rules/rule_evaluator.py +1 -1
- edc_metadata/model_mixins/updates/updates_crf_metadata_model_mixin.py +7 -4
- edc_metadata/model_mixins/updates/updates_requisition_metadata_model_mixin.py +5 -2
- edc_metadata/models/signals.py +10 -11
- edc_navbar/auths.py +18 -13
- edc_notification/auths.py +9 -4
- edc_notification/notification/graded_event_notification.py +2 -2
- edc_notification/notification/model_notification.py +3 -30
- edc_notification/notification/new_model_notification.py +1 -1
- edc_notification/notification/notification.py +1 -1
- edc_notification/notification/updated_model_notification.py +2 -2
- edc_offstudy/auths.py +12 -7
- edc_pdutils/df_exporters/csv_model_exporter.py +5 -2
- edc_pharmacy/auths.py +19 -15
- edc_pharmacy/models/medication/formulation.py +5 -7
- edc_pharmacy/prescribe/create_prescription.py +3 -3
- edc_pharmacy/utils/confirm_stock.py +1 -1
- edc_pharmacy/utils/confirm_stock_at_site.py +1 -1
- edc_pharmacy/views/confirmation_at_site_view.py +6 -9
- edc_prn/admin_site.py +5 -0
- edc_prn/prn.py +10 -11
- edc_prn/urls.py +11 -0
- edc_protocol_incident/action_items.py +4 -4
- edc_protocol_incident/auths.py +27 -20
- edc_pylabels/auths.py +6 -1
- edc_qareports/auths.py +11 -7
- edc_randomization/admin.py +30 -24
- edc_randomization/auths.py +12 -7
- edc_randomization/randomizer.py +22 -20
- edc_randomization/utils.py +17 -16
- edc_refusal/auths.py +7 -2
- edc_refusal/model_mixins.py +1 -1
- edc_registration/auths.py +28 -23
- edc_registration/model_mixins/updates_or_creates_registered_subject_model_mixin.py +13 -4
- edc_registration/models/registered_subject.py +1 -1
- edc_reportable/utils/get_reference_range_collection.py +2 -3
- edc_reportable/utils/load_data.py +1 -1
- edc_review_dashboard/auths.py +23 -18
- edc_screening/age_evaluator.py +3 -3
- edc_screening/auths.py +35 -30
- edc_screening/eligibility.py +1 -1
- edc_screening/gender_evaluator.py +1 -1
- edc_screening/model_mixins/eligibility_model_mixin.py +0 -2
- edc_screening/model_mixins/screening_methods_model_mixin.py +1 -1
- edc_screening/screening_eligibility.py +2 -4
- edc_screening/utils.py +9 -9
- edc_search/generate_slug.py +26 -0
- edc_search/model_mixins.py +10 -21
- edc_sites/auths.py +8 -3
- edc_subject_dashboard/auths.py +27 -22
- edc_timepoint/apps.py +0 -21
- edc_unblinding/auths.py +9 -4
- edc_utils/__init__.py +3 -1
- edc_utils/show_urls.py +29 -2
- edc_visit_schedule/auths.py +6 -1
- edc_visit_schedule/site_visit_schedules.py +2 -2
- edc_visit_tracking/models/signals.py +2 -2
- edc_form_label/models.py +0 -0
- edc_search/constants.py +0 -1
- edc_search/models.py +0 -0
- edc_search/search_slug.py +0 -51
- edc_search/updater.py +0 -30
- edc_search/wsgi.py +0 -7
- {clinicedc-2.0.7.dist-info → clinicedc-2.0.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -26,7 +26,7 @@ def confirm_stock_at_site(
|
|
|
26
26
|
stock_transfer: StockTransfer,
|
|
27
27
|
stock_codes: list[str],
|
|
28
28
|
location: UUID,
|
|
29
|
-
request: WSGIRequest = None,
|
|
29
|
+
request: WSGIRequest | None = None,
|
|
30
30
|
) -> tuple[list[str], list[str], list[str]]:
|
|
31
31
|
"""Confirm stock instances given a list of stock codes
|
|
32
32
|
and a request/receive pk.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import uuid
|
|
4
5
|
from uuid import uuid4
|
|
5
6
|
|
|
@@ -97,13 +98,12 @@ class ConfirmationAtSiteView(
|
|
|
97
98
|
return min(unconfirmed_count, 12)
|
|
98
99
|
|
|
99
100
|
def get_stock_codes(self, stock_transfer):
|
|
100
|
-
|
|
101
|
+
return [
|
|
101
102
|
code
|
|
102
103
|
for code in stock_transfer.stocktransferitem_set.values_list(
|
|
103
104
|
"stock__code", flat=True
|
|
104
105
|
).all()
|
|
105
106
|
]
|
|
106
|
-
return stock_codes
|
|
107
107
|
|
|
108
108
|
def get_unconfirmed_count(self, stock_transfer) -> int:
|
|
109
109
|
return (
|
|
@@ -120,10 +120,8 @@ class ConfirmationAtSiteView(
|
|
|
120
120
|
def site(self) -> Site | None:
|
|
121
121
|
obj = None
|
|
122
122
|
if self.kwargs.get("site_id"):
|
|
123
|
-
|
|
123
|
+
with contextlib.suppress(ObjectDoesNotExist):
|
|
124
124
|
obj = Site.objects.get(id=self.kwargs.get("site_id"))
|
|
125
|
-
except ObjectDoesNotExist:
|
|
126
|
-
pass
|
|
127
125
|
return obj
|
|
128
126
|
|
|
129
127
|
@property
|
|
@@ -169,14 +167,13 @@ class ConfirmationAtSiteView(
|
|
|
169
167
|
def confirmation_at_site_changelist_url(self) -> str:
|
|
170
168
|
if self.confirmation_at_site:
|
|
171
169
|
url = reverse("edc_pharmacy_admin:edc_pharmacy_confirmationatsite_changelist")
|
|
172
|
-
|
|
173
|
-
return url
|
|
170
|
+
return f"{url}?q={self.confirmation_at_site.transfer_confirmation_identifier}"
|
|
174
171
|
return "/"
|
|
175
172
|
|
|
176
173
|
def get_stock_transfer(
|
|
177
174
|
self,
|
|
178
175
|
stock_transfer_identifier: str,
|
|
179
|
-
suppress_msg: bool = None,
|
|
176
|
+
suppress_msg: bool | None = None,
|
|
180
177
|
) -> StockTransfer | None:
|
|
181
178
|
stock_transfer = None
|
|
182
179
|
try:
|
|
@@ -198,7 +195,7 @@ class ConfirmationAtSiteView(
|
|
|
198
195
|
)
|
|
199
196
|
return stock_transfer
|
|
200
197
|
|
|
201
|
-
def post(self, request, *args, **kwargs) -> HttpResponseRedirect:
|
|
198
|
+
def post(self, request, *args, **kwargs) -> HttpResponseRedirect: # noqa: ARG002
|
|
202
199
|
# cancel
|
|
203
200
|
if request.POST.get("cancel") and request.POST.get("cancel") == "cancel":
|
|
204
201
|
url = reverse("edc_pharmacy:home_url")
|
edc_prn/admin_site.py
ADDED
edc_prn/prn.py
CHANGED
|
@@ -58,19 +58,18 @@ class Prn:
|
|
|
58
58
|
try:
|
|
59
59
|
return django_apps.get_model(self.model)
|
|
60
60
|
except LookupError as e:
|
|
61
|
-
raise PrnError(f"{e}. See {self!r}")
|
|
61
|
+
raise PrnError(f"{e}. See {self!r}") from e
|
|
62
62
|
|
|
63
|
-
def get_show_on_dashboard(self, subject_identifier=None, **kwargs):
|
|
63
|
+
def get_show_on_dashboard(self, subject_identifier=None, **kwargs): # noqa: ARG002
|
|
64
64
|
count = 0
|
|
65
|
-
if self.show_on_dashboard:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return True if count and self.show_on_dashboard else False
|
|
65
|
+
if self.show_on_dashboard and subject_identifier:
|
|
66
|
+
opts = dict(subject_identifier=subject_identifier)
|
|
67
|
+
try:
|
|
68
|
+
count = self.model_cls.objects.filter(**opts).count()
|
|
69
|
+
except FieldError:
|
|
70
|
+
opts = self.get_query_opts(subject_identifier)
|
|
71
|
+
count = self.model_cls.objects.filter(**opts).count()
|
|
72
|
+
return count and self.show_on_dashboard
|
|
74
73
|
|
|
75
74
|
def get_query_opts(self, subject_identifier):
|
|
76
75
|
"""Returns alternative query opts to search on
|
edc_prn/urls.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from django.views.generic.base import RedirectView
|
|
3
|
+
|
|
4
|
+
from .admin_site import edc_prn_admin
|
|
5
|
+
|
|
6
|
+
app_name = "edc_prn"
|
|
7
|
+
|
|
8
|
+
urlpatterns = [
|
|
9
|
+
path("admin/", edc_prn_admin.urls),
|
|
10
|
+
path("", RedirectView.as_view(url="admin/"), name="home_url"),
|
|
11
|
+
]
|
|
@@ -9,8 +9,8 @@ from .constants import (
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ProtocolDeviationViolationAction(ActionWithNotification):
|
|
12
|
-
reference_model =
|
|
13
|
-
admin_site_name =
|
|
12
|
+
reference_model = "edc_protocol_incident.protocoldeviationviolation"
|
|
13
|
+
admin_site_name = "edc_protocol_incident_admin"
|
|
14
14
|
|
|
15
15
|
name = PROTOCOL_DEVIATION_VIOLATION_ACTION
|
|
16
16
|
display_name = "Submit Protocol Deviation / Violation Report"
|
|
@@ -25,8 +25,8 @@ class ProtocolDeviationViolationAction(ActionWithNotification):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class ProtocolIncidentAction(ActionWithNotification):
|
|
28
|
-
reference_model =
|
|
29
|
-
admin_site_name =
|
|
28
|
+
reference_model = "edc_protocol_incident.protocolincident"
|
|
29
|
+
admin_site_name = "edc_protocol_incident_admin"
|
|
30
30
|
|
|
31
31
|
name = PROTOCOL_INCIDENT_ACTION
|
|
32
32
|
display_name = "Submit Protocol Incident Report"
|
edc_protocol_incident/auths.py
CHANGED
|
@@ -14,24 +14,31 @@ from .auth_objects import (
|
|
|
14
14
|
)
|
|
15
15
|
from .constants import PROTOCOL_DEVIATION_VIOLATION, PROTOCOL_INCIDENT
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
site_auths.add_group(*protocol_incident_codenames, name=PROTOCOL_INCIDENT)
|
|
22
|
-
site_auths.add_group(*protocol_incident_view_codenames, name=PROTOCOL_INCIDENT_VIEW)
|
|
23
|
-
|
|
24
|
-
if incident_type == PROTOCOL_DEVIATION_VIOLATION:
|
|
25
|
-
site_auths.update_role(PROTOCOL_VIOLATION, name=CLINICIAN_ROLE)
|
|
26
|
-
site_auths.update_role(PROTOCOL_VIOLATION, name=CLINICIAN_SUPER_ROLE)
|
|
27
|
-
site_auths.update_role(PROTOCOL_VIOLATION_VIEW, name=AUDITOR_ROLE)
|
|
28
|
-
elif incident_type == PROTOCOL_INCIDENT:
|
|
29
|
-
site_auths.update_role(PROTOCOL_INCIDENT, name=CLINICIAN_ROLE)
|
|
30
|
-
site_auths.update_role(PROTOCOL_INCIDENT, name=CLINICIAN_SUPER_ROLE)
|
|
31
|
-
site_auths.update_role(PROTOCOL_INCIDENT_VIEW, name=AUDITOR_ROLE)
|
|
32
|
-
else:
|
|
33
|
-
raise ValueError(
|
|
34
|
-
"Invalid value for settings.EDC_PROTOCOL_VIOLATION_TYPE. "
|
|
35
|
-
f"Expected `{PROTOCOL_INCIDENT}` or `{PROTOCOL_DEVIATION_VIOLATION}`. "
|
|
36
|
-
f"Got {incident_type}."
|
|
17
|
+
|
|
18
|
+
def update_site_auths() -> None:
|
|
19
|
+
incident_type = getattr(
|
|
20
|
+
settings, "EDC_PROTOCOL_VIOLATION_TYPE", PROTOCOL_DEVIATION_VIOLATION
|
|
37
21
|
)
|
|
22
|
+
|
|
23
|
+
site_auths.add_group(*protocol_violation_codenames, name=PROTOCOL_VIOLATION)
|
|
24
|
+
site_auths.add_group(*protocol_violation_view_codenames, name=PROTOCOL_VIOLATION_VIEW)
|
|
25
|
+
site_auths.add_group(*protocol_incident_codenames, name=PROTOCOL_INCIDENT)
|
|
26
|
+
site_auths.add_group(*protocol_incident_view_codenames, name=PROTOCOL_INCIDENT_VIEW)
|
|
27
|
+
|
|
28
|
+
if incident_type == PROTOCOL_DEVIATION_VIOLATION:
|
|
29
|
+
site_auths.update_role(PROTOCOL_VIOLATION, name=CLINICIAN_ROLE)
|
|
30
|
+
site_auths.update_role(PROTOCOL_VIOLATION, name=CLINICIAN_SUPER_ROLE)
|
|
31
|
+
site_auths.update_role(PROTOCOL_VIOLATION_VIEW, name=AUDITOR_ROLE)
|
|
32
|
+
elif incident_type == PROTOCOL_INCIDENT:
|
|
33
|
+
site_auths.update_role(PROTOCOL_INCIDENT, name=CLINICIAN_ROLE)
|
|
34
|
+
site_auths.update_role(PROTOCOL_INCIDENT, name=CLINICIAN_SUPER_ROLE)
|
|
35
|
+
site_auths.update_role(PROTOCOL_INCIDENT_VIEW, name=AUDITOR_ROLE)
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"Invalid value for settings.EDC_PROTOCOL_VIOLATION_TYPE. "
|
|
39
|
+
f"Expected `{PROTOCOL_INCIDENT}` or `{PROTOCOL_DEVIATION_VIOLATION}`. "
|
|
40
|
+
f"Got {incident_type}."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
update_site_auths()
|
edc_pylabels/auths.py
CHANGED
|
@@ -2,4 +2,9 @@ from edc_auth.site_auths import site_auths
|
|
|
2
2
|
|
|
3
3
|
from .auth_objects import PYLABELS, codenames
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
def update_site_auths() -> None:
|
|
7
|
+
site_auths.add_group(*codenames, name=PYLABELS, no_delete=False)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
update_site_auths()
|
edc_qareports/auths.py
CHANGED
|
@@ -9,12 +9,16 @@ from .auth_objects import (
|
|
|
9
9
|
qa_reports_codenames,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
# groups
|
|
13
|
-
site_auths.add_group(*qa_reports_codenames, name=QA_REPORTS)
|
|
14
|
-
site_auths.add_group(*qa_reports_codenames, name=QA_REPORTS_AUDIT, view_only=True)
|
|
15
12
|
|
|
13
|
+
def update_site_auths() -> None:
|
|
14
|
+
# groups
|
|
15
|
+
site_auths.add_group(*qa_reports_codenames, name=QA_REPORTS)
|
|
16
|
+
site_auths.add_group(*qa_reports_codenames, name=QA_REPORTS_AUDIT, view_only=True)
|
|
16
17
|
|
|
17
|
-
# roles
|
|
18
|
-
site_auths.add_role(QA_REPORTS, name=QA_REPORTS_ROLE)
|
|
19
|
-
site_auths.add_role(QA_REPORTS, name=QA_REPORTS_SUPER_ROLE)
|
|
20
|
-
site_auths.add_role(QA_REPORTS_AUDIT, name=QA_REPORTS_AUDIT_ROLE)
|
|
18
|
+
# roles
|
|
19
|
+
site_auths.add_role(QA_REPORTS, name=QA_REPORTS_ROLE)
|
|
20
|
+
site_auths.add_role(QA_REPORTS, name=QA_REPORTS_SUPER_ROLE)
|
|
21
|
+
site_auths.add_role(QA_REPORTS_AUDIT, name=QA_REPORTS_AUDIT_ROLE)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
update_site_auths()
|
edc_randomization/admin.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
1
3
|
from django.conf import settings
|
|
2
4
|
from django.contrib import admin
|
|
3
5
|
from django.contrib.admin.sites import AlreadyRegistered
|
|
@@ -65,7 +67,8 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
|
|
|
65
67
|
|
|
66
68
|
def get_readonly_fields(self, request, obj=None):
|
|
67
69
|
readonly_fields = super().get_readonly_fields(request, obj=obj)
|
|
68
|
-
readonly_fields
|
|
70
|
+
readonly_fields = (
|
|
71
|
+
*readonly_fields,
|
|
69
72
|
"subject_identifier",
|
|
70
73
|
"sid",
|
|
71
74
|
"site_name",
|
|
@@ -75,7 +78,8 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
|
|
|
75
78
|
"allocated_datetime",
|
|
76
79
|
"allocated_site",
|
|
77
80
|
"randomizer_name",
|
|
78
|
-
|
|
81
|
+
*audit_fields,
|
|
82
|
+
)
|
|
79
83
|
return tuple(set(readonly_fields))
|
|
80
84
|
|
|
81
85
|
def get_queryset(self, request):
|
|
@@ -90,7 +94,7 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
|
|
|
90
94
|
return qs
|
|
91
95
|
|
|
92
96
|
def get_list_display(self, request) -> tuple[str, ...]:
|
|
93
|
-
|
|
97
|
+
fields = (
|
|
94
98
|
"sid",
|
|
95
99
|
"assignment",
|
|
96
100
|
"site_name",
|
|
@@ -98,22 +102,24 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
|
|
|
98
102
|
"allocated_datetime",
|
|
99
103
|
"allocated_site",
|
|
100
104
|
"randomizer_name",
|
|
101
|
-
|
|
105
|
+
)
|
|
106
|
+
if flds := site_randomizers.get_by_model(
|
|
107
|
+
self.model._meta.label_lower
|
|
108
|
+
).get_extra_list_display():
|
|
109
|
+
fields = list(fields)
|
|
110
|
+
for pos, fname in flds:
|
|
111
|
+
fields.insert(pos, fname)
|
|
112
|
+
fields = tuple(fields)
|
|
102
113
|
if user_is_blinded(request.user.username) or (
|
|
103
114
|
not user_is_blinded(request.user.username)
|
|
104
115
|
and RANDO_UNBLINDED not in [g.name for g in request.user.groups.all()]
|
|
105
116
|
):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
self.model._meta.label_lower
|
|
109
|
-
).get_extra_list_display():
|
|
110
|
-
for pos, fldname in flds:
|
|
111
|
-
list_display.insert(pos, fldname)
|
|
112
|
-
return tuple(list_display)
|
|
117
|
+
fields = tuple([fname for fname in fields if fname != "assignment"])
|
|
118
|
+
return fields
|
|
113
119
|
|
|
114
120
|
@staticmethod
|
|
115
121
|
def get_fieldnames(request) -> tuple[str, ...]:
|
|
116
|
-
fields =
|
|
122
|
+
fields = (
|
|
117
123
|
"subject_identifier",
|
|
118
124
|
"sid",
|
|
119
125
|
"assignment",
|
|
@@ -122,33 +128,35 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
|
|
|
122
128
|
"allocated_datetime",
|
|
123
129
|
"allocated_site",
|
|
124
130
|
"randomizer_name",
|
|
125
|
-
|
|
131
|
+
)
|
|
126
132
|
if user_is_blinded(request.user.username) or (
|
|
127
133
|
not user_is_blinded(request.user.username)
|
|
128
134
|
and RANDO_UNBLINDED not in [g.name for g in request.user.groups.all()]
|
|
129
135
|
):
|
|
130
|
-
fields
|
|
131
|
-
return
|
|
136
|
+
fields = tuple([fname for fname in fields if fname != "assignment"])
|
|
137
|
+
return fields
|
|
132
138
|
|
|
133
139
|
def get_list_filter(self, request) -> tuple[str, ...]:
|
|
134
|
-
|
|
140
|
+
fields = (
|
|
135
141
|
"assignment",
|
|
136
142
|
"allocated_datetime",
|
|
137
143
|
"allocated_site",
|
|
138
144
|
"site_name",
|
|
139
145
|
"randomizer_name",
|
|
140
|
-
|
|
146
|
+
)
|
|
141
147
|
if flds := site_randomizers.get_by_model(
|
|
142
148
|
self.model._meta.label_lower
|
|
143
149
|
).get_extra_list_filter():
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
fields = list(fields)
|
|
151
|
+
for pos, fname in flds:
|
|
152
|
+
fields.insert(pos, fname)
|
|
153
|
+
fields = tuple(fields)
|
|
146
154
|
if user_is_blinded(request.user.username) or (
|
|
147
155
|
not user_is_blinded(request.user.username)
|
|
148
156
|
and RANDO_UNBLINDED not in [g.name for g in request.user.groups.all()]
|
|
149
157
|
):
|
|
150
|
-
|
|
151
|
-
return
|
|
158
|
+
fields = tuple([fname for fname in fields if fname != "assignment"])
|
|
159
|
+
return fields
|
|
152
160
|
|
|
153
161
|
|
|
154
162
|
def register_admin():
|
|
@@ -156,10 +164,8 @@ def register_admin():
|
|
|
156
164
|
for randomizer_cls in site_randomizers._registry.values():
|
|
157
165
|
model = randomizer_cls.model_cls()
|
|
158
166
|
admin_cls = type(f"{model.__name__}ModelAdmin", (RandomizationListModelAdmin,), {})
|
|
159
|
-
|
|
167
|
+
with contextlib.suppress(AlreadyRegistered):
|
|
160
168
|
edc_randomization_admin.register(model, admin_cls)
|
|
161
|
-
except AlreadyRegistered:
|
|
162
|
-
pass
|
|
163
169
|
|
|
164
170
|
|
|
165
171
|
register_admin()
|
edc_randomization/auths.py
CHANGED
|
@@ -9,11 +9,16 @@ from .auth_objects import (
|
|
|
9
9
|
update_rando_group_permissions,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
site_auths.add_post_update_func(
|
|
13
|
-
"edc_randomization", remove_default_model_permissions_from_edc_permissions
|
|
14
|
-
)
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
site_auths.
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
def update_site_auths() -> None:
|
|
14
|
+
site_auths.add_post_update_func(
|
|
15
|
+
"edc_randomization", remove_default_model_permissions_from_edc_permissions
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
site_auths.add_group(get_rando_permissions_codenames, name=RANDO_BLINDED, view_only=True)
|
|
19
|
+
site_auths.add_group(get_rando_permissions_codenames, name=RANDO_UNBLINDED, view_only=True)
|
|
20
|
+
site_auths.add_post_update_func("edc_randomization", update_rando_group_permissions)
|
|
21
|
+
site_auths.add_post_update_func("edc_randomization", make_randomizationlist_view_only)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
update_site_auths()
|
edc_randomization/randomizer.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import warnings
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
@@ -8,6 +9,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
9
|
from django.apps import apps as django_apps
|
|
9
10
|
from django.conf import settings
|
|
10
11
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
12
|
+
from django.db.models import Q
|
|
11
13
|
|
|
12
14
|
from edc_registration.utils import get_registered_subject_model_cls
|
|
13
15
|
|
|
@@ -25,15 +27,15 @@ if TYPE_CHECKING:
|
|
|
25
27
|
from edc_registration.models import RegisteredSubject
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
class InvalidAssignmentDescriptionMap(Exception):
|
|
30
|
+
class InvalidAssignmentDescriptionMap(Exception): # noqa: N818
|
|
29
31
|
pass
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
class RandomizationListFileNotFound(Exception):
|
|
34
|
+
class RandomizationListFileNotFound(Exception): # noqa: N818
|
|
33
35
|
pass
|
|
34
36
|
|
|
35
37
|
|
|
36
|
-
class RandomizationListNotLoaded(Exception):
|
|
38
|
+
class RandomizationListNotLoaded(Exception): # noqa: N818
|
|
37
39
|
pass
|
|
38
40
|
|
|
39
41
|
|
|
@@ -41,7 +43,7 @@ class RandomizationError(Exception):
|
|
|
41
43
|
pass
|
|
42
44
|
|
|
43
45
|
|
|
44
|
-
class AlreadyRandomized(ValidationError):
|
|
46
|
+
class AlreadyRandomized(ValidationError): # noqa: N818
|
|
45
47
|
pass
|
|
46
48
|
|
|
47
49
|
|
|
@@ -113,10 +115,10 @@ class Randomizer:
|
|
|
113
115
|
subject_identifier: str | None = None,
|
|
114
116
|
identifier_attr: str | None = None,
|
|
115
117
|
identifier_object_name: str | None = None,
|
|
116
|
-
report_datetime: datetime = None,
|
|
117
|
-
site: Any = None,
|
|
118
|
-
user: str = None,
|
|
119
|
-
**kwargs,
|
|
118
|
+
report_datetime: datetime | None = None,
|
|
119
|
+
site: Any | None = None,
|
|
120
|
+
user: str | None = None,
|
|
121
|
+
**kwargs, # noqa: ARG002
|
|
120
122
|
):
|
|
121
123
|
self._model_obj = None
|
|
122
124
|
self._registration_obj = None
|
|
@@ -208,8 +210,10 @@ class Randomizer:
|
|
|
208
210
|
@property
|
|
209
211
|
def sid(self):
|
|
210
212
|
"""Returns the SID."""
|
|
211
|
-
if self.model_obj.sid
|
|
212
|
-
raise RandomizationError(
|
|
213
|
+
if not self.model_obj.sid:
|
|
214
|
+
raise RandomizationError(
|
|
215
|
+
f"SID cannot be None. See {self.model_obj}. Got {self.model_obj.sid}"
|
|
216
|
+
)
|
|
213
217
|
return self.model_obj.sid
|
|
214
218
|
|
|
215
219
|
@property
|
|
@@ -233,7 +237,7 @@ class Randomizer:
|
|
|
233
237
|
if not self._model_obj:
|
|
234
238
|
try:
|
|
235
239
|
obj = self.model_cls().objects.get(**self.identifier_opts)
|
|
236
|
-
except ObjectDoesNotExist:
|
|
240
|
+
except ObjectDoesNotExist as e:
|
|
237
241
|
opts = dict(site_name=self.site.name, **self.extra_model_obj_options)
|
|
238
242
|
self._model_obj = (
|
|
239
243
|
self.model_cls()
|
|
@@ -245,7 +249,7 @@ class Randomizer:
|
|
|
245
249
|
fld_str = ", ".join([f"{k}=`{v}`" for k, v in opts.items()])
|
|
246
250
|
raise AllocationError(
|
|
247
251
|
f"Randomization failed. No additional SIDs available for {fld_str}."
|
|
248
|
-
)
|
|
252
|
+
) from e
|
|
249
253
|
else:
|
|
250
254
|
raise AlreadyRandomized(
|
|
251
255
|
f"{self.identifier_object_name.title()} already randomized. "
|
|
@@ -282,7 +286,7 @@ class Randomizer:
|
|
|
282
286
|
Called by `registration_obj`.
|
|
283
287
|
"""
|
|
284
288
|
return self.get_registration_model_cls().objects.get(
|
|
285
|
-
sid__isnull=True, **self.identifier_opts
|
|
289
|
+
(Q(sid__isnull=True) | Q(sid="")), **self.identifier_opts
|
|
286
290
|
)
|
|
287
291
|
|
|
288
292
|
@property
|
|
@@ -298,14 +302,14 @@ class Randomizer:
|
|
|
298
302
|
if not self._registration_obj:
|
|
299
303
|
try:
|
|
300
304
|
self._registration_obj = self.get_unallocated_registration_obj()
|
|
301
|
-
except ObjectDoesNotExist:
|
|
305
|
+
except ObjectDoesNotExist as e:
|
|
302
306
|
try:
|
|
303
307
|
obj = self.get_registration_model_cls().objects.get(**self.identifier_opts)
|
|
304
|
-
except ObjectDoesNotExist:
|
|
308
|
+
except ObjectDoesNotExist as e:
|
|
305
309
|
raise RandomizationError(
|
|
306
310
|
f"{self.identifier_object_name.title()} does not exist. "
|
|
307
311
|
f"Got {getattr(self, self.identifier_attr)}"
|
|
308
|
-
)
|
|
312
|
+
) from e
|
|
309
313
|
else:
|
|
310
314
|
raise AlreadyRandomized(
|
|
311
315
|
f"{self.identifier_object_name.title()} already randomized. "
|
|
@@ -313,7 +317,7 @@ class Randomizer:
|
|
|
313
317
|
f"Got {getattr(obj, self.identifier_attr)} "
|
|
314
318
|
f"SID={obj.sid}",
|
|
315
319
|
code=self.get_registration_model_cls()._meta.label_lower,
|
|
316
|
-
)
|
|
320
|
+
) from e
|
|
317
321
|
return self._registration_obj
|
|
318
322
|
|
|
319
323
|
@property
|
|
@@ -347,7 +351,7 @@ class Randomizer:
|
|
|
347
351
|
"Randomization list file not found. "
|
|
348
352
|
f"Got `{cls.get_randomizationlist_path()}`. See Randomizer {cls.name}."
|
|
349
353
|
)
|
|
350
|
-
|
|
354
|
+
with contextlib.suppress(RandomizationListAlreadyImported):
|
|
351
355
|
result = cls.importer_cls(
|
|
352
356
|
assignment_map=cls.assignment_map,
|
|
353
357
|
randomizationlist_path=cls.get_randomizationlist_path(),
|
|
@@ -356,8 +360,6 @@ class Randomizer:
|
|
|
356
360
|
extra_csv_fieldnames=cls.extra_csv_fieldnames,
|
|
357
361
|
**kwargs,
|
|
358
362
|
).import_list(**kwargs)
|
|
359
|
-
except RandomizationListAlreadyImported:
|
|
360
|
-
pass
|
|
361
363
|
return result
|
|
362
364
|
|
|
363
365
|
@classmethod
|
edc_randomization/utils.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import csv
|
|
4
|
-
import
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from django.conf import settings
|
|
@@ -20,13 +21,13 @@ class RandomizationListExporterError(Exception):
|
|
|
20
21
|
pass
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class SubjectNotRandomization(Exception):
|
|
24
|
+
class SubjectNotRandomization(Exception): # noqa: N818
|
|
24
25
|
pass
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def get_assignment_for_subject(
|
|
28
29
|
subject_identifier: str,
|
|
29
|
-
randomizer_name: str
|
|
30
|
+
randomizer_name: str,
|
|
30
31
|
identifier_fld: str | None = None,
|
|
31
32
|
) -> str:
|
|
32
33
|
"""Returns the assignment for a randomized subject.
|
|
@@ -42,7 +43,7 @@ def get_assignment_for_subject(
|
|
|
42
43
|
|
|
43
44
|
def get_assignment_description_for_subject(
|
|
44
45
|
subject_identifier: str,
|
|
45
|
-
randomizer_name: str
|
|
46
|
+
randomizer_name: str,
|
|
46
47
|
identifier_fld: str | None = None,
|
|
47
48
|
) -> str:
|
|
48
49
|
"""Returns the assignment description for a randomized subject.
|
|
@@ -60,7 +61,7 @@ def get_assignment_description_for_subject(
|
|
|
60
61
|
|
|
61
62
|
def get_object_for_subject(
|
|
62
63
|
subject_identifier: str,
|
|
63
|
-
randomizer_name: str
|
|
64
|
+
randomizer_name: str,
|
|
64
65
|
identifier_fld: str | None = None,
|
|
65
66
|
label: str | None = None,
|
|
66
67
|
) -> Any:
|
|
@@ -81,11 +82,11 @@ def get_object_for_subject(
|
|
|
81
82
|
}
|
|
82
83
|
try:
|
|
83
84
|
obj = randomizer_cls.model_cls().objects.get(**opts)
|
|
84
|
-
except ObjectDoesNotExist:
|
|
85
|
+
except ObjectDoesNotExist as e:
|
|
85
86
|
raise SubjectNotRandomization(
|
|
86
87
|
f"{label.title()} not randomized. See Randomizer `{randomizer_name}`. "
|
|
87
88
|
f"Got {identifier_fld}=`{subject_identifier}`."
|
|
88
|
-
)
|
|
89
|
+
) from e
|
|
89
90
|
return obj
|
|
90
91
|
|
|
91
92
|
|
|
@@ -118,7 +119,7 @@ def generate_fake_randomization_list(
|
|
|
118
119
|
|
|
119
120
|
# get site ID and write the file
|
|
120
121
|
site_id = sites.get_by_attr("name", site_name)
|
|
121
|
-
with open(
|
|
122
|
+
with Path(filename).open("a+", newline="") as f:
|
|
122
123
|
writer = csv.DictWriter(f, fieldnames=["sid", "assignment", "site_name", "country"])
|
|
123
124
|
if write_header:
|
|
124
125
|
writer.writeheader()
|
|
@@ -133,7 +134,7 @@ def generate_fake_randomization_list(
|
|
|
133
134
|
)
|
|
134
135
|
)
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
sys.stdout.write(f"(*) Added {slots} slots for {site_name}.\n")
|
|
137
138
|
|
|
138
139
|
|
|
139
140
|
def export_randomization_list(
|
|
@@ -143,20 +144,20 @@ def export_randomization_list(
|
|
|
143
144
|
|
|
144
145
|
try:
|
|
145
146
|
user = get_user_model().objects.get(username=username)
|
|
146
|
-
except ObjectDoesNotExist:
|
|
147
|
-
raise RandomizationListExporterError(f"User `{username}` does not exist")
|
|
147
|
+
except ObjectDoesNotExist as e:
|
|
148
|
+
raise RandomizationListExporterError(f"User `{username}` does not exist") from e
|
|
148
149
|
if not user.has_perm(randomizer_cls.model_cls()._meta.label_lower.replace(".", ".view_")):
|
|
149
150
|
raise RandomizationListExporterError(
|
|
150
151
|
f"User `{username}` does not have "
|
|
151
152
|
f"permission to view '{randomizer_cls.model_cls()._meta.label_lower}'"
|
|
152
153
|
)
|
|
153
|
-
path = path or settings.EXPORT_FOLDER
|
|
154
|
+
path = Path(path or settings.EXPORT_FOLDER)
|
|
154
155
|
timestamp = timezone.now().strftime("%Y%m%d%H%M")
|
|
155
|
-
filename =
|
|
156
|
+
filename = Path(
|
|
156
157
|
f"~/{settings.APP_NAME}_{randomizer_cls.name}_"
|
|
157
158
|
f"randomizationlist_exported_{timestamp}.csv"
|
|
158
|
-
)
|
|
159
|
-
filename =
|
|
159
|
+
).expanduser()
|
|
160
|
+
filename = path / filename
|
|
160
161
|
|
|
161
162
|
df = (
|
|
162
163
|
read_frame(randomizer_cls.model_cls().objects.all(), verbose=False)
|
|
@@ -172,5 +173,5 @@ def export_randomization_list(
|
|
|
172
173
|
sep="|",
|
|
173
174
|
)
|
|
174
175
|
df.to_csv(**opts)
|
|
175
|
-
|
|
176
|
+
sys.stdout.write(f"{filename!s}\n")
|
|
176
177
|
return filename
|
edc_refusal/auths.py
CHANGED
|
@@ -3,5 +3,10 @@ from edc_screening.auth_objects import SCREENING, SCREENING_SUPER
|
|
|
3
3
|
|
|
4
4
|
from .auth_objects import codenames
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
def update_site_auths() -> None:
|
|
8
|
+
site_auths.update_group(*codenames, name=SCREENING, no_delete=True)
|
|
9
|
+
site_auths.update_group(*codenames, name=SCREENING_SUPER)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
update_site_auths()
|