diff --git a/config/default.yaml b/config/default.yaml index 05c6ca63..a216dcac 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -21,7 +21,7 @@ serviceName: 'Submit and update your planning data' # NOTE: the keys in this map are sometimes referred to as "serviceType" in the templates serviceNames: { submit: 'Submit and update your planning data', - check: 'Check planning and housing data for England', + check: 'Submit and update your planning data', manage: 'Submit and update your planning data' } checkService: diff --git a/src/controllers/chooseDatasetController.js b/src/controllers/chooseDatasetController.js index 71480192..1612fb72 100644 --- a/src/controllers/chooseDatasetController.js +++ b/src/controllers/chooseDatasetController.js @@ -1,16 +1,10 @@ import PageController from './pageController.js' -import { dataSubjects } from '../utils/utils.js' +import { dataSubjects, availableDatasets } from '../utils/utils.js' class ChooseDatasetController extends PageController { locals (req, res, next) { - const availableDataSubjects = Object.values(dataSubjects).filter(dataSubject => dataSubject.available) - const dataSets = Object.values(availableDataSubjects).map(dataSubject => dataSubject.dataSets).flat() - const availableDatasets = dataSets.filter(dataSet => dataSet.available) - availableDatasets.sort((a, b) => a.text.localeCompare(b.text)) - - req.form.options.datasetItems = availableDatasets - + req.form.options.datasetItems = availableDatasets(dataSubjects) super.locals(req, res, next) } } diff --git a/src/controllers/datasetController.js b/src/controllers/datasetController.js index 04c25a95..8d5b1b6d 100644 --- a/src/controllers/datasetController.js +++ b/src/controllers/datasetController.js @@ -4,46 +4,41 @@ import PageController from './pageController.js' // ToDo: we shouldn't hardcode these values here, should we get them from the API // maybe take from specification -import { dataSubjects } from '../utils/utils.js' +import { dataSubjects, datasets, availableDatasets } from '../utils/utils.js' + +/** + * @param {Object} req + * @returns {boolean} + */ +export function requiresGeometryTypeToBeSelected (req) { + const dataset = req.body.dataset + const dataSet = datasets.get(dataset) + return dataSet?.requiresGeometryTypeSelection || false +} + +/** + * @param {Object} req - The HTTP request object. + * @returns {boolean} + */ +export function requiresGeometryTypeToBeSelectedViaDeepLink (req) { + const { dataset } = req.query + const dataSet = datasets.get(dataset) + return dataSet?.requiresGeometryTypeSelection || false +} class DatasetController extends PageController { locals (req, res, next) { - const availableDataSubjects = Object.values(dataSubjects).filter(dataSubject => dataSubject.available) - const dataSets = Object.values(availableDataSubjects).map(dataSubject => dataSubject.dataSets).flat() - const availableDatasets = dataSets.filter(dataSet => dataSet.available) - availableDatasets.sort((a, b) => a.text.localeCompare(b.text)) - - req.form.options.datasetItems = availableDatasets - + req.form.options.datasetItems = availableDatasets(dataSubjects) super.locals(req, res, next) } // we shouldn't need this here but as we dont currently set the datasubject, we need to do so here post (req, res, next) { const dataset = req.body.dataset - // set the data-subject based on the dataset selected - let dataSubject = '' - for (const [key, value] of Object.entries(dataSubjects)) { - if (value.dataSets.find(dataSet => dataSet.value === dataset)) { - dataSubject = key - break - } - } + const { dataSubject } = datasets.get(dataset) || { dataSubject: '' } req.body['data-subject'] = dataSubject super.post(req, res, next) } - - requiresGeometryTypeToBeSelected (req) { - const dataset = req.body.dataset - - if (!dataset) { - return false - } - - const dataSubject = Object.values(dataSubjects).find(dataSubject => dataSubject.dataSets.find(dataSet => dataSet.value === dataset)) - const dataSet = dataSubject.dataSets.find(dataSet => dataSet.value === dataset) - return dataSet.requiresGeometryTypeSelection || false - } } export default DatasetController diff --git a/src/controllers/deepLinkController.js b/src/controllers/deepLinkController.js new file mode 100644 index 00000000..cb2bf139 --- /dev/null +++ b/src/controllers/deepLinkController.js @@ -0,0 +1,59 @@ +import PageController from './pageController.js' + +import { datasets } from '../utils/utils.js' +import logger from '../utils/logger.js' +import { types } from '../utils/logging.js' +import * as v from 'valibot' +import { NonEmptyString } from '../routes/schemas.js' + +const QueryParams = v.object({ + dataset: NonEmptyString, + orgName: NonEmptyString +}) + +/** + * Handles deep links in the Check Tool. + * + * It is meant to extract required params from query params + * and partially pre-populate the session with them, + * then redirect the user to the "next" page in the wizard + */ +class DeepLinkController extends PageController { + get (req, res, next) { + // if the query params don't contain what we need, redirect to the "get started" page, + // this way the user can still proceed (but need to fill the dataset+orgName themselves) + const { dataset, orgName } = req.query + const validationResult = v.safeParse(QueryParams, req.query) + if (!(validationResult.success && datasets.has(dataset))) { + logger.info('DeepLinkController.get(): invalid params for deep link, redirecting to start page', + { type: types.App, query: req.query }) + return res.redirect('/check') + } + + req.sessionModel.set('dataset', dataset) + const datasetInfo = datasets.get(dataset) ?? { dataSubject: '', requiresGeometryTypeSelection: false } + req.sessionModel.set('data-subject', datasetInfo.dataSubject) + req.sessionModel.set(this.checkToolDeepLinkSessionKey, + { 'data-subject': datasetInfo.dataSubject, orgName, dataset, datasetName: datasetInfo.text }) + + this.#addHistoryStep(req, '/check/dataset') + + super.post(req, res, next) + } + + #addHistoryStep (req, path, next) { + const newItem = { + path, + wizard: 'check-wizard', + fields: ['dataset', 'data-subject'], + skip: false, + continueOnEdit: false + } + + const history = req.journeyModel.get('history') || [] + history.push(newItem) + req.journeyModel.set('history', history) + } +} + +export default DeepLinkController diff --git a/src/controllers/pageController.js b/src/controllers/pageController.js index ae2a4d17..6f314d4d 100644 --- a/src/controllers/pageController.js +++ b/src/controllers/pageController.js @@ -3,15 +3,22 @@ import { logPageView } from '../utils/logging.js' const { Controller } = hmpoFormWizard class PageController extends Controller { - configure (req, res, callback) { - req.form.options.lastPage = this.options.backLink ? this.options.backLink : undefined - super.configure(req, res, callback) - } + checkToolDeepLinkSessionKey = 'check-tool-deep-link' get (req, res, next) { logPageView(this.options.route, req.sessionID, req.ip) super.get(req, res, next) } + + locals (req, res, next) { + if (this.options.backLink) { + req.form.options.lastPage = this.options.backLink + } + if (req.sessionModel) { + req.form.options.deepLink = req.sessionModel.get(this.checkToolDeepLinkSessionKey) + } + super.locals(req, res, next) + } } export default PageController diff --git a/src/controllers/uploadFileController.js b/src/controllers/uploadFileController.js index 674cbc04..53ba3aa7 100644 --- a/src/controllers/uploadFileController.js +++ b/src/controllers/uploadFileController.js @@ -64,7 +64,7 @@ class UploadFileController extends UploadController { logger.info('UploadFileController: file submitted for processing:', { type: 'fileUploaded', name: req.file.originalname, mimetype: req.file.mimetype, size: req.file.size }) - super.post(req, res, next) + await super.post(req, res, next) } catch (error) { next(error) } diff --git a/src/filters/checkToolDeepLink.js b/src/filters/checkToolDeepLink.js new file mode 100644 index 00000000..df6f6e8e --- /dev/null +++ b/src/filters/checkToolDeepLink.js @@ -0,0 +1,11 @@ +/** + * Returns the deep link to the check tool for a given dataset and organisation + * + * @param {{name:string}} organisation + * @param {{dataset:string, name:string}} dataset + * + * @return {string} + */ +export function checkToolDeepLink (organisation, dataset) { + return `/check/link?dataset=${encodeURIComponent(dataset.dataset)}&orgName=${encodeURIComponent(organisation.name)}` +} diff --git a/src/filters/filters.js b/src/filters/filters.js index e333f633..8e1c33ab 100644 --- a/src/filters/filters.js +++ b/src/filters/filters.js @@ -5,6 +5,7 @@ import toErrorList from './toErrorList.js' import prettifyColumnName from './prettifyColumnName.js' import getFullServiceName from './getFullServiceName.js' import { makeDatasetSlugToReadableNameFilter } from './makeDatasetSlugToReadableNameFilter.js' +import { checkToolDeepLink } from './checkToolDeepLink.js' import pluralize from 'pluralize' /** maps dataset status (as returned by `fetchLpaOverview` middleware to a @@ -40,6 +41,7 @@ const addFilters = (nunjucksEnv, { datasetNameMapping }) => { nunjucksEnv.addFilter('getFullServiceName', getFullServiceName) nunjucksEnv.addFilter('statusToTagClass', statusToTagClass) nunjucksEnv.addFilter('pluralise', pluralize) + nunjucksEnv.addFilter('checkToolDeepLink', checkToolDeepLink) } export default addFilters diff --git a/src/middleware/common.middleware.js b/src/middleware/common.middleware.js index 97e480ec..27e96c22 100644 --- a/src/middleware/common.middleware.js +++ b/src/middleware/common.middleware.js @@ -79,7 +79,7 @@ export const fetchOrgInfo = fetchOne({ * @param {*} res * @param {*} next */ -export function validateQueryParams (req, res, next) { +export function validateQueryParamsFn (req, res, next) { try { v.parse(this.schema || v.any(), req.params) next() @@ -88,6 +88,10 @@ export function validateQueryParams (req, res, next) { } } +export function validateQueryParams (context) { + return validateQueryParamsFn.bind(context) +} + export const fetchLpaDatasetIssues = fetchMany({ query: ({ params, req }) => performanceDbApi.datasetIssuesQuery(req.resourceStatus.resource, params.dataset), result: 'issues' diff --git a/src/middleware/datasetTaskList.middleware.js b/src/middleware/datasetTaskList.middleware.js index 802cdbe6..e77d3b1a 100644 --- a/src/middleware/datasetTaskList.middleware.js +++ b/src/middleware/datasetTaskList.middleware.js @@ -1,7 +1,8 @@ -import { fetchDatasetInfo, isResourceAccessible, isResourceNotAccessible, fetchLatestResource, fetchEntityCount, logPageError, fetchLpaDatasetIssues } from './common.middleware.js' +import { fetchDatasetInfo, isResourceAccessible, isResourceNotAccessible, fetchLatestResource, fetchEntityCount, logPageError, fetchLpaDatasetIssues, validateQueryParams } from './common.middleware.js' import { fetchOne, fetchIf, onlyIf, renderTemplate } from './middleware.builders.js' import performanceDbApi from '../services/performanceDbApi.js' import { statusToTagClass } from '../filters/filters.js' +import * as v from 'valibot' /** * Fetches the resource status @@ -111,7 +112,15 @@ const getDatasetTaskListError = renderTemplate({ handlerName: 'getDatasetTaskListError' }) +const validateParams = validateQueryParams({ + schema: v.object({ + lpa: v.string(), + dataset: v.string() + }) +}) + export default [ + validateParams, fetchResourceStatus, fetchOrgInfoWithStatGeo, fetchDatasetInfo, diff --git a/src/middleware/issueDetails.middleware.js b/src/middleware/issueDetails.middleware.js index 2e79d059..a537a3c9 100644 --- a/src/middleware/issueDetails.middleware.js +++ b/src/middleware/issueDetails.middleware.js @@ -15,7 +15,7 @@ export const IssueDetailsQueryParams = v.object({ resourceId: v.optional(v.string()) }) -const validateIssueDetailsQueryParams = validateQueryParams.bind({ +const validateIssueDetailsQueryParams = validateQueryParams({ schema: IssueDetailsQueryParams }) diff --git a/src/middleware/overview.middleware.js b/src/middleware/overview.middleware.js index 10bb11d2..55f738b8 100644 --- a/src/middleware/overview.middleware.js +++ b/src/middleware/overview.middleware.js @@ -56,7 +56,7 @@ const fetchEntityCounts = async (req, res, next) => { const statusOrdering = new Map(['Live', 'Needs fixing', 'Error', 'Not submitted'].map((status, i) => [status, i])) /** - * The overview data can contain multiple rows per dataset + * The overview data can contain multiple rows per dataset, * and we want a collection of with one item per dataset, * because that's how we display it on the page. * @@ -94,7 +94,7 @@ export function aggregateOverviewData (lpaOverview) { } /** - * Calculates overal "health" of the datasets (not)provided by an organisation. + * Calculates overall "health" of the datasets (not)provided by an organisation. * * @param {[number, number, number]} accumulator * @param {{ endpoint?: string, status: string }} dataset diff --git a/src/routes/form-wizard/check/steps.js b/src/routes/form-wizard/check/steps.js index a38e9c8c..125c070b 100644 --- a/src/routes/form-wizard/check/steps.js +++ b/src/routes/form-wizard/check/steps.js @@ -1,10 +1,14 @@ // ToDo: Split this into two form wizards import PageController from '../../../controllers/pageController.js' -import datasetController from '../../../controllers/datasetController.js' +import datasetController, { + requiresGeometryTypeToBeSelected, + requiresGeometryTypeToBeSelectedViaDeepLink +} from '../../../controllers/datasetController.js' import uploadFileController from '../../../controllers/uploadFileController.js' import submitUrlController from '../../../controllers/submitUrlController.js' import statusController from '../../../controllers/statusController.js' import resultsController from '../../../controllers/resultsController.js' +import deepLinkController from '../../../controllers/deepLinkController.js' const baseSettings = { controller: PageController, @@ -20,17 +24,13 @@ export default { template: 'check/start.html', noPost: true }, - // '/data-subject': { - // ...baseSettings, - // fields: ['data-subject'], - // next: 'dataset' - // }, '/dataset': { ...baseSettings, controller: datasetController, fields: ['dataset', 'data-subject'], + checkJourney: false, next: [ - { field: 'dataset', fn: 'requiresGeometryTypeToBeSelected', next: 'geometry-type' }, + { field: 'dataset', fn: requiresGeometryTypeToBeSelected, next: 'geometry-type' }, 'upload-method' ], backLink: './' @@ -39,7 +39,8 @@ export default { ...baseSettings, fields: ['geomType'], next: 'upload-method', - backLink: './dataset' + backLink: './dataset', + checkJourney: false }, '/upload-method': { ...baseSettings, @@ -48,21 +49,24 @@ export default { { field: 'upload-method', op: '===', value: 'url', next: 'url' }, 'upload' ], - backLink: './dataset' + backLink: './dataset', + checkJourney: false }, '/url': { ...baseSettings, controller: submitUrlController, fields: ['url', 'request_id'], next: (req, res) => `status/${req.sessionModel.get('request_id')}`, - backLink: './upload-method' + backLink: './upload-method', + checkJourney: false }, '/upload': { ...baseSettings, controller: uploadFileController, fields: ['datafile', 'request_id'], next: (req, res) => `status/${req.sessionModel.get('request_id')}`, - backLink: './upload-method' + backLink: './upload-method', + checkJourney: false }, '/status/:id': { ...baseSettings, @@ -90,5 +94,22 @@ export default { noPost: true, checkJourney: false, // ToDo: it would be useful here if we make sure they have selected if their results are ok from the previous step template: 'check/confirmation.html' + }, + // This step allows to fill in some of the required data via query params. + // This way we can link from a dataset issues page to the Check Tool without + // the user having to go through the whole process again. + // This means it doesn't render a page, but redirects the client to the next step + '/link': { + ...baseSettings, + controller: deepLinkController, + next: [ + { field: 'dataset', fn: requiresGeometryTypeToBeSelectedViaDeepLink, next: 'geometry-type' }, + 'upload-method' + ], + entryPoint: true, + resetJourney: true, + reset: true, + skip: true, + checkJourney: false } } diff --git a/src/routes/schemas.js b/src/routes/schemas.js index 7c9ed66b..7434f22d 100644 --- a/src/routes/schemas.js +++ b/src/routes/schemas.js @@ -14,7 +14,7 @@ export const ErrorParams = v.strictObject({ err: v.object({}) }) -const NonEmptyString = v.pipe(v.string(), v.nonEmpty()) +export const NonEmptyString = v.pipe(v.string(), v.nonEmpty()) export const Base = v.object({ // serviceName: NonEmptyString, diff --git a/src/serverSetup/session.js b/src/serverSetup/session.js index 19f0de57..2c1ee45a 100644 --- a/src/serverSetup/session.js +++ b/src/serverSetup/session.js @@ -1,13 +1,11 @@ import session from 'express-session' import { createClient } from 'redis' import RedisStore from 'connect-redis' -import cookieParser from 'cookie-parser' import config from '../../config/index.js' import logger from '../utils/logger.js' import { types } from '../utils/logging.js' export async function setupSession (app) { - app.use(cookieParser()) let sessionStore if ('redis' in config) { const urlPrefix = `redis${config.redis.secure ? 's' : ''}` diff --git a/src/services/asyncRequestApi.js b/src/services/asyncRequestApi.js index cdcc831f..e96f7ea5 100644 --- a/src/services/asyncRequestApi.js +++ b/src/services/asyncRequestApi.js @@ -45,8 +45,9 @@ const postRequest = async (formData) => { const errorMessage = `post request failed: response.status = '${error.response?.status}', ` + `data: '${error.response?.data}', ` + `cause: '${error?.cause}' ` + + `code: ${error.code}, ` + (error.request ? 'No response received, ' : '') + - `message: '${error.message ?? 'no meesage provided'}', ` + + `message: '${error.message ?? 'no message provided'}', ` + (error.config ? `Error in Axios configuration ${error?.config}` : '') throw new Error(errorMessage) diff --git a/src/utils/utils.js b/src/utils/utils.js index 0f63291c..136be3c2 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -94,6 +94,35 @@ export const dataSubjects = { } } +export function makeDatasetsLookup (dataSubjects) { + const lookup = new Map() + for (const [key, dataSubject] of Object.entries(dataSubjects)) { + for (const dataSet of dataSubject.dataSets) { + lookup.set(dataSet.value, { ...dataSet, dataSubject: key }) + } + } + + return lookup +} + +/** + * @type {Map} + */ +export const datasets = makeDatasetsLookup(dataSubjects) + +/** + * + * @param dataSubjects + * @returns {FlatArray<*[], 1>[]} datasets sorted by 'text' property + */ +export function availableDatasets (dataSubjects) { + const availableDataSubjects = Object.values(dataSubjects).filter(dataSubject => dataSubject.available) + const dataSets = Object.values(availableDataSubjects).map(dataSubject => dataSubject.dataSets).flat() + const availableDatasets = dataSets.filter(dataSet => dataSet.available) + availableDatasets.sort((a, b) => a.text.localeCompare(b.text)) + return availableDatasets +} + export const finishedProcessingStatuses = [ 'COMPLETE', 'FAILED' diff --git a/src/views/check/geometry-type.html b/src/views/check/geometry-type.html index 24c8853b..acdc8696 100644 --- a/src/views/check/geometry-type.html +++ b/src/views/check/geometry-type.html @@ -2,6 +2,7 @@ {% from 'govuk/components/radios/macro.njk' import govukRadios %} {% from 'govuk/components/error-message/macro.njk' import govukErrorMessage %} {% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} +{% from 'components/dataset-banner.html' import datasetBanner %} {% extends "layouts/main.html" %} @@ -23,6 +24,7 @@ {% endblock %} {% block content %} +
{% if error %} @@ -38,6 +40,10 @@ {% endif %}
+ {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% endif %} + {{ govukRadios({ name: "geomType", fieldset: { diff --git a/src/views/check/results/errors.html b/src/views/check/results/errors.html index e123c145..9027c5cb 100644 --- a/src/views/check/results/errors.html +++ b/src/views/check/results/errors.html @@ -4,8 +4,8 @@ {% from 'govuk/components/radios/macro.njk' import govukRadios %} {% from 'govuk/components/inset-text/macro.njk' import govukInsetText %} {% from "govuk/components/pagination/macro.njk" import govukPagination %} - {% from "../../components/table.html" import table %} +{% from '../../components/dataset-banner.html' import datasetBanner %} {% set serviceType = 'Check' %} {% set pageName = 'Your data has errors' %} @@ -21,11 +21,16 @@ {% endblock %} {% block content %} +
- - {{options.requestParams.dataset}} - + + {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% else %} + {{options.requestParams.dataset}} + {% endif %} +

{{pageName}}

diff --git a/src/views/check/results/no-errors.html b/src/views/check/results/no-errors.html index 1466c87c..a27665d2 100644 --- a/src/views/check/results/no-errors.html +++ b/src/views/check/results/no-errors.html @@ -4,7 +4,7 @@ {% from 'govuk/components/radios/macro.njk' import govukRadios %} {% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} {% from "govuk/components/pagination/macro.njk" import govukPagination %} - +{% from '../../components/dataset-banner.html' import datasetBanner %} {% from "../../components/table.html" import table %} @@ -33,8 +33,12 @@ {% block content %} +
+ {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% endif %}

{{pageName}}

diff --git a/src/views/check/statusPage/status.html b/src/views/check/statusPage/status.html index 0ac2b69b..f4e66ec9 100644 --- a/src/views/check/statusPage/status.html +++ b/src/views/check/statusPage/status.html @@ -1,6 +1,7 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% from "./checkingFileMacro.html" import checkingFileContent %} {% from "./fileCheckedMacro.html" import fileCheckedContent %} +{% from '../../components/dataset-banner.html' import datasetBanner %} {% extends "layouts/main.html" %} @@ -20,6 +21,9 @@ {% block content %}
+ {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% endif %} {{ pageContent }}
diff --git a/src/views/check/upload-method.html b/src/views/check/upload-method.html index d0b08f27..e00832b4 100644 --- a/src/views/check/upload-method.html +++ b/src/views/check/upload-method.html @@ -2,6 +2,7 @@ {% from 'govuk/components/radios/macro.njk' import govukRadios %} {% from 'govuk/components/error-message/macro.njk' import govukErrorMessage %} {% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} +{% from 'components/dataset-banner.html' import datasetBanner %} {% extends "layouts/main.html" %} @@ -23,6 +24,7 @@ {% endblock %} {% block content %} +
{% if error %} @@ -38,6 +40,10 @@ {% endif %} + {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% endif %} + {{ govukRadios({ name: "upload-method", fieldset: { diff --git a/src/views/check/upload.html b/src/views/check/upload.html index 67630bf4..1d0a53d8 100644 --- a/src/views/check/upload.html +++ b/src/views/check/upload.html @@ -4,6 +4,7 @@ {% from "govuk/components/button/macro.njk" import govukButton %} {% from 'govuk/components/error-message/macro.njk' import govukErrorMessage %} {% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} +{% from 'components/dataset-banner.html' import datasetBanner %} {% set serviceType = 'Check' %} {% set pageName = 'Upload data' %} @@ -30,6 +31,7 @@ {% endblock %} {% block content %} +
{% if error %} @@ -45,6 +47,10 @@ {% endif %} + {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% endif %} + {{ govukFileUpload({ id: "datafile", name: "datafile", diff --git a/src/views/check/url.html b/src/views/check/url.html index 6f703517..5953e1a1 100644 --- a/src/views/check/url.html +++ b/src/views/check/url.html @@ -4,6 +4,7 @@ {% from 'govuk/components/input/macro.njk' import govukInput %} {% from 'govuk/components/error-message/macro.njk' import govukErrorMessage %} {% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} +{% from 'components/dataset-banner.html' import datasetBanner %} {% set serviceType = 'Check' %} {% set pageName = 'URL' %} @@ -32,7 +33,9 @@ {% endblock %} {% block content %} +
+
{% if error %} {{ govukErrorSummary({ @@ -46,7 +49,9 @@ }) }} {% endif %} - + {% if options.deepLink %} + {{ datasetBanner(options.deepLink) }} + {% endif %} {{ govukInput({ id: "url", name: "url", diff --git a/src/views/components/dataset-banner.html b/src/views/components/dataset-banner.html new file mode 100644 index 00000000..b402c483 --- /dev/null +++ b/src/views/components/dataset-banner.html @@ -0,0 +1,3 @@ +{% macro datasetBanner(params) %} +{{ params.datasetName }} +{% endmacro %} \ No newline at end of file diff --git a/src/views/organisations/datasetTaskList.html b/src/views/organisations/datasetTaskList.html index b61dbc1e..83cba216 100644 --- a/src/views/organisations/datasetTaskList.html +++ b/src/views/organisations/datasetTaskList.html @@ -77,7 +77,7 @@

  1. Fix the errors indicated
  2. -
  3. Use the check service to make sure the data meets +
  4. Use the check service to make sure the data meets the standard
  5. Publish the updated data on the data URL
diff --git a/src/views/organisations/get-started.html b/src/views/organisations/get-started.html index 74ff3bf0..ddb762a5 100644 --- a/src/views/organisations/get-started.html +++ b/src/views/organisations/get-started.html @@ -100,7 +100,7 @@

  1. - The check service can help you understand + The check service can help you understand if your data is ready to submit or if you need to change anything before you publish it on your website.

    @@ -120,7 +120,7 @@

    1. Check your data + href="{{ organisation | checkToolDeepLink(dataset) }}">Check your data

  2. diff --git a/src/views/organisations/issueDetails.html b/src/views/organisations/issueDetails.html index 72379f28..988acd3f 100644 --- a/src/views/organisations/issueDetails.html +++ b/src/views/organisations/issueDetails.html @@ -45,7 +45,6 @@ {% block content %} -
    {% include "includes/_dataset-page-header.html" %}
    diff --git a/test/unit/check-answers.test.js b/test/unit/check-answers.test.js index 3bf450f4..6a19a89d 100644 --- a/test/unit/check-answers.test.js +++ b/test/unit/check-answers.test.js @@ -21,7 +21,7 @@ describe('check-answers View', async () => { const html = stripWhitespace(nunjucks.render('check-answers.html', params)) runGenericPageTests(html, { - pageTitle: 'Check your answers - Check planning and housing data for England' + pageTitle: 'Check your answers - Submit and update your planning data' }) it('should render the lpa selected', () => { diff --git a/test/unit/chooseDatasetController.test.js b/test/unit/chooseDatasetController.test.js index 1fd2c8d4..995e4893 100644 --- a/test/unit/chooseDatasetController.test.js +++ b/test/unit/chooseDatasetController.test.js @@ -10,13 +10,15 @@ describe('ChooseDatasetController', () => { route: '/dataset' }) - vi.mock('../../src/utils/utils.js', () => { + vi.mock(import('../../src/utils/utils.js'), async (importOriginal) => { + const { availableDatasets } = await importOriginal() return { dataSubjects: { subject1: { available: true, dataSets: [{ available: true, text: 'B', value: 'B', requiresGeometryTypeSelection: true }, { available: false, text: 'A', value: 'A', requiresGeometryTypeSelection: false }] }, subject2: { available: false, dataSets: [{ available: true, text: 'C', value: 'C', requiresGeometryTypeSelection: false }] }, subject3: { available: true, dataSets: [{ available: true, text: 'A', value: 'A', requiresGeometryTypeSelection: true }] } - } + }, + availableDatasets } }) }) diff --git a/test/unit/datasetController.test.js b/test/unit/datasetController.test.js index 889afff7..21608ca1 100644 --- a/test/unit/datasetController.test.js +++ b/test/unit/datasetController.test.js @@ -1,5 +1,7 @@ -import DatasetController from '../../src/controllers/datasetController.js' - +import DatasetController, { + requiresGeometryTypeToBeSelected, + requiresGeometryTypeToBeSelectedViaDeepLink +} from '../../src/controllers/datasetController.js' import { describe, it, vi, expect, beforeEach } from 'vitest' describe('DatasetController', () => { @@ -10,13 +12,22 @@ describe('DatasetController', () => { route: '/dataset' }) - vi.mock('../../src/utils/utils.js', () => { + vi.mock(import('../../src/utils/utils.js'), async (importOriginal) => { + const { availableDatasets, makeDatasetsLookup } = await importOriginal() + const dataSubjects = { + subject1: { + available: true, + dataSets: + [{ available: true, text: 'B', value: 'B', requiresGeometryTypeSelection: true }, + { available: false, text: 'D', value: 'D', requiresGeometryTypeSelection: false }] + }, + subject2: { available: false, dataSets: [{ available: true, text: 'C', value: 'C', requiresGeometryTypeSelection: false }] }, + subject3: { available: true, dataSets: [{ available: true, text: 'A', value: 'A', requiresGeometryTypeSelection: true }] } + } return { - dataSubjects: { - subject1: { available: true, dataSets: [{ available: true, text: 'B', value: 'B', requiresGeometryTypeSelection: true }, { available: false, text: 'A', value: 'A', requiresGeometryTypeSelection: false }] }, - subject2: { available: false, dataSets: [{ available: true, text: 'C', value: 'C', requiresGeometryTypeSelection: false }] }, - subject3: { available: true, dataSets: [{ available: true, text: 'A', value: 'A', requiresGeometryTypeSelection: true }] } - } + availableDatasets, + dataSubjects, + datasets: makeDatasetsLookup(dataSubjects) } }) }) @@ -58,14 +69,28 @@ describe('DatasetController', () => { it('Correctly determines whether a geometry type selection is required', () => { // Mock req with dataset that requires geometry type selection const req1 = { body: { dataset: 'B' } } - expect(datasetController.requiresGeometryTypeToBeSelected(req1)).toEqual(true) + expect(requiresGeometryTypeToBeSelected(req1)).toEqual(true) // Mock req with dataset that does not require geometry type selection - const req2 = { body: { dataset: 'A' } } - expect(datasetController.requiresGeometryTypeToBeSelected(req2)).toEqual(false) + const req2 = { body: { dataset: 'D' } } + expect(requiresGeometryTypeToBeSelected(req2)).toEqual(false) // Mock req with no dataset const req3 = { body: {} } - expect(datasetController.requiresGeometryTypeToBeSelected(req3)).toEqual(false) + expect(requiresGeometryTypeToBeSelected(req3)).toEqual(false) + }) + + it('Correctly determines whether a geometry type selection is required via deep link', () => { + // Mock req with dataset that requires geometry type selection + const req1 = { query: { dataset: 'B' } } + expect(requiresGeometryTypeToBeSelectedViaDeepLink(req1)).toEqual(true) + + // Mock req with dataset that does not require geometry type selection + const req2 = { query: { dataset: 'D' } } + expect(requiresGeometryTypeToBeSelectedViaDeepLink(req2)).toEqual(false) + + // Mock req with no dataset + const req3 = { query: {} } + expect(requiresGeometryTypeToBeSelectedViaDeepLink(req3)).toEqual(false) }) }) diff --git a/test/unit/deepLinkController.test.js b/test/unit/deepLinkController.test.js new file mode 100644 index 00000000..5fafe574 --- /dev/null +++ b/test/unit/deepLinkController.test.js @@ -0,0 +1,53 @@ +import { describe, it, vi, expect, beforeEach } from 'vitest' +import DeepLinkController from '../../src/controllers/deepLinkController.js' + +function mockRequestObject () { + const sessionModel = new Map() + const journeyModel = new Map() + return { sessionModel, journeyModel, query: {} } +} + +function mockMiddlewareArgs (reqOpts) { + return { + req: { ...mockRequestObject(), ...reqOpts }, + res: { redirect: vi.fn() }, + next: vi.fn() + } +} + +describe('DeepLinkController', () => { + let deepLinkController + + beforeEach(() => { + deepLinkController = new DeepLinkController({ + route: '/deep-link' + }) + }) + + describe('get()', () => { + it('should redirect to check tool start page when params invalid', async () => { + const { req, res, next } = mockMiddlewareArgs({ query: {} }) + deepLinkController.get(req, res, next) + + expect(res.redirect).toHaveBeenCalledWith('/check') + expect(Array.from(req.sessionModel.keys())).toStrictEqual([]) + expect(next).toBeCalledTimes(0) + }) + + it('should update session with deep link info', async () => { + const query = { dataset: 'conservation-area', orgName: 'Some Org' } + const { req, res, next } = mockMiddlewareArgs({ query }) + + deepLinkController.get(req, res, next) + + expect(req.sessionModel.get(deepLinkController.checkToolDeepLinkSessionKey)).toStrictEqual({ + 'data-subject': 'conservation-area', + orgName: 'Some Org', + dataset: 'conservation-area', + datasetName: 'Conservation area dataset' + }) + expect(req.journeyModel.get('history').length).toBe(1) + expect(next).toBeCalledTimes(1) + }) + }) +}) diff --git a/test/unit/util.test.js b/test/unit/util.test.js new file mode 100644 index 00000000..3436295d --- /dev/null +++ b/test/unit/util.test.js @@ -0,0 +1,33 @@ +import * as util from '../../src/utils/utils.js' +import { describe, it, expect } from 'vitest' + +const dataSubjects = { + subject1: { + available: true, + dataSets: + [{ available: true, text: 'B', value: 'B', requiresGeometryTypeSelection: true }, + { available: false, text: 'D', value: 'D' }] + }, + subject2: { available: false, dataSets: [{ available: true, text: 'C', value: 'C', requiresGeometryTypeSelection: false }] }, + subject3: { available: true, dataSets: [{ available: true, text: 'A', value: 'A', requiresGeometryTypeSelection: true }] } +} + +describe('utils/utils', () => { + it('makeDatasetsLookup()', () => { + const lookup = util.makeDatasetsLookup(dataSubjects) + + expect(lookup.get('A')).toEqual({ ...dataSubjects.subject3.dataSets[0], dataSubject: 'subject3' }) + expect(lookup.get('B')).toEqual({ ...dataSubjects.subject1.dataSets[0], dataSubject: 'subject1' }) + expect(lookup.get('C')).toEqual({ ...dataSubjects.subject2.dataSets[0], dataSubject: 'subject2' }) + expect(lookup.get('D')).toEqual({ ...dataSubjects.subject1.dataSets[1], dataSubject: 'subject1' }) + + const allDatasets = Object.entries(dataSubjects).map(([_, sub]) => sub.dataSets.map(ds => ds.value)).flat() + const uniqueDatasets = new Set(allDatasets) + expect(lookup.length).toBe(uniqueDatasets.length) + }) + + it('availableDatasets()', () => { + const datasets = util.availableDatasets(dataSubjects) + expect(new Set(datasets.map(ds => ds.value))).toEqual(new Set(['A', 'B'])) + }) +})