clinicedc 2.0.12__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.

Files changed (101) hide show
  1. {clinicedc-2.0.12.dist-info → clinicedc-2.0.13.dist-info}/METADATA +2 -1
  2. {clinicedc-2.0.12.dist-info → clinicedc-2.0.13.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.13.dist-info}/WHEEL +0 -0
  101. {clinicedc-2.0.12.dist-info → clinicedc-2.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,32 @@
1
+ from django.db import models
2
+ from edc_constants.choices import YES_NO_NA
3
+ from edc_constants.constants import NOT_APPLICABLE
4
+
5
+ from .followup_review_model_mixin import FollowupReviewModelMixin
6
+
7
+
8
+ class HivFollowupReviewModelMixin(FollowupReviewModelMixin, models.Model):
9
+ dx = models.CharField(
10
+ verbose_name="Has the patient been infected with HIV?",
11
+ max_length=15,
12
+ choices=YES_NO_NA,
13
+ default=NOT_APPLICABLE,
14
+ )
15
+
16
+ arv_initiated = models.CharField(
17
+ verbose_name="Has the patient started antiretroviral therapy (ART)?",
18
+ max_length=15,
19
+ choices=YES_NO_NA,
20
+ default=NOT_APPLICABLE,
21
+ help_text="Select `not applicable` if previously reported.",
22
+ )
23
+ arv_initiation_actual_date = models.DateField(
24
+ verbose_name="Date started antiretroviral therapy (ART)",
25
+ null=True,
26
+ blank=True,
27
+ )
28
+
29
+ class Meta(FollowupReviewModelMixin.Meta):
30
+ abstract = True
31
+ verbose_name = "HIV Review"
32
+ verbose_name_plural = "HIV Review"
@@ -0,0 +1,6 @@
1
+ from .chol_initial_review_model_mixin import CholInitialReviewModelMixin
2
+ from .hiv_initial_model_mixins import (
3
+ HivArvInitiationModelMixin,
4
+ HivArvMonitoringModelMixin,
5
+ )
6
+ from .ncd_initial_review_model_mixin import NcdInitialReviewModelMixin
@@ -0,0 +1,34 @@
1
+ from django.db import models
2
+ from edc_constants.choices import YES_NO
3
+ from edc_constants.constants import NOT_APPLICABLE
4
+
5
+ from ...choices import CHOL_MANAGEMENT
6
+ from ..dx_location_model_mixin import DxLocationModelMixin
7
+ from .ncd_initial_review_model_mixin import NcdInitialReviewModelMixin
8
+
9
+
10
+ class CholInitialReviewModelMixin(
11
+ DxLocationModelMixin,
12
+ NcdInitialReviewModelMixin,
13
+ ):
14
+ ncd_condition_label = "cholesterol"
15
+
16
+ managed_by = models.CharField(
17
+ verbose_name="How is the patient's cholesterol managed?",
18
+ max_length=25,
19
+ choices=CHOL_MANAGEMENT,
20
+ default=NOT_APPLICABLE,
21
+ )
22
+
23
+ chol_performed = models.CharField(
24
+ verbose_name=(
25
+ "Has the patient had their cholesterol measured in the last few months?"
26
+ ),
27
+ max_length=15,
28
+ choices=YES_NO,
29
+ )
30
+
31
+ class Meta:
32
+ abstract = True
33
+ verbose_name = "High Cholesterol Initial Review"
34
+ verbose_name_plural = "High Cholesterol Initial Reviews"
@@ -0,0 +1,119 @@
1
+ from django.core.validators import MaxValueValidator, MinValueValidator
2
+ from django.db import models
3
+ from edc_constants.choices import YES_NO, YES_NO_NA, YES_NO_PENDING_NA
4
+ from edc_constants.constants import NOT_APPLICABLE, YES
5
+ from edc_lab.choices import VL_QUANTIFIER_NA
6
+ from edc_model import estimated_date_from_ago
7
+ from edc_model import models as edc_models
8
+ from edc_model.validators import date_not_future
9
+ from edc_reportable import CELLS_PER_MILLIMETER_CUBED_DISPLAY, COPIES_PER_MILLILITER
10
+
11
+
12
+ class HivArvInitiationModelMixin(models.Model):
13
+ arv_initiated = models.CharField(
14
+ verbose_name="Has the patient started antiretroviral therapy (ART)?",
15
+ max_length=15,
16
+ choices=YES_NO,
17
+ default=YES,
18
+ )
19
+
20
+ arv_initiation_ago = edc_models.DurationYMDField(
21
+ verbose_name="How long ago did the patient start ART?",
22
+ null=True,
23
+ blank=True,
24
+ )
25
+
26
+ arv_initiation_actual_date = models.DateField(
27
+ verbose_name="Date started antiretroviral therapy (ART)",
28
+ validators=[date_not_future],
29
+ null=True,
30
+ blank=True,
31
+ help_text="Calculated based on response to `arv_initiation_ago`",
32
+ )
33
+
34
+ arv_initiation_estimated_date = models.DateField(
35
+ verbose_name="Estimated Date started antiretroviral therapy (ART)",
36
+ validators={date_not_future},
37
+ null=True,
38
+ editable=False,
39
+ help_text="Calculated based on response to `arv_initiation_ago`",
40
+ )
41
+
42
+ arv_initiation_date_is_estimated = models.CharField(
43
+ verbose_name="Was the ART start date estimated?",
44
+ max_length=15,
45
+ choices=YES_NO,
46
+ default=YES,
47
+ editable=False,
48
+ )
49
+
50
+ def save(self, *args, **kwargs):
51
+ self.dx_estimated_date = estimated_date_from_ago(instance=self, ago_field="dx_ago")
52
+ self.arv_initiation_estimated_date = estimated_date_from_ago(
53
+ instance=self, ago_field="arv_initiation_ago"
54
+ )
55
+ super().save(*args, **kwargs)
56
+
57
+ @property
58
+ def best_art_initiation_date(self):
59
+ return self.arv_initiation_actual_date or self.arv_initiation_estimated_date
60
+
61
+ class Meta:
62
+ abstract = True
63
+
64
+
65
+ class HivArvMonitoringModelMixin(models.Model):
66
+ # Viral Load
67
+ has_vl = models.CharField(
68
+ verbose_name="Is the patient's most recent viral load result available?",
69
+ max_length=25,
70
+ choices=YES_NO_PENDING_NA,
71
+ default=NOT_APPLICABLE,
72
+ )
73
+ vl = models.IntegerField(
74
+ verbose_name="Most recent viral load",
75
+ validators=[MinValueValidator(0), MaxValueValidator(9999999)],
76
+ null=True,
77
+ blank=True,
78
+ help_text=COPIES_PER_MILLILITER,
79
+ )
80
+
81
+ vl_quantifier = models.CharField(
82
+ max_length=10,
83
+ choices=VL_QUANTIFIER_NA,
84
+ null=True,
85
+ blank=True,
86
+ )
87
+
88
+ vl_date = models.DateField(
89
+ verbose_name="Date of most recent viral load",
90
+ validators=[date_not_future],
91
+ null=True,
92
+ blank=True,
93
+ )
94
+
95
+ # CD4
96
+ has_cd4 = models.CharField(
97
+ verbose_name="Is the patient's most recent CD4 result available?",
98
+ max_length=25,
99
+ choices=YES_NO_NA,
100
+ default=NOT_APPLICABLE,
101
+ )
102
+
103
+ cd4 = models.IntegerField(
104
+ verbose_name="Most recent CD4",
105
+ validators=[MinValueValidator(0), MaxValueValidator(3000)],
106
+ null=True,
107
+ blank=True,
108
+ help_text=CELLS_PER_MILLIMETER_CUBED_DISPLAY,
109
+ )
110
+
111
+ cd4_date = models.DateField(
112
+ verbose_name="Date of most recent CD4",
113
+ validators=[date_not_future],
114
+ null=True,
115
+ blank=True,
116
+ )
117
+
118
+ class Meta:
119
+ abstract = True
@@ -0,0 +1,42 @@
1
+ from django.db import models
2
+ from edc_constants.choices import YES_NO
3
+ from edc_constants.constants import YES
4
+ from edc_model import models as edc_models
5
+ from edc_model.utils import duration_to_date
6
+
7
+
8
+ class NcdInitialReviewModelMixin(models.Model):
9
+ ncd_condition_label: str = None
10
+
11
+ med_start_ago = edc_models.DurationYMDField(
12
+ verbose_name=(
13
+ "If the patient is taking medicines for ncd_condition_label, "
14
+ "how long have they been taking these?"
15
+ ),
16
+ null=True,
17
+ blank=True,
18
+ )
19
+
20
+ med_start_estimated_date = models.DateField(
21
+ verbose_name="Estimated medication start date",
22
+ null=True,
23
+ editable=False,
24
+ )
25
+
26
+ med_start_date_is_estimated = models.CharField(
27
+ verbose_name="Was the medication start date estimated?",
28
+ max_length=15,
29
+ choices=YES_NO,
30
+ default=YES,
31
+ editable=False,
32
+ )
33
+
34
+ def save(self, *args, **kwargs):
35
+ if self.med_start_ago:
36
+ self.med_start_estimated_date = duration_to_date(
37
+ self.med_start_ago, self.report_datetime
38
+ )
39
+ super().save(*args, **kwargs)
40
+
41
+ class Meta:
42
+ abstract = True
@@ -0,0 +1,20 @@
1
+ from edc_list_data.model_mixins import ListModelManager, ListModelMixin
2
+ from edc_model.models import HistoricalRecords
3
+
4
+
5
+ class ReasonsForTesting(ListModelMixin):
6
+ objects = ListModelManager()
7
+ history = HistoricalRecords()
8
+
9
+ class Meta(ListModelMixin.Meta):
10
+ verbose_name = "Reasons for Testing"
11
+ verbose_name_plural = "Reasons for Testing"
12
+
13
+
14
+ class DiagnosisLocations(ListModelMixin):
15
+ objects = ListModelManager()
16
+ history = HistoricalRecords()
17
+
18
+ class Meta(ListModelMixin.Meta):
19
+ verbose_name = "Diagnosis Locations"
20
+ verbose_name_plural = "Diagnosis Locations"
@@ -0,0 +1,30 @@
1
+ from typing import Dict
2
+
3
+ from django.contrib import admin
4
+ from edc_dx import get_diagnosis_labels_prefixes
5
+
6
+
7
+ def get_clinical_review_cond_radio_fields() -> Dict[str, int]:
8
+ radio_fields = {}
9
+ for prefix in get_diagnosis_labels_prefixes():
10
+ cond = prefix.lower()
11
+ radio_fields.update(
12
+ {
13
+ f"{cond}_dx": admin.VERTICAL,
14
+ f"{cond}_test": admin.VERTICAL,
15
+ }
16
+ )
17
+ return radio_fields
18
+
19
+
20
+ def get_clinical_review_baseline_cond_radio_fields() -> Dict[str, int]:
21
+ radio_fields = {}
22
+ for prefix in get_diagnosis_labels_prefixes():
23
+ cond = prefix.lower()
24
+ radio_fields.update(
25
+ {
26
+ f"{cond}_dx": admin.VERTICAL,
27
+ f"{cond}_dx_at_screening": admin.VERTICAL,
28
+ }
29
+ )
30
+ return radio_fields
edc_dx_review/utils.py ADDED
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime
4
+ from typing import TYPE_CHECKING
5
+
6
+ from django import forms
7
+ from django.apps import apps as django_apps
8
+ from django.conf import settings
9
+ from django.core.exceptions import ObjectDoesNotExist
10
+ from edc_constants.constants import HIV, YES
11
+ from edc_model.utils import model_exists_or_raise
12
+ from edc_visit_schedule.baseline import VisitScheduleBaselineError
13
+ from edc_visit_schedule.utils import is_baseline
14
+
15
+ if TYPE_CHECKING:
16
+ from edc_model.models import BaseUuidModel
17
+ from edc_visit_tracking.model_mixins import VisitTrackingCrfModelMixin
18
+
19
+ class CrfLikeModel(VisitTrackingCrfModelMixin, BaseUuidModel):
20
+ pass
21
+
22
+
23
+ EDC_DX_REVIEW_APP_LABEL = getattr(settings, "EDC_DX_REVIEW_APP_LABEL", "edc_dx_review")
24
+
25
+
26
+ class ModelNotDefined(Exception):
27
+ pass
28
+
29
+
30
+ class BaselineModelError(Exception):
31
+ pass
32
+
33
+
34
+ def get_list_model_app():
35
+ return getattr(
36
+ settings, "EDC_DX_REVIEW_LIST_MODEL_APP_LABEL", settings.LIST_MODEL_APP_LABEL
37
+ )
38
+
39
+
40
+ def get_clinical_review_baseline_model_cls():
41
+ clinicalreviewbaseline = get_extra_attrs().get(
42
+ "clinicalreviewbaseline", "clinicalreviewbaseline"
43
+ )
44
+
45
+ return django_apps.get_model(f"{EDC_DX_REVIEW_APP_LABEL}.{clinicalreviewbaseline}")
46
+
47
+
48
+ def get_clinical_review_model_cls():
49
+ clinicalreview = get_extra_attrs().get("clinicalreview", "clinicalreview")
50
+ return django_apps.get_model(f"{EDC_DX_REVIEW_APP_LABEL}.{clinicalreview}")
51
+
52
+
53
+ def get_medication_model_cls():
54
+ medications = get_extra_attrs().get("medications", "medications")
55
+ return django_apps.get_model(f"{EDC_DX_REVIEW_APP_LABEL}.{medications}")
56
+
57
+
58
+ def get_initial_review_model_cls(prefix):
59
+ initialreview = get_extra_attrs().get("initialreview", "initialreview")
60
+ return django_apps.get_model(f"{EDC_DX_REVIEW_APP_LABEL}.{prefix.lower()}{initialreview}")
61
+
62
+
63
+ def get_review_model_cls(prefix):
64
+ review = get_extra_attrs().get("review", "review")
65
+ return django_apps.get_model(f"{EDC_DX_REVIEW_APP_LABEL}.{prefix.lower()}{review}")
66
+
67
+
68
+ def get_extra_attrs():
69
+ """Settings from EDC_DX_REVIEW_EXTRA_ATTRS.
70
+
71
+ See model name suffixes used in model_cls getters in utils.py.
72
+ """
73
+ extra_attrs = {
74
+ "clinicalreview": "clinicalreview",
75
+ "clinicalreviewbaseline": "clinicalreviewbaseline",
76
+ "initialreview": "initialreview",
77
+ "medications": "medications",
78
+ "review": "review",
79
+ }
80
+ try:
81
+ data = getattr(settings, "EDC_DX_REVIEW_EXTRA_ATTRS")
82
+ except AttributeError:
83
+ pass
84
+ else:
85
+ extra_attrs.update(data)
86
+ return extra_attrs
87
+
88
+
89
+ def raise_if_clinical_review_does_not_exist(subject_visit) -> CrfLikeModel:
90
+ try:
91
+ baseline = is_baseline(instance=subject_visit)
92
+ except VisitScheduleBaselineError as e:
93
+ raise forms.ValidationError(str(e))
94
+ else:
95
+ if baseline:
96
+ model_cls = get_clinical_review_baseline_model_cls()
97
+ else:
98
+ model_cls = get_clinical_review_model_cls()
99
+ try:
100
+ obj = model_exists_or_raise(subject_visit=subject_visit, model_cls=model_cls)
101
+ except ObjectDoesNotExist:
102
+ raise forms.ValidationError(f"Complete {model_cls._meta.verbose_name} CRF first.")
103
+ return obj
104
+
105
+
106
+ def raise_if_both_ago_and_actual_date(
107
+ cleaned_data: dict | None = None,
108
+ date_fld: str | None = None,
109
+ ago_fld: str | None = None,
110
+ dx_ago: str | None = None,
111
+ dx_date: date | None = None,
112
+ ) -> None:
113
+ if cleaned_data:
114
+ dx_ago = cleaned_data.get(date_fld)
115
+ dx_date = cleaned_data.get(ago_fld)
116
+ if dx_ago and dx_date:
117
+ raise forms.ValidationError(
118
+ {
119
+ ago_fld: (
120
+ "Date conflict. Do not provide a response "
121
+ "here if the date of diagnosis is available."
122
+ )
123
+ }
124
+ )
125
+
126
+
127
+ def requires_clinical_review_at_baseline(subject_visit):
128
+ try:
129
+ get_clinical_review_baseline_model_cls().objects.get(
130
+ subject_visit__subject_identifier=subject_visit.subject_identifier
131
+ )
132
+ except ObjectDoesNotExist:
133
+ raise forms.ValidationError(
134
+ "Please complete the "
135
+ f"{get_clinical_review_baseline_model_cls()._meta.verbose_name} first."
136
+ )
137
+
138
+
139
+ def raise_if_initial_review_does_not_exist(subject_visit, prefix):
140
+ model_exists_or_raise(
141
+ subject_visit=subject_visit,
142
+ model_cls=get_initial_review_model_cls(prefix),
143
+ )
144
+
145
+
146
+ def raise_if_review_does_not_exist(subject_visit, prefix):
147
+ model_exists_or_raise(
148
+ subject_visit=subject_visit,
149
+ model_cls=get_review_model_cls(prefix),
150
+ )
151
+
152
+
153
+ def medications_exists_or_raise(subject_visit) -> bool:
154
+ if subject_visit:
155
+ try:
156
+ get_medication_model_cls().objects.get(subject_visit=subject_visit)
157
+ except ObjectDoesNotExist:
158
+ raise forms.ValidationError(
159
+ f"Complete the `{get_medication_model_cls()._meta.verbose_name}` CRF first."
160
+ )
161
+ return True
162
+
163
+
164
+ def is_rx_initiated(
165
+ subject_identifier: str, report_datetime: datetime, instance_id: str | None = None
166
+ ) -> bool:
167
+ """Return True if already initiated"""
168
+ try:
169
+ get_initial_review_model_cls(HIV).objects.get(
170
+ subject_visit__subject_identifier=subject_identifier,
171
+ report_datetime__lte=report_datetime,
172
+ rx_init=YES,
173
+ )
174
+ except ObjectDoesNotExist:
175
+ exclude = {}
176
+ if instance_id:
177
+ exclude = {"id": instance_id}
178
+ rx_initiated = (
179
+ get_review_model_cls(HIV)
180
+ .objects.filter(
181
+ subject_visit__subject_identifier=subject_identifier,
182
+ report_datetime__lte=report_datetime,
183
+ rx_init=YES,
184
+ )
185
+ .exclude(**exclude)
186
+ .exists()
187
+ )
188
+ else:
189
+ rx_initiated = True
190
+ return rx_initiated
191
+
192
+
193
+ def art_initiation_date(subject_identifier: str, report_datetime: datetime) -> date:
194
+ """Returns date initiated on ART or None by querying
195
+ the HIV Initial Review and then the HIV Review.
196
+ """
197
+ art_date = None
198
+ try:
199
+ initial_review = get_initial_review_model_cls(HIV).objects.get(
200
+ subject_visit__subject_identifier=subject_identifier,
201
+ report_datetime__lte=report_datetime,
202
+ )
203
+ except ObjectDoesNotExist:
204
+ pass
205
+ else:
206
+ if initial_review.arv_initiated == YES:
207
+ art_date = initial_review.best_art_initiation_date
208
+ else:
209
+ for review in (
210
+ get_review_model_cls(HIV)
211
+ .objects.filter(
212
+ subject_visit__subject_identifier=subject_identifier,
213
+ report_datetime__lte=report_datetime,
214
+ )
215
+ .order_by("-report_datetime")
216
+ ):
217
+ if review.arv_initiated == YES:
218
+ art_date = review.arv_initiation_actual_date
219
+ break
220
+ return art_date
@@ -2,7 +2,7 @@
2
2
 
3
3
  import django_audit_fields.fields.hostname_modification_field
4
4
  import django_audit_fields.fields.userfield
5
- import django_audit_fields.models.audit_model_mixin
5
+ import django.utils.timezone
6
6
  from django.db import migrations, models
7
7
  import django.utils.timezone
8
8
 
@@ -4,7 +4,7 @@ import _socket
4
4
  import django_audit_fields.fields.hostname_modification_field
5
5
  import django_audit_fields.fields.userfield
6
6
  import django_audit_fields.fields.uuid_auto_field
7
- import django_audit_fields.models.audit_model_mixin
7
+ import django.utils.timezone
8
8
  import django_revision.revision_field
9
9
  from django.db import migrations, models
10
10
  import django.utils.timezone
@@ -8,7 +8,7 @@ import django.db.models.deletion
8
8
  import django_audit_fields.fields.hostname_modification_field
9
9
  import django_audit_fields.fields.userfield
10
10
  import django_audit_fields.fields.uuid_auto_field
11
- import django_audit_fields.models.audit_model_mixin
11
+ import django.utils.timezone
12
12
  import django_revision.revision_field
13
13
  import simple_history.models
14
14
  from django.conf import settings
@@ -66,6 +66,7 @@ class BloodPressureModelMixin(models.Model):
66
66
  choices=YES_NO,
67
67
  help_text="Based on the above readings. Severe HTN is any BP reading > 180/110mmHg",
68
68
  blank=True,
69
+ default="",
69
70
  )
70
71
 
71
72
  class Meta: