Skip to content

Commit a91dc9b

Browse files
committed
Added Notify message filters and parser utility for criteria string parsing
- Implemented filter for Notify queued message status using EXISTS clause with symbolic lookup - Implemented filter for Notify archived message status with multi-table join and event/code/status matching - Added `parse_notify_criteria` utility to extract type, code, and status from composite strings (e.g. "S1 (S1w) - sending") - Documented parser usage in a dedicated Markdown guide with examples, formats, and integration references
1 parent 11a3cba commit a91dc9b

File tree

4 files changed

+146
-17
lines changed

4 files changed

+146
-17
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Utility Guide: Notify Criteria Parser
2+
**Source:** [`utils/notify_criteria_parser.py`](../../utils/notify_criteria_parser.py)
3+
4+
The Notify Criteria Parser is a lightweight utility that extracts structured values from compact Notify message criteria strings. It is used by selection builders to support Notify filter logic—like `"S1 - new"` or `"S1 (S1w) - sending"` — by parsing these inputs into cleanly separated parts: `message type`, `message code (optional)`, and `status`.
5+
6+
## Table of Contents
7+
8+
- [Utility Guide: Notify Criteria Parser](#utility-guide-notify-criteria-parser)
9+
- [Table of Contents](#table-of-contents)
10+
- [Overview](#overview)
11+
- [Using the Parser](#using-the-parser)
12+
- [Expected Input Formats](#expected-input-formats)
13+
- [Example Usage](#example-usage)
14+
- [Output Structure](#output-structure)
15+
- [Edge Case: none](#edge-case-none)
16+
- [Error Handling](#error-handling)
17+
- [Integration Points](#integration-points)
18+
19+
---
20+
21+
## Overview
22+
23+
Notify message filters are written as short text descriptions like "S1 - new" or "S1 (S1w) - sending".
24+
The parser splits them into meaningful parts so that the system knows what message type to look for, whether there's a specific message code, and the message's status (like "new", "sending", etc). This parser breaks those strings into usable components for SQL query builders.
25+
26+
- `"S1 - new"`
27+
- `"S1 (S1w) - sending"`
28+
- `"none"`
29+
30+
---
31+
32+
## Using the Parser
33+
34+
Import the parser function and give it a string like "S1 (S1w) - sending", and it gives you back each piece of information separately, like the message type, the code (if there is one), and the status.
35+
36+
```python
37+
from utils.notify_criteria_parser import parse_notify_criteria
38+
39+
parts = parse_notify_criteria("S1 (S1w) - sending")
40+
```
41+
42+
## Expected Input Formats
43+
The parser supports the following input patterns:
44+
45+
| Format | Meaning |
46+
| ------------------------- | ---------------------------------------------- |
47+
| `Type - status` | e.g. `"S1 - new"` |
48+
| `Type (Code) - status` | e.g. `"S1 (S1w) - sending"` |
49+
| `none` (case-insensitive) | Special case meaning “no message should exist” |
50+
51+
## Example Usage
52+
Here are a few examples of what the parser returns. Think of it like splitting a sentence into parts so each part can be used in a database search:
53+
54+
```python
55+
parse_notify_criteria("S2 (X9) - failed")
56+
# ➜ {'type': 'S2', 'code': 'X9', 'status': 'failed'}
57+
58+
parse_notify_criteria("S1 - new")
59+
# ➜ {'type': 'S1', 'code': None, 'status': 'new'}
60+
61+
parse_notify_criteria("None")
62+
# ➜ {'status': 'none'}
63+
```
64+
65+
## Output Structure
66+
The returned value is a dictionary containing:
67+
68+
```python
69+
{
70+
"type": str, # the main message group (like S1 or M1)
71+
"code": Optional[str], # the specific version (optional)
72+
"status": str # the message’s progress, such as "new", "sending", or "none"
73+
}
74+
```
75+
## Edge Case: none
76+
If someone enters `none` as the criteria, it means "we're looking for subjects who do not have a matching message." The parser handles this specially, and the SQL builder will write `NOT EXISTS` logic behind the scenes to exclude those cases, so the parser returns:
77+
78+
```python
79+
{'status': 'none'}
80+
```
81+
This signals `NOT EXISTS` logic for Notify message filtering.
82+
83+
## Error Handling
84+
If the input doesn’t match an expected pattern, the parser raises:
85+
86+
```python
87+
ValueError("Invalid Notify criteria format: 'your_input'")
88+
```
89+
e.g. If a tester or user types something like `S1 - banana` or forgets the - status bit, the parser will throw an error. This helps catch typos or unsupported formats early.
90+
91+
## Integration Points
92+
These are the parts of the system that use the parser to decide whether to include or exclude Notify messages from a search:
93+
`SubjectSelectionQueryBuilder._add_criteria_notify_queued_message_status()` – for messages currently in the system
94+
95+
`SubjectSelectionQueryBuilder._add_criteria_notify_archived_message_status()` – for messages already sent or stored
96+
97+
You can also reuse it in any other part of the system that needs to interpret Notify message filters.

utils/oracle/mock_selection_builder.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,36 +121,33 @@ def _add_join_to_surveillance_review(self):
121121
# Replace this with the one you want to test,
122122
# then use utils/oracle/test_subject_criteria_dev.py to run your scenarios
123123

124-
def _add_criteria_notify_queued_message_status(self) -> None:
124+
def _add_criteria_notify_archived_message_status(self) -> None:
125125
"""
126-
Filters subjects based on Notify queued message status, e.g. 'S1 (S1w) - new'.
126+
Filters subjects based on archived Notify message criteria, e.g. 'S1 (S1w) - sending'.
127127
"""
128128
try:
129129
parts = parse_notify_criteria(self.criteria_value)
130130
status = parts["status"]
131131

132-
if status == "none":
133-
clause = "NOT EXISTS"
134-
else:
135-
clause = "EXISTS"
132+
clause = "NOT EXISTS" if status == "none" else "EXISTS"
136133

137134
self.sql_where.append(f"AND {clause} (")
138135
self.sql_where.append(
139-
"SELECT 1 FROM notify_message_queue nmq "
140-
"INNER JOIN notify_message_definition nmd ON nmd.message_definition_id = nmq.message_definition_id "
141-
"WHERE nmq.nhs_number = c.nhs_number "
136+
"SELECT 1 FROM notify_message_record nmr "
137+
"INNER JOIN notify_message_batch nmb ON nmb.batch_id = nmr.batch_id "
138+
"INNER JOIN notify_message_definition nmd ON nmd.message_definition_id = nmb.message_definition_id "
139+
"WHERE nmr.subject_id = ss.screening_subject_id "
142140
)
143141

144-
# Simulate getNotifyMessageEventStatusIdFromCriteria()
145142
event_status_id = NotifyEventStatus.get_id(parts["type"])
146143
self.sql_where.append(f"AND nmd.event_status_id = {event_status_id} ")
147144

148-
if status != "none":
149-
self.sql_where.append(f"AND nmq.message_status = '{status}' ")
150-
151145
if "code" in parts and parts["code"]:
152146
self.sql_where.append(f"AND nmd.message_code = '{parts['code']}' ")
153147

148+
if status != "none":
149+
self.sql_where.append(f"AND nmr.message_status = '{status}' ")
150+
154151
self.sql_where.append(")")
155152

156153
except Exception:

utils/oracle/subject_selection_query_builder.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,38 @@ def _add_criteria_notify_queued_message_status(self) -> None:
22842284
except Exception:
22852285
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
22862286

2287+
def _add_criteria_notify_archived_message_status(self) -> None:
2288+
"""
2289+
Filters subjects based on archived Notify message criteria, e.g. 'S1 (S1w) - sending'.
2290+
"""
2291+
try:
2292+
parts = parse_notify_criteria(self.criteria_value)
2293+
status = parts["status"]
2294+
2295+
clause = "NOT EXISTS" if status == "none" else "EXISTS"
2296+
2297+
self.sql_where.append(f"AND {clause} (")
2298+
self.sql_where.append(
2299+
"SELECT 1 FROM notify_message_record nmr "
2300+
"INNER JOIN notify_message_batch nmb ON nmb.batch_id = nmr.batch_id "
2301+
"INNER JOIN notify_message_definition nmd ON nmd.message_definition_id = nmb.message_definition_id "
2302+
"WHERE nmr.subject_id = ss.screening_subject_id "
2303+
)
2304+
2305+
event_status_id = NotifyEventStatus.get_id(parts["type"])
2306+
self.sql_where.append(f"AND nmd.event_status_id = {event_status_id} ")
2307+
2308+
if "code" in parts and parts["code"]:
2309+
self.sql_where.append(f"AND nmd.message_code = '{parts['code']}' ")
2310+
2311+
if status != "none":
2312+
self.sql_where.append(f"AND nmr.message_status = '{status}' ")
2313+
2314+
self.sql_where.append(")")
2315+
2316+
except Exception:
2317+
raise SelectionBuilderException(self.criteria_key_name, self.criteria_value)
2318+
22872319
# ------------------------------------------------------------------------
22882320
# 🧬 CADS Clinical Dataset Filters
22892321
# ------------------------------------------------------------------------

utils/oracle/test_subject_criteria_dev.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ def make_builder(key, value, index=0, comparator="="):
4141
return b
4242

4343

44-
# === Test: Notify message (S1 - new) ===
45-
b = make_builder(SubjectSelectionCriteriaKey.NOTIFY_QUEUED_MESSAGE_STATUS, "S1 - new")
46-
b._add_criteria_notify_queued_message_status()
47-
print(b.dump_sql())
44+
# === Test: NOTIFY_ARCHIVED_MESSAGE_STATUS (S1 (S1w) - sending) ===
45+
b = make_builder(
46+
SubjectSelectionCriteriaKey.NOTIFY_ARCHIVED_MESSAGE_STATUS, "S1 (S1w) - sending"
47+
)
48+
b._add_criteria_notify_archived_message_status()
49+
print("=== NOTIFY_ARCHIVED_MESSAGE_STATUS (S1 (S1w) - sending) ===")
50+
print(b.dump_sql(), end="\n\n")

0 commit comments

Comments
 (0)