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
|
@@ -6,8 +6,8 @@ from django import template
|
|
|
6
6
|
|
|
7
7
|
from edc_auth.utils import get_user
|
|
8
8
|
from edc_constants.constants import CANCELLED, CLOSED, HIGH_PRIORITY, NEW, OPEN
|
|
9
|
-
from edc_utils import formatted_date
|
|
10
9
|
from edc_utils.date import to_local
|
|
10
|
+
from edc_utils.text import formatted_date
|
|
11
11
|
|
|
12
12
|
from ..models import ActionItem
|
|
13
13
|
from ..site_action_items import site_action_items
|
|
@@ -9,7 +9,7 @@ from django.conf import settings
|
|
|
9
9
|
|
|
10
10
|
from edc_constants.constants import CLOSED, OTHER
|
|
11
11
|
from edc_form_validators.base_form_validator import INVALID_ERROR
|
|
12
|
-
from edc_utils import convert_php_dateformat
|
|
12
|
+
from edc_utils.text import convert_php_dateformat
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from datetime import date
|
|
@@ -9,7 +9,7 @@ from django.conf import settings
|
|
|
9
9
|
from django.core.exceptions import ObjectDoesNotExist
|
|
10
10
|
|
|
11
11
|
from edc_constants.constants import DEAD
|
|
12
|
-
from edc_utils import convert_php_dateformat
|
|
12
|
+
from edc_utils.text import convert_php_dateformat
|
|
13
13
|
|
|
14
14
|
from ..constants import DEATH_REPORT_NOT_FOUND
|
|
15
15
|
from ..utils import get_ae_model
|
|
@@ -46,9 +46,11 @@ class BaseRequiresDeathReportFormValidatorMixin:
|
|
|
46
46
|
return self.death_report_model_cls.objects.get(
|
|
47
47
|
subject_identifier=self.subject_identifier
|
|
48
48
|
)
|
|
49
|
-
except ObjectDoesNotExist:
|
|
49
|
+
except ObjectDoesNotExist as e:
|
|
50
50
|
verbose_name = self.death_report_model_cls._meta.verbose_name
|
|
51
|
-
self.raise_validation_error(
|
|
51
|
+
self.raise_validation_error(
|
|
52
|
+
f"`{verbose_name}` not found.", DEATH_REPORT_NOT_FOUND, exc=e
|
|
53
|
+
)
|
|
52
54
|
|
|
53
55
|
@property
|
|
54
56
|
def death_report_date(self) -> date:
|
|
@@ -25,8 +25,8 @@ class DeathReportTmgFormValidator(
|
|
|
25
25
|
obj = get_ae_model("deathreport").objects.get(
|
|
26
26
|
subject_identifier=self.cleaned_data.get("subject_identifier")
|
|
27
27
|
)
|
|
28
|
-
except ObjectDoesNotExist:
|
|
29
|
-
self.raise_validation_error("Death report not found.", INVALID_ERROR)
|
|
28
|
+
except ObjectDoesNotExist as e:
|
|
29
|
+
self.raise_validation_error("Death report not found.", INVALID_ERROR, exc=e)
|
|
30
30
|
death_date = getattr(obj, obj.death_date_field)
|
|
31
31
|
try:
|
|
32
32
|
death_date = death_date.date()
|
|
@@ -9,7 +9,7 @@ from edc_action_item.fieldsets import action_fieldset_tuple
|
|
|
9
9
|
from edc_action_item.modeladmin_mixins import ActionItemModelAdminMixin
|
|
10
10
|
from edc_constants.constants import NOT_APPLICABLE, OTHER
|
|
11
11
|
from edc_model_admin.dashboard import ModelAdminSubjectDashboardMixin
|
|
12
|
-
from edc_utils import convert_php_dateformat
|
|
12
|
+
from edc_utils.text import convert_php_dateformat
|
|
13
13
|
|
|
14
14
|
from ..forms import AeTmgForm
|
|
15
15
|
from ..models import AeClassification
|
|
@@ -8,7 +8,7 @@ from django.conf import settings
|
|
|
8
8
|
from django.urls import reverse
|
|
9
9
|
|
|
10
10
|
from edc_model.models import BaseUuidModel
|
|
11
|
-
from edc_utils import convert_php_dateformat
|
|
11
|
+
from edc_utils.text import convert_php_dateformat
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from django.contrib import admin
|
edc_adverse_event/utils.py
CHANGED
|
@@ -11,7 +11,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|
|
11
11
|
from django.utils.translation import gettext as _
|
|
12
12
|
|
|
13
13
|
from edc_model_admin.utils import add_to_messages_once
|
|
14
|
-
from edc_utils import convert_php_dateformat
|
|
14
|
+
from edc_utils.text import convert_php_dateformat
|
|
15
15
|
|
|
16
16
|
from .constants import TMG_ROLE
|
|
17
17
|
|
|
@@ -54,7 +54,7 @@ class AppointmentAdmin(
|
|
|
54
54
|
):
|
|
55
55
|
show_cancel = True
|
|
56
56
|
form = AppointmentForm
|
|
57
|
-
actions =
|
|
57
|
+
actions = (appointment_mark_as_done, appointment_mark_as_new)
|
|
58
58
|
date_hierarchy = "appt_datetime"
|
|
59
59
|
list_display = (
|
|
60
60
|
"appointment_subject",
|
|
@@ -129,7 +129,7 @@ class AppointmentAdmin(
|
|
|
129
129
|
audit_fieldset_tuple,
|
|
130
130
|
)
|
|
131
131
|
|
|
132
|
-
radio_fields = {
|
|
132
|
+
radio_fields = { # noqa: RUF012
|
|
133
133
|
"appt_type": admin.VERTICAL,
|
|
134
134
|
"appt_status": admin.VERTICAL,
|
|
135
135
|
"appt_reason": admin.VERTICAL,
|
|
@@ -140,22 +140,22 @@ class AppointmentAdmin(
|
|
|
140
140
|
|
|
141
141
|
def get_readonly_fields(self, request, obj=None) -> tuple:
|
|
142
142
|
readonly_fields = super().get_readonly_fields(request, obj=obj)
|
|
143
|
-
return (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
return tuple(
|
|
144
|
+
{
|
|
145
|
+
*readonly_fields,
|
|
146
|
+
*visit_schedule_fields,
|
|
147
147
|
"subject_identifier",
|
|
148
148
|
"timepoint",
|
|
149
149
|
"timepoint_datetime",
|
|
150
150
|
"visit_code_sequence",
|
|
151
151
|
"facility_name",
|
|
152
|
-
|
|
152
|
+
}
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
def get_search_fields(self, request) -> tuple[str, ...]:
|
|
156
156
|
search_fields = super().get_search_fields(request)
|
|
157
157
|
if "subject_identifier" not in search_fields:
|
|
158
|
-
search_fields = ("subject_identifier",
|
|
158
|
+
search_fields = tuple({"subject_identifier", *search_fields})
|
|
159
159
|
return search_fields
|
|
160
160
|
|
|
161
161
|
def has_delete_permission(self, request, obj=None):
|
|
@@ -165,19 +165,19 @@ class AppointmentAdmin(
|
|
|
165
165
|
See `edc_visit_schedule.off_schedule_or_raise()`
|
|
166
166
|
"""
|
|
167
167
|
has_delete_permission = super().has_delete_permission(request, obj=obj)
|
|
168
|
-
if has_delete_permission and obj
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
168
|
+
if (has_delete_permission and obj) and (
|
|
169
|
+
(obj.visit_code_sequence == 0)
|
|
170
|
+
or (obj.visit_code_sequence != 0 and obj.appt_status != NEW_APPT)
|
|
171
|
+
):
|
|
172
|
+
try:
|
|
173
|
+
off_schedule_or_raise(
|
|
174
|
+
subject_identifier=obj.subject_identifier,
|
|
175
|
+
report_datetime=obj.appt_datetime,
|
|
176
|
+
visit_schedule_name=obj.visit_schedule_name,
|
|
177
|
+
schedule_name=obj.schedule_name,
|
|
178
|
+
)
|
|
179
|
+
except OnScheduleError:
|
|
180
|
+
has_delete_permission = False
|
|
181
181
|
return has_delete_permission
|
|
182
182
|
|
|
183
183
|
@admin.display(description="Timing", ordering="appt_timing")
|
|
@@ -262,7 +262,7 @@ class AppointmentAdmin(
|
|
|
262
262
|
)
|
|
263
263
|
return AppointmentType.objects.all().order_by("display_index")
|
|
264
264
|
|
|
265
|
-
def get_appt_reason_choices(self, request) -> tuple[Any, ...]:
|
|
265
|
+
def get_appt_reason_choices(self, request) -> tuple[Any, ...]: # noqa: ARG002
|
|
266
266
|
"""Return a choices tuple.
|
|
267
267
|
|
|
268
268
|
Important: left side of the tuple MUST have the default
|
|
@@ -280,13 +280,13 @@ class AppointmentAdmin(
|
|
|
280
280
|
return tuple([tpl for tpl in APPT_TIMING if tpl[0] != NOT_APPLICABLE])
|
|
281
281
|
return APPT_TIMING
|
|
282
282
|
|
|
283
|
-
def allow_skipped_appointments(self, request) -> bool:
|
|
283
|
+
def allow_skipped_appointments(self, request) -> bool: # noqa: ARG002
|
|
284
284
|
"""Returns True if settings.EDC_APPOINTMENT_ALLOW_SKIPPED_APPT_USING
|
|
285
285
|
has value.
|
|
286
286
|
|
|
287
287
|
Relates to use of `SKIPPED_APPT` feature.
|
|
288
288
|
"""
|
|
289
|
-
return
|
|
289
|
+
return bool(get_allow_skipped_appt_using())
|
|
290
290
|
|
|
291
291
|
def get_queryset(self, request):
|
|
292
292
|
qs = super().get_queryset(request)
|
|
@@ -34,10 +34,10 @@ class AppointmentStatusListFilter(SimpleListFilter):
|
|
|
34
34
|
parameter_name = "appt_status"
|
|
35
35
|
field_name = "appt_status"
|
|
36
36
|
|
|
37
|
-
def lookups(self, request, model_admin) -> tuple:
|
|
38
|
-
return APPT_STATUS
|
|
37
|
+
def lookups(self, request, model_admin) -> tuple[tuple[str, str], ...]: # noqa: ARG002
|
|
38
|
+
return *APPT_STATUS, (ATTENDED_APPT, "Attended (In progress, incomplete, done)")
|
|
39
39
|
|
|
40
|
-
def queryset(self, request, queryset):
|
|
40
|
+
def queryset(self, request, queryset): # noqa: ARG002
|
|
41
41
|
qs = None
|
|
42
42
|
if self.value() == ATTENDED_APPT:
|
|
43
43
|
qs = queryset.filter(
|
|
@@ -52,7 +52,7 @@ class AppointmentOverdueListFilter(SimpleListFilter):
|
|
|
52
52
|
title = "Overdue (days)"
|
|
53
53
|
parameter_name = "overdue"
|
|
54
54
|
|
|
55
|
-
def lookups(self, request, model_admin) -> tuple:
|
|
55
|
+
def lookups(self, request, model_admin) -> tuple[tuple[str, str], ...]: # noqa: ARG002
|
|
56
56
|
return (
|
|
57
57
|
(LT_30_DAYS, _("2-30 days")),
|
|
58
58
|
(GTE_30_TO_60_DAYS, _("30-60 days")),
|
|
@@ -61,7 +61,7 @@ class AppointmentOverdueListFilter(SimpleListFilter):
|
|
|
61
61
|
(GTE_180, _("180+ days")),
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
-
def queryset(self, request, queryset) -> QuerySet | None:
|
|
64
|
+
def queryset(self, request, queryset) -> QuerySet | None: # noqa: ARG002
|
|
65
65
|
now = timezone.now().replace(second=59, hour=23, minute=59)
|
|
66
66
|
qs = None
|
|
67
67
|
if self.value() == LT_30_DAYS:
|
|
@@ -48,9 +48,10 @@ class AppointmentReasonUpdater(MetadataHelperMixin):
|
|
|
48
48
|
|
|
49
49
|
def __init__(
|
|
50
50
|
self,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
*,
|
|
52
|
+
appointment: Appointment,
|
|
53
|
+
appt_timing: str,
|
|
54
|
+
appt_reason: str,
|
|
54
55
|
commit: bool | None = None,
|
|
55
56
|
):
|
|
56
57
|
self._related_visit = None
|
|
@@ -72,7 +73,7 @@ class AppointmentReasonUpdater(MetadataHelperMixin):
|
|
|
72
73
|
appointment=appointment, appt_timing=self.appt_timing
|
|
73
74
|
)
|
|
74
75
|
except (AppointmentBaselineError, UnscheduledAppointmentError) as e:
|
|
75
|
-
raise AppointmentReasonUpdaterError(e)
|
|
76
|
+
raise AppointmentReasonUpdaterError(e) from e
|
|
76
77
|
|
|
77
78
|
self.appt_reason = appt_reason or self.appointment.appt_reason
|
|
78
79
|
|
|
@@ -163,7 +164,7 @@ class AppointmentReasonUpdater(MetadataHelperMixin):
|
|
|
163
164
|
)
|
|
164
165
|
elif self.requisition_metadata_keyed_exists:
|
|
165
166
|
raise AppointmentReasonUpdaterRequisitionsExistsError(
|
|
166
|
-
"Invalid. Requisitions have already been entered
|
|
167
|
+
"Invalid. Requisitions have already been entered for this timepoint."
|
|
167
168
|
)
|
|
168
169
|
|
|
169
170
|
@property
|
|
@@ -33,9 +33,9 @@ class AppointmentStatusUpdater:
|
|
|
33
33
|
self.appointment.appt_status = IN_PROGRESS_APPT
|
|
34
34
|
self.appointment.save_base(update_fields=["appt_status"])
|
|
35
35
|
if clear_others_in_progress:
|
|
36
|
-
for
|
|
36
|
+
for appt in self.appointment.__class__.objects.filter(
|
|
37
37
|
visit_schedule_name=self.appointment.visit_schedule_name,
|
|
38
38
|
schedule_name=self.appointment.schedule_name,
|
|
39
39
|
appt_status=IN_PROGRESS_APPT,
|
|
40
40
|
).exclude(id=self.appointment.id):
|
|
41
|
-
update_appt_status(
|
|
41
|
+
update_appt_status(appt, save=True)
|
|
@@ -8,11 +8,10 @@ from edc_appointment.constants import (
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def constants(request) -> dict:
|
|
11
|
-
|
|
11
|
+
return dict(
|
|
12
12
|
COMPLETE_APPT=COMPLETE_APPT,
|
|
13
13
|
INCOMPLETE_APPT=INCOMPLETE_APPT,
|
|
14
14
|
IN_PROGRESS_APPT=IN_PROGRESS_APPT,
|
|
15
15
|
MISSED_APPT=MISSED_APPT,
|
|
16
16
|
NEW_APPT=NEW_APPT,
|
|
17
17
|
)
|
|
18
|
-
return dct
|
|
@@ -38,16 +38,17 @@ if TYPE_CHECKING:
|
|
|
38
38
|
class AppointmentCreator:
|
|
39
39
|
def __init__(
|
|
40
40
|
self,
|
|
41
|
-
|
|
41
|
+
*,
|
|
42
|
+
subject_identifier: str,
|
|
43
|
+
visit: Visit, # from edc_visit_schedule
|
|
44
|
+
visit_schedule_name: str,
|
|
45
|
+
schedule_name: str,
|
|
46
|
+
timepoint_datetime: datetime,
|
|
42
47
|
timepoint: Decimal | None = None,
|
|
43
|
-
visit: Visit | None = None, # from edc_visit_schedule
|
|
44
48
|
visit_code_sequence: int | None = None,
|
|
45
49
|
facility: Facility | None = None,
|
|
46
|
-
appointment_model: str = None,
|
|
50
|
+
appointment_model: str | None = None,
|
|
47
51
|
taken_datetimes: list[datetime] | None = None,
|
|
48
|
-
subject_identifier: str = None,
|
|
49
|
-
visit_schedule_name: str = None,
|
|
50
|
-
schedule_name: str = None,
|
|
51
52
|
default_appt_type: str | None = None,
|
|
52
53
|
default_appt_reason: str | None = None,
|
|
53
54
|
appt_status: str | None = None,
|
|
@@ -89,10 +90,10 @@ class AppointmentCreator:
|
|
|
89
90
|
f"Naive datetime not allowed. {self!r}. Got {timepoint_datetime}"
|
|
90
91
|
)
|
|
91
92
|
self.timepoint_datetime = timepoint_datetime
|
|
92
|
-
except AttributeError:
|
|
93
|
+
except AttributeError as e:
|
|
93
94
|
raise AppointmentCreatorError(
|
|
94
95
|
f"Expected 'timepoint_datetime'. Got None. {self!r}."
|
|
95
|
-
)
|
|
96
|
+
) from e
|
|
96
97
|
# suggested_datetime (defaults to timepoint_datetime)
|
|
97
98
|
# If provided, the rules for window period/rdelta relative
|
|
98
99
|
# to timepoint_datetime still apply.
|
|
@@ -6,8 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
from dateutil.relativedelta import relativedelta
|
|
7
7
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
|
8
8
|
|
|
9
|
-
from edc_utils import formatted_datetime,
|
|
10
|
-
from edc_utils.date import to_local
|
|
9
|
+
from edc_utils import formatted_date, formatted_datetime, to_local
|
|
11
10
|
from edc_visit_schedule.site_visit_schedules import site_visit_schedules
|
|
12
11
|
from edc_visit_schedule.utils import get_lower_datetime
|
|
13
12
|
|
|
@@ -49,13 +48,14 @@ class UnscheduledAppointmentCreator:
|
|
|
49
48
|
|
|
50
49
|
def __init__(
|
|
51
50
|
self,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
*,
|
|
52
|
+
subject_identifier: str,
|
|
53
|
+
visit_schedule_name: str,
|
|
54
|
+
schedule_name: str,
|
|
55
|
+
visit_code: str,
|
|
56
|
+
suggested_visit_code_sequence: int | None = None,
|
|
57
57
|
suggested_appt_datetime: datetime | None = None,
|
|
58
|
-
facility: Facility = None,
|
|
58
|
+
facility: Facility | None = None,
|
|
59
59
|
request: Any | None = None,
|
|
60
60
|
):
|
|
61
61
|
self._parent_appointment = None
|
|
@@ -125,7 +125,7 @@ class UnscheduledAppointmentCreator:
|
|
|
125
125
|
appt_status=IN_PROGRESS_APPT,
|
|
126
126
|
)
|
|
127
127
|
except MultipleObjectsReturned as e:
|
|
128
|
-
raise UnscheduledAppointmentError(e)
|
|
128
|
+
raise UnscheduledAppointmentError(e) from e
|
|
129
129
|
except ObjectDoesNotExist:
|
|
130
130
|
pass
|
|
131
131
|
else:
|
|
@@ -154,7 +154,7 @@ class UnscheduledAppointmentCreator:
|
|
|
154
154
|
msg = str(e).replace("Perhaps catch this in the form", "")
|
|
155
155
|
raise UnscheduledAppointmentError(
|
|
156
156
|
f"Unable to create unscheduled appointment. {msg}"
|
|
157
|
-
)
|
|
157
|
+
) from e
|
|
158
158
|
self.appointment = appointment_creator.appointment
|
|
159
159
|
|
|
160
160
|
def has_perm_or_raise(self, request) -> None:
|
|
@@ -170,25 +170,32 @@ class UnscheduledAppointmentCreator:
|
|
|
170
170
|
def suggested_appt_datetime(self):
|
|
171
171
|
return self._suggested_appt_datetime
|
|
172
172
|
|
|
173
|
+
def after_calling_appt_or_raise(self, suggested_dte: datetime):
|
|
174
|
+
"""Raises if on same day or before otherwise returns True"""
|
|
175
|
+
suggested_dt = to_local(suggested_dte).date()
|
|
176
|
+
calling_dt = to_local(self.calling_appointment.appt_datetime).date()
|
|
177
|
+
if suggested_dt <= calling_dt:
|
|
178
|
+
suggested = formatted_date(suggested_dt)
|
|
179
|
+
calling = formatted_date(calling_dt)
|
|
180
|
+
raise CreateAppointmentError(
|
|
181
|
+
"Suggested appointment date must be after the calling appointment date. "
|
|
182
|
+
f"Got {suggested} not after {calling}."
|
|
183
|
+
)
|
|
184
|
+
return True
|
|
185
|
+
|
|
173
186
|
@suggested_appt_datetime.setter
|
|
174
|
-
def suggested_appt_datetime(self,
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
raise CreateAppointmentError(
|
|
178
|
-
"Suggested date/time must be after the calling appointment date/time"
|
|
179
|
-
)
|
|
180
|
-
self._suggested_appt_datetime = value
|
|
187
|
+
def suggested_appt_datetime(self, suggested_dte: datetime | None):
|
|
188
|
+
if suggested_dte and self.after_calling_appt_or_raise(suggested_dte):
|
|
189
|
+
self._suggested_appt_datetime = suggested_dte
|
|
181
190
|
else:
|
|
182
191
|
self._suggested_appt_datetime = (
|
|
183
192
|
self.calling_appointment.appt_datetime + relativedelta(days=1)
|
|
184
193
|
)
|
|
185
|
-
if self.parent_appointment.next and
|
|
186
|
-
self.
|
|
187
|
-
)
|
|
188
|
-
dt = formatted_datetime(
|
|
189
|
-
next_dt = formatted_datetime(
|
|
190
|
-
to_local(get_lower_datetime(self.parent_appointment.next))
|
|
191
|
-
)
|
|
194
|
+
if self.parent_appointment.next and self.suggested_appt_datetime >= get_lower_datetime(
|
|
195
|
+
self.parent_appointment.next
|
|
196
|
+
):
|
|
197
|
+
dt = formatted_datetime(self.suggested_appt_datetime)
|
|
198
|
+
next_dt = formatted_datetime(get_lower_datetime(self.parent_appointment.next))
|
|
192
199
|
raise UnscheduledAppointmentError(
|
|
193
200
|
"Appointment date exceeds window period. Next appointment is "
|
|
194
201
|
f"{self.parent_appointment.next.visit_code} and lower window starts "
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
@@ -7,7 +8,6 @@ from dateutil.relativedelta import relativedelta
|
|
|
7
8
|
from django.db import transaction
|
|
8
9
|
|
|
9
10
|
from ..constants import INCOMPLETE_APPT, NEW_APPT
|
|
10
|
-
from ..utils import get_appointment_by_datetime
|
|
11
11
|
from .unscheduled_appointment_creator import (
|
|
12
12
|
CreateAppointmentError,
|
|
13
13
|
UnscheduledAppointmentCreator,
|
|
@@ -18,13 +18,14 @@ if TYPE_CHECKING:
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def create_next_appointment_as_interim(
|
|
21
|
-
next_appt_datetime: datetime
|
|
21
|
+
appointment: Appointment, next_appt_datetime: datetime | None = None
|
|
22
22
|
) -> Appointment | None:
|
|
23
|
-
return create_unscheduled_appointment(
|
|
23
|
+
return create_unscheduled_appointment(appointment, next_appt_datetime=next_appt_datetime)
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def create_unscheduled_appointment(
|
|
27
|
-
|
|
27
|
+
appointment: Appointment,
|
|
28
|
+
next_appt_datetime: datetime | None = None,
|
|
28
29
|
) -> Appointment | None:
|
|
29
30
|
"""Create and return an unscheduled appointment if
|
|
30
31
|
`next_appt_datetime` is within the window period of
|
|
@@ -35,36 +36,33 @@ def create_unscheduled_appointment(
|
|
|
35
36
|
next_appt_datetime = next_appt_datetime or appointment.appt_datetime + relativedelta(
|
|
36
37
|
days=1
|
|
37
38
|
)
|
|
38
|
-
next_appointment = get_appointment_by_datetime(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
if appointment == next_appointment:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
appointment.appt_status = appt_status
|
|
69
|
-
appointment.save_base(update_fields=["appt_status"])
|
|
70
|
-
return unscheduled_appointment
|
|
39
|
+
# next_appointment = get_appointment_by_datetime(
|
|
40
|
+
# next_appt_datetime,
|
|
41
|
+
# appointment.subject_identifier,
|
|
42
|
+
# appointment.visit_schedule_name,
|
|
43
|
+
# appointment.schedule_name,
|
|
44
|
+
# raise_if_in_gap=False,
|
|
45
|
+
# )
|
|
46
|
+
# if appointment == next_appointment:
|
|
47
|
+
if (
|
|
48
|
+
appointment.relative_next
|
|
49
|
+
and appointment.relative_next.appt_status == NEW_APPT
|
|
50
|
+
and appointment.relative_next.visit_code_sequence > 0
|
|
51
|
+
):
|
|
52
|
+
appointment.relative_next.delete()
|
|
53
|
+
appt_status = appointment.appt_status
|
|
54
|
+
appointment.appt_status = INCOMPLETE_APPT
|
|
55
|
+
appointment.save_base(update_fields=["appt_status"])
|
|
56
|
+
with transaction.atomic(), contextlib.suppress(CreateAppointmentError):
|
|
57
|
+
unscheduled_appointment = UnscheduledAppointmentCreator(
|
|
58
|
+
subject_identifier=appointment.subject_identifier,
|
|
59
|
+
visit_schedule_name=appointment.visit_schedule_name,
|
|
60
|
+
schedule_name=appointment.schedule_name,
|
|
61
|
+
visit_code=appointment.visit_code,
|
|
62
|
+
facility=appointment.facility,
|
|
63
|
+
suggested_appt_datetime=next_appt_datetime,
|
|
64
|
+
suggested_visit_code_sequence=appointment.visit_code_sequence + 1,
|
|
65
|
+
)
|
|
66
|
+
appointment.appt_status = appt_status
|
|
67
|
+
appointment.save_base(update_fields=["appt_status"])
|
|
68
|
+
return getattr(unscheduled_appointment, "appointment", None)
|
edc_appointment/exceptions.py
CHANGED
|
@@ -30,7 +30,7 @@ class AppointmentDatetimeError(Exception):
|
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class UnknownVisitCode(Exception):
|
|
33
|
+
class UnknownVisitCode(Exception): # noqa: N818
|
|
34
34
|
pass
|
|
35
35
|
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ class AppointmentWindowError(Exception):
|
|
|
38
38
|
pass
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
class AppointmentPermissionsRequired(Exception):
|
|
41
|
+
class AppointmentPermissionsRequired(Exception): # noqa: N818
|
|
42
42
|
pass
|
|
43
43
|
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ class AppointmentMissingValuesError(Exception):
|
|
|
46
46
|
pass
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class UnscheduledAppointmentNotAllowed(Exception):
|
|
49
|
+
class UnscheduledAppointmentNotAllowed(Exception): # noqa: N818
|
|
50
50
|
pass
|
|
51
51
|
|
|
52
52
|
|
edc_appointment/form_runners.py
CHANGED
|
@@ -7,5 +7,5 @@ from edc_form_runners.form_runner import FormRunner
|
|
|
7
7
|
@register()
|
|
8
8
|
class AppointmentFormRunner(FormRunner):
|
|
9
9
|
model_name = "edc_appointment.appointment"
|
|
10
|
-
extra_fieldnames =
|
|
11
|
-
exclude_formfields =
|
|
10
|
+
extra_fieldnames = ("appt_datetime",)
|
|
11
|
+
exclude_formfields = ("appt_close_datetime",)
|
|
@@ -82,7 +82,7 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
|
|
|
82
82
|
visit_code=self.visit_code,
|
|
83
83
|
visit_code_sequence=0,
|
|
84
84
|
)
|
|
85
|
-
except ObjectDoesNotExist:
|
|
85
|
+
except ObjectDoesNotExist as e:
|
|
86
86
|
self.raise_validation_error(
|
|
87
87
|
{
|
|
88
88
|
self.visit_code_fld: (
|
|
@@ -91,6 +91,7 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
|
|
|
91
91
|
)
|
|
92
92
|
},
|
|
93
93
|
INVALID_ERROR,
|
|
94
|
+
exc=e,
|
|
94
95
|
)
|
|
95
96
|
if instance == self.related_visit.appointment:
|
|
96
97
|
self.raise_validation_error(
|
|
@@ -117,7 +118,7 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
|
|
|
117
118
|
appointment_validator.validate()
|
|
118
119
|
except ValidationError as e:
|
|
119
120
|
if e.message_dict.get("appt_datetime"):
|
|
120
|
-
raise ValidationError({"appt_date": e.message_dict["appt_datetime"]})
|
|
121
|
+
raise ValidationError({"appt_date": e.message_dict["appt_datetime"]}) from e
|
|
121
122
|
raise
|
|
122
123
|
|
|
123
124
|
@property
|
|
@@ -134,9 +135,8 @@ class NextAppointmentCrfFormValidatorMixin(FormValidator):
|
|
|
134
135
|
|
|
135
136
|
@property
|
|
136
137
|
def clinic_days(self) -> list[int]:
|
|
137
|
-
if not self._clinic_days:
|
|
138
|
-
|
|
139
|
-
self._clinic_days = self.health_facility.clinic_days
|
|
138
|
+
if not self._clinic_days and self.cleaned_data.get("health_facility"):
|
|
139
|
+
self._clinic_days = self.health_facility.clinic_days
|
|
140
140
|
return self._clinic_days
|
|
141
141
|
|
|
142
142
|
def validate_date_is_on_clinic_day(self):
|
|
@@ -38,7 +38,7 @@ class WindowPeriodFormValidatorMixin:
|
|
|
38
38
|
and appointment.visit_code_sequence > 0
|
|
39
39
|
and appointment.next
|
|
40
40
|
and appointment.next.appt_status in [INCOMPLETE_APPT, COMPLETE_APPT]
|
|
41
|
-
and
|
|
41
|
+
and proposed_appt_datetime < appointment.next.appt_datetime
|
|
42
42
|
):
|
|
43
43
|
value = True
|
|
44
44
|
return value
|
|
@@ -52,13 +52,11 @@ class WindowPeriodFormValidatorMixin:
|
|
|
52
52
|
if proposed_appt_datetime:
|
|
53
53
|
try:
|
|
54
54
|
appointment.schedule.datetime_in_window(
|
|
55
|
-
timepoint_datetime=
|
|
56
|
-
dt=
|
|
55
|
+
timepoint_datetime=appointment.timepoint_datetime,
|
|
56
|
+
dt=proposed_appt_datetime,
|
|
57
57
|
visit_code=appointment.visit_code,
|
|
58
58
|
visit_code_sequence=appointment.visit_code_sequence,
|
|
59
|
-
baseline_timepoint_datetime=
|
|
60
|
-
self.baseline_timepoint_datetime(appointment)
|
|
61
|
-
),
|
|
59
|
+
baseline_timepoint_datetime=self.baseline_timepoint_datetime(appointment),
|
|
62
60
|
)
|
|
63
61
|
except UnScheduledVisitWindowError:
|
|
64
62
|
if not self.ignore_window_period_for_unscheduled(
|
|
@@ -90,7 +88,9 @@ class WindowPeriodFormValidatorMixin:
|
|
|
90
88
|
UNSCHEDULED_WINDOW_ERROR,
|
|
91
89
|
)
|
|
92
90
|
except ScheduledVisitWindowError as e:
|
|
93
|
-
self.raise_validation_error(
|
|
91
|
+
self.raise_validation_error(
|
|
92
|
+
{form_field: (str(e))}, SCHEDULED_WINDOW_ERROR, exc=e
|
|
93
|
+
)
|
|
94
94
|
|
|
95
95
|
@staticmethod
|
|
96
96
|
def baseline_timepoint_datetime(appointment: Appointment) -> datetime:
|