From 95e6cd79b53da15cfe52e922cdd3594123749c1c Mon Sep 17 00:00:00 2001 From: said-moj <45761276+said-moj@users.noreply.github.com> Date: Mon, 10 Mar 2025 09:21:37 +0000 Subject: [PATCH 1/5] LGA-3495 - Add means test answers to the reviews page (#113) * Add means test answers to the reviews page. * Store only the category code in session * Create a new CategoryAnswer model to hold category and onward questions answers --- app/categories/constants.py | 47 ++- app/categories/models.py | 38 +++ app/categories/views.py | 138 +++++---- app/main/filters.py | 4 +- app/means_test/__init__.py | 3 + app/means_test/api.py | 1 + app/means_test/fields.py | 5 +- app/means_test/forms/__init__.py | 106 +++++++ app/means_test/forms/benefits.py | 3 +- app/means_test/forms/review.py | 6 + app/means_test/views.py | 142 ++++++++- app/session.py | 106 +++++-- app/templates/categories/landing.html | 8 +- .../means_test/components/progress-bar.html | 2 +- app/templates/means_test/review.html | 64 +++- app/translations/cy/LC_MESSAGES/messages.mo | Bin 75862 -> 76114 bytes app/translations/cy/LC_MESSAGES/messages.po | 12 + app/translations/en/LC_MESSAGES/messages.pot | 12 + tests/functional_tests/means_test/conftest.py | 65 +++- .../means_test/test_benefits_page.py | 2 +- .../means_test/test_outgoings.py | 8 +- .../means_test/test_progress.py | 4 +- .../means_test/test_review.py | 277 ++++++++++++++++++ tests/unit_tests/categories/test_session.py | 105 ++++--- tests/unit_tests/categories/test_views.py | 53 +++- .../means_test/test_form_summary.py | 107 +++++++ .../means_test/test_views_summary.py | 226 ++++++++++++++ 27 files changed, 1390 insertions(+), 154 deletions(-) create mode 100644 app/categories/models.py create mode 100644 app/means_test/forms/review.py create mode 100644 tests/functional_tests/means_test/test_review.py create mode 100644 tests/unit_tests/means_test/test_form_summary.py create mode 100644 tests/unit_tests/means_test/test_views_summary.py diff --git a/app/categories/constants.py b/app/categories/constants.py index 0138e77f2..f2e20790d 100644 --- a/app/categories/constants.py +++ b/app/categories/constants.py @@ -13,9 +13,14 @@ class Category: # Internal code code: Optional[str] = None children: dict[str, "Category"] | None = field(default_factory=dict) + parent_code: Optional[str] = None _referrer_text: Optional[LazyString] = None exit_page: Optional[bool] = False + @property + def url_friendly_name(self): + return self.code.replace("_", "-").lower() + @property def display_text(self): return self.title @@ -26,15 +31,23 @@ def sub(self): class Subcategory: def __init__(self, category): self.children: dict[str, Category] = category.children + self.category: Category = category def __getattr__(self, item): + if item not in self.children: + raise AttributeError( + f"Could not find {item} in category {self.category.title}" + ) return self.children.get(item) return Subcategory(self) @classmethod def from_dict(cls, data: dict) -> "Category": - children: dict = data.pop("children", {}) + data = data.copy() + children = {} + if "children" in data: + children: dict = data.pop("children") category = cls(**data) if children: for name, child in children.items(): @@ -474,21 +487,27 @@ def __str__(self): def init_children(category: Category) -> None: for child in category.children.values(): child.chs_code = child.chs_code or category.chs_code + child.parent_code = category.code child.article_category_name = ( child.article_category_name or category.article_category_name ) -ALL_CATEGORIES = [ - DOMESTIC_ABUSE, - FAMILY, - HOUSING, - DISCRIMINATION, - EDUCATION, - COMMUNITY_CARE, - BENEFITS, - PUBLIC_LAW, - ASYLUM_AND_IMMIGRATION, - MENTAL_CAPACITY, -] -list(map(init_children, ALL_CATEGORIES)) +ALL_CATEGORIES = { + DOMESTIC_ABUSE.code: DOMESTIC_ABUSE, + FAMILY.code: FAMILY, + HOUSING.code: HOUSING, + DISCRIMINATION.code: DISCRIMINATION, + EDUCATION.code: EDUCATION, + COMMUNITY_CARE.code: COMMUNITY_CARE, + BENEFITS.code: BENEFITS, + PUBLIC_LAW.code: PUBLIC_LAW, + ASYLUM_AND_IMMIGRATION.code: ASYLUM_AND_IMMIGRATION, + MENTAL_CAPACITY.code: MENTAL_CAPACITY, +} + +list(map(init_children, ALL_CATEGORIES.values())) + + +def get_category_from_code(code: str) -> Category: + return ALL_CATEGORIES[code] diff --git a/app/categories/models.py b/app/categories/models.py new file mode 100644 index 000000000..44be24cac --- /dev/null +++ b/app/categories/models.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional + +from flask import url_for +from app.categories.constants import Category + + +class QuestionType(str, Enum): + SUB_CATEGORY = "sub_category" + ONWARD = "onward_question" + + +@dataclass +class CategoryAnswer: + question: str + answer_value: str + answer_label: str + category: Category + question_page: str + next_page: str + question_type: Optional[QuestionType] = field(default=QuestionType.SUB_CATEGORY) + + @property + def edit_url(self): + return url_for(self.question_page) + + @property + def next_url(self): + return url_for(self.next_page) + + @property + def question_type_is_sub_category(self): + return self.question_type == QuestionType.SUB_CATEGORY + + @property + def question_type_is_onward(self): + return self.question_type == QuestionType.ONWARD diff --git a/app/categories/views.py b/app/categories/views.py index cc47f8c5b..8963c1b32 100644 --- a/app/categories/views.py +++ b/app/categories/views.py @@ -3,6 +3,7 @@ from flask import render_template, redirect, url_for, session, request from app.categories.forms import QuestionForm from app.categories.constants import Category +from app.categories.models import CategoryAnswer, QuestionType class CategoryPage(View): @@ -13,21 +14,17 @@ class CategoryPage(View): def __init__(self, template, *args, **kwargs): self.template = template - def update_session(self, question: str, answer: str, category: Category) -> None: + def update_session(self, category_answer: CategoryAnswer) -> None: """ Update the session with the current page and answer. """ - session.set_category_question_answer( - question_title=question, - answer=answer, - category=category, - ) + session.set_category_question_answer(category_answer) def dispatch_request(self): category = getattr(self, "category", None) if category is not None: - session["category"] = category + session.category = category response = self.process_request() if not response: @@ -41,76 +38,103 @@ def process_request(self): class CategoryLandingPage(CategoryPage): template: str = "categories/landing.html" - routing_map: dict[str, str] = {} + routing_map: dict[str, list] = {} + """ + A dictionary that organizes category listings into different sections: "main", "more", and "other". + + - "main" and "more" contain lists of tuples, where each tuple consists of: + - `category`: Category object. + - `route`: String - an intermediary route than stores the selected category before redirecting to the target + + - "other" is a string representing an intermediary route than stores the selected answer before redirecting to the target + """ + listing: dict[str, list] = {} + + def __init__(self, route_endpoint: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.routing_map and route_endpoint: + self.listing["main"] = [] + for category, next_page in self.routing_map["main"]: + self.listing["main"].append( + (category, f"categories.{route_endpoint}.{category.code}") + ) + + self.listing["more"] = [] + for category, next_page in self.routing_map["more"]: + self.listing["more"].append( + (category, f"categories.{route_endpoint}.{category.code}") + ) + + self.listing["other"] = f"categories.{route_endpoint}.other" def process_request(self): return render_template( - self.template, category=self.category, routing_map=self.routing_map + self.template, category=self.category, listing=self.listing ) @classmethod def register_routes(cls, blueprint: Blueprint, path: str = None): if not path: - path = cls.category.code.lower().replace("_", "-") + path = cls.category.url_friendly_name blueprint.add_url_rule( f"/{path}/", - view_func=cls.as_view("landing", template=cls.template), + view_func=cls.as_view( + "landing", route_endpoint=blueprint.name, template=cls.template + ), ) cls.register_sub_routes(blueprint, path, cls.routing_map["main"]) cls.register_sub_routes(blueprint, path, cls.routing_map["more"]) if "other" in cls.routing_map and cls.routing_map["other"] is not None: + category_answer = CategoryAnswer( + question=cls.question_title, + question_page=f"categories.{blueprint.name}.landing", + answer_value="other", + answer_label="Other", + next_page=cls.routing_map["other"], + category=cls.category, + ) blueprint.add_url_rule( f"/{path}/answer/other", - view_func=CategoryAnswerPage.as_view( - "other", - question=cls.question_title, - answer="other", - next_page=cls.routing_map["other"], - category=cls.category, - ), + view_func=CategoryAnswerPage.as_view("other", category_answer), ) @classmethod def register_sub_routes(cls, blueprint: Blueprint, path, routes): for sub_category, next_page in routes: + category_answer = CategoryAnswer( + question=cls.question_title, + question_page=f"categories.{blueprint.name}.landing", + answer_value=sub_category.code, + answer_label=sub_category.title, + next_page=next_page, + category=sub_category, + ) blueprint.add_url_rule( - f"/{path}/answer/{sub_category.code.replace('_', '-')}", + f"/{path}/answer/{sub_category.url_friendly_name}", view_func=CategoryAnswerPage.as_view( - sub_category.code, - question=cls.question_title, - answer=sub_category.code, - next_page=next_page, - category=sub_category, + sub_category.code, category_answer ), ) class CategoryAnswerPage(View): - def __init__(self, question, answer, next_page, category): - self.question = question - self.answer = answer - self.next_page = next_page - self.category = category + def __init__(self, category_answer: CategoryAnswer): + self.category_answer = category_answer def update_session(self) -> None: """ Update the session with the current page and answer. """ - session["previous_page"] = request.endpoint - session.set_category_question_answer( - question_title=self.question, - answer=self.answer, - category=self.category, - ) + session.set_category_question_answer(self.category_answer) def dispatch_request(self): self.update_session() - if isinstance(self.next_page, dict): - return redirect(url_for(**self.next_page)) - return redirect(url_for(self.next_page)) + if isinstance(self.category_answer.next_page, dict): + return redirect(url_for(**self.category_answer.next_page)) + return redirect(url_for(self.category_answer.next_page)) class QuestionPage(CategoryPage): @@ -138,14 +162,14 @@ def __init__(self, form_class: type[QuestionForm], template=None): self.category = form_class.category super().__init__(self.template) - def get_next_page(self, answer: str) -> redirect: + def get_next_page(self, answer: str) -> str: """Determine and redirect to the next page based on the user's answer. Args: answer: The user's selected answer Returns: - A Flask redirect response to the next appropriate page + A string representing the next page to take the user to Raises: ValueError if the answer does not have a mapping to a next page @@ -156,21 +180,39 @@ def get_next_page(self, answer: str) -> redirect: ] # We should only route to these pages if they are the only answer if len(answer) == 1 and answer[0] in optional_answers: - return redirect(url_for(self.form_class.next_step_mapping[answer[0]])) + return url_for(self.form_class.next_step_mapping[answer[0]]) if isinstance(answer, list): for a in answer: if a in self.form_class.next_step_mapping and a not in optional_answers: - return redirect(url_for(self.form_class.next_step_mapping[a])) + return url_for(self.form_class.next_step_mapping[a]) answer = "*" if answer not in self.form_class.next_step_mapping: raise ValueError(f"No mapping found for answer: {answer}") next_page = self.form_class.next_step_mapping[answer] + if isinstance(next_page, dict): - return redirect(url_for(**next_page)) - return redirect(url_for(next_page)) + return url_for(**next_page) + return url_for(next_page) + + def update_session(self, form: QuestionForm) -> None: + answer = form.question.data + answer = answer if isinstance(answer, list) else [answer] + answer_labels = [ + label for value, label in form.question.choices if value in answer + ] + category_answer = CategoryAnswer( + question=form.title, + answer_value=form.question.data, + answer_label=answer_labels if len(answer) > 1 else answer_labels[0], + category=form.category, + next_page=self.get_next_page(form.question.data), + question_page=request.url_rule.endpoint, + question_type=QuestionType.ONWARD, + ) + super().update_session(category_answer) def process_request(self): """Handle requests for the question page, including form submissions. @@ -183,13 +225,11 @@ def process_request(self): Either a redirect to the next page or the rendered template """ form = self.form_class(request.args) - session["category"] = form.category + session.category = form.category if form.submit.data and form.validate(): - self.update_session( - question=form.title, answer=form.question.data, category=form.category - ) - return self.get_next_page(form.question.data) + self.update_session(form) + return redirect(self.get_next_page(form.question.data)) # Pre-populate form with previous answer if it exists previous_answer = session.get_category_question_answer(form.title) diff --git a/app/main/filters.py b/app/main/filters.py index 6e6015d63..e3f82bd01 100644 --- a/app/main/filters.py +++ b/app/main/filters.py @@ -8,12 +8,12 @@ @bp.app_template_filter("markdown") -def render_markdown(text): +def render_markdown(text, **kwargs): """Renders Markdown text as HTML, this can be invoked in Jinja templates using the markdown filter: {{ "# Hello, World!" | markdown }} We use markupsafe to ensure characters are escaped correctly so they can be safely rendered. """ - return Markup(markdown(text)) + return Markup(markdown(text, **kwargs)) @bp.app_template_filter("dict") diff --git a/app/means_test/__init__.py b/app/means_test/__init__.py index 88186eb89..3238b4ac4 100644 --- a/app/means_test/__init__.py +++ b/app/means_test/__init__.py @@ -1,9 +1,12 @@ from flask import Blueprint +from flask_babel import lazy_gettext as _ bp = Blueprint("means_test", __name__) YES = "1" +YES_LABEL = _("Yes") NO = "0" +NO_LABEL = _("No") def is_yes(value): diff --git a/app/means_test/api.py b/app/means_test/api.py index 6908fd2b4..871ef9bef 100644 --- a/app/means_test/api.py +++ b/app/means_test/api.py @@ -30,6 +30,7 @@ def is_eligible(reference): def get_means_test_payload(eligibility_data) -> dict: + # Todo: Need to add notes about = eligibility_data.forms.get("about-you", {}) savings_form = eligibility_data.forms.get("savings", {}) income_form = eligibility_data.forms.get("income", {}) diff --git a/app/means_test/fields.py b/app/means_test/fields.py index b1c1c6663..b2b467201 100644 --- a/app/means_test/fields.py +++ b/app/means_test/fields.py @@ -145,7 +145,10 @@ def clean_input(value): return re.sub(r"[£\s,]", "", value.strip()) def process_formdata(self, valuelist): - if valuelist: + if valuelist and isinstance(valuelist[0], int): + # The form is being restored from the session where the conversion has already taken place + self.data = valuelist[0] + elif valuelist: self._user_input = valuelist[0] # Clean the input diff --git a/app/means_test/forms/__init__.py b/app/means_test/forms/__init__.py index 1f719d108..0345890dc 100644 --- a/app/means_test/forms/__init__.py +++ b/app/means_test/forms/__init__.py @@ -1,8 +1,14 @@ from flask_wtf import FlaskForm from govuk_frontend_wtf.wtforms_widgets import GovSubmitInput from wtforms.fields.simple import SubmitField +from wtforms.fields.choices import SelectField, SelectMultipleField +from wtforms.csrf.core import CSRFTokenField from flask_babel import lazy_gettext as _ from flask import session +from app.means_test.fields import MoneyIntervalField, MoneyInterval, MoneyField +import decimal +from wtforms.fields.core import Field +from app.means_test.validators import ValidateIf, StopValidation, ValidateIfSession class BaseMeansTestForm(FlaskForm): @@ -47,3 +53,103 @@ def render_conditional(self, field, sub_field, conditional_value) -> str: conditional = {"value": conditional_value, "html": sub_field_rendered} field.render_kw = {"conditional": conditional} return field() + + def filter_summary(self, summary: dict) -> dict: + """Override this method to remove items from review page for this given form""" + return summary + + def is_unvalidated_conditional_field(self, field: Field): + # Return True if field has a ValidateIf or ValidateIfSession validator that raise a StopValidation + for validator in field.validators: + if isinstance(validator, (ValidateIf, ValidateIfSession)): + try: + validator(self, field) + except StopValidation: + return True + except Exception: + return False + return False + + def summary(self) -> dict: + """ + Generates a summary of all fields in this form, including their labels (questions) and formatted values (answers). + Monetary fields will have a '£' prefix, and selection fields will display their choice label. + + Fields with no data or empty values will be excluded. + + :return: A dictionary summarizing the form fields. + Each entry follows this structure: + { + "question": field label, + "answer": formatted field value (or choice label for selection fields), + "id": field ID + } + """ + summary = {} + for field_name, field_instance in self._fields.items(): + if isinstance(field_instance, (SubmitField, CSRFTokenField)): + continue + + if field_instance.data in [None, "None"]: + continue + + # Skip fields that use ValidateIf or ValidateIfSession validator that raise a StopValidation + if self.is_unvalidated_conditional_field(field_instance): + continue + + question = str(field_instance.label.text) + answer = field_instance.data + + if isinstance(field_instance, SelectField): + answer = self.get_selected_answer(field_instance) + elif isinstance(field_instance, MoneyIntervalField): + answer = self.get_money_interval_field_answers(field_instance) + elif isinstance(field_instance, MoneyField): + answer = self.get_money_field_answers(field_instance) + + # Skip empty answers + if answer is None: + continue + + summary[field_instance.name] = { + "question": question, + "answer": answer, + "id": field_instance.id, + } + + summary = self.filter_summary(summary) + return summary + + @staticmethod + def get_selected_answer(field_instance): + def selected_answers_only(choice): + return field_instance.data and choice[0] in field_instance.data + + # Remove any unselected choices + selected_choices = list(filter(selected_answers_only, field_instance.choices)) + # Get the labels of the selected answers + answer_labels = [str(choice[1]) for choice in selected_choices] + if not isinstance(field_instance, SelectMultipleField): + return answer_labels[0] + return answer_labels + + @staticmethod + def get_money_interval_field_answers(field_instance): + if field_instance.data["per_interval_value"] is None: + return None + + if field_instance.data["per_interval_value"] == 0: + return "£0" + amount = decimal.Decimal(int(field_instance.data["per_interval_value"]) / 100) + amount = amount.quantize(decimal.Decimal("0.01")) + interval = MoneyInterval._intervals[field_instance.data["interval_period"]][ + "label" + ] + return f"£{amount} ({interval})" + + @staticmethod + def get_money_field_answers(field_instance): + if field_instance.data == 0: + return "£0" + + return f"£{field_instance._value()}" diff --git a/app/means_test/forms/benefits.py b/app/means_test/forms/benefits.py index ae7790f53..a7c2a3715 100644 --- a/app/means_test/forms/benefits.py +++ b/app/means_test/forms/benefits.py @@ -12,7 +12,6 @@ ValidateIfType, ) from app.means_test import YES, NO - from dataclasses import dataclass, field @@ -121,7 +120,7 @@ def data(self): @classmethod def should_show(cls) -> bool: - return session.get("eligibility").on_benefits + return session.get_eligibility().on_benefits def get_payload(self) -> dict: """Returns the benefits payload for the user and the partner. diff --git a/app/means_test/forms/review.py b/app/means_test/forms/review.py new file mode 100644 index 000000000..1624d529e --- /dev/null +++ b/app/means_test/forms/review.py @@ -0,0 +1,6 @@ +from flask_babel import lazy_gettext as _ +from app.means_test.forms import BaseMeansTestForm + + +class ReviewForm(BaseMeansTestForm): + title = _("Check your answers and confirm") diff --git a/app/means_test/views.py b/app/means_test/views.py index bbfd013c5..c7cf2f888 100644 --- a/app/means_test/views.py +++ b/app/means_test/views.py @@ -1,17 +1,20 @@ +from typing import List from flask.views import View, MethodView from flask import render_template, url_for, redirect, session, request +from flask_babel import lazy_gettext as _, gettext from werkzeug.datastructures import MultiDict from app.means_test.api import update_means_test, get_means_test_payload -from app.means_test.forms import BaseMeansTestForm from app.means_test.forms.about_you import AboutYouForm from app.means_test.forms.benefits import BenefitsForm, AdditionalBenefitsForm from app.means_test.forms.property import MultiplePropertiesForm from app.means_test.forms.income import IncomeForm from app.means_test.forms.savings import SavingsForm from app.means_test.forms.outgoings import OutgoingsForm +from app.means_test.forms.review import ReviewForm, BaseMeansTestForm +from app.categories.models import CategoryAnswer -class MeansTest(View): +class FormsMixin: forms = { "about-you": AboutYouForm, "benefits": BenefitsForm, @@ -22,6 +25,8 @@ class MeansTest(View): "outgoings": OutgoingsForm, } + +class MeansTest(FormsMixin, View): def __init__(self, current_form_class, current_name): self.form_class = current_form_class self.current_name = current_name @@ -46,10 +51,8 @@ def handle_multiple_properties_ajax_request(self, form): def dispatch_request(self): eligibility = session.get_eligibility() - form_data = MultiDict(eligibility.forms.get(self.current_name, {})) - form = self.form_class( - formdata=request.form or None, data=form_data if not request.form else None - ) + form_data = eligibility.forms.get(self.current_name, {}) + form = self.form_class(formdata=request.form or None, data=form_data) if isinstance(form, MultiplePropertiesForm): response = self.handle_multiple_properties_ajax_request(form) if response is not None: @@ -120,6 +123,129 @@ def get_form_progress(self, current_form: BaseMeansTestForm) -> dict: } -class CheckYourAnswers(MethodView): +class CheckYourAnswers(FormsMixin, MethodView): + template = "check-your-answers.html" + def get(self): - return render_template("means_test/review.html", data=session.get_eligibility()) + eligibility = session.get_eligibility() + means_test_summary = {} + for key, form_class in self.forms.items(): + if key not in eligibility.forms: + continue + form_data = MultiDict(eligibility.forms.get(key, {})) + form = form_class(form_data) + if form.should_show(): + means_test_summary[str(form.page_title)] = self.get_form_summary( + form, key + ) + + params = { + "means_test_summary": means_test_summary, + "form": ReviewForm(), + "category": session.category, + "category_answers": self.get_category_answers_summary(), + } + return render_template("means_test/review.html", **params) + + @staticmethod + def get_category_answers_summary(): + def get_your_problem__no_description(): + return [ + { + "key": {"text": _("The problem you need help with")}, + "value": {"text": session.category.title}, + "actions": { + "items": [ + {"text": _("Change"), "href": url_for("categories.index")} + ], + }, + }, + ] + + def get_your_problem__with_description(first_answer): + value = "\n".join( + [ + f"**{str(first_answer.category.title)}**", + str(first_answer.category.description), + ] + ) + return [ + { + "key": {"text": _("The problem you need help with")}, + "value": {"markdown": value}, + "actions": { + "items": [{"text": _("Change"), "href": first_answer.edit_url}], + }, + }, + ] + + answers: List[CategoryAnswer] = session.category_answers + if not answers: + return [] + + category = session.category + category_has_children = bool(getattr(category, "children")) + if category_has_children: + if answers[0].question_type_is_sub_category: + results = get_your_problem__with_description(answers.pop(0)) + else: + # Sometimes there is only one answer and it was an onward question. + # However we still need to show 2 items: 'The problem you need help with' and the onward question + # Example journey: + # -> Children, families, relationships + # -> If there is domestic abuse in your family + # -> Are you worried about someone's safety? + # + # Then the two items need be shown in `About the problem` section: + # The `The problem you need help with` should be Domestic abuse with its description + # And the `Are you worried about someone's safety` onward question + first_answer = CategoryAnswer(**answers[0].__dict__) + first_answer.question_page = "categories.index" + results = get_your_problem__with_description(first_answer) + else: + # if a category doesn't have children then it does not have subpages so we don't show the category description + results = get_your_problem__no_description() + + for answer in answers: + answer_key = "text" + answer_label = answer.answer_label + if isinstance(answer_label, list): + # Multiple items need to be separated by a new line + answer_key = "markdown" + answer_label = "\n".join([gettext(label) for label in answer_label]) + else: + answer_label = gettext(answer_label) + + results.append( + { + "key": {"text": gettext(answer.question)}, + "value": {answer_key: answer_label}, + "actions": { + "items": [{"text": _("Change"), "href": answer.edit_url}] + }, + } + ) + return results + + @staticmethod + def get_form_summary(form: BaseMeansTestForm, form_name: str) -> list: + summary = [] + for item in form.summary().values(): + answer_key = "text" + if isinstance(item["answer"], list): + # Multiple items need to be separated by a new line + answer_key = "markdown" + item["answer"] = "\n".join(item["answer"]) + + change_link = url_for(f"means_test.{form_name}", _anchor=item["id"]) + summary.append( + { + "key": {"text": item["question"]}, + "value": {answer_key: item["answer"]}, + "actions": {"items": [{"href": change_link, "text": _("Change")}]}, + } + ) + return summary + + def post(self): + return self.get() diff --git a/app/session.py b/app/session.py index a19065fb6..bde3315b7 100644 --- a/app/session.py +++ b/app/session.py @@ -1,15 +1,17 @@ from flask.sessions import SecureCookieSession, SecureCookieSessionInterface -from app.categories.constants import Category +from app.categories.constants import Category, get_category_from_code from flask import session from dataclasses import dataclass from datetime import timedelta +from app.categories.models import CategoryAnswer +from flask_babel import LazyString @dataclass class Eligibility: - def __init__(self, forms, _notes): + def __init__(self, forms, _notes=None): self.forms = forms - self._notes = _notes + self._notes = _notes or {} forms: dict[str, dict] @@ -141,9 +143,31 @@ def category(self) -> Category | None: category_dict = self.get("category") if category_dict is None: return None - if isinstance(category_dict, Category): - return category_dict - return Category.from_dict(category_dict) + + return self._category_from_dict_from_session_storage(category_dict) + + @category.setter + def category(self, category: Category): + current_category = self.category + if current_category and current_category.code != category.code: + self["category_answers"] = [] + self["category"] = self._category_to_dict_for_session_storage(category) + + @staticmethod + def _category_to_dict_for_session_storage(category: Category): + data = {"code": category.code} + if category.parent_code: + data["parent_code"] = category.parent_code + return data + + @staticmethod + def _category_from_dict_from_session_storage(category_dict: dict): + parent_code = category_dict.get("parent_code", None) + if parent_code: + category = get_category_from_code(parent_code) + return category.children[category_dict["code"]] + else: + return get_category_from_code(category_dict["code"]) @property def has_children(self): @@ -155,9 +179,39 @@ def has_dependants(self): # Todo: Needs implementation return True - def set_category_question_answer( - self, question_title: str, answer: str, category: Category - ) -> None: + @property + def category_answers(self) -> list[CategoryAnswer]: + items: list[dict] = self.get("category_answers", []) + category_answers = [] + for item in items: + answer = item.copy() + answer["category"] = self._category_from_dict_from_session_storage( + answer["category"] + ) + category_answers.append(CategoryAnswer(**answer)) + + return category_answers + + @staticmethod + def _untranslate_category_answer(category_answer: CategoryAnswer): + """Remove translation from the category_answer object""" + category_answer_dict = {} + for key, value in category_answer.__dict__.items(): + if isinstance(value, list): + values = [] + for item in value: + if isinstance(item, LazyString): + values.append(item._args[0]) + else: + values.append(item) + value = values + elif isinstance(value, LazyString): + value = value._args[0] + category_answer_dict[key] = value + + return CategoryAnswer(**category_answer_dict) + + def set_category_question_answer(self, category_answer: CategoryAnswer) -> None: """Store a question-answer pair with the question category in the session. Args: @@ -168,19 +222,30 @@ def set_category_question_answer( Side effects: Updates session['category_answers'] list """ + + # Remove translation from the category_answer object before saving + category_answer = self._untranslate_category_answer(category_answer) + if "category_answers" not in self: self["category_answers"] = [] - answers: list[dict[str, str]] = self["category_answers"] + answers: list[CategoryAnswer] = self.category_answers # Remove existing entry if present - answers = [entry for entry in answers if entry["question"] != question_title] + answers = [ + entry for entry in answers if entry.question != category_answer.question + ] + answers.append(category_answer) - answers.append( - {"question": question_title, "answer": answer, "category": category} - ) + category_answers = [] + for answer in answers: + answer_dict = answer.__dict__ + answer_dict["category"] = self._category_to_dict_for_session_storage( + answer.category + ) + category_answers.append(answer_dict) - self["category_answers"] = answers + self["category_answers"] = category_answers def get_category_question_answer(self, question_title: str) -> str | None: """Retrieve an answer for a question from the session. @@ -191,14 +256,11 @@ def get_category_question_answer(self, question_title: str) -> str | None: Returns: The stored answer string if found, None otherwise """ - if "category_answers" not in self: - return None - - answers: list[dict[str, str]] = self["category_answers"] - + answers = self.category_answers for answer in answers: - if answer["question"] == question_title: - return answer["answer"] + if answer.question == question_title: + return answer.answer_value + return None diff --git a/app/templates/categories/landing.html b/app/templates/categories/landing.html index 61a968091..8c22c6b90 100644 --- a/app/templates/categories/landing.html +++ b/app/templates/categories/landing.html @@ -25,18 +25,18 @@

{{ category.title}}


- {% for sub_category, route in routing_map.main %} + {% for sub_category, route in listing.main %} {{ list_item(sub_category.title, sub_category.description, route|category_url_for) }} {% endfor %}
- {% if routing_map.more %} + {% if listing.more %}

{% trans %}More problems{% endtrans %}


- {% for sub_category, route in routing_map.more %} + {% for sub_category, route in listing.more %} {{ list_item_small(sub_category.title, sub_category.description, route|category_url_for) }} {% endfor %} {% endif %} @@ -49,7 +49,7 @@

{% trans %}More problems{% endtrans %}

{% elif category.code == "send" %} {% set other_text = _("SEND") %} {% endif %} - {{ cannot_find_your_problem(other_text, routing_map.other|category_url_for)}} + {{ cannot_find_your_problem(other_text, listing.other|category_url_for)}} diff --git a/app/templates/means_test/components/progress-bar.html b/app/templates/means_test/components/progress-bar.html index 62e907bb1..92952bbb8 100644 --- a/app/templates/means_test/components/progress-bar.html +++ b/app/templates/means_test/components/progress-bar.html @@ -70,7 +70,7 @@

{% tra {% endif %} {{ step_status(not has_remaining_forms, current_step.name == 'review') }} - {% trans %}Review your answers{% endtrans %} + {% trans %}Check your answers and confirm{% endtrans %} {% if has_remaining_forms or is_contact_page %} {% else %} diff --git a/app/templates/means_test/review.html b/app/templates/means_test/review.html index 41a496d5b..0994056b7 100644 --- a/app/templates/means_test/review.html +++ b/app/templates/means_test/review.html @@ -1,24 +1,76 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} {%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} - -{% block pageTitle %}Review your answers - GOV.UK{% endblock %} +{%- from 'govuk_frontend_jinja/components/summary-list/macro.html' import govukSummaryList -%} +{% set title = _("Check your answers and confirm") %} +{% block pageTitle %}{{ title }} - GOV.UK{% endblock %} {% block beforeContent%} {{ super() }} {{ govukBackLink() }} - {% endblock %} {% block content %} - {{ super() }} -
{% block formHeading %} -

Review your answers

+

{{ title }}

+
+ {{ form.csrf_token }} + + {% if category_answers %} +

{% trans %}About the problem{% endtrans %}

+ {# Parse the markdown before passing it to govukSummaryList #} + {% set ns = namespace(rows=[]) %} + {% for answer in category_answers %} + {% if answer.value.markdown %} + {# The markdown extension nl2br converts \n to
. #} + {% set _ = answer.value.update({"text": answer.value.markdown | markdown(extensions=['nl2br'])}) %} + {% endif %} + {% set _ = ns.rows.append(answer) %} + {% endfor %} + {{ govukSummaryList( + { + "title": { + "classes": "govuk-heading-xl" + }, + "rows": ns.rows, + "attributes": { + "data-question": "The problem you need help with" + } + } + )}} + {% endif %} + + {% for title, questions in means_test_summary.items() %} +

{{ title }}

+ + {# Parse the markdown before passing it to govukSummaryList #} + {% set ns = namespace(rows=[]) %} + {% for question in questions %} + {% if question.value.markdown %} + {# The markdown extension nl2br converts \n to
. #} + {% set _ = question.value.update({"text": question.value.markdown | markdown(extensions=['nl2br'])}) %} + {% endif %} + {% set _ = ns.rows.append(question) %} + {% endfor %} + + {{ govukSummaryList( + { + "title": { + "classes": "govuk-heading-xl" + }, + "rows": ns.rows, + "attributes": { + "data-question": title, + } + } + )}} + {% endfor %} + {{ form.submit }} +
{% endblock %}
diff --git a/app/translations/cy/LC_MESSAGES/messages.mo b/app/translations/cy/LC_MESSAGES/messages.mo index 78cbb310bb341e6623c4af276cb1fec4ecf2d035..1edaf8c71dc8525e3bbc88d89139da9026ffa084 100644 GIT binary patch delta 9889 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 U@^!0sY*~LHXU{G_$BM}R1D1l$v;Y7A diff --git a/app/translations/cy/LC_MESSAGES/messages.po b/app/translations/cy/LC_MESSAGES/messages.po index 30bf91f36..16ffda40b 100644 --- a/app/translations/cy/LC_MESSAGES/messages.po +++ b/app/translations/cy/LC_MESSAGES/messages.po @@ -873,6 +873,12 @@ msgstr "pob mis" msgid "per year" msgstr "y flwyddyn" +msgid "The problem you need help with" +msgstr "Y broblem y mae angen cymorth arnoch gyda hi" + +msgid "Change" +msgstr "Newid" + msgid "About you" msgstr "Amdanoch chi" @@ -1650,6 +1656,9 @@ msgstr "Eich eiddo" msgid "You and your partner’s property" msgstr "Eich eiddo chi ac eiddo’ch partner" +msgid "Check your answers and confirm" +msgstr "Gwiriwch eich atebion a chadarnhewch" + msgid "Your savings" msgstr "Eich cynilion" @@ -2479,6 +2488,9 @@ msgstr "Dileu eiddo" msgid "Add another property" msgstr "Ychwanegu eiddo arall" +msgid "About the problem" +msgstr "Am y broblem" + msgid "" "Any cash, savings or investments held in your name, your partner’s name " "or both your names." diff --git a/app/translations/en/LC_MESSAGES/messages.pot b/app/translations/en/LC_MESSAGES/messages.pot index b88928ff2..c1d1d0ef7 100644 --- a/app/translations/en/LC_MESSAGES/messages.pot +++ b/app/translations/en/LC_MESSAGES/messages.pot @@ -781,6 +781,12 @@ msgstr "" msgid "per year" msgstr "" +msgid "The problem you need help with" +msgstr "" + +msgid "Change" +msgstr "" + msgid "About you" msgstr "" @@ -1463,6 +1469,9 @@ msgstr "" msgid "You and your partner’s property" msgstr "" +msgid "Check your answers and confirm" +msgstr "" + msgid "Your savings" msgstr "" @@ -2186,6 +2195,9 @@ msgstr "" msgid "Add another property" msgstr "" +msgid "About the problem" +msgstr "" + msgid "" "Any cash, savings or investments held in your name, your partner’s name " "or both your names." diff --git a/tests/functional_tests/means_test/conftest.py b/tests/functional_tests/means_test/conftest.py index 2ab009d10..78201a6c0 100644 --- a/tests/functional_tests/means_test/conftest.py +++ b/tests/functional_tests/means_test/conftest.py @@ -1,4 +1,4 @@ -from playwright.sync_api import Page +from playwright.sync_api import Page, expect import pytest @@ -8,3 +8,66 @@ def navigate_to_means_test(page: Page): page.get_by_role("link", name="Homelessness").click() page.get_by_role("button", name="Check if you qualify financially").click() return page + + +@pytest.fixture +def complete_about_you_form( + page: Page, about_you_answers: dict, navigate_to_means_test +): + for question, answer in about_you_answers.items(): + form_group = page.get_by_role("group", name=question) + if question == "Do you have a partner?": + locator = "#has_partner" if answer == "Yes" else "#has_partner-2" + form_group.locator(locator).check() + elif question == "How many children aged 15 or under?": + page.locator("#num_children").fill(answer) + else: + form_group.get_by_label(answer).first.check() + + # Submit form + page.get_by_role("button", name="Continue").click() + return page + + +@pytest.fixture +def complete_benefits_form(page: Page, benefits_answers: dict, complete_about_you_form): + for question, answer in benefits_answers.items(): + form_group = page.get_by_role("group", name=question) + if question == "If yes, enter the total amount you get for all your children": + page.get_by_label("Amount").fill(answer["Amount"]) + page.get_by_label("Frequency").select_option(label=answer["Frequency"]) + elif isinstance(answer, list): + for label in answer: + form_group.get_by_label(label).first.check() + else: + form_group.get_by_label(answer).first.check() + # Submit form + page.get_by_role("button", name="Continue").click() + + return page + + +def assert_about_you_form_is_prefilled(page: Page, about_you_answers: dict): + for question, answer in about_you_answers.items(): + if question == "Do you have a partner?": + selector = "#has_partner" if answer == "Yes" else "#has_partner-2" + expect(page.locator(selector)).to_be_checked() + elif question == "How many children aged 15 or under?": + expect(page.locator("#num_children")).to_have_value(answer) + else: + locator = page.get_by_text(question).locator("..") + expect(locator.get_by_label(answer)).to_be_checked() + + +def assert_benefits_form_is_prefilled(page: Page, benefits_answers: dict): + for question, answer in benefits_answers.items(): + if question == "If yes, enter the total amount you get for all your children": + expect(page.get_by_label("Amount")).to_have_value(answer["Amount"]) + expect( + page.get_by_label("Frequency").locator("option:checked") + ).to_have_text(answer["Frequency"]) + elif isinstance(answer, list): + for label in answer: + expect(page.get_by_label(label)).to_be_checked() + else: + expect(page.get_by_label(question)).to_be_checked() diff --git a/tests/functional_tests/means_test/test_benefits_page.py b/tests/functional_tests/means_test/test_benefits_page.py index 95cd2e57a..7341e6fde 100644 --- a/tests/functional_tests/means_test/test_benefits_page.py +++ b/tests/functional_tests/means_test/test_benefits_page.py @@ -4,7 +4,7 @@ from app.means_test import YES, NO -next_page_heading = "Review your answers" +next_page_heading = "Check your answers and confirm" rfc_form_routing = [ pytest.param( ["Universal Credit"], diff --git a/tests/functional_tests/means_test/test_outgoings.py b/tests/functional_tests/means_test/test_outgoings.py index 343445359..7523d54cf 100644 --- a/tests/functional_tests/means_test/test_outgoings.py +++ b/tests/functional_tests/means_test/test_outgoings.py @@ -93,7 +93,9 @@ def test_outgoings_routing(page: Page, navigate_to_outgoings): page.get_by_role("textbox", name="Monthly Income Contribution").fill("500") page.get_by_role("button", name="Review your answers").click() - expect(page.get_by_role("heading", name="Review your answers")).to_be_visible() + expect( + page.get_by_role("heading", name="Check your answers and confirm") + ).to_be_visible() @pytest.mark.usefixtures("live_server") @@ -123,4 +125,6 @@ def test_outgoings_childcare(page: Page, navigate_to_outgoings): ) page.get_by_role("button", name="Review your answers").click() - expect(page.get_by_role("heading", name="Review your answers")).to_be_visible() + expect( + page.get_by_role("heading", name="Check your answers and confirm") + ).to_be_visible() diff --git a/tests/functional_tests/means_test/test_progress.py b/tests/functional_tests/means_test/test_progress.py index 2fa9b6892..6e090d8b8 100644 --- a/tests/functional_tests/means_test/test_progress.py +++ b/tests/functional_tests/means_test/test_progress.py @@ -57,7 +57,9 @@ def test_progress_component_full(page: Page): expect( page.get_by_text("Future page: You and your partner’s income and tax") ).to_be_visible() - expect(page.get_by_text("Future page: Review your answers")).to_be_visible() + expect( + page.get_by_text("Future page: Check your answers and confirm") + ).to_be_visible() expect(page.get_by_text("Future page: Contact information")).to_be_visible() page.get_by_role("link", name="Completed page: About you").click() diff --git a/tests/functional_tests/means_test/test_review.py b/tests/functional_tests/means_test/test_review.py new file mode 100644 index 000000000..9ae9970f5 --- /dev/null +++ b/tests/functional_tests/means_test/test_review.py @@ -0,0 +1,277 @@ +import pytest + +from flask import url_for +from playwright.sync_api import Page, expect +from tests.functional_tests.means_test.conftest import ( + assert_benefits_form_is_prefilled, + assert_about_you_form_is_prefilled, +) + + +about_you_form_routing = [ + pytest.param( + { + "Do you have a partner?": "No", + "Do you receive any benefits (including Child Benefit)?": "Yes", + "Do you have any children aged 15 or under?": "Yes", + "How many children aged 15 or under?": "1", + "Do you have any dependants aged 16 or over?": "No", + "Do you own any property?": "No", + "Are you employed?": "No", + "Are you self-employed?": "No", + "Are you or your partner (if you have one) aged 60 or over?": "No", + "Do you have any savings or investments?": "No", + "Do you have any valuable items worth over £500 each?": "No", + }, + id="about_you", + ) +] + +benefits_form_routing = [ + pytest.param( + { + "Which benefits do you receive?": [ + "Child Benefit", + "Guarantee Credit", + "Income Support", + "Income-based Jobseeker's Allowance", + "Income-related Employment and Support Allowance", + "Universal Credit", + ], + "If yes, enter the total amount you get for all your children": { + "Amount": "500.89", + "Frequency": "4 weekly", + }, + }, + id="benefits", + ) +] + + +def get_answers(): + return { + "The problem you need help with": { + "The problem you need help with": "Homelessness\nHelp if you’re homeless, or might be homeless in the next 2 months. This could be because of rent arrears, debt, the end of a relationship, or because you have nowhere to live." + }, + "About you": about_you_form_routing[0].values[0].copy(), + "Which benefits do you receive?": benefits_form_routing[0].values[0].copy(), + } + + +def assert_answers(page: Page, answers): + for title, route in answers.items(): + table = page.locator(f".govuk-summary-list[data-question='{title}']") + for question, answer in route.items(): + if isinstance(answer, dict) and "Amount" in answer: + answer = f"{answer['Amount']} ({answer['Frequency']})" + if not answer.startswith("£"): + answer = f"£{answer}" + if question == "How many children aged 15 or under?": + question = "How many?" + question_cell = table.get_by_text(question) + expect(question_cell).to_be_visible() + if isinstance(answer, list): + answer = "\n".join(answer) + expect(question_cell.locator(" + dd")).to_have_text(answer) + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_reviews_page(page: Page, complete_benefits_form): + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + # These forms were not completed and should not be on the reviews form + expect(page.get_by_role("heading", name="Your income and tax")).not_to_be_visible() + expect(page.get_by_role("heading", name="Your property")).not_to_be_visible() + + # This was a conditional field which was not triggered and should not be on the review form + expect( + page.get_by_text("Are you in a dispute with your partner?") + ).not_to_be_visible() + + # These forms WERE completed and should be on the reviews form + expect(page.get_by_role("heading", name="About you")).to_be_visible() + expect( + page.get_by_role("heading", name="Which benefits do you receive?") + ).to_be_visible() + + assert_answers(page, get_answers()) + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_reviews_page_change_benefits_answer(page: Page, complete_benefits_form): + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + answers = get_answers() + page.locator("a[href='/benefits#benefits']").click() + expect(page).to_have_title("Which benefits do you receive? - GOV.UK") + assert_benefits_form_is_prefilled(page, answers["Which benefits do you receive?"]) + + # Remove 'Universal Credit' as a selected benefit + page.get_by_label("Universal Credit").first.uncheck() + page.get_by_role("button", name="Continue").click() + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + + benefits_answers = answers["Which benefits do you receive?"][ + "Which benefits do you receive?" + ][:] + # Remove universal credit from the list of answers + benefits_answers = [ + item for item in benefits_answers if item not in ["Universal Credit"] + ] + answers["Which benefits do you receive?"]["Which benefits do you receive?"] = ( + benefits_answers + ) + assert_answers(page, answers) + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_reviews_page_change_sub_category(page: Page, complete_benefits_form): + answers = get_answers() + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + page.locator(".govuk-summary-list__actions a[href='/housing/']").click() + page.get_by_text("Eviction, told to leave your home").click() + expect(page).to_have_title( + "Legal aid is available for this type of problem - Access Civil Legal Aid – GOV.UK" + ) + + page.locator("a[href='/about-you']").click() + expect(page).to_have_title("About you - GOV.UK") + assert_about_you_form_is_prefilled(page, answers["About you"]) + page.get_by_role("button", name="Continue").click() + + expect(page).to_have_title("Which benefits do you receive? - GOV.UK") + assert_benefits_form_is_prefilled(page, answers["Which benefits do you receive?"]) + page.get_by_role("button", name="Continue").click() + + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + + answers["The problem you need help with"]["The problem you need help with"] = ( + "Eviction, told to leave your home\nLandlord has told you to leave or is trying to force you to leave. Includes if you’ve got a Section 21 or a possession order." + ) + assert_answers(page, answers) + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_reviews_page_change_category(page: Page, complete_benefits_form): + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + page.goto(url_for("categories.index", _external=True)) + page.get_by_text("Discrimination").click() + page.get_by_label( + "Work - including colleagues, employer or employment agency" + ).check() + page.get_by_role("button", name="Continue").click() + page.get_by_label("Race, colour, ethnicity, nationality").check() + page.get_by_role("button", name="Continue").click() + page.get_by_label("No").check() + page.get_by_role("button", name="Continue").click() + expect(page).to_have_title( + "Legal aid is available for this type of problem - Access Civil Legal Aid – GOV.UK" + ) + page.goto(url_for("means_test.review", _external=True)) + answers = get_answers() + answers["The problem you need help with"].update( + { + "The problem you need help with": "Discrimination", + "Where did the discrimination happen?": "Work - including colleagues, employer or employment agency", + "Why were you discriminated against?": "Race, colour, ethnicity, nationality", + "Are you under 18?": "No", + } + ) + assert_answers(page, answers) + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_change_answer_skip_means(page: Page, complete_benefits_form): + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + page.goto(url_for("categories.domestic_abuse.landing", _external=True)) + page.get_by_text("Problems with neighbours, landlords or other people").click() + page.get_by_role("heading", name="Contact us page") + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_change_answer_out_of_scope_problem(page: Page, complete_benefits_form): + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + page.goto(url_for("categories.housing.landing", _external=True)) + page.get_by_text("Next steps to get help").click() + page.get_by_role("heading", name="Legal aid doesn’t cover all types of problem") + + +@pytest.mark.usefixtures("live_server") +@pytest.mark.parametrize("about_you_answers", about_you_form_routing) +@pytest.mark.parametrize("benefits_answers", benefits_form_routing) +def test_change_answer_nolonger_passported(page: Page, complete_benefits_form): + # Change from passported means to non-passported + answers = get_answers() + + expect(page).to_have_title("Check your answers and confirm - GOV.UK") + page.goto(url_for("means_test.about-you", _external=True)) + assert_about_you_form_is_prefilled(page, answers["About you"]) + # You no longer receive benefits + locator = page.get_by_text( + "Do you receive any benefits (including Child Benefit)?" + ).locator("..") + locator.get_by_label("No").click() + page.get_by_role("button", name="Continue").click() + + expect(page).to_have_title("Your money coming in - GOV.UK") + answers["Your money coming in"] = { + "Maintenance received": { + "Amount": "120.56", + "Frequency": "per month", + "prefix": "maintenance_received", + }, + "Pension received": { + "Amount": "8.01", + "Frequency": "per month", + "prefix": "pension", + }, + "Any other income": { + "Amount": "100.50", + "Frequency": "per month", + "prefix": "other_income", + }, + } + for field in answers["Your money coming in"].values(): + page.locator(f"#{field['prefix']}-value").scroll_into_view_if_needed() + page.locator(f"#{field['prefix']}-value").fill(field["Amount"]) + page.locator(f"#{field['prefix']}-interval").select_option(field["Frequency"]) + + page.get_by_role("button", name="Continue").click() + + answers["Your outgoings"] = { + "Rent": {"Amount": "50.99", "Frequency": "per week", "prefix": "rent"}, + "Maintenance": { + "Amount": "18.28", + "Frequency": "per month", + "prefix": "maintenance", + }, + "Childcare": { + "Amount": "12.34", + "Frequency": "per month", + "prefix": "childcare", + }, + } + for field in answers["Your outgoings"].values(): + page.locator(f"#{field['prefix']}-value").scroll_into_view_if_needed() + page.locator(f"#{field['prefix']}-value").fill(field["Amount"]) + page.locator(f"#{field['prefix']}-interval").select_option(field["Frequency"]) + + answers["Your outgoings"]["Monthly Income Contribution Order"] = "£50.00" + page.get_by_label("Monthly Income Contribution Order").fill("50.00") + page.get_by_role("button", name="Review your answers").click() + # No longer on benefits + del answers["Which benefits do you receive?"] + del answers["About you"]["Do you receive any benefits (including Child Benefit)?"] + assert_answers(page, answers) + + expect(page.get_by_text("Which benefits do you receive?")).not_to_be_visible() diff --git a/tests/unit_tests/categories/test_session.py b/tests/unit_tests/categories/test_session.py index 789c09661..06b1a0839 100644 --- a/tests/unit_tests/categories/test_session.py +++ b/tests/unit_tests/categories/test_session.py @@ -1,25 +1,48 @@ -from app.categories.constants import Category +from app.categories.constants import Category, HOUSING +from app.categories.models import CategoryAnswer def test_set_category_question_answer_new_session(app, client): with client.session_transaction() as session: - session.set_category_question_answer("Q1", "A1", "C1") + answer = CategoryAnswer( + question="What is your favourite mode of transport?", + answer_value="bus", + answer_label="Bus", + next_page="categories.index", + question_page="categories.housing.landing", + category=HOUSING, + ) + session.set_category_question_answer(answer) assert "category_answers" in session - assert session["category_answers"] == [ - {"question": "Q1", "answer": "A1", "category": "C1"} - ] + expected_category_answers = [answer.__dict__] + expected_category_answers[0]["category"] = {"code": "housing"} + assert session["category_answers"] == expected_category_answers def test_set_category_question_answer_updates_existing(app, client): with client.session_transaction() as session: - session["category_answers"] = [ - {"question": "Q1", "answer": "A1", "category": "C1"} - ] - session.set_category_question_answer("Q1", "A2", "C2") - assert session["category_answers"] == [ - {"question": "Q1", "answer": "A2", "category": "C2"} - ] + answer = CategoryAnswer( + question="What is your favourite mode of transport?", + answer_value="bus", + answer_label="Bus", + next_page="categories.index", + question_page="categories.housing.landing", + category=HOUSING, + ) + session.set_category_question_answer(answer) + updated_answer = CategoryAnswer( + question="What is your favourite mode of transport?", + answer_value="car", + answer_label="Car", + next_page="categories.index", + question_page="categories.housing.landing", + category=HOUSING, + ) + session.set_category_question_answer(updated_answer) + expected_category_answers = [updated_answer.__dict__] + expected_category_answers[0]["category"] = {"code": "housing"} + assert session["category_answers"] == expected_category_answers def test_get_category_question_answer_empty_session(app, client): @@ -29,20 +52,41 @@ def test_get_category_question_answer_empty_session(app, client): def test_get_category_question_answer_found(app, client): + first_answer = CategoryAnswer( + question="What is your favourite mode of transport?", + answer_value="bus", + answer_label="Bus", + next_page="categories.index", + question_page="categories.housing.landing", + category=HOUSING, + ) + second_answer = CategoryAnswer( + question="Where did this happen?", + answer_value="home", + answer_label="Home", + next_page="categories.index", + question_page="categories.housing.landing", + category=HOUSING, + ) with client.session_transaction() as session: - session["category_answers"] = [ - {"question": "test_question", "answer": "A1", "category": "C1"} - ] - result = session.get_category_question_answer("test_question") - assert result == "A1" + session.set_category_question_answer(first_answer) + session.set_category_question_answer(second_answer) + result = session.get_category_question_answer("Where did this happen?") + assert result == "home" def test_get_category_question_answer_not_found(app, client): + answer = CategoryAnswer( + question="What is your favourite mode of transport?", + answer_value="bus", + answer_label="Bus", + next_page="categories.index", + question_page="categories.housing.landing", + category=HOUSING, + ) with client.session_transaction() as session: - session["category_answers"] = [ - {"question": "other_question", "answer": "A1", "category": "C1"} - ] - result = session.get_category_question_answer("test_question") + session.set_category_question_answer(answer) + result = session.get_category_question_answer("Hello?") assert result is None @@ -52,23 +96,6 @@ def test_set_category_dataclass(app, client): assert isinstance(EDUCATION, Category) with client.session_transaction() as session: - session["category"] = EDUCATION + session["category"] = {"code": EDUCATION.code} assert session.category == EDUCATION assert isinstance(session.category, Category) - - -def test_set_category_dict(app, client): - new_category = { - "code": "EDUCATION", - "title": "Education", - "description": "This is a test", - "chs_code": "EDUCATION", - "article_category_name": "education", - } - - assert isinstance(new_category, dict) - - with client.session_transaction() as session: - session["category"] = new_category - assert session.category == Category(**new_category) - assert isinstance(session.category, Category) diff --git a/tests/unit_tests/categories/test_views.py b/tests/unit_tests/categories/test_views.py index 2954a2de2..7479159da 100644 --- a/tests/unit_tests/categories/test_views.py +++ b/tests/unit_tests/categories/test_views.py @@ -7,11 +7,17 @@ CannotFindYourProblemPage, NextStepsPage, ) +from wtforms import RadioField +from wtforms.validators import InputRequired +from app.categories.widgets import CategoryRadioInput +from app.categories.views import QuestionPage, QuestionForm def test_category_page_dispatch(app): with app.app_context(): - page = FamilyLandingPage(FamilyLandingPage.template) + page = FamilyLandingPage( + route_endpoint="family", template=FamilyLandingPage.template + ) page.dispatch_request() assert session.category == FamilyLandingPage.category @@ -67,3 +73,48 @@ def test_init_with_category(self): def test_init_with_no_help_organisations(self): page = NextStepsPage(get_help_organisations=False) assert page.template == "categories/next-steps.html" + + +def test_question_page_next_page(app): + class TestQuestionForm(QuestionForm): + next_step_mapping = { + "yes": "categories.results.in_scope", + "no": "categories.results.refer", + "notsure": "categories.index", + "fala": { + "endpoint": "find-a-legal-adviser.search", + "category": "mhe", + "secondary_category": "com", + }, + } + question = RadioField( + "This is a test question?", + widget=CategoryRadioInput( + show_divider=False + ), # Uses our override class to support setting custom CSS on the label title + validators=[InputRequired(message="Validation failed message")], + choices=[ + ("yes", "Yes"), + ("no", "No"), + ], + ) + + with app.app_context(): + form = TestQuestionForm(category=FAMILY, question="yes") + view = QuestionPage(form_class=form) + assert "/legal-aid-available" == view.get_next_page("yes") + + form = TestQuestionForm(category=FAMILY, question="no") + view = QuestionPage(form_class=form) + assert "/refer" == view.get_next_page("no") + + form = TestQuestionForm(category=FAMILY, question="notsure") + view = QuestionPage(form_class=form) + assert "/find-your-problem" == view.get_next_page("notsure") + + form = TestQuestionForm(category=FAMILY, question="notsure") + view = QuestionPage(form_class=form) + assert ( + "/find-a-legal-adviser?category=mhe&secondary_category=com" + == view.get_next_page("fala") + ) diff --git a/tests/unit_tests/means_test/test_form_summary.py b/tests/unit_tests/means_test/test_form_summary.py new file mode 100644 index 000000000..4df97fb24 --- /dev/null +++ b/tests/unit_tests/means_test/test_form_summary.py @@ -0,0 +1,107 @@ +from unittest import mock +from wtforms.fields import IntegerField, RadioField, SelectMultipleField +from app.means_test.forms import BaseMeansTestForm +from app.means_test.validators import ValidateIf, ValidateIfSession +from app.means_test.fields import MoneyIntervalField + + +class TestForm(BaseMeansTestForm): + confirm = RadioField("Do you like colours", choices=[("yes", "Yes"), ("no", "No")]) + age = IntegerField("Please enter your age") + colour = SelectMultipleField( + label="Please select your favourite colour", + choices=[("red", "Red"), ("green", "Green"), ("blue", "Blue")], + ) + salary = MoneyIntervalField("What is your salary?") + + def filter_summary(self, summary: dict) -> dict: + confirm = summary.get("confirm", "no") + if "colour" in summary and confirm == "no": + del summary["colour"] + return summary + + +def test_summary(app): + expected_summary = { + "confirm": { + "question": "Do you like colours", + "answer": "Yes", + "id": "confirm", + }, + "age": { + "question": "Please enter your age", + "answer": 20, + "id": "age", + }, + "colour": { + "question": "Please select your favourite colour", + "answer": ["Red"], + "id": "colour", + }, + "salary": { + "question": "What is your salary?", + "answer": "£200.50 (4 weekly)", + "id": "salary", + }, + } + + with app.app_context(): + salary = {"per_interval_value": 20050, "interval_period": "per_4week"} + form = TestForm( + **{"confirm": "yes", "age": 20, "colour": ["red"], "salary": salary} + ) + summary = form.summary() + assert summary == expected_summary + + +def test_summary_with_multiple(app): + expected_summary = { + "confirm": { + "question": "Do you like colours", + "answer": "Yes", + "id": "confirm", + }, + "colour": { + "question": "Please select your favourite colour", + "answer": ["Red", "Blue"], + "id": "colour", + }, + } + + with app.app_context(): + form = TestForm(**{"confirm": "yes", "colour": ["red", "blue"]}) + summary = form.summary() + assert summary == expected_summary + + +def test_is_unvalidated_conditional_field(app): + class TestConditionalForm(BaseMeansTestForm): + confirm = RadioField( + "Do you like colours", choices=[("yes", "Yes"), ("no", "No")] + ) + colours = SelectMultipleField( + validators=[ValidateIf("confirm", "yes")], + label="Please select your favourite colour", + choices=[("red", "Red"), ("green", "Green"), ("blue", "Blue")], + ) + user_id = IntegerField( + "User Id", validators=[ValidateIfSession("is_returning_user", "yes")] + ) + + with app.app_context(): + form = TestConditionalForm(**{"confirm": "yes", "colours": ["red", "blue"]}) + # The colours validator condition has been met and should not be skipped + assert form.is_unvalidated_conditional_field(form.colours) is False + form = TestConditionalForm(**{"confirm": "no"}) + # The colours validator condition has NOT been met and should be skipped + assert form.is_unvalidated_conditional_field(form.colours) is True + with mock.patch("app.means_test.validators.session") as mock_session: + form = TestConditionalForm(**{"confirm": "yes", "colours": ["red", "blue"]}) + eligibility = mock.Mock() + mock_session.get_eligibility = mock.Mock(side_effect=lambda: eligibility) + eligibility.configure_mock(is_returning_user="no") + # The user_id validator condition has NOT been met and should be skipped + assert form.is_unvalidated_conditional_field(form.user_id) is True + eligibility.configure_mock(is_returning_user="yes") + # The user_id validator condition has been met and should not be skipped + assert form.is_unvalidated_conditional_field(form.user_id) is False diff --git a/tests/unit_tests/means_test/test_views_summary.py b/tests/unit_tests/means_test/test_views_summary.py new file mode 100644 index 000000000..8bcf4e397 --- /dev/null +++ b/tests/unit_tests/means_test/test_views_summary.py @@ -0,0 +1,226 @@ +from unittest import mock +from flask_babel import lazy_gettext as _ +from app.means_test.views import CheckYourAnswers, ReviewForm +from app.means_test import YES, NO +from app.session import Eligibility +from app.categories.models import CategoryAnswer +from app.categories.constants import DISCRIMINATION, HOUSING + + +def mock_render_template(template_name, **kwargs): + return kwargs + + +def mock_session_get_eligibility(): + return Eligibility( + **{ + "forms": { + "about-you": { + "has_partner": NO, + "on_benefits": YES, + "have_children": NO, + "have_dependents": NO, + "own_property": NO, + }, + "benefits": {"benefits": ["employment_support", "universal_credit"]}, + } + } + ) + + +@mock.patch("app.means_test.views.render_template", mock_render_template) +@mock.patch( + "app.means_test.views.session.get_eligibility", mock_session_get_eligibility +) +def test_views_summary(app): + expected_summary = { + "About you": [ + { + "key": {"text": "Do you have a partner?"}, + "value": {"text": "No"}, + "actions": { + "items": [{"href": "/about-you#has_partner", "text": _("Change")}] + }, + }, + { + "key": { + "text": "Do you receive any benefits (including Child Benefit)?" + }, + "value": {"text": "Yes"}, + "actions": { + "items": [{"href": "/about-you#on_benefits", "text": _("Change")}] + }, + }, + { + "key": {"text": "Do you have any children aged 15 or under?"}, + "value": {"text": "No"}, + "actions": { + "items": [{"href": "/about-you#have_children", "text": _("Change")}] + }, + }, + { + "key": {"text": "Do you have any dependants aged 16 or over?"}, + "value": {"text": "No"}, + "actions": { + "items": [ + {"href": "/about-you#have_dependents", "text": _("Change")} + ] + }, + }, + { + "key": {"text": "Do you own any property?"}, + "value": {"text": "No"}, + "actions": { + "items": [{"href": "/about-you#own_property", "text": _("Change")}] + }, + }, + ], + "Which benefits do you receive?": [ + { + "key": {"text": "Which benefits do you receive?"}, + "value": { + "markdown": "Income-related Employment and Support Allowance\nUniversal Credit" + }, + "actions": { + "items": [{"href": "/benefits#benefits", "text": _("Change")}] + }, + } + ], + } + + with app.app_context(): + summary = CheckYourAnswers().get() + assert summary["means_test_summary"] == expected_summary + + assert isinstance(summary["form"], ReviewForm) + + +def test_get_category_answers_summary_no_description(app): + expected_summary = [ + { + "key": {"text": _("The problem you need help with")}, + "value": {"text": _("Discrimination")}, + "actions": {"items": [{"text": _("Change"), "href": "/find-your-problem"}]}, + }, + { + "key": {"text": "Where did the discrimination happen?"}, + "value": { + "text": "Work - including colleagues, employer or employment agency" + }, + "actions": { + "items": [{"text": _("Change"), "href": "/discrimination/where"}] + }, + }, + { + "key": {"text": "Why were you discriminated against?"}, + "value": {"text": "Disability, health condition, mental health condition"}, + "actions": { + "items": [{"text": _("Change"), "href": "/discrimination/why"}] + }, + }, + { + "key": {"text": "Are you under 18?"}, + "value": {"text": "No"}, + "actions": { + "items": [{"text": _("Change"), "href": "/discrimination/age"}] + }, + }, + ] + + category_mocker = mock.patch( + "app.session.Session.category", + new_callable=mock.PropertyMock, + return_value=DISCRIMINATION, + ) + category_mocker.start() + + mock_category_answers = [ + CategoryAnswer( + question="Where did the discrimination happen?", + category=DISCRIMINATION, + answer_label="Work - including colleagues, employer or employment agency", + answer_value="work", + next_page="categories.discrimination.why", + question_page="categories.discrimination.where", + ), + CategoryAnswer( + question="Why were you discriminated against?", + category=DISCRIMINATION, + answer_label="Disability, health condition, mental health condition", + answer_value="disability", + next_page="categories.discrimination.age", + question_page="categories.discrimination.why", + ), + CategoryAnswer( + question="Are you under 18?", + category=DISCRIMINATION, + answer_label="No", + answer_value="no", + next_page="categories.index", + question_page="categories.discrimination.age", + ), + ] + + with app.app_context(): + with mock.patch( + "app.session.Session.category_answers", new_callable=mock.PropertyMock + ) as mocker: + mocker.return_value = mock_category_answers + summary = CheckYourAnswers().get_category_answers_summary() + assert summary == expected_summary + + category_mocker.stop() + + +def test_get_category_answers_summary_with_description(app): + expected_summary = [ + { + "key": {"text": _("The problem you need help with")}, + "value": { + "markdown": "**Homelessness**\nHelp if you’re homeless, or might be homeless in the next 2 months. This could be because of rent arrears, debt, the end of a relationship, or because you have nowhere to live." + }, + "actions": {"items": [{"text": _("Change"), "href": "/housing/"}]}, + }, + { + "key": {"text": "Are you under 18?"}, + "value": {"text": "No"}, + "actions": { + "items": [{"text": _("Change"), "href": "/discrimination/age"}] + }, + }, + ] + + mock_category_answers = [ + CategoryAnswer( + question="Homelessness", + category=HOUSING.sub.homelessness, + answer_label="Homelessness", + answer_value="homelessness", + next_page="categories.discrimination.age", + question_page="categories.housing.landing", + ), + CategoryAnswer( + question="Are you under 18?", + category=HOUSING.sub.homelessness, + answer_label="No", + answer_value="no", + next_page="categories.index", + question_page="categories.discrimination.age", + ), + ] + + category_mocker = mock.patch( + "app.session.Session.category", new_callable=mock.PropertyMock + ) + category_mocker.return_value = HOUSING + category_mocker.start() + + with app.app_context(): + with mock.patch( + "app.session.Session.category_answers", new_callable=mock.PropertyMock + ) as mocker: + mocker.return_value = mock_category_answers + summary = CheckYourAnswers().get_category_answers_summary() + assert summary == expected_summary + + category_mocker.stop() From 75dea0fc9bdbcb2f40ac5ea60f0f80f8f9e71b5c Mon Sep 17 00:00:00 2001 From: Ben Millar Date: Mon, 10 Mar 2025 10:23:14 +0000 Subject: [PATCH 2/5] Fix Savings Validators (#189) * Add ValidateIfSession for savings and investments fields * Add functional tests --- app/means_test/forms/savings.py | 6 +- .../means_test/test_savings.py | 125 ++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/app/means_test/forms/savings.py b/app/means_test/forms/savings.py index 7901d8694..c5d9a5570 100644 --- a/app/means_test/forms/savings.py +++ b/app/means_test/forms/savings.py @@ -20,7 +20,8 @@ class SavingsForm(BaseMeansTestForm): "The total amount of savings in cash, bank or building society; or enter 0 if you have none" ), validators=[ - InputRequired(message=_("Enter your total savings, or 0 if you have none")) + ValidateIfSession("has_savings", True), + InputRequired(message=_("Enter your total savings, or 0 if you have none")), ], ) @@ -31,9 +32,10 @@ class SavingsForm(BaseMeansTestForm): "This includes stocks, shares, bonds (but not property); enter 0 if you have none" ), validators=[ + ValidateIfSession("has_savings", True), InputRequired( message=_("Enter your total investments, or 0 if you have none") - ) + ), ], ) diff --git a/tests/functional_tests/means_test/test_savings.py b/tests/functional_tests/means_test/test_savings.py index 8764d79ad..7ce6b6d56 100644 --- a/tests/functional_tests/means_test/test_savings.py +++ b/tests/functional_tests/means_test/test_savings.py @@ -99,3 +99,128 @@ def test_savings_form(page: Page, scenario: str, form_inputs: dict, expected: di if "nth" in field: locator = locator.nth(field["nth"]) expect(locator).to_be_visible() + + +@pytest.mark.parametrize( + "scenario,about_you_form_inputs,savings_form_inputs, expected_errors", + [ + ( + "single_person_savings", + { + "has_partner": "No", + "Do you receive any benefits": "No", + "Do you have any children aged": "No", + "Do you have any dependants": "No", + "Do you own any property?": "No", + "Are you employed?": "No", + "Are you self-employed?": "No", + "Are you or your partner (if": "No", + "Do you have any savings or": "No", + "Do you have any valuable": "Yes", + }, + { + "Total value of items worth over": "499", + }, + { + "errors": [ + "Error: Enter 0 if you have no valuable items worth over £500 each" + ] + }, + ), + ( + "single_person_savings_valuables", + { + "has_partner": "No", + "Do you receive any benefits": "No", + "Do you have any children aged": "No", + "Do you have any dependants": "No", + "Do you own any property?": "No", + "Are you employed?": "No", + "Are you self-employed?": "No", + "Are you or your partner (if": "No", + "Do you have any savings or": "Yes", + "Do you have any valuable": "Yes", + }, + { + "Total value of items worth over": "500", + "Savings": "£1000", + "Investments": "£1000.00", + }, + {"errors": []}, + ), + ( + "single_person_valuables_no_errors", + { + "has_partner": "No", + "Do you receive any benefits": "No", + "Do you have any children aged": "No", + "Do you have any dependants": "No", + "Do you own any property?": "No", + "Are you employed?": "No", + "Are you self-employed?": "No", + "Are you or your partner (if": "No", + "Do you have any savings or": "No", + "Do you have any valuable": "Yes", + }, + { + "Total value of items worth over": "500", + }, + {"errors": []}, + ), + ( + "single_person_savings_valuables", + { + "has_partner": "No", + "Do you receive any benefits": "No", + "Do you have any children aged": "No", + "Do you have any dependants": "No", + "Do you own any property?": "No", + "Are you employed?": "No", + "Are you self-employed?": "No", + "Are you or your partner (if": "No", + "Do you have any savings or": "Yes", + "Do you have any valuable": "Yes", + }, + { + "Total value of items worth over": "500", + "Savings": "", + "Investments": "", + }, + { + "errors": [ + "Error: Enter your total savings, or 0 if you have none", + "Error: Enter your total investments, or 0 if you have none", + ] + }, + ), + ], +) +@pytest.mark.usefixtures("live_server") +def test_savings_form_validators( + page: Page, + scenario: str, + about_you_form_inputs: dict, + savings_form_inputs: dict, + expected_errors: dict, +): + page.goto(url_for("means_test.about-you", _external=True)) + + page.locator( + "#has_partner" + if about_you_form_inputs["has_partner"] == "Yes" + else "#has_partner-2" + ).check() + del about_you_form_inputs["has_partner"] + + fill_about_form(page, about_you_form_inputs) + page.get_by_role("button", name="Continue").click() + + for field, value in savings_form_inputs.items(): + page.get_by_role("textbox", name=field).fill(value) + + page.get_by_role("button", name="Continue").click() + + for error in expected_errors["errors"]: + expect(page.get_by_text(error)).to_be_visible() + if len(expected_errors["errors"]) == 0: + expect(page.get_by_role("heading", name="Your money coming in")) From c9110dff6c269501ad9308b25189119451b4bcd0 Mon Sep 17 00:00:00 2001 From: Kyle O'Brien <65071578+TawneeOwl@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:46:39 +0000 Subject: [PATCH 3/5] feature/LGA-3448-exit-this-page (#190) * change spacing * Update contact page logic * add in welsh translations * Update format of exit this page * Update back to compenent * Update session category * Update tests --- app/main/__init__.py | 12 ++++++++- app/templates/base.html | 6 +++++ app/templates/categories/benefits/appeal.html | 5 ---- app/templates/categories/in-scope.html | 1 - app/templates/categories/landing.html | 4 --- app/templates/categories/public/landing.html | 1 - app/templates/categories/question-page.html | 5 ---- app/templates/contact/contact.html | 6 +---- app/templates/contact/rfc.html | 1 - app/templates/means_test/form-page.html | 1 - app/translations/cy/LC_MESSAGES/messages.po | 25 +++++++++++++----- app/translations/en/LC_MESSAGES/messages.pot | 7 +++-- .../domestic_abuse/test_domestic_abuse.py | 8 ++---- tests/unit_tests/test_exit_this_page.py | 26 +++++++++++++++++++ 14 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 tests/unit_tests/test_exit_this_page.py diff --git a/app/main/__init__.py b/app/main/__init__.py index 83318cadf..86c8dc345 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -1,4 +1,4 @@ -from flask import Blueprint, request, url_for +from flask import Blueprint, request, url_for, session from flask import current_app bp = Blueprint("main", __name__, template_folder="../templates/main") @@ -30,3 +30,13 @@ def inject_language_switcher(): }, } } + + +@bp.app_context_processor +def inject_exit_this_page(): + category = session.category + + if not category: + return {"show_exit_this_page": False} + + return {"show_exit_this_page": getattr(category, "exit_page", False)} diff --git a/app/templates/base.html b/app/templates/base.html index 7bf9d755d..ec353198e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,6 +4,7 @@ {%- from 'govuk_frontend_jinja/components/cookie-banner/macro.html' import govukCookieBanner-%} {%- from 'govuk_frontend_jinja/components/error-summary/macro.html' import govukErrorSummary-%} +{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {%- from 'govuk_frontend_jinja/components/notification-banner/macro.html' import govukNotificationBanner -%} {%- from 'govuk_frontend_jinja/components/phase-banner/macro.html' import govukPhaseBanner -%} {%- from 'main/_modal-dialog.html' import timeout_dialog %} @@ -129,6 +130,11 @@ }, 'html': 'Contact us if you need help or would like to give feedback to improve this service' }) }} + {% if show_exit_this_page %} + {{ govukExitThisPage({ + "text": _("Exit this page") + }) }} + {% endif %} {% endblock %} diff --git a/app/templates/categories/benefits/appeal.html b/app/templates/categories/benefits/appeal.html index f43e05150..b6c7fbcdd 100644 --- a/app/templates/categories/benefits/appeal.html +++ b/app/templates/categories/benefits/appeal.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {% block pageTitle %}{%- if form.errors %}Error: {% endif -%}{{ form.title }} - GOV.UK{% endblock %} @@ -8,10 +7,6 @@ {{ super() }} {{ govukBackLink() }} -{% if form.category == "Domestic Abuse" %} - {{ govukExitThisPage("Exit this page")}} -{% endif %} - {% endblock %} {% block content %} diff --git a/app/templates/categories/in-scope.html b/app/templates/categories/in-scope.html index 57cb688ad..048beb364 100644 --- a/app/templates/categories/in-scope.html +++ b/app/templates/categories/in-scope.html @@ -1,7 +1,6 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} {%- from 'govuk_frontend_jinja/components/button/macro.html' import govukButton %} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {%- from 'categories/components/help-organisations.html' import helpOrganisationsList %} {% set title = _('Legal aid is available for this type of problem') %} diff --git a/app/templates/categories/landing.html b/app/templates/categories/landing.html index 8c22c6b90..92fafe3da 100644 --- a/app/templates/categories/landing.html +++ b/app/templates/categories/landing.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {%- from 'categories/components/list-item.html' import list_item, list_item_small -%} {%- from 'categories/components/cannot-find-your-problem.html' import cannot_find_your_problem -%} @@ -10,9 +9,6 @@ {{ super() }} {{ govukBackLink() }} -{% if category.exit_page %} - {{ govukExitThisPage("Exit this page")}} -{% endif %} {% endblock %} {% block content %} diff --git a/app/templates/categories/public/landing.html b/app/templates/categories/public/landing.html index ab7bbb4fc..f89da45b2 100644 --- a/app/templates/categories/public/landing.html +++ b/app/templates/categories/public/landing.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {%- from 'categories/components/list-item.html' import list_item, list_item_small -%} {%- from 'categories/components/cannot-find-your-problem.html' import cannot_find_your_problem -%} diff --git a/app/templates/categories/question-page.html b/app/templates/categories/question-page.html index 0a466a94f..778fdc6d4 100644 --- a/app/templates/categories/question-page.html +++ b/app/templates/categories/question-page.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {% block pageTitle %}{%- if form.errors %}Error: {% endif -%}{{ form.title }} - GOV.UK{% endblock %} @@ -8,10 +7,6 @@ {{ super() }} {{ govukBackLink() }} -{% if form.category == "Category.DOMESTIC_ABUSE" %} - {{ govukExitThisPage("Exit this page")}} -{% endif %} - {% endblock %} {% block content %} diff --git a/app/templates/contact/contact.html b/app/templates/contact/contact.html index b29458b87..3e3fe0444 100644 --- a/app/templates/contact/contact.html +++ b/app/templates/contact/contact.html @@ -2,7 +2,6 @@ {%- from 'components/back_link.html' import govukBackLink -%} {%- from 'categories/components/list-item.html' import list_item, list_item_small -%} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {%- from 'govuk_frontend_jinja/components/inset-text/macro.html' import govukInsetText %} {%- from 'govuk_frontend_jinja/components/warning-text/macro.html' import govukWarningText %} @@ -182,9 +181,6 @@ {{ super() }} {{ govukBackLink() }} -{% if session.category.exit_page %} - {{ govukExitThisPage("Exit this page")}} -{% endif %} {% endblock %} {% block content %} @@ -199,7 +195,7 @@

{{ form.page_title }}

{% endblock %} {% block formDescription %} - {% if session.category.exit_page %} + {% if show_exit_this_page %} {{ govukWarningText({ "text": _("If you’re in an emergency situation, please call the police on 999.") }) }} diff --git a/app/templates/contact/rfc.html b/app/templates/contact/rfc.html index 4cb351c26..39ff473f0 100644 --- a/app/templates/contact/rfc.html +++ b/app/templates/contact/rfc.html @@ -1,7 +1,6 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} {%- from 'govuk_frontend_jinja/components/button/macro.html' import govukButton %} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {% block pageTitle %}Why do you want to contact Civil Legal Advice?{% endblock %} diff --git a/app/templates/means_test/form-page.html b/app/templates/means_test/form-page.html index b1984b3ee..178c701a4 100644 --- a/app/templates/means_test/form-page.html +++ b/app/templates/means_test/form-page.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {%- from 'components/back_link.html' import govukBackLink -%} -{%- from 'govuk_frontend_jinja/components/exit-this-page/macro.html' import govukExitThisPage -%} {%- from 'govuk_frontend_jinja/components/inset-text/macro.html' import govukInsetText %} {%- from 'means_test/components/progress-bar.html' import progress as means_test_progress %} diff --git a/app/translations/cy/LC_MESSAGES/messages.po b/app/translations/cy/LC_MESSAGES/messages.po index 16ffda40b..e94a3240e 100644 --- a/app/translations/cy/LC_MESSAGES/messages.po +++ b/app/translations/cy/LC_MESSAGES/messages.po @@ -17,7 +17,10 @@ msgid "" "Includes keeping you or your family safe, getting court orders and help " "if someone is ignoring a court order. Also, if you’re being stalked, " "threatened or harassed." -msgstr "Mae hyn yn cynnwys eich cadw chi neu eich teulu’n ddiogel, cael gorchmynion llys a chymorth os bydd rhywun yn anwybyddu gorchymyn llys. Hefyd, os ydych chi’n cael eich stelcio, eich bygwth neu eich aflonyddu." +msgstr "" +"Mae hyn yn cynnwys eich cadw chi neu eich teulu’n ddiogel, cael " +"gorchmynion llys a chymorth os bydd rhywun yn anwybyddu gorchymyn llys. " +"Hefyd, os ydych chi’n cael eich stelcio, eich bygwth neu eich aflonyddu." msgid "Leaving an abusive relationship" msgstr "Gadael perthynas gamdriniol" @@ -25,7 +28,9 @@ msgstr "Gadael perthynas gamdriniol" msgid "" "Help with divorce, separation, or leaving your partner. Includes legal " "arrangements for children, money and housing." -msgstr "Cymorth i ysgaru, i wahanu neu i adael eich partner. Mae hyn yn cynnwys trefniadau cyfreithiol ar gyfer plant, arian a thai." +msgstr "" +"Cymorth i ysgaru, i wahanu neu i adael eich partner. Mae hyn yn cynnwys " +"trefniadau cyfreithiol ar gyfer plant, arian a thai." msgid "Problems with an ex-partner: children or money" msgstr "Problemau gyda chyn-bartner: plant neu arian" @@ -34,7 +39,10 @@ msgid "" "Includes arrangements for children and money. If an ex-partner is not " "following agreements or court orders. If you’re worried about a child, or" " if a child is taken or kept without your permission." -msgstr "Mae'n cynnwys trefniadau ar gyfer plant ac arian. Os nad yw cynbartner yn dilyn cytundebau neu orchmynion llys. Os ydych chi'n poeni am blentyn, neu os yw plentyn yn cael ei gymryd neu ei gadw heb eich caniatâd." +msgstr "" +"Mae'n cynnwys trefniadau ar gyfer plant ac arian. Os nad yw cynbartner yn" +" dilyn cytundebau neu orchmynion llys. Os ydych chi'n poeni am blentyn, " +"neu os yw plentyn yn cael ei gymryd neu ei gadw heb eich caniatâd." msgid "Problems with neighbours, landlords or other people" msgstr "Problemau gyda chymdogion, landlordiaid neu bobl eraill" @@ -48,7 +56,10 @@ msgstr "Tai, digartrefedd, colli eich cartref" msgid "" "Includes being forced to leave your home, problems with council housing, " "if you’re homeless or might be homeless in the next 2 months." -msgstr "Mae hyn yn cynnwys cael eich gorfodi i adael eich cartref, problemau gyda thai cyngor, os ydych chi'n ddigartref neu y gallech fod yn ddigartref yn ystod y 2 fis nesaf." +msgstr "" +"Mae hyn yn cynnwys cael eich gorfodi i adael eich cartref, problemau gyda" +" thai cyngor, os ydych chi'n ddigartref neu y gallech fod yn ddigartref " +"yn ystod y 2 fis nesaf." msgid "Forced Marriage" msgstr "Priodas dan orfod" @@ -62,8 +73,7 @@ msgid "Female genital mutilation (FGM)" msgstr "Anffurfio Organau Cenhedlu Benywod (FGM)" msgid "If you or someone else is at risk of FGM." -msgstr "" -"Os ydych chi neu rywun arall mewn perygl o FGM." +msgstr "Os ydych chi neu rywun arall mewn perygl o FGM." msgid "Children, families, relationships" msgstr "Plant, teuluoedd, perthnasoedd" @@ -1703,6 +1713,9 @@ msgstr "Rhowch gyfanswm yr holl eitemau gwerthfawr dros £500" msgid "Enter 0 if you have no valuable items worth over £500 each" msgstr "Rhowch 0 os nad oes gennych eitemau gwerthfawr sy’n werth dros £500 yr un" +msgid "Exit this page" +msgstr "Gadael y dudalen hon" + msgid "Sorry, you’re not likely to get legal aid" msgstr "" diff --git a/app/translations/en/LC_MESSAGES/messages.pot b/app/translations/en/LC_MESSAGES/messages.pot index c1d1d0ef7..bc4bc046b 100644 --- a/app/translations/en/LC_MESSAGES/messages.pot +++ b/app/translations/en/LC_MESSAGES/messages.pot @@ -8,14 +8,14 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-03-05 08:35+0000\n" +"POT-Creation-Date: 2025-03-10 08:24+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.16.0\n" +"Generated-By: Babel 2.17.0\n" msgid "Domestic abuse" msgstr "" @@ -1512,6 +1512,9 @@ msgstr "" msgid "Enter 0 if you have no valuable items worth over £500 each" msgstr "" +msgid "Exit this page" +msgstr "" + msgid "Sorry, you’re not likely to get legal aid" msgstr "" diff --git a/tests/functional_tests/categories/domestic_abuse/test_domestic_abuse.py b/tests/functional_tests/categories/domestic_abuse/test_domestic_abuse.py index 940e34a16..c60b67a11 100644 --- a/tests/functional_tests/categories/domestic_abuse/test_domestic_abuse.py +++ b/tests/functional_tests/categories/domestic_abuse/test_domestic_abuse.py @@ -62,9 +62,7 @@ def test_onward_routing(self, page: Page, routing: dict): @pytest.mark.parametrize("routing", ROUTING) def test_exit_this_page(self, page: Page, routing: dict): page.get_by_role("link", name="Domestic abuse").click() - expect( - page.get_by_role("button", name="Emergency Exit this page") - ).to_be_visible() + expect(page.get_by_role("button", name="Exit this page")).to_be_visible() page.get_by_role("link", name=routing["link_text"]).click() expect( page.get_by_role("heading", name=routing["next_page_heading"]) @@ -73,6 +71,4 @@ def test_exit_this_page(self, page: Page, routing: dict): page.get_by_role("heading", name=routing["next_page_heading"]) == risk_of_harm_page_heading ): - expect( - page.get_by_role("button", name="Emergency Exit this page") - ).to_be_visible() + expect(page.get_by_role("button", name="Exit this page")).to_be_visible() diff --git a/tests/unit_tests/test_exit_this_page.py b/tests/unit_tests/test_exit_this_page.py new file mode 100644 index 000000000..30b8f3992 --- /dev/null +++ b/tests/unit_tests/test_exit_this_page.py @@ -0,0 +1,26 @@ +import pytest +from flask import session +from app.main import inject_exit_this_page + + +class Category(dict): + def __init__(self, exit_page, code="default_code"): + super().__init__() + self["exit_page"] = exit_page + self["code"] = code + + +@pytest.mark.parametrize( + "session_data, expected_output", + [ + ({}, {"show_exit_this_page": False}), + ({"category": None}, {"show_exit_this_page": False}), + ({"category": Category(True, "domestic_abuse")}, {"show_exit_this_page": True}), + ({"category": Category(False, "housing")}, {"show_exit_this_page": False}), + ], +) +def test_inject_exit_this_page(session_data, expected_output, app): + with app.test_request_context(): + session.clear() + session.update(session_data) + assert inject_exit_this_page() == expected_output From 70a19ab14b4cc6f87bdf7fbbd4a7b67fedb6896b Mon Sep 17 00:00:00 2001 From: Kyle O'Brien <65071578+TawneeOwl@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:57:36 +0000 Subject: [PATCH 4/5] Create Grafana Dashboard (#156) * Initial commit * Update configmap * Update dashboard yaml * Update dashboard format * Remove conditional * Change to shorter yaml * Change namespace to release namespace * Update only on uat] * Update dashboard json with new uuid * Update dashboard to inclue dns usage * Update dashboard json * Update dashboard --- .../files/dashboard.json | 938 ++++++++++++++++++ .../templates/dashboard.yaml | 16 + 2 files changed, 954 insertions(+) create mode 100644 helm_deploy/laa-access-civil-legal-aid/files/dashboard.json create mode 100644 helm_deploy/laa-access-civil-legal-aid/templates/dashboard.yaml diff --git a/helm_deploy/laa-access-civil-legal-aid/files/dashboard.json b/helm_deploy/laa-access-civil-legal-aid/files/dashboard.json new file mode 100644 index 000000000..0b4b519ee --- /dev/null +++ b/helm_deploy/laa-access-civil-legal-aid/files/dashboard.json @@ -0,0 +1,938 @@ + { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "name": "Annotations & Alerts", + "showIn": 0, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 199, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 22, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "normal", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 100 + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "editorMode": "code", + "expr": "sum by (remote_addr) (rate(container_network_transmit_packets_total{namespace=\"laa-access-civil-legal-aid-dnstest\"}[1m]))", + "legendFormat": "Usage of Access", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by (remote_addr) (rate(container_network_transmit_packets_total{namespace=\"laa-cla-public-dnstest\"}[1m]))\n", + "hide": false, + "instant": false, + "legendFormat": "Usage of Check", + "range": true, + "refId": "B" + } + ], + "title": "DNS Site Usage DNStest", + "type": "barchart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 16, + "panels": [], + "title": "Access Civil Legal Aid", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 0, + "y": 9 + }, + "id": 15, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum(kube_pod_container_info{namespace='$namespace'})", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Total", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum({namespace='$namespace' ,phase='Running'} + 0)", + "hide": false, + "instant": false, + "legendFormat": "Running", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum({namespace='$namespace',phase='Pending'} + 0)", + "hide": false, + "instant": false, + "legendFormat": "Pending", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum({namespace='$namespace',phase='Failed'} + 0)", + "hide": false, + "instant": false, + "legendFormat": "Failed", + "range": true, + "refId": "D" + } + ], + "title": "Pod Info", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number of HTTP requests by status", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Number of requests", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 15, + "x": 9, + "y": 9 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "(sum(rate(nginx_ingress_controller_requests{exported_namespace=\"$namespace\", path!~\"/socket.io/.*\", ingress=\"$ingress\", status!=\"200\"}[5m])) by (status)) * 100\n", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "HTTP Request Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "editorMode": "code", + "expr": "kube_pod_container_status_restarts_total{namespace=\"$namespace\"}", + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Container Restarts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "sum(rate(nginx_ingress_controller_request_duration_seconds_count{exported_namespace = \"$namespace\", path !~\"/socket.io/.*\", ingress = \"$ingress\"}[5m]))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "req / m", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Ingress Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(pod_name)(container_memory_usage_bytes{namespace='$namespace'})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "POD: {{ pod_name}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "avg(kube_pod_container_resource_requests_memory_bytes{namespace='$namespace'})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Requested (soft limit)", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "avg(kube_pod_container_resource_limits_memory_bytes{namespace='$namespace'})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Limit (hard limit)", + "refId": "B" + } + ], + "title": "Memory usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum by (pod_name)(rate(container_cpu_usage_seconds_total{namespace='$namespace'}[5m]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "POD: {{ pod_name}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "avg(kube_pod_container_resource_requests_cpu_cores{namespace='$namespace'})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Requested (soft limit)", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "avg(kube_pod_container_resource_limits_cpu_cores{namespace='$namespace'})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Limit (hard limit)", + "refId": "C" + } + ], + "title": "CPU usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sort_desc(avg(sum by (pod_name) (rate(container_network_receive_bytes_total{namespace='$namespace'}[5m]))))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Recv", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sort_desc(avg(sum by (pod_name) (rate(container_network_transmit_bytes_total{namespace='$namespace'}[5m]))))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Sent", + "refId": "B" + } + ], + "title": "Network", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "laa-access-civil-legal-aid-dnstest", + "value": "laa-access-civil-legal-aid-dnstest" + }, + "datasource": "Prometheus", + "definition": "label_values(kube_deployment_metadata_generation, namespace)", + "includeAll": false, + "label": "Namespace", + "name": "namespace", + "options": [], + "query": "label_values(kube_deployment_metadata_generation, namespace)", + "refresh": 1, + "regex": "/^laa-access-civil-legal-aid-/", + "type": "query" + }, + { + "current": { + "text": "", + "value": "" + }, + "definition": "label_values(nginx_ingress_controller_requests{exported_namespace=\"$namespace\"},ingress)", + "name": "ingress", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(nginx_ingress_controller_requests{exported_namespace=\"$namespace\"},ingress)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "LAA Access Civil Legal Aid", + "uid": "d2w1ev32edfgwc", + "version": 1, + "weekStart": "" + } \ No newline at end of file diff --git a/helm_deploy/laa-access-civil-legal-aid/templates/dashboard.yaml b/helm_deploy/laa-access-civil-legal-aid/templates/dashboard.yaml new file mode 100644 index 000000000..4e59ccc4d --- /dev/null +++ b/helm_deploy/laa-access-civil-legal-aid/templates/dashboard.yaml @@ -0,0 +1,16 @@ +{{- if eq .Values.environment "uat" -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: laa-access-civil-legal-aid-dashboard + namespace: {{ .Release.Namespace }} + labels: + grafana_dashboard: "" + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + annotations: + meta.helm.sh/release-name: {{ .Release.Name | quote }} + meta.helm.sh/release-namespace: {{ .Release.Namespace | quote }} +data: + laa-access-civil-legal-aid-dashboard.json: | +{{ .Files.Get "files/dashboard.json" | indent 4 }} +{{- end }} From 23f86cd6f085a397ee5c464a94a230a968a63c57 Mon Sep 17 00:00:00 2001 From: Ben Millar Date: Mon, 10 Mar 2025 22:20:33 +0000 Subject: [PATCH 5/5] LGA-3556: Stay safe when you use this website (#192) * Add online safety page * Add Welsh translation to link footer * Add unit tests --- app/main/routes.py | 7 +- app/templates/base.html | 4 + app/templates/main/online-safety.html | 70 ++++++++++ app/translations/cy/LC_MESSAGES/messages.mo | Bin 76114 -> 82962 bytes app/translations/cy/LC_MESSAGES/messages.po | 131 +++++++++++++++++++ app/translations/en/LC_MESSAGES/messages.pot | 128 ++++++++++++++++++ tests/unit_tests/test_pages.py | 16 +++ 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 app/templates/main/online-safety.html diff --git a/app/main/routes.py b/app/main/routes.py index d57ad2f68..ab25a9f53 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -153,7 +153,12 @@ def cookies(): @bp.route("/privacy", methods=["GET"]) def privacy(): - return render_template("privacy.html") + return render_template("main/privacy.html") + + +@bp.route("/online-safety") +def online_safety(): + return render_template("main/online-safety.html") @bp.route("/session-expired", methods=["GET"]) diff --git a/app/templates/base.html b/app/templates/base.html index ec353198e..88e966466 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -162,6 +162,10 @@ 'href': language.switch.href, 'text': language.switch.text, 'attributes': {'x-data': 'language-switcher'}, + }, + { + 'href': url_for('main.online_safety'), + 'text': _("Staying safe online") }, { 'href': url_for('main.accessibility'), diff --git a/app/templates/main/online-safety.html b/app/templates/main/online-safety.html new file mode 100644 index 000000000..5168782f7 --- /dev/null +++ b/app/templates/main/online-safety.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{%- from 'components/back_link.html' import govukBackLink -%} + +{% block page_title %}{{ _('Staying safe online') }} - {{ super() }}{% endblock %} + +{% block beforeContent %} + {{ super() }} + {{ govukBackLink({ 'text': _('Back') }) }} +{% endblock %} + +{% block content %} +
+
+

{{ _('Stay safe when you use this website') }}

+ +

{{ _('If you need to hide what you’re doing on this website, or protect yourself from someone, it can be safer not to use your home computer or your own mobile phone.') }}

+

{{ _('For example, use a computer at the library, or borrow a phone from someone you trust.') }}

+ + +

{{ _('What this website saves to your device') }}

+

{{ _('When you use this website, and every time you use the internet, your device saves a record of:') }}

+
    +
  • {{ _('the pages you’ve looked at and the files you’ve downloaded - this is called your ‘browsing history’') }}
  • +
  • {{ _('small files called ‘cookies’') }}
  • +
+ +

{{ _('How to delete cookies and your browsing history') }}

+

{{ _('Be careful because:') }}

+
    +
  • {{ _('deleting cookies will also delete stored passwords for online accounts') }}
  • +
  • {{ _('clearing your history could make someone more suspicious') }}
  • +
+

{{ _('Try to only remove information about the websites you want to keep private.') }}

+

{{ _('Find out how to do that on:') }}

+ + + +

{{ _('How the ‘Exit this page’ button works on this website') }}

+

+ {{ _('There is an ‘Exit this page’ button on some pages. If you click this button, all the information you’ve entered will be deleted. This website will close.')}} +

+

+ {{ _('You’ll automatically go to the BBC Weather homepage:') }} {{ _('https://www.bbc.co.uk/weather/') }} +

+ +

{{ _('Cookies and browsing history will still be saved to your device.') }}

+

{{ _('On any page where you see the ‘Exit this page’ button, pressing the ‘Shift’ key 3 times will work the same way.') }}

+

{{ _('To find this website again, search ‘Check if you can get legal aid’.') }}

+ +

{{ _('How to use this website without storing cookies and browser history') }}

+

{{ _('Use the ‘private browsing’ settings. Go to the menu on your browser, click on ‘File’ and turn on:') }}

+
    +
  • {{ _('Incognito on Google Chrome') }}
  • +
  • {{ _('Private Window on Safari') }}
  • +
  • {{ _('InPrivate on Internet Explorer') }}
  • +
  • {{ _('Private Browsing on Mozilla Firefox') }}
  • +
+

{{ _('On Samsung internet, you need to turn on') }} {{ _('Secret mode.') }}

+ +

{{ _('Get help to stay safe online') }}

+

{{ _('There are other ways someone can track you online, for example by using spying or tracking programmes. See the Refuge website') }} {{ _('Secure your tech.') }}

+
+
+{% endblock %} diff --git a/app/translations/cy/LC_MESSAGES/messages.mo b/app/translations/cy/LC_MESSAGES/messages.mo index 1edaf8c71dc8525e3bbc88d89139da9026ffa084..95773f26352f788257d32cbd023bf043988f2127 100644 GIT binary patch delta 16383 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 MOz-K}-^GzX}y{` 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 K`Q?swg8hF(YG6bF diff --git a/app/translations/cy/LC_MESSAGES/messages.po b/app/translations/cy/LC_MESSAGES/messages.po index e94a3240e..61fe63455 100644 --- a/app/translations/cy/LC_MESSAGES/messages.po +++ b/app/translations/cy/LC_MESSAGES/messages.po @@ -1716,6 +1716,9 @@ msgstr "Rhowch 0 os nad oes gennych eitemau gwerthfawr sy’n werth dros £500 y msgid "Exit this page" msgstr "Gadael y dudalen hon" +msgid "Staying safe online" +msgstr "Cadw’n ddiogel ar-lein" + msgid "Sorry, you’re not likely to get legal aid" msgstr "" @@ -2452,6 +2455,134 @@ msgstr "Ymestyn y terfyn amser" msgid "Exit service" msgstr "Gadael y gwasanaeth" +msgid "Stay safe when you use this website" +msgstr "Cadwch yn ddiogel wrth ddefnyddio’r wefan hon" + +msgid "" +"If you need to hide what you’re doing on this website, or protect " +"yourself from someone, it can be safer not to use your home computer or " +"your own mobile phone." +msgstr "Os oes angen i chi guddio’r hyn rydych chi’n ei wneud ar y wefan hon, neu ddiogelu eich hun rhag rhywun, gall fod yn fwy diogel peidio â defnyddio eich cyfrifiadur gartref na’ch ffôn symudol eich hun." + +msgid "" +"For example, use a computer at the library, or borrow a phone from " +"someone you trust." +msgstr "Er enghraifft, defnyddiwch gyfrifiadur yn y llyfrgell, neu fenthyg ffôn gan rywun rydych chi’n ymddiried ynddo." + +msgid "What this website saves to your device" +msgstr "Beth mae'r wefan hon yn ei gadw ar eich dyfais" + +msgid "" +"When you use this website, and every time you use the internet, your " +"device saves a record of:" +msgstr "Pan fyddwch chi’n defnyddio’r wefan hon, a phob tro y byddwch chi’n defnyddio’r rhyngrwyd, bydd eich dyfais yn cadw cofnod o’r canlynol:" + +msgid "" +"the pages you’ve looked at and the files you’ve downloaded - this is " +"called your ‘browsing history’" +msgstr "y tudalennau rydych chi wedi edrych arnyn nhw a'r ffeiliau rydych chi wedi'u llwytho i lawr – gelwir hyn yn 'hanes pori'" + +msgid "small files called ‘cookies’" +msgstr "ffeiliau bach o’r enw ‘cwcis’" + +msgid "How to delete cookies and your browsing history" +msgstr "Sut mae dileu cwcis a'ch hanes pori" + +msgid "Be careful because:" +msgstr "Byddwch yn ofalus oherwydd:" + +msgid "deleting cookies will also delete stored passwords for online accounts" +msgstr "bydd dileu cwcis hefyd yn dileu cyfrineiriau sydd wedi’u storio ar gyfer cyfrifon ar-lein" + +msgid "clearing your history could make someone more suspicious" +msgstr "gallai clirio eich hanes wneud rhywun yn fwy amheus" + +msgid "" +"Try to only remove information about the websites you want to keep " +"private." +msgstr "Ceisiwch ddileu’r wybodaeth am y gwefannau rydych chi am eu cadw'n breifat yn unig." + +msgid "Find out how to do that on:" +msgstr "Dysgwch sut mae gwneud hynny ar:" + +msgid "Google Chrome" +msgstr "" + +msgid "Internet Explorer" +msgstr "" + +msgid "Mozilla Firefox" +msgstr "" + +msgid "Safari" +msgstr "" + +msgid "Samsung internet" +msgstr "" + +msgid "How the ‘Exit this page’ button works on this website" +msgstr "Sut mae’r botwm ‘Gadael y dudalen hon’ yn gweithio ar y wefan hon" + +msgid "" +"There is an ‘Exit this page’ button on some pages. If you click this " +"button, all the information you’ve entered will be deleted. This website " +"will close." +msgstr "Mae botwm ‘Gadael y dudalen hon’ ar rai tudalennau. Os byddwch chi’n clicio’r botwm hwn, bydd yr holl wybodaeth rydych chi wedi’i rhoi yn cael ei dileu. Bydd y wefan hon yn cau." + +msgid "You’ll automatically go to the BBC Weather homepage:" +msgstr "Byddwch yn mynd yn awtomatig i dudalen hafan BBC Weather:" + +msgid "https://www.bbc.co.uk/weather/" +msgstr "" + +msgid "Cookies and browsing history will still be saved to your device." +msgstr "Bydd y cwcis a’r hanes pori yn dal i gael eu cadw ar eich dyfais." + +msgid "" +"On any page where you see the ‘Exit this page’ button, pressing the " +"‘Shift’ key 3 times will work the same way." +msgstr "Ar unrhyw dudalen lle gwelwch y botwm ‘Gadael y dudalen hon’, bydd pwyso’r fysell ‘Shift’ 3 gwaith yn gwneud yr un peth." + +msgid "To find this website again, search ‘Check if you can get legal aid’." +msgstr "I ddod o hyd i’r wefan hon eto, chwiliwch am ‘Gwirio a ydych yn gymwys i gael cymorth cyfreithiol’." + +msgid "How to use this website without storing cookies and browser history" +msgstr "Sut mae defnyddio'r wefan hon heb storio cwcis a hanes pori" + +msgid "" +"Use the ‘private browsing’ settings. Go to the menu on your browser, " +"click on ‘File’ and turn on:" +msgstr "Defnyddiwch y gosodiadau ‘pori’n breifat’. Ewch i’r ddewislen ar eich porwr, cliciwch ‘Ffeil’ a rhoi’r canlynol ar waith:" + +msgid "Incognito on Google Chrome" +msgstr "Incognito ar Google Chrome" + +msgid "Private Window on Safari" +msgstr "Private Window ar Safari" + +msgid "InPrivate on Internet Explorer" +msgstr "InPrivate ar Internet Explorer" + +msgid "Private Browsing on Mozilla Firefox" +msgstr "Private Browsing ar Mozilla Firefox" + +msgid "On Samsung internet, you need to turn on" +msgstr "Ar Samsung Internet, mae angen i chi roi" + +msgid "Secret mode." +msgstr "Secret mode ar waith." + +msgid "Get help to stay safe online" +msgstr "Cael help i gadw’n ddiogel ar-lein" + +msgid "" +"There are other ways someone can track you online, for example by using " +"spying or tracking programmes. See the Refuge website" +msgstr "Mae ffyrdd eraill y gall rhywun eich tracio ar-lein, er enghraifft drwy ddefnyddio rhaglenni tracio neu ysbïo. Ewch i’r dudalen" + +msgid "Secure your tech." +msgstr "Secure your tech ar wefan Refuge." + msgid "Privacy notice" msgstr "Welsh Privacy notice" diff --git a/app/translations/en/LC_MESSAGES/messages.pot b/app/translations/en/LC_MESSAGES/messages.pot index bc4bc046b..3f0d8af67 100644 --- a/app/translations/en/LC_MESSAGES/messages.pot +++ b/app/translations/en/LC_MESSAGES/messages.pot @@ -2153,6 +2153,134 @@ msgstr "" msgid "Exit service" msgstr "" +msgid "Stay safe when you use this website" +msgstr "" + +msgid "" +"If you need to hide what you’re doing on this website, or protect " +"yourself from someone, it can be safer not to use your home computer or " +"your own mobile phone." +msgstr "" + +msgid "" +"For example, use a computer at the library, or borrow a phone from " +"someone you trust." +msgstr "" + +msgid "What this website saves to your device" +msgstr "" + +msgid "" +"When you use this website, and every time you use the internet, your " +"device saves a record of:" +msgstr "" + +msgid "" +"the pages you’ve looked at and the files you’ve downloaded - this is " +"called your ‘browsing history’" +msgstr "" + +msgid "small files called ‘cookies’" +msgstr "" + +msgid "How to delete cookies and your browsing history" +msgstr "" + +msgid "Be careful because:" +msgstr "" + +msgid "deleting cookies will also delete stored passwords for online accounts" +msgstr "" + +msgid "clearing your history could make someone more suspicious" +msgstr "" + +msgid "" +"Try to only remove information about the websites you want to keep " +"private." +msgstr "" + +msgid "Find out how to do that on:" +msgstr "" + +msgid "Google Chrome" +msgstr "" + +msgid "Internet Explorer" +msgstr "" + +msgid "Mozilla Firefox" +msgstr "" + +msgid "Safari" +msgstr "" + +msgid "Samsung internet" +msgstr "" + +msgid "How the ‘Exit this page’ button works on this website" +msgstr "" + +msgid "" +"There is an ‘Exit this page’ button on some pages. If you click this " +"button, all the information you’ve entered will be deleted. This website " +"will close." +msgstr "" + +msgid "You’ll automatically go to the BBC Weather homepage:" +msgstr "" + +msgid "https://www.bbc.co.uk/weather/" +msgstr "" + +msgid "Cookies and browsing history will still be saved to your device." +msgstr "" + +msgid "" +"On any page where you see the ‘Exit this page’ button, pressing the " +"‘Shift’ key 3 times will work the same way." +msgstr "" + +msgid "To find this website again, search ‘Check if you can get legal aid’." +msgstr "" + +msgid "How to use this website without storing cookies and browser history" +msgstr "" + +msgid "" +"Use the ‘private browsing’ settings. Go to the menu on your browser, " +"click on ‘File’ and turn on:" +msgstr "" + +msgid "Incognito on Google Chrome" +msgstr "" + +msgid "Private Window on Safari" +msgstr "" + +msgid "InPrivate on Internet Explorer" +msgstr "" + +msgid "Private Browsing on Mozilla Firefox" +msgstr "" + +msgid "On Samsung internet, you need to turn on" +msgstr "" + +msgid "Secret mode." +msgstr "" + +msgid "Get help to stay safe online" +msgstr "" + +msgid "" +"There are other ways someone can track you online, for example by using " +"spying or tracking programmes. See the Refuge website" +msgstr "" + +msgid "Secure your tech." +msgstr "" + msgid "Privacy notice" msgstr "" diff --git a/tests/unit_tests/test_pages.py b/tests/unit_tests/test_pages.py index db122f8c9..09b23ba27 100644 --- a/tests/unit_tests/test_pages.py +++ b/tests/unit_tests/test_pages.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest @@ -54,3 +56,17 @@ def test_header_link_clears_session(app, client): with client.session_transaction() as session: assert "test" not in session + + +@patch("app.main.routes.render_template") +def test_privacy_template(mock_render_template, client): + response = client.get("/privacy") + assert response.status_code == 200 + mock_render_template.assert_called_once_with("main/privacy.html") + + +@patch("app.main.routes.render_template") +def test_online_safety_template(mock_render_template, client): + response = client.get("/online-safety") + assert response.status_code == 200 + mock_render_template.assert_called_once_with("main/online-safety.html")