Skip to content

Commit

Permalink
LGA 3467: Contact us page (#119)
Browse files Browse the repository at this point in the history
* Initial commit

* Add in additional fields

* Update page format and add postcode

* Add in postcode box and geocode lookup

* Make trufflehog ignore env files

* Update the street address field logic

* Add in email validation

* add callback field and conditionals

* Add thirdparty options

* Add in another day logic

* Migrate all the js

* Add validation to day time field

* UPdate another call logic to persist

* Add error message to postcode field

* Allow postcode to search on Enter

* Add bsl logic

* Refactor the bsl webcam code

* Add payload constructor

* Add payload

* Initial setup of backend connection

* Post to backend

* Redirect to confirmation page

* Repair RFC

* Refactor the time code

* Add email orchestrator code

* Add exception catching for duplicate case

* Update validators

* UPdate os places key

* Update variables for contact number

* Amend languages config call

* Change Config to current app config

* Update welsh logic

* Add in eligible pagee

* Add default welsh if welsh selected

* Add in eligility notes

* Change how rfc notes is handled

* Update error handling

* Remove today if no slots

* Update logic for handling another day

* Add another day logic

* Add handling for no slots

* Hide call me back if no slots are available

* Add domestic abuse exit this page

* Add domestic abuse exit this page

* Add in progress bar

* Add welsh translations

* Update functional tests

* Fix accessibility tests

* Fix contact tests

* Amend the eligible route

* Fix test cases

* Fix test session

* Replace pytz with zoneinfo

* Remove post from confirmation

* Add in unit tests

* Add additional unit tests

* Update test contact

* Add mocked checker slots

* Add test contact changes

* Update notify unit tests

* Update welsh translations

* Update unit tests to test validators

* Remove API log

* Change to pytest from unit test

* Seperate callback funcs and add tests

* Update naming

* Add key dynamically to address lookup

* Add tests for get all time slots

* Add additional unit tests

* Move domestic abuse to contact us if risk

* Refactor code

* Update and repair tests

* Api test repair

* Add domestic abuse field

* Add test get email

* Update comments

* Update routing

* Update formatting of headings and change time slots

* Update contact tests to use conftest

* Amend CSS, add url to property of address, change to jsonify

* Change time format to ., update domestic abuse family route, hard code email ids

* Remove spellcheck

* Update slots to use params

* Fix adaptation data not retaining information when form is reloaded

* Amend erroneous Welsh translation

* Add missing Welsh translations

* Refactor get callback time method

* Replace all SelectMultipleFields with SelectField

* Refactor ContactUsForm __init__

* Update unit tests

* Update validators and tests to work with SelectFields

* Set LANG_CHOICES to use the lowercase value as the constant

* Update ec notes to include text from "Tell us more about your problem"

* Only validate the bsl_email field is the user has not entered an email in the optional field

* Update bsl_email validator

* Refactor send email

* Run toggleEmailFields on pageload

* Refactor confirmation email sending

* Refactor confirmation email

* Fix test get email

* Update unit tests

* Update confirmation email tests

* Add tests for the ContactUs view

* Add get callback time tests

* Add freezegun as a requirement

* Merge changes

* Merge changes

* Add tests for the ValidateIf validator

* Add tests for the ValidateIf validator

* Add additional ContactUs unit tests

* Add Welsh translation for the contact preferences

* Add in welsh for time slots

* Update translations

* Add clear eligibility to session

* Clear session but persist case reference

* Persist additional data

* Merge the dpa format address

* Revert means test validator

* Remove print statements

* Update formatting

* Update tests and change session clear eligibility

* Update test validators

* Rebase contact.html

* Repair the welsh translation issue

* Minor amendments to translations and naming of css

* Remove duplicate session cookie secure

---------

Co-authored-by: Ben Millar <ben.millar@digital.justice.gov.uk>
  • Loading branch information
TawneeOwl and BenMillar-MOJ authored Mar 6, 2025
1 parent 54019d4 commit 7127d77
Show file tree
Hide file tree
Showing 49 changed files with 3,798 additions and 132 deletions.
1 change: 1 addition & 0 deletions .trufflehog3.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
exclude: # exclude matching issues
- message: Build directories
paths:
- .env
- build/**
- dist/**
- python3.12/**
Expand Down
29 changes: 28 additions & 1 deletion app/api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from urllib.parse import urljoin
import requests
from flask_babel import LazyString
from flask import current_app
from flask import current_app, session
import logging
from datetime import datetime
from app.extensions import cache

logger = logging.getLogger(__name__)


class BackendAPIClient:
CALLBACK_API_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"

@property
def hostname(self):
return current_app.config["CLA_BACKEND_URL"]
Expand Down Expand Up @@ -130,5 +133,29 @@ def post_reasons_for_contacting(self, form=None, payload=None):
payload = form.api_payload() if form else payload
return self.post("checker/api/v1/reasons_for_contacting/", json=payload)

def get_time_slots(self, num_days=8, is_third_party_callback=False):
params = {"third_party_callback": is_third_party_callback, "num_days": num_days}
slots = self.get(
"checker/api/v1/callback_time_slots/",
params=params,
)["slots"]
slots = [
datetime.strptime(slot, self.CALLBACK_API_DATETIME_FORMAT) for slot in slots
]

return slots

def post_case(self, payload=None):
contact_endpoint = "checker/api/v1/case"
payload["eligibility_check"] = session.get("ec_reference")

response = self.post(contact_endpoint, json=payload)
return response

def update_reasons_for_contacting(self, reference, payload=None):
return self.patch(
f"checker/api/v1/reasons_for_contacting/{reference}", json=payload
)


cla_backend = BackendAPIClient()
2 changes: 1 addition & 1 deletion app/categories/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class SafeguardingQuestionForm(QuestionForm):
title = _("Are you worried about someone's safety?")

next_step_mapping = {
"yes": "categories.results.in_scope",
"yes": "contact.contact_us",
"no": "categories.results.in_scope",
}

Expand Down
2 changes: 2 additions & 0 deletions app/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ class Config(object):
CLA_BACKEND_URL = os.environ.get("CLA_BACKEND_URL", "http://localhost:8010")
SESSION_TIMEOUT = timedelta(minutes=30)
SESSION_COOKIE_SECURE = True
OS_PLACES_API_KEY = os.environ.get("OS_PLACES_API_KEY")
EMAIL_ORCHESTRATOR_URL = os.environ.get("EMAIL_ORCHESTRATOR_URL")
88 changes: 88 additions & 0 deletions app/contact/address_finder/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import logging
import re
from flask import current_app

import requests

logger = logging.getLogger(__name__)


class AddressLookup(object):
url = "https://api.os.uk/search/places/v1/postcode"

def __init__(self):
self.key = current_app.config["OS_PLACES_API_KEY"]

def by_postcode(self, postcode):
params = {
"postcode": postcode,
"key": self.key,
"output_srs": "WGS84", # Specifies the coordinate reference system (WGS84 is a global standard)
"dataset": "DPA", # Specifies the dataset to query ("DPA" stands for "Definitive Postcode Address")
}
try:
os_places_response = requests.get(self.url, params=params, timeout=3)
os_places_response.raise_for_status()
except requests.exceptions.ConnectTimeout as e:
logger.error(f"OS Places request timed out: {e}")
except requests.exceptions.RequestException as e:
logger.error(f"OS Places request error: {e}")
else:
try:
return os_places_response.json().get("results", [])
except ValueError as e:
logger.warning(f"OS Places response JSON parse error: {e}")
return []


class FormattedAddressLookup(AddressLookup):
"""
A subclass of AddressLookup that formats the raw address data returned by the OS Places API.
This class transforms the raw address components into a well-structured, readable address string.
"""

def format_address_from_result(self, raw_result):
dpa_result = raw_result.get("DPA")

if dpa_result:
address_format = [
{"fields": ["ORGANISATION_NAME"]},
{"fields": ["SUB_BUILDING_NAME"]},
{"fields": ["BUILDING_NAME"]},
{"fields": ["BUILDING_NUMBER", "THOROUGHFARE_NAME"]},
{"fields": ["DEPENDENT_LOCALITY"]},
{"fields": ["POST_TOWN"]},
{"fields": ["POSTCODE"], "transform": "upper"},
]

formatted_lines = self.format_lines(address_format, dpa_result)
return "\n".join([c for c in formatted_lines if c])
else:
return None

def format_lines(self, address_format, raw_result):
for line_format in address_format:
line_components = []
for field in line_format["fields"]:
line_components.append(raw_result.get(field, ""))
line_string = " ".join(line_components)
transform = line_format.get("transform")
if transform:
transformed_line = getattr(line_string, transform)()
else:
transformed_line = self.special_title_case(line_string)
yield transformed_line.strip()

@staticmethod
def special_title_case(original_string, exceptions=None):
if not exceptions:
exceptions = ["of", "the"]
word_list = re.split(" ", original_string.lower())
final = [word_list[0].capitalize()]
for word in word_list[1:]:
final.append(word if word in exceptions else word.title())
return " ".join(final)

def by_postcode(self, postcode):
os_places_results = super(FormattedAddressLookup, self).by_postcode(postcode)
return [self.format_address_from_result(result) for result in os_places_results]
202 changes: 202 additions & 0 deletions app/contact/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
from flask_babel import lazy_gettext as _

ADAPTATION_LANGUAGES = [
("", ""),
("ASSAMESE", "Assamese"),
("AZERI", "Azeri"),
("AFRIKAANS", "Afrikaans"),
("ALGERIAN", "Algerian"),
("ASHANTI", "Ashanti"),
("AKAN", "Akan"),
("ALBANIAN", "Albanian"),
("AMHARIC", "Amharic"),
("ARMENIAN", "Armenian"),
("ARABIC", "Arabic"),
("ASSYRIAN", "Assyrian"),
("AZERBAIJANI", "Azerbaijani"),
("BADINI", "Badini"),
("BENGALI", "Bengali"),
("BURMESE", "Burmese"),
("BAJUNI", "Bajuni"),
("BELORUSSIAN", "Belorussian"),
("BOSNIAN", "Bosnian"),
("BERBER", "Berber"),
("BASQUE", "Basque"),
("BULGARIAN", "Bulgarian"),
("BRAVA", "Brava"),
("BRAZILIAN", "Brazilian"),
("CANTONESE", "Cantonese"),
("CEBUANO", "Cebuano"),
("CREOLE", "Creole"),
("CHINESE", "Chinese"),
("CHEROKEE", "Cherokee"),
("COLUMBIAN", "Columbian"),
("CAMBODIAN", "Cambodian"),
("CHAOCHOW", "Chaochow"),
("CROATIAN", "Croatian"),
("CATALAN", "Catalan"),
("CZECH", "Czech"),
("DANISH", "Danish"),
("DARI", "Dari"),
("DUTCH", "Dutch"),
("EGYPTIAN", "Egyptian"),
("ENGLISH", "English"),
("ESTONIAN", "Estonian"),
("ERITREAN", "Eritrean"),
("ESPERANTO", "Esperanto"),
("ETHIOPIAN", "Ethiopian"),
("FARSI", "Farsi"),
("FIJIAN", "Fijian"),
("FLEMISH", "Flemish"),
("FANTI", "Fanti"),
("FRENCH", "French"),
("FINNISH", "Finnish"),
("FULLA", "Fulla"),
("GA", "Ga"),
("GERMAN", "German"),
("GURMUKHI", "Gurmukhi"),
("GAELIC", "Gaelic"),
("GORANI", "Gorani"),
("GEORGIAN", "Georgian"),
("GREEK", "Greek"),
("GUJARATI", "Gujarati"),
("HAKKA", "Hakka"),
("HEBREW", "Hebrew"),
("HINDI", "Hindi"),
("HOMA", "Homa"),
("HAUSA", "Hausa"),
("HUNGARIAN", "Hungarian"),
("HUI", "Hui"),
("ICELANDIC", "Icelandic"),
("IGBO", "Igbo"),
("ILOCANO", "Ilocano"),
("INDONESIAN", "Indonesian"),
("IRAQI", "Iraqi"),
("IRANIAN", "Iranian"),
("ITALIAN", "Italian"),
("JAPANESE", "Japanese"),
("KASHMIRI", "Kashmiri"),
("KREO", "Kreo"),
("KIRUNDI", "Kirundi"),
("KURMANJI", "Kurmanji"),
("KANNADA", "Kannada"),
("KOREAN", "Korean"),
("KRIO", "Krio"),
("KOSOVAN", "Kosovan"),
("KURDISH", "Kurdish"),
("KINYARWANDA", "Kinyarwanda"),
("KINYAMIRENGE", "Kinyamirenge"),
("KAZAKH", "Kazakh"),
("LATVIAN", "Latvian"),
("LAOTIAN", "Laotian"),
("LAO", "Lao"),
("LUBWISI", "Lubwisi"),
("LEBANESE", "Lebanese"),
("LINGALA", "Lingala"),
("LUO", "Luo"),
("LUSOGA", "Lusoga"),
("LITHUANIAN", "Lithuanian"),
("LUGANDA", "Luganda"),
("MANDARIN", "Mandarin"),
("MACEDONIAN", "Macedonian"),
("MOLDOVAN", "Moldovan"),
("MIRPURI", "Mirpuri"),
("MANDINKA", "Mandinka"),
("MALAY", "Malay"),
("MONGOLIAN", "Mongolian"),
("MOROCCAN", "Moroccan"),
("MARATHI", "Marathi"),
("MALTESE", "Maltese"),
("MALAYALAM", "Malayalam"),
("NDEBELE", "Ndebele"),
("NEPALESE", "Nepalese"),
("NIGERIAN", "Nigerian"),
("NORWEGIAN", "Norwegian"),
("NYAKUSE", "Nyakuse"),
("OROMO", "Oromo"),
("OTHER", "Other"),
("PAHARI", "Pahari"),
("PERSIAN", "Persian"),
("PORTUGUESE", "Portuguese"),
("PHILIPINO", "Philipino"),
("POLISH", "Polish"),
("POTHWARI", "Pothwari"),
("PUSTHU", "Pusthu"),
("PUNJABI", "Punjabi"),
("ROMANIAN", "Romanian"),
("RUSSIAN", "Russian"),
("SOTHO", "Sotho"),
("SERBO-CROAT", "Serbo-Croat"),
("SWEDISH", "Swedish"),
("SERBIAN", "Serbian"),
("SHONA", "Shona"),
("SINHALESE", "Sinhalese"),
("SIRAIKI", "Siraiki"),
("SLOVAK", "Slovak"),
("SAMOAN", "Samoan"),
("SLOVENIAN", "Slovenian"),
("SOMALI", "Somali"),
("SORANI", "Sorani"),
("SPANISH", "Spanish"),
("SRI LANKAN", "Sri Lankan"),
("SCOTTISH GAELIC", "Scottish Gaelic"),
("SUDANESE", "Sudanese"),
("SWAHILI", "Swahili"),
("SWAHILLI", "Swahilli"),
("SYLHETI", "Sylheti"),
("TAMIL", "Tamil"),
("TIBETAN", "Tibetan"),
("TELEGU", "Telegu"),
("ELAKIL", "Elakil"),
("TAGALOG", "Tagalog"),
("THAI", "Thai"),
("TIGRINIAN", "Tigrinian"),
("TIGRE", "Tigre"),
("TAJIK", "Tajik"),
("TAIWANESE", "Taiwanese"),
("TURKMANISH", "Turkmanish"),
("TSWANA", "Tswana"),
("TURKISH", "Turkish"),
("TWI", "Twi"),
("UGANDAN", "Ugandan"),
("UKRANIAN", "Ukranian"),
("URDU", "Urdu"),
("USSIAN", "Ussian"),
("UZBEK", "Uzbek"),
("VIETNAMESE", "Vietnamese"),
("WELSH", "Welsh"),
("WOLOF", "Wolof"),
("XHOSA", "Xhosa"),
("YUGOSLAVIAN", "Yugoslavian"),
("YIDDISH", "Yiddish"),
("YORUBA", "Yoruba"),
("ZULU", "Zulu"),
]

LANG_CHOICES = [
(lang_code.lower(), lang_name)
for lang_code, lang_name in ADAPTATION_LANGUAGES
if lang_code not in ("ENGLISH", "WELSH")
]

CONTACT_PREFERENCE = [
("call", _("I will call you")),
("callback", _("Call me back")),
("thirdparty", _("Call someone else instead of me")),
]

# If there are no slots available for call me back this will show
NO_SLOT_CONTACT_PREFERENCE = [
("call", _("I will call you")),
("thirdparty", _("Call someone else instead of me")),
]

SELECT_OPTION_DEFAULT = [("", _("-- Please select --"))]
THIRDPARTY_RELATIONSHIP = [
("parent_guardian", _("Parent or guardian")),
("family_friend", _("Family member or friend")),
("professional", _("Professional")),
("legal_advisor", _("Legal adviser")),
("other", _("Other")),
]
THIRDPARTY_RELATIONSHIP_CHOICES = SELECT_OPTION_DEFAULT + THIRDPARTY_RELATIONSHIP
Loading

0 comments on commit 7127d77

Please sign in to comment.