@@ -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 :
0 commit comments