clinicedc 2.0.33__py3-none-any.whl → 2.0.35__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.33.dist-info → clinicedc-2.0.35.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.33.dist-info → clinicedc-2.0.35.dist-info}/RECORD +155 -165
  3. {clinicedc-2.0.33.dist-info → clinicedc-2.0.35.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 +26 -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.33.dist-info → clinicedc-2.0.35.dist-info}/licenses/LICENSE +0 -0
@@ -17,8 +17,8 @@ from edc_form_validators import INVALID_ERROR
17
17
  from edc_form_validators.form_validator import FormValidator
18
18
  from edc_metadata.metadata_helper import MetadataHelperMixin
19
19
  from edc_sites.form_validator_mixin import SiteFormValidatorMixin
20
- from edc_utils import formatted_datetime, to_utc
21
20
  from edc_utils.date import to_local
21
+ from edc_utils.text import formatted_datetime
22
22
  from edc_visit_schedule.site_visit_schedules import site_visit_schedules
23
23
  from edc_visit_schedule.subject_schedule import NotOnScheduleError
24
24
  from edc_visit_schedule.utils import get_onschedule_model_instance, is_baseline
@@ -219,11 +219,7 @@ class AppointmentFormValidator(
219
219
  def validate_not_future_appt_datetime(self: Any) -> None:
220
220
  appt_datetime = self.cleaned_data.get("appt_datetime")
221
221
  appt_status = self.cleaned_data.get("appt_status")
222
- if (
223
- appt_datetime
224
- and appt_status != NEW_APPT
225
- and to_utc(appt_datetime) > timezone.now()
226
- ):
222
+ if appt_datetime and appt_status != NEW_APPT and appt_datetime > timezone.now():
227
223
  self.raise_validation_error(
228
224
  {"appt_datetime": "Cannot be a future date/time."},
229
225
  INVALID_APPT_DATE,
@@ -242,14 +238,12 @@ class AppointmentFormValidator(
242
238
  appt_datetime = self.cleaned_data.get("appt_datetime")
243
239
  appt_status = self.cleaned_data.get("appt_status")
244
240
  if appt_datetime and appt_status != NEW_APPT:
245
- appt_datetime_utc = to_utc(appt_datetime)
246
241
  consent_datetime = self.get_consent_datetime_or_raise(
247
- report_datetime=appt_datetime_utc,
248
- site=self.site,
249
- fldname="appt_datetime",
242
+ reference_datetime=appt_datetime,
243
+ reference_datetime_field="appt_datetime",
250
244
  error_code=INVALID_APPT_DATE,
251
245
  )
252
- if appt_datetime_utc.date() < consent_datetime.date():
246
+ if to_local(appt_datetime).date() < to_local(consent_datetime).date():
253
247
  formatted_date = formatted_datetime(
254
248
  to_local(consent_datetime), format_as_date=True
255
249
  )
@@ -283,11 +277,11 @@ class AppointmentFormValidator(
283
277
  )
284
278
  except AppointmentBaselineError as e:
285
279
  self.raise_validation_error(
286
- {"appt_timing": str(e)}, INVALID_APPT_STATUS_AT_BASELINE
280
+ {"appt_timing": str(e)}, INVALID_APPT_STATUS_AT_BASELINE, exc=e
287
281
  )
288
282
  except UnscheduledAppointmentError as e:
289
283
  self.raise_validation_error(
290
- {"appt_timing": str(e)}, INVALID_MISSED_APPT_NOT_ALLOWED
284
+ {"appt_timing": str(e)}, INVALID_MISSED_APPT_NOT_ALLOWED, exc=e
291
285
  )
292
286
 
293
287
  try:
@@ -299,14 +293,14 @@ class AppointmentFormValidator(
299
293
  )
300
294
  except AppointmentReasonUpdaterCrfsExistsError as e:
301
295
  self.raise_validation_error(
302
- {"appt_timing": str(e)}, INVALID_APPT_TIMING_CRFS_EXIST
296
+ {"appt_timing": str(e)}, INVALID_APPT_TIMING_CRFS_EXIST, exc=e
303
297
  )
304
298
  except AppointmentReasonUpdaterRequisitionsExistsError as e:
305
299
  self.raise_validation_error(
306
- {"appt_timing": str(e)}, INVALID_APPT_TIMING_REQUISITIONS_EXIST
300
+ {"appt_timing": str(e)}, INVALID_APPT_TIMING_REQUISITIONS_EXIST, exc=e
307
301
  )
308
302
  except AppointmentReasonUpdaterError as e:
309
- self.raise_validation_error({"appt_timing": str(e)}, INVALID_APPT_TIMING)
303
+ self.raise_validation_error({"appt_timing": str(e)}, INVALID_APPT_TIMING, exc=e)
310
304
 
311
305
  def validate_appt_datetime_not_before_previous_appt_datetime(self):
312
306
  appt_datetime = self.cleaned_data.get("appt_datetime")
@@ -316,7 +310,7 @@ class AppointmentFormValidator(
316
310
  and appt_status
317
311
  and appt_status != NEW_APPT
318
312
  and self.instance.relative_previous
319
- and to_utc(appt_datetime) < self.instance.relative_previous.appt_datetime
313
+ and appt_datetime < self.instance.relative_previous.appt_datetime
320
314
  ):
321
315
  formatted_date = formatted_datetime(self.instance.relative_previous.appt_datetime)
322
316
  self.raise_validation_error(
@@ -338,7 +332,7 @@ class AppointmentFormValidator(
338
332
  and appt_status
339
333
  and appt_status != NEW_APPT
340
334
  and self.instance.relative_next
341
- and to_utc(appt_datetime) > self.instance.relative_next.appt_datetime
335
+ and appt_datetime > self.instance.relative_next.appt_datetime
342
336
  ):
343
337
  formatted_date = formatted_datetime(self.instance.relative_next.appt_datetime)
344
338
  self.raise_validation_error(
@@ -425,7 +419,7 @@ class AppointmentFormValidator(
425
419
  {
426
420
  "appt_status": format_html(
427
421
  'Invalid. Not all <a href="{url}">required CRFs</a> have been keyed',
428
- url=mark_safe(url), # nosec B703, B308
422
+ url=mark_safe(url), # nosec B703, B308 # noqa: S308
429
423
  )
430
424
  },
431
425
  INVALID_APPT_STATUS,
@@ -443,7 +437,7 @@ class AppointmentFormValidator(
443
437
  'Invalid. Not all <a href="{url}">required requisitions</a> '
444
438
  "have been keyed"
445
439
  ),
446
- url=mark_safe(url), # nosec B703, B308
440
+ url=mark_safe(url), # nosec B703, B308 # noqa: S308
447
441
  )
448
442
  },
449
443
  INVALID_APPT_STATUS,
@@ -599,12 +593,11 @@ class AppointmentFormValidator(
599
593
  def changelist_url(self: Any, model_name: str) -> Any:
600
594
  """Returns the model's changelist url with filter querystring"""
601
595
  url = reverse(f"edc_metadata_admin:edc_metadata_{model_name}_changelist")
602
- url = (
596
+ return (
603
597
  f"{url}?q={self.subject_identifier}"
604
598
  f"&visit_code={self.instance.visit_code}"
605
599
  f"&visit_code_sequence={self.instance.visit_code_sequence}"
606
600
  )
607
- return url
608
601
 
609
602
  def validate_subject_on_schedule(self: Any) -> None:
610
603
  if self.cleaned_data.get("appt_datetime"):
@@ -623,7 +616,7 @@ class AppointmentFormValidator(
623
616
  subject_identifier=subject_identifier,
624
617
  visit_schedule_name=self.instance.visit_schedule_name,
625
618
  schedule_name=self.instance.schedule_name,
626
- reference_datetime=to_utc(appt_datetime),
619
+ reference_datetime=appt_datetime,
627
620
  )
628
621
  except NotOnScheduleError:
629
622
  self.raise_validation_error(
@@ -18,7 +18,7 @@ def close_appointments():
18
18
  form.save(commit=False)
19
19
  except ValueError:
20
20
  obj.refresh_from_db()
21
- print(
21
+ print( # noqa: T201
22
22
  obj.subject_identifier,
23
23
  obj.visit_code,
24
24
  obj.visit_code_sequence,
@@ -42,7 +42,7 @@ def close_appointments():
42
42
  pass
43
43
  else:
44
44
  obj.refresh_from_db()
45
- print(
45
+ print( # noqa: T201
46
46
  obj.subject_identifier,
47
47
  obj.visit_code,
48
48
  obj.visit_code_sequence,
@@ -52,5 +52,5 @@ def close_appointments():
52
52
 
53
53
 
54
54
  class Command(BaseCommand):
55
- def handle(self, *args, **options):
55
+ def handle(self, *args, **options): # noqa: ARG002
56
56
  close_appointments()
@@ -14,7 +14,7 @@ class Command(BaseCommand):
14
14
  "and reset if needed."
15
15
  )
16
16
 
17
- def handle(self, *args, **options):
17
+ def handle(self, *args, **options): # noqa: ARG002
18
18
  sys.stdout.write(
19
19
  "Validating (and resetting, if needed) appointment visit code sequences ...\n"
20
20
  )
@@ -8,9 +8,9 @@ from edc_appointment.utils import update_appt_status
8
8
  class Command(BaseCommand):
9
9
  help = "Update appointment status for all appointments"
10
10
 
11
- def handle(self, *args, **options) -> None:
11
+ def handle(self, *args, **options) -> None: # noqa: ARG002
12
12
  appointments = Appointment.objects.all().order_by("subject_identifier")
13
13
  total = appointments.count()
14
14
  for appointment in tqdm(appointments, total=total):
15
15
  update_appt_status(appointment, save=True)
16
- print("\n\nDone")
16
+ print("\n\nDone") # noqa: T201
@@ -17,9 +17,9 @@ style = color_style()
17
17
  class Command(BaseCommand):
18
18
  help = "Update skipped appointments"
19
19
 
20
- def handle(self, *args, **options) -> None:
20
+ def handle(self, *args, **options) -> None: # noqa: ARG002
21
21
  errors: dict[str, list[str]] = {}
22
- for model, _ in get_allow_skipped_appt_using().items():
22
+ for model in get_allow_skipped_appt_using():
23
23
  crf_model_cls = django_apps.get_model(model)
24
24
  qs = RegisteredSubject.objects.all().order_by("subject_identifier")
25
25
  total = qs.count()
@@ -47,10 +47,10 @@ class Command(BaseCommand):
47
47
  errors[subject_visit.subject_identifier].append(msg)
48
48
  except KeyError:
49
49
  errors.update({subject_visit.subject_identifier: [msg]})
50
- print(msg)
51
- print("\nERRORS\n")
50
+ print(msg) # noqa: T201
51
+ print("\nERRORS\n") # noqa: T201
52
52
  for k, v in errors.items():
53
- print(f"{k} ---------------")
53
+ print(f"{k} ---------------") # noqa: T201
54
54
  for msg in v:
55
- print(msg)
56
- print("\n\nDone")
55
+ print(msg) # noqa: T201
56
+ print("\n\nDone") # noqa: T201
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from typing import TYPE_CHECKING, Any
4
5
 
5
6
  from django.db import models, transaction
@@ -43,7 +44,7 @@ class AppointmentManager(models.Manager):
43
44
  )
44
45
 
45
46
  @staticmethod
46
- def get_query_options(**kwargs) -> dict[Any]:
47
+ def get_query_options(**kwargs) -> dict[str, Any]:
47
48
  """Returns a dictionary or options.
48
49
 
49
50
  Dictionary is based on the appointment instance or everything
@@ -53,14 +54,14 @@ class AppointmentManager(models.Manager):
53
54
  schedule_name = kwargs.get("schedule_name")
54
55
  subject_identifier = kwargs.get("subject_identifier")
55
56
  visit_schedule_name = kwargs.get("visit_schedule_name")
56
- options: dict[Any] = dict(visit_code_sequence=0)
57
+ options = dict(visit_code_sequence=0)
57
58
  try:
58
59
  options.update(
59
60
  subject_identifier=appointment.subject_identifier,
60
61
  visit_schedule_name=appointment.visit_schedule_name,
61
62
  schedule_name=appointment.schedule_name,
62
63
  )
63
- except AttributeError:
64
+ except AttributeError as e:
64
65
  options.update(subject_identifier=subject_identifier)
65
66
  try:
66
67
  visit_schedule_name, schedule_name = visit_schedule_name.split(".")
@@ -75,7 +76,7 @@ class AppointmentManager(models.Manager):
75
76
  raise TypeError(
76
77
  f"Expected visit_schedule_name for schedule_name "
77
78
  f"'{schedule_name}'. Got {visit_schedule_name}"
78
- )
79
+ ) from e
79
80
  if schedule_name:
80
81
  options.update(schedule_name=schedule_name)
81
82
  return options
@@ -230,10 +231,8 @@ class AppointmentManager(models.Manager):
230
231
  f"appt_datetime__{op}": cutoff_datetime,
231
232
  }
232
233
  if not is_offstudy:
233
- try:
234
+ with contextlib.suppress(ValueError, AttributeError):
234
235
  visit_schedule_name, schedule_name = visit_schedule_name.split(".")
235
- except (ValueError, AttributeError):
236
- pass
237
236
  if not schedule_name or not visit_schedule_name:
238
237
  raise AppointmentManagerError(
239
238
  f"Expected both the visit_schedule_name and schedule_name. "
@@ -17,7 +17,7 @@ from edc_identifier.model_mixins import NonUniqueSubjectIdentifierFieldMixin
17
17
  from edc_metadata.model_mixins import MetadataHelperModelMixin
18
18
  from edc_offstudy.model_mixins import OffstudyNonCrfModelMixin
19
19
  from edc_timepoint.model_mixins import TimepointModelMixin
20
- from edc_utils import formatted_datetime, to_utc
20
+ from edc_utils.text import formatted_datetime
21
21
  from edc_visit_schedule.model_mixins import VisitScheduleModelMixin
22
22
  from edc_visit_schedule.site_visit_schedules import site_visit_schedules
23
23
  from edc_visit_schedule.subject_schedule import NotOnScheduleError
@@ -78,8 +78,7 @@ class AppointmentModelMixin(
78
78
  schedule.onschedule_model
79
79
  ).objects.get(
80
80
  subject_identifier=self.subject_identifier,
81
- onschedule_datetime__lte=to_utc(self.appt_datetime)
82
- + relativedelta(seconds=1),
81
+ onschedule_datetime__lte=self.appt_datetime + relativedelta(seconds=1),
83
82
  )
84
83
  except ObjectDoesNotExist as e:
85
84
  dte_as_str = formatted_datetime(self.appt_datetime)
@@ -92,7 +91,7 @@ class AppointmentModelMixin(
92
91
  # update appointment timepoints
93
92
  schedule.put_on_schedule(
94
93
  subject_identifier=self.subject_identifier,
95
- onschedule_datetime=to_utc(self.appt_datetime),
94
+ onschedule_datetime=self.appt_datetime,
96
95
  skip_baseline=True,
97
96
  )
98
97
  else:
@@ -46,7 +46,7 @@ class NextAppointmentCrfModelAdminMixin(admin.ModelAdmin):
46
46
  audit_fieldset_tuple,
47
47
  )
48
48
 
49
- radio_fields = {
49
+ radio_fields = { # noqa: RUF012
50
50
  "offschedule_today": admin.VERTICAL,
51
51
  "crf_status": admin.VERTICAL,
52
52
  "info_source": admin.VERTICAL,
@@ -103,5 +103,5 @@ class NextAppointmentCrfModelAdminMixin(admin.ModelAdmin):
103
103
  site=next_appt.site
104
104
  )
105
105
 
106
- def get_default_info_source(self, request):
106
+ def get_default_info_source(self, request): # noqa: ARG002
107
107
  return django_apps.get_model("edc_appointment.infosources").objects.get(name=PATIENT)
@@ -8,8 +8,8 @@ from django.conf import settings
8
8
  from django.utils.translation import gettext_lazy as _
9
9
 
10
10
  from edc_metadata.utils import has_keyed_metadata
11
- from edc_utils import convert_php_dateformat
12
11
  from edc_utils.date import to_local
12
+ from edc_utils.text import convert_php_dateformat
13
13
  from edc_visit_schedule.exceptions import ScheduledVisitWindowError
14
14
 
15
15
  from ..utils import get_appointment_by_datetime
@@ -46,31 +46,32 @@ class NextAppointmentCrfModelFormMixin:
46
46
  )
47
47
 
48
48
  def validate_suggested_date_with_future_appointments(self):
49
- if self.suggested_date and (
50
- self.related_visit.appointment.next.related_visit
51
- or has_keyed_metadata(self.related_visit.appointment.next)
52
- ):
53
- if (
49
+ if (
50
+ self.suggested_date
51
+ and (
52
+ self.related_visit.appointment.next.related_visit
53
+ or has_keyed_metadata(self.related_visit.appointment.next)
54
+ )
55
+ and (
54
56
  self.suggested_date
55
57
  != to_local(self.related_visit.appointment.next.appt_datetime).date()
56
- ):
57
- appointment = self.related_visit.appointment.next
58
- date_format = convert_php_dateformat(settings.SHORT_DATE_FORMAT)
59
- next_appt_date = (
60
- to_local(appointment.appt_datetime).date().strftime(date_format)
61
- )
62
- raise forms.ValidationError(
63
- {
64
- self.appt_date_fld: _(
65
- "Invalid. Next visit report already submitted. Expected "
66
- "`%(dt)s`. See `%(visit_code)s`."
67
- )
68
- % {
69
- "dt": next_appt_date,
70
- "visit_code": appointment.visit_code,
71
- }
58
+ )
59
+ ):
60
+ appointment = self.related_visit.appointment.next
61
+ date_format = convert_php_dateformat(settings.SHORT_DATE_FORMAT)
62
+ next_appt_date = to_local(appointment.appt_datetime).date().strftime(date_format)
63
+ raise forms.ValidationError(
64
+ {
65
+ self.appt_date_fld: _(
66
+ "Invalid. Next visit report already submitted. Expected "
67
+ "`%(dt)s`. See `%(visit_code)s`."
68
+ )
69
+ % {
70
+ "dt": next_appt_date,
71
+ "visit_code": appointment.visit_code,
72
72
  }
73
- )
73
+ }
74
+ )
74
75
 
75
76
  if (
76
77
  self.suggested_date
@@ -107,7 +108,7 @@ class NextAppointmentCrfModelFormMixin:
107
108
  raise_if_in_gap=False,
108
109
  )
109
110
  except ScheduledVisitWindowError as e:
110
- raise forms.ValidationError({self.appt_date_fld: str(e)})
111
+ raise forms.ValidationError({self.appt_date_fld: str(e)}) from e
111
112
  if not appointment:
112
113
  raise forms.ValidationError(
113
114
  {self.appt_date_fld: _("Invalid. Must be within the followup period.")}
@@ -4,7 +4,7 @@ from django.dispatch import receiver
4
4
  from django.utils import timezone
5
5
 
6
6
  from edc_constants.constants import NO
7
- from edc_utils import formatted_datetime
7
+ from edc_utils.text import formatted_datetime
8
8
  from edc_visit_schedule.site_visit_schedules import site_visit_schedules
9
9
  from edc_visit_tracking.utils import get_related_visit_model_cls
10
10
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from datetime import date, datetime
4
5
  from typing import TYPE_CHECKING, Any
5
6
  from zoneinfo import ZoneInfo
@@ -104,20 +105,14 @@ class SkipAppointments:
104
105
  ).exclude(
105
106
  appt_status__in=[SKIPPED_APPT, NEW_APPT],
106
107
  ):
107
- try:
108
+ with contextlib.suppress(AppointmentAlreadyStarted):
108
109
  reset_appointment(appointment)
109
- except AppointmentAlreadyStarted:
110
- pass
111
110
 
112
111
  for appointment in self.scheduled_appointments.filter(
113
112
  appt_datetime__gt=self.appointment.appt_datetime,
114
113
  ):
115
- try:
114
+ with contextlib.suppress(IntegrityError, AppointmentAlreadyStarted):
116
115
  reset_appointment(appointment)
117
- except IntegrityError as e:
118
- print(e)
119
- except AppointmentAlreadyStarted:
120
- pass
121
116
 
122
117
  def update_appointments(self) -> bool:
123
118
  """Return True if next scheduled appointment is updated.
@@ -152,10 +147,8 @@ class SkipAppointments:
152
147
  next_scheduled_appointment_updated = True
153
148
  break
154
149
  else:
155
- try:
150
+ with contextlib.suppress(AppointmentAlreadyStarted):
156
151
  skip_appointment(appointment, comment=skip_comment)
157
- except AppointmentAlreadyStarted:
158
- pass
159
152
  appointment = appointment.relative_next
160
153
  for appointment in cancelled_appointments:
161
154
  appointment.delete()
@@ -198,7 +191,7 @@ class SkipAppointments:
198
191
  except FieldError as e:
199
192
  raise SkipAppointmentsFieldError(
200
193
  f"{e}. See {self.crf_model_cls._meta.label_lower}."
201
- )
194
+ ) from e
202
195
  return self._last_crf_obj
203
196
 
204
197
  @property
@@ -215,11 +208,11 @@ class SkipAppointments:
215
208
  if not self._next_appt_date:
216
209
  try:
217
210
  self._next_appt_date = getattr(self.last_crf_obj, self.dt_fld)
218
- except AttributeError:
211
+ except AttributeError as e:
219
212
  raise SkipAppointmentsFieldError(
220
213
  f"Unknown field name for next scheduled appointment date. See "
221
214
  f"{self.last_crf_obj._meta.label_lower}. Got `{self.dt_fld}`."
222
- )
215
+ ) from e
223
216
  return self._next_appt_date
224
217
 
225
218
  @property
@@ -265,11 +258,11 @@ class SkipAppointments:
265
258
  if not self._next_visit_code:
266
259
  try:
267
260
  self._next_visit_code = getattr(self.last_crf_obj, self.visit_code_fld)
268
- except AttributeError:
261
+ except AttributeError as e:
269
262
  raise SkipAppointmentsFieldError(
270
263
  "Unknown field name for visit code. See "
271
264
  f"{self.last_crf_obj._meta.label_lower}. Got `{self.visit_code_fld}`."
272
- )
265
+ ) from e
273
266
  self._next_visit_code = getattr(
274
267
  self._next_visit_code, "visit_code", self._next_visit_code
275
268
  )
@@ -285,7 +278,7 @@ class SkipAppointments:
285
278
  try:
286
279
  raise_on_appt_datetime_not_in_window(appointment)
287
280
  except AppointmentWindowError as e:
288
- raise SkipAppointmentsValueError(e)
281
+ raise SkipAppointmentsValueError(e) from e
289
282
 
290
283
  next_appt = get_appointment_by_datetime(
291
284
  self.next_appt_datetime,
@@ -309,15 +302,3 @@ class SkipAppointments:
309
302
  raise_if_in_gap=False,
310
303
  )
311
304
  return self.appointment == next_appointment
312
-
313
- # and getattr(
314
- # self.crf_obj, "allow_create_interim", None
315
- # ):
316
- # subject_identifier=self.appointment.subject_identifier,
317
- # visit_schedule_name=self.appointment.visit_schedule_name,
318
- # schedule_name=self.appointment.schedule_name,
319
- # timepoint=self.appointment.timepoint,
320
- # visit_code=self.appointment.visit_code,
321
- # visit_code_sequence=self.appointment.visit_code_sequence + 1,
322
- # facility=self.appointment.facility,
323
- # )
edc_appointment/utils.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import calendar
4
4
  import sys
5
5
  import warnings
6
+ from collections.abc import Callable
6
7
  from datetime import datetime
7
8
  from typing import TYPE_CHECKING, Any
8
9
 
@@ -18,7 +19,7 @@ from django.core.exceptions import (
18
19
  ValidationError,
19
20
  )
20
21
  from django.core.handlers.wsgi import WSGIRequest
21
- from django.db import IntegrityError, transaction
22
+ from django.db import transaction
22
23
  from django.db.models import Count, ProtectedError
23
24
  from django.urls import reverse
24
25
  from django.utils.translation import gettext as _
@@ -33,8 +34,8 @@ from edc_metadata.utils import (
33
34
  get_requisition_metadata_model_cls,
34
35
  has_keyed_metadata,
35
36
  )
36
- from edc_utils import convert_php_dateformat
37
37
  from edc_utils.date import to_local
38
+ from edc_utils.text import convert_php_dateformat
38
39
  from edc_visit_schedule.exceptions import (
39
40
  ScheduledVisitWindowError,
40
41
  UnScheduledVisitWindowError,
@@ -200,6 +201,7 @@ def get_appt_reason_default() -> str:
200
201
  "Settings attribute `EDC_APPOINTMENT_DEFAULT_APPT_REASON` has "
201
202
  "been deprecated in favor of `EDC_APPOINTMENT_APPT_REASON_DEFAULT`. ",
202
203
  DeprecationWarning,
204
+ stacklevel=2,
203
205
  )
204
206
  return value or SCHEDULED_APPT
205
207
 
@@ -258,10 +260,10 @@ def missed_appointment(appointment: Appointment) -> None:
258
260
 
259
261
 
260
262
  def reset_visit_code_sequence_or_pass(
261
- subject_identifier: str = None,
262
- visit_schedule_name: str = None,
263
- schedule_name: str = None,
264
- visit_code: str = None,
263
+ subject_identifier: str,
264
+ visit_schedule_name: str,
265
+ schedule_name: str,
266
+ visit_code: str,
265
267
  appointment: Appointment | None = None,
266
268
  write_stdout: bool | None = None,
267
269
  ) -> Appointment | None:
@@ -294,36 +296,33 @@ def reset_visit_code_sequence_or_pass(
294
296
  visit_code_sequence__gt=0, **opts
295
297
  ).delete()
296
298
 
297
- try:
298
- with transaction.atomic():
299
- # set appt and related visit visit_code_sequences to the
300
- # negative of the current value
301
- for obj in get_appointment_model_cls().objects.filter(
302
- visit_code_sequence__gt=0, **opts
303
- ):
304
- obj.visit_code_sequence = obj.visit_code_sequence * -1
305
- obj.save_base(update_fields=["visit_code_sequence"])
306
- if getattr(obj, "related_visit", None):
307
- obj.related_visit.visit_code_sequence = obj.visit_code_sequence
308
- obj.related_visit.save_base(update_fields=["visit_code_sequence"])
309
- obj.related_visit.metadata_create()
310
-
311
- # reset sequence order by appt_datetime
312
- for index, obj in enumerate(
313
- get_appointment_model_cls()
314
- .objects.filter(visit_code_sequence__lt=0, **opts)
315
- .order_by("appt_datetime"),
316
- start=1,
317
- ):
318
- obj.visit_code_sequence = index
319
- obj.save_base(update_fields=["visit_code_sequence"])
320
- if getattr(obj, "related_visit", None):
321
- obj.related_visit.visit_code_sequence = index
322
- obj.related_visit.save_base(update_fields=["visit_code_sequence"])
323
- obj.related_visit.metadata_create()
324
-
325
- except IntegrityError:
326
- raise
299
+ with transaction.atomic():
300
+ # set appt and related visit visit_code_sequences to the
301
+ # negative of the current value
302
+ for obj in get_appointment_model_cls().objects.filter(
303
+ visit_code_sequence__gt=0, **opts
304
+ ):
305
+ obj.visit_code_sequence = obj.visit_code_sequence * -1
306
+ obj.save_base(update_fields=["visit_code_sequence"])
307
+ if getattr(obj, "related_visit", None):
308
+ obj.related_visit.visit_code_sequence = obj.visit_code_sequence
309
+ obj.related_visit.save_base(update_fields=["visit_code_sequence"])
310
+ obj.related_visit.metadata_create()
311
+
312
+ # reset sequence order by appt_datetime
313
+ for index, obj in enumerate(
314
+ get_appointment_model_cls()
315
+ .objects.filter(visit_code_sequence__lt=0, **opts)
316
+ .order_by("appt_datetime"),
317
+ start=1,
318
+ ):
319
+ obj.visit_code_sequence = index
320
+ obj.save_base(update_fields=["visit_code_sequence"])
321
+ if getattr(obj, "related_visit", None):
322
+ obj.related_visit.visit_code_sequence = index
323
+ obj.related_visit.save_base(update_fields=["visit_code_sequence"])
324
+ obj.related_visit.metadata_create()
325
+
327
326
  if appointment:
328
327
  # refresh the given appt if not None since
329
328
  # appointment visit_code_sequence may have changed
@@ -332,9 +331,9 @@ def reset_visit_code_sequence_or_pass(
332
331
 
333
332
 
334
333
  def reset_visit_code_sequence_for_subject(
335
- subject_identifier: str = None,
336
- visit_schedule_name: str = None,
337
- schedule_name: str = None,
334
+ subject_identifier: str,
335
+ visit_schedule_name: str,
336
+ schedule_name: str,
338
337
  ) -> None:
339
338
  """Resets / validates appointment `visit code sequences` for any
340
339
  `visit code` with unscheduled appointments for the given subject
@@ -376,7 +375,7 @@ def update_appt_status(appointment: Appointment, save: bool | None = None):
376
375
  relative to the visit tracking model and CRFs and
377
376
  requisitions
378
377
  """
379
- if appointment.appt_status == CANCELLED_APPT or appointment.appt_status == SKIPPED_APPT:
378
+ if appointment.appt_status in (CANCELLED_APPT, SKIPPED_APPT):
380
379
  pass
381
380
  elif not appointment.related_visit:
382
381
  appointment.appt_status = NEW_APPT
@@ -640,16 +639,16 @@ def get_appointment_by_datetime(
640
639
  and in_next_window_adjusted
641
640
  and appointment.next.visit.add_window_gap_to_lower
642
641
  ):
643
- appointment = appointment.next
642
+ appointment = appointment.next # noqa: PLW2901
644
643
  break
645
644
  if (
646
645
  in_gap
647
646
  and not in_next_window_adjusted
648
647
  and appointment.next.visit.add_window_gap_to_lower
649
648
  ):
650
- appointment = None
649
+ appointment = None # noqa: PLW2901
651
650
  break
652
- appointment = appointment.next
651
+ appointment = appointment.next # noqa: PLW2901
653
652
  else:
654
653
  break
655
654
  return appointment
@@ -790,7 +789,7 @@ def refresh_appointments(
790
789
 
791
790
 
792
791
  def validate_date_is_on_clinic_day(
793
- cleaned_data: dict = None, clinic_days=None, raise_validation_error: callable = None
792
+ cleaned_data: dict, clinic_days: list[int], raise_validation_error: Callable | None = None
794
793
  ):
795
794
  raise_validation_error = raise_validation_error or ValidationError
796
795
  if cleaned_data.get("appt_date"):
@@ -833,9 +832,6 @@ def validate_date_is_on_clinic_day(
833
832
  not in clinic_days
834
833
  ):
835
834
  days_str = [day_abbr[d] for d in clinic_days]
836
- days_str = []
837
- for d in clinic_days:
838
- days_str.append(day_abbr[d])
839
835
  raise raise_validation_error(
840
836
  {
841
837
  "appt_date": _(