Skip to content

Commit 096e2e6

Browse files
committed
added latest episode selection criteria and restructured query builder with headings
Includes all recall and episode-based filters, null-safe handling, and internal sectioning for maintainability.
1 parent 01622c9 commit 096e2e6

File tree

3 files changed

+252
-30
lines changed

3 files changed

+252
-30
lines changed

utils/oracle/mock_selection_builder.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,35 +35,33 @@ def dump_sql(self):
3535
# Replace this with the one you want to test,
3636
# then use utils/oracle/test_subject_criteria_dev.py to run your scenarios
3737

38-
def _add_criteria_latest_episode_sub_type(self) -> None:
38+
def _add_criteria_latest_episode_recall_surveillance_type(self) -> None:
3939
"""
40-
Adds a SQL condition that filters based on the episode_subtype_id of a subject's latest episode.
41-
42-
Translates a human-readable episode sub-type string into an internal numeric ID.
40+
Adds a filter for recall_polyp_surv_type_id based on the type of surveillance used during recall.
41+
Supports mapped descriptions and null values.
4342
"""
4443
try:
4544
value = self.criteria_value.lower()
46-
comparator = self.criteria_comparator
4745

48-
# Simulated EpisodeSubType enum mapping
49-
episode_subtype_map = {
50-
"routine screening": 10,
51-
"urgent referral": 11,
52-
"pre-assessment": 12,
53-
"follow-up": 13,
54-
"surveillance": 14,
55-
# Add more mappings as needed
46+
recall_surv_type_map = {
47+
"routine": 500,
48+
"enhanced": 501,
49+
"annual": 502,
50+
"null": None,
5651
}
5752

58-
if value not in episode_subtype_map:
59-
raise ValueError(f"Unknown episode sub-type: {value}")
53+
if value not in recall_surv_type_map:
54+
raise ValueError(f"Unknown recall surveillance type: {value}")
6055

61-
episode_subtype_id = episode_subtype_map[value]
56+
surv_id = recall_surv_type_map[value]
6257

63-
# Add SQL condition using the mapped ID
64-
self.sql_where.append(
65-
f"AND ep.episode_subtype_id {comparator} {episode_subtype_id}"
66-
)
58+
if surv_id is None:
59+
self.sql_where.append("AND ep.recall_polyp_surv_type_id IS NULL")
60+
else:
61+
comparator = self.criteria_comparator
62+
self.sql_where.append(
63+
f"AND ep.recall_polyp_surv_type_id {comparator} {surv_id}"
64+
)
6765

6866
except Exception:
6967
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)

utils/oracle/subject_selection_query_builder.py

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ def _add_variable_selection_criteria(
158158
self._check_if_not_modifier_is_valid_for_criteria_key()
159159

160160
match self.criteria_key:
161+
# ------------------------------------------------------------------------
162+
# 👤 Demographics & Subject Identity Criteria
163+
# ------------------------------------------------------------------------
161164
case SubjectSelectionCriteriaKey.NHS_NUMBER:
162165
self.criteria_value.replace(" ", "")
163166
self._add_criteria_nhs_number()
@@ -168,6 +171,9 @@ def _add_variable_selection_criteria(
168171
self._add_criteria_subject_age()
169172
case SubjectSelectionCriteriaKey.SUBJECT_HUB_CODE:
170173
self._add_criteria_subject_hub_code(user)
174+
# ------------------------------------------------------------------------
175+
# 🏥 Screening Centre & GP Linkage Criteria
176+
# ------------------------------------------------------------------------
171177
case (
172178
SubjectSelectionCriteriaKey.RESPONSIBLE_SCREENING_CENTRE_CODE
173179
):
@@ -178,6 +184,9 @@ def _add_variable_selection_criteria(
178184
SubjectSelectionCriteriaKey.HAS_GP_PRACTICE_ASSOCIATED_WITH_SCREENING_CENTRE_CODE
179185
):
180186
self._add_criteria_has_gp_practice_linked_to_sc()
187+
# ------------------------------------------------------------------------
188+
# 🩺 Screening Status & Change History Criteria
189+
# ------------------------------------------------------------------------
181190
case SubjectSelectionCriteriaKey.SCREENING_STATUS:
182191
self._add_criteria_screening_status(subject)
183192
case SubjectSelectionCriteriaKey.PREVIOUS_SCREENING_STATUS:
@@ -190,6 +199,9 @@ def _add_variable_selection_criteria(
190199
self._add_criteria_date_field(
191200
subject, "ALL_PATHWAYS", "SCREENING_STATUS_CHANGE_DATE"
192201
)
202+
# ------------------------------------------------------------------------
203+
# ⏰ Due Dates: Screening, Surveillance & Lynch Pathways
204+
# ------------------------------------------------------------------------
193205
case SubjectSelectionCriteriaKey.PREVIOUS_LYNCH_DUE_DATE:
194206
self._add_criteria_date_field(
195207
subject, "LYNCH", "PREVIOUS_DUE_DATE"
@@ -246,6 +258,9 @@ def _add_variable_selection_criteria(
246258
)
247259
case SubjectSelectionCriteriaKey.BOWEL_SCOPE_DUE_DATE_REASON:
248260
self._add_criteria_bowel_scope_due_date_reason()
261+
# ------------------------------------------------------------------------
262+
# ⛔ Cease & Manual Override Criteria
263+
# ------------------------------------------------------------------------
249264
case SubjectSelectionCriteriaKey.MANUAL_CEASE_REQUESTED:
250265
self._add_criteria_manual_cease_requested()
251266
case SubjectSelectionCriteriaKey.CEASED_CONFIRMATION_DATE:
@@ -258,6 +273,9 @@ def _add_variable_selection_criteria(
258273
self._add_criteria_ceased_confirmation_user_id(user)
259274
case SubjectSelectionCriteriaKey.CLINICAL_REASON_FOR_CEASE:
260275
self._add_criteria_clinical_reason_for_cease()
276+
# ------------------------------------------------------------------------
277+
# 📦 Event Status & System Update Flags
278+
# ------------------------------------------------------------------------
261279
case (
262280
SubjectSelectionCriteriaKey.SUBJECT_HAS_EVENT_STATUS
263281
| SubjectSelectionCriteriaKey.SUBJECT_DOES_NOT_HAVE_EVENT_STATUS
@@ -269,6 +287,9 @@ def _add_variable_selection_criteria(
269287
self._add_criteria_has_unprocessed_sspi_updates()
270288
case SubjectSelectionCriteriaKey.SUBJECT_HAS_USER_DOB_UPDATES:
271289
self._add_criteria_has_user_dob_update()
290+
# ------------------------------------------------------------------------
291+
# 📁 Subject Has Episode & Age-Based Criteria
292+
# ------------------------------------------------------------------------
272293
case (
273294
SubjectSelectionCriteriaKey.SUBJECT_HAS_EPISODES
274295
| SubjectSelectionCriteriaKey.SUBJECT_HAS_AN_OPEN_EPISODE
@@ -280,12 +301,16 @@ def _add_variable_selection_criteria(
280301
self._add_criteria_subject_lower_fobt_age()
281302
case SubjectSelectionCriteriaKey.SUBJECT_LOWER_LYNCH_AGE:
282303
self._add_criteria_subject_lower_lynch_age()
304+
# ------------------------------------------------------------------------
305+
# 🧱 Latest Episode Attributes
306+
# ------------------------------------------------------------------------
283307
case SubjectSelectionCriteriaKey.LATEST_EPISODE_TYPE:
284308
self._add_criteria_latest_episode_type()
285309
case SubjectSelectionCriteriaKey.LATEST_EPISODE_SUB_TYPE:
286310
self._add_criteria_latest_episode_sub_type()
287311
case SubjectSelectionCriteriaKey.LATEST_EPISODE_STATUS:
288312
self._add_criteria_latest_episode_status()
313+
289314
case SubjectSelectionCriteriaKey.LATEST_EPISODE_STATUS_REASON:
290315
self._add_criteria_latest_episode_status_reason()
291316
case (
@@ -300,6 +325,10 @@ def _add_variable_selection_criteria(
300325
SubjectSelectionCriteriaKey.LATEST_EPISODE_RECALL_SURVEILLANCE_TYPE
301326
):
302327
self._add_criteria_latest_episode_recall_surveillance_type()
328+
# TODO: Continue working on the case statements below, copying the Java code
329+
# ------------------------------------------------------------------------
330+
# 🔄 Event & Workflow State Criteria
331+
# ------------------------------------------------------------------------
303332
case SubjectSelectionCriteriaKey.LATEST_EVENT_STATUS:
304333
self._add_criteria_event_status("ep.latest_event_status_id")
305334
case SubjectSelectionCriteriaKey.PRE_INTERRUPT_EVENT_STATUS:
@@ -553,8 +582,6 @@ def _add_variable_selection_criteria(
553582
f"Invalid subject selection criteria key: {self.criteria_key_name}"
554583
)
555584

556-
# TODO: Add more case statemented here, copying the Java code
557-
558585
except Exception:
559586
raise SelectionBuilderException(
560587
f"Invalid subject selection criteria key: {self.criteria_key_name}"
@@ -698,6 +725,203 @@ def _add_criteria_latest_episode_type(self) -> None:
698725
except Exception:
699726
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
700727

728+
def _add_criteria_latest_episode_sub_type(self) -> None:
729+
"""
730+
Adds a SQL condition that filters based on the episode_subtype_id of a subject's latest episode.
731+
732+
Translates a human-readable episode sub-type string into an internal numeric ID.
733+
"""
734+
try:
735+
value = self.criteria_value.lower()
736+
comparator = self.criteria_comparator
737+
738+
# Simulated EpisodeSubType enum mapping
739+
episode_subtype_map = {
740+
"routine screening": 10,
741+
"urgent referral": 11,
742+
"pre-assessment": 12,
743+
"follow-up": 13,
744+
"surveillance": 14,
745+
# Add more mappings as needed
746+
}
747+
748+
if value not in episode_subtype_map:
749+
raise ValueError(f"Unknown episode sub-type: {value}")
750+
751+
episode_subtype_id = episode_subtype_map[value]
752+
753+
# Add SQL condition using the mapped ID
754+
self.sql_where.append(
755+
f"AND ep.episode_subtype_id {comparator} {episode_subtype_id}"
756+
)
757+
758+
except Exception:
759+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
760+
761+
def _add_criteria_latest_episode_status(self) -> None:
762+
"""
763+
Adds a SQL condition that filters based on the episode_status_id of a subject's latest episode.
764+
765+
Translates a human-readable episode status into an internal numeric ID.
766+
"""
767+
try:
768+
value = self.criteria_value.lower()
769+
comparator = self.criteria_comparator
770+
771+
# Simulated EpisodeStatusType mapping
772+
episode_status_map = {
773+
"active": 100,
774+
"completed": 101,
775+
"pending": 102,
776+
"cancelled": 103,
777+
"invalid": 104,
778+
# Add actual mappings as needed
779+
}
780+
781+
if value not in episode_status_map:
782+
raise ValueError(f"Unknown episode status: {value}")
783+
784+
episode_status_id = episode_status_map[value]
785+
786+
self.sql_where.append(
787+
f"AND ep.episode_status_id {comparator} {episode_status_id}"
788+
)
789+
790+
except Exception:
791+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
792+
793+
def _add_criteria_latest_episode_status_reason(self) -> None:
794+
"""
795+
Adds a SQL condition that filters based on the episode_status_reason_id of the subject's latest episode.
796+
797+
Allows for explicit mapping or handling of NULL where no status reason is recorded.
798+
"""
799+
try:
800+
value = self.criteria_value.lower()
801+
802+
# Simulated EpisodeStatusReasonType enum
803+
episode_status_reason_map = {
804+
"completed screening": 200,
805+
"no longer eligible": 201,
806+
"deceased": 202,
807+
"moved away": 203,
808+
"null": None, # Special case to represent SQL IS NULL
809+
# Extend as needed
810+
}
811+
812+
if value not in episode_status_reason_map:
813+
raise ValueError(f"Unknown episode status reason: {value}")
814+
815+
status_reason_id = episode_status_reason_map[value]
816+
817+
if status_reason_id is None:
818+
self.sql_where.append("AND ep.episode_status_reason_id IS NULL")
819+
else:
820+
comparator = self.criteria_comparator
821+
self.sql_where.append(
822+
f"AND ep.episode_status_reason_id {comparator} {status_reason_id}"
823+
)
824+
825+
except Exception:
826+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
827+
828+
def _add_criteria_latest_episode_recall_calc_method(self) -> None:
829+
"""
830+
Adds a SQL condition filtering on recall_calculation_method_id from the latest episode.
831+
832+
Handles mapped descriptions or nulls for closed episodes with no recall method.
833+
"""
834+
try:
835+
value = self.criteria_value.lower()
836+
837+
# Simulated enum-like mapping
838+
recall_calc_method_map = {
839+
"standard": 300,
840+
"accelerated": 301,
841+
"paused": 302,
842+
"null": None, # For episodes with no recall method
843+
# Extend with real values as needed
844+
}
845+
846+
if value not in recall_calc_method_map:
847+
raise ValueError(f"Unknown recall calculation method: {value}")
848+
849+
method_id = recall_calc_method_map[value]
850+
851+
if method_id is None:
852+
self.sql_where.append("AND ep.recall_calculation_method_id IS NULL")
853+
else:
854+
comparator = self.criteria_comparator
855+
self.sql_where.append(
856+
f"AND ep.recall_calculation_method_id {comparator} {method_id}"
857+
)
858+
859+
except Exception:
860+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
861+
862+
def _add_criteria_latest_episode_recall_episode_type(self) -> None:
863+
"""
864+
Adds a filter for recall_episode_type_id based on the type of episode that triggered the recall.
865+
Supports mapped descriptions and IS NULL.
866+
"""
867+
try:
868+
value = self.criteria_value.lower()
869+
870+
recall_episode_type_map = {
871+
"referral": 1,
872+
"invitation": 2,
873+
"reminder": 3,
874+
"episode_end": 4,
875+
"null": None,
876+
}
877+
878+
if value not in recall_episode_type_map:
879+
raise ValueError(f"Unknown recall episode type: {value}")
880+
881+
type_id = recall_episode_type_map[value]
882+
883+
if type_id is None:
884+
self.sql_where.append("AND ep.recall_episode_type_id IS NULL")
885+
else:
886+
comparator = self.criteria_comparator
887+
self.sql_where.append(
888+
f"AND ep.recall_episode_type_id {comparator} {type_id}"
889+
)
890+
891+
except Exception:
892+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
893+
894+
def _add_criteria_latest_episode_recall_surveillance_type(self) -> None:
895+
"""
896+
Adds a filter for recall_polyp_surv_type_id based on the type of surveillance used during recall.
897+
Supports mapped descriptions and null values.
898+
"""
899+
try:
900+
value = self.criteria_value.lower()
901+
902+
recall_surv_type_map = {
903+
"routine": 500,
904+
"enhanced": 501,
905+
"annual": 502,
906+
"null": None,
907+
}
908+
909+
if value not in recall_surv_type_map:
910+
raise ValueError(f"Unknown recall surveillance type: {value}")
911+
912+
surv_id = recall_surv_type_map[value]
913+
914+
if surv_id is None:
915+
self.sql_where.append("AND ep.recall_polyp_surv_type_id IS NULL")
916+
else:
917+
comparator = self.criteria_comparator
918+
self.sql_where.append(
919+
f"AND ep.recall_polyp_surv_type_id {comparator} {surv_id}"
920+
)
921+
922+
except Exception:
923+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
924+
701925
def _add_criteria_subject_hub_code(self, user: "User") -> None:
702926
hub_code = None
703927
try:

utils/oracle/test_subject_criteria_dev.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@
3030
# === Example usage ===
3131
# Replace the examples below with the method you want to test
3232

33-
# === Test: LATEST_EPISODE_SUB_TYPERoutine Screening ===
33+
# === Test: LATEST_EPISODE_RECALL_SURVEILLANCE_TYPEEnhanced ===
3434
builder = MockSelectionBuilder(
35-
SubjectSelectionCriteriaKey.LATEST_EPISODE_SUB_TYPE, "routine screening"
35+
SubjectSelectionCriteriaKey.LATEST_EPISODE_RECALL_SURVEILLANCE_TYPE, "enhanced"
3636
)
37-
builder._add_criteria_latest_episode_sub_type()
38-
print("=== LATEST_EPISODE_SUB_TYPEroutine screening ===")
37+
builder._add_criteria_latest_episode_recall_surveillance_type()
38+
print("=== LATEST_EPISODE_RECALL_SURVEILLANCE_TYPEenhanced ===")
3939
print(builder.dump_sql(), end="\n\n")
4040

41-
# === Test: LATEST_EPISODE_SUB_TYPEFollow-up with custom comparator ===
41+
# === Test: LATEST_EPISODE_RECALL_SURVEILLANCE_TYPENull ===
4242
builder = MockSelectionBuilder(
43-
SubjectSelectionCriteriaKey.LATEST_EPISODE_SUB_TYPE, "follow-up", "!="
43+
SubjectSelectionCriteriaKey.LATEST_EPISODE_RECALL_SURVEILLANCE_TYPE, "null"
4444
)
45-
builder._add_criteria_latest_episode_sub_type()
46-
print("=== LATEST_EPISODE_SUB_TYPEfollow-up (!=) ===")
45+
builder._add_criteria_latest_episode_recall_surveillance_type()
46+
print("=== LATEST_EPISODE_RECALL_SURVEILLANCE_TYPEnull ===")
4747
print(builder.dump_sql(), end="\n\n")

0 commit comments

Comments
 (0)