Skip to content

Commit e33f475

Browse files
authored
Merge pull request #235 from NHSDigital/restrict-submission-when-all-answers-completed
Restrict submission when all answers completed
2 parents 8c6748f + 491d470 commit e33f475

31 files changed

Lines changed: 437 additions & 200 deletions

features/questionnaire.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Feature: Questionnaire
22
Scenario: Cannot change responses once submitted
33
Given I am logged in
4-
And I have already submitted my responses
4+
And I have recently submitted my responses
55
When I go to "/start"
66
And I click "Start now"
77
Then I am on "/confirmation"

features/steps/participant_steps.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from behave import given
22
from django.utils import timezone
33

4+
from lung_cancer_screening.questions.tests.factories.response_set_factory import ResponseSetFactory
45

5-
@given('I have already submitted my responses')
6+
7+
@given('I have recently submitted my responses')
68
def given_i_have_already_submitted_my_responses(context):
7-
context.current_user.responseset_set.create(
8-
submitted_at=timezone.now()
9+
ResponseSetFactory.create(
10+
user=context.current_user,
11+
recently_submitted=True
912
)
1013

1114
@given('I have started the questionnaire')

lung_cancer_screening/questions/jinja2/responses.jinja

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
<form action="{{ request.path }}" method="post">
4343
{{ csrf_input }}
4444
{{ button({
45-
"text": "Submit"
45+
"text": "Submit",
46+
"disabled": not response_set.is_complete()
4647
}) }}
4748
</form>
4849
</div>

lung_cancer_screening/questions/models/family_history_lung_cancer_response.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ class FamilyHistoryLungCancerResponse(BaseModel):
1515
response_set = models.OneToOneField(ResponseSet, on_delete=models.CASCADE, related_name='family_history_lung_cancer')
1616
value = models.CharField(max_length=1, choices=FamilyHistoryLungCancerValues.choices)
1717

18+
def is_truthy(self):
19+
return self.value == FamilyHistoryLungCancerValues.YES
20+
1821

1922
@receiver(post_save, sender=FamilyHistoryLungCancerResponse)
2023
def remove_relatives_age_when_diagnosed_if_not_yes(sender, instance, **kwargs):
2124
if (
22-
instance.value != FamilyHistoryLungCancerValues.YES
25+
not instance.is_truthy()
2326
and instance.response_set
2427
and hasattr(instance.response_set, "relatives_age_when_diagnosed")
2528
):

lung_cancer_screening/questions/models/response_set.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,49 @@ class Meta:
4242
def clean(self):
4343
super().clean()
4444
self._validate_any_submitted_response_set_recently()
45+
self._validate_complete_on_submission()
4546

4647

4748
def has_user_submitted_response_set_recently(self):
48-
return self.user and self.user.has_recently_submitted_responses()
49+
return self.user and self.user.has_recently_submitted_responses(excluding=self)
4950

5051

5152
def _validate_any_submitted_response_set_recently(self):
5253
if self.has_user_submitted_response_set_recently():
5354
raise ValidationError(
5455
"Responses have already been submitted for this user"
5556
)
57+
58+
59+
def _validate_complete_on_submission(self):
60+
if self.submitted_at and not self.is_complete():
61+
raise ValidationError(
62+
"Response set must be complete before it can be submitted"
63+
)
64+
65+
66+
def _response_attrs(self):
67+
response_attrs = [
68+
'asbestos_exposure_response',
69+
'cancer_diagnosis_response',
70+
'check_need_appointment_response',
71+
'date_of_birth_response',
72+
'education_response',
73+
'ethnicity_response',
74+
'family_history_lung_cancer',
75+
'gender_response',
76+
'have_you_ever_smoked_response',
77+
'height_response',
78+
'respiratory_conditions_response',
79+
'sex_at_birth_response',
80+
'weight_response',
81+
]
82+
83+
if hasattr(self, 'family_history_lung_cancer') and self.family_history_lung_cancer.is_truthy():
84+
response_attrs.append('relatives_age_when_diagnosed')
85+
86+
return response_attrs
87+
88+
89+
def is_complete(self):
90+
return all(hasattr(self, attr) for attr in self._response_attrs())

lung_cancer_screening/questions/models/user.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,10 @@ class Meta:
3636
verbose_name = 'user'
3737
verbose_name_plural = 'users'
3838

39-
def has_recently_submitted_responses(self):
40-
return self.responseset_set.recently_submitted().exists()
39+
def has_recently_submitted_responses(self, excluding=None):
40+
query = self.responseset_set.recently_submitted()
41+
42+
if excluding:
43+
query = query.exclude(id=excluding.id)
44+
45+
return query.exists()

lung_cancer_screening/questions/presenters/response_set_presenter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ def family_history_responses_items(self):
218218
return items
219219

220220

221+
def is_complete(self):
222+
return self.response_set.is_complete()
223+
224+
225+
221226
def _list_to_sentence(self, list, final_separator = "and"):
222227
if len(list) == 0:
223228
return ''
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,95 @@
11
import factory
2+
from datetime import timedelta
3+
from django.utils import timezone
24

35
from .user_factory import UserFactory
46
from ...models.response_set import ResponseSet
57

68

9+
def set_submitted_at_recently(response_set, create, extracted):
10+
response_set.submitted_at = timezone.now() - timedelta(
11+
days=ResponseSet.RECENTLY_SUBMITTED_PERIOD_DAYS - 1
12+
)
13+
14+
15+
def set_submitted_at_not_recently(response_set, create, extracted):
16+
response_set.submitted_at = timezone.now() - timedelta(
17+
days=ResponseSet.RECENTLY_SUBMITTED_PERIOD_DAYS + 1
18+
)
19+
20+
721
class ResponseSetFactory(factory.django.DjangoModelFactory):
822
class Meta:
923
model = ResponseSet
1024

1125
user = factory.SubFactory(UserFactory)
26+
27+
class Params:
28+
complete = factory.Trait(
29+
asbestos_exposure_response=factory.RelatedFactory(
30+
"lung_cancer_screening.questions.tests.factories.asbestos_exposure_response_factory.AsbestosExposureResponseFactory",
31+
factory_related_name="response_set"
32+
),
33+
cancer_diagnosis_response=factory.RelatedFactory(
34+
"lung_cancer_screening.questions.tests.factories.cancer_diagnosis_response_factory.CancerDiagnosisResponseFactory",
35+
factory_related_name="response_set"
36+
),
37+
check_need_appointment_response=factory.RelatedFactory(
38+
"lung_cancer_screening.questions.tests.factories.check_need_appointment_response_factory.CheckNeedAppointmentResponseFactory",
39+
factory_related_name="response_set"
40+
),
41+
date_of_birth_response=factory.RelatedFactory(
42+
"lung_cancer_screening.questions.tests.factories.date_of_birth_response_factory.DateOfBirthResponseFactory",
43+
factory_related_name="response_set"
44+
),
45+
education_response=factory.RelatedFactory(
46+
"lung_cancer_screening.questions.tests.factories.education_response_factory.EducationResponseFactory",
47+
factory_related_name="response_set"
48+
),
49+
ethnicity_response=factory.RelatedFactory(
50+
"lung_cancer_screening.questions.tests.factories.ethnicity_response_factory.EthnicityResponseFactory",
51+
factory_related_name="response_set"
52+
),
53+
family_history_lung_cancer_response=factory.RelatedFactory(
54+
"lung_cancer_screening.questions.tests.factories.family_history_lung_cancer_response_factory.FamilyHistoryLungCancerResponseFactory",
55+
factory_related_name="response_set"
56+
),
57+
gender_response=factory.RelatedFactory(
58+
"lung_cancer_screening.questions.tests.factories.gender_response_factory.GenderResponseFactory",
59+
factory_related_name="response_set"
60+
),
61+
have_you_ever_smoked_response=factory.RelatedFactory(
62+
"lung_cancer_screening.questions.tests.factories.have_you_ever_smoked_response_factory.HaveYouEverSmokedResponseFactory",
63+
factory_related_name="response_set"
64+
),
65+
height_response=factory.RelatedFactory(
66+
"lung_cancer_screening.questions.tests.factories.height_response_factory.HeightResponseFactory",
67+
factory_related_name="response_set"
68+
),
69+
relatives_age_when_diagnosed_response=factory.RelatedFactory(
70+
"lung_cancer_screening.questions.tests.factories.relatives_age_when_diagnosed_response_factory.RelativesAgeWhenDiagnosedResponseFactory",
71+
factory_related_name="response_set"
72+
),
73+
respiratory_conditions_response=factory.RelatedFactory(
74+
"lung_cancer_screening.questions.tests.factories.respiratory_conditions_response_factory.RespiratoryConditionsResponseFactory",
75+
factory_related_name="response_set"
76+
),
77+
sex_at_birth_response=factory.RelatedFactory(
78+
"lung_cancer_screening.questions.tests.factories.sex_at_birth_response_factory.SexAtBirthResponseFactory",
79+
factory_related_name="response_set"
80+
),
81+
weight_response=factory.RelatedFactory(
82+
"lung_cancer_screening.questions.tests.factories.weight_response_factory.WeightResponseFactory",
83+
factory_related_name="response_set"
84+
),
85+
)
86+
87+
not_recently_submitted = factory.Trait(
88+
complete=True,
89+
submitted_at=factory.PostGeneration(set_submitted_at_not_recently)
90+
)
91+
92+
recently_submitted = factory.Trait(
93+
complete=True,
94+
submitted_at=factory.PostGeneration(set_submitted_at_recently)
95+
)

lung_cancer_screening/questions/tests/unit/models/test_family_history_lung_cancer_response.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ def test_has_value_as_string(self):
4040
self.assertIsInstance(response.value, str)
4141

4242

43+
def test_is_truthy_returns_true_if_value_is_yes(self):
44+
response = FamilyHistoryLungCancerResponseFactory(
45+
value=FamilyHistoryLungCancerValues.YES
46+
)
47+
48+
self.assertTrue(response.is_truthy())
49+
50+
51+
def test_is_truthy_returns_false_if_value_is_no(self):
52+
response = FamilyHistoryLungCancerResponseFactory(
53+
value=FamilyHistoryLungCancerValues.NO
54+
)
55+
56+
self.assertFalse(response.is_truthy())
57+
58+
4359
def test_deletes_family_age_when_diagnosed_if_response_is_no(self):
4460
RelativesAgeWhenDiagnosedResponseFactory.create(response_set=self.response_set)
4561

lung_cancer_screening/questions/tests/unit/models/test_response_set.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from django.test import TestCase
1+
from django.test import TestCase, tag
22
from datetime import datetime
3-
from dateutil.relativedelta import relativedelta
43
from django.utils import timezone
54

65
from django.core.exceptions import ValidationError
@@ -9,7 +8,10 @@
98
from ...factories.response_set_factory import ResponseSetFactory
109
from ....models.user import User
1110
from ....models.response_set import ResponseSet
11+
from ....models.family_history_lung_cancer_response import FamilyHistoryLungCancerValues
1212

13+
14+
@tag("ResponseSet")
1315
class TestResponseSet(TestCase):
1416
def setUp(self):
1517
self.user = UserFactory()
@@ -67,20 +69,38 @@ def test_is_invalid_if_another_unsubmitted_response_set_exists(self):
6769
)
6870

6971
def test_is_invalid_if_another_response_set_was_submitted_within_the_recently_submitted_period(self):
70-
user = UserFactory()
71-
user.responseset_set.create(
72-
submitted_at=timezone.now() - relativedelta(days=ResponseSet.RECENTLY_SUBMITTED_PERIOD_DAYS - 1)
73-
)
72+
response_set = ResponseSetFactory.create(recently_submitted=True)
7473

7574
with self.assertRaises(ValidationError) as context:
76-
user.responseset_set.create()
75+
response_set.user.responseset_set.create()
7776

7877
self.assertEqual(
7978
context.exception.messages[0],
8079
"Responses have already been submitted for this user"
8180
)
8281

83-
# Query managers
82+
83+
def test_Saving_a_submitted_response_Set_does_not_included_itself_in_unsubmitted_response_sets_validation(self):
84+
response_set = ResponseSetFactory.create(complete=True)
85+
response_set.submitted_at = timezone.now()
86+
87+
response_set.full_clean()
88+
89+
90+
def test_submitted_response_set_is_invalid_if_incomplete(self):
91+
self.response_set.submitted_at = timezone.now()
92+
93+
with self.assertRaises(ValidationError) as context:
94+
self.response_set.full_clean()
95+
96+
self.assertEqual(
97+
context.exception.messages[0],
98+
"Response set must be complete before it can be submitted"
99+
)
100+
101+
102+
# Query managers
103+
84104
def test_objects_returns_all_response_sets(self):
85105
unsubmitted_response_set = ResponseSetFactory()
86106
submitted_response_set = ResponseSetFactory(submitted_at=timezone.now())
@@ -95,9 +115,10 @@ def test_objects_returns_all_response_sets(self):
95115
response_sets,
96116
)
97117

118+
98119
def test_unsubmitted_returns_only_unsubmitted_response_sets(self):
99120
unsubmitted_response_set = ResponseSetFactory()
100-
submitted_response_set = ResponseSetFactory(submitted_at=timezone.now())
121+
submitted_response_set = ResponseSetFactory(recently_submitted=True)
101122

102123
unsubmitted_response_sets = ResponseSet.objects.unsubmitted().all()
103124
self.assertIn(
@@ -111,7 +132,7 @@ def test_unsubmitted_returns_only_unsubmitted_response_sets(self):
111132

112133
def test_submitted_returns_only_submitted_response_sets(self):
113134
unsubmitted_response_set = ResponseSetFactory()
114-
submitted_response_set = ResponseSetFactory(submitted_at=timezone.now() - relativedelta(years=1))
135+
submitted_response_set = ResponseSetFactory(not_recently_submitted=True)
115136

116137
submitted_response_sets = ResponseSet.objects.submitted().all()
117138
self.assertIn(
@@ -125,14 +146,10 @@ def test_submitted_returns_only_submitted_response_sets(self):
125146

126147
def test_submitted_recently_returns_only_submitted_response_sets_in_the_recently_submitted_period(self):
127148
recently_submitted_response = ResponseSetFactory(
128-
submitted_at=timezone.now() - relativedelta(
129-
days=ResponseSet.RECENTLY_SUBMITTED_PERIOD_DAYS - 1
130-
)
149+
recently_submitted=True
131150
)
132151
old_submitted_response = ResponseSetFactory(
133-
submitted_at=timezone.now() - relativedelta(
134-
days=ResponseSet.RECENTLY_SUBMITTED_PERIOD_DAYS + 1
135-
)
152+
not_recently_submitted=True
136153
)
137154

138155
recently_submitted_response_sets = ResponseSet.objects.recently_submitted().all()
@@ -144,3 +161,29 @@ def test_submitted_recently_returns_only_submitted_response_sets_in_the_recently
144161
old_submitted_response,
145162
recently_submitted_response_sets,
146163
)
164+
165+
166+
def test_is_complete_returns_true_if_all_questions_are_answered(self):
167+
response_set = ResponseSetFactory.create(complete=True)
168+
169+
self.assertTrue(response_set.is_complete())
170+
171+
172+
def test_is_complete_returns_false_if_a_single_question_is_not_answered(self):
173+
response_set = ResponseSetFactory.create(complete=True)
174+
response_set.asbestos_exposure_response.delete()
175+
response_set.refresh_from_db()
176+
177+
self.assertFalse(response_set.is_complete())
178+
179+
180+
def test_is_complete_returns_true_if_family_history_cancer_no_and_none_age_diagnosed(self):
181+
response_set = ResponseSetFactory.create(complete=True)
182+
183+
family_history = response_set.family_history_lung_cancer
184+
family_history.value = FamilyHistoryLungCancerValues.NO
185+
family_history.save()
186+
187+
response_set.refresh_from_db()
188+
189+
self.assertTrue(response_set.is_complete())

0 commit comments

Comments
 (0)