Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ __pycache__
lung_cancer_screening/assets/compiled/*
!lung_cancer_screening/assets/compiled/.gitkeep
.DS_Store
.venv
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# This file is for you! Please, updated to the versions agreed by your team.

python 3.13.8
terraform 1.7.0
pre-commit 4.3.0
vale 3.6.0
Expand Down
20 changes: 19 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,23 @@
"MD013": false,
"MD024": { "siblings_only": true },
"MD033": false
}
},
"cSpell.words": [
"dateutil",
"endcall",
"endset",
"fieldset",
"gunicorn",
"jinja",
"makemigrations",
"nhsuk",
"novalidate",
"psycopg",
"relativedelta",
"responseset",
"stylesheet",
"toplevel",
"unsubmitted",
"whitenoise"
]
}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM node:24.10.0-alpine3.21 AS asset_builder
WORKDIR /app

COPY package.json package-lock.json rollup.config.js ./
COPY lung_cancer_screening ./lung_cancer_screening
COPY . .
RUN npm ci
RUN npm run compile

Expand Down
5 changes: 4 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services:
- .env
depends_on:
- db
- asset_builder
volumes:
- ./:/app
- venv:/app/.venv
Expand All @@ -26,7 +27,8 @@ services:
target: asset_builder
command: npm run watch
volumes:
- ./lung_cancer_screening/assets/compiled:/app/lung_cancer_screening/assets/compiled
- ./:/app
- node_modules:/app/node_modules

db:
image: postgres:15-alpine
Expand All @@ -48,3 +50,4 @@ volumes:
redis_data:
venv:
playwright_browsers:
node_modules:
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.multi-field-input__item {
display: inline-block;
margin-bottom: 0;
margin-right: 24px;
}
2 changes: 1 addition & 1 deletion lung_cancer_screening/assets/sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
@forward "nhsuk-frontend/dist/nhsuk";

// Components that are not in the NHS.UK frontend library
// @import "components/button";
@forward "components/multi_field_input";
101 changes: 100 additions & 1 deletion lung_cancer_screening/core/form_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,33 @@ def widget_attrs(self, widget):

return attrs

class DecimalField(forms.DecimalField):
def __init__(
self,
*args,
hint=None,
label_classes=None,
classes=None,
**kwargs,
):
kwargs["template_name"] = "forms/input.jinja"

self.hint = hint
self.classes = classes
self.label_classes = label_classes

super().__init__(*args, **kwargs)

def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)

# Don't use min/max/step attributes.
attrs.pop("min", None)
attrs.pop("max", None)
attrs.pop("step", None)

return attrs


class BoundChoiceField(forms.BoundField):
"""
Expand All @@ -225,7 +252,7 @@ def __init__(self, form: forms.Form, field: "ChoiceField", name: str):
def add_conditional_html(self, value, html):
if isinstance(self.field.widget, widgets.Select):
raise ValueError(
"select comonent does not support conditional fields")
"select component does not support conditional fields")

self._conditional_html[value] = html

Expand Down Expand Up @@ -342,3 +369,75 @@ def __init__(
self.label_classes = label_classes

super().__init__(*args, **kwargs)


class ImperialHeightWidget(widgets.MultiWidget):
"""
A widget that splits height into feet and inches inputs.
"""

def __init__(self, attrs=None):
height_widgets = (
widgets.NumberInput(attrs=attrs),
widgets.NumberInput(attrs=attrs),
)
super().__init__(height_widgets, attrs)

def decompress(self, value):
"""
Convert total inches back to feet and inches for display.
"""
if value:
feet = int(value // 12)
inches = value % 12
return [feet, inches]
return [None, None]

def subwidgets(self, name, value, attrs=None):
"""
Expose data for each subwidget, so that we can render them separately in the template.
"""
context = self.get_context(name, value, attrs)
for subwidget in context["widget"]["subwidgets"]:
yield subwidget


class ImperialHeightField(forms.MultiValueField):
"""
A field that combines feet and inches into a single height value in cm.
"""

widget = ImperialHeightWidget

def __init__(self, *args, **kwargs):
error_messages = kwargs.get("error_messages", {})

feet_kwargs = {
"error_messages": {
'invalid': 'Feet must be in whole numbers.',
**error_messages,
},
}
inches_kwargs = {
"error_messages": {
'invalid': 'Inches must be in whole numbers.',
**error_messages,
},
}
fields = (
IntegerField(**feet_kwargs),
IntegerField(**inches_kwargs),
)
kwargs["template_name"] = "forms/imperial-height-input.jinja"

super().__init__(fields, *args, **kwargs)

def compress(self, data_list):
"""
Convert feet and inches to total inches.
"""
if data_list and all(data_list):
feet, inches = data_list
total_inches = feet * 12 + inches
return int(total_inches)
return None
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% from 'nhsuk/components/input/macro.jinja' import input %}
{% from 'nhsuk/components/error-message/macro.jinja' import errorMessage %}

{% if field.errors | length > 0 %}
{% set error_message = field.errors | first %}
{% endif %}
{% set unbound_field = field.field %}
{% set hint = unbound_field.hint %}
{% set form_group_error_classes = ' nhsuk-form-group--error' if error_message else '' %}
{% set field_error_classes = ' nhsuk-input--error' if error_message else '' %}

<div id="{{ field.html_name }}" class="multi-field-input nhsuk-form-group{{ form_group_error_classes }}">
{% if error_message %}
{{ errorMessage({
"id": field.auto_id,
"text": error_message
}) }}
{% endif %}

<div class="multi-field-input__item">
{{ input({
"label": {
"text": "Feet",
"classes": "nhsuk-fieldset__legend--m",
"isPageHeading": false
},
"hint": {
"text": unbound_field.hint
} if unbound_field.hint,
"id": field.auto_id + "_0",
"name": field.html_name + "_0",
"value": field.subwidgets.0.data.value,
"classes": "nhsuk-input--width-2" + field_error_classes,
"type": "number"
}) }}
</div>

<div class="multi-field-input__item">
{{ input({
"label": {
"text": "Inches",
"classes": "nhsuk-fieldset__legend--m",
"isPageHeading": false
},
"id": field.auto_id + "_1",
"name": field.html_name + "_1",
"value": field.subwidgets.1.data.value,
"classes": "nhsuk-input--width-2" + field_error_classes,
"type": "number"
}) }}
</div>
</div>
34 changes: 34 additions & 0 deletions lung_cancer_screening/core/jinja2/forms/input.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{% from "nhsuk/components/input/macro.jinja" import input %}
{% set unbound_field = field.field %}
{% set widget = unbound_field.widget %}
{% if unbound_field.visually_hidden_label_suffix %}
{% set label_html %}
{{ field.label }}<span class="nhsuk-u-visually-hidden">: {{ unbound_field.visually_hidden_label_suffix }}</span>
{% endset %}
{% endif %}
{% set input_params = {
"label": {
"text": field.label,
"html": label_html if label_html,
"classes": unbound_field.label_classes if unbound_field.label_classes
},
"hint": {
"text": unbound_field.hint
} if unbound_field.hint,
"id": field.auto_id,
"name": field.html_name,
"value": field.value() or "",
"classes": unbound_field.classes if unbound_field.classes,
"attributes": widget.attrs,
"type": widget.input_type
} %}
{% if field.errors %}
{% set error_params = {
"errorMessage": {
"text": field.errors | first
}
} %}
{% set input_params = dict(input_params, **error_params) %}
{% endif %}

{{ input(input_params) }}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def fill_in_and_submit_participant_id(page, participant_id):
page.click('text=Start now')


def fill_in_and_submit_smoking_elligibility(page, smoking_status):
def fill_in_and_submit_smoking_eligibility(page, smoking_status):
expect(page.locator("legend")).to_have_text(
"Have you ever smoked?")

Expand All @@ -22,3 +22,18 @@ def fill_in_and_submit_date_of_birth(page, age):
page.get_by_label("Year").fill(str(age.year))

page.click("text=Continue")

def fill_in_and_submit_height_metric(page, height):
expect(page.locator("h1")).to_have_text("What is your height?")

page.get_by_label("Centimetre").fill(str(height))

page.click("text=Continue")

def fill_in_and_submit_height_imperial(page, feet, inches):
expect(page.locator("h1")).to_have_text("What is your height?")

page.get_by_label("Feet").fill(str(feet))
page.get_by_label("Inches").fill(str(inches))

page.click("text=Continue")
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from dateutil.relativedelta import relativedelta

from .helpers.user_interaction_helpers import (
fill_in_and_submit_height_metric,
fill_in_and_submit_participant_id,
fill_in_and_submit_smoking_elligibility,
fill_in_and_submit_smoking_eligibility,
fill_in_and_submit_date_of_birth
)

Expand Down Expand Up @@ -35,8 +36,9 @@ def test_cannot_change_responses_once_checked_and_submitted(self):
page.goto(f"{self.live_server_url}/start")

fill_in_and_submit_participant_id(page, participant_id)
fill_in_and_submit_smoking_elligibility(page, smoking_status)
fill_in_and_submit_smoking_eligibility(page, smoking_status)
fill_in_and_submit_date_of_birth(page, age)
fill_in_and_submit_height_metric(page, "170")

page.click("text=Submit")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from .helpers.user_interaction_helpers import (
fill_in_and_submit_participant_id,
fill_in_and_submit_smoking_elligibility
fill_in_and_submit_smoking_eligibility
)


Expand All @@ -31,7 +31,7 @@ def test_participant_not_smoker(self):
page.goto(f"{self.live_server_url}/start")

fill_in_and_submit_participant_id(page, participant_id)
fill_in_and_submit_smoking_elligibility(page, 'No, I have never smoked')
fill_in_and_submit_smoking_eligibility(page, 'No, I have never smoked')

expect(page).to_have_url(f"{self.live_server_url}/non-smoker-exit")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from .helpers.user_interaction_helpers import (
fill_in_and_submit_participant_id,
fill_in_and_submit_smoking_elligibility,
fill_in_and_submit_smoking_eligibility,
fill_in_and_submit_date_of_birth
)

Expand All @@ -33,7 +33,7 @@ def test_participant_out_of_age_range(self):
page.goto(f"{self.live_server_url}/start")

fill_in_and_submit_participant_id(page, participant_id)
fill_in_and_submit_smoking_elligibility(page, 'Yes, I used to smoke regularly')
fill_in_and_submit_smoking_eligibility(page, 'Yes, I used to smoke regularly')

age = datetime.now() - relativedelta(years=20)
fill_in_and_submit_date_of_birth(page, age)
Expand Down
Loading
Loading