diff --git a/src/controllers/lpaDetailsController.js b/src/controllers/lpaDetailsController.js new file mode 100644 index 00000000..6301564a --- /dev/null +++ b/src/controllers/lpaDetailsController.js @@ -0,0 +1,19 @@ +import PageController from './pageController.js' +import { fetchLocalAuthorities } from '../utils/fetchLocalAuthorities.js' + +class LpaDetailsController extends PageController { + async locals (req, res, next) { + const localAuthoritiesNames = await fetchLocalAuthorities() + + const listItems = localAuthoritiesNames.map(name => ({ + text: name, + value: name + })) + + req.form.options.localAuthorities = listItems + + super.locals(req, res, next) + } +} + +export default LpaDetailsController diff --git a/src/routes/form-wizard/endpoint-submission-form/steps.js b/src/routes/form-wizard/endpoint-submission-form/steps.js index 74639ed8..1462cd97 100644 --- a/src/routes/form-wizard/endpoint-submission-form/steps.js +++ b/src/routes/form-wizard/endpoint-submission-form/steps.js @@ -1,5 +1,5 @@ -// ToDo: Split this into two form wizards import chooseDatasetController from '../../../controllers/chooseDatasetController.js' +import LpaDetailsController from '../../../controllers/lpaDetailsController.js' import PageController from '../../../controllers/pageController.js' import CheckAnswersController from '../../../controllers/CheckAnswersController.js' @@ -20,6 +20,7 @@ export default { ...defaultParams, fields: ['lpa', 'name', 'email'], next: 'choose-dataset', + controller: LpaDetailsController, backLink: '/start' }, '/choose-dataset': { diff --git a/src/serverSetup/middlewares.js b/src/serverSetup/middlewares.js index 408b35ef..f2a617a1 100644 --- a/src/serverSetup/middlewares.js +++ b/src/serverSetup/middlewares.js @@ -18,6 +18,7 @@ export function setupMiddlewares (app) { }) app.use('/assets', express.static('./node_modules/govuk-frontend/dist/govuk/assets')) + app.use('/assets', express.static('./node_modules/@x-govuk/govuk-prototype-components/x-govuk')) app.use('/public', express.static('./public')) app.use(cookieParser()) diff --git a/src/utils/fetchLocalAuthorities.js b/src/utils/fetchLocalAuthorities.js new file mode 100644 index 00000000..5fb29790 --- /dev/null +++ b/src/utils/fetchLocalAuthorities.js @@ -0,0 +1,42 @@ +import axios from 'axios' + +/** + * Fetches a list of local authority names from a specified dataset. + * + * This function queries a dataset for local authorities, extracting a distinct list of names. + * It performs an HTTP GET request to retrieve the data, then processes the response to return + * only the names of the local authorities. + * + * @returns {Promise} A promise that resolves to an array of local authority names. + * @throws {Error} Throws an error if the HTTP request fails or data processing encounters an issue. + */ +export const fetchLocalAuthorities = async () => { + const sql = `select + distinct provision.organisation, + organisation.name, + organisation.dataset + from + provision, + organisation + where + provision.organisation = organisation.organisation + order by + provision.organisation` + + const url = `https://datasette.planning.data.gov.uk/digital-land.json?sql=${encodeURIComponent(sql)}` + try { + const response = await axios.get(url) + const names = response.data.rows.map(row => { + if (row[1] === null) { + console.log('Null value found in response:', row) + return null + } else { + return row[1] + } + }).filter(name => name !== null) // Filter out null values + return names // Return the fetched data + } catch (error) { + console.error('Error fetching local authorities data:', error) + throw error // Rethrow the error to be handled by the caller + } +} diff --git a/src/views/layouts/main.html b/src/views/layouts/main.html index 697a331e..291d8488 100644 --- a/src/views/layouts/main.html +++ b/src/views/layouts/main.html @@ -73,6 +73,13 @@

Get help

{% block bodyEnd %} {{ super()}} {%block scripts %} + + ... + + + {{ super() }} {% endblock %} diff --git a/src/views/submit/lpa-details.html b/src/views/submit/lpa-details.html index 9d8585b7..82c61718 100644 --- a/src/views/submit/lpa-details.html +++ b/src/views/submit/lpa-details.html @@ -1,7 +1,7 @@ {% extends "layouts/main.html" %} - {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/button/macro.njk" import govukButton %} +{% from "x-govuk/components/autocomplete/macro.njk" import xGovukAutocomplete %} {% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} {% set serviceType = 'Submit' %} @@ -40,22 +40,21 @@

- {{ - govukInput({ - label: { - text: "Local planning authority", - classes: "govuk-label--m", - isPageHeading: false - }, - id: "lpa", - name: "lpa", - classes: "govuk-!-width-three-quarters", - errorMessage: { - text: 'lpa' | validationMessageLookup(errors['lpa'].type) - } if 'lpa' in errors, - value: values.lpa - }) - }} + {{ xGovukAutocomplete({ + id: "lpa", + name: "lpa", + allowEmpty: false, + label: { + classes: "govuk-label--m", + isPageHeading: false, + text: "Choose your local planning authority" + }, + items: options.localAuthorities, + errorMessage: { + text: 'lpa' | validationMessageLookup(errors['lpa'].type) + } if 'lpa' in errors, + value: values.lpa + }) }} {{ govukInput({ label: { diff --git a/test/unit/fetchLocalAuthorities.test.js b/test/unit/fetchLocalAuthorities.test.js new file mode 100644 index 00000000..9b3c73f2 --- /dev/null +++ b/test/unit/fetchLocalAuthorities.test.js @@ -0,0 +1,41 @@ +import axios from 'axios' +import { vi, it, describe, expect } from 'vitest' +import { fetchLocalAuthorities } from '../../src/utils/fetchLocalAuthorities' + +// Mock axios.get to return a fake response +vi.mock('axios') +axios.get.mockResolvedValue({ + data: { + rows: [ + [1, 'Local Authority 1'], + [2, 'Local Authority 2'], + [3, 'Local Authority 3'] + ] + } +}) + +describe('fetchLocalAuthorities', () => { + it('should fetch local authority names', async () => { + const result = await fetchLocalAuthorities() + expect(result).toEqual(['Local Authority 1', 'Local Authority 2', 'Local Authority 3']) + }) + + it('should throw an error if the HTTP request fails', async () => { + axios.get.mockRejectedValue(new Error('Failed to fetch data')) + await expect(fetchLocalAuthorities()).rejects.toThrow('Failed to fetch data') + }) + + it('should throw an error if data processing encounters an issue', async () => { + axios.get.mockResolvedValue({ + data: { + rows: [ + [1, 'Local Authority 1'], + [2, null], // Simulate null value in the response + [3, 'Local Authority 3'] + ] + } + }) + const result = await fetchLocalAuthorities() + expect(result).toEqual(['Local Authority 1', 'Local Authority 3']) + }) +}) diff --git a/test/unit/lpaDetailsController.test.js b/test/unit/lpaDetailsController.test.js new file mode 100644 index 00000000..21355a83 --- /dev/null +++ b/test/unit/lpaDetailsController.test.js @@ -0,0 +1,66 @@ +/* eslint-disable no-import-assign */ +/* eslint-disable new-cap */ + +import PageController from '../../src/controllers/pageController.js' +import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' + +vi.mock('../../src/utils/fetchLocalAuthorities.js') + +describe('lpaDetailsController', async () => { + let fetchLocalAuthorities + let controller + + beforeEach(async () => { + fetchLocalAuthorities = await import('../../src/utils/fetchLocalAuthorities') + const LpaDetailsController = await import('../../src/controllers/lpaDetailsController.js') + controller = new LpaDetailsController.default({ + route: '/lpa-details' + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('locals', () => { + it('should set localAuthorities options in the form', async () => { + const req = { + form: { + options: {} + } + } + const res = {} + const next = vi.fn() + + const localAuthoritiesNames = ['Authority 1', 'Authority 2'] + + fetchLocalAuthorities.fetchLocalAuthorities = vi.fn().mockResolvedValue(localAuthoritiesNames) + + await controller.locals(req, res, next) + + expect(fetchLocalAuthorities.fetchLocalAuthorities).toHaveBeenCalled() + expect(req.form.options.localAuthorities).toEqual([ + { text: 'Authority 1', value: 'Authority 1' }, + { text: 'Authority 2', value: 'Authority 2' } + ]) + expect(next).toHaveBeenCalled() + }) + + it('should call super.locals', async () => { + const req = { + form: { + options: {} + } + } + const res = {} + const next = vi.fn() + + fetchLocalAuthorities.fetchLocalAuthorities = vi.fn().mockResolvedValue([]) + const superLocalsSpy = vi.spyOn(PageController.prototype, 'locals') + + await controller.locals(req, res, next) + + expect(superLocalsSpy).toHaveBeenCalledWith(req, res, next) + }) + }) +})