clinicedc 2.0.7__py3-none-any.whl → 2.0.8__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.8.dist-info}/METADATA +1 -1
- {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/RECORD +134 -137
- {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.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/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/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.8.dist-info}/licenses/LICENSE +0 -0
edc_action_item/auths.py
CHANGED
|
@@ -15,39 +15,44 @@ from .auth_objects import (
|
|
|
15
15
|
navbar_tuples,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
site_auths.add_post_update_func(
|
|
19
|
-
"edc_action_item",
|
|
20
|
-
remove_default_model_permissions_from_edc_permissions,
|
|
21
|
-
)
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
model="edc_action_item.edcpermissions", codename_tuples=navbar_tuples
|
|
25
|
-
)
|
|
19
|
+
def update_site_auths():
|
|
26
20
|
|
|
27
|
-
site_auths.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"edc_action_item.export_actiontype",
|
|
32
|
-
"edc_action_item.export_historicalactionitem",
|
|
33
|
-
name=ACTION_ITEM_EXPORT,
|
|
34
|
-
)
|
|
21
|
+
site_auths.add_post_update_func(
|
|
22
|
+
"edc_action_item",
|
|
23
|
+
remove_default_model_permissions_from_edc_permissions,
|
|
24
|
+
)
|
|
35
25
|
|
|
36
|
-
site_auths.add_custom_permissions_tuples(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
site_auths.add_custom_permissions_tuples(
|
|
27
|
+
model="edc_action_item.edcpermissions", codename_tuples=navbar_tuples
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
site_auths.add_group(*action_items_codenames, name=ACTION_ITEM)
|
|
31
|
+
site_auths.add_group(*action_items_codenames, name=ACTION_ITEM_VIEW_ONLY, view_only=True)
|
|
32
|
+
site_auths.add_group(
|
|
33
|
+
"edc_action_item.export_actionitem",
|
|
34
|
+
"edc_action_item.export_actiontype",
|
|
35
|
+
"edc_action_item.export_historicalactionitem",
|
|
36
|
+
name=ACTION_ITEM_EXPORT,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
site_auths.add_custom_permissions_tuples(
|
|
40
|
+
model="edc_action_item.historicalactionitem",
|
|
41
|
+
codename_tuples=[
|
|
42
|
+
(
|
|
43
|
+
"edc_action_item.export_historicalactionitem",
|
|
44
|
+
"Cane export historicalactionitem",
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"edc_action_item.export_historicalreference",
|
|
48
|
+
"Cane export historicalreference",
|
|
49
|
+
),
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
site_auths.update_role(ACTION_ITEM, name=CLINICIAN_ROLE)
|
|
54
|
+
site_auths.update_role(ACTION_ITEM, name=NURSE_ROLE)
|
|
55
|
+
site_auths.update_role(ACTION_ITEM, name=CLINICIAN_SUPER_ROLE)
|
|
56
|
+
site_auths.update_role(ACTION_ITEM_VIEW_ONLY, name=AUDITOR_ROLE)
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
site_auths.update_role(ACTION_ITEM, name=NURSE_ROLE)
|
|
52
|
-
site_auths.update_role(ACTION_ITEM, name=CLINICIAN_SUPER_ROLE)
|
|
53
|
-
site_auths.update_role(ACTION_ITEM_VIEW_ONLY, name=AUDITOR_ROLE)
|
|
58
|
+
update_site_auths()
|
|
@@ -113,8 +113,7 @@ class ActionNoManagersModelMixin(models.Model):
|
|
|
113
113
|
def natural_key(self: Any) -> tuple:
|
|
114
114
|
return (self.action_identifier,)
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
natural_key.dependencies = ["edc_action_item.actionitem"] # type:ignore
|
|
116
|
+
natural_key.dependencies = ("edc_action_item.actionitem",)
|
|
118
117
|
|
|
119
118
|
@classmethod
|
|
120
119
|
def get_action_cls(cls) -> type[Action]:
|
|
@@ -97,18 +97,17 @@ def action_on_reference_model_post_delete(sender, instance: Any, using, **kwargs
|
|
|
97
97
|
raise
|
|
98
98
|
else:
|
|
99
99
|
reset_and_delete_action_item(instance, using)
|
|
100
|
-
elif isinstance(instance, ActionItem):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
parent_reference_obj.action_item
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
).create_next_action_items()
|
|
100
|
+
elif isinstance(instance, ActionItem) and instance.parent_action_item:
|
|
101
|
+
try:
|
|
102
|
+
parent_reference_obj = instance.parent_reference_obj
|
|
103
|
+
except ObjectDoesNotExist:
|
|
104
|
+
pass
|
|
105
|
+
else:
|
|
106
|
+
parent_reference_obj.action_item.action_cls(
|
|
107
|
+
action_item=instance.parent_reference_obj.action_item,
|
|
108
|
+
subject_identifier=instance.subject_identifier,
|
|
109
|
+
using=using,
|
|
110
|
+
).create_next_action_items()
|
|
112
111
|
|
|
113
112
|
|
|
114
113
|
@receiver(
|
|
@@ -126,15 +125,15 @@ def action_item_notification_on_post_create_historical_record(
|
|
|
126
125
|
if (
|
|
127
126
|
site_notifications.loaded
|
|
128
127
|
and instance._meta.label_lower == "edc_action_item.actionitem"
|
|
128
|
+
and instance.status != CLOSED
|
|
129
129
|
):
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
site_notifications.notify(**opts)
|
|
130
|
+
opts = dict(
|
|
131
|
+
instance=instance,
|
|
132
|
+
user=instance.user_modified or instance.user_created,
|
|
133
|
+
history_date=history_date,
|
|
134
|
+
history_user=history_user,
|
|
135
|
+
history_change_reason=history_change_reason,
|
|
136
|
+
fail_silently=True,
|
|
137
|
+
**kwargs,
|
|
138
|
+
)
|
|
139
|
+
site_notifications.notify(**opts)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import copy
|
|
4
5
|
import sys
|
|
5
6
|
from dataclasses import InitVar, dataclass, field
|
|
@@ -26,7 +27,7 @@ if TYPE_CHECKING:
|
|
|
26
27
|
from .action_with_notification import ActionWithNotification
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
class AlreadyRegistered(Exception):
|
|
30
|
+
class AlreadyRegistered(Exception): # noqa: N818
|
|
30
31
|
pass
|
|
31
32
|
|
|
32
33
|
|
|
@@ -84,10 +85,8 @@ class SiteActionItemCollection:
|
|
|
84
85
|
model=action_cls.get_reference_model(),
|
|
85
86
|
url_namespace=action_cls.admin_site_name,
|
|
86
87
|
)
|
|
87
|
-
|
|
88
|
+
with contextlib.suppress(PrnAlreadyRegistered):
|
|
88
89
|
site_prn_forms.register(prn)
|
|
89
|
-
except PrnAlreadyRegistered:
|
|
90
|
-
pass
|
|
91
90
|
try:
|
|
92
91
|
action_cls.notification_email_to
|
|
93
92
|
except AttributeError:
|
|
@@ -103,10 +102,8 @@ class SiteActionItemCollection:
|
|
|
103
102
|
See edc_notification.
|
|
104
103
|
"""
|
|
105
104
|
if action_cls.notification_cls():
|
|
106
|
-
|
|
105
|
+
with contextlib.suppress(NotificationAlreadyRegistered):
|
|
107
106
|
site_notifications.register(action_cls.notification_cls())
|
|
108
|
-
except NotificationAlreadyRegistered:
|
|
109
|
-
pass
|
|
110
107
|
|
|
111
108
|
@property
|
|
112
109
|
def all(self) -> dict[str, type[Action] | type[ActionWithNotification]]:
|
|
@@ -129,12 +126,11 @@ class SiteActionItemCollection:
|
|
|
129
126
|
return None
|
|
130
127
|
|
|
131
128
|
def get_add_actions_to_show(self) -> dict[str, type[Action]]:
|
|
132
|
-
|
|
129
|
+
return {
|
|
133
130
|
action_cls.name: action_cls
|
|
134
131
|
for action_cls in self.registry.values()
|
|
135
132
|
if action_cls.show_link_to_add
|
|
136
133
|
}
|
|
137
|
-
return actions
|
|
138
134
|
|
|
139
135
|
def create_or_update_action_types(self) -> None:
|
|
140
136
|
"""Populates the ActionType model."""
|
edc_action_item/utils.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
4
|
+
|
|
3
5
|
from django.apps import apps as django_apps
|
|
4
6
|
from django.core.exceptions import ObjectDoesNotExist
|
|
5
7
|
from tqdm import tqdm
|
|
@@ -68,10 +70,8 @@ def reset_and_delete_action_item(instance, using=None):
|
|
|
68
70
|
|
|
69
71
|
def register_actions(*action_cls):
|
|
70
72
|
for cls in action_cls:
|
|
71
|
-
|
|
73
|
+
with contextlib.suppress(AlreadyRegistered):
|
|
72
74
|
site_action_items.register(cls)
|
|
73
|
-
except AlreadyRegistered:
|
|
74
|
-
pass
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def get_reference_obj(action_item: ActionItem | None):
|
edc_adverse_event/auths.py
CHANGED
|
@@ -17,65 +17,69 @@ from .auth_objects import (
|
|
|
17
17
|
)
|
|
18
18
|
from .constants import AE, AE_REVIEW, AE_ROLE, AE_SUPER, TMG, TMG_REVIEW, TMG_ROLE
|
|
19
19
|
|
|
20
|
-
site_auths.add_post_update_func(
|
|
21
|
-
"edc_adverse_event", remove_default_model_permissions_from_edc_permissions
|
|
22
|
-
)
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
def update_site_auths():
|
|
22
|
+
site_auths.add_post_update_func(
|
|
23
|
+
"edc_adverse_event", remove_default_model_permissions_from_edc_permissions
|
|
24
|
+
)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
site_auths.add_custom_permissions_tuples(
|
|
28
|
-
model=permissions_model, codename_tuples=ae_dashboard_tuples
|
|
29
|
-
)
|
|
30
|
-
site_auths.add_custom_permissions_tuples(
|
|
31
|
-
model=permissions_model, codename_tuples=tmg_dashboard_tuples
|
|
32
|
-
)
|
|
26
|
+
permissions_model = "edc_adverse_event.edcpermissions"
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
# custom perms
|
|
29
|
+
site_auths.add_custom_permissions_tuples(
|
|
30
|
+
model=permissions_model, codename_tuples=ae_dashboard_tuples
|
|
31
|
+
)
|
|
32
|
+
site_auths.add_custom_permissions_tuples(
|
|
33
|
+
model=permissions_model, codename_tuples=tmg_dashboard_tuples
|
|
34
|
+
)
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
site_auths.add_custom_permissions_tuples(
|
|
37
|
+
model=permissions_model, codename_tuples=tmg_navbar_tuples
|
|
38
|
+
)
|
|
39
|
+
site_auths.add_custom_permissions_tuples(
|
|
40
|
+
model=permissions_model, codename_tuples=ae_navbar_tuples
|
|
41
|
+
)
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
site_auths.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
43
|
+
# groups
|
|
44
|
+
site_auths.add_group(*ae_codenames, name=AE, no_delete=True)
|
|
45
|
+
site_auths.update_group(
|
|
46
|
+
*[c[0] for c in ae_dashboard_tuples], *[c[0] for c in ae_navbar_tuples], name=AE
|
|
47
|
+
)
|
|
53
48
|
|
|
54
|
-
site_auths.add_group(*ae_codenames, name=
|
|
55
|
-
site_auths.update_group(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
49
|
+
site_auths.add_group(*ae_codenames, name=AE_SUPER)
|
|
50
|
+
site_auths.update_group(
|
|
51
|
+
*[c[0] for c in ae_dashboard_tuples],
|
|
52
|
+
*[c[0] for c in ae_navbar_tuples],
|
|
53
|
+
name=AE_SUPER,
|
|
54
|
+
)
|
|
60
55
|
|
|
61
|
-
site_auths.add_group(*
|
|
62
|
-
site_auths.update_group(
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
site_auths.add_group(*ae_codenames, name=AE_REVIEW, view_only=True)
|
|
57
|
+
site_auths.update_group(
|
|
58
|
+
*[c[0] for c in ae_dashboard_tuples],
|
|
59
|
+
*[c[0] for c in ae_navbar_tuples],
|
|
60
|
+
name=AE_REVIEW,
|
|
61
|
+
)
|
|
65
62
|
|
|
66
|
-
site_auths.add_group(*tmg_codenames, name=
|
|
67
|
-
site_auths.update_group(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
63
|
+
site_auths.add_group(*tmg_codenames, name=TMG)
|
|
64
|
+
site_auths.update_group(
|
|
65
|
+
*[c[0] for c in tmg_dashboard_tuples], *[c[0] for c in tmg_navbar_tuples], name=TMG
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
site_auths.add_group(*tmg_codenames, name=TMG_REVIEW, view_only=True)
|
|
69
|
+
site_auths.update_group(
|
|
70
|
+
*[c[0] for c in tmg_dashboard_tuples],
|
|
71
|
+
*[c[0] for c in tmg_navbar_tuples],
|
|
72
|
+
name=TMG_REVIEW,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# add roles
|
|
76
|
+
site_auths.add_role(AE, name=AE_ROLE)
|
|
77
|
+
site_auths.add_role(AE_REVIEW, TMG, name=TMG_ROLE)
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
site_auths.
|
|
75
|
-
site_auths.
|
|
79
|
+
site_auths.update_role(AE, name=CLINICIAN_ROLE)
|
|
80
|
+
site_auths.update_role(AE, name=NURSE_ROLE)
|
|
81
|
+
site_auths.update_role(AE_REVIEW, TMG_REVIEW, name=AUDITOR_ROLE)
|
|
82
|
+
site_auths.update_role(AE_SUPER, name=CLINICIAN_SUPER_ROLE)
|
|
76
83
|
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
site_auths.update_role(AE, name=NURSE_ROLE)
|
|
80
|
-
site_auths.update_role(AE_REVIEW, TMG_REVIEW, name=AUDITOR_ROLE)
|
|
81
|
-
site_auths.update_role(AE_SUPER, name=CLINICIAN_SUPER_ROLE)
|
|
85
|
+
update_site_auths()
|
|
@@ -18,8 +18,6 @@ class AeTmgMethodsModelMixin(models.Model):
|
|
|
18
18
|
def get_action_item_reason(self):
|
|
19
19
|
return self.ae_initial.ae_description
|
|
20
20
|
|
|
21
|
-
def get_search_slug_fields(self):
|
|
21
|
+
def get_search_slug_fields(self) -> tuple[str]:
|
|
22
22
|
fields = super().get_search_slug_fields()
|
|
23
|
-
fields
|
|
24
|
-
fields.append("report_status")
|
|
25
|
-
return fields
|
|
23
|
+
return *fields, "subject_identifier", "report_status"
|
edc_appointment/auths.py
CHANGED
|
@@ -8,17 +8,21 @@ from edc_auth.site_auths import site_auths
|
|
|
8
8
|
|
|
9
9
|
from .auth_objects import APPOINTMENT, APPOINTMENT_EXPORT, APPOINTMENT_VIEW, codenames
|
|
10
10
|
|
|
11
|
-
site_auths.add_group(*codenames, name=APPOINTMENT_VIEW, view_only=True)
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
def update_site_auths():
|
|
13
|
+
site_auths.add_group(*codenames, name=APPOINTMENT_VIEW, view_only=True)
|
|
14
14
|
|
|
15
|
-
site_auths.add_group(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
site_auths.add_group(*codenames, name=APPOINTMENT)
|
|
16
|
+
|
|
17
|
+
site_auths.add_group(
|
|
18
|
+
"edc_appointment.export_appointment",
|
|
19
|
+
name=APPOINTMENT_EXPORT,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
site_auths.update_role(APPOINTMENT, name=CLINICIAN_ROLE)
|
|
23
|
+
site_auths.update_role(APPOINTMENT, name=NURSE_ROLE)
|
|
24
|
+
site_auths.update_role(APPOINTMENT_VIEW, name=AUDITOR_ROLE)
|
|
25
|
+
site_auths.update_role(APPOINTMENT, name=CLINICIAN_SUPER_ROLE)
|
|
19
26
|
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
site_auths.update_role(APPOINTMENT, name=NURSE_ROLE)
|
|
23
|
-
site_auths.update_role(APPOINTMENT_VIEW, name=AUDITOR_ROLE)
|
|
24
|
-
site_auths.update_role(APPOINTMENT, name=CLINICIAN_SUPER_ROLE)
|
|
28
|
+
update_site_auths()
|
|
@@ -215,7 +215,7 @@ class AppointmentCreator:
|
|
|
215
215
|
raise CreateAppointmentDateError(
|
|
216
216
|
f"{e} Visit={self.visit!r}. "
|
|
217
217
|
f"Try setting 'best_effort_available_datetime=True' on facility."
|
|
218
|
-
)
|
|
218
|
+
) from e
|
|
219
219
|
else:
|
|
220
220
|
return self.suggested_datetime
|
|
221
221
|
return arw.datetime
|
|
@@ -85,7 +85,7 @@ class AppointmentsCreator:
|
|
|
85
85
|
except FacilityError as e:
|
|
86
86
|
raise CreateAppointmentError(
|
|
87
87
|
f"{e} See {visit!r}. Got facility_name={visit.facility_name}"
|
|
88
|
-
)
|
|
88
|
+
) from e
|
|
89
89
|
appointment = self.update_or_create_appointment(
|
|
90
90
|
visit=visit,
|
|
91
91
|
taken_datetimes=taken_datetimes,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from typing import TYPE_CHECKING, TypeVar
|
|
4
5
|
|
|
5
6
|
from django.core.exceptions import ObjectDoesNotExist
|
|
@@ -154,10 +155,8 @@ class AppointmentMethodsModelMixin(models.Model):
|
|
|
154
155
|
def related_visit(self: Appointment) -> VisitModel | None:
|
|
155
156
|
"""Returns the related visit model for the current instance."""
|
|
156
157
|
related_visit = None
|
|
157
|
-
|
|
158
|
+
with contextlib.suppress(ObjectDoesNotExist):
|
|
158
159
|
related_visit = getattr(self, self.related_visit_model_attr())
|
|
159
|
-
except ObjectDoesNotExist:
|
|
160
|
-
pass
|
|
161
160
|
return related_visit
|
|
162
161
|
|
|
163
162
|
@property
|
|
@@ -87,7 +87,7 @@ class AppointmentModelMixin(
|
|
|
87
87
|
"Subject is not on a schedule. Using subject_identifier="
|
|
88
88
|
f"`{self.subject_identifier}` and appt_datetime=`{dte_as_str}`."
|
|
89
89
|
f"Got {e}"
|
|
90
|
-
)
|
|
90
|
+
) from e
|
|
91
91
|
if self.appt_datetime > onschedule_obj.onschedule_datetime:
|
|
92
92
|
# update appointment timepoints
|
|
93
93
|
schedule.put_on_schedule(
|
|
@@ -122,33 +122,36 @@ class AppointmentModelMixin(
|
|
|
122
122
|
return self.pk
|
|
123
123
|
|
|
124
124
|
def validate_appt_datetime_not_before_previous(self) -> None:
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
125
|
+
if (
|
|
126
|
+
self.appt_status != CANCELLED_APPT
|
|
127
|
+
and self.appt_datetime
|
|
128
|
+
and self.relative_previous
|
|
129
|
+
and self.appt_datetime <= self.relative_previous.appt_datetime
|
|
130
|
+
):
|
|
131
|
+
appt_datetime = formatted_datetime(self.appt_datetime)
|
|
132
|
+
previous_appt_datetime = formatted_datetime(self.relative_previous.appt_datetime)
|
|
133
|
+
raise AppointmentDatetimeError(
|
|
134
|
+
"Datetime cannot be on or before previous appointment datetime. "
|
|
135
|
+
f"Got {appt_datetime} <= {previous_appt_datetime}. "
|
|
136
|
+
f"See appointment `{self}` and "
|
|
137
|
+
f"`{self.relative_previous}`."
|
|
138
|
+
)
|
|
140
139
|
|
|
141
140
|
def validate_appt_datetime_not_after_next(self) -> None:
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
if (
|
|
142
|
+
self.appt_status != CANCELLED_APPT
|
|
143
|
+
and self.appt_datetime
|
|
144
|
+
and self.relative_next
|
|
145
|
+
and self.appt_datetime >= self.relative_next.appt_datetime
|
|
146
|
+
):
|
|
147
|
+
appt_datetime = formatted_datetime(self.appt_datetime)
|
|
148
|
+
next_appt_datetime = formatted_datetime(self.relative_next.appt_datetime)
|
|
149
|
+
raise AppointmentDatetimeError(
|
|
150
|
+
"Datetime cannot be on or after next appointment datetime. "
|
|
151
|
+
f"Got {appt_datetime} >= {next_appt_datetime}. "
|
|
152
|
+
f"See appointment `{self}` and "
|
|
153
|
+
f"`{self.relative_next}`."
|
|
154
|
+
)
|
|
152
155
|
|
|
153
156
|
@property
|
|
154
157
|
def title(self: Appointment) -> str:
|
|
@@ -171,7 +174,7 @@ class AppointmentModelMixin(
|
|
|
171
174
|
|
|
172
175
|
class Meta(NonUniqueSubjectIdentifierFieldMixin.Meta):
|
|
173
176
|
abstract = True
|
|
174
|
-
constraints =
|
|
177
|
+
constraints = (
|
|
175
178
|
UniqueConstraint(
|
|
176
179
|
fields=[
|
|
177
180
|
"subject_identifier",
|
|
@@ -192,7 +195,7 @@ class AppointmentModelMixin(
|
|
|
192
195
|
],
|
|
193
196
|
name="unique_%(app_label)s_%(class)s_200",
|
|
194
197
|
),
|
|
195
|
-
|
|
198
|
+
)
|
|
196
199
|
indexes = (
|
|
197
200
|
models.Index(fields=["appt_datetime"]),
|
|
198
201
|
models.Index(fields=["appt_status"]),
|
|
@@ -23,7 +23,7 @@ class Appointment(AppointmentModelMixin, SiteModelMixin, BaseUuidModel):
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
# noinspection PyTypeHints
|
|
26
|
-
natural_key.dependencies =
|
|
26
|
+
natural_key.dependencies = ("sites.Site",)
|
|
27
27
|
|
|
28
28
|
class Meta(AppointmentModelMixin.Meta, SiteModelMixin.Meta, BaseUuidModel.Meta):
|
|
29
29
|
indexes = (*AppointmentModelMixin.Meta.indexes, *BaseUuidModel.Meta.indexes)
|
edc_appointment/utils.py
CHANGED
|
@@ -80,7 +80,7 @@ class AppointmentDateWindowPeriodGapError(Exception):
|
|
|
80
80
|
pass
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
class AppointmentAlreadyStarted(Exception):
|
|
83
|
+
class AppointmentAlreadyStarted(Exception): # noqa: N818
|
|
84
84
|
pass
|
|
85
85
|
|
|
86
86
|
|
|
@@ -157,10 +157,9 @@ def raise_on_appt_may_not_be_missed(
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def get_appointment_form_meta_options() -> dict:
|
|
160
|
-
|
|
160
|
+
return getattr(
|
|
161
161
|
settings, "EDC_APPOINTMENT_FORM_META_OPTIONS", dict(labels={}, help_texts={})
|
|
162
162
|
)
|
|
163
|
-
return options
|
|
164
163
|
|
|
165
164
|
|
|
166
165
|
def get_appt_reason_choices() -> tuple[str, ...]:
|
|
@@ -516,11 +515,11 @@ def raise_on_appt_datetime_not_in_window(
|
|
|
516
515
|
except ScheduledVisitWindowError as e:
|
|
517
516
|
msg = str(e)
|
|
518
517
|
msg.replace("Invalid datetime", "Invalid appointment datetime (S)")
|
|
519
|
-
raise AppointmentWindowError(msg)
|
|
518
|
+
raise AppointmentWindowError(msg) from e
|
|
520
519
|
except UnScheduledVisitWindowError as e:
|
|
521
520
|
msg = str(e)
|
|
522
521
|
msg.replace("Invalid datetime", "Invalid appointment datetime (U)")
|
|
523
|
-
raise AppointmentWindowError(msg)
|
|
522
|
+
raise AppointmentWindowError(msg) from e
|
|
524
523
|
|
|
525
524
|
|
|
526
525
|
def get_window_gap_days(appointment) -> int:
|
|
@@ -623,7 +622,7 @@ def get_appointment_by_datetime(
|
|
|
623
622
|
raise_on_appt_datetime_not_in_window(
|
|
624
623
|
appointment, appt_datetime=suggested_appt_datetime
|
|
625
624
|
)
|
|
626
|
-
except AppointmentWindowError:
|
|
625
|
+
except AppointmentWindowError as e:
|
|
627
626
|
in_gap = appt_datetime_in_gap(appointment, suggested_appt_datetime)
|
|
628
627
|
in_next_window_adjusted = appt_datetime_in_next_window_adjusted_for_gap(
|
|
629
628
|
appointment, suggested_appt_datetime
|
|
@@ -635,7 +634,7 @@ def get_appointment_by_datetime(
|
|
|
635
634
|
raise AppointmentDateWindowPeriodGapError(
|
|
636
635
|
f"Date falls in a `window period gap` between {appointment.visit_code} "
|
|
637
636
|
f"and {appointment.next.visit_code}. Got {dt}."
|
|
638
|
-
)
|
|
637
|
+
) from e
|
|
639
638
|
if (
|
|
640
639
|
in_gap
|
|
641
640
|
and in_next_window_adjusted
|
|
@@ -674,7 +673,7 @@ def reset_appointment(appointment: Appointment, **kwargs):
|
|
|
674
673
|
appt_status=appointment._meta.get_field("appt_status").default,
|
|
675
674
|
appt_timing=appointment._meta.get_field("appt_timing").default,
|
|
676
675
|
appt_type=None,
|
|
677
|
-
appt_type_other=
|
|
676
|
+
appt_type_other="",
|
|
678
677
|
appt_datetime=appointment.timepoint_datetime,
|
|
679
678
|
comment="",
|
|
680
679
|
)
|
|
@@ -705,7 +704,7 @@ def skip_appointment(appointment: Appointment, comment: str | None = None):
|
|
|
705
704
|
appt_status=SKIPPED_APPT,
|
|
706
705
|
appt_timing=NOT_APPLICABLE,
|
|
707
706
|
appt_type=NOT_APPLICABLE,
|
|
708
|
-
comment=comment,
|
|
707
|
+
comment=comment or "",
|
|
709
708
|
)
|
|
710
709
|
|
|
711
710
|
|
|
@@ -722,11 +721,6 @@ def get_unscheduled_appointment_url(appointment: Appointment = None) -> str:
|
|
|
722
721
|
visit_code_sequence=appointment.visit_code_sequence + 1,
|
|
723
722
|
timepoint=appointment.timepoint,
|
|
724
723
|
)
|
|
725
|
-
# if appointment := (
|
|
726
|
-
# appointment.__class__.objects.filter(visit_code_sequence__gt=0, **kwargs)
|
|
727
|
-
# .order_by("visit_code_sequence")
|
|
728
|
-
# .last()
|
|
729
|
-
# ):
|
|
730
724
|
kwargs.update(visit_code_sequence=str(appointment.visit_code_sequence + 1))
|
|
731
725
|
kwargs.update(redirect_url=dashboard_url)
|
|
732
726
|
return reverse(unscheduled_appointment_url_name, kwargs=kwargs)
|
|
@@ -741,19 +735,18 @@ def update_appt_status_for_timepoint(related_visit: RelatedVisitModel) -> None:
|
|
|
741
735
|
):
|
|
742
736
|
related_visit.appointment.appt_status = INCOMPLETE_APPT
|
|
743
737
|
related_visit.appointment.save_base(update_fields=["appt_status"])
|
|
744
|
-
elif related_visit.appointment.appt_status == INCOMPLETE_APPT
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
related_visit.appointment.save_base(update_fields=["appt_status"])
|
|
738
|
+
elif related_visit.appointment.appt_status == INCOMPLETE_APPT and (
|
|
739
|
+
not related_visit.metadata[CRF].filter(entry_status=REQUIRED).exists()
|
|
740
|
+
and not related_visit.metadata[REQUISITION].filter(entry_status=REQUIRED).exists()
|
|
741
|
+
):
|
|
742
|
+
related_visit.appointment.appt_status = COMPLETE_APPT
|
|
743
|
+
related_visit.appointment.save_base(update_fields=["appt_status"])
|
|
751
744
|
|
|
752
745
|
|
|
753
746
|
def refresh_appointments(
|
|
754
|
-
subject_identifier: str = None,
|
|
755
|
-
visit_schedule_name: str = None,
|
|
756
|
-
schedule_name: str = None,
|
|
747
|
+
subject_identifier: str | None = None,
|
|
748
|
+
visit_schedule_name: str | None = None,
|
|
749
|
+
schedule_name: str | None = None,
|
|
757
750
|
request: WSGIRequest | None = None,
|
|
758
751
|
warn_only: bool | None = None,
|
|
759
752
|
) -> tuple[str, str]:
|
|
@@ -839,6 +832,8 @@ def validate_date_is_on_clinic_day(
|
|
|
839
832
|
and calendar.weekday(appt_date.year, appt_date.month, appt_date.day)
|
|
840
833
|
not in clinic_days
|
|
841
834
|
):
|
|
835
|
+
|
|
836
|
+
days_str = [day_abbr[d] for d in clinic_days]
|
|
842
837
|
days_str = []
|
|
843
838
|
for d in clinic_days:
|
|
844
839
|
days_str.append(day_abbr[d])
|