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.

Files changed (140) hide show
  1. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/RECORD +134 -137
  3. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/WHEEL +1 -1
  4. edc_action_item/auths.py +37 -32
  5. edc_action_item/models/action_model_mixin.py +1 -2
  6. edc_action_item/models/signals.py +22 -23
  7. edc_action_item/site_action_items.py +5 -9
  8. edc_action_item/utils.py +3 -3
  9. edc_adverse_event/auths.py +55 -51
  10. edc_adverse_event/model_mixins/ae_tmg/ae_tmg_methods_model_mixin.py +2 -4
  11. edc_appointment/auths.py +14 -10
  12. edc_appointment/creators/appointment_creator.py +1 -1
  13. edc_appointment/creators/appointments_creator.py +1 -1
  14. edc_appointment/model_mixins/appointment_methods_model_mixin.py +2 -3
  15. edc_appointment/model_mixins/appointment_model_mixin.py +31 -28
  16. edc_appointment/models/appointment.py +1 -1
  17. edc_appointment/utils.py +19 -24
  18. edc_auth/auth_objects/__init__.py +2 -20
  19. edc_auth/auth_objects/default_groups.py +13 -11
  20. edc_auth/auth_objects/default_roles.py +26 -24
  21. edc_auth/auth_updater/auth_updater.py +13 -2
  22. edc_auth/auth_updater/group_updater.py +12 -10
  23. edc_auth/auth_updater/role_updater.py +2 -2
  24. edc_auth/constants.py +10 -0
  25. edc_auth/import_users.py +3 -3
  26. edc_auth/models/user_profile.py +14 -11
  27. edc_auth/site_auths.py +80 -67
  28. edc_consent/auths.py +18 -12
  29. edc_constants/constants.py +1 -0
  30. edc_crf/auths.py +5 -0
  31. edc_dashboard/auths.py +10 -6
  32. edc_dashboard/url_config.py +92 -83
  33. edc_dashboard/url_names.py +4 -4
  34. edc_dashboard/view_mixins/url_request_context_mixin.py +6 -5
  35. edc_data_manager/admin/data_query_admin.py +12 -11
  36. edc_data_manager/auths.py +37 -34
  37. edc_data_manager/rule/query_rule_wrapper.py +7 -7
  38. edc_export/archive_exporter.py +3 -2
  39. edc_export/auths.py +32 -28
  40. edc_export/model_exporter/model_exporter.py +4 -1
  41. edc_facility/auths.py +8 -3
  42. edc_facility/facility.py +8 -9
  43. edc_form_label/custom_label_condition.py +11 -8
  44. edc_form_label/form_label.py +1 -1
  45. edc_form_runners/auths.py +11 -6
  46. edc_form_validators/applicable_field_validator.py +7 -6
  47. edc_form_validators/base_form_validator.py +8 -9
  48. edc_form_validators/other_specify_field_validator.py +2 -8
  49. edc_form_validators/required_field_validator.py +19 -16
  50. edc_identifier/research_identifier.py +11 -10
  51. edc_identifier/simple_identifier.py +8 -2
  52. edc_lab/auths.py +26 -23
  53. edc_lab/lab/aliquot_creator.py +5 -8
  54. edc_lab/lab/primary_aliquot.py +14 -5
  55. edc_lab/model_mixins/requisition/requisition_model_mixin.py +6 -8
  56. edc_lab/models/aliquot.py +2 -2
  57. edc_lab/models/manifest/manifest.py +2 -2
  58. edc_lab/models/manifest/manifest_item.py +1 -1
  59. edc_lab_dashboard/auths.py +16 -11
  60. edc_lab_results/calculate_missing.py +8 -8
  61. edc_lab_results/form_validator_mixins/blood_results_form_validator_mixin.py +2 -2
  62. edc_lab_results/get_summary.py +26 -25
  63. edc_lab_results/model_mixins/blood_result_model_mixin.py +2 -0
  64. edc_label/auths.py +6 -1
  65. edc_label/label_template.py +8 -8
  66. edc_list_data/load_model_data.py +3 -3
  67. edc_list_data/post_migrate_signals.py +1 -1
  68. edc_list_data/preload_data.py +2 -2
  69. edc_list_data/row.py +1 -1
  70. edc_list_data/site_list_data.py +6 -5
  71. edc_locator/auths.py +18 -13
  72. edc_metadata/auths.py +11 -7
  73. edc_metadata/metadata/metadata.py +1 -1
  74. edc_metadata/metadata_rules/crf/crf_rule.py +1 -1
  75. edc_metadata/metadata_rules/metadata_rule_evaluator.py +5 -3
  76. edc_metadata/metadata_rules/rule.py +2 -3
  77. edc_metadata/metadata_rules/rule_evaluator.py +1 -1
  78. edc_metadata/model_mixins/updates/updates_crf_metadata_model_mixin.py +7 -4
  79. edc_metadata/model_mixins/updates/updates_requisition_metadata_model_mixin.py +5 -2
  80. edc_metadata/models/signals.py +10 -11
  81. edc_navbar/auths.py +18 -13
  82. edc_notification/auths.py +9 -4
  83. edc_notification/notification/graded_event_notification.py +2 -2
  84. edc_notification/notification/model_notification.py +3 -30
  85. edc_notification/notification/new_model_notification.py +1 -1
  86. edc_notification/notification/notification.py +1 -1
  87. edc_notification/notification/updated_model_notification.py +2 -2
  88. edc_offstudy/auths.py +12 -7
  89. edc_pdutils/df_exporters/csv_model_exporter.py +5 -2
  90. edc_pharmacy/auths.py +19 -15
  91. edc_pharmacy/models/medication/formulation.py +5 -7
  92. edc_pharmacy/prescribe/create_prescription.py +3 -3
  93. edc_pharmacy/utils/confirm_stock.py +1 -1
  94. edc_pharmacy/utils/confirm_stock_at_site.py +1 -1
  95. edc_pharmacy/views/confirmation_at_site_view.py +6 -9
  96. edc_prn/admin_site.py +5 -0
  97. edc_prn/prn.py +10 -11
  98. edc_prn/urls.py +11 -0
  99. edc_protocol_incident/action_items.py +4 -4
  100. edc_protocol_incident/auths.py +27 -20
  101. edc_pylabels/auths.py +6 -1
  102. edc_qareports/auths.py +11 -7
  103. edc_randomization/admin.py +30 -24
  104. edc_randomization/auths.py +12 -7
  105. edc_randomization/randomizer.py +22 -20
  106. edc_randomization/utils.py +17 -16
  107. edc_refusal/auths.py +7 -2
  108. edc_refusal/model_mixins.py +1 -1
  109. edc_registration/auths.py +28 -23
  110. edc_registration/model_mixins/updates_or_creates_registered_subject_model_mixin.py +13 -4
  111. edc_registration/models/registered_subject.py +1 -1
  112. edc_reportable/utils/get_reference_range_collection.py +2 -3
  113. edc_reportable/utils/load_data.py +1 -1
  114. edc_review_dashboard/auths.py +23 -18
  115. edc_screening/age_evaluator.py +3 -3
  116. edc_screening/auths.py +35 -30
  117. edc_screening/eligibility.py +1 -1
  118. edc_screening/gender_evaluator.py +1 -1
  119. edc_screening/model_mixins/eligibility_model_mixin.py +0 -2
  120. edc_screening/model_mixins/screening_methods_model_mixin.py +1 -1
  121. edc_screening/screening_eligibility.py +2 -4
  122. edc_screening/utils.py +9 -9
  123. edc_search/generate_slug.py +26 -0
  124. edc_search/model_mixins.py +10 -21
  125. edc_sites/auths.py +8 -3
  126. edc_subject_dashboard/auths.py +27 -22
  127. edc_timepoint/apps.py +0 -21
  128. edc_unblinding/auths.py +9 -4
  129. edc_utils/__init__.py +3 -1
  130. edc_utils/show_urls.py +29 -2
  131. edc_visit_schedule/auths.py +6 -1
  132. edc_visit_schedule/site_visit_schedules.py +2 -2
  133. edc_visit_tracking/models/signals.py +2 -2
  134. edc_form_label/models.py +0 -0
  135. edc_search/constants.py +0 -1
  136. edc_search/models.py +0 -0
  137. edc_search/search_slug.py +0 -51
  138. edc_search/updater.py +0 -30
  139. edc_search/wsgi.py +0 -7
  140. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.17
2
+ Generator: uv 0.8.18
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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
- site_auths.add_custom_permissions_tuples(
24
- model="edc_action_item.edcpermissions", codename_tuples=navbar_tuples
25
- )
19
+ def update_site_auths():
26
20
 
27
- site_auths.add_group(*action_items_codenames, name=ACTION_ITEM)
28
- site_auths.add_group(*action_items_codenames, name=ACTION_ITEM_VIEW_ONLY, view_only=True)
29
- site_auths.add_group(
30
- "edc_action_item.export_actionitem",
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
- model="edc_action_item.historicalactionitem",
38
- codename_tuples=[
39
- (
40
- "edc_action_item.export_historicalactionitem",
41
- "Cane export historicalactionitem",
42
- ),
43
- (
44
- "edc_action_item.export_historicalreference",
45
- "Cane export historicalreference",
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
- site_auths.update_role(ACTION_ITEM, name=CLINICIAN_ROLE)
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
- # noinspection PyTypeHints
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
- if instance.parent_action_item:
102
- try:
103
- parent_reference_obj = instance.parent_reference_obj
104
- except ObjectDoesNotExist:
105
- pass
106
- else:
107
- parent_reference_obj.action_item.action_cls(
108
- action_item=instance.parent_reference_obj.action_item,
109
- subject_identifier=instance.subject_identifier,
110
- using=using,
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
- if instance.status != CLOSED:
131
- opts = dict(
132
- instance=instance,
133
- user=instance.user_modified or instance.user_created,
134
- history_date=history_date,
135
- history_user=history_user,
136
- history_change_reason=history_change_reason,
137
- fail_silently=True,
138
- **kwargs,
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
- try:
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
- try:
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
- actions = {
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
- try:
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):
@@ -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
- permissions_model = "edc_adverse_event.edcpermissions"
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
- # custom perms
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
- site_auths.add_custom_permissions_tuples(
35
- model=permissions_model, codename_tuples=tmg_navbar_tuples
36
- )
37
- site_auths.add_custom_permissions_tuples(
38
- model=permissions_model, codename_tuples=ae_navbar_tuples
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
- # groups
42
- site_auths.add_group(*ae_codenames, name=AE, no_delete=True)
43
- site_auths.update_group(
44
- *[c[0] for c in ae_dashboard_tuples], *[c[0] for c in ae_navbar_tuples], name=AE
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
- site_auths.add_group(*ae_codenames, name=AE_SUPER)
48
- site_auths.update_group(
49
- *[c[0] for c in ae_dashboard_tuples],
50
- *[c[0] for c in ae_navbar_tuples],
51
- name=AE_SUPER,
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=AE_REVIEW, view_only=True)
55
- site_auths.update_group(
56
- *[c[0] for c in ae_dashboard_tuples],
57
- *[c[0] for c in ae_navbar_tuples],
58
- name=AE_REVIEW,
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(*tmg_codenames, name=TMG)
62
- site_auths.update_group(
63
- *[c[0] for c in tmg_dashboard_tuples], *[c[0] for c in tmg_navbar_tuples], name=TMG
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=TMG_REVIEW, view_only=True)
67
- site_auths.update_group(
68
- *[c[0] for c in tmg_dashboard_tuples],
69
- *[c[0] for c in tmg_navbar_tuples],
70
- name=TMG_REVIEW,
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
- # add roles
74
- site_auths.add_role(AE, name=AE_ROLE)
75
- site_auths.add_role(AE_REVIEW, TMG, name=TMG_ROLE)
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
- site_auths.update_role(AE, name=CLINICIAN_ROLE)
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.append("subject_identifier")
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
- site_auths.add_group(*codenames, name=APPOINTMENT)
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
- "edc_appointment.export_appointment",
17
- name=APPOINTMENT_EXPORT,
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
- site_auths.update_role(APPOINTMENT, name=CLINICIAN_ROLE)
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
- try:
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 self.appt_status != CANCELLED_APPT and self.appt_datetime:
126
- if (
127
- self.relative_previous
128
- and self.appt_datetime <= self.relative_previous.appt_datetime
129
- ):
130
- appt_datetime = formatted_datetime(self.appt_datetime)
131
- previous_appt_datetime = formatted_datetime(
132
- self.relative_previous.appt_datetime
133
- )
134
- raise AppointmentDatetimeError(
135
- "Datetime cannot be on or before previous appointment datetime. "
136
- f"Got {appt_datetime} <= {previous_appt_datetime}. "
137
- f"See appointment `{self}` and "
138
- f"`{self.relative_previous}`."
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 self.appt_status != CANCELLED_APPT and self.appt_datetime and self.relative_next:
143
- if self.appt_datetime >= self.relative_next.appt_datetime:
144
- appt_datetime = formatted_datetime(self.appt_datetime)
145
- next_appt_datetime = formatted_datetime(self.relative_next.appt_datetime)
146
- raise AppointmentDatetimeError(
147
- "Datetime cannot be on or after next appointment datetime. "
148
- f"Got {appt_datetime} >= {next_appt_datetime}. "
149
- f"See appointment `{self}` and "
150
- f"`{self.relative_next}`."
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 = ["sites.Site"] # type: ignore
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
- options = getattr(
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=None,
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
- if (
746
- not related_visit.metadata[CRF].filter(entry_status=REQUIRED).exists()
747
- and not related_visit.metadata[REQUISITION].filter(entry_status=REQUIRED).exists()
748
- ):
749
- related_visit.appointment.appt_status = COMPLETE_APPT
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])