Skip to content

Commit f205d75

Browse files
committed
PPHA-262: Add imperial height form
1 parent 1bb1a99 commit f205d75

17 files changed

+370
-41
lines changed

lung_cancer_screening/core/form_fields.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,61 @@ def __init__(
369369
self.label_classes = label_classes
370370

371371
super().__init__(*args, **kwargs)
372+
373+
374+
class ImperialHeightWidget(widgets.MultiWidget):
375+
"""
376+
A widget that splits height into feet and inches inputs.
377+
"""
378+
379+
def __init__(self, attrs=None):
380+
height_widgets = (
381+
widgets.NumberInput(attrs=attrs),
382+
widgets.NumberInput(attrs=attrs),
383+
)
384+
super().__init__(height_widgets, attrs)
385+
386+
def decompress(self, value):
387+
"""
388+
Convert total inches back to feet and inches for display.
389+
"""
390+
if value:
391+
feet = int(value // 12)
392+
inches = value % 12
393+
return [feet, inches]
394+
return [None, None]
395+
396+
def subwidgets(self, name, value, attrs=None):
397+
"""
398+
Expose data for each subwidget, so that we can render them separately in the template.
399+
"""
400+
context = self.get_context(name, value, attrs)
401+
for subwidget in context["widget"]["subwidgets"]:
402+
yield subwidget
403+
404+
405+
class ImperialHeightField(forms.MultiValueField):
406+
"""
407+
A field that combines feet and inches into a single height value in cm.
408+
"""
409+
410+
widget = ImperialHeightWidget
411+
412+
def __init__(self, *args, **kwargs):
413+
fields = (
414+
IntegerField(label="Feet"),
415+
IntegerField(label="Inches"),
416+
)
417+
kwargs["template_name"] = "forms/imperial-height-input.jinja"
418+
419+
super().__init__(fields, *args, **kwargs)
420+
421+
def compress(self, data_list):
422+
"""
423+
Convert feet and inches to total inches.
424+
"""
425+
if data_list and all(data_list):
426+
feet, inches = data_list
427+
total_inches = feet * 12 + inches
428+
return int(total_inches)
429+
return None
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% from 'nhsuk/components/input/macro.jinja' import input %}
2+
3+
{% if field.errors %}
4+
{% set error_message = {"text": field.errors | first} %}
5+
{% endif %}
6+
{% set unbound_field = field.field %}
7+
{% set hint = unbound_field.hint %}
8+
9+
{{ input({
10+
"label": {
11+
"text": "Feet",
12+
"classes": "nhsuk-fieldset__legend--m",
13+
"isPageHeading": false
14+
},
15+
"hint": {
16+
"text": unbound_field.hint
17+
} if unbound_field.hint,
18+
"errorMessage": error_message,
19+
"id": field.auto_id,
20+
"name": field.html_name + "_0",
21+
"value": field.subwidgets.0.data.value,
22+
"classes": "nhsuk-input--width-2",
23+
"type": "number"
24+
}) }}
25+
26+
{{ input({
27+
"label": {
28+
"text": "Inches",
29+
"classes": "nhsuk-fieldset__legend--m",
30+
"isPageHeading": false
31+
},
32+
"errorMessage": error_message,
33+
"id": field.auto_id + "_1",
34+
"name": field.html_name + "_1",
35+
"value": field.subwidgets.1.data.value,
36+
"classes": "nhsuk-input--width-2",
37+
"type": "number"
38+
}) }}

lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py

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

77
from .helpers.user_interaction_helpers import (
88
fill_in_and_submit_height_metric,
9-
fill_in_and_submit_height_imperial,
109
fill_in_and_submit_participant_id,
1110
fill_in_and_submit_smoking_eligibility,
1211
fill_in_and_submit_date_of_birth

lung_cancer_screening/core/tests/acceptance/test_questionnaire.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_full_questionnaire_user_journey(self):
7070
expect(responses).to_contain_text("Have you ever smoked? Yes, I used to smoke regularly")
7171
expect(responses).to_contain_text(
7272
age.strftime("What is your date of birth? %Y-%m-%d"))
73-
expect(responses).to_contain_text(f"What is your height? {height}")
73+
expect(responses).to_contain_text(f"What is your height? {feet} feet {inches} inches")
7474

7575
page.click("text=Submit")
7676

lung_cancer_screening/core/tests/acceptance/test_questionnaire_validation_errors.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,26 @@ def test_date_of_birth_validation_errors(self):
3737
expect(page.locator(".nhsuk-error-message")).to_contain_text(
3838
"Enter your date of birth."
3939
)
40+
41+
def test_height_validation_errors(self):
42+
participant_id = '123'
43+
44+
page = self.browser.new_page()
45+
page.goto(f"{self.live_server_url}/start")
46+
fill_in_and_submit_participant_id(page, participant_id)
47+
page.goto(f"{self.live_server_url}/height")
48+
49+
page.click("text=Continue")
50+
51+
expect(page.locator(".nhsuk-error-message")).to_contain_text(
52+
"Enter your height."
53+
)
54+
55+
page.click("text=Switch to imperial")
56+
57+
page.click("text=Continue")
58+
59+
for error in page.locator(".nhsuk-error-message"):
60+
expect(error).to_contain_text(
61+
"Enter your height."
62+
)
Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
1-
import decimal
21
from django import forms
32

4-
from lung_cancer_screening.core.form_fields import DecimalField
3+
from lung_cancer_screening.core.form_fields import ImperialHeightField
54
from ..models.response_set import ResponseSet
65

7-
class MetricHeightForm(forms.ModelForm):
6+
class ImperialHeightForm(forms.ModelForm):
87

98
def __init__(self, *args, **kwargs):
109
self.participant = kwargs.pop('participant')
1110
super().__init__(*args, **kwargs)
1211
self.instance.participant = self.participant
1312

14-
self.fields["height_feet"] = DecimalField(
15-
label="Feet",
16-
classes="nhsuk-input--width-4",
17-
)
18-
19-
self.fields["height_inches"] = DecimalField(
20-
label="Inches",
21-
classes="nhsuk-input--width-4",
13+
self.fields["height_imperial"] = ImperialHeightField(
14+
label="Height",
15+
required=True
2216
)
2317

2418
def clean_height(self):
25-
data = self.cleaned_data['height_feet']*12 + self.cleaned_data['height_inches']
26-
return data*2.54
19+
return None
2720

2821
class Meta:
2922
model = ResponseSet
30-
fields = ['height']
23+
fields = ['height_imperial', 'height']

lung_cancer_screening/questions/forms/metric_height_form.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import decimal
21
from django import forms
32

43
from lung_cancer_screening.core.form_fields import DecimalField
@@ -14,11 +13,17 @@ def __init__(self, *args, **kwargs):
1413
self.fields["height"] = DecimalField(
1514
label="Centimetres",
1615
classes="nhsuk-input--width-4",
16+
error_messages={
17+
'required': 'Enter your height.',
18+
}
1719
)
1820

1921
def clean_height(self):
2022
return self.cleaned_data['height'] * 10
2123

24+
def clean_height_imperial(self):
25+
return None
26+
2227
class Meta:
2328
model = ResponseSet
24-
fields = ['height']
29+
fields = ['height', 'height_imperial']

lung_cancer_screening/questions/jinja2/height.jinja

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
{% block page_content %}
1616
<div class="nhsuk-grid-row">
1717
<div class="nhsuk-grid-column-two-thirds">
18-
<form action="{{ request.path }}" method="POST" novalidate>
18+
{% if unit %}
19+
{% set action_url = request.path + '?unit=' + unit %}
20+
{% else %}
21+
{% set action_url = request.path %}
22+
{% endif %}
23+
<form action="{{ action_url }}" method="POST" novalidate>
1924
{{ csrf_input }}
2025
<h1 class="nhsuk-heading-l">What is your height?</h1>
2126
<p>An accurate measurement is important.
@@ -28,9 +33,13 @@
2833
}
2934
}) %}
3035

31-
{{ form.height.as_field_group() }}
32-
33-
<a href="?unit={{ switch_to_unit }}">Switch to {{ switch_to_unit }}</a>
36+
{% if unit == "imperial" %}
37+
{{ form.height_imperial.as_field_group() }}
38+
{% else %}
39+
{{ form.height.as_field_group() }}
40+
{% endif %}
41+
42+
<a href="?unit={{ switch_to_unit }}">Switch to {{ switch_to_unit }}</a>
3443

3544
{% endcall %}
3645

@@ -40,4 +49,4 @@
4049
</form>
4150
</div>
4251
</div>
43-
{% endblock %}
52+
{% endblock %}

lung_cancer_screening/questions/jinja2/responses.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<ul class="responses">
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>
21-
<li>What is your height? {{ response_set.height // 10 if response_set.height else ""}}</li>
21+
<li>What is your height? {{ response_set.formatted_height }}</li>
2222
</ul>
2323

2424
<form action="{{ request.path }}" method="post">
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.2.7 on 2025-10-17 13:52
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', '0009_responseset_height_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='responseset',
16+
name='height_imperial',
17+
field=models.PositiveIntegerField(blank=True, null=True),
18+
),
19+
migrations.AlterField(
20+
model_name='responseset',
21+
name='height',
22+
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1397, message='Height must be between 139.7cm and 243.8 cm'), django.core.validators.MaxValueValidator(2438, message='Height must be between 139.7cm and 243.8 cm')]),
23+
),
24+
migrations.AddConstraint(
25+
model_name='responseset',
26+
constraint=models.UniqueConstraint(condition=models.Q(('submitted_at__isnull', True)), fields=('participant',), name='unique_unsubmitted_response_per_participant', violation_error_message='An unsubmitted response set already exists for this participant'),
27+
),
28+
]

0 commit comments

Comments
 (0)