From 9aed82da35efd1b5fae04af380cc66a99ef07658 Mon Sep 17 00:00:00 2001 From: Ben Millar Date: Fri, 28 Feb 2025 01:48:35 +0000 Subject: [PATCH] WIP: Refactor means test to always use boolean values --- app/means_test/__init__.py | 8 ---- app/means_test/fields.py | 28 ++++++++++++- app/means_test/forms/about_you.py | 42 +++++++------------ app/means_test/forms/benefits.py | 3 +- app/means_test/forms/property.py | 37 ++++++++-------- app/means_test/partner_fields.py | 6 ++- app/session.py | 30 ++++++------- app/templates/means_test/about-you.html | 4 +- .../means_test/additional-benefits.html | 2 +- .../means_test/test_benefits_page.py | 5 +-- tests/unit_tests/means_test/test_property.py | 28 ++++++------- tests/unit_tests/means_test/test_session.py | 16 +++---- 12 files changed, 105 insertions(+), 104 deletions(-) diff --git a/app/means_test/__init__.py b/app/means_test/__init__.py index 88186eb89..ac6a2f337 100644 --- a/app/means_test/__init__.py +++ b/app/means_test/__init__.py @@ -2,12 +2,4 @@ bp = Blueprint("means_test", __name__) -YES = "1" -NO = "0" - - -def is_yes(value): - return YES == value - - from app.means_test import urls # noqa: E402,F401 diff --git a/app/means_test/fields.py b/app/means_test/fields.py index b1c1c6663..fcc4ab857 100644 --- a/app/means_test/fields.py +++ b/app/means_test/fields.py @@ -2,7 +2,8 @@ from decimal import Decimal, InvalidOperation from flask import render_template -from flask_babel import lazy_gettext as _ +from flask_babel import lazy_gettext as _, LazyString +from wtforms import RadioField from wtforms.widgets import TextInput from markupsafe import Markup from wtforms import Field, IntegerField as BaseIntegerField @@ -11,6 +12,31 @@ from app.means_test.validators import CurrencyValidator +class YesNoField(RadioField): + def __init__(self, label: LazyString = None, validators=None, **kwargs): + if "coerce" in kwargs: + raise ValueError("The 'coerce' parameter is not allowed in a YesNoField") + if "choices" in kwargs: + raise ValueError( + "Choices in a YesNoField are fixed and cannot be overridden" + ) + choices = [(True, _("Yes")), (False, _("No"))] + coerce = self._coerce_to_boolean + super(YesNoField, self).__init__( + label=label, validators=validators, choices=choices, coerce=coerce, **kwargs + ) # noqa + + @staticmethod + def _coerce_to_boolean(value): + # Converts the string value from the HTML request to a boolean + # "Yes" being True, "No" being False + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() in ("true", "yes", "y", "1", "t") + return bool(value) + + class MoneyIntervalWidget(TextInput): def __call__(self, field, *args, **kwargs): # Pass the current values to the template diff --git a/app/means_test/forms/about_you.py b/app/means_test/forms/about_you.py index 45c02877d..e257a2f40 100644 --- a/app/means_test/forms/about_you.py +++ b/app/means_test/forms/about_you.py @@ -1,4 +1,3 @@ -from wtforms.fields import RadioField from govuk_frontend_wtf.wtforms_widgets import GovTextInput from wtforms.validators import InputRequired, NumberRange from app.means_test.validators import ValidateIf @@ -6,7 +5,7 @@ from flask_babel import lazy_gettext as _ from app.means_test import YES, NO from app.means_test.forms import BaseMeansTestForm -from app.means_test.fields import IntegerField +from app.means_test.fields import IntegerField, YesNoField class AboutYouForm(BaseMeansTestForm): @@ -14,9 +13,8 @@ class AboutYouForm(BaseMeansTestForm): template = "means_test/about-you.html" - has_partner = RadioField( + has_partner = YesNoField( _("Do you have a partner?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_( "Your husband, wife, civil partner (unless you have permanently separated) or someone you live with as if you're married" @@ -24,12 +22,11 @@ class AboutYouForm(BaseMeansTestForm): validators=[InputRequired(message=_("Tell us whether you have a partner"))], ) - are_you_in_a_dispute = RadioField( + are_you_in_a_dispute = YesNoField( _("Are you in a dispute with your partner?"), description=_( "This means your partner is the opponent in the dispute you need help with, for example a dispute over money or property" ), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), validators=[ ValidateIf("has_partner", YES), @@ -39,17 +36,15 @@ class AboutYouForm(BaseMeansTestForm): ], ) - on_benefits = RadioField( + on_benefits = YesNoField( _("Do you receive any benefits (including Child Benefit)?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_("Being on some benefits can help you qualify for legal aid"), validators=[InputRequired(message=_("Tell us whether you receive benefits"))], ) - have_children = RadioField( + have_children = YesNoField( _("Do you have any children aged 15 or under?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_("Don't include any children who don't live with you"), validators=[ @@ -71,9 +66,8 @@ class AboutYouForm(BaseMeansTestForm): ], ) - have_dependents = RadioField( + have_dependents = YesNoField( _("Do you have any dependants aged 16 or over?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_( "People who you live with and support financially. This could be a young person for whom you get Child Benefit" @@ -95,17 +89,15 @@ class AboutYouForm(BaseMeansTestForm): ], ) - own_property = RadioField( + own_property = YesNoField( _("Do you own any property?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_("For example, a house, static caravan or flat"), validators=[InputRequired(message=_("Tell us if you own any properties"))], ) - is_employed = RadioField( + is_employed = YesNoField( _("Are you employed?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_( "This means working as an employee - you may be both employed and self-employed" @@ -113,12 +105,11 @@ class AboutYouForm(BaseMeansTestForm): validators=[InputRequired(message=_("Tell us if you are employed"))], ) - partner_is_employed = RadioField( + partner_is_employed = YesNoField( _("Is your partner employed?"), description=_( "This means working as an employee - your partner may be both employed and self-employed" ), - choices=[(YES, _("Yes")), (NO, _("No"))], validators=[ ValidateIf("are_you_in_a_dispute", NO), InputRequired(message=_("Tell us whether your partner is employed")), @@ -126,9 +117,8 @@ class AboutYouForm(BaseMeansTestForm): widget=MeansTestRadioInput(), ) - is_self_employed = RadioField( + is_self_employed = YesNoField( _("Are you self-employed?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_( "This means working for yourself - you may be both employed and self-employed" @@ -136,12 +126,11 @@ class AboutYouForm(BaseMeansTestForm): validators=[InputRequired(message=_("Tell us if you are self-employed"))], ) - partner_is_self_employed = RadioField( + partner_is_self_employed = YesNoField( _("Is your partner self-employed?"), description=_( "This means working for yourself - your partner may be both employed and self-employed" ), - choices=[(YES, _("Yes")), (NO, _("No"))], validators=[ ValidateIf("are_you_in_a_dispute", NO), InputRequired(message=_("Tell us whether your partner is self-employed")), @@ -149,9 +138,8 @@ class AboutYouForm(BaseMeansTestForm): widget=MeansTestRadioInput(), ) - aged_60_or_over = RadioField( + aged_60_or_over = YesNoField( _("Are you or your partner (if you have one) aged 60 or over?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), validators=[ InputRequired( @@ -160,18 +148,16 @@ class AboutYouForm(BaseMeansTestForm): ], ) - have_savings = RadioField( + have_savings = YesNoField( _("Do you have any savings or investments?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), validators=[ InputRequired(message=_("Tell us whether you have savings or investments")) ], ) - have_valuables = RadioField( + have_valuables = YesNoField( _("Do you have any valuable items worth over £500 each?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), validators=[ InputRequired( diff --git a/app/means_test/forms/benefits.py b/app/means_test/forms/benefits.py index b500cbc65..b2ff267e0 100644 --- a/app/means_test/forms/benefits.py +++ b/app/means_test/forms/benefits.py @@ -11,7 +11,7 @@ ValidateIf, ValidateIfType, ) -from app.means_test import YES, NO +from app.means_test import YES from dataclasses import dataclass, field @@ -194,7 +194,6 @@ class AdditionalBenefitsForm(BaseMeansTestForm): "Allowance" ), widget=MeansTestRadioInput(), - choices=[(YES, _("Yes")), (NO, _("No"))], validators=[ InputRequired(message=_("Tell us whether you receive any other benefits")) ], diff --git a/app/means_test/forms/property.py b/app/means_test/forms/property.py index 312ff1f87..6b167bdc2 100644 --- a/app/means_test/forms/property.py +++ b/app/means_test/forms/property.py @@ -1,14 +1,18 @@ from flask import session from wtforms import ValidationError -from wtforms.fields import RadioField, FieldList, FormField +from wtforms.fields import FieldList, FormField from wtforms.fields.core import Label from govuk_frontend_wtf.wtforms_widgets import GovTextInput from wtforms.validators import InputRequired, NumberRange from app.means_test.widgets import MeansTestRadioInput from flask_babel import lazy_gettext as _ -from app.means_test import YES, NO from app.means_test.forms import BaseMeansTestForm -from app.means_test.fields import MoneyIntervalField, MoneyIntervalWidget, MoneyField +from app.means_test.fields import ( + MoneyIntervalField, + MoneyIntervalWidget, + MoneyField, + YesNoField, +) from app.means_test.validators import MoneyIntervalAmountRequired from app.means_test.validators import ValidateIf from app.means_test.money_interval import MoneyInterval, to_amount @@ -22,10 +26,10 @@ def val(field): return form_data.get(field) def yes(field): - return form_data.get(field) == YES + return form_data.get(field) def no(field): - return form_data.get(field) == NO + return not form_data.get(field) self.update( { @@ -84,14 +88,14 @@ def validate_single_main_home(form, field): properties = form.properties.data main_home_count = 0 for property_data in properties: - if property_data.get("is_main_home") == "True": + if property_data.get("is_main_home"): main_home_count += 1 if main_home_count > 1: raise ValidationError(_("You can only have 1 main property")) -class PartnerRadioField(RadioField): +class PartnerYesNoFieldField(YesNoField): def __init__(self, label, partner_label, *args, **kwargs): super().__init__(*args, **kwargs) self._label = label @@ -116,9 +120,8 @@ class PropertyForm(BaseMeansTestForm): def has_partner(self): return session.get_eligibility().has_partner - is_main_home = RadioField( + is_main_home = YesNoField( _("Is this property your main home?"), - choices=[(True, _("Yes")), (False, _("No"))], widget=MeansTestRadioInput(), description=_( "If you’re temporarily living away from the property, select ‘Yes’" @@ -126,12 +129,11 @@ def has_partner(self): validators=[InputRequired(message=_("Tell us whether this is your main home"))], ) - other_shareholders = PartnerRadioField( + other_shareholders = PartnerYesNoFieldField( partner_label=_( "Does anyone else (other than you or your partner) own a share of the property?" ), label=_("Does anyone else own a share of the property?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), description=_( "Select ‘Yes’ if you share ownership with a friend, relative or ex-partner" @@ -182,9 +184,8 @@ def has_partner(self): ], ) - is_rented = RadioField( + is_rented = YesNoField( _("Do you rent out any part of this property?"), - choices=[(YES, _("Yes")), (NO, _("No"))], widget=MeansTestRadioInput(), validators=[ InputRequired( @@ -199,7 +200,7 @@ def has_partner(self): exclude_intervals=["per_4week"], widget=MoneyIntervalWidget(), validators=[ - ValidateIf("is_rented", YES), + ValidateIf("is_rented", True), MoneyIntervalAmountRequired( message=_("Tell us how much rent you receive from this property"), freq_message=_("Tell us how often you receive this rent"), @@ -208,9 +209,8 @@ def has_partner(self): ], ) - in_dispute = RadioField( + in_dispute = YesNoField( _("Is your share of the property in dispute?"), - choices=[(True, _("Yes")), (False, _("No"))], widget=MeansTestRadioInput(), description=_("For example, as part of the financial settlement of a divorce"), validators=[ @@ -225,10 +225,7 @@ class MultiplePropertiesForm(BaseMeansTestForm): @classmethod def should_show(cls) -> bool: - return ( - session.get_eligibility().forms.get("about-you", {}).get("own_property") - == YES - ) + return session.get_eligibility().forms.get("about-you", {}).get("own_property") properties = FieldList( FormField(PropertyForm), # Each entry is an instance of PropertyForm diff --git a/app/means_test/partner_fields.py b/app/means_test/partner_fields.py index 3213d6dd8..d501be6d7 100644 --- a/app/means_test/partner_fields.py +++ b/app/means_test/partner_fields.py @@ -1,5 +1,7 @@ from flask import session -from wtforms.fields import SelectMultipleField, RadioField +from wtforms.fields import SelectMultipleField + +from app.means_test.fields import YesNoField class PartnerMixin(object): @@ -18,5 +20,5 @@ class PartnerMultiCheckboxField(PartnerMixin, SelectMultipleField): pass -class PartnerYesNoField(PartnerMixin, RadioField): +class PartnerYesNoField(PartnerMixin, YesNoField): pass diff --git a/app/session.py b/app/session.py index 47c6f3052..3e06826e4 100644 --- a/app/session.py +++ b/app/session.py @@ -30,55 +30,55 @@ def is_no(self, form_name, field_name) -> bool | None: @property def has_partner(self): - return self.is_yes("about-you", "has_partner") and not self.is_yes( - "about-you", "are_you_in_a_dispute" - ) + return self.forms.get("about-you", {}).get( + "has_partner", False + ) and not self.forms.get("about-you", {}).get("are_you_in_a_dispute", False) @property def is_employed(self): - return self.is_yes("about-you", "is_employed") + return self.forms.get("about-you", {}).get("is_employed", False) @property def is_self_employed(self): - return self.is_yes("about-you", "is_self_employed") + return self.forms.get("about-you", {}).get("is_self_employed", False) @property def is_employed_or_self_employed(self): - return self.is_yes("about-you", "is_employed") or self.is_yes( - "about-you", "is_self_employed" - ) + return self.forms.get("about-you", {}).get("is_employed") or self.forms.get( + "about-you", {} + ).get("is_self_employed", False) @property def is_partner_employed(self): if not self.has_partner: return False - return self.is_yes("about-you", "partner_is_employed") + return self.forms.get("about-you", {}).get("partner_is_employed") @property def is_partner_self_employed(self): if not self.has_partner: return False - return self.is_yes("about-you", "partner_is_self_employed") + return self.forms.get("about-you", {}).get("partner_is_self_employed") @property def has_savings(self): - return self.is_yes("about-you", "have_savings") + return self.forms.get("about-you", {}).get("have_savings", False) @property def has_valuables(self): - return self.is_yes("about-you", "have_valuables") + return self.forms.get("about-you", {}).get("have_valuables", False) @property def has_children(self) -> bool: - return self.is_yes("about-you", "have_children") + return self.forms.get("about-you", {}).get("have_children", False) @property def has_dependants(self) -> bool: - return self.is_yes("about-you", "have_dependents") + return self.forms.get("about-you", {}).get("have_dependents", False) @property def on_benefits(self) -> bool: - return self.is_yes("about-you", "on_benefits") + return self.forms.get("about-you", {}).get("on_benefits", False) @property def is_eligible_for_child_benefits(self) -> bool: diff --git a/app/templates/means_test/about-you.html b/app/templates/means_test/about-you.html index b835dfdf0..a174d0157 100644 --- a/app/templates/means_test/about-you.html +++ b/app/templates/means_test/about-you.html @@ -75,7 +75,7 @@ {{ form.partner_is_employed(params={ "attributes": { "data-controlled-by": "are_you_in_a_dispute", - "data-show-value": "0" + "data-show-value": "False" } }) }} @@ -84,7 +84,7 @@ {{ form.partner_is_self_employed(params={ "attributes": { "data-controlled-by": "are_you_in_a_dispute", - "data-show-value": "0" + "data-show-value": "False" } }) }} diff --git a/app/templates/means_test/additional-benefits.html b/app/templates/means_test/additional-benefits.html index c0191009d..5add5984c 100644 --- a/app/templates/means_test/additional-benefits.html +++ b/app/templates/means_test/additional-benefits.html @@ -5,7 +5,7 @@ {{ form.csrf_token }} {{ form.benefits }} - {{ form.render_conditional(form.other_benefits, form.total_other_benefit, "1") }} + {{ form.render_conditional(form.other_benefits, form.total_other_benefit, "True") }} {{ form.submit }} {% endblock %} \ No newline at end of file diff --git a/tests/functional_tests/means_test/test_benefits_page.py b/tests/functional_tests/means_test/test_benefits_page.py index 95cd2e57a..169f2a8e0 100644 --- a/tests/functional_tests/means_test/test_benefits_page.py +++ b/tests/functional_tests/means_test/test_benefits_page.py @@ -1,7 +1,6 @@ import pytest from flask import url_for from playwright.sync_api import Page, expect -from app.means_test import YES, NO next_page_heading = "Review your answers" @@ -105,7 +104,7 @@ def test_child_benefits_available_have_children(page: Page, client): with client.session_transaction() as session: # update the session session.get_eligibility().add( - "about-you", {"have_children": YES, "have_dependents": NO} + "about-you", {"have_children": True, "have_dependents": False} ) url = url_for("means_test.benefits", _external=True) @@ -123,7 +122,7 @@ def test_child_benefits_available_have_dependents(page: Page, client): with client.session_transaction() as session: # update the session session.get_eligibility().add( - "about-you", {"have_children": NO, "have_dependents": YES} + "about-you", {"have_children": False, "have_dependents": True} ) url = url_for("means_test.benefits", _external=True) diff --git a/tests/unit_tests/means_test/test_property.py b/tests/unit_tests/means_test/test_property.py index 9fe1d44b5..9d342fe61 100644 --- a/tests/unit_tests/means_test/test_property.py +++ b/tests/unit_tests/means_test/test_property.py @@ -7,18 +7,18 @@ def test_property_payload_with_valid_data(): form_data = { "submit": False, - "is_main_home": "True", - "other_shareholders": "0", + "is_main_home": True, + "other_shareholders": False, "property_value": 230000, "mortgage_remaining": 100000, "mortgage_payments": 500, - "is_rented": "1", + "is_rented": True, "rent_amount": { "per_interval_value": 500, "per_interval_value_pounds": 50.0, "interval_period": "per_week", }, - "in_dispute": "False", + "in_dispute": False, "csrf_token": None, } expected_property_value = 23000000 @@ -31,8 +31,8 @@ def test_property_payload_with_valid_data(): assert payload["value"] == expected_property_value assert payload["mortgage_left"] == expected_mortgage_remaining assert payload["share"] == expected_share - assert payload["disputed"] == "False" - assert payload["main"] == "True" + assert payload["disputed"] is False + assert payload["main"] rent = payload["rent"]["per_interval_value"] assert rent == expected_rent @@ -41,18 +41,18 @@ def test_property_payload_with_valid_data(): def test_property_payload_with_missing_rent(): form_data = { "submit": False, - "is_main_home": "True", - "other_shareholders": "0", + "is_main_home": True, + "other_shareholders": False, "property_value": 230000, "mortgage_remaining": 100000, "mortgage_payments": 500, - "is_rented": "1", + "is_rented": True, "rent_amount": { "per_interval_value": None, "per_interval_value_pounds": None, "interval_period": None, }, - "in_dispute": "False", + "in_dispute": False, "csrf_token": None, } @@ -66,8 +66,8 @@ def test_property_payload_with_missing_rent(): assert payload["value"] == expected_property_value assert payload["mortgage_left"] == expected_mortgage_remaining assert payload["share"] == expected_share - assert payload["disputed"] == "False" - assert payload["main"] == "True" + assert payload["disputed"] is False + assert payload["main"] rent = payload["rent"] assert rent == expected_rent @@ -86,8 +86,8 @@ def __init__(self, data): def test_validate_single_main_home_multiple_main_homes(): """Test with multiple main homes.""" form_data = [ - {"is_main_home": "True"}, - {"is_main_home": "True"}, + {"is_main_home": True}, + {"is_main_home": True}, ] form = MockForm(form_data) diff --git a/tests/unit_tests/means_test/test_session.py b/tests/unit_tests/means_test/test_session.py index 3be0b0dd2..a2a646c4e 100644 --- a/tests/unit_tests/means_test/test_session.py +++ b/tests/unit_tests/means_test/test_session.py @@ -23,13 +23,13 @@ def test_is_no(): def test_has_partner(): eligibility = Eligibility( - forms={"about-you": {"has_partner": "1", "are_you_in_a_dispute": "0"}} + forms={"about-you": {"has_partner": True, "are_you_in_a_dispute": False}} ) assert eligibility.has_partner eligibility = Eligibility( - forms={"about-you": {"has_partner": "1", "are_you_in_a_dispute": "1"}} + forms={"about-you": {"has_partner": True, "are_you_in_a_dispute": True}} ) assert not eligibility.has_partner @@ -37,7 +37,7 @@ def test_has_partner(): def test_employment_status(): eligibility = Eligibility( - forms={"about-you": {"is_employed": "1", "is_self_employed": "0"}} + forms={"about-you": {"is_employed": True, "is_self_employed": False}} ) assert eligibility.is_employed @@ -51,10 +51,10 @@ def test_partner_employment(): eligibility = Eligibility( forms={ "about-you": { - "has_partner": "1", - "are_you_in_a_dispute": "0", - "partner_is_employed": "1", - "partner_is_self_employed": "0", + "has_partner": True, + "are_you_in_a_dispute": False, + "partner_is_employed": True, + "partner_is_self_employed": False, } } ) @@ -63,7 +63,7 @@ def test_partner_employment(): assert not eligibility.is_partner_self_employed - eligibility = Eligibility(forms={"about-you": {"has_partner": "0"}}) + eligibility = Eligibility(forms={"about-you": {"has_partner": False}}) assert not eligibility.is_partner_employed