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
@@ -4,16 +4,15 @@ from ...models import Stock
4
4
 
5
5
 
6
6
  class StockForm(forms.ModelForm):
7
-
8
7
  class Meta:
9
8
  model = Stock
10
9
  fields = "__all__"
11
- help_text = {
10
+ help_text = { # noqa: RUF012
12
11
  "stock_identifier": "(read-only)",
13
12
  "receive_item": "(read-only)",
14
13
  "container": "(read-only)",
15
14
  }
16
- widgets = {
15
+ widgets = { # noqa: RUF012
17
16
  "stock_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
18
17
  "receive_item": forms.TextInput(attrs={"readonly": "readonly"}),
19
18
  "container": forms.TextInput(attrs={"readonly": "readonly"}),
@@ -62,13 +62,15 @@ class StockRequestForm(forms.ModelForm):
62
62
 
63
63
  if not self.instance.id and cleaned_data.get("cancel") == "CANCEL":
64
64
  raise forms.ValidationError("Leave this blank")
65
- if cleaned_data.get("cancel") == "CANCEL":
66
- if Allocation.objects.filter(
65
+ if (
66
+ cleaned_data.get("cancel") == "CANCEL"
67
+ and Allocation.objects.filter(
67
68
  stock_request_item__stock_request=self.instance
68
- ).exists():
69
- raise forms.ValidationError(
70
- "May not be cancelled. Stock has been allocated for this request"
71
- )
69
+ ).exists()
70
+ ):
71
+ raise forms.ValidationError(
72
+ "May not be cancelled. Stock has been allocated for this request"
73
+ )
72
74
  return cleaned_data
73
75
 
74
76
  @staticmethod
@@ -112,7 +114,7 @@ class StockRequestForm(forms.ModelForm):
112
114
  class Meta:
113
115
  model = StockRequest
114
116
  fields = "__all__"
115
- help_text = {"request_identifier": "(read-only)"}
116
- widgets = {
117
+ help_text = {"request_identifier": "(read-only)"} # noqa: RUF012
118
+ widgets = { # noqa: RUF012
117
119
  "request_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
118
120
  }
@@ -7,7 +7,6 @@ from ...models import StockRequestItem
7
7
 
8
8
 
9
9
  class StockRequestItemForm(forms.ModelForm):
10
-
11
10
  def clean(self):
12
11
  cleaned_data = super().clean()
13
12
  try:
@@ -16,14 +15,16 @@ class StockRequestItemForm(forms.ModelForm):
16
15
  consent_datetime__isnull=False,
17
16
  randomization_datetime__isnull=False,
18
17
  )
19
- except ObjectDoesNotExist:
20
- raise forms.ValidationError({"subject_identifier": "Subject does not exist"})
18
+ except ObjectDoesNotExist as e:
19
+ raise forms.ValidationError(
20
+ {"subject_identifier": "Subject does not exist"}
21
+ ) from e
21
22
  return cleaned_data
22
23
 
23
24
  class Meta:
24
25
  model = StockRequestItem
25
26
  fields = "__all__"
26
- help_text = {"request_item_identifier": "(read-only)"}
27
- widgets = {
27
+ help_text = {"request_item_identifier": "(read-only)"} # noqa: RUF012
28
+ widgets = { # noqa: RUF012
28
29
  "request_item_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
29
30
  }
@@ -4,29 +4,31 @@ from ...models import StockTransfer, StockTransferItem
4
4
 
5
5
 
6
6
  class StockTransferForm(forms.ModelForm):
7
-
8
7
  def clean(self):
9
8
  cleaned_data = super().clean()
10
9
  items_qs = StockTransferItem.objects.filter(stock_transfer__pk=self.instance.pk)
11
- if cleaned_data.get("to_location") and items_qs.count() > 0:
12
- if (
10
+ if (
11
+ cleaned_data.get("to_location")
12
+ and items_qs.count() > 0
13
+ and (
13
14
  items_qs[0].stock.allocation.registered_subject.site
14
15
  != cleaned_data.get("to_location").site
15
- ):
16
- raise forms.ValidationError(
17
- {
18
- "to_location": (
19
- "Invalid location. Does not match the intended location of "
20
- "existing stock items for this transfer."
21
- )
22
- }
23
- )
16
+ )
17
+ ):
18
+ raise forms.ValidationError(
19
+ {
20
+ "to_location": (
21
+ "Invalid location. Does not match the intended location of "
22
+ "existing stock items for this transfer."
23
+ )
24
+ }
25
+ )
24
26
  return cleaned_data
25
27
 
26
28
  class Meta:
27
29
  model = StockTransfer
28
30
  fields = "__all__"
29
- help_text = {"transfer_identifier": "(read-only)"}
30
- widgets = {
31
+ help_text = {"transfer_identifier": "(read-only)"} # noqa: RUF012
32
+ widgets = { # noqa: RUF012
31
33
  "transfer_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
32
34
  }
@@ -7,7 +7,7 @@ class SupplierForm(forms.ModelForm):
7
7
  class Meta:
8
8
  model = Supplier
9
9
  fields = "__all__"
10
- help_text = {"supplier_identifier": "(read-only)"}
11
- widgets = {
10
+ help_text = {"supplier_identifier": "(read-only)"} # noqa: RUF012
11
+ widgets = { # noqa: RUF012
12
12
  "supplier_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
13
13
  }
@@ -3,12 +3,11 @@ import string
3
3
 
4
4
 
5
5
  class LabelData:
6
-
7
6
  def __init__(self):
8
- self.gender = random.choice(["M", "F"]) # nosec B311
7
+ self.gender = random.choice(["M", "F"]) # nosec B311 # noqa: S311
9
8
  self.subject_identifier = "999-99-9999-9" # nosec B311
10
9
  self.reference = "".join(
11
- random.choices(string.ascii_letters.upper() + "23456789", k=6) # nosec B311
10
+ random.choices(string.ascii_letters.upper() + "23456789", k=6) # nosec B311 # noqa: S311
12
11
  )
13
12
  self.sid = "12345"
14
13
  self.site_name = "AMANA"
@@ -7,7 +7,7 @@ from django.core.exceptions import ObjectDoesNotExist
7
7
 
8
8
  from edc_appointment.utils import get_next_appointment
9
9
  from edc_constants.constants import YES
10
- from edc_utils import formatted_datetime
10
+ from edc_utils.text import formatted_datetime
11
11
  from edc_visit_tracking.utils import get_next_related_visit
12
12
 
13
13
  from ..exceptions import NextStudyMedicationError, StudyMedicationError
@@ -83,9 +83,9 @@ class DosageGuideline(BaseUuidModel):
83
83
  class Meta(BaseUuidModel.Meta):
84
84
  verbose_name = "Dosage Guideline"
85
85
  verbose_name_plural = "Dosage Guidelines"
86
- constraints = [
86
+ constraints = (
87
87
  UniqueConstraint(
88
88
  fields=["medication", "dose", "dose_units", "dose_per_kg"],
89
89
  name="%(app_label)s_%(class)s_med_dose_uniq",
90
- )
91
- ]
90
+ ),
91
+ )
@@ -1,31 +1,33 @@
1
1
  from django.db import models
2
2
 
3
+ from edc_constants.constants import NULL_STRING
4
+
3
5
 
4
6
  class AddressModelMixin(models.Model):
5
- address_one = models.CharField(max_length=255, default="", blank=True)
7
+ address_one = models.CharField(max_length=255, default=NULL_STRING, blank=True)
6
8
 
7
- address_two = models.CharField(max_length=255, default="", blank=True)
9
+ address_two = models.CharField(max_length=255, default=NULL_STRING, blank=True)
8
10
 
9
- city = models.CharField(max_length=255, default="", blank=True)
11
+ city = models.CharField(max_length=255, default=NULL_STRING, blank=True)
10
12
 
11
- postal_code = models.CharField(max_length=255, default="", blank=True)
13
+ postal_code = models.CharField(max_length=255, default=NULL_STRING, blank=True)
12
14
 
13
- state = models.CharField(max_length=255, default="", blank=True)
15
+ state = models.CharField(max_length=255, default=NULL_STRING, blank=True)
14
16
 
15
- country = models.CharField(max_length=255, default="", blank=True)
17
+ country = models.CharField(max_length=255, default=NULL_STRING, blank=True)
16
18
 
17
19
  class Meta:
18
20
  abstract = True
19
21
 
20
22
 
21
23
  class ContactModelMixin(models.Model):
22
- email = models.EmailField(default="", blank=True)
24
+ email = models.EmailField(default=NULL_STRING, blank=True)
23
25
 
24
- email_alternative = models.EmailField(default="", blank=True)
26
+ email_alternative = models.EmailField(default=NULL_STRING, blank=True)
25
27
 
26
- telephone = models.CharField(max_length=15, default="", blank=True)
28
+ telephone = models.CharField(max_length=15, default=NULL_STRING, blank=True)
27
29
 
28
- telephone_alternative = models.CharField(max_length=15, default="", blank=True)
30
+ telephone_alternative = models.CharField(max_length=15, default=NULL_STRING, blank=True)
29
31
 
30
32
  class Meta:
31
33
  abstract = True
@@ -13,7 +13,7 @@ from edc_randomization.site_randomizers import site_randomizers
13
13
  from edc_registration.models import RegisteredSubject
14
14
  from edc_sites.managers import CurrentSiteManager
15
15
  from edc_sites.model_mixins import SiteModelMixin
16
- from edc_utils import formatted_age
16
+ from edc_utils.age import formatted_age
17
17
 
18
18
  from ...choices import PRESCRIPTION_STATUS
19
19
  from ...constants import PRESCRIPTION_ACTION
@@ -9,8 +9,8 @@ from django.db.models.deletion import PROTECT
9
9
  from edc_model.models import BaseUuidModel, HistoricalRecords
10
10
  from edc_sites.managers import CurrentSiteManager
11
11
  from edc_sites.model_mixins import SiteModelMixin
12
- from edc_utils import convert_php_dateformat
13
12
  from edc_utils.round_up import round_half_away_from_zero
13
+ from edc_utils.text import convert_php_dateformat
14
14
 
15
15
  from ...dosage_calculator import DosageCalculator
16
16
  from ...model_mixins import MedicationOrderModelMixin, PreviousNextModelMixin
@@ -278,6 +278,5 @@ def create_or_update_refills_on_post_save(
278
278
  def update_previous_refill_end_datetime_on_post_save(
279
279
  sender, instance, raw, created, update_fields, **kwargs
280
280
  ):
281
- if not raw and not update_fields:
282
- if isinstance(instance, (StudyMedicationCrfModelMixin,)):
283
- update_previous_refill_end_datetime(instance)
281
+ if not raw and not update_fields and isinstance(instance, (StudyMedicationCrfModelMixin,)):
282
+ update_previous_refill_end_datetime(instance)
@@ -48,10 +48,10 @@ def repackage(
48
48
 
49
49
 
50
50
  def repackage_for_subject(
51
- rando_sid: str = None,
52
- subject_identifier: str = None,
53
- randomizer_name=None,
54
- source_container=None,
51
+ rando_sid: str,
52
+ subject_identifier: str | None,
53
+ randomizer_name: str,
54
+ source_container,
55
55
  **kwargs,
56
56
  ):
57
57
  site_randomizers.get(randomizer_name)
@@ -22,13 +22,12 @@ class NumberedCanvas(BaseNumberedCanvas):
22
22
 
23
23
 
24
24
  class ManifestReport(Report):
25
-
26
25
  def __init__(self, stock_transfer: StockTransfer = None, **kwargs):
27
26
  self.stock_transfer = stock_transfer
28
27
  self.protocol_name = ResearchProtocolConfig().protocol_title
29
28
  super().__init__(**kwargs)
30
29
 
31
- def draw_header(self, canvas, doc):
30
+ def draw_header(self, canvas, doc): # noqa: ARG002
32
31
  width, height = A4
33
32
  canvas.setFontSize(6)
34
33
  text_width = stringWidth(self.protocol_name, "Helvetica", 6)
@@ -48,7 +47,7 @@ class ManifestReport(Report):
48
47
  "stock__allocation__registered_subject__subject_identifier"
49
48
  )
50
49
 
51
- def get_report_story(self, document_template: SimpleDocTemplate = None, **kwargs):
50
+ def get_report_story(self, document_template: SimpleDocTemplate = None, **kwargs): # noqa: ARG002
52
51
  story = []
53
52
 
54
53
  data = [
@@ -147,7 +146,6 @@ class ManifestReport(Report):
147
146
 
148
147
  @property
149
148
  def stock_transfer_items_as_table(self) -> Table:
150
-
151
149
  style = ParagraphStyle(
152
150
  name="line_data_medium",
153
151
  alignment=TA_CENTER,
@@ -267,8 +265,7 @@ class ManifestReport(Report):
267
265
  Paragraph(_("Received count"), style=style),
268
266
  ],
269
267
  ]
270
- table = Table(data, colWidths=(None, None, None, None), rowHeights=(10, 10))
271
- return table
268
+ return Table(data, colWidths=(None, None, None, None), rowHeights=(10, 10))
272
269
 
273
270
  @property
274
271
  def comment_box_as_table(self) -> Table:
@@ -16,13 +16,12 @@ from ..utils import get_related_or_none
16
16
 
17
17
 
18
18
  class StockReport(Report):
19
-
20
19
  def __init__(self, queryset: QuerySet[Stock] = None, **kwargs):
21
20
  self.queryset = queryset.order_by("from_stock__code", "code")
22
21
  self.protocol_name = ResearchProtocolConfig().protocol_title
23
22
  super().__init__(**kwargs)
24
23
 
25
- def draw_header(self, canvas, doc):
24
+ def draw_header(self, canvas, doc): # noqa: ARG002
26
25
  width, height = self.page.get("pagesize")
27
26
  canvas.setFontSize(6)
28
27
  text_width = stringWidth(self.protocol_name, "Helvetica", 6)
@@ -72,7 +71,6 @@ class StockReport(Report):
72
71
 
73
72
  @property
74
73
  def stock_items_as_table(self) -> Table:
75
-
76
74
  style = ParagraphStyle(
77
75
  name="line_data_medium",
78
76
  alignment=TA_CENTER,
@@ -4,13 +4,12 @@ from datetime import datetime
4
4
  from decimal import Decimal
5
5
  from typing import TYPE_CHECKING
6
6
  from uuid import uuid4
7
- from zoneinfo import ZoneInfo
8
7
 
9
8
  from dateutil.relativedelta import relativedelta
10
9
  from django.conf import settings
11
10
  from django.core.exceptions import ObjectDoesNotExist
12
11
 
13
- from edc_utils import convert_php_dateformat
12
+ from edc_utils.text import convert_php_dateformat
14
13
 
15
14
  from ..exceptions import (
16
15
  PrescriptionError,
@@ -43,9 +42,9 @@ class RefillCreator:
43
42
  self._next_rx_refill = None
44
43
  self._prev_rx_refill = None
45
44
  self.refill_identifier = refill_identifier
46
- self.refill_end_datetime = refill_end_datetime.astimezone(ZoneInfo("UTC"))
45
+ self.refill_end_datetime = refill_end_datetime
47
46
  self.subject_identifier = subject_identifier
48
- self.refill_start_datetime = refill_start_datetime.astimezone(ZoneInfo("UTC"))
47
+ self.refill_start_datetime = refill_start_datetime
49
48
  self.formulation = formulation
50
49
  self.dosage_guideline = dosage_guideline
51
50
  self.roundup_divisible_by = roundup_divisible_by or 0
@@ -1,10 +1,10 @@
1
1
  from datetime import datetime
2
2
  from zoneinfo import ZoneInfo
3
3
 
4
+ from django.conf import settings
4
5
  from django.core.exceptions import ValidationError
5
- from django.utils import timezone
6
6
 
7
- from edc_utils import formatted_datetime
7
+ from edc_utils.text import formatted_datetime
8
8
 
9
9
  from .research_protocol_config import ResearchProtocolConfig
10
10
 
@@ -12,14 +12,11 @@ from .research_protocol_config import ResearchProtocolConfig
12
12
  def date_not_before_study_start(value):
13
13
  if value:
14
14
  protocol_config = ResearchProtocolConfig()
15
- value_utc = datetime(*[*value.timetuple()][0:6], tzinfo=ZoneInfo("UTC"))
16
- if value_utc < protocol_config.study_open_datetime:
17
- opened = formatted_datetime(
18
- timezone.localtime(protocol_config.study_open_datetime)
19
- )
20
- got = formatted_datetime(timezone.localtime(value_utc))
15
+ dte = datetime(*[*value.timetuple()][0:6], tzinfo=ZoneInfo(settings.TIME_ZONE))
16
+ if dte < protocol_config.study_open_datetime:
17
+ opened = formatted_datetime(protocol_config.study_open_datetime)
21
18
  raise ValidationError(
22
- f"Invalid date. Study opened on {opened}. Got {got}. "
19
+ f"Invalid date. Study opened on {opened}. Got {formatted_datetime(dte)}. "
23
20
  f"See edc_protocol.AppConfig."
24
21
  )
25
22
 
@@ -27,13 +24,10 @@ def date_not_before_study_start(value):
27
24
  def datetime_not_before_study_start(value_datetime):
28
25
  if value_datetime:
29
26
  protocol_config = ResearchProtocolConfig()
30
- value_utc = value_datetime.astimezone(ZoneInfo("UTC"))
31
- if value_utc < protocol_config.study_open_datetime:
32
- opened = formatted_datetime(
33
- timezone.localtime(protocol_config.study_open_datetime)
34
- )
35
- got = formatted_datetime(timezone.localtime(value_utc))
27
+ dte = value_datetime
28
+ if dte < protocol_config.study_open_datetime:
29
+ opened = formatted_datetime(protocol_config.study_open_datetime)
36
30
  raise ValidationError(
37
- f"Invalid date/time. Study opened on {opened}. Got {got}."
31
+ f"Invalid date/time. Study opened on {opened}. Got {formatted_datetime(dte)}."
38
32
  f"See edc_protocol.AppConfig."
39
33
  )
@@ -58,7 +58,7 @@ class ReportablesFormValidatorMixin:
58
58
  try:
59
59
  reference_range_evaluator.validate_reportable_fields()
60
60
  except NotEvaluated as e:
61
- self.raise_validation_error({"__all__": str(e)}, INVALID_ERROR)
61
+ self.raise_validation_error({"__all__": str(e)}, INVALID_ERROR, exc=e)
62
62
  reference_range_evaluator.validate_results_abnormal_field()
63
63
  self.applicable_if(
64
64
  YES, field="results_abnormal", field_applicable="results_reportable"
@@ -43,10 +43,11 @@ class SubjectScreeningFormValidatorMixin:
43
43
  self._subject_screening = self.subject_screening_model_cls.objects.get(
44
44
  screening_identifier=self.screening_identifier
45
45
  )
46
- except ObjectDoesNotExist:
46
+ except ObjectDoesNotExist as e:
47
47
  self.raise_validation_error(
48
48
  'Complete the "Subject Screening" form before proceeding.',
49
49
  error_code="missing_subject_screening",
50
+ exc=e,
50
51
  )
51
52
  return self._subject_screening
52
53
 
@@ -6,7 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist
6
6
  from edc_constants.constants import DWTA, OTHER
7
7
  from edc_form_validators import FormValidator
8
8
  from edc_prn.modelform_mixins import PrnFormValidatorMixin
9
- from edc_utils import convert_php_dateformat
9
+ from edc_utils.text import convert_php_dateformat
10
10
 
11
11
  from .constants import TRANSFERRED
12
12
 
@@ -8,7 +8,7 @@ from edc_constants.choices import YES_NO, YES_NO_UNSURE
8
8
  from edc_identifier.model_mixins import UniqueSubjectIdentifierFieldMixin
9
9
  from edc_model import models as edc_models
10
10
  from edc_sites.model_mixins import SiteModelMixin
11
- from edc_utils import convert_php_dateformat
11
+ from edc_utils.text import convert_php_dateformat
12
12
 
13
13
  from .choices import TRANSFER_INITIATORS
14
14
  from .constants import SUBJECT_TRANSFER_ACTION
edc_utils/age.py CHANGED
@@ -8,6 +8,8 @@ from dateutil.relativedelta import relativedelta
8
8
  from django.conf import settings
9
9
  from django.utils import timezone
10
10
 
11
+ from edc_utils.text import formatted_datetime
12
+
11
13
 
12
14
  class AgeValueError(Exception):
13
15
  pass
@@ -35,23 +37,23 @@ def get_dob(age_in_years: int, now: date | datetime | None = None) -> date:
35
37
  def age(born: date | datetime, reference_dt: date | datetime) -> relativedelta:
36
38
  """Returns a relative delta.
37
39
 
38
- Convert dates or datetimes to UTC datetimes.
40
+ Convert dates to datetimes in local timezone.
39
41
  """
40
- if born is None:
42
+ if born is None or reference_dt is None:
41
43
  raise AgeValueError("DOB cannot be None")
42
- try:
43
- born_utc = born.astimezone(ZoneInfo("UTC"))
44
- except AttributeError:
45
- born_utc = datetime(*[*born.timetuple()][0:6], tzinfo=ZoneInfo("UTC"))
46
- try:
47
- reference_dt_utc = reference_dt.astimezone(ZoneInfo("UTC"))
48
- except AttributeError:
49
- reference_dt_utc = datetime(*[*reference_dt.timetuple()][0:6], tzinfo=ZoneInfo("UTC"))
50
- rdelta = relativedelta(reference_dt_utc, born_utc)
51
- if born_utc > reference_dt_utc:
44
+ if reference_dt is None:
45
+ raise AgeValueError("Reference cannot be None")
46
+ if not hasattr(born, "date"):
47
+ born = datetime(*[*born.timetuple()][0:6], tzinfo=ZoneInfo(settings.TIME_ZONE))
48
+ if not hasattr(reference_dt, "date"):
49
+ reference_dt = datetime(
50
+ *[*reference_dt.timetuple()][0:6], tzinfo=ZoneInfo(settings.TIME_ZONE)
51
+ )
52
+ rdelta = relativedelta(reference_dt, born)
53
+ if born > reference_dt:
52
54
  raise AgeValueError(
53
- f"Reference date {reference_dt} {reference_dt.tzinfo!s} "
54
- f"precedes DOB {born} {timezone}. Got {rdelta}"
55
+ f"Reference date {formatted_datetime(reference_dt)} precedes DOB "
56
+ f"{formatted_datetime(born)}. Got {rdelta}"
55
57
  )
56
58
  return rdelta
57
59
 
@@ -63,7 +65,7 @@ def formatted_age(
63
65
  ) -> str:
64
66
  age_as_str = "?"
65
67
  if born:
66
- tz = tz or getattr(settings, "TIME_ZONE", "UTC")
68
+ tz = tz or settings.TIME_ZONE
67
69
  born = datetime(*[*born.timetuple()][0:6], tzinfo=ZoneInfo(tz))
68
70
  reference_dt = reference_dt or timezone.now()
69
71
  age_delta = age(born, reference_dt or timezone.now())
edc_utils/date.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from datetime import date, datetime
4
4
  from zoneinfo import ZoneInfo
5
5
 
6
- from django.conf import settings
6
+ from django.utils import timezone
7
7
 
8
8
 
9
9
  class EdcDatetimeError(Exception):
@@ -11,21 +11,21 @@ class EdcDatetimeError(Exception):
11
11
 
12
12
 
13
13
  def get_utcnow() -> datetime:
14
- return datetime.now().astimezone(ZoneInfo("UTC"))
14
+ return timezone.localtime(None, timezone=ZoneInfo("UTC"))
15
15
 
16
16
 
17
17
  def get_utcnow_as_date() -> date:
18
- return datetime.now().astimezone(ZoneInfo("UTC")).date()
18
+ return timezone.localtime(None, timezone=ZoneInfo("UTC")).date()
19
19
 
20
20
 
21
- def to_utc(dt: datetime) -> datetime:
21
+ def to_utc(dte: datetime) -> datetime:
22
22
  """Returns UTC datetime from any aware datetime."""
23
- return dt.astimezone(ZoneInfo("UTC"))
23
+ return timezone.localtime(dte, timezone=ZoneInfo("UTC"))
24
24
 
25
25
 
26
- def to_local(dt: datetime) -> datetime:
26
+ def to_local(dte: datetime) -> datetime:
27
27
  """Returns local datetime from any aware datetime."""
28
- return dt.astimezone(ZoneInfo(settings.TIME_ZONE))
28
+ return timezone.localtime(dte)
29
29
 
30
30
 
31
31
  def floor_secs(dte) -> datetime:
edc_utils/text.py CHANGED
@@ -4,6 +4,7 @@ from datetime import datetime
4
4
  from zoneinfo import ZoneInfo
5
5
 
6
6
  from django.conf import settings
7
+ from django.utils import timezone
7
8
 
8
9
  safe_allowed_chars = "ABCDEFGHKMNPRTUVWXYZ2346789"
9
10
 
@@ -63,9 +64,9 @@ def convert_from_camel(name):
63
64
 
64
65
 
65
66
  def formatted_datetime(
66
- aware_datetime: datetime | None,
67
+ dt: datetime | None,
67
68
  php_dateformat: str | None = None,
68
- tz: str | None = None,
69
+ tz: ZoneInfo | None = None,
69
70
  format_as_date: bool | None = None,
70
71
  ):
71
72
  """Returns a formatted datetime string, localized by default.
@@ -73,14 +74,14 @@ def formatted_datetime(
73
74
  format_as_date: does not affect the calculation, just the formatted output.
74
75
  """
75
76
  formatted = ""
76
- if aware_datetime:
77
- local = aware_datetime.astimezone(tz or ZoneInfo(settings.TIME_ZONE))
77
+ if dt:
78
+ localized_dt = timezone.localtime(dt, timezone=tz)
78
79
  if format_as_date:
79
80
  php_dateformat = php_dateformat or settings.SHORT_DATE_FORMAT
80
- formatted = local.date().strftime(convert_php_dateformat(php_dateformat))
81
+ formatted = localized_dt.date().strftime(convert_php_dateformat(php_dateformat))
81
82
  else:
82
83
  php_dateformat = php_dateformat or settings.SHORT_DATETIME_FORMAT
83
- formatted = local.strftime(convert_php_dateformat(php_dateformat))
84
+ formatted = localized_dt.strftime(convert_php_dateformat(php_dateformat))
84
85
  return formatted
85
86
 
86
87
 
@@ -46,10 +46,18 @@ class ScheduledVisitWindowError(Exception):
46
46
  pass
47
47
 
48
48
 
49
+ class UnScheduledVisitError(Exception):
50
+ pass
51
+
52
+
49
53
  class UnScheduledVisitWindowError(Exception):
50
54
  pass
51
55
 
52
56
 
57
+ class MissedVisitError(Exception):
58
+ pass
59
+
60
+
53
61
  class SiteVisitScheduleError(Exception):
54
62
  pass
55
63
 
@@ -13,7 +13,7 @@ from edc_identifier.model_mixins import UniqueSubjectIdentifierFieldMixin
13
13
  from edc_model.validators import datetime_not_future
14
14
  from edc_protocol.validators import datetime_not_before_study_start
15
15
  from edc_sites.managers import CurrentSiteManager as BaseCurrentSiteManager
16
- from edc_utils import convert_php_dateformat
16
+ from edc_utils.text import convert_php_dateformat
17
17
 
18
18
  from ..site_visit_schedules import site_visit_schedules
19
19
 
@@ -12,7 +12,7 @@ from edc_identifier.model_mixins import UniqueSubjectIdentifierFieldMixin
12
12
  from edc_model.models import HistoricalRecords
13
13
  from edc_model.validators import datetime_not_future
14
14
  from edc_protocol.validators import datetime_not_before_study_start
15
- from edc_utils import convert_php_dateformat
15
+ from edc_utils.text import convert_php_dateformat
16
16
 
17
17
  from ..site_visit_schedules import site_visit_schedules
18
18
 
@@ -4,8 +4,6 @@ from datetime import datetime
4
4
 
5
5
  from django import forms
6
6
 
7
- from edc_utils import to_utc
8
-
9
7
  from ..subject_schedule import InvalidOffscheduleDate
10
8
  from .visit_schedule_non_crf_modelform_mixin import VisitScheduleNonCrfModelFormMixin
11
9
 
@@ -42,7 +40,7 @@ class OffScheduleModelFormMixin(VisitScheduleNonCrfModelFormMixin):
42
40
  @property
43
41
  def offschedule_datetime(self) -> datetime | None:
44
42
  if self.offschedule_datetime_field_attr in self.cleaned_data:
45
- return to_utc(self.cleaned_data.get(self.offschedule_datetime_field_attr))
43
+ return self.cleaned_data.get(self.offschedule_datetime_field_attr)
46
44
  return getattr(self.instance, self.offschedule_datetime_field_attr)
47
45
 
48
46
  @property