clinicedc 2.0.11__py3-none-any.whl → 2.0.13__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.11.dist-info → clinicedc-2.0.13.dist-info}/METADATA +2 -1
- {clinicedc-2.0.11.dist-info → clinicedc-2.0.13.dist-info}/RECORD +137 -24
- edc_action_item/migrations/0017_auto_20190305_0123.py +1 -1
- edc_action_item/migrations/0030_edcpermissions.py +1 -1
- edc_action_item/migrations/0041_alter_actionitem_revision_alter_actiontype_revision_and_more.py +86 -0
- edc_adverse_event/migrations/0001_initial.py +1 -1
- edc_adverse_event/migrations/0002_auto_20190802_0059.py +1 -1
- edc_adverse_event/migrations/0008_auto_20220825_0451.py +1 -1
- edc_adverse_event/migrations/0009_auto_20220907_0157.py +1 -1
- edc_adverse_event/migrations/0017_alter_aeactionclassification_revision_and_more.py +77 -0
- edc_adverse_event/model_mixins/hospitaization/hospitalization_model_mixin.py +1 -3
- edc_analytics/__init__.py +3 -0
- edc_analytics/apps.py +8 -0
- edc_analytics/constants.py +26 -0
- edc_analytics/custom_tables/__init__.py +11 -0
- edc_analytics/custom_tables/age.py +72 -0
- edc_analytics/custom_tables/art.py +88 -0
- edc_analytics/custom_tables/bmi.py +125 -0
- edc_analytics/custom_tables/bp.py +103 -0
- edc_analytics/custom_tables/fasting.py +126 -0
- edc_analytics/custom_tables/fbg.py +98 -0
- edc_analytics/custom_tables/fbg_ogtt.py +384 -0
- edc_analytics/custom_tables/gender.py +12 -0
- edc_analytics/custom_tables/hba1c.py +87 -0
- edc_analytics/custom_tables/ogtt.py +95 -0
- edc_analytics/custom_tables/waist.py +105 -0
- edc_analytics/data.py +36 -0
- edc_analytics/row/__init__.py +4 -0
- edc_analytics/row/row_definition.py +43 -0
- edc_analytics/row/row_definitions.py +32 -0
- edc_analytics/row/row_statistics.py +88 -0
- edc_analytics/row/row_statistics_with_gender.py +115 -0
- edc_analytics/stata/__init__.py +1 -0
- edc_analytics/stata/get_stata_labels_from_model.py +44 -0
- edc_analytics/styler.py +93 -0
- edc_analytics/table.py +108 -0
- edc_analytics/urls.py +6 -0
- edc_appointment/migrations/0018_auto_20190305_0123.py +1 -1
- edc_appointment/migrations/0051_alter_appointment_revision_and_more.py +38 -0
- edc_auth/migrations/0001_squashed_0033_alter_userprofile_is_multisite_viewer.py +1 -1
- edc_auth/migrations/0012_auto_20191026_0034.py +1 -1
- edc_auth/migrations/0013_auto_20191026_0055.py +1 -1
- edc_auth/migrations/0025_permissions.py +1 -1
- edc_auth/migrations/0037_alter_edcpermissions_revision_alter_role_revision.py +38 -0
- edc_consent/migrations/0001_initial.py +1 -1
- edc_consent/migrations/0007_alter_edcpermissions_revision.py +26 -0
- edc_crf/migrations/0010_alter_crfstatus_revision.py +26 -0
- edc_dashboard/migrations/0001_initial.py +1 -1
- edc_dashboard/migrations/0007_alter_edcpermissions_revision.py +26 -0
- edc_data_manager/migrations/0001_initial.py +1 -1
- edc_data_manager/migrations/0025_edcpermissions.py +1 -1
- edc_data_manager/migrations/0042_alter_datadictionary_revision_and_more.py +98 -0
- edc_dx/__init__.py +6 -0
- edc_dx/apps.py +5 -0
- edc_dx/diagnoses.py +250 -0
- edc_dx/form_validators/__init__.py +2 -0
- edc_dx/form_validators/diagnosis_form_validator_mixin.py +54 -0
- edc_dx/form_validators/result_form_validator_mixin.py +65 -0
- edc_dx/utils.py +42 -0
- edc_dx_review/__init__.py +0 -0
- edc_dx_review/apps.py +5 -0
- edc_dx_review/auth_objects.py +13 -0
- edc_dx_review/auths.py +12 -0
- edc_dx_review/choices.py +24 -0
- edc_dx_review/constants.py +7 -0
- edc_dx_review/fieldsets.py +47 -0
- edc_dx_review/form_mixins/__init__.py +3 -0
- edc_dx_review/form_mixins/clinical_review_baseline_required_form_mixin.py +25 -0
- edc_dx_review/form_validator_mixins/__init__.py +6 -0
- edc_dx_review/form_validator_mixins/clinical_review_baseline_form_validator_mixin.py +7 -0
- edc_dx_review/form_validator_mixins/clinical_review_followup_form_validator_mixin.py +25 -0
- edc_dx_review/list_data.py +19 -0
- edc_dx_review/medical_date.py +195 -0
- edc_dx_review/migrations/0001_initial.py +307 -0
- edc_dx_review/migrations/0002_diagnosislocations_extra_value_and_more.py +32 -0
- edc_dx_review/migrations/0003_alter_diagnosislocations_options_and_more.py +148 -0
- edc_dx_review/migrations/0004_remove_diagnosislocations_edc_dx_revi_name_a39b40_idx_and_more.py +20 -0
- edc_dx_review/migrations/__init__.py +0 -0
- edc_dx_review/model_mixins/__init__.py +20 -0
- edc_dx_review/model_mixins/clinical_review_baseline_model_mixin.py +25 -0
- edc_dx_review/model_mixins/clinical_review_followup/__init__.py +5 -0
- edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_chol_model_mixin.py +54 -0
- edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_dm_model_mixin.py +54 -0
- edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_hiv_model_mixin.py +54 -0
- edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_htn_model_mixin.py +56 -0
- edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_model_mixin.py +25 -0
- edc_dx_review/model_mixins/dx_location_model_mixin.py +17 -0
- edc_dx_review/model_mixins/factory/__init__.py +4 -0
- edc_dx_review/model_mixins/factory/baseline_review_model_mixin_factory.py +55 -0
- edc_dx_review/model_mixins/factory/calculate_date.py +43 -0
- edc_dx_review/model_mixins/factory/dx_initial_review_model_mixin_factory.py +97 -0
- edc_dx_review/model_mixins/factory/followup_review_model_mixin_factory.py +39 -0
- edc_dx_review/model_mixins/factory/rx_initial_review_model_mixin_factory.py +69 -0
- edc_dx_review/model_mixins/followup_review/__init__.py +2 -0
- edc_dx_review/model_mixins/followup_review/followup_review_model_mixin.py +22 -0
- edc_dx_review/model_mixins/followup_review/hiv_followup_review_model_mixin.py +32 -0
- edc_dx_review/model_mixins/initial_review/__init__.py +6 -0
- edc_dx_review/model_mixins/initial_review/chol_initial_review_model_mixin.py +34 -0
- edc_dx_review/model_mixins/initial_review/hiv_initial_model_mixins.py +119 -0
- edc_dx_review/model_mixins/initial_review/ncd_initial_review_model_mixin.py +42 -0
- edc_dx_review/models.py +20 -0
- edc_dx_review/radio_fields.py +30 -0
- edc_dx_review/utils.py +220 -0
- edc_export/migrations/0004_auto_20190305_0123.py +1 -1
- edc_export/migrations/0013_edcpermissions.py +1 -1
- edc_export/migrations/0024_alter_datarequest_revision_and_more.py +170 -0
- edc_facility/migrations/0005_healthfacility_healthfacilitytypes_and_more.py +1 -1
- edc_facility/migrations/0018_alter_healthfacility_revision_and_more.py +38 -0
- edc_form_runners/migrations/0006_alter_issue_revision.py +26 -0
- edc_identifier/migrations/0012_alter_identifiermodel_revision.py +26 -0
- edc_lab/migrations/0039_alter_aliquot_revision_alter_box_revision_and_more.py +269 -0
- edc_lab_dashboard/migrations/0006_alter_edcpermissions_revision.py +26 -0
- edc_label/migrations/0008_alter_zpllabeltemplates_revision.py +26 -0
- edc_listboard/migrations/0008_alter_listboard_revision.py +26 -0
- edc_locator/migrations/0042_alter_historicalsubjectlocator_revision_and_more.py +38 -0
- edc_metadata/migrations/0032_alter_crfmetadata_revision_and_more.py +38 -0
- edc_navbar/migrations/0010_alter_edcpermissions_revision.py +26 -0
- edc_notification/migrations/0012_alter_notification_revision.py +26 -0
- edc_offstudy/migrations/0025_alter_historicalsubjectoffstudy_revision_and_more.py +41 -0
- edc_pharmacy/migrations/0091_alter_allocation_revision_alter_assignment_revision_and_more.py +794 -0
- edc_protocol_incident/migrations/0026_alter_historicalprotocoldeviationviolation_revision_and_more.py +65 -0
- edc_pylabels/migrations/0014_alter_labelconfiguration_revision.py +26 -0
- edc_qareports/migrations/0021_alter_edcpermissions_revision_alter_note_revision.py +38 -0
- edc_randomization/migrations/0015_alter_edcpermissions_revision_and_more.py +50 -0
- edc_refusal/migrations/0014_alter_historicalsubjectrefusal_revision_and_more.py +38 -0
- edc_registration/migrations/0034_alter_historicalregisteredsubject_revision_and_more.py +41 -0
- edc_reportable/migrations/0008_alter_gradingdata_revision_and_more.py +110 -0
- edc_review_dashboard/migrations/0007_alter_edcpermissions_revision.py +26 -0
- edc_screening/migrations/0006_alter_edcpermissions_revision.py +26 -0
- edc_sites/migrations/0011_alter_edcpermissions_revision.py +26 -0
- edc_subject_dashboard/migrations/0006_alter_edcpermissions_revision.py +26 -0
- edc_unblinding/migrations/0016_alter_historicalunblindingrequest_revision_and_more.py +65 -0
- edc_visit_schedule/migrations/0021_alter_historicalonschedule_revision_and_more.py +89 -0
- edc_visit_tracking/migrations/0011_alter_historicalsubjectvisit_revision_and_more.py +65 -0
- edc_vitals/model_mixins/blood_pressure_model_mixin.py +1 -0
- {clinicedc-2.0.11.dist-info → clinicedc-2.0.13.dist-info}/WHEEL +0 -0
- {clinicedc-2.0.11.dist-info → clinicedc-2.0.13.dist-info}/licenses/LICENSE +0 -0
edc_dx/diagnoses.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
|
7
|
+
from edc_constants.constants import YES
|
|
8
|
+
from edc_dx_review.utils import (
|
|
9
|
+
get_clinical_review_baseline_model_cls,
|
|
10
|
+
get_clinical_review_model_cls,
|
|
11
|
+
get_initial_review_model_cls,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .utils import get_diagnosis_labels
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InitialReviewRequired(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MultipleInitialReviewsExist(Exception):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClinicalReviewBaselineRequired(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DiagnosesError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Diagnoses:
|
|
34
|
+
"""
|
|
35
|
+
Tightly coupled to models
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
subject_identifier: str = None,
|
|
41
|
+
report_datetime: datetime = None,
|
|
42
|
+
subject_visit=None,
|
|
43
|
+
lte: bool | None = None,
|
|
44
|
+
limit_to_single_condition_prefix: str | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.single_condition_prefix = (
|
|
47
|
+
limit_to_single_condition_prefix.lower()
|
|
48
|
+
if limit_to_single_condition_prefix
|
|
49
|
+
else None
|
|
50
|
+
)
|
|
51
|
+
if subject_visit:
|
|
52
|
+
if subject_identifier or report_datetime:
|
|
53
|
+
raise DiagnosesError(
|
|
54
|
+
"Ambiguous parameters provided. Expected either "
|
|
55
|
+
"`subject_visit` or `subject_identifier, report_datetime`. Not both."
|
|
56
|
+
)
|
|
57
|
+
self.report_datetime = subject_visit.report_datetime
|
|
58
|
+
self.subject_identifier = subject_visit.appointment.subject_identifier
|
|
59
|
+
else:
|
|
60
|
+
self.report_datetime = report_datetime
|
|
61
|
+
self.subject_identifier = subject_identifier
|
|
62
|
+
self.lte = lte
|
|
63
|
+
self.clinical_review_baseline_exists_or_raise()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def diagnosis_labels(self):
|
|
67
|
+
if self.single_condition_prefix:
|
|
68
|
+
return {
|
|
69
|
+
k.lower(): v
|
|
70
|
+
for k, v in get_diagnosis_labels().items()
|
|
71
|
+
if k == self.single_condition_prefix
|
|
72
|
+
}
|
|
73
|
+
return get_diagnosis_labels()
|
|
74
|
+
|
|
75
|
+
def get_dx_by_model(self, instance) -> str:
|
|
76
|
+
dx = None
|
|
77
|
+
for prefix in self.diagnosis_labels:
|
|
78
|
+
if instance.__class__.__name__.lower().startswith(prefix.lower()):
|
|
79
|
+
dx = self.get_dx(prefix)
|
|
80
|
+
break
|
|
81
|
+
if not dx:
|
|
82
|
+
raise DiagnosesError(
|
|
83
|
+
f"Invalid. No diagnoses detected. "
|
|
84
|
+
f"See responses on {self.clinical_review_baseline._meta.verbose_name}."
|
|
85
|
+
)
|
|
86
|
+
return dx
|
|
87
|
+
|
|
88
|
+
def get_dx_date(self, prefix: str) -> date | None:
|
|
89
|
+
"""Returns a dx date from the initial review for the condition.
|
|
90
|
+
|
|
91
|
+
Raises if initial review does not exist."""
|
|
92
|
+
prefix = prefix.lower()
|
|
93
|
+
if self.initial_reviews.get(prefix):
|
|
94
|
+
return self.initial_reviews.get(prefix).get_best_dx_date()
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def get_dx(self, prefix: str) -> str | None:
|
|
98
|
+
"""Returns YES if any diagnoses for this condition otherwise None.
|
|
99
|
+
|
|
100
|
+
References clinical_review_baseline, clinical_review
|
|
101
|
+
|
|
102
|
+
name is `dm`, `hiv` or `htn`.
|
|
103
|
+
"""
|
|
104
|
+
diagnoses = [
|
|
105
|
+
getattr(self.clinical_review_baseline, f"{prefix.lower()}_dx", "") == YES,
|
|
106
|
+
*[
|
|
107
|
+
(getattr(obj, f"{prefix.lower()}_dx", "") == YES)
|
|
108
|
+
for obj in self.clinical_reviews
|
|
109
|
+
],
|
|
110
|
+
]
|
|
111
|
+
if any(diagnoses):
|
|
112
|
+
return YES
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def clinical_review_baseline_exists_or_raise(self):
|
|
116
|
+
return self.clinical_review_baseline
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def clinical_review_baseline(self):
|
|
120
|
+
try:
|
|
121
|
+
obj = get_clinical_review_baseline_model_cls().objects.get(
|
|
122
|
+
subject_visit__subject_identifier=self.subject_identifier,
|
|
123
|
+
)
|
|
124
|
+
except ObjectDoesNotExist:
|
|
125
|
+
raise ClinicalReviewBaselineRequired(
|
|
126
|
+
"Please complete "
|
|
127
|
+
f"{get_clinical_review_baseline_model_cls()._meta.verbose_name}."
|
|
128
|
+
)
|
|
129
|
+
return obj
|
|
130
|
+
|
|
131
|
+
def report_datetime_opts(
|
|
132
|
+
self, prefix: str = None, lte: bool = None
|
|
133
|
+
) -> Dict[str, datetime]:
|
|
134
|
+
opts = {}
|
|
135
|
+
prefix = prefix.lower() or ""
|
|
136
|
+
if self.report_datetime:
|
|
137
|
+
if lte or self.lte:
|
|
138
|
+
opts.update({f"{prefix.lower()}report_datetime__lte": self.report_datetime})
|
|
139
|
+
else:
|
|
140
|
+
opts.update({f"{prefix.lower()}report_datetime__lt": self.report_datetime})
|
|
141
|
+
return opts
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def clinical_reviews(self):
|
|
145
|
+
return get_clinical_review_model_cls().objects.filter(
|
|
146
|
+
subject_visit__subject_identifier=self.subject_identifier,
|
|
147
|
+
**self.report_datetime_opts("subject_visit__"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_initial_reviews(self):
|
|
151
|
+
return self.initial_reviews
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def initial_reviews(self):
|
|
155
|
+
"""Returns a dict of initial review model instances
|
|
156
|
+
for each diagnosis.
|
|
157
|
+
|
|
158
|
+
If any initial review is expected but does not exist,
|
|
159
|
+
an expection is raised.
|
|
160
|
+
"""
|
|
161
|
+
initial_reviews = {}
|
|
162
|
+
|
|
163
|
+
options = []
|
|
164
|
+
for prefix, label in self.diagnosis_labels.items():
|
|
165
|
+
prefix = prefix.lower()
|
|
166
|
+
options.append(
|
|
167
|
+
(
|
|
168
|
+
prefix,
|
|
169
|
+
self.get_dx(prefix),
|
|
170
|
+
get_initial_review_model_cls(prefix),
|
|
171
|
+
f"{label.title()} diagnosis",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
for name, diagnosis, initial_review_model_cls, description in options:
|
|
175
|
+
if diagnosis:
|
|
176
|
+
extra_msg = description.title()
|
|
177
|
+
try:
|
|
178
|
+
obj = initial_review_model_cls.objects.get(
|
|
179
|
+
subject_visit__subject_identifier=self.subject_identifier,
|
|
180
|
+
**self.report_datetime_opts("subject_visit__", lte=True),
|
|
181
|
+
)
|
|
182
|
+
except ObjectDoesNotExist:
|
|
183
|
+
subject_visit = self.initial_diagnosis_visit(name)
|
|
184
|
+
if subject_visit:
|
|
185
|
+
visit_label = (
|
|
186
|
+
f"{subject_visit.visit_code}."
|
|
187
|
+
f"{subject_visit.visit_code_sequence}"
|
|
188
|
+
)
|
|
189
|
+
extra_msg = f"{description} was reported on visit {visit_label}. "
|
|
190
|
+
raise InitialReviewRequired(
|
|
191
|
+
f"{extra_msg}. Complete the "
|
|
192
|
+
f"`{initial_review_model_cls._meta.verbose_name}` CRF first."
|
|
193
|
+
)
|
|
194
|
+
except MultipleObjectsReturned:
|
|
195
|
+
qs = initial_review_model_cls.objects.filter(
|
|
196
|
+
subject_visit__subject_identifier=self.subject_identifier,
|
|
197
|
+
**self.report_datetime_opts("subject_visit__", lte=True),
|
|
198
|
+
).order_by(
|
|
199
|
+
"subject_visit__visit_code",
|
|
200
|
+
"subject_visit__visit_code_sequence",
|
|
201
|
+
)
|
|
202
|
+
visits_str = ", ".join(
|
|
203
|
+
[
|
|
204
|
+
(
|
|
205
|
+
f"{obj.subject_visit.visit_code}."
|
|
206
|
+
f"{obj.subject_visit.visit_code_sequence}"
|
|
207
|
+
)
|
|
208
|
+
for obj in qs
|
|
209
|
+
]
|
|
210
|
+
)
|
|
211
|
+
raise MultipleInitialReviewsExist(
|
|
212
|
+
f"More than one `{initial_review_model_cls._meta.verbose_name}` "
|
|
213
|
+
f"has been submitted. "
|
|
214
|
+
f"This needs to be corrected. Try removing all but the first "
|
|
215
|
+
f"`{initial_review_model_cls._meta.verbose_name}` "
|
|
216
|
+
"before continuing. "
|
|
217
|
+
f"`{initial_review_model_cls._meta.verbose_name}` "
|
|
218
|
+
"CRFs have been submitted "
|
|
219
|
+
f"for visits {visits_str}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
initial_reviews.update({name: obj})
|
|
224
|
+
return initial_reviews
|
|
225
|
+
|
|
226
|
+
def initial_diagnosis_visit(self, prefix):
|
|
227
|
+
related_visit_model_attr = (
|
|
228
|
+
get_clinical_review_baseline_model_cls().related_visit_model_attr()
|
|
229
|
+
)
|
|
230
|
+
opts = {
|
|
231
|
+
f"{related_visit_model_attr}__subject_identifier": self.subject_identifier,
|
|
232
|
+
f"{prefix.lower()}_dx": YES,
|
|
233
|
+
}
|
|
234
|
+
opts.update(**self.report_datetime_opts(f"{related_visit_model_attr}__", lte=True))
|
|
235
|
+
try:
|
|
236
|
+
clinical_review_baseline = get_clinical_review_baseline_model_cls().objects.get(
|
|
237
|
+
**opts
|
|
238
|
+
)
|
|
239
|
+
except ObjectDoesNotExist:
|
|
240
|
+
subject_visit = None
|
|
241
|
+
else:
|
|
242
|
+
subject_visit = clinical_review_baseline.related_visit
|
|
243
|
+
if not subject_visit:
|
|
244
|
+
try:
|
|
245
|
+
clinical_review = get_clinical_review_model_cls().objects.get(**opts)
|
|
246
|
+
except ObjectDoesNotExist:
|
|
247
|
+
subject_visit = None
|
|
248
|
+
else:
|
|
249
|
+
subject_visit = clinical_review.related_visit
|
|
250
|
+
return subject_visit
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from edc_constants.constants import YES
|
|
3
|
+
|
|
4
|
+
from ..diagnoses import (
|
|
5
|
+
ClinicalReviewBaselineRequired,
|
|
6
|
+
Diagnoses,
|
|
7
|
+
InitialReviewRequired,
|
|
8
|
+
MultipleInitialReviewsExist,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiagnosisFormValidatorMixin:
|
|
13
|
+
def get_diagnoses(self) -> Diagnoses:
|
|
14
|
+
try:
|
|
15
|
+
diagnoses = Diagnoses(
|
|
16
|
+
subject_identifier=self.subject_identifier,
|
|
17
|
+
report_datetime=self.report_datetime,
|
|
18
|
+
)
|
|
19
|
+
except ClinicalReviewBaselineRequired as e:
|
|
20
|
+
raise forms.ValidationError(e)
|
|
21
|
+
try:
|
|
22
|
+
diagnoses.get_initial_reviews()
|
|
23
|
+
except InitialReviewRequired as e:
|
|
24
|
+
raise forms.ValidationError(e)
|
|
25
|
+
except MultipleInitialReviewsExist as e:
|
|
26
|
+
raise forms.ValidationError(e)
|
|
27
|
+
return diagnoses
|
|
28
|
+
|
|
29
|
+
def applicable_if_not_diagnosed(
|
|
30
|
+
self, diagnoses=None, prefix=None, field_applicable=None, label=None
|
|
31
|
+
) -> bool:
|
|
32
|
+
diagnoses = diagnoses or self.get_diagnoses()
|
|
33
|
+
return self.applicable_if_true(
|
|
34
|
+
diagnoses.get_dx(prefix) != YES,
|
|
35
|
+
field_applicable=field_applicable,
|
|
36
|
+
applicable_msg=(
|
|
37
|
+
f"Patient was not previously diagnosed with {label}. Expected YES or NO."
|
|
38
|
+
),
|
|
39
|
+
not_applicable_msg=f"Patient was previously diagnosed with {label}.",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def applicable_if_diagnosed(
|
|
43
|
+
self, diagnoses=None, prefix=None, field_applicable=None, label=None
|
|
44
|
+
) -> bool:
|
|
45
|
+
diagnoses = diagnoses or self.get_diagnoses()
|
|
46
|
+
diagnosed = diagnoses.get_dx(prefix) == YES
|
|
47
|
+
return self.applicable_if_true(
|
|
48
|
+
diagnosed,
|
|
49
|
+
field_applicable=field_applicable,
|
|
50
|
+
applicable_msg=(
|
|
51
|
+
f"Patient was previously diagnosed with {label}. Expected YES or NO."
|
|
52
|
+
),
|
|
53
|
+
not_applicable_msg=f"Patient was not previously diagnosed with {label}.",
|
|
54
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
from django import forms
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from edc_dx_review.utils import raise_if_clinical_review_does_not_exist
|
|
8
|
+
from edc_form_validators import INVALID_ERROR, FormValidator
|
|
9
|
+
from edc_utils import convert_php_dateformat
|
|
10
|
+
from edc_visit_schedule.utils import raise_if_baseline
|
|
11
|
+
|
|
12
|
+
from ..diagnoses import ClinicalReviewBaselineRequired, Diagnoses, InitialReviewRequired
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResultFormValidatorMixin(FormValidator):
|
|
16
|
+
dx: Tuple[str, str] # e.g. (HIV, "HIV Infection")
|
|
17
|
+
|
|
18
|
+
def clean(self):
|
|
19
|
+
raise_if_baseline(self.cleaned_data.get("subject_visit"))
|
|
20
|
+
try:
|
|
21
|
+
raise_if_clinical_review_does_not_exist(self.cleaned_data.get("subject_visit"))
|
|
22
|
+
except ClinicalReviewBaselineRequired as e:
|
|
23
|
+
self.raise_validation_error(str(e), INVALID_ERROR)
|
|
24
|
+
try:
|
|
25
|
+
self.validate_drawn_date_by_dx_date(*self.dx)
|
|
26
|
+
except ClinicalReviewBaselineRequired as e:
|
|
27
|
+
self.raise_validation_error(str(e), INVALID_ERROR)
|
|
28
|
+
|
|
29
|
+
def validate_test_date_by_dx_date(
|
|
30
|
+
self, prefix: str, dx_msg_label: str, test_date_fld: str | None = None
|
|
31
|
+
) -> None:
|
|
32
|
+
return self.validate_drawn_date_by_dx_date(
|
|
33
|
+
prefix=prefix, dx_msg_label=dx_msg_label, drawn_date_fld=test_date_fld
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def validate_drawn_date_by_dx_date(
|
|
37
|
+
self, prefix: str, dx_msg_label: str, drawn_date_fld: str | None = None
|
|
38
|
+
):
|
|
39
|
+
drawn_date_fld = drawn_date_fld or "drawn_date"
|
|
40
|
+
dx = Diagnoses(
|
|
41
|
+
subject_visit=self.cleaned_data.get("subject_visit"),
|
|
42
|
+
lte=True,
|
|
43
|
+
limit_to_single_condition_prefix=prefix,
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
dx_date = dx.get_dx_date(prefix)
|
|
47
|
+
except InitialReviewRequired:
|
|
48
|
+
dx_date = None
|
|
49
|
+
if not dx_date:
|
|
50
|
+
raise forms.ValidationError(
|
|
51
|
+
f"A {dx_msg_label} diagnosis has not been reported for this subject."
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
if dx_date > self.cleaned_data.get(drawn_date_fld):
|
|
55
|
+
formatted_date = dx_date.strftime(
|
|
56
|
+
convert_php_dateformat(settings.SHORT_DATE_FORMAT)
|
|
57
|
+
)
|
|
58
|
+
raise forms.ValidationError(
|
|
59
|
+
{
|
|
60
|
+
"drawn_date": (
|
|
61
|
+
"Invalid. Subject was diagnosed with "
|
|
62
|
+
f"{dx_msg_label} on {formatted_date}."
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
)
|
edc_dx/utils.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DiagnosisLabelError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_diagnosis_labels() -> dict:
|
|
13
|
+
diagnosis_labels = getattr(
|
|
14
|
+
settings,
|
|
15
|
+
"EDC_DX_LABELS",
|
|
16
|
+
dict(hiv="HIV", dm="Diabetes", htn="Hypertension", chol="High Cholesterol"),
|
|
17
|
+
)
|
|
18
|
+
return {k.lower(): v for k, v in diagnosis_labels.items()}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_diagnosis_labels_prefixes() -> list[str]:
|
|
22
|
+
return [k for k in get_diagnosis_labels()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def raise_on_unknown_diagnosis_labels(obj: Any, fld_suffix: str, fld_value: Any) -> None:
|
|
26
|
+
"""Raises an exception if a diagnosis field has a response
|
|
27
|
+
but is not an expected condition.
|
|
28
|
+
|
|
29
|
+
See also EDC_DX_LABELS.
|
|
30
|
+
"""
|
|
31
|
+
labels = [
|
|
32
|
+
fld.name.split(fld_suffix)[0]
|
|
33
|
+
for fld in obj._meta.get_fields()
|
|
34
|
+
if fld.name.endswith(fld_suffix)
|
|
35
|
+
and fld.name.split(fld_suffix)[0] not in get_diagnosis_labels_prefixes()
|
|
36
|
+
and getattr(obj, fld.name) == fld_value
|
|
37
|
+
]
|
|
38
|
+
if labels:
|
|
39
|
+
raise DiagnosisLabelError(
|
|
40
|
+
"Diagnosis prefix not expected. See settings.EDC_DX_LABELS. "
|
|
41
|
+
f"Expected one of {get_diagnosis_labels_prefixes()}. Got {labels}."
|
|
42
|
+
)
|
|
File without changes
|
edc_dx_review/apps.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.apps import apps as django_apps
|
|
2
|
+
|
|
3
|
+
EDC_DX_REVIEW = "EDC_DX_REVIEW"
|
|
4
|
+
EDC_DX_REVIEW_SUPER = "EDC_DX_REVIEW_SUPER"
|
|
5
|
+
EDC_DX_REVIEW_VIEW = "EDC_DX_REVIEW_VIEW"
|
|
6
|
+
|
|
7
|
+
codenames = []
|
|
8
|
+
app_config = django_apps.get_app_config("edc_dx_review")
|
|
9
|
+
for model_cls in app_config.get_models():
|
|
10
|
+
if "historical" not in model_cls._meta.label_lower:
|
|
11
|
+
for action in ["view_", "add_", "change_", "delete_", "view_historical"]:
|
|
12
|
+
codenames.append(f".{action}".join(model_cls._meta.label_lower.split(".")))
|
|
13
|
+
codenames.sort()
|
edc_dx_review/auths.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from edc_auth.site_auths import site_auths
|
|
2
|
+
|
|
3
|
+
from .auth_objects import (
|
|
4
|
+
EDC_DX_REVIEW,
|
|
5
|
+
EDC_DX_REVIEW_SUPER,
|
|
6
|
+
EDC_DX_REVIEW_VIEW,
|
|
7
|
+
codenames,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
site_auths.add_group(*codenames, name=EDC_DX_REVIEW_VIEW, view_only=True)
|
|
11
|
+
site_auths.add_group(*codenames, name=EDC_DX_REVIEW, no_delete=True)
|
|
12
|
+
site_auths.add_group(*codenames, name=EDC_DX_REVIEW_SUPER)
|
edc_dx_review/choices.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from edc_constants.constants import NOT_APPLICABLE, OTHER
|
|
2
|
+
|
|
3
|
+
from .constants import DIET_LIFESTYLE, DRUGS, INSULIN, THIS_CLINIC
|
|
4
|
+
|
|
5
|
+
CARE_ACCESS = (
|
|
6
|
+
(THIS_CLINIC, "Patient comes to this facility for their care"),
|
|
7
|
+
(OTHER, "Patient goes to a different clinic"),
|
|
8
|
+
(NOT_APPLICABLE, "Not applicable"),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
CHOL_MANAGEMENT = (
|
|
12
|
+
(DRUGS, "Oral drugs"),
|
|
13
|
+
(DIET_LIFESTYLE, "Diet and lifestyle alone"),
|
|
14
|
+
)
|
|
15
|
+
DM_MANAGEMENT = (
|
|
16
|
+
(INSULIN, "Insulin injections"),
|
|
17
|
+
(DRUGS, "Oral drugs"),
|
|
18
|
+
(DIET_LIFESTYLE, "Diet and lifestyle alone"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
HTN_MANAGEMENT = (
|
|
22
|
+
(DRUGS, "Drugs / Medicine"),
|
|
23
|
+
(DIET_LIFESTYLE, "Diet and lifestyle alone"),
|
|
24
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from edc_dx import get_diagnosis_labels
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_clinical_review_baseline_cond_fieldset(
|
|
5
|
+
cond: str, title: str = None
|
|
6
|
+
) -> tuple[str, dict]:
|
|
7
|
+
if not title:
|
|
8
|
+
title = cond.upper()
|
|
9
|
+
return (
|
|
10
|
+
title,
|
|
11
|
+
{"fields": (f"{cond}_dx", f"{cond}_dx_at_screening")},
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_clinical_review_baseline_cond_fieldsets() -> tuple[tuple]:
|
|
16
|
+
fieldsets = ()
|
|
17
|
+
for prefix, label in get_diagnosis_labels().items():
|
|
18
|
+
fieldsets = fieldsets + (
|
|
19
|
+
get_clinical_review_baseline_cond_fieldset(cond=prefix.lower(), title=label),
|
|
20
|
+
)
|
|
21
|
+
return fieldsets
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_clinical_review_cond_fieldset(cond: str, title: str = None) -> tuple[str, dict]:
|
|
25
|
+
if not title:
|
|
26
|
+
title = cond.upper()
|
|
27
|
+
return (
|
|
28
|
+
title,
|
|
29
|
+
{
|
|
30
|
+
"fields": (
|
|
31
|
+
f"{cond}_test",
|
|
32
|
+
f"{cond}_test_date",
|
|
33
|
+
f"{cond}_reason",
|
|
34
|
+
f"{cond}_reason_other",
|
|
35
|
+
f"{cond}_dx",
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_clinical_review_cond_fieldsets() -> tuple[tuple]:
|
|
42
|
+
fieldsets = ()
|
|
43
|
+
for prefix, label in get_diagnosis_labels().items():
|
|
44
|
+
fieldsets = fieldsets + (
|
|
45
|
+
get_clinical_review_cond_fieldset(cond=prefix.lower(), title=label),
|
|
46
|
+
)
|
|
47
|
+
return fieldsets
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
3
|
+
from edc_model.utils import model_exists_or_raise
|
|
4
|
+
|
|
5
|
+
from ..utils import get_clinical_review_baseline_model_cls
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClinicalReviewBaselineRequiredModelFormMixin:
|
|
9
|
+
"""Asserts Baseline Clinical Review exists or raise"""
|
|
10
|
+
|
|
11
|
+
def clean(self) -> dict:
|
|
12
|
+
cleaned_data = super().clean()
|
|
13
|
+
model_cls = get_clinical_review_baseline_model_cls()
|
|
14
|
+
if self._meta.model != model_cls and cleaned_data.get("subject_visit"):
|
|
15
|
+
try:
|
|
16
|
+
model_exists_or_raise(
|
|
17
|
+
subject_visit=cleaned_data.get("subject_visit"),
|
|
18
|
+
model_cls=model_cls,
|
|
19
|
+
singleton=True,
|
|
20
|
+
)
|
|
21
|
+
except ObjectDoesNotExist:
|
|
22
|
+
raise forms.ValidationError(
|
|
23
|
+
f"Complete the `{model_cls._meta.verbose_name}` CRF first."
|
|
24
|
+
)
|
|
25
|
+
return cleaned_data
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from edc_constants.constants import OTHER, YES
|
|
2
|
+
from edc_dx import get_diagnosis_labels
|
|
3
|
+
from edc_dx.form_validators import DiagnosisFormValidatorMixin
|
|
4
|
+
from edc_visit_schedule.utils import raise_if_baseline
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ClinicalReviewFollowupFormValidatorMixin(DiagnosisFormValidatorMixin):
|
|
8
|
+
def _clean(self):
|
|
9
|
+
raise_if_baseline(self.cleaned_data.get("subject_visit"))
|
|
10
|
+
for prefix, label in get_diagnosis_labels().items():
|
|
11
|
+
cond = prefix.lower()
|
|
12
|
+
self.applicable_if_not_diagnosed(
|
|
13
|
+
prefix=cond,
|
|
14
|
+
field_applicable=f"{cond}_test",
|
|
15
|
+
label=label,
|
|
16
|
+
)
|
|
17
|
+
self.required_if(YES, field=f"{cond}_test", field_required=f"{cond}_test_date")
|
|
18
|
+
self.m2m_required_if(YES, field=f"{cond}_test", m2m_field=f"{cond}_reason")
|
|
19
|
+
self.m2m_other_specify(
|
|
20
|
+
OTHER,
|
|
21
|
+
m2m_field=f"{cond}_reason",
|
|
22
|
+
field_other=f"{cond}_reason_other",
|
|
23
|
+
)
|
|
24
|
+
self.applicable_if(YES, field=f"{cond}_test", field_applicable=f"{cond}_dx")
|
|
25
|
+
super()._clean()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from edc_constants.constants import OTHER, UNKNOWN
|
|
2
|
+
|
|
3
|
+
list_data = {
|
|
4
|
+
"edc_dx_review.diagnosislocations": [
|
|
5
|
+
("hospital", "Hospital"),
|
|
6
|
+
("gov_clinic", "Government clinic"),
|
|
7
|
+
("private_clinic", "Private clinic"),
|
|
8
|
+
("private_doctor", "Private doctor"),
|
|
9
|
+
("study_clinic", "Study clinic"),
|
|
10
|
+
(UNKNOWN, "Don't recall"),
|
|
11
|
+
(OTHER, "Other, specify"),
|
|
12
|
+
],
|
|
13
|
+
"edc_dx_review.reasonsfortesting": [
|
|
14
|
+
("patient_request", "Patient was well and made a request"),
|
|
15
|
+
("patient_complication", "Patient had a clinical complication"),
|
|
16
|
+
("signs_symptoms", "Patient had suggestive signs and symptoms"),
|
|
17
|
+
(OTHER, "Other reason (specify below)"),
|
|
18
|
+
],
|
|
19
|
+
}
|