Skip to content

Commit 5f4a466

Browse files
committed
PPHA-526: Add types of tobacco smoked
1 parent 0d280a7 commit 5f4a466

16 files changed

Lines changed: 395 additions & 5 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@TypesTobaccoSmoking
2+
Feature: Types tobacco smoking page
3+
Scenario: The page is accessible
4+
Given I am logged in
5+
When I go to "/types-tobacco-smoking"
6+
Then there are no accessibility violations
7+
8+
Scenario: Form errors
9+
Given I am logged in
10+
When I go to "/types-tobacco-smoking"
11+
And I click "Continue"
12+
Then I am on "/types-tobacco-smoking"
13+
And I see a form error "Select the type of tobacco you smoke or have smoked"
14+
And there are no accessibility violations
15+
16+
Scenario: Navigating backwards and forwards
17+
Given I am logged in
18+
When I go to "/types-tobacco-smoking"
19+
Then I see a back link to "/relatives-age-when-diagnosed"
20+
When I check "Cigarettes"
21+
And I submit the form
22+
Then I am on "/check-your-answers"
23+
24+
Scenario: Checking responses and changing them
25+
Given I am logged in
26+
When I go to "/types-tobacco-smoking"
27+
And I check "Cigarettes"
28+
And I check "Cigars"
29+
And I submit the form
30+
When I go to "/check-your-answers"
31+
Then I see "Cigarettes and Cigars" as a response to "Types of tobacco smoked" under "Smoking history"
32+
And I see "/types-tobacco-smoking?change=True" as a link to change "Types of tobacco smoked" under "Smoking history"
33+
When I click the link to change "Types of tobacco smoked" under "Smoking history"
34+
Then I am on "/types-tobacco-smoking?change=True"
35+
And I see "Cigarettes" selected
36+
And I see "Cigars" selected
37+
When I check "Pipe"
38+
And I click "Continue"
39+
Then I am on "/check-your-answers"
40+
And I see "Cigarettes, Pipe, and Cigars" as a response to "Types of tobacco smoked" under "Smoking history"
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 ...nhsuk_forms.choice_field import MultipleChoiceField
4+
from ..models.types_tobacco_smoking_response import TypesTobaccoSmokingResponse, TypesTobaccoSmokingValues
5+
6+
7+
class TypesTobaccoSmokingForm(forms.ModelForm):
8+
9+
def __init__(self, *args, **kwargs):
10+
super().__init__(*args, **kwargs)
11+
12+
self.fields["value"] = MultipleChoiceField(
13+
choices=TypesTobaccoSmokingValues.choices,
14+
widget=forms.CheckboxSelectMultiple,
15+
label="What do you smoke?",
16+
label_classes="nhsuk-fieldset__legend--l",
17+
label_is_page_heading=True,
18+
hint="Select all that apply",
19+
error_messages={
20+
"required": "Select the type of tobacco you smoke or have smoked"
21+
},
22+
)
23+
24+
class Meta:
25+
model = TypesTobaccoSmokingResponse
26+
fields = ['value']

lung_cancer_screening/questions/jinja2/responses.jinja

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@
3939
}) }}
4040
</section>
4141

42+
<section>
43+
<h2 class="nhsuk-heading-m">Smoking history</h2>
44+
45+
{{ summaryList({
46+
"rows": response_set.smoking_history_responses_items()
47+
}) }}
48+
</section>
49+
4250
<form action="{{ request.path }}" method="post">
4351
{{ csrf_input }}
4452
{{ button({
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 5.2.10 on 2026-01-21 16:57
2+
3+
import django.contrib.postgres.fields
4+
import django.db.models.deletion
5+
import lung_cancer_screening.questions.models.validators.singleton_option
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('questions', '0042_alter_educationresponse_value'),
13+
]
14+
15+
operations = [
16+
migrations.AlterField(
17+
model_name='ethnicityresponse',
18+
name='value',
19+
field=models.CharField(choices=[('A', 'Asian or Asian British'), ('B', 'Black, African, Caribbean or Black British'), ('M', 'Mixed or multiple ethnic groups'), ('W', 'White'), ('O', 'Other ethnic group'), ('N', 'Prefer not to say')], max_length=1),
20+
),
21+
migrations.AlterField(
22+
model_name='sexatbirthresponse',
23+
name='value',
24+
field=models.CharField(choices=[('F', 'Female'), ('M', 'Male'), ('I', 'Intersex')], max_length=1),
25+
),
26+
migrations.CreateModel(
27+
name='TypesTobaccoSmokingResponse',
28+
fields=[
29+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
30+
('created_at', models.DateTimeField(auto_now_add=True)),
31+
('updated_at', models.DateTimeField(auto_now=True)),
32+
('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('C', 'Cigarettes'), ('G', 'Cigars'), ('P', 'Pipes'), ('E', 'E-cigarettes or vaping'), ('N', 'None of the above')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option])),
33+
('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='types_tobacco_smoking_response', to='questions.responseset')),
34+
],
35+
options={
36+
'abstract': False,
37+
},
38+
),
39+
]

lung_cancer_screening/questions/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
from .relatives_age_when_diagnosed_response import RelativesAgeWhenDiagnosedResponse # noqa: F401
1515
from .respiratory_conditions_response import RespiratoryConditionsResponse # noqa: F401
1616
from .sex_at_birth_response import SexAtBirthResponse # noqa: F401
17+
from .types_tobacco_smoking_response import TypesTobaccoSmokingResponse # noqa: F401
1718
from .weight_response import WeightResponse # noqa: F401
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.db import models
2+
from django.contrib.postgres.fields import ArrayField
3+
4+
from .base import BaseModel
5+
from .response_set import ResponseSet
6+
7+
8+
class TypesTobaccoSmokingValues(models.TextChoices):
9+
CIGARETTES = "C", "Cigarettes"
10+
ROLLED_CIGARETTES = "R", "Rolled cigarettes, or roll-ups"
11+
PIPE = "P", "Pipe"
12+
CIGARS = "G", "Cigars"
13+
CIGARILLOS = "E", "Cigarillos"
14+
SHISHAS = "S", "Shisha"
15+
16+
17+
class TypesTobaccoSmokingResponse(BaseModel):
18+
response_set = models.OneToOneField(ResponseSet, on_delete=models.CASCADE, related_name='types_tobacco_smoking_response')
19+
value = ArrayField(
20+
models.CharField(max_length=1, choices=TypesTobaccoSmokingValues.choices)
21+
)

lung_cancer_screening/questions/presenters/response_set_presenter.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ..models.education_response import EducationValues
55
from ..models.respiratory_conditions_response import RespiratoryConditionValues
66
from ..models.family_history_lung_cancer_response import FamilyHistoryLungCancerValues
7+
from ..models.types_tobacco_smoking_response import TypesTobaccoSmokingValues
78

89
class ResponseSetPresenter:
910
DATE_FORMAT = "%-d %B %Y" # eg 8 September 2000
@@ -130,9 +131,18 @@ def respiratory_conditions(self):
130131
for code in self.response_set.respiratory_conditions_response.value
131132
])
132133

134+
@property
135+
def types_tobacco_smoking(self):
136+
if not hasattr(self.response_set, 'types_tobacco_smoking_response'):
137+
return None
138+
139+
return self._list_to_sentence([
140+
TypesTobaccoSmokingValues(code).label
141+
for code in self.response_set.types_tobacco_smoking_response.value
142+
])
133143

134144
def eligibility_responses_items(self):
135-
return [
145+
items = [
136146
self._check_your_answer_item(
137147
"Have you ever smoked tobacco?",
138148
self.have_you_ever_smoked,
@@ -142,9 +152,11 @@ def eligibility_responses_items(self):
142152
"Date of birth",
143153
self.date_of_birth,
144154
"questions:date_of_birth",
145-
)
155+
),
146156
]
147157

158+
return items
159+
148160
def about_you_responses_items(self):
149161
return [
150162
self._check_your_answer_item(
@@ -218,6 +230,18 @@ def family_history_responses_items(self):
218230
return items
219231

220232

233+
def smoking_history_responses_items(self):
234+
items = [
235+
self._check_your_answer_item(
236+
"Types of tobacco smoked",
237+
self.types_tobacco_smoking,
238+
"questions:types_tobacco_smoking",
239+
)
240+
]
241+
242+
return items
243+
244+
221245
def is_complete(self):
222246
return self.response_set.is_complete()
223247

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import factory
2+
3+
from .response_set_factory import ResponseSetFactory
4+
from ...models.types_tobacco_smoking_response import TypesTobaccoSmokingResponse, TypesTobaccoSmokingValues
5+
6+
7+
class TypesTobaccoSmokingResponseFactory(factory.django.DjangoModelFactory):
8+
class Meta:
9+
model = TypesTobaccoSmokingResponse
10+
11+
response_set = factory.SubFactory(ResponseSetFactory)
12+
value = [TypesTobaccoSmokingValues.CIGARETTES]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.test import TestCase, tag
2+
3+
from ...factories.response_set_factory import ResponseSetFactory
4+
from ....models.types_tobacco_smoking_response import (
5+
TypesTobaccoSmokingResponse,
6+
TypesTobaccoSmokingValues,
7+
)
8+
from ....forms.types_tobacco_smoking_form import TypesTobaccoSmokingForm
9+
10+
11+
@tag("TypesTobaccoSmoking")
12+
class TestTypesTobaccoSmokingForm(TestCase):
13+
def setUp(self):
14+
self.response_set = ResponseSetFactory()
15+
self.response = TypesTobaccoSmokingResponse.objects.create(
16+
response_set=self.response_set,
17+
value=["C"]
18+
)
19+
20+
def test_is_valid_a_valid_value(self):
21+
form = TypesTobaccoSmokingForm(
22+
instance=self.response,
23+
data={
24+
"value": [TypesTobaccoSmokingValues.CIGARETTES]
25+
}
26+
)
27+
self.assertTrue(form.is_valid())
28+
self.assertEqual(
29+
form.cleaned_data["value"],
30+
[TypesTobaccoSmokingValues.CIGARETTES]
31+
)
32+
33+
34+
def test_is_invalid_with_an_invalid_value(self):
35+
form = TypesTobaccoSmokingForm(
36+
instance=self.response,
37+
data={
38+
"value": ["invalid"]
39+
}
40+
)
41+
self.assertFalse(form.is_valid())
42+
self.assertEqual(
43+
form.errors["value"],
44+
["Select a valid choice. invalid is not one of the available choices."]
45+
)
46+
47+
def test_is_invalid_when_no_option_is_selected(self):
48+
form = TypesTobaccoSmokingForm(
49+
instance=self.response,
50+
data={
51+
"value": []
52+
}
53+
)
54+
self.assertFalse(form.is_valid())
55+
self.assertEqual(
56+
form.errors["value"],
57+
["Select the type of tobacco you smoke or have smoked"],
58+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django.test import TestCase, tag
2+
3+
from ...factories.response_set_factory import ResponseSetFactory
4+
from ...factories.types_tobacco_smoking_response_factory import TypesTobaccoSmokingResponseFactory
5+
6+
from ....models.types_tobacco_smoking_response import TypesTobaccoSmokingResponse, TypesTobaccoSmokingValues
7+
8+
9+
@tag("TypesTobaccoSmoking")
10+
class TestTypesTobaccoSmokingResponse(TestCase):
11+
def setUp(self):
12+
self.response_set = ResponseSetFactory()
13+
14+
def test_has_a_valid_factory(self):
15+
model = TypesTobaccoSmokingResponseFactory.build(response_set=self.response_set)
16+
model.full_clean()
17+
18+
def test_has_response_set_as_foreign_key(self):
19+
response_set = ResponseSetFactory()
20+
response = TypesTobaccoSmokingResponse.objects.create(
21+
response_set=response_set,
22+
value=[TypesTobaccoSmokingValues.CIGARETTES]
23+
)
24+
25+
self.assertEqual(response.response_set, response_set)
26+
27+
def test_has_value_as_list(self):
28+
response_set = ResponseSetFactory()
29+
response = TypesTobaccoSmokingResponse.objects.create(
30+
response_set=response_set,
31+
value=[TypesTobaccoSmokingValues.CIGARETTES, TypesTobaccoSmokingValues.CIGARS]
32+
)
33+
34+
self.assertIsInstance(response.value, list)

0 commit comments

Comments
 (0)