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