Skip to content

Commit fbf5026

Browse files
committed
Move HeightResponse to its own model
1 parent 5ffffec commit fbf5026

File tree

11 files changed

+253
-103
lines changed

11 files changed

+253
-103
lines changed
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from django import forms
22

33
from ...nhsuk_forms.imperial_height_field import ImperialHeightField
4-
from ..models.response_set import ResponseSet
4+
from ..models.height_response import HeightResponse
55

66

77
class ImperialHeightForm(forms.ModelForm):
88

99
def __init__(self, *args, **kwargs):
1010
super().__init__(*args, **kwargs)
1111

12-
self.fields["height_imperial"] = ImperialHeightField(
12+
self.fields["imperial"] = ImperialHeightField(
1313
label="Height",
1414
required=True,
1515
require_all_fields=False,
@@ -18,9 +18,9 @@ def __init__(self, *args, **kwargs):
1818
}
1919
)
2020

21-
def clean_height(self):
21+
def clean_metric(self):
2222
return None
2323

2424
class Meta:
25-
model = ResponseSet
26-
fields = ['height_imperial', 'height_metric']
25+
model = HeightResponse
26+
fields = ['imperial', 'metric']
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django import forms
22

33
from ...nhsuk_forms.decimal_field import DecimalField
4-
from ..models.response_set import ResponseSet
4+
from ..models.height_response import HeightResponse
55

66

77
class MetricHeightForm(forms.ModelForm):
@@ -10,10 +10,10 @@ def __init__(self, *args, **kwargs):
1010
super().__init__(*args, **kwargs)
1111

1212
# Convert mm to cm for display
13-
if self.instance and self.instance.height_metric is not None:
14-
self.initial['height_metric'] = self.instance.height_metric / 10
13+
if self.instance and self.instance.metric is not None:
14+
self.initial['metric'] = self.instance.metric / 10
1515

16-
self.fields["height_metric"] = DecimalField(
16+
self.fields["metric"] = DecimalField(
1717
decimal_places=1,
1818
label="Centimetres",
1919
classes="nhsuk-input--width-4",
@@ -27,12 +27,12 @@ def __init__(self, *args, **kwargs):
2727
suffix="cm"
2828
)
2929

30-
def clean_height_metric(self):
31-
return self.cleaned_data['height_metric'] * 10
30+
def clean_metric(self):
31+
return int(self.cleaned_data['metric'] * 10)
3232

33-
def clean_height_imperial(self):
33+
def clean_imperial(self):
3434
return None
3535

3636
class Meta:
37-
model = ResponseSet
38-
fields = ['height_metric', 'height_imperial']
37+
model = HeightResponse
38+
fields = ['metric', 'imperial']

lung_cancer_screening/questions/jinja2/height.jinja

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@
3838
%}
3939

4040
{% if unit == "imperial" %}
41-
{{ form.height_imperial.as_field_group() }}
41+
{{ form.imperial.as_field_group() }}
4242
{% else %}
43-
{{ form.height_metric.as_field_group() }}
43+
{{ form.metric.as_field_group() }}
4444
{% endif %}
4545

4646

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated manually to create HeightResponse and copy data
2+
3+
import django.core.validators
4+
import django.db.models.deletion
5+
from django.db import migrations, models
6+
7+
8+
def copy_height_data(apps, schema_editor):
9+
with schema_editor.connection.cursor() as cursor:
10+
cursor.execute("""
11+
INSERT INTO questions_heightresponse (created_at, updated_at, metric, imperial, response_set_id)
12+
SELECT NOW(), NOW(), height_metric, height_imperial, id
13+
FROM questions_responseset
14+
WHERE height_metric IS NOT NULL OR height_imperial IS NOT NULL
15+
ON CONFLICT (response_set_id) DO NOTHING
16+
""")
17+
18+
19+
def reverse_copy_height_data(apps, schema_editor):
20+
with schema_editor.connection.cursor() as cursor:
21+
cursor.execute("""
22+
UPDATE questions_responseset rs
23+
SET height_metric = h.metric,
24+
height_imperial = h.imperial
25+
FROM questions_heightresponse h
26+
WHERE rs.id = h.response_set_id
27+
""")
28+
29+
30+
class Migration(migrations.Migration):
31+
32+
dependencies = [
33+
('questions', '0029_genderresponse'),
34+
]
35+
36+
operations = [
37+
migrations.CreateModel(
38+
name='HeightResponse',
39+
fields=[
40+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
41+
('created_at', models.DateTimeField(auto_now_add=True)),
42+
('updated_at', models.DateTimeField(auto_now=True)),
43+
('metric', 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')])),
44+
('imperial', 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')])),
45+
('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='height_response', to='questions.responseset')),
46+
],
47+
options={
48+
'abstract': False,
49+
},
50+
),
51+
migrations.RunPython(copy_height_data, reverse_copy_height_data),
52+
migrations.RemoveField(
53+
model_name='responseset',
54+
name='height_metric',
55+
),
56+
migrations.RemoveField(
57+
model_name='responseset',
58+
name='height_imperial',
59+
),
60+
]

lung_cancer_screening/questions/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from .date_of_birth_response import DateOfBirthResponse # noqa: F401
66
from .ethnicity_response import EthnicityResponse # noqa: F401
77
from .gender_response import GenderResponse # noqa: F401
8+
from .height_response import HeightResponse # noqa: F401
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.db import models
2+
from django.core.exceptions import ValidationError
3+
from django.core.validators import MaxValueValidator, MinValueValidator
4+
from decimal import Decimal
5+
6+
from .base import BaseModel
7+
from .response_set import ResponseSet
8+
9+
10+
class HeightResponse(BaseModel):
11+
MAX_HEIGHT_METRIC = 2438
12+
MIN_HEIGHT_METRIC = 1397
13+
MAX_HEIGHT_IMPERIAL = 96
14+
MIN_HEIGHT_IMPERIAL = 55
15+
16+
response_set = models.OneToOneField(ResponseSet, on_delete=models.CASCADE, related_name='height_response')
17+
18+
metric = models.PositiveIntegerField(null=True, blank=True, validators=[
19+
MinValueValidator(MIN_HEIGHT_METRIC, message="Height must be between 139.7cm and 243.8 cm"),
20+
MaxValueValidator(MAX_HEIGHT_METRIC, message="Height must be between 139.7cm and 243.8 cm"),
21+
])
22+
imperial = models.PositiveIntegerField(null=True, blank=True, validators=[
23+
MinValueValidator(MIN_HEIGHT_IMPERIAL, message="Height must be between 4 feet 7 inches and 8 feet"),
24+
MaxValueValidator(MAX_HEIGHT_IMPERIAL, message="Height must be between 4 feet 7 inches and 8 feet"),
25+
])
26+
27+
def clean(self):
28+
if not self.metric and not self.imperial:
29+
raise ValidationError("Either metric or imperial height must be provided.")
30+
if self.metric and self.imperial:
31+
raise ValidationError("Cannot provide both metric and imperial height.")
32+
33+
@property
34+
def formatted(self):
35+
if self.metric:
36+
return f"{Decimal(self.metric) / 10}cm"
37+
elif self.imperial:
38+
value = Decimal(self.imperial)
39+
return f"{value // 12} feet {value % 12} inches"
40+
return None
Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,161 @@
11
from django.test import TestCase
22

3-
from ...factories.user_factory import UserFactory
4-
from ....models.response_set import ResponseSet
3+
from ...factories.response_set_factory import ResponseSetFactory
4+
from ....models.height_response import HeightResponse
55
from ....forms.imperial_height_form import ImperialHeightForm
66

77

88
class TestImperialHeightForm(TestCase):
99
def setUp(self):
10-
self.user = UserFactory()
11-
self.response_set = ResponseSet(user=self.user)
12-
self.response_set.height_metric = 1704
10+
self.response_set = ResponseSetFactory()
11+
self.response = HeightResponse.objects.create(
12+
response_set=self.response_set,
13+
metric=1704
14+
)
1315

1416
def test_is_valid_with_valid_input(self):
1517
form = ImperialHeightForm(
16-
instance=self.response_set,
18+
instance=self.response,
1719
data={
18-
"height_imperial_0": "5", # feet
19-
"height_imperial_1": "9" # inches
20+
"imperial_0": "5", # feet
21+
"imperial_1": "9" # inches
2022
}
2123
)
2224

2325
self.assertTrue(form.is_valid())
2426

2527
def test_converts_feet_and_inches_to_an_inches_integer(self):
2628
form = ImperialHeightForm(
27-
instance=self.response_set,
29+
instance=self.response,
2830
data={
29-
"height_imperial_0": "5", # feet
30-
"height_imperial_1": "9" # inches
31+
"imperial_0": "5", # feet
32+
"imperial_1": "9" # inches
3133
}
3234
)
3335

3436
self.assertTrue(form.is_valid(), f"Form errors: {form.errors}")
35-
self.assertEqual(form.cleaned_data['height_imperial'], 69)
37+
self.assertEqual(form.cleaned_data['imperial'], 69)
3638

3739
def test_setting_imperial_height_clears_height(self):
3840
form = ImperialHeightForm(
39-
instance=self.response_set,
41+
instance=self.response,
4042
data={
41-
"height_imperial_0": "5", # feet
42-
"height_imperial_1": "9" # inches
43+
"imperial_0": "5", # feet
44+
"imperial_1": "9" # inches
4345
}
4446
)
4547
form.save()
46-
self.assertEqual(self.response_set.height_metric, None)
48+
self.response.refresh_from_db()
49+
self.assertEqual(self.response.metric, None)
4750

4851
def test_is_invalid_with_missing_data(self):
4952
form = ImperialHeightForm(
50-
instance=self.response_set,
53+
instance=self.response,
5154
data={
5255
# missing feet
5356
# missing inches
5457
}
5558
)
5659
self.assertFalse(form.is_valid())
5760
self.assertEqual(
58-
form.errors["height_imperial"],
61+
form.errors["imperial"],
5962
["Enter your height"]
6063
)
6164

6265
def test_is_invalid_with_missing_inches(self):
6366
form = ImperialHeightForm(
64-
instance=self.response_set,
67+
instance=self.response,
6568
data={
66-
"height_imperial_0": "5",
69+
"imperial_0": "5",
6770
# missing inches
6871
}
6972
)
7073
self.assertFalse(form.is_valid())
7174
self.assertEqual(
72-
form.errors["height_imperial"],
75+
form.errors["imperial"],
7376
["Inches must be between 0 and 11"]
7477
)
7578

7679
def test_is_invalid_with_missing_feet(self):
7780
form = ImperialHeightForm(
78-
instance=self.response_set,
81+
instance=self.response,
7982
data={
80-
#"height_imperial_0": "5",
81-
"height_imperial_1": "5"
83+
#"imperial_0": "5",
84+
"imperial_1": "5"
8285
}
8386
)
8487
self.assertFalse(form.is_valid())
8588
self.assertEqual(
86-
form.errors["height_imperial"],
89+
form.errors["imperial"],
8790
["Feet must be between 4 and 8"]
8891
)
8992

9093
def test_is_invalid_when_given_a_decimal_feet_value(self):
9194
form = ImperialHeightForm(
92-
instance=self.response_set,
95+
instance=self.response,
9396
data={
94-
"height_imperial_0": "5.2",
95-
"height_imperial_1": "0"
97+
"imperial_0": "5.2",
98+
"imperial_1": "0"
9699
}
97100
)
98101
self.assertFalse(form.is_valid())
99102
self.assertEqual(
100-
form.errors["height_imperial"],
103+
form.errors["imperial"],
101104
["Feet must be in whole numbers"]
102105
)
103106

104-
def test_is_invalid_when_inches_under_11(self):
107+
def test_is_invalid_when_inches_over_11(self):
105108
form = ImperialHeightForm(
106-
instance=self.response_set,
109+
instance=self.response,
107110
data={
108-
"height_imperial_0": "5",
109-
"height_imperial_1": "12"
111+
"imperial_0": "5",
112+
"imperial_1": "12"
110113
}
111114
)
112115
self.assertFalse(form.is_valid())
113116
self.assertEqual(
114-
form.errors["height_imperial"],
117+
form.errors["imperial"],
115118
["Inches must be between 0 and 11"]
116119
)
117120

118-
def test_is_invalid_when_inches_over_0(self):
121+
def test_is_invalid_when_inches_under_0(self):
119122
form = ImperialHeightForm(
120-
instance=self.response_set,
123+
instance=self.response,
121124
data={
122-
"height_imperial_0": "5",
123-
"height_imperial_1": "-1"
125+
"imperial_0": "5",
126+
"imperial_1": "-1"
124127
}
125128
)
126129
self.assertFalse(form.is_valid())
127130
self.assertEqual(
128-
form.errors["height_imperial"],
131+
form.errors["imperial"],
129132
["Inches must be between 0 and 11"]
130133
)
131134

132135
def test_is_invalid_when_feet_over_4(self):
133136
form = ImperialHeightForm(
134-
instance=self.response_set,
137+
instance=self.response,
135138
data={
136-
"height_imperial_0": "3",
137-
"height_imperial_1": "10"
139+
"imperial_0": "3",
140+
"imperial_1": "10"
138141
}
139142
)
140143
self.assertFalse(form.is_valid())
141144
self.assertEqual(
142-
form.errors["height_imperial"],
145+
form.errors["imperial"],
143146
["Feet must be between 4 and 8"]
144147
)
145148

146149
def test_is_invalid_when_feet_under_8(self):
147150
form = ImperialHeightForm(
148-
instance=self.response_set,
151+
instance=self.response,
149152
data={
150-
"height_imperial_0": "9",
151-
"height_imperial_1": "0"
153+
"imperial_0": "9",
154+
"imperial_1": "0"
152155
}
153156
)
154157
self.assertFalse(form.is_valid())
155158
self.assertEqual(
156-
form.errors["height_imperial"],
159+
form.errors["imperial"],
157160
["Feet must be between 4 and 8"]
158161
)

0 commit comments

Comments
 (0)