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
9 changes: 6 additions & 3 deletions features/education.feature
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ Feature: Education page
Given I am logged in
When I go to "/education"
Then I see a back link to "/ethnicity"
When I fill in and submit my education with "A-levels"
When I check "A-levels"
And I click "Continue"
Then I am on "/respiratory-conditions"

Scenario: Checking responses and changing them
Given I am logged in
When I go to "/education"
And I fill in and submit my education with "A-levels"
And I check "A-levels"
And I check "GCSEs"
And I click "Continue"
When I go to "/check-your-answers"
Then I see "A-levels" as a response to "Highest level of education" under "About you"
Then I see "GCSEs and A-levels" as a response to "Highest level of education" under "About you"
And I see "/education?change=True" as a link to change "Highest level of education" under "About you"
When I click the link to change "Highest level of education" under "About you"
Then I am on "/education?change=True"
Expand Down
6 changes: 6 additions & 0 deletions features/steps/form_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ def then_i_see_a_date_x_years_ago_filled_in(context, years):
assert context.page.get_by_label('Month').input_value() == str(date_of_birth.month)
assert context.page.get_by_label('Year').input_value() == str(date_of_birth.year)


@then(u'I see "{value}" filled in for "{label}"')
def then_i_see_value_filled_in_for_label(context, value, label):
assert context.page.get_by_label(label, exact=True).input_value() == value


@when('I check "{label}"')
def when_i_check_label(context, label):
context.page.get_by_label(label, exact=True).check()
41 changes: 33 additions & 8 deletions lung_cancer_screening/questions/forms/education_form.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django import forms

from ...nhsuk_forms.choice_field import ChoiceField
from ...nhsuk_forms.choice_field import MultipleChoiceField
from ..models.education_response import EducationResponse, EducationValues


Expand All @@ -9,17 +9,42 @@ class EducationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.fields["value"] = ChoiceField(
self.fields["value"] = MultipleChoiceField(
choices=EducationValues.choices,
label="What level of education have you completed?",
widget=forms.RadioSelect,
widget=forms.CheckboxSelectMultiple,
label_classes="nhsuk-fieldset__legend--m",
hint=(
"Select all that apply"
),
hint="Select all that apply",
error_messages={
'required': 'Select your level of education'
}
"required": "Select your level of education",
"singleton_option": (
"Select your level of education, or select "
"'Prefer not to say'"
),
},
)

# Add hints for each choice
education_field = self["value"]
education_field.add_hint_for_choice(
EducationValues.GCSES,
"Previously O-levels",
)
education_field.add_hint_for_choice(
EducationValues.A_LEVELS,
"Previously Higher School Certificate (HSC)",
)
education_field.add_hint_for_choice(
EducationValues.FURTHER_EDUCATION,
"For example, apprenticeships or Higher National Certificates (HNC)",
)
education_field.add_hint_for_choice(
EducationValues.BACHELORS_DEGREE,
"A university degree, also known as an undergraduate degree",
)
education_field.add_hint_for_choice(
EducationValues.POSTGRADUATE_DEGREE,
"For example, a Masters or PhD",
)

self["value"].add_divider_after(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='responseset',
name='respiratory_conditions',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), blank=True, null=True, size=None, validators=[lung_cancer_screening.questions.models.respiratory_conditions_response.validate_singleton_option]),
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), blank=True, null=True, size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option]),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.respiratory_conditions_response.validate_singleton_option])),
('value', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('P', 'Pneumonia'), ('E', 'Emphysema'), ('B', 'Bronchitis'), ('T', 'Tuberculosis (TB)'), ('C', 'Chronic obstructive pulmonary disease (COPD)'), ('N', 'No, I have not had any of these respiratory conditions')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option])),
('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='respiratory_conditions_response', to='questions.responseset')),
],
options={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.2.10 on 2026-01-15 16:41

import django.contrib.postgres.fields
import lung_cancer_screening.questions.models.validators.singleton_option
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('questions', '0040_educationresponse'),
]

operations = [
migrations.AlterField(
model_name='educationresponse',
name='value',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('X', 'I finished school before the age of 15'), ('G', 'GCSEs'), ('A', 'A-levels'), ('B', "Bachelor's degree"), ('P', 'Postgraduate degree'), ('N', "I'd prefer not to say")], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option]),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.2.10 on 2026-01-19 10:47

import django.contrib.postgres.fields
import lung_cancer_screening.questions.models.validators.singleton_option
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('questions', '0041_alter_educationresponse_value'),
]

operations = [
migrations.AlterField(
model_name='educationresponse',
name='value',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('X', 'I finished school before the age of 15'), ('G', 'GCSEs'), ('A', 'A-levels'), ('F', 'Further education'), ('B', "Bachelor's degree"), ('P', 'Postgraduate degree'), ('N', 'Prefer not to say')], max_length=1), size=None, validators=[lung_cancer_screening.questions.models.validators.singleton_option.validate_singleton_option]),
),
]
12 changes: 10 additions & 2 deletions lung_cancer_screening/questions/models/education_response.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField

from .validators.singleton_option import validate_singleton_option
from .base import BaseModel
from .response_set import ResponseSet

Expand All @@ -8,11 +10,17 @@ class EducationValues(models.TextChoices):
FINISHED_SCHOOL_BEFORE_15 = "X", "I finished school before the age of 15"
GCSES = "G", "GCSEs"
A_LEVELS = "A", "A-levels"
FURTHER_EDUCATION = "F", "Further education"
BACHELORS_DEGREE = "B", "Bachelor's degree"
POSTGRADUATE_DEGREE = "P", "Postgraduate degree"
PREFER_NOT_TO_SAY = "N", "I'd prefer not to say"
PREFER_NOT_TO_SAY = "N", "Prefer not to say"


class EducationResponse(BaseModel):
response_set = models.OneToOneField(ResponseSet, on_delete=models.CASCADE, related_name='education_response')
value = models.CharField(max_length=1, choices=EducationValues.choices)
value = ArrayField(
models.CharField(max_length=1, choices=EducationValues.choices),
validators=[
validate_singleton_option
]
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError

from .validators.singleton_option import validate_singleton_option
from .base import BaseModel
from .response_set import ResponseSet

Expand All @@ -15,17 +15,11 @@ class RespiratoryConditionValues(models.TextChoices):
NONE = "N", "No, I have not had any of these respiratory conditions"


def validate_singleton_option(value):
if value and "N" in value and len(value) > 1:
raise ValidationError(
"Cannot have singleton value and other values selected",
code="singleton_option",
)


class RespiratoryConditionsResponse(BaseModel):
response_set = models.OneToOneField(ResponseSet, on_delete=models.CASCADE, related_name='respiratory_conditions_response')
value = ArrayField(
models.CharField(max_length=1, choices=RespiratoryConditionValues.choices),
validators=[validate_singleton_option]
validators=[
validate_singleton_option
]
)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.core.exceptions import ValidationError

def validate_singleton_option(value):
if value and "N" in value and len(value) > 1:
raise ValidationError(
"Cannot have singleton value and other values selected",
code="singleton_option",
)

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from decimal import Decimal

from ..models.education_response import EducationValues
from ..models.respiratory_conditions_response import RespiratoryConditionValues

class ResponseSetPresenter:
Expand Down Expand Up @@ -74,7 +75,10 @@ def education(self):
if not hasattr(self.response_set, 'education_response'):
return None

return self.response_set.education_response.get_value_display()
return self._list_to_sentence([
EducationValues(code).label
for code in self.response_set.education_response.value
])

@property
def asbestos_exposure(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ class Meta:
model = EducationResponse

response_set = factory.SubFactory(ResponseSetFactory)
value = factory.Iterator(EducationValues)
value = [EducationValues.GCSES]
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ def setUp(self):
self.response_set = ResponseSetFactory()
self.response = EducationResponse.objects.create(
response_set=self.response_set,
value=EducationValues.GCSES
value=[EducationValues.GCSES]
)

def test_is_valid_with_a_valid_value(self):
form = EducationForm(
instance=self.response,
data={
'value': EducationValues.GCSES
'value': [EducationValues.GCSES]
}
)
self.assertTrue(form.is_valid())
self.assertEqual(
form.cleaned_data['value'],
EducationValues.GCSES
[EducationValues.GCSES]
)

def test_is_invalid_with_an_invalid_value(self):
form = EducationForm(
instance=self.response,
data={
"value": "invalid"
"value": ["invalid"]
}
)
self.assertFalse(form.is_valid())
Expand All @@ -44,7 +44,7 @@ def test_is_invalid_when_no_option_is_selected(self):
form = EducationForm(
instance=self.response,
data={
"value": None
"value": []
}
)
self.assertFalse(form.is_valid())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ def test_has_a_valid_factory(self):
model.full_clean()

def test_has_response_set_as_foreign_key(self):
response_set = ResponseSetFactory()
response = EducationResponse.objects.create(
response_set=response_set,
value=EducationValues.A_LEVELS
response_set=self.response_set,
value=[EducationValues.A_LEVELS]
)

self.assertEqual(response.response_set, response_set)
self.assertEqual(response.response_set, self.response_set)

def test_has_value_as_string(self):
response_set = ResponseSetFactory()
def test_has_value_as_list_of_strings(self):
response = EducationResponse.objects.create(
response_set=response_set,
value=EducationValues.A_LEVELS
response_set=self.response_set,
value=[EducationValues.A_LEVELS]
)

self.assertIsInstance(response.value, str)
self.assertIsInstance(response.value, list)
self.assertIsInstance(response.value[0], str)
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,21 @@ def test_education_with_no_value(self):
presenter = ResponseSetPresenter(self.response_set)
self.assertEqual(presenter.education, None)


def test_education_with_value(self):
EducationResponseFactory(
response_set=self.response_set,
value=EducationValues.GCSES
value=[
EducationValues.GCSES,
EducationValues.A_LEVELS,
],
)
presenter = ResponseSetPresenter(self.response_set)
self.assertEqual(presenter.education, EducationValues.GCSES.label)
self.assertEqual(
presenter.education,
EducationValues.GCSES.label
+ " and "
+ EducationValues.A_LEVELS.label,
)


def test_asbestos_exposure_with_no_value(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class TestPostEducation(TestCase):
def setUp(self):
self.user = login_user(self.client)

self.valid_params = {"value": EducationValues.A_LEVELS}
self.valid_params = {"value": [EducationValues.A_LEVELS]}


def test_post_redirects_if_the_user_is_not_logged_in(self):
Expand Down Expand Up @@ -87,7 +87,8 @@ def test_post_creates_unsubmitted_response_set_when_no_response_set_exists(
self.assertEqual(self.user.responseset_set.count(), 1)
self.assertEqual(response_set.submitted_at, None)
self.assertEqual(
EducationResponse.objects.get(response_set=response_set).value, self.valid_params["value"]
EducationResponse.objects.get(response_set=response_set).value,
self.valid_params["value"]
)
self.assertEqual(response_set.user, self.user)

Expand Down