From 59e3c42b9066d9e2c4d51a0b5647bcfaf7b21c84 Mon Sep 17 00:00:00 2001 From: Jamie Falcus <50366804+jamiefalcus@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:49:06 +0000 Subject: [PATCH] PPHA-669-terms-of-use-pages --- features/agree_terms_of_use.feature | 21 +++ features/questionnaire.feature | 3 + .../core/jinja2/layout.jinja | 23 ++- lung_cancer_screening/jinja2_env.py | 10 +- .../nhsuk_forms/choice_field.py | 4 + .../nhsuk_forms/jinja2/checkbox.jinja | 35 ++++ .../forms/agree_terms_of_use_form.py | 22 +++ .../questions/jinja2/agree_terms_of_use.jinja | 7 + .../questions/jinja2/start.jinja | 2 +- .../questions/jinja2/terms_of_use.jinja | 170 ++++++++++++++++++ .../migrations/0007_termsofuseresponse.py | 27 +++ .../questions/models/__init__.py | 1 + .../questions/models/terms_of_use_response.py | 11 ++ .../terms_of_use_response_factory.py | 21 +++ .../forms/test_agree_terms_of_use_form.py | 55 ++++++ .../test_agree_terms_of_use_response.py | 52 ++++++ .../tests/unit/views/test_terms_of_use.py | 113 ++++++++++++ lung_cancer_screening/questions/urls.py | 3 + .../questions/views/agree_terms_of_use.py | 33 ++++ .../questions/views/have_you_ever_smoked.py | 2 +- scripts/tests/unit.sh | 7 +- 21 files changed, 613 insertions(+), 9 deletions(-) create mode 100644 features/agree_terms_of_use.feature create mode 100644 lung_cancer_screening/nhsuk_forms/jinja2/checkbox.jinja create mode 100644 lung_cancer_screening/questions/forms/agree_terms_of_use_form.py create mode 100644 lung_cancer_screening/questions/jinja2/agree_terms_of_use.jinja create mode 100644 lung_cancer_screening/questions/jinja2/terms_of_use.jinja create mode 100644 lung_cancer_screening/questions/migrations/0007_termsofuseresponse.py create mode 100644 lung_cancer_screening/questions/models/terms_of_use_response.py create mode 100644 lung_cancer_screening/questions/tests/factories/terms_of_use_response_factory.py create mode 100644 lung_cancer_screening/questions/tests/unit/forms/test_agree_terms_of_use_form.py create mode 100644 lung_cancer_screening/questions/tests/unit/models/test_agree_terms_of_use_response.py create mode 100644 lung_cancer_screening/questions/tests/unit/views/test_terms_of_use.py create mode 100644 lung_cancer_screening/questions/views/agree_terms_of_use.py diff --git a/features/agree_terms_of_use.feature b/features/agree_terms_of_use.feature new file mode 100644 index 00000000..77ef896a --- /dev/null +++ b/features/agree_terms_of_use.feature @@ -0,0 +1,21 @@ +@TermsOfUse +Feature: Check if you need an appointment page + Scenario: The page is accessible + Given I am logged in + When I go to "/agree-terms-of-use" + Then there are no accessibility violations + + Scenario: Form errors + Given I am logged in + When I go to "/agree-terms-of-use" + And I click "Continue" + Then I am on "/agree-terms-of-use" + And I see a form error "Agree to the terms of use to continue" + And there are no accessibility violations + + Scenario: Navigating backwards and forwards + Given I am logged in + When I go to "/agree-terms-of-use" + Then I see a back link to "/start" + When I check "I agree" and submit + Then I am on "/have-you-ever-smoked" diff --git a/features/questionnaire.feature b/features/questionnaire.feature index 1efa5ea1..4643e666 100644 --- a/features/questionnaire.feature +++ b/features/questionnaire.feature @@ -17,6 +17,9 @@ Feature: Questionnaire When I go to "/start" And I click "Continue" + Then I am on "/agree-terms-of-use" + When I check "I agree" and submit + Then I am on "/have-you-ever-smoked" When I fill in and submit my smoking status with "Yes, I used to smoke" diff --git a/lung_cancer_screening/core/jinja2/layout.jinja b/lung_cancer_screening/core/jinja2/layout.jinja index d8530ab0..0820f905 100644 --- a/lung_cancer_screening/core/jinja2/layout.jinja +++ b/lung_cancer_screening/core/jinja2/layout.jinja @@ -6,6 +6,23 @@ {% block head %} + {% endblock %} @@ -63,7 +80,11 @@ { "href": url("questions:privacy_policy"), "text": "Privacy policy" - } + }, + { + "href": url("questions:terms_of_use"), + "text": "Terms of use" + }, ] } }) }} diff --git a/lung_cancer_screening/jinja2_env.py b/lung_cancer_screening/jinja2_env.py index bbbe4d1d..c31f3d29 100644 --- a/lung_cancer_screening/jinja2_env.py +++ b/lung_cancer_screening/jinja2_env.py @@ -38,8 +38,12 @@ def environment(**options): {"singularize": singularize} ) - env.filters.update( - {"singularize": singularize} - ) + env.filters['print'] = lambda x: "" + if (settings.DEBUG): + env.filters['print']=debug return env + +def debug(text): + print(text) + return '' diff --git a/lung_cancer_screening/nhsuk_forms/choice_field.py b/lung_cancer_screening/nhsuk_forms/choice_field.py index a95ab21b..c88c5154 100644 --- a/lung_cancer_screening/nhsuk_forms/choice_field.py +++ b/lung_cancer_screening/nhsuk_forms/choice_field.py @@ -87,6 +87,10 @@ def _template_name(widget): isinstance(widget, type) and issubclass(widget, widgets.Select) ) or isinstance(widget, widgets.Select): return "select.jinja" + elif ( + isinstance(widget, type) and issubclass(widget, widgets.CheckboxInput) + ) or isinstance(widget, widgets.CheckboxInput): + return "checkbox.jinja" class MultipleChoiceField(forms.MultipleChoiceField): diff --git a/lung_cancer_screening/nhsuk_forms/jinja2/checkbox.jinja b/lung_cancer_screening/nhsuk_forms/jinja2/checkbox.jinja new file mode 100644 index 00000000..63938298 --- /dev/null +++ b/lung_cancer_screening/nhsuk_forms/jinja2/checkbox.jinja @@ -0,0 +1,35 @@ +{% from 'nhsuk/components/checkboxes/macro.jinja' import checkboxes %} +{% set unbound_field = field.field %} +{% if field.errors %} + {% set error_message = {"text": field.errors | first} %} +{% endif %} +{% set ns = namespace(items=[]) %} +{% for value, text in unbound_field.choices %} + {% set hint_text = field.get_hint_for_choice(value) %} + {% set ns.items = ns.items + [{ + "id": field.auto_id ~ '_' ~ loop.index0, + "value": value, + "text": text, + "checked": value == field.value()|string, + "hint": { + "text": hint_text + } if hint_text else undefined + }] %} +{% endfor %} +{{ checkboxes({ + "name": field.html_name, + "idPrefix": field.auto_id, + "fieldset": { + "legend": { + "text": field.label, + "classes": unbound_field.label_classes, + "isPageHeading": unbound_field.label_is_page_heading + } + } if field.use_fieldset else none, + "errorMessage": error_message, + "classes": unbound_field.classes if unbound_field.classes, + "hint": { + "html": unbound_field.hint|e + } if unbound_field.hint, + "items": ns.items +}) }} diff --git a/lung_cancer_screening/questions/forms/agree_terms_of_use_form.py b/lung_cancer_screening/questions/forms/agree_terms_of_use_form.py new file mode 100644 index 00000000..c20701d9 --- /dev/null +++ b/lung_cancer_screening/questions/forms/agree_terms_of_use_form.py @@ -0,0 +1,22 @@ +from django import forms + +from ...nhsuk_forms.choice_field import ChoiceField + +from ..models.terms_of_use_response import TermsOfUseResponse + + +class TermsOfUseForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["value"] = ChoiceField( + choices=[(True, 'I agree')], + widget=forms.CheckboxInput, + label="I agree", + error_messages={ + 'required': 'Agree to the terms of use to continue', + 'invalid_choice': 'Agree to the terms of use to continue' + } + ) + class Meta: + model = TermsOfUseResponse + fields = ['value'] diff --git a/lung_cancer_screening/questions/jinja2/agree_terms_of_use.jinja b/lung_cancer_screening/questions/jinja2/agree_terms_of_use.jinja new file mode 100644 index 00000000..bc608dcd --- /dev/null +++ b/lung_cancer_screening/questions/jinja2/agree_terms_of_use.jinja @@ -0,0 +1,7 @@ +{% extends 'question_form.jinja' %} + +{% block prelude %} +

Accept terms of use

+ +

To continue, confirm that you have read and agree to the NHS Check if you need a lung scan terms of use (opens in new tab)

+{% endblock %} diff --git a/lung_cancer_screening/questions/jinja2/start.jinja b/lung_cancer_screening/questions/jinja2/start.jinja index 5f50a347..56d9efa2 100644 --- a/lung_cancer_screening/questions/jinja2/start.jinja +++ b/lung_cancer_screening/questions/jinja2/start.jinja @@ -42,7 +42,7 @@ {{ button({ "text": "Continue", - "href": url("questions:have_you_ever_smoked"), + "href": url("questions:agree_terms_of_use"), "classes": "nhsuk-button--login" }) }} diff --git a/lung_cancer_screening/questions/jinja2/terms_of_use.jinja b/lung_cancer_screening/questions/jinja2/terms_of_use.jinja new file mode 100644 index 00000000..d9beadd7 --- /dev/null +++ b/lung_cancer_screening/questions/jinja2/terms_of_use.jinja @@ -0,0 +1,170 @@ +{% extends 'layout.jinja' %} + +{% block content %} +
+
+

NHS check if you need a lung scan terms of use

+ + +

Information:

+ +

Version 3, 5 March 2026

+ + + +
+

Introduction

+ +
    +
  1. We (NHS England) have developed a digital service called NHS check if you need a lung scan. This is a digital way to access NHS lung cancer screening. This service is currently in pilot so is only available in certain areas, and if you are invited by your GP to take part.
  2. +

    +

    This service will not provide you with any results or information in relation to the lung cancer screening, you will need to complete your lung cancer screening by phone to receive a result.

    + +

    To find out more about who we are and our role, visit the NHS England website.

    + + +
  3. Contacting us: if you have any questions about the service, contact us by email: england.digitallungcancerscreening@nhs.net
  4. +
+
+
+

When these terms apply

+ +
    +
  1. Please read these terms of use and our privacy policy, cookies policy and accessibility statement. By continuing to use the NHS check if you need a lung scan, you agree to be bound by these terms.
  2. +
  3. We may, at any time and in our sole discretion, amend these terms for any reason. The latest version of our terms will be accessible through the NHS check if you need a lung scan.
  4. +
+
+
+

How to use the NHS check if you need a lung scan

+ +
    +
  1. NHS check if you need a lung scan is free. To use NHS check if you need a lung scan, you must be invited to take part by your GP. Your GP will send you a letter and an SMS with a link to access the NHS check if you need a lung scan pilot service in a web browser.
  2. +
  3. To use the NHS check if you need a lung scan you need an NHS login account with a medium level of identity verification. If you do not have an account or a medium level of identity verification, you will be able to set this up as part of your application. Find out more about NHS login.
  4. +
  5. If you access or attempt to access the NHS check if you need a lung scan from outside of England, you are responsible for complying with any local laws that apply to you in the country from which you are using NHS check if you need a lung scan.
  6. +
  7. The NHS check if you need a lung scan is split into different sections. You must answer all the questions in each section to enable us to process your information. More information on this and how your information is used and processed is in the privacy policy.
  8. +
  9. The NHS check if you need a lung scan can only be accessed if you are aged between 55 and 74, registered with a supported GP surgery, and you smoke or used to smoke tobacco.
  10. +
+
+ +
+

Accessing the NHS check if you need a lung scan

+ +
    +
  1. You are responsible for making all arrangements necessary for you to access the NHS check if you need a lung scan, including but not limited to:
  2. +
      +
    • a secure internet connection (see Cyber Aware website)
    • +
    • an appropriate device, operating system and browser
    • +
    • using your own virus protection software (and regularly updating it) when accessing and using the NHS check if you need a lung scan
    • +
    + +
+
+ +
+

Details about the NHS check if you need a lung scan

+ +
    +
  1. If you download, print or export any of your submitted information, you are responsible for ensuring that this is held securely, and we will not be liable for any associated disclosure of sensitive and personal data.
  2. +
  3. In order for you to receive the intended benefits of the NHS check if you need a lung scan, you must ensure that all data provided by you is complete and accurate.
  4. +
  5. Your GP health records are created and kept up to date by your GP and remain under your GP’s control. We do not hold or have access to any GP records and are unable to answer queries about them or provide hard copies.
  6. +
  7. The NHS check if you need a lung scan: +
      +
    • is not a substitute for seeking medical advice - always follow any medical advice given by your healthcare professionals
    • +
    • does not provide medical or clinical diagnostic services
    • +
    • does not arrange or guarantee further healthcare treatment. You remain responsible for booking further healthcare treatment via the phone service as directed by your GP.
    • +
    +
  8. +
  9. We are not responsible for any delay or lack of response by a GP surgery or for the outcome of any decision your GP may make about any follow-on treatment or advice. No information or results will be shared with GPs through NHS check if you need a lung scan, and we do not guarantee it will lead to onward healthcare.
  10. +
+
+ +
+

Ending your use of the NHS check if you need a lung scan

+
    +
  1. You may stop using the NHS check if you need a lung scan at any time. If you fail to complete your NHS check if you need a lung scan within a set period of time your data will automatically be deleted. More information on how long your information is kept is in the privacy policy.
  2. +
+
+ +
+

Your right to use the NHS check if you need a lung scan

+
    +
  1. We own or have the right to use all intellectual property rights used for the provision of the NHS check if you need a lung scan, including rights in copyright, patents, database rights, trademarks and other intellectual property rights, ("NHS IPR").
  2. +
  3. 7.2. You have permission to use the NHS check if you need a lung scan for the sole purposes described in these terms and must not use it in any other way.
  4. +
  5. 7.3. Unless permitted by law or under these terms, you will:
  6. +
      +
    • not copy the NHS check if you need a lung scan or any NHS IPR, except where such copying is incidental to normal use
    • +
    • not rent, lease, sub-license, loan, translate, merge, adapt or modify the NHS check if you need a lung scan or any NHS IPR
    • +
    • not combine or incorporate the NHS check if you need a lung scan in any other programmes or services
    • +
    • not disassemble, decompile, reverse-engineer or create derivative works based on the whole or any part of the NHS check if you need a lung scan or other NHS IPR
    • +
    • comply with all technology control or export laws that apply to the technology used by the NHS check if you need a lung scan or any other NHS IPR
    • +
    + +
+
+ +
+

Prohibited uses

+
    +
  1. You may not use the NHS check if you need a lung scan: +
      +
    • to collect any data or attempt to decipher any transmissions to or from our servers
    • +
    • in a way that could damage, disable, overburden, impair or compromise our systems or security
    • +
    • to transmit any material that is insulting or offensive
    • +
    • in a way that interferes with other users
    • +
    • in any unlawful manner or for any unlawful purpose
    • +
    • in a manner that is improper use or inconsistent with these terms
    • +
    • to act fraudulently or maliciously by seeking to access or add data to another patient's GP record
    • +
    • to transmit, send or upload any data that contains viruses, Trojan horses, worms, spyware or any other harmful programs designed to adversely affect the operation of computer software or hardware
    • +
    • in connection with any kind of denial of service attack
    • +
    • on any device or operating system that has been modified outside the mobile device or operating system vendor supported or warranted configurations. This includes devices that have been "jail-broken" or "rooted"
    • +
    • with someone else's NHS login account
    • +
    +
  2. +
+

If you do any of the above acts you may also be committing a criminal offence, and we will report any such activity to the relevant law enforcement authorities. We will co-operate with those authorities by disclosing your identity to them.

+
+ +
+

Our liability to you

+
    +
  1. Although we make reasonable efforts to provide, maintain and update the NHS check if you need a lung scan it is provided "as is" and, to the extent permitted by law, we make no representations, warranties or guarantees, whether express or implied (including but not limited to the implied warranties of satisfactory quality and fitness for a particular purpose), that the NHS check if you need a lung scan: +
      +
    • is accurate, complete or up-to-date
    • +
    • will meet your particular requirements or needs
    • +
    • will always be available, error free, uninterrupted or free of viruses
    • +
    +
  2. We are not responsible for external links to or from the NHS check if you need a lung scan and cannot guarantee these will always work.
  3. +
  4. Nothing in these terms excludes or limits our liability for:
  5. +
      +
    • death or personal injury arising from our negligence
    • +
    • fraud or fraudulent misrepresentation
    • +
    • any loss or damage to a device or digital content belonging to you, if you can show that a) this was caused by NHS check if you need a lung scan and b) we failed use to use reasonable skill and care to prevent this
    • +
    • any other liability that cannot be excluded or limited under English law
    • +
    + +
  6. Subject to clause 9.3 of these terms, we will not be liable or responsible to you or any other person for:
  7. +
      +
    • any harm, loss or damage suffered where this is not caused by i) our negligence or ii) our breach of these terms
    • +
    • any loss or damage arising from an inability to access or use the NHS check if you need a lung scan in whole or in part
    • +
    • any business loss (including but not limited to loss of profits, revenue, contracts, anticipated savings, data, goodwill or wasted expenditure)
    • +
    • any indirect or consequential losses that were not foreseeable to both you and us when you commenced using the NHS check if you need a lung scan (loss or damage is "foreseeable" if it was an obvious consequence of our breach or if it was recognised by you and us at the time we entered into the contract created by your use of the NHS check if you need a lung scan)
    • +
    + +
  8. This clause 9 does not affect any legal rights you may have as a consumer in relation to defective services or software. Advice about your legal rights is available from your local Citizen's Advice or Trading Standards Office.
  9. +
+ +
+ +
+

General

+
    +
  1. These terms, any instructions in the service, and any other terms or policies referenced, set out the entire agreement between you and us in respect of your use of the NHS check if you need a lung scan.
  2. +
  3. These terms do not give any rights to any third party to enforce any of these terms.
  4. +
  5. Each of the clauses and sub-clauses of these terms operates separately. If any part is determined to be invalid or unenforceable it will be superseded by a valid and enforceable provision that most closely matches the intent of the original and all other terms shall continue in effect.
  6. +
  7. Even if we delay in enforcing these terms, we can still enforce them later.
  8. +
  9. The laws of England shall apply exclusively to these terms and all matters relating to use of the NHS check if you need a lung scan, and any dispute shall be subject to the exclusive jurisdiction of the courts of England.
  10. +
+
+
+
+{% endblock %} diff --git a/lung_cancer_screening/questions/migrations/0007_termsofuseresponse.py b/lung_cancer_screening/questions/migrations/0007_termsofuseresponse.py new file mode 100644 index 00000000..a538358b --- /dev/null +++ b/lung_cancer_screening/questions/migrations/0007_termsofuseresponse.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.11 on 2026-03-09 15:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0006_alter_smokingfrequencyresponse_value'), + ] + + operations = [ + migrations.CreateModel( + name='TermsOfUseResponse', + fields=[ + ('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', models.BooleanField()), + ('response_set', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='terms_of_use_response', to='questions.responseset')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/lung_cancer_screening/questions/models/__init__.py b/lung_cancer_screening/questions/models/__init__.py index 5ed6ceda..97c0536a 100644 --- a/lung_cancer_screening/questions/models/__init__.py +++ b/lung_cancer_screening/questions/models/__init__.py @@ -19,5 +19,6 @@ from .smoking_current_response import SmokingCurrentResponse # noqa: F401 from .smoking_frequency_response import SmokingFrequencyResponse # noqa: F401 from .smoked_amount_response import SmokedAmountResponse # noqa: F401 +from .terms_of_use_response import TermsOfUseResponse # noqa: F401 from .tobacco_smoking_history import TobaccoSmokingHistory # noqa: F401 from .weight_response import WeightResponse # noqa: F401 diff --git a/lung_cancer_screening/questions/models/terms_of_use_response.py b/lung_cancer_screening/questions/models/terms_of_use_response.py new file mode 100644 index 00000000..3a559a05 --- /dev/null +++ b/lung_cancer_screening/questions/models/terms_of_use_response.py @@ -0,0 +1,11 @@ +from django.db import models + +from .base import BaseModel +from .response_set import ResponseSet + +class TermsOfUseResponse(BaseModel): + response_set = models.OneToOneField(ResponseSet, on_delete=models.CASCADE, related_name='terms_of_use_response') + value = models.BooleanField() + + def has_accepted(self): + return self.value diff --git a/lung_cancer_screening/questions/tests/factories/terms_of_use_response_factory.py b/lung_cancer_screening/questions/tests/factories/terms_of_use_response_factory.py new file mode 100644 index 00000000..2e0e8c78 --- /dev/null +++ b/lung_cancer_screening/questions/tests/factories/terms_of_use_response_factory.py @@ -0,0 +1,21 @@ +import factory + +from .response_set_factory import ResponseSetFactory +from ...models.terms_of_use_response import TermsOfUseResponse + + +class TermsOfUseResponseFactory(factory.django.DjangoModelFactory): + class Meta: + model = TermsOfUseResponse + + response_set = factory.SubFactory(ResponseSetFactory) + value = factory.Faker('boolean') + + class Params: + accepted = factory.Trait( + value=True + ) + + not_accepted = factory.Trait( + value=False + ) diff --git a/lung_cancer_screening/questions/tests/unit/forms/test_agree_terms_of_use_form.py b/lung_cancer_screening/questions/tests/unit/forms/test_agree_terms_of_use_form.py new file mode 100644 index 00000000..985dd9c2 --- /dev/null +++ b/lung_cancer_screening/questions/tests/unit/forms/test_agree_terms_of_use_form.py @@ -0,0 +1,55 @@ +from django.test import TestCase, tag + +from ....models.terms_of_use_response import TermsOfUseResponse + +from ...factories.response_set_factory import ResponseSetFactory +from ....forms.agree_terms_of_use_form import TermsOfUseForm + +@tag("TermsOfUse") +class TestAgreeTermsOfUseForm(TestCase): + def setUp(self): + self.response_set = ResponseSetFactory() + self.response = TermsOfUseResponse.objects.create( + response_set=self.response_set, + value=False + ) + + + def test_is_valid_with_a_valid_value(self): + form = TermsOfUseForm( + instance=self.response, + data={ + "value": True + } + ) + self.assertTrue(form.is_valid()) + self.assertEqual( + form.data["value"], + True + ) + + def test_is_invalid_with_an_invalid_value(self): + form = TermsOfUseForm( + instance=self.response, + data={ + "value": False + } + ) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors["value"], + ["Agree to the terms of use to continue"] + ) + + def test_is_invalid_when_no_option_is_selected(self): + form = TermsOfUseForm( + instance=self.response, + data={ + "value": None + } + ) + self.assertFalse(form.is_valid()) + self.assertEqual( + form.errors["value"], + ["Agree to the terms of use to continue"] + ) diff --git a/lung_cancer_screening/questions/tests/unit/models/test_agree_terms_of_use_response.py b/lung_cancer_screening/questions/tests/unit/models/test_agree_terms_of_use_response.py new file mode 100644 index 00000000..64d94cec --- /dev/null +++ b/lung_cancer_screening/questions/tests/unit/models/test_agree_terms_of_use_response.py @@ -0,0 +1,52 @@ +from django.test import TestCase, tag + +from ...factories.response_set_factory import ResponseSetFactory +from ...factories.terms_of_use_response_factory import TermsOfUseResponseFactory + +from ....models.terms_of_use_response import TermsOfUseResponse + +@tag("TermsOfUse") +class TestTermsOfUseResponse(TestCase): + def setUp(self): + self.response_set = ResponseSetFactory() + + def test_has_a_valid_factory(self): + model = TermsOfUseResponseFactory.build(response_set=self.response_set) + model.full_clean() + + + def test_has_response_set_as_foreign_key(self): + response_set = ResponseSetFactory() + response = TermsOfUseResponse.objects.create( + response_set=response_set, + value=True + ) + + self.assertEqual(response.response_set, response_set) + + def test_has_value_as_bool(self): + response_set = ResponseSetFactory() + response = TermsOfUseResponse.objects.create( + response_set=response_set, + value=False + ) + + self.assertIsInstance(response.value, bool) + + + def test_has_accepted_returns_true_when_value_is_true(self): + response = TermsOfUseResponse.objects.create( + response_set=self.response_set, + value=True + ) + + self.assertTrue(response.has_accepted()) + + + def test_has_accepted_returns_false_when_value_is_false(self): + response = TermsOfUseResponse.objects.create( + response_set=self.response_set, + value=False + ) + + self.assertFalse(response.has_accepted()) diff --git a/lung_cancer_screening/questions/tests/unit/views/test_terms_of_use.py b/lung_cancer_screening/questions/tests/unit/views/test_terms_of_use.py new file mode 100644 index 00000000..4287570a --- /dev/null +++ b/lung_cancer_screening/questions/tests/unit/views/test_terms_of_use.py @@ -0,0 +1,113 @@ +from django.test import TestCase, tag +from django.urls import reverse + +from ...factories.terms_of_use_response_factory import TermsOfUseResponseFactory + +from .helpers.authentication import login_user +from ...factories.response_set_factory import ResponseSetFactory + +@tag("TermsOfUse") +class TestGetAgreeTermsOfUse(TestCase): + def setUp(self): + self.user = login_user(self.client) + + self.response_set = ResponseSetFactory.create(user=self.user) + + + def test_redirects_if_the_user_is_not_logged_in(self): + self.client.logout() + + response = self.client.get( + reverse("questions:agree_terms_of_use") + ) + + self.assertRedirects( + response, + "/oidc/authenticate/?next=/agree-terms-of-use", + fetch_redirect_response=False + ) + + + def test_redirects_when_a_submitted_response_set_exists_within_the_last_year(self): + self.response_set.delete() + ResponseSetFactory.create( + user=self.user, + recently_submitted=True + ) + + response = self.client.get( + reverse("questions:agree_terms_of_use") + ) + + self.assertRedirects(response, reverse("questions:confirmation")) + + + def test_responds_successfully(self): + TermsOfUseResponseFactory.create( + response_set=self.response_set, accepted=True + ) + + response = self.client.get(reverse("questions:agree_terms_of_use")) + + self.assertEqual(response.status_code, 200) + + +@tag("TermsOfUse") +class TestPostAgreeTermsOfUse(TestCase): + def setUp(self): + self.user = login_user(self.client) + self.response_set = ResponseSetFactory.create(user=self.user) + + self.valid_params = {"value": True} + + + def test_redirects_if_the_user_is_not_logged_in(self): + self.client.logout() + + response = self.client.post( + reverse("questions:agree_terms_of_use"), + self.valid_params + ) + + self.assertRedirects(response, "/oidc/authenticate/?next=/agree-terms-of-use", fetch_redirect_response=False) + + + def test_redirects_when_a_submitted_response_set_exists_within_the_last_year(self): + self.response_set.delete() + ResponseSetFactory.create( + user=self.user, + recently_submitted=True + ) + + response = self.client.post( + reverse("questions:agree_terms_of_use"), + self.valid_params + ) + + self.assertRedirects(response, reverse("questions:confirmation")) + + + def test_redirects_to_the_next_page(self): + TermsOfUseResponseFactory.create( + response_set=self.response_set, accepted=True + ) + + response = self.client.post( + reverse("questions:agree_terms_of_use"), + self.valid_params + ) + + self.assertRedirects(response, reverse("questions:have_you_ever_smoked"), fetch_redirect_response=False) + + + def test_responds_with_422_if_the_response_fails_to_create(self): + TermsOfUseResponseFactory.create( + response_set=self.response_set, accepted=True + ) + + response = self.client.post( + reverse("questions:agree_terms_of_use"), + {"value": False} + ) + + self.assertEqual(response.status_code, 422) diff --git a/lung_cancer_screening/questions/urls.py b/lung_cancer_screening/questions/urls.py index a32af907..236389ca 100644 --- a/lung_cancer_screening/questions/urls.py +++ b/lung_cancer_screening/questions/urls.py @@ -45,11 +45,13 @@ from .views.start import StartView from .views.weight import WeightView from .views.confirmation import ConfirmationView +from .views.agree_terms_of_use import AgreeTermsOfUseView urlpatterns = [ path('', RedirectView.as_view(url='/start'), name='root'), path('age-range-exit', AgeRangeExitView.as_view(), name='age_range_exit'), path('age-when-started-smoking', AgeWhenStartedSmokingView.as_view(), name='age_when_started_smoking'), + path('agree-terms-of-use', AgreeTermsOfUseView.as_view(), name='agree_terms_of_use'), path("agree-to-share-information", TemplateView.as_view(template_name="agree_to_share_information.jinja"), name="agree_to_share_information"), path('asbestos-exposure', AsbestosExposureView.as_view(), name='asbestos_exposure'), path('call-us-to-book-an-appointment', BookAnAppointmentExitView.as_view(), name='book_an_appointment'), @@ -78,6 +80,7 @@ path('check-your-answers', ResponsesView.as_view(), name='responses'), path('sex-at-birth', SexAtBirthView.as_view(), name='sex_at_birth'), path('start', StartView.as_view(), name='start'), + path('terms-of-use', TemplateView.as_view(template_name='terms_of_use.jinja'), name='terms_of_use'), path('weight', WeightView.as_view(), name='weight'), path('confirmation', ConfirmationView.as_view(), name='confirmation'), path('privacy-policy', TemplateView.as_view(template_name='privacy_policy.jinja'), name='privacy_policy'), diff --git a/lung_cancer_screening/questions/views/agree_terms_of_use.py b/lung_cancer_screening/questions/views/agree_terms_of_use.py new file mode 100644 index 00000000..3ea4cece --- /dev/null +++ b/lung_cancer_screening/questions/views/agree_terms_of_use.py @@ -0,0 +1,33 @@ +from django.urls import reverse, reverse_lazy +from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import redirect + +from .mixins.ensure_response_set import EnsureResponseSet +from .question_base_view import QuestionBaseView +from ..forms.agree_terms_of_use_form import TermsOfUseForm +from ..models.terms_of_use_response import TermsOfUseResponse + + +class EnsureAcceptedTermsEligible: + def dispatch(self, request, *args, **kwargs): + if ( + not hasattr(request.response_set, "terms_of_use_response") + or not request.response_set.terms_of_use_response.has_accepted() + ): + return redirect(reverse("questions:agree_terms_of_use")) + else: + return super().dispatch(request, *args, **kwargs) + + +class AgreeTermsOfUseView(LoginRequiredMixin, EnsureResponseSet, QuestionBaseView): + template_name = "agree_terms_of_use.jinja" + form_class = TermsOfUseForm + model = TermsOfUseResponse + success_url = reverse_lazy("questions:have_you_ever_smoked") + back_link_url = reverse_lazy("questions:start") + + def get_success_url(self): + if self.object.value: + return reverse("questions:have_you_ever_smoked") + else: + return super().get_success_url() diff --git a/lung_cancer_screening/questions/views/have_you_ever_smoked.py b/lung_cancer_screening/questions/views/have_you_ever_smoked.py index c3168587..c54f04b6 100644 --- a/lung_cancer_screening/questions/views/have_you_ever_smoked.py +++ b/lung_cancer_screening/questions/views/have_you_ever_smoked.py @@ -14,7 +14,7 @@ class HaveYouEverSmokedView(LoginRequiredMixin, EnsureResponseSet, QuestionBaseV form_class = HaveYouEverSmokedForm model = HaveYouEverSmokedResponse success_url = reverse_lazy("questions:date_of_birth") - back_link_url = reverse_lazy("questions:start") + back_link_url = reverse_lazy("questions:agree_terms_of_use") def get_success_url(self): if self.object.has_smoked_regularly(): diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index a379966f..6573b06d 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -21,8 +21,10 @@ cd "$(git rev-parse --show-toplevel)" if [[ -n "${TAG:-}" ]]; then TAG="--tag=$TAG" + COVERAGE="" else TAG="" + COVERAGE="&& coverage report -m --skip-covered" fi if [[ -n "${TEST_MODULE:-}" ]]; then @@ -38,6 +40,5 @@ fi env UID="$(id -u)" docker compose run --rm web sh -c " \ poetry run coverage run manage.py test $TEST_MODULE $TAG \ --settings=lung_cancer_screening.settings_test \ - --exclude-tag=accessibility && \ - coverage report -m --skip-covered \ -" + --exclude-tag=accessibility \ + $COVERAGE"