clinicedc 2.0.34__py3-none-any.whl → 2.0.36__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 (165) hide show
  1. {clinicedc-2.0.34.dist-info → clinicedc-2.0.36.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.34.dist-info → clinicedc-2.0.36.dist-info}/RECORD +155 -165
  3. {clinicedc-2.0.34.dist-info → clinicedc-2.0.36.dist-info}/WHEEL +1 -1
  4. edc_action_item/templatetags/action_item_extras.py +1 -1
  5. edc_adverse_event/form_validator_mixins/death_report_form_validator.py +1 -1
  6. edc_adverse_event/form_validator_mixins/requires_death_report_form_validator_mixin.py +5 -3
  7. edc_adverse_event/form_validators/death_report_tmg.py +2 -2
  8. edc_adverse_event/modeladmin_mixins/ae_tmg_admin_mixin.py +1 -1
  9. edc_adverse_event/modeladmin_mixins/utils.py +1 -1
  10. edc_adverse_event/utils.py +1 -1
  11. edc_appointment/admin/appointment_admin.py +24 -24
  12. edc_appointment/admin/list_filters.py +5 -5
  13. edc_appointment/appointment_reason_updater.py +6 -5
  14. edc_appointment/appointment_status_updater.py +2 -2
  15. edc_appointment/context_processors.py +1 -2
  16. edc_appointment/creators/appointment_creator.py +9 -8
  17. edc_appointment/creators/unscheduled_appointment_creator.py +31 -24
  18. edc_appointment/creators/utils.py +35 -37
  19. edc_appointment/exceptions.py +3 -3
  20. edc_appointment/form_runners.py +2 -2
  21. edc_appointment/form_validator_mixins/next_appointment_crf_form_validator_mixin.py +5 -5
  22. edc_appointment/form_validator_mixins/window_period_form_validator_mixin.py +7 -7
  23. edc_appointment/form_validators/appointment_form_validator.py +16 -23
  24. edc_appointment/management/commands/close_appointments.py +3 -3
  25. edc_appointment/management/commands/reset_visit_code_sequences.py +1 -1
  26. edc_appointment/management/commands/update_appointment_status.py +2 -2
  27. edc_appointment/management/commands/update_skipped_appointments.py +7 -7
  28. edc_appointment/managers.py +6 -7
  29. edc_appointment/model_mixins/appointment_model_mixin.py +3 -4
  30. edc_appointment/modeladmin_mixins/next_appointment_crf_modeladmin_mixin.py +2 -2
  31. edc_appointment/modelform_mixins/next_appointment_crf_modelform_mixins.py +25 -24
  32. edc_appointment/models/signals.py +1 -1
  33. edc_appointment/skip_appointments.py +10 -29
  34. edc_appointment/utils.py +43 -47
  35. edc_appointment/view_mixins/appointment_view_mixin.py +11 -13
  36. edc_appointment/views/unscheduled_appointment_view.py +5 -6
  37. edc_consent/consent_definition.py +5 -13
  38. edc_consent/consent_definition_extension.py +0 -2
  39. edc_consent/form_validators/consent_definition_form_validator_mixin.py +25 -30
  40. edc_consent/form_validators/subject_consent_form_validator.py +3 -3
  41. edc_consent/modelform_mixins/consent_modelform_mixin/consent_modelform_validation_mixin.py +1 -1
  42. edc_consent/modelform_mixins/requires_consent_modelform_mixin.py +4 -5
  43. edc_consent/site_consents.py +13 -14
  44. edc_crf/crf_form_validator.py +13 -19
  45. edc_crf/crf_form_validator_mixins.py +2 -17
  46. edc_data_manager/admin/actions.py +1 -1
  47. edc_data_manager/admin/data_query_admin.py +1 -1
  48. edc_dx/form_validators/result_form_validator_mixin.py +1 -1
  49. edc_dx_review/medical_date.py +1 -1
  50. edc_egfr/egfr.py +4 -8
  51. edc_egfr/form_validator_mixins/egfr_form_validator_mixins.py +2 -2
  52. edc_facility/facility.py +7 -11
  53. edc_facility/holidays.py +3 -3
  54. edc_facility/models/holiday.py +1 -1
  55. edc_form_runners/form_runner.py +15 -16
  56. edc_form_validators/base_form_validator.py +5 -1
  57. edc_form_validators/date_range_validator.py +49 -61
  58. edc_form_validators/date_validator.py +1 -1
  59. edc_form_validators/extra_mixins/study_day_form_validator.py +1 -1
  60. edc_lab/form_validators/crf_requisition_form_validator_mixin.py +21 -15
  61. edc_lab/form_validators/requisition_form_validator_mixin.py +3 -3
  62. edc_lab_results/form_validator_mixins/blood_results_form_validator_mixin.py +1 -1
  63. edc_ltfu/modelform_mixins.py +1 -1
  64. edc_metadata/metadata/metadata.py +20 -7
  65. edc_metadata/metadata_rules/logic.py +5 -4
  66. edc_metadata/metadata_rules/predicate.py +22 -24
  67. edc_model_form/mixins/report_datetime_modelform_mixin.py +1 -5
  68. edc_offstudy/model_mixins/offstudy_model_mixin.py +1 -1
  69. edc_offstudy/modelform_mixins/crf/offstudy_crf_modelform_mixin.py +2 -2
  70. edc_offstudy/utils.py +4 -4
  71. edc_pdf_reports/crf_pdf_report.py +2 -1
  72. edc_pdutils/helper.py +3 -3
  73. edc_pdutils/utils/convert_dates_from_model.py +4 -3
  74. edc_pharmacy/admin/actions/confirm_stock.py +3 -3
  75. edc_pharmacy/admin/actions/delete_items_for_stock_request.py +4 -3
  76. edc_pharmacy/admin/actions/delete_order_items.py +7 -3
  77. edc_pharmacy/admin/actions/delete_receive_items.py +5 -3
  78. edc_pharmacy/admin/actions/process_repack_request.py +7 -11
  79. edc_pharmacy/admin/autocomplete_admin.py +1 -1
  80. edc_pharmacy/admin/list_filters.py +48 -46
  81. edc_pharmacy/admin/medication/assignment_admin.py +4 -4
  82. edc_pharmacy/admin/medication/dosage_guideline_admin.py +3 -3
  83. edc_pharmacy/admin/medication/formulation_admin.py +5 -5
  84. edc_pharmacy/admin/medication/medication_admin.py +3 -3
  85. edc_pharmacy/admin/prescription/rx_admin.py +2 -2
  86. edc_pharmacy/admin/prescription/rx_refill_admin.py +11 -17
  87. edc_pharmacy/admin/remove_fields_for_blinded_users.py +1 -1
  88. edc_pharmacy/admin/reports/stock_availability_admin.py +12 -8
  89. edc_pharmacy/admin/stock/allocation_admin.py +13 -17
  90. edc_pharmacy/admin/stock/allocation_proxy_admin.py +1 -2
  91. edc_pharmacy/admin/stock/confirmation_admin.py +4 -8
  92. edc_pharmacy/admin/stock/confirmation_at_site_item_admin.py +1 -1
  93. edc_pharmacy/admin/stock/container_admin.py +3 -3
  94. edc_pharmacy/admin/stock/location_admin.py +3 -6
  95. edc_pharmacy/admin/stock/lot_admin.py +9 -12
  96. edc_pharmacy/admin/stock/order_admin.py +2 -5
  97. edc_pharmacy/admin/stock/order_item_admin.py +14 -22
  98. edc_pharmacy/admin/stock/product_admin.py +6 -9
  99. edc_pharmacy/admin/stock/receive_admin.py +2 -2
  100. edc_pharmacy/admin/stock/receive_item_admin.py +3 -3
  101. edc_pharmacy/admin/stock/repack_request_admin.py +7 -10
  102. edc_pharmacy/admin/stock/stock_adjustment_admin.py +1 -1
  103. edc_pharmacy/admin/stock/stock_admin.py +17 -28
  104. edc_pharmacy/admin/stock/stock_proxy_admin.py +3 -4
  105. edc_pharmacy/admin/stock/stock_request_admin.py +6 -10
  106. edc_pharmacy/admin/stock/stock_request_item_admin.py +9 -18
  107. edc_pharmacy/admin/stock/stock_transfer_admin.py +5 -5
  108. edc_pharmacy/admin/stock/stock_transfer_item_admin.py +3 -3
  109. edc_pharmacy/admin/stock/storage_bin_admin.py +1 -1
  110. edc_pharmacy/admin/stock/storage_bin_item_admin.py +2 -2
  111. edc_pharmacy/form_validators/crf/study_medication_form_validator.py +27 -27
  112. edc_pharmacy/forms/stock/confirmation_form.py +2 -2
  113. edc_pharmacy/forms/stock/dispense_form.py +2 -2
  114. edc_pharmacy/forms/stock/lot_form.py +6 -6
  115. edc_pharmacy/forms/stock/order_form.py +4 -6
  116. edc_pharmacy/forms/stock/product_form.py +2 -3
  117. edc_pharmacy/forms/stock/receive_form.py +4 -4
  118. edc_pharmacy/forms/stock/receive_item_form.py +7 -5
  119. edc_pharmacy/forms/stock/repack_request_form.py +10 -8
  120. edc_pharmacy/forms/stock/stock_form.py +2 -3
  121. edc_pharmacy/forms/stock/stock_request_form.py +10 -8
  122. edc_pharmacy/forms/stock/stock_request_item_form.py +6 -5
  123. edc_pharmacy/forms/stock/stock_transfer_form.py +16 -14
  124. edc_pharmacy/forms/stock/supplier_form.py +2 -2
  125. edc_pharmacy/labels/label_data.py +2 -3
  126. edc_pharmacy/model_mixins/study_medication_crf_model_mixin.py +1 -1
  127. edc_pharmacy/models/medication/dosage_guideline.py +3 -3
  128. edc_pharmacy/models/model_mixins.py +12 -10
  129. edc_pharmacy/models/prescription/rx.py +1 -1
  130. edc_pharmacy/models/prescription/rx_refill.py +1 -1
  131. edc_pharmacy/models/signals.py +2 -3
  132. edc_pharmacy/models/storage/utils.py +4 -4
  133. edc_pharmacy/pdf_reports/manifest_pdf_report.py +3 -6
  134. edc_pharmacy/pdf_reports/stock_pdf_report.py +1 -3
  135. edc_pharmacy/refill/refill_creator.py +3 -4
  136. edc_protocol/validators.py +10 -16
  137. edc_reportable/forms/reportables_form_validator_mixin.py +1 -1
  138. edc_screening/form_validator_mixins.py +2 -1
  139. edc_transfer/form_validators.py +1 -1
  140. edc_transfer/model_mixins.py +1 -1
  141. edc_utils/age.py +17 -15
  142. edc_utils/date.py +7 -7
  143. edc_utils/text.py +7 -6
  144. edc_visit_schedule/exceptions.py +8 -0
  145. edc_visit_schedule/model_mixins/off_schedule_model_mixin.py +1 -1
  146. edc_visit_schedule/model_mixins/on_schedule_model_mixin.py +1 -1
  147. edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py +1 -3
  148. edc_visit_schedule/schedule/visit_collection.py +1 -1
  149. edc_visit_schedule/schedule/window.py +0 -2
  150. edc_visit_schedule/subject_schedule.py +1 -1
  151. edc_visit_schedule/utils.py +4 -4
  152. edc_visit_schedule/visit/visit.py +9 -3
  153. edc_visit_schedule/visit/window_period.py +1 -1
  154. edc_visit_tracking/form_validators/visit_form_validator.py +1 -1
  155. edc_metadata/metadata_wrappers/__init__.py +0 -5
  156. edc_metadata/metadata_wrappers/crf_metadata_wrapper.py +0 -5
  157. edc_metadata/metadata_wrappers/crf_metadata_wrappers.py +0 -8
  158. edc_metadata/metadata_wrappers/metadata_wrapper.py +0 -74
  159. edc_metadata/metadata_wrappers/metadata_wrappers.py +0 -33
  160. edc_metadata/metadata_wrappers/requisition_metadata_wrapper.py +0 -26
  161. edc_metadata/metadata_wrappers/requisition_metadata_wrappers.py +0 -10
  162. edc_pharmacy/management/__init__.py +0 -0
  163. edc_pharmacy/management/commands/__init__.py +0 -0
  164. edc_pharmacy/management/commands/update_initial_pharmacy_data.py +0 -10
  165. {clinicedc-2.0.34.dist-info → clinicedc-2.0.36.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.22
2
+ Generator: uv 0.8.23
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -6,8 +6,8 @@ from django import template
6
6
 
7
7
  from edc_auth.utils import get_user
8
8
  from edc_constants.constants import CANCELLED, CLOSED, HIGH_PRIORITY, NEW, OPEN
9
- from edc_utils import formatted_date
10
9
  from edc_utils.date import to_local
10
+ from edc_utils.text import formatted_date
11
11
 
12
12
  from ..models import ActionItem
13
13
  from ..site_action_items import site_action_items
@@ -9,7 +9,7 @@ from django.conf import settings
9
9
 
10
10
  from edc_constants.constants import CLOSED, OTHER
11
11
  from edc_form_validators.base_form_validator import INVALID_ERROR
12
- from edc_utils import convert_php_dateformat
12
+ from edc_utils.text import convert_php_dateformat
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from datetime import date
@@ -9,7 +9,7 @@ from django.conf import settings
9
9
  from django.core.exceptions import ObjectDoesNotExist
10
10
 
11
11
  from edc_constants.constants import DEAD
12
- from edc_utils import convert_php_dateformat
12
+ from edc_utils.text import convert_php_dateformat
13
13
 
14
14
  from ..constants import DEATH_REPORT_NOT_FOUND
15
15
  from ..utils import get_ae_model
@@ -46,9 +46,11 @@ class BaseRequiresDeathReportFormValidatorMixin:
46
46
  return self.death_report_model_cls.objects.get(
47
47
  subject_identifier=self.subject_identifier
48
48
  )
49
- except ObjectDoesNotExist:
49
+ except ObjectDoesNotExist as e:
50
50
  verbose_name = self.death_report_model_cls._meta.verbose_name
51
- self.raise_validation_error(f"`{verbose_name}` not found.", DEATH_REPORT_NOT_FOUND)
51
+ self.raise_validation_error(
52
+ f"`{verbose_name}` not found.", DEATH_REPORT_NOT_FOUND, exc=e
53
+ )
52
54
 
53
55
  @property
54
56
  def death_report_date(self) -> date:
@@ -25,8 +25,8 @@ class DeathReportTmgFormValidator(
25
25
  obj = get_ae_model("deathreport").objects.get(
26
26
  subject_identifier=self.cleaned_data.get("subject_identifier")
27
27
  )
28
- except ObjectDoesNotExist:
29
- self.raise_validation_error("Death report not found.", INVALID_ERROR)
28
+ except ObjectDoesNotExist as e:
29
+ self.raise_validation_error("Death report not found.", INVALID_ERROR, exc=e)
30
30
  death_date = getattr(obj, obj.death_date_field)
31
31
  try:
32
32
  death_date = death_date.date()
@@ -9,7 +9,7 @@ from edc_action_item.fieldsets import action_fieldset_tuple
9
9
  from edc_action_item.modeladmin_mixins import ActionItemModelAdminMixin
10
10
  from edc_constants.constants import NOT_APPLICABLE, OTHER
11
11
  from edc_model_admin.dashboard import ModelAdminSubjectDashboardMixin
12
- from edc_utils import convert_php_dateformat
12
+ from edc_utils.text import convert_php_dateformat
13
13
 
14
14
  from ..forms import AeTmgForm
15
15
  from ..models import AeClassification
@@ -8,7 +8,7 @@ from django.conf import settings
8
8
  from django.urls import reverse
9
9
 
10
10
  from edc_model.models import BaseUuidModel
11
- from edc_utils import convert_php_dateformat
11
+ from edc_utils.text import convert_php_dateformat
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from django.contrib import admin
@@ -11,7 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist
11
11
  from django.utils.translation import gettext as _
12
12
 
13
13
  from edc_model_admin.utils import add_to_messages_once
14
- from edc_utils import convert_php_dateformat
14
+ from edc_utils.text import convert_php_dateformat
15
15
 
16
16
  from .constants import TMG_ROLE
17
17
 
@@ -54,7 +54,7 @@ class AppointmentAdmin(
54
54
  ):
55
55
  show_cancel = True
56
56
  form = AppointmentForm
57
- actions = [appointment_mark_as_done, appointment_mark_as_new]
57
+ actions = (appointment_mark_as_done, appointment_mark_as_new)
58
58
  date_hierarchy = "appt_datetime"
59
59
  list_display = (
60
60
  "appointment_subject",
@@ -129,7 +129,7 @@ class AppointmentAdmin(
129
129
  audit_fieldset_tuple,
130
130
  )
131
131
 
132
- radio_fields = {
132
+ radio_fields = { # noqa: RUF012
133
133
  "appt_type": admin.VERTICAL,
134
134
  "appt_status": admin.VERTICAL,
135
135
  "appt_reason": admin.VERTICAL,
@@ -140,22 +140,22 @@ class AppointmentAdmin(
140
140
 
141
141
  def get_readonly_fields(self, request, obj=None) -> tuple:
142
142
  readonly_fields = super().get_readonly_fields(request, obj=obj)
143
- return (
144
- readonly_fields
145
- + visit_schedule_fields
146
- + (
143
+ return tuple(
144
+ {
145
+ *readonly_fields,
146
+ *visit_schedule_fields,
147
147
  "subject_identifier",
148
148
  "timepoint",
149
149
  "timepoint_datetime",
150
150
  "visit_code_sequence",
151
151
  "facility_name",
152
- )
152
+ }
153
153
  )
154
154
 
155
155
  def get_search_fields(self, request) -> tuple[str, ...]:
156
156
  search_fields = super().get_search_fields(request)
157
157
  if "subject_identifier" not in search_fields:
158
- search_fields = ("subject_identifier",) + search_fields
158
+ search_fields = tuple({"subject_identifier", *search_fields})
159
159
  return search_fields
160
160
 
161
161
  def has_delete_permission(self, request, obj=None):
@@ -165,19 +165,19 @@ class AppointmentAdmin(
165
165
  See `edc_visit_schedule.off_schedule_or_raise()`
166
166
  """
167
167
  has_delete_permission = super().has_delete_permission(request, obj=obj)
168
- if has_delete_permission and obj:
169
- if obj.visit_code_sequence == 0 or (
170
- obj.visit_code_sequence != 0 and obj.appt_status != NEW_APPT
171
- ):
172
- try:
173
- off_schedule_or_raise(
174
- subject_identifier=obj.subject_identifier,
175
- report_datetime=obj.appt_datetime,
176
- visit_schedule_name=obj.visit_schedule_name,
177
- schedule_name=obj.schedule_name,
178
- )
179
- except OnScheduleError:
180
- has_delete_permission = False
168
+ if (has_delete_permission and obj) and (
169
+ (obj.visit_code_sequence == 0)
170
+ or (obj.visit_code_sequence != 0 and obj.appt_status != NEW_APPT)
171
+ ):
172
+ try:
173
+ off_schedule_or_raise(
174
+ subject_identifier=obj.subject_identifier,
175
+ report_datetime=obj.appt_datetime,
176
+ visit_schedule_name=obj.visit_schedule_name,
177
+ schedule_name=obj.schedule_name,
178
+ )
179
+ except OnScheduleError:
180
+ has_delete_permission = False
181
181
  return has_delete_permission
182
182
 
183
183
  @admin.display(description="Timing", ordering="appt_timing")
@@ -262,7 +262,7 @@ class AppointmentAdmin(
262
262
  )
263
263
  return AppointmentType.objects.all().order_by("display_index")
264
264
 
265
- def get_appt_reason_choices(self, request) -> tuple[Any, ...]:
265
+ def get_appt_reason_choices(self, request) -> tuple[Any, ...]: # noqa: ARG002
266
266
  """Return a choices tuple.
267
267
 
268
268
  Important: left side of the tuple MUST have the default
@@ -280,13 +280,13 @@ class AppointmentAdmin(
280
280
  return tuple([tpl for tpl in APPT_TIMING if tpl[0] != NOT_APPLICABLE])
281
281
  return APPT_TIMING
282
282
 
283
- def allow_skipped_appointments(self, request) -> bool:
283
+ def allow_skipped_appointments(self, request) -> bool: # noqa: ARG002
284
284
  """Returns True if settings.EDC_APPOINTMENT_ALLOW_SKIPPED_APPT_USING
285
285
  has value.
286
286
 
287
287
  Relates to use of `SKIPPED_APPT` feature.
288
288
  """
289
- return True if get_allow_skipped_appt_using() else False
289
+ return bool(get_allow_skipped_appt_using())
290
290
 
291
291
  def get_queryset(self, request):
292
292
  qs = super().get_queryset(request)
@@ -34,10 +34,10 @@ class AppointmentStatusListFilter(SimpleListFilter):
34
34
  parameter_name = "appt_status"
35
35
  field_name = "appt_status"
36
36
 
37
- def lookups(self, request, model_admin) -> tuple:
38
- return APPT_STATUS + ((ATTENDED_APPT, "Attended (In progress, incomplete, done)"),)
37
+ def lookups(self, request, model_admin) -> tuple[tuple[str, str], ...]: # noqa: ARG002
38
+ return *APPT_STATUS, (ATTENDED_APPT, "Attended (In progress, incomplete, done)")
39
39
 
40
- def queryset(self, request, queryset):
40
+ def queryset(self, request, queryset): # noqa: ARG002
41
41
  qs = None
42
42
  if self.value() == ATTENDED_APPT:
43
43
  qs = queryset.filter(
@@ -52,7 +52,7 @@ class AppointmentOverdueListFilter(SimpleListFilter):
52
52
  title = "Overdue (days)"
53
53
  parameter_name = "overdue"
54
54
 
55
- def lookups(self, request, model_admin) -> tuple:
55
+ def lookups(self, request, model_admin) -> tuple[tuple[str, str], ...]: # noqa: ARG002
56
56
  return (
57
57
  (LT_30_DAYS, _("2-30 days")),
58
58
  (GTE_30_TO_60_DAYS, _("30-60 days")),
@@ -61,7 +61,7 @@ class AppointmentOverdueListFilter(SimpleListFilter):
61
61
  (GTE_180, _("180+ days")),
62
62
  )
63
63
 
64
- def queryset(self, request, queryset) -> QuerySet | None:
64
+ def queryset(self, request, queryset) -> QuerySet | None: # noqa: ARG002
65
65
  now = timezone.now().replace(second=59, hour=23, minute=59)
66
66
  qs = None
67
67
  if self.value() == LT_30_DAYS:
@@ -48,9 +48,10 @@ class AppointmentReasonUpdater(MetadataHelperMixin):
48
48
 
49
49
  def __init__(
50
50
  self,
51
- appointment: Appointment = None,
52
- appt_timing: str = None,
53
- appt_reason: str = None,
51
+ *,
52
+ appointment: Appointment,
53
+ appt_timing: str,
54
+ appt_reason: str,
54
55
  commit: bool | None = None,
55
56
  ):
56
57
  self._related_visit = None
@@ -72,7 +73,7 @@ class AppointmentReasonUpdater(MetadataHelperMixin):
72
73
  appointment=appointment, appt_timing=self.appt_timing
73
74
  )
74
75
  except (AppointmentBaselineError, UnscheduledAppointmentError) as e:
75
- raise AppointmentReasonUpdaterError(e)
76
+ raise AppointmentReasonUpdaterError(e) from e
76
77
 
77
78
  self.appt_reason = appt_reason or self.appointment.appt_reason
78
79
 
@@ -163,7 +164,7 @@ class AppointmentReasonUpdater(MetadataHelperMixin):
163
164
  )
164
165
  elif self.requisition_metadata_keyed_exists:
165
166
  raise AppointmentReasonUpdaterRequisitionsExistsError(
166
- "Invalid. Requisitions have already been entered " "for this timepoint."
167
+ "Invalid. Requisitions have already been entered for this timepoint."
167
168
  )
168
169
 
169
170
  @property
@@ -33,9 +33,9 @@ class AppointmentStatusUpdater:
33
33
  self.appointment.appt_status = IN_PROGRESS_APPT
34
34
  self.appointment.save_base(update_fields=["appt_status"])
35
35
  if clear_others_in_progress:
36
- for appointment in self.appointment.__class__.objects.filter(
36
+ for appt in self.appointment.__class__.objects.filter(
37
37
  visit_schedule_name=self.appointment.visit_schedule_name,
38
38
  schedule_name=self.appointment.schedule_name,
39
39
  appt_status=IN_PROGRESS_APPT,
40
40
  ).exclude(id=self.appointment.id):
41
- update_appt_status(appointment, save=True)
41
+ update_appt_status(appt, save=True)
@@ -8,11 +8,10 @@ from edc_appointment.constants import (
8
8
 
9
9
 
10
10
  def constants(request) -> dict:
11
- dct = dict(
11
+ return dict(
12
12
  COMPLETE_APPT=COMPLETE_APPT,
13
13
  INCOMPLETE_APPT=INCOMPLETE_APPT,
14
14
  IN_PROGRESS_APPT=IN_PROGRESS_APPT,
15
15
  MISSED_APPT=MISSED_APPT,
16
16
  NEW_APPT=NEW_APPT,
17
17
  )
18
- return dct
@@ -38,16 +38,17 @@ if TYPE_CHECKING:
38
38
  class AppointmentCreator:
39
39
  def __init__(
40
40
  self,
41
- timepoint_datetime: datetime = None,
41
+ *,
42
+ subject_identifier: str,
43
+ visit: Visit, # from edc_visit_schedule
44
+ visit_schedule_name: str,
45
+ schedule_name: str,
46
+ timepoint_datetime: datetime,
42
47
  timepoint: Decimal | None = None,
43
- visit: Visit | None = None, # from edc_visit_schedule
44
48
  visit_code_sequence: int | None = None,
45
49
  facility: Facility | None = None,
46
- appointment_model: str = None,
50
+ appointment_model: str | None = None,
47
51
  taken_datetimes: list[datetime] | None = None,
48
- subject_identifier: str = None,
49
- visit_schedule_name: str = None,
50
- schedule_name: str = None,
51
52
  default_appt_type: str | None = None,
52
53
  default_appt_reason: str | None = None,
53
54
  appt_status: str | None = None,
@@ -89,10 +90,10 @@ class AppointmentCreator:
89
90
  f"Naive datetime not allowed. {self!r}. Got {timepoint_datetime}"
90
91
  )
91
92
  self.timepoint_datetime = timepoint_datetime
92
- except AttributeError:
93
+ except AttributeError as e:
93
94
  raise AppointmentCreatorError(
94
95
  f"Expected 'timepoint_datetime'. Got None. {self!r}."
95
- )
96
+ ) from e
96
97
  # suggested_datetime (defaults to timepoint_datetime)
97
98
  # If provided, the rules for window period/rdelta relative
98
99
  # to timepoint_datetime still apply.
@@ -6,8 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
  from dateutil.relativedelta import relativedelta
7
7
  from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
8
8
 
9
- from edc_utils import formatted_datetime, to_utc
10
- from edc_utils.date import to_local
9
+ from edc_utils import formatted_date, formatted_datetime, to_local
11
10
  from edc_visit_schedule.site_visit_schedules import site_visit_schedules
12
11
  from edc_visit_schedule.utils import get_lower_datetime
13
12
 
@@ -49,13 +48,14 @@ class UnscheduledAppointmentCreator:
49
48
 
50
49
  def __init__(
51
50
  self,
52
- subject_identifier: str = None,
53
- visit_schedule_name: str = None,
54
- schedule_name: str = None,
55
- visit_code: str = None,
56
- suggested_visit_code_sequence: int = None,
51
+ *,
52
+ subject_identifier: str,
53
+ visit_schedule_name: str,
54
+ schedule_name: str,
55
+ visit_code: str,
56
+ suggested_visit_code_sequence: int | None = None,
57
57
  suggested_appt_datetime: datetime | None = None,
58
- facility: Facility = None,
58
+ facility: Facility | None = None,
59
59
  request: Any | None = None,
60
60
  ):
61
61
  self._parent_appointment = None
@@ -125,7 +125,7 @@ class UnscheduledAppointmentCreator:
125
125
  appt_status=IN_PROGRESS_APPT,
126
126
  )
127
127
  except MultipleObjectsReturned as e:
128
- raise UnscheduledAppointmentError(e)
128
+ raise UnscheduledAppointmentError(e) from e
129
129
  except ObjectDoesNotExist:
130
130
  pass
131
131
  else:
@@ -154,7 +154,7 @@ class UnscheduledAppointmentCreator:
154
154
  msg = str(e).replace("Perhaps catch this in the form", "")
155
155
  raise UnscheduledAppointmentError(
156
156
  f"Unable to create unscheduled appointment. {msg}"
157
- )
157
+ ) from e
158
158
  self.appointment = appointment_creator.appointment
159
159
 
160
160
  def has_perm_or_raise(self, request) -> None:
@@ -170,25 +170,32 @@ class UnscheduledAppointmentCreator:
170
170
  def suggested_appt_datetime(self):
171
171
  return self._suggested_appt_datetime
172
172
 
173
+ def after_calling_appt_or_raise(self, suggested_dte: datetime):
174
+ """Raises if on same day or before otherwise returns True"""
175
+ suggested_dt = to_local(suggested_dte).date()
176
+ calling_dt = to_local(self.calling_appointment.appt_datetime).date()
177
+ if suggested_dt <= calling_dt:
178
+ suggested = formatted_date(suggested_dt)
179
+ calling = formatted_date(calling_dt)
180
+ raise CreateAppointmentError(
181
+ "Suggested appointment date must be after the calling appointment date. "
182
+ f"Got {suggested} not after {calling}."
183
+ )
184
+ return True
185
+
173
186
  @suggested_appt_datetime.setter
174
- def suggested_appt_datetime(self, value: datetime | None):
175
- if value:
176
- if to_utc(value).date() <= self.calling_appointment.appt_datetime.date():
177
- raise CreateAppointmentError(
178
- "Suggested date/time must be after the calling appointment date/time"
179
- )
180
- self._suggested_appt_datetime = value
187
+ def suggested_appt_datetime(self, suggested_dte: datetime | None):
188
+ if suggested_dte and self.after_calling_appt_or_raise(suggested_dte):
189
+ self._suggested_appt_datetime = suggested_dte
181
190
  else:
182
191
  self._suggested_appt_datetime = (
183
192
  self.calling_appointment.appt_datetime + relativedelta(days=1)
184
193
  )
185
- if self.parent_appointment.next and to_utc(
186
- self.suggested_appt_datetime
187
- ) >= get_lower_datetime(self.parent_appointment.next):
188
- dt = formatted_datetime(to_local(self.suggested_appt_datetime))
189
- next_dt = formatted_datetime(
190
- to_local(get_lower_datetime(self.parent_appointment.next))
191
- )
194
+ if self.parent_appointment.next and self.suggested_appt_datetime >= get_lower_datetime(
195
+ self.parent_appointment.next
196
+ ):
197
+ dt = formatted_datetime(self.suggested_appt_datetime)
198
+ next_dt = formatted_datetime(get_lower_datetime(self.parent_appointment.next))
192
199
  raise UnscheduledAppointmentError(
193
200
  "Appointment date exceeds window period. Next appointment is "
194
201
  f"{self.parent_appointment.next.visit_code} and lower window starts "
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from datetime import datetime
4
5
  from typing import TYPE_CHECKING
5
6
 
@@ -7,7 +8,6 @@ from dateutil.relativedelta import relativedelta
7
8
  from django.db import transaction
8
9
 
9
10
  from ..constants import INCOMPLETE_APPT, NEW_APPT
10
- from ..utils import get_appointment_by_datetime
11
11
  from .unscheduled_appointment_creator import (
12
12
  CreateAppointmentError,
13
13
  UnscheduledAppointmentCreator,
@@ -18,13 +18,14 @@ if TYPE_CHECKING:
18
18
 
19
19
 
20
20
  def create_next_appointment_as_interim(
21
- next_appt_datetime: datetime = None, appointment: Appointment = None
21
+ appointment: Appointment, next_appt_datetime: datetime | None = None
22
22
  ) -> Appointment | None:
23
- return create_unscheduled_appointment(next_appt_datetime, appointment)
23
+ return create_unscheduled_appointment(appointment, next_appt_datetime=next_appt_datetime)
24
24
 
25
25
 
26
26
  def create_unscheduled_appointment(
27
- next_appt_datetime: datetime = None, appointment: Appointment = None
27
+ appointment: Appointment,
28
+ next_appt_datetime: datetime | None = None,
28
29
  ) -> Appointment | None:
29
30
  """Create and return an unscheduled appointment if
30
31
  `next_appt_datetime` is within the window period of
@@ -35,36 +36,33 @@ def create_unscheduled_appointment(
35
36
  next_appt_datetime = next_appt_datetime or appointment.appt_datetime + relativedelta(
36
37
  days=1
37
38
  )
38
- next_appointment = get_appointment_by_datetime(
39
- next_appt_datetime,
40
- appointment.subject_identifier,
41
- appointment.visit_schedule_name,
42
- appointment.schedule_name,
43
- raise_if_in_gap=False,
44
- )
45
- if appointment == next_appointment:
46
- if (
47
- appointment.relative_next
48
- and appointment.relative_next.appt_status == NEW_APPT
49
- and appointment.relative_next.visit_code_sequence > 0
50
- ):
51
- appointment.relative_next.delete()
52
- appt_status = appointment.appt_status
53
- appointment.appt_status = INCOMPLETE_APPT
54
- appointment.save_base(update_fields=["appt_status"])
55
- with transaction.atomic():
56
- try:
57
- unscheduled_appointment = UnscheduledAppointmentCreator(
58
- subject_identifier=appointment.subject_identifier,
59
- visit_schedule_name=appointment.visit_schedule_name,
60
- schedule_name=appointment.schedule_name,
61
- visit_code=appointment.visit_code,
62
- facility=appointment.facility,
63
- suggested_appt_datetime=next_appt_datetime,
64
- suggested_visit_code_sequence=appointment.visit_code_sequence + 1,
65
- )
66
- except CreateAppointmentError:
67
- pass
68
- appointment.appt_status = appt_status
69
- appointment.save_base(update_fields=["appt_status"])
70
- return unscheduled_appointment
39
+ # next_appointment = get_appointment_by_datetime(
40
+ # next_appt_datetime,
41
+ # appointment.subject_identifier,
42
+ # appointment.visit_schedule_name,
43
+ # appointment.schedule_name,
44
+ # raise_if_in_gap=False,
45
+ # )
46
+ # if appointment == next_appointment:
47
+ if (
48
+ appointment.relative_next
49
+ and appointment.relative_next.appt_status == NEW_APPT
50
+ and appointment.relative_next.visit_code_sequence > 0
51
+ ):
52
+ appointment.relative_next.delete()
53
+ appt_status = appointment.appt_status
54
+ appointment.appt_status = INCOMPLETE_APPT
55
+ appointment.save_base(update_fields=["appt_status"])
56
+ with transaction.atomic(), contextlib.suppress(CreateAppointmentError):
57
+ unscheduled_appointment = UnscheduledAppointmentCreator(
58
+ subject_identifier=appointment.subject_identifier,
59
+ visit_schedule_name=appointment.visit_schedule_name,
60
+ schedule_name=appointment.schedule_name,
61
+ visit_code=appointment.visit_code,
62
+ facility=appointment.facility,
63
+ suggested_appt_datetime=next_appt_datetime,
64
+ suggested_visit_code_sequence=appointment.visit_code_sequence + 1,
65
+ )
66
+ appointment.appt_status = appt_status
67
+ appointment.save_base(update_fields=["appt_status"])
68
+ return getattr(unscheduled_appointment, "appointment", None)
@@ -30,7 +30,7 @@ class AppointmentDatetimeError(Exception):
30
30
  pass
31
31
 
32
32
 
33
- class UnknownVisitCode(Exception):
33
+ class UnknownVisitCode(Exception): # noqa: N818
34
34
  pass
35
35
 
36
36
 
@@ -38,7 +38,7 @@ class AppointmentWindowError(Exception):
38
38
  pass
39
39
 
40
40
 
41
- class AppointmentPermissionsRequired(Exception):
41
+ class AppointmentPermissionsRequired(Exception): # noqa: N818
42
42
  pass
43
43
 
44
44
 
@@ -46,7 +46,7 @@ class AppointmentMissingValuesError(Exception):
46
46
  pass
47
47
 
48
48
 
49
- class UnscheduledAppointmentNotAllowed(Exception):
49
+ class UnscheduledAppointmentNotAllowed(Exception): # noqa: N818
50
50
  pass
51
51
 
52
52
 
@@ -7,5 +7,5 @@ from edc_form_runners.form_runner import FormRunner
7
7
  @register()
8
8
  class AppointmentFormRunner(FormRunner):
9
9
  model_name = "edc_appointment.appointment"
10
- extra_fieldnames = ["appt_datetime"]
11
- exclude_formfields = ["appt_close_datetime"]
10
+ extra_fieldnames = ("appt_datetime",)
11
+ exclude_formfields = ("appt_close_datetime",)
@@ -82,7 +82,7 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
82
82
  visit_code=self.visit_code,
83
83
  visit_code_sequence=0,
84
84
  )
85
- except ObjectDoesNotExist:
85
+ except ObjectDoesNotExist as e:
86
86
  self.raise_validation_error(
87
87
  {
88
88
  self.visit_code_fld: (
@@ -91,6 +91,7 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
91
91
  )
92
92
  },
93
93
  INVALID_ERROR,
94
+ exc=e,
94
95
  )
95
96
  if instance == self.related_visit.appointment:
96
97
  self.raise_validation_error(
@@ -117,7 +118,7 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
117
118
  appointment_validator.validate()
118
119
  except ValidationError as e:
119
120
  if e.message_dict.get("appt_datetime"):
120
- raise ValidationError({"appt_date": e.message_dict["appt_datetime"]})
121
+ raise ValidationError({"appt_date": e.message_dict["appt_datetime"]}) from e
121
122
  raise
122
123
 
123
124
  @property
@@ -134,9 +135,8 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
134
135
 
135
136
  @property
136
137
  def clinic_days(self) -> list[int]:
137
- if not self._clinic_days:
138
- if self.cleaned_data.get("health_facility"):
139
- self._clinic_days = self.health_facility.clinic_days
138
+ if not self._clinic_days and self.cleaned_data.get("health_facility"):
139
+ self._clinic_days = self.health_facility.clinic_days
140
140
  return self._clinic_days
141
141
 
142
142
  def validate_date_is_on_clinic_day(self):
@@ -38,7 +38,7 @@ class WindowPeriodFormValidatorMixin:
38
38
  and appointment.visit_code_sequence > 0
39
39
  and appointment.next
40
40
  and appointment.next.appt_status in [INCOMPLETE_APPT, COMPLETE_APPT]
41
- and to_local(proposed_appt_datetime) < to_local(appointment.next.appt_datetime)
41
+ and proposed_appt_datetime < appointment.next.appt_datetime
42
42
  ):
43
43
  value = True
44
44
  return value
@@ -52,13 +52,11 @@ class WindowPeriodFormValidatorMixin:
52
52
  if proposed_appt_datetime:
53
53
  try:
54
54
  appointment.schedule.datetime_in_window(
55
- timepoint_datetime=to_local(appointment.timepoint_datetime),
56
- dt=to_local(proposed_appt_datetime),
55
+ timepoint_datetime=appointment.timepoint_datetime,
56
+ dt=proposed_appt_datetime,
57
57
  visit_code=appointment.visit_code,
58
58
  visit_code_sequence=appointment.visit_code_sequence,
59
- baseline_timepoint_datetime=to_local(
60
- self.baseline_timepoint_datetime(appointment)
61
- ),
59
+ baseline_timepoint_datetime=self.baseline_timepoint_datetime(appointment),
62
60
  )
63
61
  except UnScheduledVisitWindowError:
64
62
  if not self.ignore_window_period_for_unscheduled(
@@ -90,7 +88,9 @@ class WindowPeriodFormValidatorMixin:
90
88
  UNSCHEDULED_WINDOW_ERROR,
91
89
  )
92
90
  except ScheduledVisitWindowError as e:
93
- self.raise_validation_error({form_field: (str(e))}, SCHEDULED_WINDOW_ERROR)
91
+ self.raise_validation_error(
92
+ {form_field: (str(e))}, SCHEDULED_WINDOW_ERROR, exc=e
93
+ )
94
94
 
95
95
  @staticmethod
96
96
  def baseline_timepoint_datetime(appointment: Appointment) -> datetime: