Skip to content

Commit

Permalink
Merge pull request #550 from digital-land/rosado/307-check-tool-deep-…
Browse files Browse the repository at this point in the history
…links

Check Tool: Deep links support
  • Loading branch information
GeorgeGoodall-GovUk authored Oct 17, 2024
2 parents dbd776c + b097e7f commit 4ea328f
Show file tree
Hide file tree
Showing 33 changed files with 368 additions and 87 deletions.
2 changes: 1 addition & 1 deletion config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 2 additions & 8 deletions src/controllers/chooseDatasetController.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
Expand Down
51 changes: 23 additions & 28 deletions src/controllers/datasetController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
59 changes: 59 additions & 0 deletions src/controllers/deepLinkController.js
Original file line number Diff line number Diff line change
@@ -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
15 changes: 11 additions & 4 deletions src/controllers/pageController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/controllers/uploadFileController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
11 changes: 11 additions & 0 deletions src/filters/checkToolDeepLink.js
Original file line number Diff line number Diff line change
@@ -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)}`
}
2 changes: 2 additions & 0 deletions src/filters/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
6 changes: 5 additions & 1 deletion src/middleware/common.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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'
Expand Down
11 changes: 10 additions & 1 deletion src/middleware/datasetTaskList.middleware.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/issueDetails.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const IssueDetailsQueryParams = v.object({
resourceId: v.optional(v.string())
})

const validateIssueDetailsQueryParams = validateQueryParams.bind({
const validateIssueDetailsQueryParams = validateQueryParams({
schema: IssueDetailsQueryParams
})

Expand Down
4 changes: 2 additions & 2 deletions src/middleware/overview.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
43 changes: 32 additions & 11 deletions src/routes/form-wizard/check/steps.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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: './'
Expand All @@ -39,7 +39,8 @@ export default {
...baseSettings,
fields: ['geomType'],
next: 'upload-method',
backLink: './dataset'
backLink: './dataset',
checkJourney: false
},
'/upload-method': {
...baseSettings,
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
}
Loading

0 comments on commit 4ea328f

Please sign in to comment.