Skip to content

Commit 9c447eb

Browse files
authored
ELI-533: Improvements in Campaign config validator (#469)
1 parent db8a9cc commit 9c447eb

File tree

17 files changed

+479
-10
lines changed

17 files changed

+479
-10
lines changed

.nojekyll

Whitespace-only changes.

index.html

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Campaign Config Validator - NHS Digital</title>
6+
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
7+
<style>
8+
body {
9+
margin: 0;
10+
font-family: "Frutiger W01", Arial, sans-serif; /* NHS standard font stack */
11+
background-color: #f0f4f5; /* NHS Light Grey */
12+
color: #212b32; /* NHS Black */
13+
display: flex;
14+
flex-direction: column;
15+
min-height: 100vh;
16+
}
17+
18+
.nhs-header {
19+
background-color: #005eb8; /* NHS Blue */
20+
padding: 12px 16px;
21+
border-bottom: 4px solid white; /* NHS Standard header spacing */
22+
}
23+
.nhs-header-content {
24+
max-width: 960px;
25+
margin: 0 auto;
26+
display: flex;
27+
align-items: center;
28+
}
29+
.nhs-logo {
30+
width: 80px;
31+
height: 32px;
32+
}
33+
.nhs-container {
34+
max-width: 960px;
35+
margin: 2rem auto;
36+
padding: 0 16px;
37+
width: 100%;
38+
box-sizing: border-box;
39+
flex: 1;
40+
}
41+
h1 {
42+
font-size: 2.5rem;
43+
font-weight: 700;
44+
margin-bottom: 1.5rem;
45+
}
46+
.nhs-card {
47+
background: white;
48+
border: 1px solid #d8dde0;
49+
padding: 2rem;
50+
margin-bottom: 2rem;
51+
}
52+
.status-bar {
53+
background: #e8edee;
54+
color: #212b32;
55+
padding: 12px;
56+
border-left: 10px solid #005eb8;
57+
margin-bottom: 24px;
58+
font-weight: 600;
59+
display: flex;
60+
justify-content: space-between;
61+
align-items: center;
62+
}
63+
.nhs-button {
64+
background-color: #007f3b; /* NHS Green */
65+
color: white;
66+
font-size: 19px;
67+
line-height: 19px;
68+
font-weight: 600;
69+
padding: 12px 24px;
70+
border: none;
71+
cursor: pointer;
72+
box-shadow: 0 4px 0 #00401e; /* The distinct NHS button shadow */
73+
margin-top: 1rem;
74+
width: 100%;
75+
text-align: center;
76+
text-decoration: none;
77+
display: inline-block;
78+
}
79+
.nhs-button:hover:not(:disabled) {
80+
background-color: #00642e; /* Darker Green hover */
81+
}
82+
.nhs-button:active:not(:disabled) {
83+
transform: translateY(4px); /* Button press effect */
84+
box-shadow: none;
85+
}
86+
.nhs-button:disabled {
87+
background-color: #d8dde0;
88+
color: #768692;
89+
box-shadow: none;
90+
cursor: not-allowed;
91+
}
92+
label {
93+
display: block;
94+
margin-bottom: 8px;
95+
font-weight: 600;
96+
font-size: 1.1rem;
97+
}
98+
input[type="file"] {
99+
font-size: 1.1rem;
100+
padding: 10px;
101+
background: #f0f4f5;
102+
border: 2px solid #212b32;
103+
width: 100%;
104+
box-sizing: border-box;
105+
cursor: pointer;
106+
}
107+
.log {
108+
background: #212b32;
109+
color: white;
110+
font-family: monospace;
111+
padding: 20px;
112+
min-height: 250px;
113+
white-space: pre-wrap;
114+
margin-top: 30px;
115+
font-size: 0.95rem;
116+
border-left: 10px solid #768692;
117+
}
118+
119+
.ansi-green { color: #007f3b; font-weight: bold; background: #ccffdd; padding: 2px 5px; }
120+
.ansi-red { color: #d81e05; font-weight: bold; background: #ffd1d1; padding: 2px 5px;}
121+
.ansi-yellow { color: #ffb81c; font-weight: bold; }
122+
123+
footer {
124+
background: #d8dde0;
125+
padding: 2rem 0;
126+
margin-top: auto;
127+
font-size: 0.9rem;
128+
text-align: center;
129+
}
130+
</style>
131+
</head>
132+
<body>
133+
134+
<header class="nhs-header">
135+
<div class="nhs-header-content">
136+
<svg class="nhs-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 16" aria-hidden="true">
137+
<path fill="#fff" d="M0 0h40v16H0z"></path>
138+
<path fill="#005eb8" d="M3.9 1.5h4.4l2.6 9h.1l1.8-9h3.3l-2.8 13H9l-2.7-9h-.1l-1.8 9H1.1M17.3 1.5h3.6l-1 4.9h4L25 1.5h3.5l-2.7 13h-3.5l1.1-5.6h-4.1l-1.2 5.6h-3.4M37.7 4.4c-.7-.3-1.6-.6-2.9-.6-1.4 0-2.5.2-2.5 1.3 0 1.8 5.1 1.2 5.1 5.1 0 3.6-3.3 4.5-6.4 4.5-1.3 0-2.9-.3-4-.7l.8-2.7c.7.4 2.1.7 3.2.7 1.3 0 2.3-.2 2.3-1.4 0-2-5.1-1.2-5.1-5.1 0-4 3.7-4.5 6.4-4.5 1.2 0 2.7.3 3.5.6"></path>
139+
</svg>
140+
</div>
141+
</header>
142+
143+
<div class="nhs-container">
144+
<h1>Campaign Config Validator</h1>
145+
146+
<div class="status-bar">
147+
<span id="statusText">⏳ Booting Python Environment...</span>
148+
<span id="versionInfo" style="font-size: 0.8em; opacity: 0.7"></span>
149+
</div>
150+
151+
<div class="nhs-card">
152+
<label for="jsonfile">Upload Configuration File (JSON)</label>
153+
<span style="display:block; margin-bottom:10px; color:#4c6272; font-size:0.9rem;">Select the campaign configuration file you wish to validate against the rules API.</span>
154+
155+
<input type="file" id="jsonfile" accept=".json" disabled />
156+
157+
<button id="run" class="nhs-button" disabled>
158+
Run Validation
159+
</button>
160+
</div>
161+
162+
<h3>Validation Output</h3>
163+
<div id="result" class="log">System initializing...
164+
</div>
165+
</div>
166+
167+
<div class="nhs-card" style="margin-top:2rem;">
168+
<h3>Visualiser Output</h3>
169+
<iframe
170+
id="visualiserFrame"
171+
src="https://elidvisualiser.hadfieldjohn.com/index.html"
172+
style="width:100%; height:600px; border:1px solid #d8dde0;"
173+
title="ELiD Visualiser"
174+
></iframe>
175+
</div>
176+
177+
<footer>
178+
<div class="nhs-header-content" style="justify-content: center;">
179+
&copy; NHS England. Intended for internal use only.
180+
</div>
181+
</footer>
182+
183+
<script>
184+
let pyodide;
185+
const output = document.getElementById("result");
186+
const statusText = document.getElementById("statusText");
187+
const jsonInput = document.getElementById("jsonfile");
188+
const runBtn = document.getElementById("run");
189+
190+
function log(text) {
191+
let cleanText = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
192+
// Handle ANSI Colors for Pydantic output
193+
cleanText = cleanText
194+
.replace(/\x1b\[92m/g, '<span class="ansi-green">')
195+
.replace(/\x1b\[93m/g, '<span class="ansi-yellow">')
196+
.replace(/\x1b\[91m/g, '<span class="ansi-red">')
197+
.replace(/\x1b\[0m/g, '</span>');
198+
199+
output.innerHTML += cleanText + "\n";
200+
output.scrollTop = output.scrollHeight;
201+
}
202+
203+
function clearLog() { output.innerHTML = ""; } // Changed to innerHTML for spans
204+
205+
function setStatus(msg, ready=false) {
206+
statusText.innerHTML = ready ? `✅ ${msg}` : `⏳ ${msg}`;
207+
if(ready) {
208+
jsonInput.disabled = false;
209+
statusText.style.borderLeftColor = "#007f3b"; // Switch the status bar to green
210+
}
211+
}
212+
213+
const filesToFetch = [
214+
"src/rules_validation_api/app.py",
215+
"src/eligibility_signposting_api/__init__.py",
216+
"src/eligibility_signposting_api/model/__init__.py",
217+
"src/eligibility_signposting_api/model/campaign_config.py",
218+
"src/eligibility_signposting_api/config/__init__.py",
219+
"src/eligibility_signposting_api/config/constants.py",
220+
"src/rules_validation_api/__init__.py",
221+
"src/rules_validation_api/validators/__init__.py",
222+
"src/rules_validation_api/validators/rules_validator.py",
223+
"src/rules_validation_api/validators/campaign_config_validator.py",
224+
"src/rules_validation_api/validators/actions_mapper_validator.py",
225+
"src/rules_validation_api/validators/available_action_validator.py",
226+
"src/rules_validation_api/validators/iteration_cohort_validator.py",
227+
"src/rules_validation_api/validators/iteration_rules_validator.py",
228+
"src/rules_validation_api/validators/iteration_validator.py"
229+
];
230+
231+
async function main() {
232+
try {
233+
pyodide = await loadPyodide();
234+
pyodide.setStdout({ batched: (msg) => log(msg) });
235+
pyodide.setStderr({ batched: (msg) => log("ERR: " + msg) });
236+
237+
setStatus("Installing Python Libraries...");
238+
await pyodide.loadPackage("micropip");
239+
await pyodide.pyimport("micropip").install("pydantic");
240+
await pyodide.pyimport("micropip").install("markdown");
241+
242+
setStatus("Downloading Source Code...");
243+
for (const path of filesToFetch) {
244+
const response = await fetch(path);
245+
if (!response.ok) throw new Error(`404 Not Found: ${path}`);
246+
const content = await response.text();
247+
248+
const dir = path.substring(0, path.lastIndexOf('/'));
249+
if (dir) {
250+
const parts = dir.split('/');
251+
let current = "";
252+
for(const part of parts) {
253+
current += part + "/";
254+
if(!pyodide.FS.analyzePath(current.slice(0,-1)).exists) {
255+
pyodide.FS.mkdir(current.slice(0,-1));
256+
}
257+
}
258+
}
259+
pyodide.FS.writeFile(path, content);
260+
}
261+
262+
setStatus("System Ready", true);
263+
log("✅ Environment loaded. Please upload a JSON file.");
264+
265+
} catch (err) {
266+
setStatus("Error", false);
267+
statusText.style.color = "#d81e05";
268+
log(`❌ FATAL ERROR: ${err.message}`);
269+
}
270+
}
271+
main();
272+
273+
let configContent = null;
274+
jsonInput.addEventListener("change", (e) => {
275+
const reader = new FileReader();
276+
reader.onload = () => {
277+
configContent = reader.result;
278+
runBtn.disabled = false;
279+
log(`\n📄 Loaded: ${e.target.files[0].name}`);
280+
};
281+
reader.readAsText(e.target.files[0]);
282+
});
283+
284+
runBtn.addEventListener("click", async () => {
285+
clearLog();
286+
log("🚀 Validating Configuration...\n");
287+
pyodide.globals.set("raw_json", configContent);
288+
289+
try {
290+
await pyodide.runPythonAsync(`
291+
import sys
292+
import json
293+
import importlib
294+
import os
295+
296+
297+
if "src" not in sys.path: sys.path.insert(0, "src")
298+
from rules_validation_api import app
299+
importlib.reload(app)
300+
301+
with open("config.json", "w") as f:
302+
f.write(raw_json)
303+
304+
sys.argv = ["app.py", "--config_path", "config.json"]
305+
app.main()
306+
`);
307+
} catch (e) {
308+
log(`\n❌ EXECUTION ERROR:\n${e}`);
309+
}
310+
});
311+
</script>
312+
</body>
313+
</html>

scripts/config/vale/styles/config/vocabularies/words/accept.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ repo
3838
Preprod
3939
Dev
4040
auditable
41+
bool

src/eligibility_signposting_api/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from eligibility_signposting_api.common.cache_manager import FLASK_APP_CACHE_KEY, cache_manager
1414
from eligibility_signposting_api.common.error_handler import handle_exception
1515
from eligibility_signposting_api.config.config import config
16-
from eligibility_signposting_api.config.contants import URL_PREFIX
16+
from eligibility_signposting_api.config.constants import URL_PREFIX
1717
from eligibility_signposting_api.logging.logs_helper import log_request_ids_from_headers
1818
from eligibility_signposting_api.logging.logs_manager import add_lambda_request_id_to_logger, init_logging
1919
from eligibility_signposting_api.logging.tracing_helper import tracing_setup

src/eligibility_signposting_api/common/request_validator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
INVALID_INCLUDE_ACTIONS_ERROR,
1313
NHS_NUMBER_MISMATCH_ERROR,
1414
)
15-
from eligibility_signposting_api.config.contants import NHS_NUMBER_HEADER
15+
from eligibility_signposting_api.config.constants import NHS_NUMBER_HEADER
1616

1717
logger = logging.getLogger(__name__)
1818

File renamed without changes.

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
model_validator,
2121
)
2222

23-
from eligibility_signposting_api.config.contants import ALLOWED_CONDITIONS, RULE_STOP_DEFAULT
23+
from eligibility_signposting_api.config.constants import ALLOWED_CONDITIONS, RULE_STOP_DEFAULT
2424

2525
if typing.TYPE_CHECKING: # pragma: no cover
2626
from pydantic import SerializationInfo

src/eligibility_signposting_api/services/processors/token_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from wireup import service
77

8-
from eligibility_signposting_api.config.contants import ALLOWED_CONDITIONS
8+
from eligibility_signposting_api.config.constants import ALLOWED_CONDITIONS
99
from eligibility_signposting_api.model.person import Person
1010
from eligibility_signposting_api.services.processors.token_parser import ParsedToken, TokenParser
1111

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from eligibility_signposting_api.audit.audit_service import AuditService
1414
from eligibility_signposting_api.common.api_error_response import NHS_NUMBER_NOT_FOUND_ERROR
1515
from eligibility_signposting_api.common.request_validator import validate_request_params
16-
from eligibility_signposting_api.config.contants import URL_PREFIX
16+
from eligibility_signposting_api.config.constants import URL_PREFIX
1717
from eligibility_signposting_api.model.eligibility_status import Condition, EligibilityStatus, NHSNumber, Status
1818
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
1919
from eligibility_signposting_api.views.response_model import eligibility_response

src/rules_validation_api/app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import argparse
22
import json
3+
import logging
34
import sys
45
from pathlib import Path
56

67
from rules_validation_api.validators.rules_validator import RulesValidation
78

9+
logging.basicConfig(
10+
level=logging.INFO, # or DEBUG for more detail
11+
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
12+
force=True,
13+
)
14+
815
GREEN = "\033[92m" # pragma: no cover
916
RESET = "\033[0m" # pragma: no cover
1017
YELLOW = "\033[93m" # pragma: no cover

0 commit comments

Comments
 (0)