Skip to content

Commit f5a745e

Browse files
authored
Merge pull request #118 from NHSDigital/PPHA-263-weight-page
PPHA-263: Metric weight page
2 parents e2ebc6b + 7c7c3b7 commit f5a745e

18 files changed

+500
-9
lines changed

lung_cancer_screening/core/form_fields.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,83 @@ def compress(self, data_list):
441441
total_inches = feet * 12 + inches
442442
return int(total_inches)
443443
return None
444+
445+
446+
class ImperialWeightWidget(widgets.MultiWidget):
447+
"""
448+
A widget that splits weight into stone and pounds inputs.
449+
"""
450+
451+
def __init__(self, attrs=None):
452+
weight_widgets = (
453+
widgets.NumberInput(attrs=attrs),
454+
widgets.NumberInput(attrs=attrs),
455+
)
456+
super().__init__(weight_widgets, attrs)
457+
458+
def decompress(self, value):
459+
"""
460+
Convert total pounds back to stone and pounds for display.
461+
"""
462+
if value:
463+
stone = int(value // 14)
464+
pounds = value % 14
465+
return [stone, pounds]
466+
return [None, None]
467+
468+
def subwidgets(self, name, value, attrs=None):
469+
"""
470+
Expose data for each subwidget, so that we can render them separately in the template.
471+
"""
472+
context = self.get_context(name, value, attrs)
473+
for subwidget in context["widget"]["subwidgets"]:
474+
yield subwidget
475+
476+
477+
class ImperialWeightField(forms.MultiValueField):
478+
"""
479+
A field that combines stone and pounds into a single weight value in total pounds.
480+
"""
481+
482+
widget = ImperialWeightWidget
483+
484+
def __init__(self, *args, **kwargs):
485+
error_messages = kwargs.get("error_messages", {})
486+
487+
stone_kwargs = {
488+
"min_value": 0,
489+
"max_value": 50,
490+
"error_messages": {
491+
'invalid': 'Stone must be in whole numbers.',
492+
'min_value': 'Weight must be between 4 stone and 50 stone.',
493+
'max_value': 'Weight must be between 4 stone and 50 stone.',
494+
**error_messages,
495+
},
496+
}
497+
pounds_kwargs = {
498+
"min_value": 0,
499+
"max_value": 13,
500+
"error_messages": {
501+
'invalid': 'Pounds must be in whole numbers.',
502+
'min_value': 'Pounds must be between 0 and 13.',
503+
'max_value': 'Pounds must be between 0 and 13.',
504+
**error_messages,
505+
},
506+
}
507+
fields = (
508+
IntegerField(**stone_kwargs),
509+
IntegerField(**pounds_kwargs),
510+
)
511+
kwargs["template_name"] = "forms/imperial-weight-input.jinja"
512+
513+
super().__init__(fields, *args, **kwargs)
514+
515+
def compress(self, data_list):
516+
"""
517+
Convert stone and pounds to total pounds.
518+
"""
519+
if data_list and all(item is not None for item in data_list):
520+
stone, pounds = data_list
521+
total_pounds = stone * 14 + pounds
522+
return int(total_pounds)
523+
return None

lung_cancer_screening/core/tests/acceptance/helpers/user_interaction_helpers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,10 @@ def fill_in_and_submit_height_imperial(page, feet, inches):
3737
page.get_by_label("Inches").fill(str(inches))
3838

3939
page.click("text=Continue")
40+
41+
def fill_in_and_submit_weight_metric(page, kilograms):
42+
expect(page.locator("h1")).to_have_text("Enter your weight")
43+
44+
page.get_by_label("Kilograms").fill(str(kilograms))
45+
46+
page.click("text=Continue")

lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
from .helpers.user_interaction_helpers import (
88
fill_in_and_submit_height_metric,
9+
fill_in_and_submit_weight_metric,
910
fill_in_and_submit_participant_id,
1011
fill_in_and_submit_smoking_eligibility,
1112
fill_in_and_submit_date_of_birth
1213
)
1314

14-
1515
class TestQuestionnaire(StaticLiveServerTestCase):
1616

1717
@classmethod
@@ -39,6 +39,7 @@ def test_cannot_change_responses_once_checked_and_submitted(self):
3939
fill_in_and_submit_smoking_eligibility(page, smoking_status)
4040
fill_in_and_submit_date_of_birth(page, age)
4141
fill_in_and_submit_height_metric(page, "170")
42+
fill_in_and_submit_weight_metric(page, "25.4")
4243

4344
page.click("text=Submit")
4445

lung_cancer_screening/core/tests/acceptance/test_questionnaire.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
fill_in_and_submit_height_metric,
1010
fill_in_and_submit_participant_id,
1111
fill_in_and_submit_smoking_eligibility,
12-
fill_in_and_submit_date_of_birth
12+
fill_in_and_submit_date_of_birth,
13+
fill_in_and_submit_weight_metric
1314
)
1415

1516
from .helpers.assertion_helpers import expect_back_link_to_have_url
@@ -36,6 +37,9 @@ def test_full_questionnaire_user_journey(self):
3637
height = "170"
3738
feet = 5
3839
inches = 7
40+
weight_metric = 70
41+
# weight_stone = 5
42+
# weight_pound = 10
3943

4044
page = self.browser.new_page()
4145
page.goto(f"{self.live_server_url}/start")
@@ -56,7 +60,7 @@ def test_full_questionnaire_user_journey(self):
5660

5761
fill_in_and_submit_height_metric(page, height)
5862

59-
expect(page).to_have_url(f"{self.live_server_url}/responses")
63+
expect(page).to_have_url(f"{self.live_server_url}/weight")
6064

6165
page.click("text=Back")
6266

@@ -66,14 +70,22 @@ def test_full_questionnaire_user_journey(self):
6670

6771
fill_in_and_submit_height_imperial(page, feet, inches)
6872

73+
expect(page).to_have_url(f"{self.live_server_url}/weight")
74+
75+
fill_in_and_submit_weight_metric(page, weight_metric)
76+
77+
expect(page).to_have_url(f"{self.live_server_url}/responses")
78+
# page.click("text=Back")
79+
# page.click("text=Switch to imperial")
80+
# fill_in_and_submit_weight_imperial(page, weight_stone, weight_pound)
6981
responses = page.locator(".responses")
7082
expect(responses).to_contain_text("Have you ever smoked? Yes, I used to smoke regularly")
7183
expect(responses).to_contain_text(
7284
age.strftime("What is your date of birth? %Y-%m-%d"))
7385
expect(responses).to_contain_text(f"What is your height? {feet} feet {inches} inches")
86+
expect(responses).to_contain_text(f"What is your weight? {weight_metric}kg")
87+
# expect(responses).to_contain_text(f"What is your weight? {weight_stone} stone {weight_pound} pound")
7488

7589
page.click("text=Submit")
7690

77-
78-
7991
expect(page).to_have_url(f"{self.live_server_url}/your-results")

lung_cancer_screening/core/tests/acceptance/test_questionnaire_validation_errors.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,22 @@ def test_height_validation_errors(self):
8383
expect(page.locator(".nhsuk-error-message")).to_contain_text(
8484
"Inches must be in whole numbers"
8585
)
86+
87+
def test_weight_validation_errors(self):
88+
participant_id = '123'
89+
90+
page = self.browser.new_page()
91+
page.goto(f"{self.live_server_url}/start")
92+
fill_in_and_submit_participant_id(page, participant_id)
93+
page.goto(f"{self.live_server_url}/weight")
94+
95+
page.click("text=Continue")
96+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
97+
"Enter your weight."
98+
)
99+
# Test weight below minimum
100+
page.get_by_label("Kilograms").fill('25.3')
101+
page.click('text=Continue')
102+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
103+
"Weight must be between 25.4kg and 317.5kg"
104+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django import forms
2+
3+
from lung_cancer_screening.core.form_fields import DecimalField
4+
from ..models.response_set import ResponseSet
5+
6+
class MetricWeightForm(forms.ModelForm):
7+
8+
def __init__(self, *args, **kwargs):
9+
self.participant = kwargs.pop('participant')
10+
super().__init__(*args, **kwargs)
11+
self.instance.participant = self.participant
12+
13+
self.fields["weight_metric"] = DecimalField(
14+
label="Kilograms",
15+
classes="nhsuk-input--width-4",
16+
required=True,
17+
error_messages={
18+
'required': 'Enter your weight.',
19+
}
20+
)
21+
def clean_weight_metric(self):
22+
return self.cleaned_data['weight_metric'] * 10
23+
24+
class Meta:
25+
model = ResponseSet
26+
fields = ['weight_metric']

lung_cancer_screening/questions/jinja2/responses.jinja

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<li>Have you ever smoked? {{ response_set.get_have_you_ever_smoked_display() }}</li>
2020
<li>What is your date of birth? {{ response_set.date_of_birth }}</li>
2121
<li>What is your height? {{ response_set.formatted_height }}</li>
22+
<li>What is your weight? {{ response_set.formatted_weight }}</li>
2223
</ul>
2324

2425
<form action="{{ request.path }}" method="post">
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{% extends 'layout.jinja' %}
2+
{% from 'nhsuk/components/button/macro.jinja' import button %}
3+
{% from 'nhsuk/components/back-link/macro.jinja' import backLink %}
4+
{% from 'nhsuk/components/fieldset/macro.jinja' import fieldset %}
5+
6+
{% block beforeContent %}
7+
{{
8+
backLink({
9+
"href": url("questions:height"),
10+
"text": "Back"
11+
})
12+
}}
13+
{% endblock beforeContent %}
14+
15+
{% block page_content %}
16+
<div class="nhsuk-grid-row">
17+
<div class="nhsuk-grid-column-two-thirds">
18+
<form action="{{request.path}}" method="POST" novalidate>
19+
{{ csrf_input }}
20+
<h1 class="nhsuk-heading-l">Enter your weight</h1>
21+
22+
<p>An accurate measurement is important.
23+
24+
<p>If you have digital scales, use these to check your weight. Some pharmacies and gyms have scales where you can check for free.
25+
26+
{% call fieldset({
27+
"legend": {
28+
"text": "What is your weight",
29+
"classes": "nhsuk-label--m"
30+
}
31+
}) %}
32+
33+
{{form.weight_metric.as_field_group()}}
34+
35+
{% endcall %}
36+
37+
{{ button({
38+
"text": "Continue"
39+
}) }}
40+
</form>
41+
</div>
42+
</div>
43+
{% endblock %}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.2.7 on 2025-10-21 13:13
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('questions', '0010_responseset_height_imperial_alter_responseset_height_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='responseset',
16+
name='weight_metric',
17+
field=models.PositiveIntegerField(blank=True, null=True),
18+
),
19+
migrations.AlterField(
20+
model_name='responseset',
21+
name='height_imperial',
22+
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(55, message='Height must be between 4 feet 7 inches and 8 feet'), django.core.validators.MaxValueValidator(96, message='Height must be between 4 feet 7 inches and 8 feet')]),
23+
),
24+
]

lung_cancer_screening/questions/models/response_set.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ class ResponseSet(BaseModel):
3939
MAX_HEIGHT_IMPERIAL, message="Height must be between 4 feet 7 inches and 8 feet"),
4040
])
4141

42+
MIN_WEIGHT_METRIC = 254
43+
MAX_WEIGHT_METRIC = 3175
44+
45+
weight_metric = models.PositiveIntegerField(null=True, blank=True, validators=[
46+
MinValueValidator(MIN_WEIGHT_METRIC, message="Weight must be between 25.4kg and 317.5kg"),
47+
MaxValueValidator(MAX_WEIGHT_METRIC, message="Weight must be between 25.4kg and 317.5kg"),
48+
])
49+
4250
submitted_at = models.DateTimeField(null=True, blank=True)
4351

4452
class Meta:
@@ -70,3 +78,12 @@ def formatted_height(self):
7078
value = Decimal(self.height_imperial)
7179
return f"{value // 12} feet {value % 12} inches"
7280

81+
@property
82+
def formatted_weight(self):
83+
if self.weight_metric:
84+
return f"{Decimal(self.weight_metric) / 10}kg"
85+
# elif self.height_imperial:
86+
# value = Decimal(self.weight_imperial)
87+
# return f"{value // 12} feet {value % 12} inches"
88+
89+

0 commit comments

Comments
 (0)