diff --git a/app/__init__.py b/app/__init__.py index 51d56fcd3..190822f3d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -126,11 +126,13 @@ def create_app(config_class=Config): from app.main import bp as main_bp from app.categories import bp as categories_bp from app.find_a_legal_adviser import bp as fala_bp + from app.means_test import bp as means_test_bp from app.contact import bp as contact_bp app.register_blueprint(main_bp) app.register_blueprint(categories_bp) app.register_blueprint(fala_bp) + app.register_blueprint(means_test_bp) app.register_blueprint(contact_bp) return app diff --git a/app/api.py b/app/api.py index afd5e891f..e68ace39f 100644 --- a/app/api.py +++ b/app/api.py @@ -58,6 +58,9 @@ def _make_request( Returns: The parsed JSON response from the server """ + if not endpoint.endswith("/"): # CLA Backend requires trailing slashes + endpoint = f"{endpoint}/" + if params: params = self.clean_params( params @@ -102,6 +105,16 @@ def post(self, endpoint: str, json: dict): """ return self._make_request(method="POST", endpoint=endpoint, json=json) + def patch(self, endpoint: str, json: dict): + """Make a PATCH request to CLA Backend. + Args: + endpoint (str): The endpoint to request + json (dict): The data to send to the backend + Returns: + dict: The JSON response from the backend + """ + return self._make_request(method="PATCH", endpoint=endpoint, json=json) + @cache.memoize(timeout=86400) # 1 day def get_help_organisations(self, category: str): """Get help organisations for a given category, each unique set of arguments return value is cached for 24 hours. diff --git a/app/categories/constants.py b/app/categories/constants.py index e54e26c46..5add65bab 100644 --- a/app/categories/constants.py +++ b/app/categories/constants.py @@ -7,34 +7,58 @@ class Category: display_text: LazyString code: str article_category_name: str | None + chs_code: str | None # One of legalaid_categories.code def __str__(self): # Returns the translated display text return str(self.display_text) -FAMILY = Category(_("Children, families and relationships"), "FAMILY", "Family") +FAMILY = Category( + _("Children, families and relationships"), "FAMILY", "Family", "family" +) HOUSING = Category( - _("Housing, homelessness, losing your home"), "HOUSING", "Community care" + _("Housing, homelessness, losing your home"), "HOUSING", "Housing", "housing" +) +COMMUNITY_CARE = Category( + _("Community care"), "COMMUNITY_CARE", "Community care", "commcare" +) +DOMESTIC_ABUSE = Category( + _("Domestic abuse"), "DOMESTIC_ABUSE", "Domestic abuse", "family" ) -COMMUNITY_CARE = Category(_("Community care"), "COMMUNITY_CARE", "Community care") -DOMESTIC_ABUSE = Category(_("Domestic abuse"), "DOMESTIC_ABUSE", "Domestic abuse") BENEFITS = Category( - _("Appeal a decision about your benefits"), "BENEFITS", "Welfare benefits" + _("Appeal a decision about your benefits"), + "BENEFITS", + "Welfare benefits", + "benefits", +) +DISCRIMINATION = Category( + _("Discrimination"), "DISCRIMINATION", "Discrimination", "discrimination" ) -DISCRIMINATION = Category(_("Discrimination"), "DISCRIMINATION", "Discrimination") MENTAL_CAPACITY = Category( - _("Mental capacity, mental health"), "MENTAL_CAPACITY", "Mental health" + _("Mental capacity, mental health"), + "MENTAL_CAPACITY", + "Mental health", + "mentalhealth", ) ASYLUM_AND_IMMIGRATION = Category( - _("Asylum and immigration"), "ASYLUM_AND_IMMIGRATION", None + _("Asylum and immigration"), "ASYLUM_AND_IMMIGRATION", None, "immigration" ) SOCIAL_CARE = Category( - _("Care needs for disability and old age (social care)"), "SOCIAL_CARE", None + _("Care needs for disability and old age (social care)"), + "SOCIAL_CARE", + None, + "commcare", ) PUBLIC_LAW = Category( - _("Legal action against police and public organisations"), "PUBLIC_LAW", "Public" + _("Legal action against police and public organisations"), + "PUBLIC_LAW", + "Public", + "publiclaw", ) EDUCATION = Category( - _("Special educational needs and disability (SEND)"), "EDUCATION", "Education" + _("Special educational needs and disability (SEND)"), + "EDUCATION", + "Education", + "education", ) diff --git a/app/categories/x_cat/forms.py b/app/categories/x_cat/forms.py index 71773181a..bf22d2840 100644 --- a/app/categories/x_cat/forms.py +++ b/app/categories/x_cat/forms.py @@ -28,7 +28,7 @@ class AreYouUnder18Form(QuestionForm): class AntiSocialBehaviourForm(QuestionForm): title = _("Were you accused by a landlord or the council?") category = Category( - _("Anti-social behaviour and gangs"), "ANTI_SOCIAL_BEHAVIOUR", None + _("Anti-social behaviour and gangs"), "ANTI_SOCIAL_BEHAVIOUR", None, None ) next_step_mapping = { "yes": { diff --git a/app/config/__init__.py b/app/config/__init__.py index 85b2f2c49..3b14f14d5 100644 --- a/app/config/__init__.py +++ b/app/config/__init__.py @@ -26,4 +26,4 @@ class Config(object): "https://laa-legal-adviser-api-production.cloud-platform.service.justice.gov.uk", ) POSTCODES_IO_URL = os.environ.get("POSTCODES_IO_URL", "https://api.postcodes.io") - CLA_BACKEND_URL = os.environ.get("CLA_BACKEND_URL", "http://localhost:8010") + CLA_BACKEND_URL = os.environ.get("CLA_BACKEND_URL", "http://localhost:8000") diff --git a/app/means_test/__init__.py b/app/means_test/__init__.py new file mode 100644 index 000000000..52228f985 --- /dev/null +++ b/app/means_test/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + +bp = Blueprint("means_test", __name__) + +YES = "1" +NO = "0" + +from app.means_test import urls # noqa: E402,F401 diff --git a/app/means_test/api.py b/app/means_test/api.py new file mode 100644 index 000000000..6e9c020df --- /dev/null +++ b/app/means_test/api.py @@ -0,0 +1,28 @@ +from app.api import cla_backend +from flask import session, url_for, redirect + + +def update_means_test(payload): + means_test_endpoint = "checker/api/v1/eligibility_check/" + + ec_reference = session.get("reference") + + if ec_reference: + print(payload) + response = cla_backend.patch( + f"{means_test_endpoint}{ec_reference}", json=payload + ) + return response + else: + print(payload) + response = cla_backend.post(means_test_endpoint, json=payload) + session["reference"] = response["reference"] + session["name"] = "Ben" + print(session) + return redirect(url_for(("categories.results.in_scope"))) + + +def is_eligible(reference): + means_test_endpoint = "checker/api/v1/eligibility_check/" + response = cla_backend.post(f"{means_test_endpoint}{reference}/is_eligible/") + return response["is_eligible"] diff --git a/app/means_test/forms.py b/app/means_test/forms.py new file mode 100644 index 000000000..cfefd2a66 --- /dev/null +++ b/app/means_test/forms.py @@ -0,0 +1,265 @@ +from flask import session +from flask_wtf import FlaskForm +from wtforms.fields import RadioField, IntegerField, SelectMultipleField +from govuk_frontend_wtf.wtforms_widgets import GovTextInput, GovSubmitInput +from wtforms.fields.simple import SubmitField +from wtforms.validators import InputRequired, NumberRange + +from app.means_test.validators import ValidateIf +from app.means_test.widgets import MeansTestRadioInput, MeansTestCheckboxInput +from flask_babel import gettext as _ +from app.means_test import YES, NO + + +class BaseMeansTestForm(FlaskForm): + title = "" + template = "means_test/form-page.html" + submit = SubmitField(_("Continue"), widget=GovSubmitInput()) + + @property + def fields_to_render(self): + fields = [] + submit_fields = [] + for name, field in self._fields.items(): + if isinstance(field, SubmitField): + submit_fields.append(field) + else: + fields.append(field) + if submit_fields: + fields.extend(submit_fields) + return fields + + def payload(self) -> dict: + return {} + + @classmethod + def should_show(cls) -> bool: + return True + + +class AboutYouForm(BaseMeansTestForm): + title = "About you" + + template = "means_test/about-you.html" + + has_partner = RadioField( + "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", + validators=[InputRequired(message=_("Tell us whether you have a partner"))], + ) + + are_you_in_a_dispute = RadioField( + "Are you in a dispute with your partner?", + choices=[(YES, "Yes"), (NO, "No")], + widget=MeansTestRadioInput(), + validators=[ + ValidateIf("has_partner", YES), + InputRequired( + message=_("Tell us whether you’re in dispute with your partner") + ), + ], + ) + + on_benefits = RadioField( + "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( + "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=[ + InputRequired( + message=_("Tell us whether you have any children aged 15 or under") + ) + ], + ) + + num_children = IntegerField( + "How many?", + widget=GovTextInput(), + validators=[ + ValidateIf("have_children", YES), + InputRequired( + message=_("Tell us how many children you have aged 15 or under") + ), + NumberRange(min=1, max=50, message=_("Enter a number between 1 and 50")), + ], + ) + + have_dependents = RadioField( + "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", + validators=[ + InputRequired( + message=_("Tell us whether you have any dependants aged 16 or over") + ) + ], + ) + + num_dependents = IntegerField( + "How many?", + widget=GovTextInput(), + validators=[ + ValidateIf("have_dependents", YES), + InputRequired(_("Tell us how many dependants you have aged 16 or over")), + NumberRange(min=1, max=50, message=_("Enter a number between 1 and 50")), + ], + ) + + own_property = RadioField( + "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( + "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", + validators=[InputRequired(message=_("Tell us if you are employed"))], + ) + + partner_is_employed = RadioField( + _("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")), + ], + widget=MeansTestRadioInput(), + ) + + is_self_employed = RadioField( + "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", + validators=[InputRequired(message=_("Tell us if you are self-employed"))], + ) + + partner_is_self_employed = RadioField( + _("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")), + ], + widget=MeansTestRadioInput(), + ) + + aged_60_or_over = RadioField( + "Are you or your partner (if you have one) aged 60 or over?", + choices=[(YES, "Yes"), (NO, "No")], + widget=MeansTestRadioInput(), + validators=[ + InputRequired( + message=_("Tell us if you or your partner are aged 60 or over") + ) + ], + ) + + have_savings = RadioField( + "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( + "Do you have any valuable items worth over £500 each?", + choices=[(YES, "Yes"), (NO, "No")], + widget=MeansTestRadioInput(), + validators=[ + InputRequired( + message=_("Tell us if you have any valuable items worth over £500 each") + ) + ], + ) + + def payload(self): + payload = { + "has_partner": YES + if self.has_partner.data == YES and not self.are_you_in_a_dispute.data == NO + else NO, + "is_you_or_your_partner_over_60": self.aged_60_or_over.data, + "dependants_young": self.num_children.data + if self.have_children.data == YES + else 0, + "dependants_old": self.num_dependents.data + if self.have_dependents.data == YES + else 0, + "you": {"income": {"self_employed": self.is_self_employed.data}}, + } + + if payload["has_partner"] and self.partner_is_self_employed.data == YES: + payload["partner"] = { + "income": {"self_employed": self.partner_is_self_employed.data} + } + + if self.own_property.data: + # TODO: Get property data + pass + + if self.have_savings or self.have_valuables: + # TODO: Get savings data + pass + + if self.on_benefits.data: + # TODO: Get benefits data + pass + + # TODO: Get income and outgoing data + + return payload + + +class BenefitsForm(BaseMeansTestForm): + title = _(" Which benefits do you receive?") + + @classmethod + def should_show(cls) -> bool: + return ( + session.get_eligibility().forms.get("about-you", {}).get("on_benefits") + == YES + ) + + benefits = SelectMultipleField( + label="", + widget=MeansTestCheckboxInput( + is_inline=False, show_divider=True, hint_text=_("Select all that apply") + ), + choices=[ + ("child_benefit", _("Child Benefit")), + ("pension_credit", _("Guarantee Credit")), + ("income_support", _("Income Support")), + ("job_seekers_allowance", _("Income-based Jobseeker's Allowance")), + ( + "employment_support", + _("Income-related Employment and Support Allowance"), + ), + ("universal_credit", _("Universal Credit")), + ("", ""), + ("other-benefit", _("Any other benefits")), + ], + ) diff --git a/app/means_test/urls.py b/app/means_test/urls.py new file mode 100644 index 000000000..46ea62cc8 --- /dev/null +++ b/app/means_test/urls.py @@ -0,0 +1,11 @@ +from app.means_test.views import MeansTest, CheckYourAnswers +from app.means_test import bp + +for name, form_class in MeansTest.forms.items(): + bp.add_url_rule( + f"/{name}", + view_func=MeansTest.as_view(name, form_class, name), + methods=["GET", "POST"], + ) + +bp.add_url_rule("/review", view_func=CheckYourAnswers.as_view("review")) diff --git a/app/means_test/validators.py b/app/means_test/validators.py new file mode 100644 index 000000000..f3685c5f4 --- /dev/null +++ b/app/means_test/validators.py @@ -0,0 +1,21 @@ +from wtforms.validators import StopValidation + + +class ValidateIf: + def __init__( + self, + dependent_field_name: str, + dependent_field_value, + ): + self.dependent_field_name: str = dependent_field_name + self.dependent_field_value = dependent_field_value + + def __call__(self, form, field): + other_field = form._fields.get(self.dependent_field_name) + if other_field is None: + raise Exception('no field named "%s" in form' % self.dependent_field_name) + + # If the dependent field doesn't match the value, skip validation + if other_field.data != self.dependent_field_value: + field.errors = [] + raise StopValidation() diff --git a/app/means_test/views.py b/app/means_test/views.py new file mode 100644 index 000000000..e32d812e6 --- /dev/null +++ b/app/means_test/views.py @@ -0,0 +1,216 @@ +from flask.views import View, MethodView +from flask import render_template, url_for, redirect, session + +from app.means_test.api import update_means_test +from app.means_test.forms import BenefitsForm, AboutYouForm + + +class MeansTest(View): + forms = {"about-you": AboutYouForm, "benefits": BenefitsForm} + + def __init__(self, current_form_class, current_name): + self.form_class = current_form_class + self.current_name = current_name + + def dispatch_request(self): + form = self.form_class() + if form.validate_on_submit(): + session.get_eligibility().add(self.current_name, form.data) + next_page = url_for(f"means_test.{self.get_next_page(self.current_name)}") + payload = self.get_payload(session.get_eligibility()) + update_means_test(payload) + + return redirect(next_page) + return render_template(self.form_class.template, form=form) + + def get_next_page(self, current_key): + keys = iter(self.forms.keys()) # Create an iterator over the keys + for key in keys: + if key == current_key: + next_page = next( + keys, None + ) # Return the next key or None if no more keys + if not next_page: + return "review" + next_page_form = self.forms[next_page] + if next_page_form.should_show(): + return next_page + continue + return "review" + + @classmethod + def get_payload(cls, eligibility_data: dict) -> dict: + about = eligibility_data.forms.get("about-you", {}) + benefits_form = eligibility_data.forms.get("benefits", {}) + + benefits = benefits_form.get("benefits", []) + + payload = { + "category": eligibility_data.category, + "your_problem_notes": "", + "notes": "", + "property_set": [], + "you": { + "income": { + "earnings": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "self_employment_drawings": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "benefits": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "tax_credits": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "child_benefits": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "maintenance_received": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "pension": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "other_income": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "self_employed": about.get("is_self_employed", False), + }, + "savings": { + "bank_balance": None, + "investment_balance": None, + "asset_balance": None, + "credit_balance": None, + "total": None, + }, + "deductions": { + "income_tax": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "national_insurance": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "maintenance": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "childcare": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "mortgage": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "rent": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "criminal_legalaid_contributions": None, + }, + }, + "partner": { + "income": { + "earnings": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "self_employment_drawings": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "benefits": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "tax_credits": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "maintenance_received": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "pension": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "other_income": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "self_employed": about.get("is_partner_self_employed", False), + }, + "savings": { + "bank_balance": None, + "investment_balance": None, + "asset_balance": None, + "credit_balance": None, + }, + "deductions": { + "income_tax": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "national_insurance": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "maintenance": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "childcare": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "mortgage": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "rent": { + "per_interval_value": None, + "interval_period": "per_month", + }, + "criminal_legalaid_contributions": None, + }, + }, + "dependants_young": about.get("dependants_young", 0) + if about.get("has_children", False) + else 0, + "dependants_old": about.get("dependants_old", 0) + if about.get("has_dependants", False) + else 0, + "is_you_or_your_partner_over_60": about.get("aged_60_or_over", False), + "has_partner": about.get("has_partner", False) + and about.get("in_dispute", False), + "on_passported_benefits": False, + "on_nass_benefits": False, + "specific_benefits": { + "pension_credit": "pension_credit" in benefits, + "job_seekers_allowance": "job_seekers_allowance" in benefits, + "employment_support": "employment_support" in benefits, + "universal_credit": "universal_credit" in benefits, + "income_support": "income_support" in benefits, + }, + "disregards": [], + } + + return payload + + +class CheckYourAnswers(MethodView): + def get(self): + return render_template("means_test/review.html", data=session.get_eligibility()) diff --git a/app/means_test/widgets.py b/app/means_test/widgets.py new file mode 100644 index 000000000..b1c2d880c --- /dev/null +++ b/app/means_test/widgets.py @@ -0,0 +1,53 @@ +from govuk_frontend_wtf.wtforms_widgets import GovRadioInput, GovCheckboxesInput + + +class MeansTestInputField: + """Adds additional functionality to the GovUK Input widgets, allowing these to be set on a form by form basis, + rather being required to be passed in to the widget when it is loaded via the template. + """ + + def __init__( + self, + heading_class: str = "govuk-fieldset__legend--m", + show_divider: bool = False, + is_inline: bool = True, + hint_text: str = None, + ): + super().__init__() + self.heading_class = heading_class + self.show_divider = show_divider + self.is_inline = is_inline + self.hint_text = hint_text + + def map_gov_params(self, field, **kwargs): + if self.hint_text: + kwargs["params"] = {"hint": {"text": self.hint_text}} + + params = super().map_gov_params(field, **kwargs) + if self.is_inline: + params["classes"] = "govuk-radios--inline" + + # Get or initialize the items list + items = params.get("items", []) + + # Add divider if enabled and there are enough items + if self.show_divider and len(items) >= 2: + items[-2]["divider"] = "or" + + # Handle pre-selected answer if present + if field.data: + for item in items: + if item.get("value") == field.data: + item["checked"] = True + + label_class = self.heading_class + params["fieldset"]["legend"]["classes"] = label_class + return params + + +class MeansTestRadioInput(MeansTestInputField, GovRadioInput): + pass + + +class MeansTestCheckboxInput(MeansTestInputField, GovCheckboxesInput): + pass diff --git a/app/session.py b/app/session.py index a4fb5ff3b..b44ae1d2a 100644 --- a/app/session.py +++ b/app/session.py @@ -1,8 +1,33 @@ from flask.sessions import SecureCookieSession, SecureCookieSessionInterface from app.categories.constants import Category +from flask import session +from dataclasses import dataclass + + +@dataclass +class Eligibility: + forms = {} + + def add(self, form_name, data): + self.forms[form_name] = data + + @property + def category(self): + return session.get("category")["chs_code"] class Session(SecureCookieSession): + def __init__(self, *args, **kwargs): + print(args) + super().__init__(*args, **kwargs) + self["eligibility"] = Eligibility() + + def update_eligibility(self, form_name, form_data): + self["eligibility"].add(form_name, form_data) + + def get_eligibility(self): + return self["eligibility"] + @property def category(self) -> Category | None: """Get the category from the session. @@ -10,7 +35,10 @@ def category(self) -> Category | None: Returns: The category name if found, None otherwise """ - return Category(**self.get("category")) + category_dict = self.get("category") + if category_dict is None: + return None + return Category(**category_dict) @property def has_children(self): diff --git a/app/static/src/js/custom.js b/app/static/src/js/custom.js index e6a24879b..11e2ef83f 100644 --- a/app/static/src/js/custom.js +++ b/app/static/src/js/custom.js @@ -1,2 +1,38 @@ import { initAll } from 'govuk-frontend/dist/govuk/all.bundle.js'; -initAll(); \ No newline at end of file +initAll(); + +document.addEventListener('DOMContentLoaded', function() { + const dependentElements = document.querySelectorAll('[data-depends-on]'); + + function updateVisibility() { + dependentElements.forEach(element => { + // Check parent condition first + const parentField = element.dataset.parentField; + const parentValue = element.dataset.parentValue; + const parentSelected = document.querySelector( + `input[name="${parentField}"]:checked` + )?.value === parentValue; + + // Only check the dependent field if parent condition is met + const controllingField = element.dataset.dependsOn; + const requiredValue = element.dataset.dependsOnValue; + const dependentSelected = document.querySelector( + `input[name="${controllingField}"]:checked` + )?.value === requiredValue; + + // Show only if both conditions are met + const shouldShow = parentSelected && dependentSelected; + element.classList.toggle('govuk-radios__conditional--hidden', !shouldShow); + }); + } + + // Listen for changes on any radio button + document.addEventListener('change', function(event) { + if (event.target.type === 'radio') { + requestAnimationFrame(updateVisibility); + } + }); + + // Initial state + updateVisibility(); +}); diff --git a/app/static/src/scss/means-test.scss b/app/static/src/scss/means-test.scss new file mode 100644 index 000000000..048de71cc --- /dev/null +++ b/app/static/src/scss/means-test.scss @@ -0,0 +1,6 @@ +/* This is a fix to ensure conditional questions are shown under, rather than next to, the answer that causes them to display. */ +/* This is required to display the legacy means test pages correctly */ +.govuk-radios--inline .govuk-radios__conditional { + order: 9999; + width: 100%; +} diff --git a/app/static/src/scss/styles.scss b/app/static/src/scss/styles.scss index 7752e6dc6..4fc95d752 100644 --- a/app/static/src/scss/styles.scss +++ b/app/static/src/scss/styles.scss @@ -7,3 +7,4 @@ @import "./sidebar"; @import "./exit_this_page"; @import "./help-organisations"; +@import "means-test"; diff --git a/app/templates/base.html b/app/templates/base.html index 136f532f8..45ea2d582 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -20,6 +20,8 @@ {% endblock %} +{% set bodyClasses = "govuk-frontend-supported" %} + {% block bodyStart %} {% if "cookies_policy" not in request.cookies %} {% set html %} @@ -172,5 +174,5 @@ {% endblock %} {% block bodyEnd %} - + {% endblock %} \ No newline at end of file diff --git a/app/templates/categories/in-scope.html b/app/templates/categories/in-scope.html index bb780b9ef..8b00f76a5 100644 --- a/app/templates/categories/in-scope.html +++ b/app/templates/categories/in-scope.html @@ -56,6 +56,7 @@