Skip to content

Commit 26ed942

Browse files
committed
Added subject selection criteria methods and test kit joins to query builder
1 parent e34a187 commit 26ed942

File tree

3 files changed

+320
-31
lines changed

3 files changed

+320
-31
lines changed

utils/oracle/mock_selection_builder.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,37 @@ def __init__(self, criteria_key, criteria_value, criteria_comparator=">="):
3131
# Don't delete this method; it is used to inspect the SQL fragments
3232
def dump_sql(self):
3333
return "\n".join(self.sql_where)
34+
35+
def _add_join_to_latest_episode(self) -> None:
36+
"""
37+
Mock stub for adding latest episode join. No-op for test harness.
38+
"""
39+
self.sql_from.append("-- JOIN to latest episode placeholder")
40+
3441

3542
# === Example testable method below ===
3643
# Replace this with the one you want to test,
3744
# then use utils/oracle/test_subject_criteria_dev.py to run your scenarios
3845

39-
def _add_criteria_subject_has_kit_notes(self) -> None:
46+
def _add_criteria_kit_has_analyser_result_code(self) -> None:
4047
"""
41-
Filters subjects based on presence of active kit-related notes.
42-
Accepts values: 'yes' or 'no'.
48+
Filters kits based on whether they have an analyser error code.
49+
Requires prior join to tk_items_t as alias 'tk' (via WHICH_TEST_KIT).
50+
51+
Accepts values:
52+
- "yes" → analyser_error_code IS NOT NULL
53+
- "no" → analyser_error_code IS NULL
4354
"""
4455
try:
4556
value = self.criteria_value.strip().lower()
4657

4758
if value == "yes":
48-
prefix = "AND EXISTS"
59+
self.sql_where.append("AND tk.analyser_error_code IS NOT NULL")
4960
elif value == "no":
50-
prefix = "AND NOT EXISTS"
61+
self.sql_where.append("AND tk.analyser_error_code IS NULL")
5162
else:
52-
raise ValueError(f"Invalid value for kit notes: {value}")
53-
54-
self.sql_where.append(
55-
f"""{prefix} (
56-
SELECT 1
57-
FROM supporting_notes_t sn
58-
WHERE sn.screening_subject_id = ss.screening_subject_id
59-
AND (
60-
sn.type_id = '308015'
61-
OR sn.promote_pio_id IS NOT NULL
62-
)
63-
AND sn.status_id = 4100
64-
)
65-
AND ss.number_of_invitations > 0
66-
AND rownum = 1"""
67-
)
63+
raise ValueError(f"Invalid value for analyser result code presence: {value}")
6864

6965
except Exception:
7066
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
67+

utils/oracle/subject_selection_query_builder.py

Lines changed: 290 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,9 @@ def _add_variable_selection_criteria(
513513
self._add_criteria_date_field(
514514
subject, "ALL_PATHWAYS", "SEVENTY_FIFTH_BIRTHDAY"
515515
)
516+
# ------------------------------------------------------------------------
517+
# 🧬 CADS Clinical Dataset Filters
518+
# ------------------------------------------------------------------------
516519
case SubjectSelectionCriteriaKey.CADS_ASA_GRADE:
517520
self._add_criteria_cads_asa_grade()
518521
case SubjectSelectionCriteriaKey.CADS_STAGING_SCANS:
@@ -1111,8 +1114,6 @@ def _add_criteria_has_referral_date(self) -> None:
11111114
except Exception:
11121115
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
11131116

1114-
# TODO: Add methods below for other criteria keys as needed
1115-
11161117
def _add_criteria_has_diagnosis_date(self) -> None:
11171118
"""
11181119
Adds a filter to check if the latest episode has a diagnosis_date set,
@@ -1362,6 +1363,293 @@ def _add_criteria_subject_has_kit_notes(self) -> None:
13621363
except Exception:
13631364
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
13641365

1366+
def _add_criteria_subject_has_lynch_diagnosis(self) -> None:
1367+
"""
1368+
Adds a filter to check if a subject has an active Lynch diagnosis.
1369+
Accepts:
1370+
- "yes" → subject must have active diagnosis ('Y')
1371+
- "no" → subject must not have active diagnosis ('N')
1372+
"""
1373+
try:
1374+
value = self.criteria_value.strip().lower()
1375+
1376+
if value == "yes":
1377+
self.sql_where.append(
1378+
"AND pkg_lynch.f_subject_has_active_lynch_diagnosis (ss.screening_subject_id) = 'Y'"
1379+
)
1380+
elif value == "no":
1381+
self.sql_where.append(
1382+
"AND pkg_lynch.f_subject_has_active_lynch_diagnosis (ss.screening_subject_id) = 'N'"
1383+
)
1384+
else:
1385+
raise ValueError(f"Invalid value for Lynch diagnosis: {value}")
1386+
1387+
except Exception:
1388+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
1389+
1390+
def _add_join_to_test_kits(self) -> None:
1391+
"""
1392+
Adds joins to the tk_items_t table based on test kit selection criteria.
1393+
Handles whether any kit is considered, or a specific one from the latest episode.
1394+
1395+
Expected values (case-insensitive):
1396+
- "any_kit_in_any_episode"
1397+
- "only_kit_issued_in_latest_episode"
1398+
- "first_kit_issued_in_latest_episode"
1399+
- "latest_kit_issued_in_latest_episode"
1400+
- "only_kit_logged_in_latest_episode"
1401+
- "first_kit_logged_in_latest_episode"
1402+
- "latest_kit_logged_in_latest_episode"
1403+
"""
1404+
try:
1405+
value = self.criteria_value.strip().lower()
1406+
tk_alias = "tk" # You can extend this if you need multiple joins
1407+
1408+
# Base join for all paths (only FIT kits)
1409+
self.sql_from.append(
1410+
f"INNER JOIN tk_items_t {tk_alias} ON {tk_alias}.screening_subject_id = ss.screening_subject_id "
1411+
f"AND {tk_alias}.tk_type_id > 1"
1412+
)
1413+
1414+
if value == "any_kit_in_any_episode":
1415+
return
1416+
1417+
if "issued_in_latest_episode" in value:
1418+
self._add_join_to_latest_episode()
1419+
self.sql_from.append(
1420+
f"AND {tk_alias}.subject_epis_id = ep.subject_epis_id "
1421+
f"AND NOT EXISTS ("
1422+
f" SELECT 'tko1' FROM tk_items_t tko "
1423+
f" WHERE tko.screening_subject_id = ss.screening_subject_id "
1424+
f" AND tko.subject_epis_id = ep.subject_epis_id "
1425+
)
1426+
if value.startswith("only"):
1427+
comparator = "!="
1428+
elif value.startswith("first"):
1429+
comparator = "<"
1430+
else: # latest
1431+
comparator = ">"
1432+
self.sql_from.append(f" AND tko.kitid {comparator} {tk_alias}.kitid)")
1433+
1434+
elif "logged_in_latest_episode" in value:
1435+
self._add_join_to_latest_episode()
1436+
self.sql_from.append(
1437+
f"AND {tk_alias}.logged_subject_epis_id = ep.subject_epis_id "
1438+
f"AND NOT EXISTS ("
1439+
f" SELECT 'tko2' FROM tk_items_t tko "
1440+
f" WHERE tko.screening_subject_id = ss.screening_subject_id "
1441+
f" AND tko.logged_subject_epis_id = ep.subject_epis_id"
1442+
)
1443+
if value.startswith("only"):
1444+
self.sql_from.append(f" AND tko.kitid != {tk_alias}.kitid")
1445+
elif value.startswith("first"):
1446+
self.sql_from.append(
1447+
f" AND tko.logged_in_on < {tk_alias}.logged_in_on"
1448+
)
1449+
else: # latest
1450+
self.sql_from.append(
1451+
f" AND tko.logged_in_on > {tk_alias}.logged_in_on"
1452+
)
1453+
self.sql_from.append(")")
1454+
1455+
else:
1456+
raise ValueError(f"Invalid test kit selection value: {value}")
1457+
1458+
except Exception:
1459+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
1460+
1461+
def _add_criteria_kit_has_been_read(self) -> None:
1462+
"""
1463+
Filters test kits based on whether they have been read.
1464+
Requires prior join to tk_items_t as alias 'tk' (via WHICH_TEST_KIT).
1465+
1466+
Accepts values:
1467+
- "yes" → reading_flag = 'Y'
1468+
- "no" → reading_flag = 'N'
1469+
"""
1470+
try:
1471+
value = self.criteria_value.strip().lower()
1472+
1473+
if value == "yes":
1474+
self.sql_where.append("AND tk.reading_flag = 'Y'")
1475+
elif value == "no":
1476+
self.sql_where.append("AND tk.reading_flag = 'N'")
1477+
else:
1478+
raise ValueError(f"Invalid value for kit has been read: {value}")
1479+
1480+
except Exception:
1481+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
1482+
1483+
def _add_criteria_kit_result(self) -> None:
1484+
"""
1485+
Filters based on the result associated with the selected test kit.
1486+
Requires prior join to tk_items_t as alias 'tk' (via WHICH_TEST_KIT).
1487+
Uses comparator and uppercase value.
1488+
"""
1489+
try:
1490+
comparator = self.criteria_comparator
1491+
value = self.criteria_value.strip().upper()
1492+
self.sql_where.append(f"AND tk.test_results {comparator} '{value}'")
1493+
except Exception:
1494+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
1495+
1496+
def _add_criteria_kit_has_analyser_result_code(self) -> None:
1497+
"""
1498+
Filters kits based on whether they have an analyser error code.
1499+
Requires prior join to tk_items_t as alias 'tk' (via WHICH_TEST_KIT).
1500+
1501+
Accepts values:
1502+
- "yes" → analyser_error_code IS NOT NULL
1503+
- "no" → analyser_error_code IS NULL
1504+
"""
1505+
try:
1506+
value = self.criteria_value.strip().lower()
1507+
1508+
if value == "yes":
1509+
self.sql_where.append("AND tk.analyser_error_code IS NOT NULL")
1510+
elif value == "no":
1511+
self.sql_where.append("AND tk.analyser_error_code IS NULL")
1512+
else:
1513+
raise ValueError(
1514+
f"Invalid value for analyser result code presence: {value}"
1515+
)
1516+
1517+
except Exception:
1518+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
1519+
1520+
# ------------------------------------------------------------------------
1521+
# 🧬 CADS Clinical Dataset Filters
1522+
# ------------------------------------------------------------------------
1523+
1524+
def _add_criteria_cads_asa_grade(self) -> None:
1525+
self._add_join_to_latest_episode()
1526+
self._add_join_to_cancer_audit_dataset()
1527+
self.sql_where.append(
1528+
f"AND cads.asa_grade_id = ASAGradeType.by_description_case_insensitive(self.criteria_value).id"
1529+
)
1530+
1531+
def _add_criteria_cads_staging_scans(self) -> None:
1532+
self._add_join_to_latest_episode()
1533+
self._add_join_to_cancer_audit_dataset()
1534+
self._add_join_to_cancer_audit_dataset_staging_scan()
1535+
self.sql_where.append(
1536+
f"AND cads.staging_scans_done_id = YesNoType.by_description_case_insensitive(self.criteria_value).id"
1537+
)
1538+
1539+
def _add_criteria_cads_type_of_scan(self) -> None:
1540+
self._add_join_to_latest_episode()
1541+
self._add_join_to_cancer_audit_dataset()
1542+
self._add_join_to_cancer_audit_dataset_staging_scan()
1543+
self.sql_where.append(
1544+
f"AND dcss.type_of_scan_id = ScanType.by_description_case_insensitive(self.criteria_value).id"
1545+
)
1546+
1547+
def _add_criteria_cads_metastases_present(self) -> None:
1548+
self._add_join_to_latest_episode()
1549+
self._add_join_to_cancer_audit_dataset()
1550+
self.sql_where.append(
1551+
f"AND cads.metastases_found_id = MetastasesPresentType.by_description_case_insensitive(self.criteria_value).id"
1552+
)
1553+
1554+
def _add_criteria_cads_metastases_location(self) -> None:
1555+
self._add_join_to_latest_episode()
1556+
self._add_join_to_cancer_audit_dataset()
1557+
self._add_join_to_cancer_audit_dataset_metastasis()
1558+
self.sql_where.append(
1559+
f"AND dcm.location_of_metastasis_id = MetastasesLocationType.by_description_case_insensitive(self.criteria_value).id"
1560+
)
1561+
1562+
def _add_criteria_cads_metastases_other_location(self, other_location: str) -> None:
1563+
self._add_join_to_latest_episode()
1564+
self._add_join_to_cancer_audit_dataset()
1565+
self._add_join_to_cancer_audit_dataset_metastasis()
1566+
self.sql_where.append(
1567+
f"AND dcm.other_location_of_metastasis = '{other_location}'"
1568+
)
1569+
1570+
def _add_criteria_cads_final_pre_treatment_t_category(self) -> None:
1571+
self._add_join_to_latest_episode()
1572+
self._add_join_to_cancer_audit_dataset()
1573+
self.sql_where.append(
1574+
f"AND cads.final_pre_treat_t_category_id = FinalPretreatmentTCategoryType.by_description_case_insensitive(self.criteria_value).id"
1575+
)
1576+
1577+
def _add_criteria_cads_final_pre_treatment_n_category(self) -> None:
1578+
self._add_join_to_latest_episode()
1579+
self._add_join_to_cancer_audit_dataset()
1580+
self.sql_where.append(
1581+
f"AND cads.final_pre_treat_n_category_id = FinalPretreatmentNCategoryType.by_description_case_insensitive(self.criteria_value).id"
1582+
)
1583+
1584+
def _add_criteria_cads_final_pre_treatment_m_category(self) -> None:
1585+
self._add_join_to_latest_episode()
1586+
self._add_join_to_cancer_audit_dataset()
1587+
self.sql_where.append(
1588+
f"AND cads.final_pre_treat_m_category_id = FinalPretreatmentMCategoryType.by_description_case_insensitive(self.criteria_value).id"
1589+
)
1590+
1591+
def _add_criteria_cads_treatment_received(self) -> None:
1592+
self._add_join_to_latest_episode()
1593+
self._add_join_to_cancer_audit_dataset()
1594+
self.sql_where.append(
1595+
f"AND cads.treatment_received_id = YesNoType.by_description_case_insensitive(self.criteria_value).id"
1596+
)
1597+
1598+
def _add_criteria_cads_reason_no_treatment_received(self) -> None:
1599+
self._add_join_to_latest_episode()
1600+
self._add_join_to_cancer_audit_dataset()
1601+
self.sql_where.append(
1602+
f"AND cads.reason_no_treatment_id = ReasonNoTreatmentReceivedType.by_description_case_insensitive(self.criteria_value).id"
1603+
)
1604+
1605+
def _add_criteria_cads_tumour_location(self) -> None:
1606+
self._add_join_to_latest_episode()
1607+
self._add_join_to_cancer_audit_dataset()
1608+
self._add_join_to_cancer_audit_dataset_tumour()
1609+
self.sql_where.append(
1610+
f"AND dctu.location_id = LocationType.by_description_case_insensitive(self.criteria_value).id"
1611+
)
1612+
1613+
def _add_criteria_cads_tumour_height_of_tumour_above_anal_verge(self) -> None:
1614+
self._add_join_to_latest_episode()
1615+
self._add_join_to_cancer_audit_dataset()
1616+
self._add_join_to_cancer_audit_dataset_tumour()
1617+
self.sql_where.append(
1618+
f"AND dctu.height_above_anal_verge = {self.criteria_value}"
1619+
)
1620+
1621+
def _add_criteria_cads_tumour_previously_excised_tumour(self) -> None:
1622+
self._add_join_to_latest_episode()
1623+
self._add_join_to_cancer_audit_dataset()
1624+
self._add_join_to_cancer_audit_dataset_tumour()
1625+
self.sql_where.append(
1626+
f"AND dctu.recurrence_id = PreviouslyExcisedTumourType.by_description_case_insensitive(self.criteria_value).id"
1627+
)
1628+
1629+
def _add_criteria_cads_treatment_type(self) -> None:
1630+
self._add_join_to_latest_episode()
1631+
self._add_join_to_cancer_audit_dataset()
1632+
self._add_join_to_cancer_audit_dataset_treatment()
1633+
self.sql_where.append(
1634+
f"AND dctr.treatment_category_id = TreatmentType.by_description_case_insensitive(self.criteria_value).id"
1635+
)
1636+
1637+
def _add_criteria_cads_treatment_given(self) -> None:
1638+
self._add_join_to_latest_episode()
1639+
self._add_join_to_cancer_audit_dataset()
1640+
self._add_join_to_cancer_audit_dataset_treatment()
1641+
self.sql_where.append(
1642+
f"AND dctr.treatment_procedure_id = TreatmentGiven.by_description_case_insensitive(self.criteria_value).id"
1643+
)
1644+
1645+
def _add_criteria_cads_cancer_treatment_intent(self) -> None:
1646+
self._add_join_to_latest_episode()
1647+
self._add_join_to_cancer_audit_dataset()
1648+
self._add_join_to_cancer_audit_dataset_treatment()
1649+
self.sql_where.append(
1650+
f"AND dctr.treatment_intent_id = CancerTreatmentIntent.by_description_case_insensitive(self.criteria_value).id"
1651+
)
1652+
13651653
def _add_criteria_subject_hub_code(self, user: "User") -> None:
13661654
hub_code = None
13671655
try:

utils/oracle/test_subject_criteria_dev.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@
2929
# === Example usage ===
3030
# Replace the examples below with your tests for the method you want to test
3131

32-
# === Test: SUBJECT_HAS_KIT_NOTES — yes ===
33-
builder = MockSelectionBuilder(SubjectSelectionCriteriaKey.SUBJECT_HAS_KIT_NOTES, "yes")
34-
builder._add_criteria_subject_has_kit_notes()
35-
print("=== SUBJECT_HAS_KIT_NOTES — yes ===")
32+
# === Test: KIT_HAS_ANALYSER_RESULT_CODE — yes ===
33+
builder = MockSelectionBuilder(
34+
SubjectSelectionCriteriaKey.KIT_HAS_ANALYSER_RESULT_CODE, "yes"
35+
)
36+
builder._add_criteria_kit_has_analyser_result_code()
37+
print("=== KIT_HAS_ANALYSER_RESULT_CODE — yes ===")
3638
print(builder.dump_sql(), end="\n\n")
3739

38-
# === Test: SUBJECT_HAS_KIT_NOTES — no ===
39-
builder = MockSelectionBuilder(SubjectSelectionCriteriaKey.SUBJECT_HAS_KIT_NOTES, "no")
40-
builder._add_criteria_subject_has_kit_notes()
41-
print("=== SUBJECT_HAS_KIT_NOTES — no ===")
40+
# === Test: KIT_HAS_ANALYSER_RESULT_CODE — no ===
41+
builder = MockSelectionBuilder(
42+
SubjectSelectionCriteriaKey.KIT_HAS_ANALYSER_RESULT_CODE, "no"
43+
)
44+
builder._add_criteria_kit_has_analyser_result_code()
45+
print("=== KIT_HAS_ANALYSER_RESULT_CODE — no ===")
4246
print(builder.dump_sql(), end="\n\n")

0 commit comments

Comments
 (0)