diff --git a/.gitignore b/.gitignore index 98ab9040..31c6ac66 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ fala/static .python-version # VSCode -.vscode \ No newline at end of file +.vscode +fala/apps/adviser/fala.code-workspace \ No newline at end of file diff --git a/fala/apps/adviser/forms.py b/fala/apps/adviser/forms.py index 3dcf7b12..22f10a0f 100644 --- a/fala/apps/adviser/forms.py +++ b/fala/apps/adviser/forms.py @@ -4,7 +4,6 @@ from django.utils.translation import gettext_lazy as _ from django.utils.safestring import mark_safe from django.conf import settings - import laalaa.api as laalaa import requests from requests.adapters import HTTPAdapter @@ -193,3 +192,139 @@ def search(self): return {} else: return {} + + +class SingleCategorySearchForm(forms.Form): + postcode = CapitalisedPostcodeField( + label=_("Postcode"), + max_length=30, + required=True, # Postcode is required in this form + widget=FalaTextInput(attrs={"class": "govuk-input govuk-!-width-two-thirds"}), + ) + + def __init__(self, categories=None, *args, **kwargs): + super().__init__(*args, **kwargs) + # Ensure categories is a list and assign it to self.categories + self.categories = categories if categories is not None else [] + self._region = None + self._country_from_valid_postcode = None + + def clean_postcode(self): + postcode = self.cleaned_data.get("postcode") + if not postcode: + raise forms.ValidationError(_("Enter a valid postcode")) + return postcode + + def clean(self): + cleaned_data = super().clean() + postcode = cleaned_data.get("postcode") + categories = self.data.get("category") + + # Validate postcode and set `_country_from_valid_postcode` + if postcode: + valid_postcode = self.validate_postcode_and_return_country(postcode) + if not valid_postcode: + self.add_error("postcode", _("Enter a valid postcode")) + else: + self._country_from_valid_postcode = valid_postcode # This is used by the `region` property + + # Check if categories are provided + if not categories: + self.add_error("categories", _("Category is required.")) + else: + self.categories = [categories] + + return cleaned_data + + @property + def region(self): + # retrieve the api call variables + country_from_valid_postcode = getattr(self, "_country_from_valid_postcode", None) + + # Return `Region.ENGLAND_OR_WALES` from `clean` if set + if not country_from_valid_postcode: + region = getattr(self, "_region", None) + return region + + # for Guernsey & Jersey the country comes back as 'Channel Islands', we are using `nhs_ha` key to distinguish between them + country, nhs_ha = country_from_valid_postcode + + if country == "Northern Ireland": + return Region.NI + elif country == "Isle of Man": + return Region.IOM + elif country == "Channel Islands" and nhs_ha == "Jersey Health Authority": + return Region.JERSEY + elif country == "Channel Islands" and nhs_ha == "Guernsey Health Authority": + return Region.GUERNSEY + elif country == "Scotland": + return Region.SCOTLAND + elif country in ["England", "Wales"]: + return Region.ENGLAND_OR_WALES + else: + self.add_error("postcode", _("This service is only available for England and Wales")) + return None + + @property + def current_page(self): + page = self.cleaned_data.get("page", 1) + return page + + def validate_postcode_and_return_country(self, postcode): + try: + if not isinstance(postcode, str) or not postcode.strip(): + return False + + session = requests.Session() + retry_strategy = Retry(total=5, backoff_factor=0.1) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("https://", adapter) + + url = settings.POSTCODE_IO_URL + f"{postcode}" + response = session.get(url, timeout=5) + + if response.status_code != 200: + return False + + data = response.json() + + if not data.get("result"): + return False + + first_result_in_list = data["result"][0] + country = first_result_in_list.get("country") + nhs_ha = first_result_in_list.get("nhs_ha") + + if country and nhs_ha: + return country, nhs_ha + else: + return False + + except requests.RequestException: + self.add_error("postcode", _("Error looking up legal advisers. Please try again later.")) + return False + + def search(self): + if self.is_valid(): + try: + postcode = self.cleaned_data.get("postcode") + categories = self.categories + + # Call the API + data = laalaa.find(postcode=postcode, categories=categories, page=1) + + # Check for errors in the response + if "error" in data: + self.add_error("postcode", data["error"]) + return [] + + # Extract only the 'results' key + # this may be where the problem is with the display of the results + # i remove everythng but the results but I think the template wants a + # block called 'data' within which exists `results`` so data.results cntains the search results + results = data.get("results", []) + return results + except laalaa.LaaLaaError: + self.add_error("postcode", _("Error looking up legal advisers. Please try again later.")) + return [] + return [] diff --git a/fala/apps/adviser/models.py b/fala/apps/adviser/models.py index 7745b70c..dfabf93c 100644 --- a/fala/apps/adviser/models.py +++ b/fala/apps/adviser/models.py @@ -1,5 +1,10 @@ from django.db import models from django.utils import timezone +from .regions import Region +from .laa_laa_paginator import LaaLaaPaginator +import urllib +from django.conf import settings +import logging class SatisfactionFeedback(models.Model): @@ -10,3 +15,153 @@ class SatisfactionFeedback(models.Model): def __str__(self): return f"Feedback {self.id}" + + +logger = logging.getLogger(__name__) + + +class EnglandOrWalesState(object): + def __init__(self, form): + self._form = form + self._data = form.search() + + @property + def template_name(self): + return "adviser/results.html" + + def get_queryset(self): + if isinstance(self._data, list): # Ensure it is a list before returning + return self._data + else: + logger.error("Unexpected data format: %s", self._data) + return [] # Return an empty list if data is not valid + + def get_context_data(self): + pages = LaaLaaPaginator(self._data["count"], 10, 3, self._form.current_page) + logger.debug("343434Validating pages: %s", pages) + current_page = pages.current_page() + params = { + "postcode": self._form.cleaned_data["postcode"], + "name": self._form.cleaned_data["name"], + } + categories = self._form.cleaned_data["categories"] + + # create list of tuples which can be passed to urlencode for pagination links + category_tuples = [("categories", c) for c in categories] + + def item_for(page_num): + if len(categories) > 0: + page_params = {"page": page_num} + href = ( + "/search?" + + urllib.parse.urlencode({**page_params, **params}) + + "&" + + urllib.parse.urlencode(category_tuples) + ) + else: + page_params = {"page": page_num} + href = "/search?" + urllib.parse.urlencode({**page_params, **params}) + + return {"number": page_num, "current": self._form.current_page == page_num, "href": href} + + pagination = {"items": [item_for(page_num) for page_num in pages.page_range]} + + if current_page.has_previous(): + if len(categories) > 0: + page_params = {"page": current_page.previous_page_number()} + prev_link = ( + "/search?" + + urllib.parse.urlencode({**page_params, **params}) + + "&" + + urllib.parse.urlencode(category_tuples) + ) + else: + page_params = {"page": current_page.previous_page_number()} + prev_link = "/search?" + urllib.parse.urlencode({**page_params, **params}) + pagination["previous"] = {"href": prev_link} + + if current_page.has_next(): + if len(categories) > 0: + page_params = {"page": current_page.next_page_number()} + href = ( + "/search?" + + urllib.parse.urlencode({**page_params, **params}) + + "&" + + urllib.parse.urlencode(category_tuples) + ) + else: + page_params = {"page": current_page.next_page_number()} + href = "/search?" + urllib.parse.urlencode({**page_params, **params}) + pagination["next"] = {"href": href} + + return { + "form": self._form, + "data": self._data, + "params": params, + "FEATURE_FLAG_SURVEY_MONKEY": settings.FEATURE_FLAG_SURVEY_MONKEY, + "pagination": pagination, + } + + +class OtherJurisdictionState(object): + REGION_TO_LINK = { + Region.NI: { + "link": "https://www.nidirect.gov.uk/articles/legal-aid-schemes", + "region": "Northern Ireland", + }, + Region.IOM: { + "link": "https://www.gov.im/categories/benefits-and-financial-support/legal-aid/", + "region": "the Isle of Man", + }, + Region.JERSEY: { + "link": "https://www.legalaid.je/", + "region": "Jersey", + }, + Region.GUERNSEY: { + "link": "https://www.gov.gg/legalaid", + "region": "Guernsey", + }, + } + + def __init__(self, region, postcode): + self._region = region + self._postcode = postcode + + def get_queryset(self): + return [] + + @property + def template_name(self): + return "adviser/other_region.html" + + def get_context_data(self): + region_data = self.REGION_TO_LINK[self._region] + return { + "postcode": self._postcode, + "link": region_data["link"], + "region": region_data["region"], + } + + +class ErrorState(object): + def __init__(self, form): + self._form = form + + @property + def template_name(self): + return "search.html" + + def get_queryset(self): + return [] + + def get_context_data(self): + errorList = [] + for field, error in self._form.errors.items(): + # choose the first field is the error in form-wide + if field == "__all__": + item = {"text": error[0], "href": "#id_postcode"} + else: + item = {"text": error[0], "href": f"#id_{field}"} + errorList.append(item) + + return {"form": self._form, "data": {}, "errorList": errorList} diff --git a/fala/apps/adviser/utils.py b/fala/apps/adviser/utils.py new file mode 100644 index 00000000..3368f382 --- /dev/null +++ b/fala/apps/adviser/utils.py @@ -0,0 +1,46 @@ +# utils.py + +CATEGORY_TRANSLATIONS = { + "aap": "claims-against-public-authorities", + "med": "clinical-negligence", + "com": "community-care", + "crm": "crime", + "deb": "debt", + "disc": "discrimination", + "edu": "education", + "mat": "family", + "fmed": "family-mediation", + "hou": "housing", + "hlpas": "hlpas", + "immas": "immigration-asylum", + "mhe": "mental-health", + "mosl": "modern-slavery", + "pl": "prison-law", + "pub": "public-law", + "wb": "welfare-benefits", +} + +SLUG_TO_CODE = {v: k for k, v in CATEGORY_TRANSLATIONS.items()} + + +def get_category_code_from_slug(slug): + return SLUG_TO_CODE.get(slug) + + +def get_category_display_name(category_code): + return CATEGORY_TRANSLATIONS.get(category_code) + + +CATEGORY_MESSAGES = { + "hlpas": "Tell the adviser your home is at risk and you want advice through the Housing Loss Prevention Advice Service.", + "welfare-benefits": ( + "Legal aid for advice about welfare benefits is only available if you are appealing a decision made by the social security tribunal. " + "This appeal must be in the Upper Tribunal, Court of Appeal or Supreme Court.\n\n" + "For any other benefits issue, ask the legal adviser if you can get free legal advice or if you will have to pay for it." + ), + "clinical-negligence": "Legal aid for advice about clinical negligence is usually only available if you have a child that suffered a brain injury during pregnancy, birth or the first 8 weeks of life.", +} + +CATEGORY_DISPLAY_NAMES = { + "hlpas": "Housing Loss Prevention Advice Service", +} diff --git a/fala/apps/adviser/views.py b/fala/apps/adviser/views.py index f85ac01d..fb627a13 100644 --- a/fala/apps/adviser/views.py +++ b/fala/apps/adviser/views.py @@ -1,16 +1,17 @@ # coding=utf-8 -import urllib - from django.conf import settings from django.urls import resolve, reverse from django.views.generic import TemplateView, ListView from django.http import HttpResponse import os from django.views import View - from .forms import AdviserSearchForm, AdviserRootForm -from .laa_laa_paginator import LaaLaaPaginator from .regions import Region +from django.shortcuts import redirect, render +from .models import EnglandOrWalesState, ErrorState, OtherJurisdictionState +from .forms import SingleCategorySearchForm +from .utils import CATEGORY_MESSAGES, CATEGORY_DISPLAY_NAMES, get_category_display_name, get_category_code_from_slug +import logging class RobotsTxtView(View): @@ -52,6 +53,76 @@ def get_context_data(self, **kwargs): return context +logger = logging.getLogger(__name__) + + +class SingleCategorySearchView(TemplateView): + template_name = "adviser/single_category_search.html" + + def get(self, request, *args, **kwargs): + category_code = request.GET.get("categories") + + if category_code: + category_slug = get_category_display_name(category_code) + if category_slug: + return redirect("single_category_search", category=category_slug) + else: + return redirect("search") + + category_slug = kwargs.get("category") + if not category_slug: + return redirect("search") + + if not category_code or category_code == "None": + category_code = get_category_code_from_slug(category_slug) + + category_message = CATEGORY_MESSAGES.get(category_slug, "") + category_display_name = CATEGORY_DISPLAY_NAMES.get(category_slug, category_slug.replace("-", " ").title()) + + form = SingleCategorySearchForm(categories=category_slug, data=request.GET or None) + + # Determine the state and results + if form.is_valid(): + logger.debug("Form is valid. Determining region.") + region = form.region # Now `region` will be correctly determined + if region in [Region.ENGLAND_OR_WALES, Region.SCOTLAND]: + logger.debug("Region is England or Wales or Scotland.") + state = EnglandOrWalesState(form) + else: + logger.warning("Region is outside of England or Wales. Using OtherJurisdictionState.") + state = OtherJurisdictionState(region, form.cleaned_data["postcode"]) + else: + logger.error("Form is invalid: %s", form.errors) + state = ErrorState(form) + + # Let the state handle the logic for results + results = state.get_queryset() + # template_name = state.template_name + + search_url = reverse("single_category_search", kwargs={"category": category_slug}) + + context = { + "form": form, + "category_slug": category_slug, + "category_code": category_code, + "category_display_name": category_display_name, + "category_message": category_message, + "results": results, + "data": results, + "search_url": search_url, + } + + return render(request, self.template_name, context) + # so to show the search page this must be self.template_name + # but when i want to show results it needs to be template_name + # so that it can take it from EnglandOrWalesState which uses results.html + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["search_url"] = reverse("single_category_search", kwargs={"category": kwargs.get("category_slug")}) + return context + + class AdviserView(CommonContextMixin, TemplateView): template_name = "adviser/search.html" @@ -86,157 +157,18 @@ class CookiesView(CommonContextMixin, TemplateView): template_name = "adviser/cookies.html" -class SearchView(CommonContextMixin, ListView): - class ErrorState(object): - def __init__(self, form): - self._form = form - - @property - def template_name(self): - return "search.html" - - def get_queryset(self): - return [] - - def get_context_data(self): - errorList = [] - for field, error in self._form.errors.items(): - # choose the first field is the error in form-wide - if field == "__all__": - item = {"text": error[0], "href": "#id_postcode"} - else: - item = {"text": error[0], "href": f"#id_{field}"} - errorList.append(item) - - return {"form": self._form, "data": {}, "errorList": errorList} - - class EnglandOrWalesState(object): - def __init__(self, form): - self._form = form - self._data = form.search() - - @property - def template_name(self): - return "results.html" - - def get_queryset(self): - return self._data.get("results", None) - - def get_context_data(self): - pages = LaaLaaPaginator(self._data["count"], 10, 3, self._form.current_page) - current_page = pages.current_page() - params = { - "postcode": self._form.cleaned_data["postcode"], - "name": self._form.cleaned_data["name"], - } - categories = self._form.cleaned_data["categories"] - - # create list of tuples which can be passed to urlencode for pagination links - category_tuples = [("categories", c) for c in categories] - - def item_for(page_num): - if len(categories) > 0: - page_params = {"page": page_num} - href = ( - "/search?" - + urllib.parse.urlencode({**page_params, **params}) - + "&" - + urllib.parse.urlencode(category_tuples) - ) - else: - page_params = {"page": page_num} - href = "/search?" + urllib.parse.urlencode({**page_params, **params}) - - return {"number": page_num, "current": self._form.current_page == page_num, "href": href} - - pagination = {"items": [item_for(page_num) for page_num in pages.page_range]} - - if current_page.has_previous(): - if len(categories) > 0: - page_params = {"page": current_page.previous_page_number()} - prev_link = ( - "/search?" - + urllib.parse.urlencode({**page_params, **params}) - + "&" - + urllib.parse.urlencode(category_tuples) - ) - else: - page_params = {"page": current_page.previous_page_number()} - prev_link = "/search?" + urllib.parse.urlencode({**page_params, **params}) - pagination["previous"] = {"href": prev_link} - - if current_page.has_next(): - if len(categories) > 0: - page_params = {"page": current_page.next_page_number()} - href = ( - "/search?" - + urllib.parse.urlencode({**page_params, **params}) - + "&" - + urllib.parse.urlencode(category_tuples) - ) - else: - page_params = {"page": current_page.next_page_number()} - href = "/search?" + urllib.parse.urlencode({**page_params, **params}) - pagination["next"] = {"href": href} - - return { - "form": self._form, - "data": self._data, - "params": params, - "FEATURE_FLAG_SURVEY_MONKEY": settings.FEATURE_FLAG_SURVEY_MONKEY, - "pagination": pagination, - } - - class OtherJurisdictionState(object): - REGION_TO_LINK = { - Region.NI: { - "link": "https://www.nidirect.gov.uk/articles/legal-aid-schemes", - "region": "Northern Ireland", - }, - Region.IOM: { - "link": "https://www.gov.im/categories/benefits-and-financial-support/legal-aid/", - "region": "the Isle of Man", - }, - Region.JERSEY: { - "link": "https://www.legalaid.je/", - "region": "Jersey", - }, - Region.GUERNSEY: { - "link": "https://www.gov.gg/legalaid", - "region": "Guernsey", - }, - } - - def __init__(self, region, postcode): - self._region = region - self._postcode = postcode - - def get_queryset(self): - return [] - - @property - def template_name(self): - return "other_region.html" - - def get_context_data(self): - region_data = self.REGION_TO_LINK[self._region] - return { - "postcode": self._postcode, - "link": region_data["link"], - "region": region_data["region"], - } - +class SearchView(CommonContextMixin, ListView, EnglandOrWalesState, OtherJurisdictionState): def get(self, request, *args, **kwargs): form = AdviserSearchForm(data=request.GET or None) if form.is_valid(): region = form.region if region == Region.ENGLAND_OR_WALES or region == Region.SCOTLAND: - self.state = self.EnglandOrWalesState(form) + self.state = EnglandOrWalesState(form) else: - self.state = self.OtherJurisdictionState(region, form.cleaned_data["postcode"]) + self.state = OtherJurisdictionState(region, form.cleaned_data["postcode"]) else: - self.state = self.ErrorState(form) + self.state = ErrorState(form) return super().get(self, request, *args, **kwargs) diff --git a/fala/apps/laalaa/api.py b/fala/apps/laalaa/api.py index 2d3b3209..9cee919d 100644 --- a/fala/apps/laalaa/api.py +++ b/fala/apps/laalaa/api.py @@ -2,6 +2,7 @@ from urllib.parse import urlencode from collections import OrderedDict import requests +import logging from django.conf import settings from django.utils.translation import gettext_lazy as _ @@ -13,6 +14,8 @@ except NameError: basestring = str +logger = logging.getLogger(__name__) + def get_categories(): if settings.LAALAA_API_HOST: @@ -66,5 +69,8 @@ def find(postcode=None, categories=None, page=1, organisation_types=None, organi ) data["results"] = list(map(decode_categories, data.get("results", []))) + logger.debug( + f"find: processed results type={type(data['results'])}, processed results={data['results'][:3]}" + ) # Log first 3 processed results return data diff --git a/fala/templates/adviser/single_category_search.html b/fala/templates/adviser/single_category_search.html new file mode 100644 index 00000000..21007589 --- /dev/null +++ b/fala/templates/adviser/single_category_search.html @@ -0,0 +1,126 @@ +{% extends 'adviser/adviser_base.html' %} + +{% block pageTitle %}Find a Legal Aid Adviser or Family Mediator{% endblock %} + +{% import "macros/element.html" as Element %} +{% import "macros/forms.html" as Form %} + +{%- from 'govuk_frontend_jinja/components/error-summary/macro.html' import govukErrorSummary %} +{%- from 'govuk_frontend_jinja/components/checkboxes/macro.html' import govukCheckboxes %} +{%- from "govuk_frontend_jinja/components/input/macro.html" import govukInput %} + +{% block content %} +
+
+ {% if form.errors %} + {{ govukErrorSummary({ + "titleText": "There is a problem", + "errorList": errorList + }) }} + {% endif %} +

Find a legal aid adviser for + {% if category_slug == 'hlpas' %} + {{ category_display_name }} + {% else %} + {{ category_display_name | lower }} + {% endif %} +

+

Search for official legal aid advisers in England and Wales.

+ {% if category_message %} +

{{ category_message |linebreaks }}

+ {% endif %} +
+ + {% block search_form %} +
+ +
+ + +

DEBUG: Category slug: {{ category_slug }}

+

DEBUG: Category code: {{ category_code }}

+ + {% set errorMessage = {'err': ""} %} + + {% if form.errors %} + {% call Element.errorText() %} + {% for k, error in form.errors.items() %} + {% if errorMessage.update({'err': error | striptags}) %}{% endif %} + {% endfor %} + {% endcall %} + {% endif %} + +

Next steps

+
    +
  1. We'll show you a list of legal advisers.
  2. +
  3. When you contact the adviser they'll ask about your problem and finances to work out if you can get legal aid.
  4. +
+ + {% if 'postcode' in form.errors or form.errors['__all__'] %} + {{ govukInput({ + 'label': { + 'text': "What is your postcode?", + 'classes': 'govuk-label--s', + }, + 'value': form.postcode.value(), + 'errorMessage': { 'text': form.errors['postcode'][0] }, + 'hint': { + 'text' : "For example, SW1H 9AJ", + }, + 'attributes': { + 'maxLength': 30, + }, + 'id': 'id_postcode', + 'classes': 'govuk-input--width-10', + 'name': "postcode" + }) }} + {% else %} + {{ govukInput({ + 'label': { + 'text': "What is your postcode?", + 'classes': 'govuk-label--s', + }, + 'value': form.postcode.value(), + 'hint': { + 'text' : "For example, SW1H 9AJ", + }, + 'attributes': { + 'maxLength': 30, + }, + 'id': 'id_postcode', + 'classes': 'govuk-input--width-10', + 'name': "postcode" + }) }} + {% endif %} + + {{ govukButton({ + 'text': "Search", + 'type': "submit", + 'classes': "govuk-!-margin-bottom-2", + 'id': "searchButton", + 'name': "search" + }) }} +
+
+ {% endblock %} + + {% if results %} +

Results

+ +{% else %} +

No results found for the given postcode and category.

+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/fala/urls.py b/fala/urls.py index 33efc983..112229a3 100644 --- a/fala/urls.py +++ b/fala/urls.py @@ -1,7 +1,7 @@ # coding=utf-8 from django.conf import settings - from django.urls import re_path + from adviser.views import ( AccessibilityView, AdviserView, @@ -10,6 +10,7 @@ CookiesView, RobotsTxtView, SecurityTxtView, + SingleCategorySearchView, ) from django.views.static import serve @@ -21,4 +22,10 @@ re_path(r"^search$", SearchView.as_view(), name="search"), re_path(r"^robots\.txt$", RobotsTxtView.as_view(), name="robots_txt"), re_path(r"^security\.txt$", SecurityTxtView.as_view(), name="security_txt"), + re_path( + r"^single-category-search/(?P[\w-]+)$", + SingleCategorySearchView.as_view(), + name="single_category_search", + ), + re_path(r"^single-category-search$", SingleCategorySearchView.as_view(), name="single_category_search_query"), ] + [re_path(r"^static/(?P.*)$", serve, {"document_root": settings.STATIC_ROOT})]