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
edc_facility/facility.py CHANGED
@@ -11,7 +11,7 @@ from dateutil.relativedelta import relativedelta, weekday
11
11
  from django.conf import settings
12
12
  from django.utils import timezone
13
13
 
14
- from edc_utils import convert_php_dateformat, to_utc
14
+ from edc_utils.text import convert_php_dateformat
15
15
 
16
16
  from .exceptions import FacilityError
17
17
  from .holidays import Holidays
@@ -67,10 +67,6 @@ class Facility:
67
67
  slots_per_day = 0
68
68
  return slots_per_day
69
69
 
70
- # @property
71
- # def weekdays(self) -> list[int]:
72
- # return [d.weekday for d in self.days]
73
-
74
70
  @staticmethod
75
71
  def open_slot_on(arr) -> Arrow:
76
72
  """Hook for handling load balance by day.
@@ -80,8 +76,8 @@ class Facility:
80
76
  """
81
77
  return arr
82
78
 
83
- def is_holiday(self, dt: datetime) -> bool:
84
- return self.holidays.is_holiday(utc_datetime=to_utc(dt))
79
+ def is_holiday(self, dte: datetime) -> bool:
80
+ return self.holidays.is_holiday(dte=dte)
85
81
 
86
82
  def available_datetime(self, **kwargs) -> datetime:
87
83
  return self.available_arr(**kwargs).datetime
@@ -95,11 +91,11 @@ class Facility:
95
91
  """
96
92
  # min_arw = self.to_arrow_utc(suggested_arr.datetime - reverse_delta)
97
93
  min_arr = Arrow.fromdate(
98
- suggested_arr.datetime - reverse_delta, tzinfo=ZoneInfo("UTC")
94
+ suggested_arr.datetime - reverse_delta, tzinfo=ZoneInfo(settings.TIME_ZONE)
99
95
  )
100
96
  # max_arw = self.to_arrow_utc(suggested_arw.datetime + forward_delta)
101
97
  max_arr = Arrow.fromdate(
102
- suggested_arr.datetime + forward_delta, tzinfo=ZoneInfo("UTC")
98
+ suggested_arr.datetime + forward_delta, tzinfo=ZoneInfo(settings.TIME_ZONE)
103
99
  )
104
100
  span = [arw[0] for arw in Arrow.span_range("day", min_arr.datetime, max_arr.datetime)]
105
101
  span_lt = [arw for arw in span if arw.date() < suggested_arr.date()]
@@ -136,13 +132,13 @@ class Facility:
136
132
  close to the suggested datetime.
137
133
 
138
134
  To exclude datetimes other than holidays, pass a list of
139
- datetimes in UTC to `taken_datetimes`.
135
+ datetimes to `taken_datetimes`.
140
136
  """
141
137
  available_arr = None
142
138
  forward_delta = forward_delta or relativedelta(months=1)
143
139
  reverse_delta = reverse_delta or relativedelta(months=0)
144
140
  taken_arr = [
145
- arrow.Arrow.fromdatetime(dt, tzinfo=ZoneInfo("UTC")).to("utc")
141
+ arrow.Arrow.fromdatetime(dt, tzinfo=ZoneInfo(settings.TIME_ZONE))
146
142
  for dt in taken_datetimes or []
147
143
  ]
148
144
  if suggested_datetime:
edc_facility/holidays.py CHANGED
@@ -85,9 +85,9 @@ class Holidays:
85
85
  self._holidays = self.model_cls.objects.filter(country=self.country)
86
86
  return self._holidays
87
87
 
88
- def is_holiday(self, utc_datetime=None) -> bool:
89
- """Returns True if the UTC datetime is a holiday."""
90
- local_date = to_local(utc_datetime).date()
88
+ def is_holiday(self, dte=None) -> bool:
89
+ """Returns True if the datetime is a holiday."""
90
+ local_date = to_local(dte).date()
91
91
  try:
92
92
  self.model_cls.objects.get(country=self.country, local_date=local_date)
93
93
  except ObjectDoesNotExist:
@@ -3,7 +3,7 @@ from django.db import models
3
3
  from django.db.models import Index, UniqueConstraint
4
4
  from django.utils.translation import gettext as _
5
5
 
6
- from edc_utils import convert_php_dateformat
6
+ from edc_utils.text import convert_php_dateformat
7
7
 
8
8
 
9
9
  class Holiday(models.Model):
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import html
4
+ import sys
4
5
  import uuid
5
6
  from typing import TYPE_CHECKING, Any
6
7
 
@@ -25,8 +26,8 @@ class FormRunner:
25
26
 
26
27
  model_name: str | None = None
27
28
  issue_model = "edc_form_runners.issue"
28
- extra_formfields: list[str] | None = None
29
- exclude_formfields: list[str] | None = None
29
+ extra_formfields: tuple[str] | None = None
30
+ exclude_formfields: tuple[str] | None = None
30
31
 
31
32
  def __init__(
32
33
  self,
@@ -68,7 +69,7 @@ class FormRunner:
68
69
  self.issue_model_cls.objects.filter(**self.unique_opts(src_obj)).delete()
69
70
  self.run_one(src_obj=src_obj, skip_delete=True)
70
71
  for k, v in self.messages.items():
71
- print(f"Warning: {k}: {v}")
72
+ sys.stdout.write(f"Warning: {k}: {v}\n")
72
73
 
73
74
  def run_one(self, src_obj: Model, skip_delete: bool | None = None) -> None:
74
75
  if not skip_delete:
@@ -87,17 +88,17 @@ class FormRunner:
87
88
  self.print(str(issue_obj))
88
89
 
89
90
  @property
90
- def issue_model_cls(self) -> Issue:
91
+ def issue_model_cls(self) -> type[Issue]:
91
92
  return django_apps.get_model(self.issue_model)
92
93
 
93
94
  @property
94
- def fieldset_fields(self) -> list[str]:
95
- fields = []
95
+ def fieldset_fields(self) -> tuple[str]:
96
+ fields = ()
96
97
  if self.modeladmin_cls.form != ModelForm:
97
98
  if getattr(self.modeladmin_cls, "fieldsets", None):
98
99
  for fieldset in self.modeladmin_cls.fieldsets:
99
100
  _, data = fieldset
100
- fields.extend(data.get("fields"))
101
+ fields = {*fields, *data.get("fields")}
101
102
  else:
102
103
  self.messages.update(
103
104
  {
@@ -107,10 +108,8 @@ class FormRunner:
107
108
  }
108
109
  )
109
110
 
110
- fields = [
111
- k for k, v in getattr(self.modeladmin_cls.form(), "fields", {}).items()
112
- ]
113
- fields = list(set(fields))
111
+ fields = {k for k in getattr(self.modeladmin_cls.form(), "fields", {})}
112
+ fields = tuple(fields)
114
113
  return fields
115
114
 
116
115
  def write_to_db(self, fldname: str, errmsg: Any, src_obj: Any) -> Issue:
@@ -208,12 +207,12 @@ class FormRunner:
208
207
  def get_src_filter_options(self) -> dict[str, Any]:
209
208
  return self.src_filter_options
210
209
 
211
- def get_extra_formfields(self) -> list[str]:
212
- return self.extra_formfields or []
210
+ def get_extra_formfields(self) -> tuple[str]:
211
+ return self.extra_formfields or ()
213
212
 
214
- def get_exclude_formfields(self) -> list[str]:
215
- return self.exclude_formfields or []
213
+ def get_exclude_formfields(self) -> tuple[str]:
214
+ return self.exclude_formfields or ()
216
215
 
217
216
  def print(self, msg: str) -> None:
218
217
  if self.verbose:
219
- print(msg)
218
+ sys.stdout.write(f"{msg}\n")
@@ -96,11 +96,15 @@ class BaseFormValidator:
96
96
  field_value = self.cleaned_data.get(field)
97
97
  return field_value
98
98
 
99
- def raise_validation_error(self, message: dict[str, str] | str, error_code: str) -> None:
99
+ def raise_validation_error(
100
+ self, message: dict[str, str] | str, error_code: str, exc: Exception | None = None
101
+ ) -> None:
100
102
  if isinstance(message, str):
101
103
  message = {NON_FIELD_ERRORS: message}
102
104
  self._errors.update(message)
103
105
  self._error_codes.append(error_code)
106
+ if exc:
107
+ raise ValidationError(message, code=error_code or INVALID_ERROR) from exc
104
108
  raise ValidationError(message, code=error_code or INVALID_ERROR)
105
109
 
106
110
  def validate(self) -> dict:
@@ -1,24 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from datetime import date
4
5
 
5
6
  from django import forms
7
+ from django.utils.translation import gettext as _
6
8
 
7
- from edc_utils.date import to_utc
8
- from edc_utils.text import formatted_datetime
9
+ from edc_utils.date import to_local
9
10
 
10
11
  from .base_form_validator import BaseFormValidator
11
12
 
12
13
 
13
14
  def convert_any_to_date(date1, date2) -> tuple[date, date]:
14
- try:
15
- date1 = date1.date()
16
- except AttributeError:
17
- pass
18
- try:
19
- date2 = date2.date()
20
- except AttributeError:
21
- pass
15
+ with contextlib.suppress(AttributeError):
16
+ date1 = to_local(date1).date()
17
+ with contextlib.suppress(AttributeError):
18
+ date2 = to_local(date2).date()
22
19
  return date1, date2
23
20
 
24
21
 
@@ -36,10 +33,9 @@ class DateRangeFieldValidator(BaseFormValidator):
36
33
  date2 = self.cleaned_data.get(date_field2)
37
34
  if convert_to_date:
38
35
  date1, date2 = convert_any_to_date(date1, date2)
39
- msg = msg or f"Invalid. Cannot be before {date_field1} "
40
- if date1 and date2:
41
- if date1 > date2:
42
- raise forms.ValidationError({message_on_field or date_field2: f"{msg}."})
36
+ if date1 and date2 and (date2 < date1):
37
+ msg = msg or _("Invalid. Cannot be before %(field)s.") % {"field": date_field1}
38
+ raise forms.ValidationError({message_on_field or date_field2: msg})
43
39
 
44
40
  def date_not_after(
45
41
  self,
@@ -48,14 +44,14 @@ class DateRangeFieldValidator(BaseFormValidator):
48
44
  msg: str | None = None,
49
45
  convert_to_date: bool | None = None,
50
46
  ) -> None:
47
+ """Asserts date_field2 is not after date_field1"""
51
48
  date1 = self.cleaned_data.get(date_field1)
52
49
  date2 = self.cleaned_data.get(date_field2)
53
50
  if convert_to_date:
54
51
  date1, date2 = convert_any_to_date(date1, date2)
55
- msg = msg or f"Invalid. Cannot be after {date_field1} "
56
- if date1 and date2:
57
- if date1 < date2:
58
- raise forms.ValidationError({date_field2: f"{msg}"})
52
+ if date1 and date2 and (date2 > date1):
53
+ msg = msg or _("Invalid. Cannot be after %(field)s.") % {"field": date_field1}
54
+ raise forms.ValidationError({date_field2: msg})
59
55
 
60
56
  def date_equal(
61
57
  self,
@@ -65,15 +61,17 @@ class DateRangeFieldValidator(BaseFormValidator):
65
61
  message_on_field=None,
66
62
  convert_to_date: bool | None = None,
67
63
  ) -> None:
68
- """Asserts date1 and date2 are equal"""
64
+ """Asserts date2 and date1 are equal"""
69
65
  date1 = self.cleaned_data.get(date_field1)
70
66
  date2 = self.cleaned_data.get(date_field2)
71
67
  if convert_to_date:
72
68
  date1, date2 = convert_any_to_date(date1, date2)
73
- msg = msg or f"Invalid. Expected {date_field2} to be the same as {date_field1}."
74
- if date1 and date2:
75
- if date1 != date2:
76
- raise forms.ValidationError({message_on_field or date_field2: f"{msg}"})
69
+ if date1 and date2 and (date1 != date2):
70
+ msg = msg or _("Invalid. Expected %(field2)s to be the same as %(field1)s.") % {
71
+ "field1": date_field1,
72
+ "field2": date_field2,
73
+ }
74
+ raise forms.ValidationError({message_on_field or date_field2: msg})
77
75
 
78
76
  def date_not_equal(
79
77
  self,
@@ -83,50 +81,40 @@ class DateRangeFieldValidator(BaseFormValidator):
83
81
  message_on_field=None,
84
82
  convert_to_date: bool | None = None,
85
83
  ) -> None:
86
- """Asserts date1 and date2 are equal"""
87
- date1 = self.cleaned_data.get(date_field1)
88
- date2 = self.cleaned_data.get(date_field2)
84
+ """Asserts date2 and date1 are not equal"""
85
+ dte1 = self.cleaned_data.get(date_field1)
86
+ dte2 = self.cleaned_data.get(date_field2)
89
87
  if convert_to_date:
90
- date1, date2 = convert_any_to_date(date1, date2)
91
- msg = msg or f"Invalid. Expected {date_field2} to be the same as {date_field1}."
92
- if date1 and date2:
93
- if date1 == date2:
94
- raise forms.ValidationError({message_on_field or date_field2: f"{msg}"})
88
+ dte1, dte2 = convert_any_to_date(dte1, dte2)
89
+ if dte1 and dte2 and (dte1 == dte2):
90
+ msg = msg or _("Invalid. Expected %(field2)s to be different from %(field1)s.") % {
91
+ "field1": date_field1,
92
+ "field2": date_field2,
93
+ }
94
+ raise forms.ValidationError({message_on_field or date_field2: msg})
95
95
 
96
96
  def datetime_not_before(
97
97
  self, datetime_field1: str, datetime_field2: str, msg=None
98
98
  ) -> None:
99
- datetime1 = self.cleaned_data.get(datetime_field1)
100
- datetime2 = self.cleaned_data.get(datetime_field2)
101
- if datetime1:
102
- datetime1 = to_utc(datetime1)
103
- if datetime2:
104
- datetime2 = to_utc(datetime2)
105
- msg = msg or f"Invalid. Cannot be before {datetime_field2} "
106
- if datetime1 and datetime2:
107
- if datetime1 < datetime2:
108
- raise forms.ValidationError(
109
- {datetime_field1: f"{msg}. Got {formatted_datetime(datetime2)}."}
110
- )
99
+ """Asserts datetime_field2 is not before datetime_field1"""
100
+ dte1 = self.cleaned_data.get(datetime_field1)
101
+ dte2 = self.cleaned_data.get(datetime_field2)
102
+ if dte1 and dte2 and (dte2 < dte1):
103
+ msg = msg or _("Invalid. Cannot be before %(field)s") % {"field": datetime_field1}
104
+ raise forms.ValidationError({datetime_field2: msg})
111
105
 
112
106
  def datetime_not_after(self, datetime_field1: str, datetime_field2: str, msg=None) -> None:
113
- datetime_field1 = self.cleaned_data.get(datetime_field1)
114
- datetime_field2 = self.cleaned_data.get(datetime_field2)
115
- msg = msg or f"Invalid. Cannot be before date of {datetime_field2} "
116
- if datetime_field1:
117
- datetime_field1 = to_utc(datetime_field1)
118
- datetime_field2 = to_utc(datetime_field2)
119
- if datetime_field1 > datetime_field2:
120
- raise forms.ValidationError({datetime_field1: f"{msg}"})
107
+ """Asserts datetime_field2 is not after datetime_field1"""
108
+ dte1 = self.cleaned_data.get(datetime_field1)
109
+ dte2 = self.cleaned_data.get(datetime_field2)
110
+ if dte1 and dte2 and (dte2 > dte1):
111
+ msg = msg or _("Invalid. Cannot be after %(field)s") % {"field": datetime_field1}
112
+ raise forms.ValidationError({datetime_field2: msg})
121
113
 
122
114
  def datetime_equal(self, datetime_field1: str, datetime_field2: str, msg=None) -> None:
123
- datetime_field1 = self.cleaned_data.get(datetime_field1)
124
- datetime_field2 = self.cleaned_data.get(datetime_field2)
125
- msg = msg or f"Invalid. Cannot be before date of {datetime_field2} "
126
- if datetime_field1:
127
- datetime_field1 = to_utc(datetime_field1)
128
- datetime_field2 = to_utc(datetime_field2)
129
- if datetime_field1 == datetime_field2:
130
- raise forms.ValidationError(
131
- {datetime_field1: f"{msg}. Got {formatted_datetime(datetime_field2)}."}
132
- )
115
+ """Asserts datetime_field2 is not equal to datetime_field1"""
116
+ dte1 = self.cleaned_data.get(datetime_field1)
117
+ dte2 = self.cleaned_data.get(datetime_field2)
118
+ if dte1 and dte2 and (dte1 == dte2):
119
+ msg = msg or _("Invalid. Cannot be same as %(field)s") % {"field": datetime_field1}
120
+ raise forms.ValidationError({datetime_field2: msg})
@@ -6,8 +6,8 @@ from django.conf import settings
6
6
 
7
7
  from edc_constants.constants import EQ, GT, GTE, LT, LTE
8
8
  from edc_model import estimated_date_from_ago
9
- from edc_utils import convert_php_dateformat
10
9
  from edc_utils.date import to_local
10
+ from edc_utils.text import convert_php_dateformat
11
11
 
12
12
  from .base_form_validator import INVALID_ERROR, BaseFormValidator
13
13
 
@@ -5,7 +5,7 @@ from django.apps import apps as django_apps
5
5
  from django.conf import settings
6
6
 
7
7
  from edc_crf.crf_form_validator import CrfFormValidator
8
- from edc_utils import convert_php_dateformat
8
+ from edc_utils.text import convert_php_dateformat
9
9
 
10
10
  from ..base_form_validator import INVALID_ERROR
11
11
 
@@ -4,7 +4,8 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  from django import forms
6
6
 
7
- from edc_utils import formatted_datetime, to_utc
7
+ from edc_utils.date import to_local
8
+ from edc_utils.text import formatted_datetime
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from ..model_mixins import RequisitionModelMixin
@@ -65,17 +66,22 @@ class CrfRequisitionFormValidatorMixin:
65
66
  requisition: RequisitionModelMixin,
66
67
  assay_datetime_field: str | None = None,
67
68
  ) -> None:
68
- assay_datetime_field = assay_datetime_field or self.assay_datetime_field
69
- assay_datetime = self.cleaned_data.get(assay_datetime_field)
70
- if assay_datetime:
71
- assay_datetime = to_utc(assay_datetime)
72
- requisition_datetime = to_utc(requisition.requisition_datetime)
73
- if assay_datetime < requisition_datetime:
74
- raise forms.ValidationError(
75
- {
76
- assay_datetime_field: (
77
- f"Invalid. Cannot be before date of requisition "
78
- f"{formatted_datetime(requisition_datetime)}."
79
- )
80
- }
81
- )
69
+ """Validate assay datetime is on or after requisition
70
+ datetime.
71
+ """
72
+ assay_datetime = self.cleaned_data.get(
73
+ assay_datetime_field or self.assay_datetime_field
74
+ )
75
+ if assay_datetime > self.report_datetime:
76
+ raise forms.ValidationError(
77
+ {assay_datetime_field: "Invalid. Cannot be after report datetime."}
78
+ )
79
+ if assay_datetime < requisition.requisition_datetime:
80
+ raise forms.ValidationError(
81
+ {
82
+ assay_datetime_field: (
83
+ "Invalid. Cannot be before requisition date. Requisition date is "
84
+ f"{formatted_datetime(to_local(requisition.requisition_datetime))}."
85
+ )
86
+ }
87
+ )
@@ -7,7 +7,7 @@ from django import forms
7
7
  from django.apps import apps as django_apps
8
8
 
9
9
  from edc_constants.constants import NO, YES
10
- from edc_utils import to_utc
10
+ from edc_utils.date import to_local
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from ..models import Aliquot
@@ -77,8 +77,8 @@ class RequisitionFormValidatorMixin:
77
77
  if (
78
78
  self.requisition_datetime
79
79
  and self.cleaned_data.get("drawn_datetime")
80
- and to_utc(self.cleaned_data.get("drawn_datetime")).date()
81
- > to_utc(self.requisition_datetime).date()
80
+ and to_local(self.cleaned_data.get("drawn_datetime")).date()
81
+ > to_local(self.requisition_datetime).date()
82
82
  ):
83
83
  raise forms.ValidationError(
84
84
  {"drawn_datetime": "Invalid. Cannot be after requisition date."}
@@ -105,7 +105,7 @@ class BloodResultsFormValidatorMixin(
105
105
  for panel in self.panel_list:
106
106
  for utest_id in panel.utest_ids:
107
107
  with contextlib.suppress(ValueError):
108
- utest_id, _ = utest_id
108
+ utest_id, _ = utest_id # noqa: PLW2901
109
109
  utest_ids.append(utest_id)
110
110
  return tuple(utest_ids)
111
111
 
@@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
5
5
 
6
6
  from edc_constants.constants import NO, YES
7
7
  from edc_form_validators import FormValidator
8
- from edc_utils import convert_php_dateformat
8
+ from edc_utils.text import convert_php_dateformat
9
9
  from edc_visit_tracking.constants import MISSED_VISIT
10
10
  from edc_visit_tracking.utils import get_related_visit_model_cls
11
11
 
@@ -7,6 +7,7 @@ from django.contrib.admin.sites import all_sites
7
7
  from django.core.exceptions import ObjectDoesNotExist
8
8
  from django.db import IntegrityError, transaction
9
9
 
10
+ from edc_visit_schedule.exceptions import MissedVisitError, UnScheduledVisitError
10
11
  from edc_visit_schedule.visit import CrfCollection, RequisitionCollection
11
12
  from edc_visit_tracking.constants import MISSED_VISIT
12
13
 
@@ -206,17 +207,29 @@ class Creator:
206
207
  if self.related_visit.reason == MISSED_VISIT:
207
208
  # missed visit CRFs only
208
209
  crfs = self.related_visit.visit.crfs_missed.forms
210
+ if not crfs:
211
+ raise MissedVisitError(
212
+ "Visit not configured for missed visit. "
213
+ f"visit.crfs_missed=None. Got {self.related_visit.visit}"
214
+ )
209
215
  elif self.related_visit.visit_code_sequence != 0:
210
216
  # unscheduled + prn CRFs only
211
- models = [crf.model for crf in self.related_visit.visit.crfs_unscheduled]
212
- crfs = self.related_visit.visit.crfs_unscheduled.forms + tuple(
213
- [f for f in self.related_visit.visit.crfs_prn if f.model not in models]
217
+ models = (crf.model for crf in self.related_visit.visit.crfs_unscheduled)
218
+ crfs = (
219
+ *self.related_visit.visit.crfs_unscheduled.forms,
220
+ *{f for f in self.related_visit.visit.crfs_prn if f.model not in models},
214
221
  )
222
+ if not crfs:
223
+ raise UnScheduledVisitError(
224
+ "Visit not configured for unscheduled visit. "
225
+ f"visit.crfs_unscheduled=None. Got {self.related_visit.visit}"
226
+ )
215
227
  else:
216
228
  # scheduled + prn CRFs only
217
- models = [crf.model for crf in self.related_visit.visit.crfs]
218
- crfs = self.related_visit.visit.crfs.forms + tuple(
219
- [f for f in self.related_visit.visit.crfs_prn if f.model not in models]
229
+ models = (crf.model for crf in self.related_visit.visit.crfs)
230
+ crfs = (
231
+ *self.related_visit.visit.crfs.forms,
232
+ *{f for f in self.related_visit.visit.crfs_prn if f.model not in models},
220
233
  )
221
234
  return CrfCollection(*crfs, name="crfs")
222
235
 
@@ -278,7 +291,7 @@ class Destroyer:
278
291
  def metadata_requisition_model_cls(self) -> RequisitionMetadata:
279
292
  return django_apps.get_model(self.metadata_requisition_model)
280
293
 
281
- def delete(self, entry_status_not_in: list[str] = None) -> int:
294
+ def delete(self, entry_status_not_in: list[str] | None = None) -> int:
282
295
  """Deletes all CRF and requisition metadata for the
283
296
  related_visit instance excluding where entry_status in
284
297
  [KEYED, NOT_REQUIRED].
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from ..constants import DO_NOTHING, NOT_REQUIRED, REQUIRED
@@ -24,13 +25,13 @@ class Logic:
24
25
  rule; consequence and alternative.
25
26
  """
26
27
 
27
- valid_results = [REQUIRED, NOT_REQUIRED, DO_NOTHING]
28
+ valid_results = (REQUIRED, NOT_REQUIRED, DO_NOTHING)
28
29
 
29
30
  def __init__(
30
31
  self,
31
- predicate: P | PF | callable = None,
32
- consequence: str = None,
33
- alternative: str = None,
32
+ predicate: P | PF | Callable | None = None,
33
+ consequence: str | None = None,
34
+ alternative: str | None = None,
34
35
  comment: str | None = None,
35
36
  ) -> None:
36
37
  if not callable(predicate):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from typing import Any
4
5
 
5
6
  from django.apps import apps as django_apps
@@ -16,7 +17,7 @@ class NoValueError(Exception):
16
17
 
17
18
  class BasePredicate:
18
19
  @staticmethod
19
- def get_value(attr: str = None, source_model: str | None = None, **kwargs) -> Any:
20
+ def get_value(attr: str | None = None, source_model: str | None = None, **kwargs) -> Any:
20
21
  """Returns a value by checking for the attr on each arg.
21
22
 
22
23
  Each arg in args may be a model instance, queryset, or None.
@@ -62,23 +63,23 @@ class P(BasePredicate):
62
63
  predicate = P('age', '<=', 64)
63
64
  """
64
65
 
65
- funcs = {
66
- "is": lambda x, y: True if x is y else False,
67
- "is not": lambda x, y: True if x is not y else False,
68
- "gt": lambda x, y: True if x > y else False,
69
- ">": lambda x, y: True if x > y else False,
70
- "gte": lambda x, y: True if x >= y else False,
71
- ">=": lambda x, y: True if x >= y else False,
72
- "lt": lambda x, y: True if x < y else False,
73
- "<": lambda x, y: True if x < y else False,
74
- "lte": lambda x, y: True if x <= y else False,
75
- "<=": lambda x, y: True if x <= y else False,
76
- "eq": lambda x, y: True if x == y else False,
77
- "equals": lambda x, y: True if x == y else False,
78
- "==": lambda x, y: True if x == y else False,
79
- "neq": lambda x, y: True if x != y else False,
80
- "!=": lambda x, y: True if x != y else False,
81
- "in": lambda x, y: True if x in y else False,
66
+ funcs = { # noqa: RUF012
67
+ "is": lambda x, y: x is y,
68
+ "is not": lambda x, y: x is not y,
69
+ "gt": lambda x, y: x > y,
70
+ ">": lambda x, y: x > y,
71
+ "gte": lambda x, y: x >= y,
72
+ ">=": lambda x, y: x >= y,
73
+ "lt": lambda x, y: x < y,
74
+ "<": lambda x, y: x < y,
75
+ "lte": lambda x, y: x <= y,
76
+ "<=": lambda x, y: x <= y,
77
+ "eq": lambda x, y: x == y,
78
+ "equals": lambda x, y: x == y,
79
+ "==": lambda x, y: x == y,
80
+ "neq": lambda x, y: x != y,
81
+ "!=": lambda x, y: x != y,
82
+ "in": lambda x, y: x in y,
82
83
  }
83
84
 
84
85
  def __init__(self, attr: str, operator: str, expected_value: list | str) -> None:
@@ -91,8 +92,7 @@ class P(BasePredicate):
91
92
 
92
93
  def __repr__(self) -> str:
93
94
  return (
94
- f"{self.__class__.__name__}({self.attr}, {self.operator}, "
95
- f"{self.expected_value})"
95
+ f"{self.__class__.__name__}({self.attr}, {self.operator}, {self.expected_value})"
96
96
  )
97
97
 
98
98
  def __call__(self, **kwargs) -> bool:
@@ -122,14 +122,12 @@ class PF(BasePredicate):
122
122
 
123
123
  """
124
124
 
125
- def __init__(self, *attrs, func: callable = None) -> None:
125
+ def __init__(self, *attrs, func: Callable | None = None) -> None:
126
126
  self.attrs = attrs
127
127
  self.func = func
128
128
 
129
129
  def __call__(self, **kwargs) -> Any:
130
- values = []
131
- for attr in self.attrs:
132
- values.append(self.get_value(attr=attr, **kwargs))
130
+ values = [self.get_value(attr=attr, **kwargs) for attr in self.attrs]
133
131
  return self.func(*values)
134
132
 
135
133
  def __repr__(self) -> str:
@@ -2,8 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
4
 
5
- from edc_utils import to_utc
6
-
7
5
 
8
6
  class ReportDatetimeModelFormMixin:
9
7
  # may also be appt_datetime or requisition_datetime
@@ -11,7 +9,7 @@ class ReportDatetimeModelFormMixin:
11
9
 
12
10
  @property
13
11
  def report_datetime(self) -> datetime | None:
14
- """Returns the report_datetime in UTC or None from
12
+ """Returns the report_datetime or None from
15
13
  cleaned_data.
16
14
 
17
15
  if key does not exist, returns the instance report_datetime.
@@ -22,8 +20,6 @@ class ReportDatetimeModelFormMixin:
22
20
  report_datetime = None
23
21
  if self.report_datetime_field_attr in self.cleaned_data:
24
22
  report_datetime = self.cleaned_data.get(self.report_datetime_field_attr)
25
- if report_datetime:
26
- report_datetime = to_utc(report_datetime)
27
23
  elif self.instance:
28
24
  report_datetime = getattr(self.instance, self.report_datetime_field_attr)
29
25
  return report_datetime