Skip to content

Commit 856430c

Browse files
Add app_dashboard_card component
1 parent 2d5e633 commit 856430c

File tree

5 files changed

+203
-66
lines changed

5 files changed

+203
-66
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% from "card/macro.jinja" import card %}
2+
3+
{% macro app_dashboard_card(params) %}
4+
5+
<div class="{{ params.classes }} nhsuk-card-group__item app-card-group__item">
6+
{{ card({
7+
"classes": "app-card " ~ params.card_classes,
8+
"heading": params.heading | escape,
9+
"headingClasses": "nhsuk-heading-xs",
10+
"headingLevel": 3,
11+
"descriptionHtml": "
12+
<p class='nhsuk-card__description'>" ~
13+
params.description | escape ~
14+
"<span class='nhsuk-card__caption'>" ~
15+
params.caption | escape ~
16+
"</span>" ~
17+
"</p>",
18+
}) }}
19+
</div>
20+
21+
{% endmacro %}

mavis/reporting/templates/vaccinations.jinja

Lines changed: 46 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{% from "components/heading.jinja" import heading %}
33
{% from "card/macro.jinja" import card %}
44
{% from "tables/macro.jinja" import table %}
5+
{% from "components/app_dashboard_card.jinja" import app_dashboard_card %}
56

67
{% extends 'layouts/dashboard.jinja' %}
78

@@ -22,36 +23,29 @@
2223

2324
<div id="dashboard" class="app-grid-column-content">
2425
<div class="nhsuk-card-group nhsuk-grid-row app-card-group">
25-
<div class="nhsuk-grid-column-one-third nhsuk-card-group__item app-card-group__item">
26-
{% set child_or_children = "child" if data.cohort == 1 else "children" %}
27-
{{ card({
28-
"classes": "app-card app-card--blue",
29-
"heading": "Cohort",
30-
"headingClasses": "nhsuk-heading-xs",
31-
"headingLevel": 3,
32-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ data.cohort | thousands ~ "<span class='nhsuk-card__caption'>" ~ child_or_children ~ " </span></p>",
33-
}) }}
34-
</div>
26+
{{ app_dashboard_card({
27+
"classes": "nhsuk-grid-column-one-third",
28+
"card_classes": "app-card--blue",
29+
"heading": "Cohort",
30+
"description": data.cohort | thousands,
31+
"caption": "child" if data.cohort == 1 else "children",
32+
}) }}
3533

36-
<div class="nhsuk-grid-column-one-third nhsuk-card-group__item app-card-group__item">
37-
{{ card({
38-
"classes": "app-card app-card--red",
39-
"heading": "Not vaccinated",
40-
"headingClasses": "nhsuk-heading-xs",
41-
"headingLevel": 3,
42-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ data["not_vaccinated_percentage"] | percentage ~ "<span class='nhsuk-card__caption'>" ~ data["not_vaccinated"] | format_child_count ~ "</span></p>",
43-
}) }}
44-
</div>
34+
{{ app_dashboard_card({
35+
"classes": "nhsuk-grid-column-one-third",
36+
"card_classes": "app-card--red",
37+
"heading": "Not vaccinated",
38+
"description": data["not_vaccinated_percentage"] | percentage,
39+
"caption": data["not_vaccinated"] | format_child_count,
40+
}) }}
4541

46-
<div class="nhsuk-grid-column-one-third nhsuk-card-group__item app-card-group__item">
47-
{{ card({
48-
"classes": "app-card app-card--green",
49-
"heading": "Vaccinated",
50-
"headingClasses": "nhsuk-heading-xs",
51-
"headingLevel": 3,
52-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ data["vaccinated_percentage"] | percentage ~ "<span class='nhsuk-card__caption'>" ~ data["vaccinated"] | format_child_count ~ "</span></p>",
53-
}) }}
54-
</div>
42+
{{ app_dashboard_card({
43+
"classes": "nhsuk-grid-column-one-third",
44+
"card_classes": "app-card--green",
45+
"heading": "Vaccinated",
46+
"description": data["vaccinated_percentage"] | percentage,
47+
"caption": data["vaccinated"] | format_child_count,
48+
}) }}
5549
</div>
5650

5751
{{ heading({
@@ -61,35 +55,26 @@
6155
}) }}
6256

6357
<div class="nhsuk-grid-row app-card-group nhsuk-card-group">
64-
<div class="nhsuk-grid-column-one-third nhsuk-card-group__item app-card-group__item">
65-
{{ card({
66-
"classes": "app-card",
67-
"heading": "Vaccinated by " ~ team.name,
68-
"headingClasses": "nhsuk-heading-xs",
69-
"headingLevel": 3,
70-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ data["vaccinated_by_sais_percentage"] | percentage ~ "<span class='nhsuk-card__caption'>" ~ data["vaccinated_by_sais"] | format_child_count ~ "</span></p>",
71-
}) }}
72-
</div>
58+
{{ app_dashboard_card({
59+
"classes": "nhsuk-grid-column-one-third",
60+
"heading": "Vaccinated by " ~ team.name,
61+
"description": data["vaccinated_by_sais_percentage"] | percentage,
62+
"caption": data["vaccinated_by_sais"] | format_child_count,
63+
}) }}
7364

74-
<div class="nhsuk-grid-column-one-third nhsuk-card-group__item app-card-group__item">
75-
{{ card({
76-
"classes": "app-card",
77-
"heading": "Vaccinated elsewhere (self-reported)",
78-
"headingClasses": "nhsuk-heading-xs",
79-
"headingLevel": 3,
80-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ data["vaccinated_elsewhere_declared_percentage"] | percentage ~ "<span class='nhsuk-card__caption'>" ~ data["vaccinated_elsewhere_declared"] | format_child_count ~ "</span></p>",
81-
}) }}
82-
</div>
65+
{{ app_dashboard_card({
66+
"classes": "nhsuk-grid-column-one-third",
67+
"heading": "Vaccinated elsewhere (self-reported)",
68+
"description": data["vaccinated_elsewhere_declared_percentage"] | percentage,
69+
"caption": data["vaccinated_elsewhere_declared"] | format_child_count,
70+
}) }}
8371

84-
<div class="nhsuk-grid-column-one-third nhsuk-card-group__item app-card-group__item">
85-
{{ card({
86-
"classes": "app-card",
87-
"heading": "Vaccinated elsewhere (confirmed)",
88-
"headingClasses": "nhsuk-heading-xs",
89-
"headingLevel": 3,
90-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ data["vaccinated_elsewhere_recorded_percentage"] | percentage ~ "<span class='nhsuk-card__caption'>" ~ data["vaccinated_elsewhere_recorded"] | format_child_count ~ "</span></p>",
91-
}) }}
92-
</div>
72+
{{ app_dashboard_card({
73+
"classes": "nhsuk-grid-column-one-third",
74+
"heading": "Vaccinated elsewhere (confirmed)",
75+
"description": data["vaccinated_elsewhere_recorded_percentage"] | percentage,
76+
"caption": data["vaccinated_elsewhere_recorded"] | format_child_count,
77+
}) }}
9378
</div>
9479

9580
{% if current_filters.programme != "flu" %}
@@ -100,17 +85,12 @@
10085
}) }}
10186

10287
<div class="nhsuk-grid-row app-card-group nhsuk-card-group">
103-
<div class="nhsuk-grid-column-full nhsuk-card-group__item app-card-group__item">
104-
{% set percentage = data["vaccinated_previously_percentage"] | percentage %}
105-
{% set child_count = data["vaccinated_previously"] | format_child_count %}
106-
{{ card({
107-
"classes": "app-card",
108-
"heading": "Vaccinated in all settings",
109-
"headingClasses": "nhsuk-heading-xs",
110-
"headingLevel": 3,
111-
"descriptionHtml": "<p class='nhsuk-card__description'>" ~ percentage ~ "<span class='nhsuk-card__caption'>" ~ child_count ~ "</span></p>",
112-
}) }}
113-
</div>
88+
{{ app_dashboard_card({
89+
"classes": "nhsuk-grid-column-full",
90+
"heading": "Vaccinated in all settings",
91+
"description": data["vaccinated_previously_percentage"] | percentage,
92+
"caption": data["vaccinated_previously"] | format_child_count,
93+
}) }}
11494
</div>
11595
{% endif %}
11696

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dev = [
2424
"boto3==1.40.64",
2525
"pyyaml>=6.0.2",
2626
"pyright>=1.1.405",
27+
"beautifulsoup4>=4.14.2",
2728
]
2829

2930
[tool.ruff]
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
from bs4 import BeautifulSoup
3+
4+
from mavis.reporting import create_app
5+
6+
7+
def card_params():
8+
return {
9+
"classes": "nhsuk-grid-column-one-third",
10+
"card_classes": "app-card--blue",
11+
"heading": "Test Heading",
12+
"description": "123",
13+
"caption": "children",
14+
}
15+
16+
17+
def card_params_with_html():
18+
return {
19+
"classes": "nhsuk-grid-column-one-third",
20+
"card_classes": "app-card--blue",
21+
"heading": "<strong>Test Heading</strong>",
22+
"description": "<p>123</p>",
23+
"caption": "<strong>children</strong>",
24+
}
25+
26+
27+
def card_soup(render_macro, params):
28+
"""Get the BeautifulSoup object for a card."""
29+
html = render_macro(params)
30+
return BeautifulSoup(html, "html.parser")
31+
32+
33+
@pytest.fixture()
34+
def app():
35+
app = create_app()
36+
app.config.update(
37+
{
38+
"MAVIS_ROOT_URL": "http://mavis.test/",
39+
"TESTING": True,
40+
"CLIENT_ID": "test_client_id",
41+
"CLIENT_SECRET": "test_client_secret",
42+
}
43+
)
44+
return app
45+
46+
47+
@pytest.fixture()
48+
def render_macro(app):
49+
"""Helper fixture to render the app_dashboard_card macro."""
50+
51+
def _render(params):
52+
with app.app_context():
53+
template = app.jinja_env.get_template("components/app_dashboard_card.jinja")
54+
macro = template.module.app_dashboard_card
55+
return macro(params)
56+
57+
return _render
58+
59+
60+
class TestAppDashboardCard:
61+
def test_card_classes_are_correct(self, render_macro):
62+
"""Test that the card classes are correct."""
63+
64+
soup = card_soup(render_macro, card_params())
65+
66+
outer_div = soup.find("div", class_="nhsuk-card-group__item")
67+
assert outer_div is not None
68+
outer_classes = outer_div.get("class") or []
69+
assert "nhsuk-grid-column-one-third" in outer_classes
70+
assert "app-card-group__item" in outer_classes
71+
72+
card = outer_div.find("div", class_="app-card")
73+
assert card is not None
74+
card_classes = card.get("class") or []
75+
assert "app-card--blue" in card_classes
76+
77+
def test_heading_is_correct(self, render_macro):
78+
"""Test that the heading is rendered with correct classes and level."""
79+
80+
soup = card_soup(render_macro, card_params())
81+
heading = soup.find("h3", class_="nhsuk-heading-xs")
82+
assert heading is not None
83+
assert heading.text.strip() == "Test Heading"
84+
85+
def test_description_and_caption_are_correct(self, render_macro):
86+
"""Test that description and caption are rendered correctly."""
87+
88+
soup = card_soup(render_macro, card_params())
89+
description_p = soup.find("p", class_="nhsuk-card__description")
90+
assert description_p is not None
91+
assert description_p.contents[0].text == "123"
92+
93+
caption_span = description_p.find("span", class_="nhsuk-card__caption")
94+
assert caption_span is not None
95+
assert caption_span.text.strip() == "children"
96+
97+
def test_html_is_escaped(self, render_macro):
98+
"""Test that HTML is escaped."""
99+
100+
soup = card_soup(render_macro, card_params_with_html())
101+
heading = soup.find("h3", class_="nhsuk-heading-xs")
102+
assert heading is not None
103+
assert heading.text.strip() == "<strong>Test Heading</strong>"
104+
105+
description_p = soup.find("p", class_="nhsuk-card__description")
106+
assert description_p is not None
107+
assert description_p.contents[0].text == "<p>123</p>"
108+
109+
caption_span = description_p.find("span", class_="nhsuk-card__caption")
110+
assert caption_span is not None
111+
assert caption_span.text.strip() == "<strong>children</strong>"

uv.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)