clinicedc 2.0.12__py3-none-any.whl → 2.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of clinicedc might be problematic. Click here for more details.

Files changed (101) hide show
  1. {clinicedc-2.0.12.dist-info → clinicedc-2.0.14.dist-info}/METADATA +2 -2
  2. {clinicedc-2.0.12.dist-info → clinicedc-2.0.14.dist-info}/RECORD +101 -24
  3. edc_action_item/migrations/0017_auto_20190305_0123.py +1 -1
  4. edc_action_item/migrations/0030_edcpermissions.py +1 -1
  5. edc_adverse_event/migrations/0001_initial.py +1 -1
  6. edc_adverse_event/migrations/0002_auto_20190802_0059.py +1 -1
  7. edc_adverse_event/migrations/0008_auto_20220825_0451.py +1 -1
  8. edc_adverse_event/migrations/0009_auto_20220907_0157.py +1 -1
  9. edc_adverse_event/model_mixins/hospitaization/hospitalization_model_mixin.py +1 -3
  10. edc_analytics/__init__.py +3 -0
  11. edc_analytics/apps.py +8 -0
  12. edc_analytics/constants.py +26 -0
  13. edc_analytics/custom_tables/__init__.py +11 -0
  14. edc_analytics/custom_tables/age.py +72 -0
  15. edc_analytics/custom_tables/art.py +88 -0
  16. edc_analytics/custom_tables/bmi.py +125 -0
  17. edc_analytics/custom_tables/bp.py +103 -0
  18. edc_analytics/custom_tables/fasting.py +126 -0
  19. edc_analytics/custom_tables/fbg.py +98 -0
  20. edc_analytics/custom_tables/fbg_ogtt.py +384 -0
  21. edc_analytics/custom_tables/gender.py +12 -0
  22. edc_analytics/custom_tables/hba1c.py +87 -0
  23. edc_analytics/custom_tables/ogtt.py +95 -0
  24. edc_analytics/custom_tables/waist.py +105 -0
  25. edc_analytics/data.py +36 -0
  26. edc_analytics/row/__init__.py +4 -0
  27. edc_analytics/row/row_definition.py +43 -0
  28. edc_analytics/row/row_definitions.py +32 -0
  29. edc_analytics/row/row_statistics.py +88 -0
  30. edc_analytics/row/row_statistics_with_gender.py +115 -0
  31. edc_analytics/stata/__init__.py +1 -0
  32. edc_analytics/stata/get_stata_labels_from_model.py +44 -0
  33. edc_analytics/styler.py +93 -0
  34. edc_analytics/table.py +108 -0
  35. edc_analytics/urls.py +6 -0
  36. edc_appointment/migrations/0018_auto_20190305_0123.py +1 -1
  37. edc_auth/migrations/0001_squashed_0033_alter_userprofile_is_multisite_viewer.py +1 -1
  38. edc_auth/migrations/0012_auto_20191026_0034.py +1 -1
  39. edc_auth/migrations/0013_auto_20191026_0055.py +1 -1
  40. edc_auth/migrations/0025_permissions.py +1 -1
  41. edc_consent/migrations/0001_initial.py +1 -1
  42. edc_dashboard/migrations/0001_initial.py +1 -1
  43. edc_data_manager/migrations/0001_initial.py +1 -1
  44. edc_data_manager/migrations/0025_edcpermissions.py +1 -1
  45. edc_dx/__init__.py +6 -0
  46. edc_dx/apps.py +5 -0
  47. edc_dx/diagnoses.py +250 -0
  48. edc_dx/form_validators/__init__.py +2 -0
  49. edc_dx/form_validators/diagnosis_form_validator_mixin.py +54 -0
  50. edc_dx/form_validators/result_form_validator_mixin.py +65 -0
  51. edc_dx/utils.py +42 -0
  52. edc_dx_review/__init__.py +0 -0
  53. edc_dx_review/apps.py +5 -0
  54. edc_dx_review/auth_objects.py +13 -0
  55. edc_dx_review/auths.py +12 -0
  56. edc_dx_review/choices.py +24 -0
  57. edc_dx_review/constants.py +7 -0
  58. edc_dx_review/fieldsets.py +47 -0
  59. edc_dx_review/form_mixins/__init__.py +3 -0
  60. edc_dx_review/form_mixins/clinical_review_baseline_required_form_mixin.py +25 -0
  61. edc_dx_review/form_validator_mixins/__init__.py +6 -0
  62. edc_dx_review/form_validator_mixins/clinical_review_baseline_form_validator_mixin.py +7 -0
  63. edc_dx_review/form_validator_mixins/clinical_review_followup_form_validator_mixin.py +25 -0
  64. edc_dx_review/list_data.py +19 -0
  65. edc_dx_review/medical_date.py +195 -0
  66. edc_dx_review/migrations/0001_initial.py +307 -0
  67. edc_dx_review/migrations/0002_diagnosislocations_extra_value_and_more.py +32 -0
  68. edc_dx_review/migrations/0003_alter_diagnosislocations_options_and_more.py +148 -0
  69. edc_dx_review/migrations/0004_remove_diagnosislocations_edc_dx_revi_name_a39b40_idx_and_more.py +20 -0
  70. edc_dx_review/migrations/__init__.py +0 -0
  71. edc_dx_review/model_mixins/__init__.py +20 -0
  72. edc_dx_review/model_mixins/clinical_review_baseline_model_mixin.py +25 -0
  73. edc_dx_review/model_mixins/clinical_review_followup/__init__.py +5 -0
  74. edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_chol_model_mixin.py +54 -0
  75. edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_dm_model_mixin.py +54 -0
  76. edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_hiv_model_mixin.py +54 -0
  77. edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_htn_model_mixin.py +56 -0
  78. edc_dx_review/model_mixins/clinical_review_followup/clinical_review_followup_model_mixin.py +25 -0
  79. edc_dx_review/model_mixins/dx_location_model_mixin.py +17 -0
  80. edc_dx_review/model_mixins/factory/__init__.py +4 -0
  81. edc_dx_review/model_mixins/factory/baseline_review_model_mixin_factory.py +55 -0
  82. edc_dx_review/model_mixins/factory/calculate_date.py +43 -0
  83. edc_dx_review/model_mixins/factory/dx_initial_review_model_mixin_factory.py +97 -0
  84. edc_dx_review/model_mixins/factory/followup_review_model_mixin_factory.py +39 -0
  85. edc_dx_review/model_mixins/factory/rx_initial_review_model_mixin_factory.py +69 -0
  86. edc_dx_review/model_mixins/followup_review/__init__.py +2 -0
  87. edc_dx_review/model_mixins/followup_review/followup_review_model_mixin.py +22 -0
  88. edc_dx_review/model_mixins/followup_review/hiv_followup_review_model_mixin.py +32 -0
  89. edc_dx_review/model_mixins/initial_review/__init__.py +6 -0
  90. edc_dx_review/model_mixins/initial_review/chol_initial_review_model_mixin.py +34 -0
  91. edc_dx_review/model_mixins/initial_review/hiv_initial_model_mixins.py +119 -0
  92. edc_dx_review/model_mixins/initial_review/ncd_initial_review_model_mixin.py +42 -0
  93. edc_dx_review/models.py +20 -0
  94. edc_dx_review/radio_fields.py +30 -0
  95. edc_dx_review/utils.py +220 -0
  96. edc_export/migrations/0004_auto_20190305_0123.py +1 -1
  97. edc_export/migrations/0013_edcpermissions.py +1 -1
  98. edc_facility/migrations/0005_healthfacility_healthfacilitytypes_and_more.py +1 -1
  99. edc_vitals/model_mixins/blood_pressure_model_mixin.py +1 -0
  100. {clinicedc-2.0.12.dist-info → clinicedc-2.0.14.dist-info}/WHEEL +0 -0
  101. {clinicedc-2.0.12.dist-info → clinicedc-2.0.14.dist-info}/licenses/LICENSE +0 -0
edc_dx/apps.py ADDED
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig as DjangoAppConfig
2
+
3
+
4
+ class AppConfig(DjangoAppConfig):
5
+ name = "edc_dx"
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,2 @@
1
+ from .diagnosis_form_validator_mixin import DiagnosisFormValidatorMixin
2
+ from .result_form_validator_mixin import ResultFormValidatorMixin
@@ -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,5 @@
1
+ from django.apps import AppConfig as DjangoAppConfig
2
+
3
+
4
+ class AppConfig(DjangoAppConfig):
5
+ name = "edc_dx_review"
@@ -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)
@@ -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,7 @@
1
+ INSULIN = "insulin"
2
+ DIET_LIFESTYLE = "diet_lifestyle"
3
+ DRUGS = "drugs"
4
+ DIABETES_CLINIC = "diabetes_clinic"
5
+ HIV_CLINIC = "hiv_clinic"
6
+ HYPERTENSION_CLINIC = "hypertension_clinic"
7
+ THIS_CLINIC = "this_clinic"
@@ -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,3 @@
1
+ from .clinical_review_baseline_required_form_mixin import (
2
+ ClinicalReviewBaselineRequiredModelFormMixin,
3
+ )
@@ -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,6 @@
1
+ from .clinical_review_baseline_form_validator_mixin import (
2
+ ClinicalReviewBaselineFormValidatorMixin,
3
+ )
4
+ from .clinical_review_followup_form_validator_mixin import (
5
+ ClinicalReviewFollowupFormValidatorMixin,
6
+ )
@@ -0,0 +1,7 @@
1
+ from edc_visit_schedule.utils import raise_if_not_baseline
2
+
3
+
4
+ class ClinicalReviewBaselineFormValidatorMixin:
5
+ def _clean(self):
6
+ raise_if_not_baseline(self.cleaned_data.get("subject_visit"))
7
+ super()._clean()
@@ -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
+ }