clinicedc 2.0.34__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.
- {clinicedc-2.0.34.dist-info → clinicedc-2.0.35.dist-info}/METADATA +1 -1
- {clinicedc-2.0.34.dist-info → clinicedc-2.0.35.dist-info}/RECORD +155 -165
- {clinicedc-2.0.34.dist-info → clinicedc-2.0.35.dist-info}/WHEEL +1 -1
- edc_action_item/templatetags/action_item_extras.py +1 -1
- edc_adverse_event/form_validator_mixins/death_report_form_validator.py +1 -1
- edc_adverse_event/form_validator_mixins/requires_death_report_form_validator_mixin.py +5 -3
- edc_adverse_event/form_validators/death_report_tmg.py +2 -2
- edc_adverse_event/modeladmin_mixins/ae_tmg_admin_mixin.py +1 -1
- edc_adverse_event/modeladmin_mixins/utils.py +1 -1
- edc_adverse_event/utils.py +1 -1
- edc_appointment/admin/appointment_admin.py +24 -24
- edc_appointment/admin/list_filters.py +5 -5
- edc_appointment/appointment_reason_updater.py +6 -5
- edc_appointment/appointment_status_updater.py +2 -2
- edc_appointment/context_processors.py +1 -2
- edc_appointment/creators/appointment_creator.py +9 -8
- edc_appointment/creators/unscheduled_appointment_creator.py +31 -24
- edc_appointment/creators/utils.py +35 -37
- edc_appointment/exceptions.py +3 -3
- edc_appointment/form_runners.py +2 -2
- edc_appointment/form_validator_mixins/next_appointment_crf_form_validator_mixin.py +5 -5
- edc_appointment/form_validator_mixins/window_period_form_validator_mixin.py +7 -7
- edc_appointment/form_validators/appointment_form_validator.py +16 -23
- edc_appointment/management/commands/close_appointments.py +3 -3
- edc_appointment/management/commands/reset_visit_code_sequences.py +1 -1
- edc_appointment/management/commands/update_appointment_status.py +2 -2
- edc_appointment/management/commands/update_skipped_appointments.py +7 -7
- edc_appointment/managers.py +6 -7
- edc_appointment/model_mixins/appointment_model_mixin.py +3 -4
- edc_appointment/modeladmin_mixins/next_appointment_crf_modeladmin_mixin.py +2 -2
- edc_appointment/modelform_mixins/next_appointment_crf_modelform_mixins.py +25 -24
- edc_appointment/models/signals.py +1 -1
- edc_appointment/skip_appointments.py +10 -29
- edc_appointment/utils.py +43 -47
- edc_appointment/view_mixins/appointment_view_mixin.py +11 -13
- edc_appointment/views/unscheduled_appointment_view.py +5 -6
- edc_consent/consent_definition.py +5 -13
- edc_consent/consent_definition_extension.py +0 -2
- edc_consent/form_validators/consent_definition_form_validator_mixin.py +26 -30
- edc_consent/form_validators/subject_consent_form_validator.py +3 -3
- edc_consent/modelform_mixins/consent_modelform_mixin/consent_modelform_validation_mixin.py +1 -1
- edc_consent/modelform_mixins/requires_consent_modelform_mixin.py +4 -5
- edc_consent/site_consents.py +13 -14
- edc_crf/crf_form_validator.py +13 -19
- edc_crf/crf_form_validator_mixins.py +2 -17
- edc_data_manager/admin/actions.py +1 -1
- edc_data_manager/admin/data_query_admin.py +1 -1
- edc_dx/form_validators/result_form_validator_mixin.py +1 -1
- edc_dx_review/medical_date.py +1 -1
- edc_egfr/egfr.py +4 -8
- edc_egfr/form_validator_mixins/egfr_form_validator_mixins.py +2 -2
- edc_facility/facility.py +7 -11
- edc_facility/holidays.py +3 -3
- edc_facility/models/holiday.py +1 -1
- edc_form_runners/form_runner.py +15 -16
- edc_form_validators/base_form_validator.py +5 -1
- edc_form_validators/date_range_validator.py +49 -61
- edc_form_validators/date_validator.py +1 -1
- edc_form_validators/extra_mixins/study_day_form_validator.py +1 -1
- edc_lab/form_validators/crf_requisition_form_validator_mixin.py +21 -15
- edc_lab/form_validators/requisition_form_validator_mixin.py +3 -3
- edc_lab_results/form_validator_mixins/blood_results_form_validator_mixin.py +1 -1
- edc_ltfu/modelform_mixins.py +1 -1
- edc_metadata/metadata/metadata.py +20 -7
- edc_metadata/metadata_rules/logic.py +5 -4
- edc_metadata/metadata_rules/predicate.py +22 -24
- edc_model_form/mixins/report_datetime_modelform_mixin.py +1 -5
- edc_offstudy/model_mixins/offstudy_model_mixin.py +1 -1
- edc_offstudy/modelform_mixins/crf/offstudy_crf_modelform_mixin.py +2 -2
- edc_offstudy/utils.py +4 -4
- edc_pdf_reports/crf_pdf_report.py +2 -1
- edc_pdutils/helper.py +3 -3
- edc_pdutils/utils/convert_dates_from_model.py +4 -3
- edc_pharmacy/admin/actions/confirm_stock.py +3 -3
- edc_pharmacy/admin/actions/delete_items_for_stock_request.py +4 -3
- edc_pharmacy/admin/actions/delete_order_items.py +7 -3
- edc_pharmacy/admin/actions/delete_receive_items.py +5 -3
- edc_pharmacy/admin/actions/process_repack_request.py +7 -11
- edc_pharmacy/admin/autocomplete_admin.py +1 -1
- edc_pharmacy/admin/list_filters.py +48 -46
- edc_pharmacy/admin/medication/assignment_admin.py +4 -4
- edc_pharmacy/admin/medication/dosage_guideline_admin.py +3 -3
- edc_pharmacy/admin/medication/formulation_admin.py +5 -5
- edc_pharmacy/admin/medication/medication_admin.py +3 -3
- edc_pharmacy/admin/prescription/rx_admin.py +2 -2
- edc_pharmacy/admin/prescription/rx_refill_admin.py +11 -17
- edc_pharmacy/admin/remove_fields_for_blinded_users.py +1 -1
- edc_pharmacy/admin/reports/stock_availability_admin.py +12 -8
- edc_pharmacy/admin/stock/allocation_admin.py +13 -17
- edc_pharmacy/admin/stock/allocation_proxy_admin.py +1 -2
- edc_pharmacy/admin/stock/confirmation_admin.py +4 -8
- edc_pharmacy/admin/stock/confirmation_at_site_item_admin.py +1 -1
- edc_pharmacy/admin/stock/container_admin.py +3 -3
- edc_pharmacy/admin/stock/location_admin.py +3 -6
- edc_pharmacy/admin/stock/lot_admin.py +9 -12
- edc_pharmacy/admin/stock/order_admin.py +2 -5
- edc_pharmacy/admin/stock/order_item_admin.py +14 -22
- edc_pharmacy/admin/stock/product_admin.py +6 -9
- edc_pharmacy/admin/stock/receive_admin.py +2 -2
- edc_pharmacy/admin/stock/receive_item_admin.py +3 -3
- edc_pharmacy/admin/stock/repack_request_admin.py +7 -10
- edc_pharmacy/admin/stock/stock_adjustment_admin.py +1 -1
- edc_pharmacy/admin/stock/stock_admin.py +17 -28
- edc_pharmacy/admin/stock/stock_proxy_admin.py +3 -4
- edc_pharmacy/admin/stock/stock_request_admin.py +6 -10
- edc_pharmacy/admin/stock/stock_request_item_admin.py +9 -18
- edc_pharmacy/admin/stock/stock_transfer_admin.py +5 -5
- edc_pharmacy/admin/stock/stock_transfer_item_admin.py +3 -3
- edc_pharmacy/admin/stock/storage_bin_admin.py +1 -1
- edc_pharmacy/admin/stock/storage_bin_item_admin.py +2 -2
- edc_pharmacy/form_validators/crf/study_medication_form_validator.py +27 -27
- edc_pharmacy/forms/stock/confirmation_form.py +2 -2
- edc_pharmacy/forms/stock/dispense_form.py +2 -2
- edc_pharmacy/forms/stock/lot_form.py +6 -6
- edc_pharmacy/forms/stock/order_form.py +4 -6
- edc_pharmacy/forms/stock/product_form.py +2 -3
- edc_pharmacy/forms/stock/receive_form.py +4 -4
- edc_pharmacy/forms/stock/receive_item_form.py +7 -5
- edc_pharmacy/forms/stock/repack_request_form.py +10 -8
- edc_pharmacy/forms/stock/stock_form.py +2 -3
- edc_pharmacy/forms/stock/stock_request_form.py +10 -8
- edc_pharmacy/forms/stock/stock_request_item_form.py +6 -5
- edc_pharmacy/forms/stock/stock_transfer_form.py +16 -14
- edc_pharmacy/forms/stock/supplier_form.py +2 -2
- edc_pharmacy/labels/label_data.py +2 -3
- edc_pharmacy/model_mixins/study_medication_crf_model_mixin.py +1 -1
- edc_pharmacy/models/medication/dosage_guideline.py +3 -3
- edc_pharmacy/models/model_mixins.py +12 -10
- edc_pharmacy/models/prescription/rx.py +1 -1
- edc_pharmacy/models/prescription/rx_refill.py +1 -1
- edc_pharmacy/models/signals.py +2 -3
- edc_pharmacy/models/storage/utils.py +4 -4
- edc_pharmacy/pdf_reports/manifest_pdf_report.py +3 -6
- edc_pharmacy/pdf_reports/stock_pdf_report.py +1 -3
- edc_pharmacy/refill/refill_creator.py +3 -4
- edc_protocol/validators.py +10 -16
- edc_reportable/forms/reportables_form_validator_mixin.py +1 -1
- edc_screening/form_validator_mixins.py +2 -1
- edc_transfer/form_validators.py +1 -1
- edc_transfer/model_mixins.py +1 -1
- edc_utils/age.py +17 -15
- edc_utils/date.py +7 -7
- edc_utils/text.py +7 -6
- edc_visit_schedule/exceptions.py +8 -0
- edc_visit_schedule/model_mixins/off_schedule_model_mixin.py +1 -1
- edc_visit_schedule/model_mixins/on_schedule_model_mixin.py +1 -1
- edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py +1 -3
- edc_visit_schedule/schedule/visit_collection.py +1 -1
- edc_visit_schedule/schedule/window.py +0 -2
- edc_visit_schedule/subject_schedule.py +1 -1
- edc_visit_schedule/utils.py +4 -4
- edc_visit_schedule/visit/visit.py +9 -3
- edc_visit_schedule/visit/window_period.py +1 -1
- edc_visit_tracking/form_validators/visit_form_validator.py +1 -1
- edc_metadata/metadata_wrappers/__init__.py +0 -5
- edc_metadata/metadata_wrappers/crf_metadata_wrapper.py +0 -5
- edc_metadata/metadata_wrappers/crf_metadata_wrappers.py +0 -8
- edc_metadata/metadata_wrappers/metadata_wrapper.py +0 -74
- edc_metadata/metadata_wrappers/metadata_wrappers.py +0 -33
- edc_metadata/metadata_wrappers/requisition_metadata_wrapper.py +0 -26
- edc_metadata/metadata_wrappers/requisition_metadata_wrappers.py +0 -10
- edc_pharmacy/management/__init__.py +0 -0
- edc_pharmacy/management/commands/__init__.py +0 -0
- edc_pharmacy/management/commands/update_initial_pharmacy_data.py +0 -10
- {clinicedc-2.0.34.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
|
|
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,
|
|
84
|
-
return self.holidays.is_holiday(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
89
|
-
"""Returns True if the
|
|
90
|
-
local_date = to_local(
|
|
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:
|
edc_facility/models/holiday.py
CHANGED
|
@@ -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):
|
edc_form_runners/form_runner.py
CHANGED
|
@@ -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:
|
|
29
|
-
exclude_formfields:
|
|
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
|
-
|
|
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) ->
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
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) ->
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
15
|
-
date1 = date1.date()
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
81
|
-
>
|
|
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
|
|
edc_ltfu/modelform_mixins.py
CHANGED
|
@@ -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 =
|
|
212
|
-
crfs =
|
|
213
|
-
|
|
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 =
|
|
218
|
-
crfs =
|
|
219
|
-
|
|
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 =
|
|
28
|
+
valid_results = (REQUIRED, NOT_REQUIRED, DO_NOTHING)
|
|
28
29
|
|
|
29
30
|
def __init__(
|
|
30
31
|
self,
|
|
31
|
-
predicate: P | PF |
|
|
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:
|
|
67
|
-
"is not": lambda x, y:
|
|
68
|
-
"gt": lambda x, y:
|
|
69
|
-
">": lambda x, y:
|
|
70
|
-
"gte": lambda x, y:
|
|
71
|
-
">=": lambda x, y:
|
|
72
|
-
"lt": lambda x, y:
|
|
73
|
-
"<": lambda x, y:
|
|
74
|
-
"lte": lambda x, y:
|
|
75
|
-
"<=": lambda x, y:
|
|
76
|
-
"eq": lambda x, y:
|
|
77
|
-
"equals": lambda x, y:
|
|
78
|
-
"==": lambda x, y:
|
|
79
|
-
"neq": lambda x, y:
|
|
80
|
-
"!=": lambda x, y:
|
|
81
|
-
"in": lambda x, y:
|
|
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:
|
|
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
|
|
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
|