Skip to content

Commit b435649

Browse files
authored
Merge pull request #142 from bsgip/PDF_report_tweaks
Pdf report tweaks
2 parents a98c72f + 768b7cf commit b435649

File tree

3 files changed

+118
-7
lines changed

3 files changed

+118
-7
lines changed

src/cactus_runner/app/finalize.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from pathlib import Path
1010
from typing import cast
1111

12+
from envoy.server.model.archive.site import ArchiveSiteDERSetting
13+
from envoy.server.model.site import SiteDERSetting
14+
from sqlalchemy import select
1215
from sqlalchemy.ext.asyncio import AsyncSession
1316

1417
from cactus_runner.app import check, reporting, timeline
@@ -177,6 +180,7 @@ async def generate_pdf(
177180
sites,
178181
timeline,
179182
errors,
183+
set_max_w_varied: bool = False,
180184
) -> bytes | None:
181185
try:
182186
# Generate the pdf (as bytes)
@@ -187,6 +191,7 @@ async def generate_pdf(
187191
reading_counts=reading_counts,
188192
sites=sites,
189193
timeline=timeline,
194+
set_max_w_varied=set_max_w_varied,
190195
)
191196
except Exception as exc:
192197
logger.error("Error generating PDF report.", exc_info=exc)
@@ -206,6 +211,7 @@ async def generate_pdf(
206211
sites=sites,
207212
timeline=timeline,
208213
no_spacers=True,
214+
set_max_w_varied=set_max_w_varied,
209215
)
210216
except Exception as exc:
211217
logger.error("Error generating PDF report without Spacers. Omitting report from final zip.", exc_info=exc)
@@ -299,6 +305,20 @@ async def finish_active_test(runner_state: RunnerState, session: AsyncSession) -
299305
readings = await get_readings(session, reading_specifiers=MANDATORY_READING_SPECIFIERS)
300306
reading_counts = await get_reading_counts_grouped_by_reading_type(session)
301307

308+
# Check if setMaxW was varied during the test - any archive entry with a different max_w_value
309+
# than the current SiteDERSetting for the same site_der_id means it changed
310+
set_max_w_varied = (
311+
await session.execute(
312+
select(ArchiveSiteDERSetting.site_der_id)
313+
.join(
314+
SiteDERSetting,
315+
(SiteDERSetting.site_der_id == ArchiveSiteDERSetting.site_der_id) # Same DER
316+
& (SiteDERSetting.max_w_value != ArchiveSiteDERSetting.max_w_value), # Same setmaxw
317+
)
318+
.limit(1)
319+
)
320+
).scalar() is not None
321+
302322
pdf_data = await generate_pdf(
303323
runner_state=runner_state,
304324
check_results=check_results,
@@ -307,6 +327,7 @@ async def finish_active_test(runner_state: RunnerState, session: AsyncSession) -
307327
sites=sites,
308328
timeline=test_timeline,
309329
errors=errors,
330+
set_max_w_varied=set_max_w_varied,
310331
)
311332
except Exception as exc:
312333
logger.error("Failed to generate PDF report", exc_info=exc)

src/cactus_runner/app/reporting.py

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
WITNESS_TEST_CLASSES: list[str] = ["DER-A", "DER-G", "DER-L"] # Classes from section 14 of sa-ts-5573-2025
7171

7272
CHART_MARGINS = dict(l=80, r=20, t=40, b=80)
73+
WARNING_BANNER_COLOR = HexColor(0xFFF3E0) # Light orange background
7374

7475

7576
class ConditionalSpacer(Spacer):
@@ -455,6 +456,29 @@ def generate_criteria_failure_table(check_results: dict[str, CheckResult], style
455456
return table
456457

457458

459+
def generate_set_max_w_warning_banner(stylesheet: StyleSheet) -> list[Flowable]:
460+
"""Generate a warning banner indicating that setMaxW was varied during the test."""
461+
warning_style = TableStyle(
462+
[
463+
("BACKGROUND", (0, 0), (-1, -1), WARNING_BANNER_COLOR),
464+
("TEXTCOLOR", (0, 0), (-1, -1), TEXT_COLOR),
465+
("ALIGN", (0, 0), (-1, -1), "CENTER"),
466+
("TOPPADDING", (0, 0), (-1, -1), 10),
467+
("BOTTOMPADDING", (0, 0), (-1, -1), 10),
468+
("LEFTPADDING", (0, 0), (-1, -1), 12),
469+
("RIGHTPADDING", (0, 0), (-1, -1), 12),
470+
("BOX", (0, 0), (-1, -1), 1, HexColor(0xE65100)),
471+
]
472+
)
473+
warning_text = Paragraph(
474+
"<font color='#E65100'><b>! Warning:</b></font> setMaxW was varied during this test. "
475+
"This unexpected behaviour may affect test assumptions and DERControls."
476+
)
477+
table = Table([[warning_text]], colWidths=[stylesheet.table_width])
478+
table.setStyle(warning_style)
479+
return [table, stylesheet.spacer]
480+
481+
458482
def generate_criteria_section(
459483
check_results: dict[str, CheckResult], requires_witness_testing: bool, stylesheet: StyleSheet
460484
) -> list[Flowable]:
@@ -649,7 +673,7 @@ def generate_site_der_rating_table(site_der_rating: SiteDERRating, stylesheet: S
649673
non_null_attributes = get_non_null_attributes(site_der_rating, attributes_to_include)
650674
null_attributes_paragraph = make_null_attributes_paragraph(attributes_to_include, non_null_attributes)
651675
table_data = generate_der_table_data(site_der_rating, non_null_attributes)
652-
table_data.insert(0, ["DER Rating", "Value"])
676+
table_data.insert(0, ["DER Capability", "Value"])
653677
column_widths = [int(fraction * stylesheet.table_width) for fraction in [0.5, 0.5]]
654678
table = Table(table_data, colWidths=column_widths)
655679
table.setStyle(stylesheet.table)
@@ -1112,11 +1136,68 @@ def generate_timeline_checklist(timeline: Timeline, runner_state: RunnerState) -
11121136
return fig_to_image(fig=fig, content_width=MAX_CONTENT_WIDTH)
11131137

11141138

1139+
def generate_step_completion_table(runner_state: RunnerState, stylesheet: StyleSheet) -> list[Flowable]:
1140+
"""Generate a table summarising the completion status and timing of each test step."""
1141+
if not (runner_state.active_test_procedure and runner_state.active_test_procedure.step_status):
1142+
return []
1143+
1144+
base_timestamp = runner_state.interaction_timestamp(interaction_type=ClientInteractionType.TEST_PROCEDURE_START)
1145+
1146+
table_style = TableStyle(
1147+
[
1148+
("ALIGN", (0, 0), (-1, -1), "LEFT"),
1149+
("TOPPADDING", (0, 0), (-1, -1), 8),
1150+
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
1151+
("ROWBACKGROUNDS", (0, 0), (-1, -1), [TABLE_ROW_COLOR, TABLE_ALT_ROW_COLOR]),
1152+
("TEXTCOLOR", (0, 0), (-1, 0), TABLE_HEADER_TEXT_COLOR),
1153+
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
1154+
("LINEBELOW", (0, 0), (-1, 0), 1, TABLE_LINE_COLOR),
1155+
("LINEBELOW", (0, -1), (-1, -1), 1, TABLE_LINE_COLOR),
1156+
("FONTNAME", (3, 0), (3, -1), "Helvetica-Bold"),
1157+
]
1158+
)
1159+
1160+
table_data: list[list] = [["Step Name", "Relative Time", "UTC Time", "Status"]]
1161+
1162+
for row_index, (step_name, step_info) in enumerate(runner_state.active_test_procedure.step_status.items()):
1163+
status = step_info.get_step_status()
1164+
1165+
if status == StepStatus.RESOLVED:
1166+
timestamp = step_info.completed_at
1167+
status_label = "Resolved"
1168+
status_color = PASS_COLOR
1169+
elif status == StepStatus.ACTIVE:
1170+
timestamp = step_info.started_at
1171+
status_label = "Active"
1172+
status_color = GENTLE_WARNING_COLOR
1173+
else:
1174+
timestamp = None
1175+
status_label = "Pending"
1176+
status_color = FAIL_COLOR
1177+
1178+
if timestamp is not None and base_timestamp is not None:
1179+
relative_seconds = (timestamp - base_timestamp).total_seconds()
1180+
relative_time = duration_to_label(int(relative_seconds))
1181+
utc_time = timestamp.strftime(stylesheet.date_format)
1182+
else:
1183+
relative_time = "-"
1184+
utc_time = "-"
1185+
1186+
table_data.append([step_name, relative_time, utc_time, status_label])
1187+
table_style.add("TEXTCOLOR", (3, row_index + 1), (3, row_index + 1), status_color)
1188+
1189+
column_widths = [int(fraction * stylesheet.table_width) for fraction in [0.35, 0.2, 0.25, 0.2]]
1190+
table = Table(table_data, colWidths=column_widths)
1191+
table.setStyle(table_style)
1192+
return [table]
1193+
1194+
11151195
def generate_timeline_section(
11161196
timeline: Timeline | None, runner_state: RunnerState, sites: Sequence[Site], stylesheet: StyleSheet
11171197
) -> list[Flowable]:
11181198
elements: list[Flowable] = []
11191199
elements.append(Paragraph("Timeline", stylesheet.heading))
1200+
11201201
if timeline is not None:
11211202
elements.append(
11221203
Paragraph(
@@ -1127,6 +1208,7 @@ def generate_timeline_section(
11271208
elements.append(generate_timeline_checklist(timeline=timeline, runner_state=runner_state))
11281209
else:
11291210
elements.append(Paragraph("Timeline chart is unavailable due to a lack of data."))
1211+
elements.extend(generate_step_completion_table(runner_state=runner_state, stylesheet=stylesheet))
11301212
elements.append(stylesheet.spacer)
11311213
return elements
11321214

@@ -1489,6 +1571,7 @@ def generate_page_elements(
14891571
sites: Sequence[Site],
14901572
timeline: Timeline | None,
14911573
stylesheet: StyleSheet,
1574+
set_max_w_varied: bool = False,
14921575
) -> list[Flowable]:
14931576
active_test_procedure = runner_state.active_test_procedure
14941577
if active_test_procedure is None:
@@ -1545,6 +1628,10 @@ def generate_page_elements(
15451628
# the appropriate client interactions SHOULD be defined in the runner state.
15461629
logger.error(f"Unable to add 'test procedure overview' to PDF report. Reason={repr(e)}")
15471630

1631+
# setMaxW Warning Banner
1632+
if set_max_w_varied:
1633+
page_elements.extend(generate_set_max_w_warning_banner(stylesheet=stylesheet))
1634+
15481635
# Criteria Section
15491636
page_elements.extend(
15501637
generate_criteria_section(
@@ -1557,16 +1644,16 @@ def generate_page_elements(
15571644
generate_timeline_section(timeline=timeline, runner_state=runner_state, sites=sites, stylesheet=stylesheet)
15581645
)
15591646

1560-
# Devices Section
1561-
page_elements.extend(generate_devices_section(sites=sites, stylesheet=stylesheet))
1562-
15631647
# Readings Section
15641648
page_elements.extend(
15651649
generate_readings_section(
15661650
runner_state=runner_state, readings=readings, reading_counts=reading_counts, stylesheet=stylesheet
15671651
)
15681652
)
15691653

1654+
# Devices Section
1655+
page_elements.extend(generate_devices_section(sites=sites, stylesheet=stylesheet))
1656+
15701657
return page_elements
15711658

15721659

@@ -1578,6 +1665,7 @@ def pdf_report_as_bytes(
15781665
sites: Sequence[Site],
15791666
timeline: Timeline | None,
15801667
no_spacers: bool = False,
1668+
set_max_w_varied: bool = False,
15811669
) -> bytes:
15821670
stylesheet = get_stylesheet()
15831671
if no_spacers:
@@ -1601,6 +1689,7 @@ def pdf_report_as_bytes(
16011689
sites=sites,
16021690
timeline=timeline,
16031691
stylesheet=stylesheet,
1692+
set_max_w_varied=set_max_w_varied,
16041693
)
16051694

16061695
test_procedure_name = runner_state.active_test_procedure.name

tests/unit/app/test_reporting.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,8 @@ def test_pdf_report_everything_set():
320320
"GET-DER-SETTINGS": StepInfo(
321321
started_at=now - timedelta(seconds=200), completed_at=now - timedelta(seconds=195)
322322
),
323-
"GET-DER-STATUS": StepInfo(started_at=now - timedelta(seconds=180), completed_at=now - timedelta(seconds=175)),
324-
"PUT-DERP": StepInfo(started_at=now - timedelta(seconds=160), completed_at=now - timedelta(seconds=155)),
323+
"GET-DER-STATUS": StepInfo(started_at=now - timedelta(seconds=180)), # ACTIVE - no completed_at
324+
"PUT-DERP": StepInfo(), # PENDING - not started
325325
}
326326

327327
active_test = active_test_procedure(
@@ -429,11 +429,12 @@ def test_pdf_report_everything_set():
429429
sites=site_list,
430430
timeline=timeline(),
431431
no_spacers=False,
432+
set_max_w_varied=True,
432433
)
433434

434435
assert len(report) > 0
435436

436-
# Optional: Save and open the PDF
437+
# # Optional: Save and open the PDF
437438
# import uuid
438439
# import tempfile
439440
# import os

0 commit comments

Comments
 (0)