From 9b0c93ac17744f1207e7c002baa166c97206658c Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 20 May 2024 15:27:54 +0100 Subject: [PATCH 01/22] find place searching on alerts and warnings --- server/models/views/alerts-and-warnings.js | 6 +- server/routes/alerts-and-warnings.js | 210 ++++++++++++++------ server/views/layout.html | 2 +- server/views/location-not-found.html | 2 +- test/routes/alerts-and-warnings-POST.js | 86 -------- test/routes/alerts-and-warnings.js | 219 +++++++++++++++------ 6 files changed, 318 insertions(+), 207 deletions(-) delete mode 100644 test/routes/alerts-and-warnings-POST.js diff --git a/server/models/views/alerts-and-warnings.js b/server/models/views/alerts-and-warnings.js index 65ef129d3..22450e30e 100644 --- a/server/models/views/alerts-and-warnings.js +++ b/server/models/views/alerts-and-warnings.js @@ -2,15 +2,17 @@ const { bingKeyMaps } = require('../../config') const config = require('../../config') class ViewModel { - constructor ({ location, place, floods, station, error }) { + constructor ({ q, location, place, floods, station, canonical, error }) { Object.assign(this, { - q: location, + q: q || location, map: station ? 'map-station' : 'map', station: station || null, placeName: place ? place.name : '', placeBbox: place ? place.bbox2k : [], placeCentre: place ? place.center : [], timestamp: Date.now(), + metaCanonical: canonical, + canonicalUrl: canonical, error: error ? true : null, displayGetWarningsLink: true, displayLongTermLink: true, diff --git a/server/routes/alerts-and-warnings.js b/server/routes/alerts-and-warnings.js index f371f2b6c..7baaf0a01 100644 --- a/server/routes/alerts-and-warnings.js +++ b/server/routes/alerts-and-warnings.js @@ -1,92 +1,182 @@ +const qs = require('qs') const joi = require('@hapi/joi') +const boom = require('@hapi/boom') const ViewModel = require('../models/views/alerts-and-warnings') const Floods = require('../models/floods') const locationService = require('../services/location') const util = require('../util') +const { slugify } = require('./lib/utils') -module.exports = [{ - method: 'GET', - path: '/alerts-and-warnings', - handler: async (request, h) => { - const { q: location } = request.query - let model, place, floods - - const direction = request.query.direction === 'downstream' ? 'd' : 'u' - const station = request.query.station - ? await request.server.methods.flood.getStationById(request.query.station, direction) - : null - - if (station) { - const warningsAlerts = await request.server.methods.flood.getWarningsAlertsWithinStationBuffer(station.rloi_id) - floods = new Floods({ floods: warningsAlerts }) - model = new ViewModel({ location, place, floods, station }) - return h.view('alerts-and-warnings', { model }) - } else if (typeof location === 'undefined' || location === '' || location.match(/^england$/i)) { - floods = new Floods(await request.server.methods.flood.getFloods()) - model = new ViewModel({ location, place, floods, station }) - return h.view('alerts-and-warnings', { model }) - } else { - try { - [place] = await locationService.find(util.cleanseLocation(location)) - } catch (error) { - request.logger.warn({ - situation: `Location search error: [${error.name}] [${error.message}]`, - err: error - }) - const floods = new Floods(await request.server.methods.flood.getFloods()) - model = new ViewModel({ location, place, floods, station, error }) - return h.view('alerts-and-warnings', { model }) - } +function createQueryParametersString (queryObject) { + const { q, location, ...otherParameters } = queryObject + const queryString = qs.stringify(otherParameters, { addQueryPrefix: true, encode: false }) + return queryString +} - if (!place) { - model = new ViewModel({ location, place, floods, station }) - return h.view('alerts-and-warnings', { model }) - } +async function routeHandler (request, h) { + let location = request.query.q || request.query.location || request.payload?.location + const direction = request.query.direction === 'downstream' ? 'd' : 'u' + + const station = request.query.station + ? await request.server.methods.flood.getStationById(request.query.station, direction) + : null + + let model, floods - if (!place?.isEngland.is_england) { - // If no place return empty floods - model = new ViewModel({ location, place, floods, station }) - return h.view('alerts-and-warnings', { model }) + request.yar.set('q', location) + + if (station) { + const warningsAlerts = await request.server.methods.flood.getWarningsAlertsWithinStationBuffer(station.rloi_id) + floods = new Floods({ floods: warningsAlerts }) + model = new ViewModel({ location, floods, station }) + return h.view('alerts-and-warnings', { model }) + } + + if (location) { + location = util.cleanseLocation(location) + + const [place] = await locationService.find(location) + + if (!place) { + if (request.method === 'get') { + return boom.notFound(`Location ${location} not found`) } else { - // Data passed to floods model so the schema is the same as cached floods - const data = await request.server.methods.flood.getFloodsWithin(place.bbox2k) - floods = new Floods(data) - model = new ViewModel({ location, place, floods, station }) - return h.view('alerts-and-warnings', { model }) + return h.view('location-not-found', { pageTitle: 'Error: Find location - Check for flooding', href: 'alerts-and-warnings', location }).takeover() + } + } + + if (!place.isEngland.is_england) { + request.logger.warn({ + situation: 'Location search error: Valid response but location not in England.' + }) + + if (request.method === 'post') { + return h.view('location-not-found', { pageTitle: 'Error: Find location - Check for flooding', href: 'alerts-and-warnings', location }).takeover() } } - }, + + const queryString = createQueryParametersString(request.query) + + return h.redirect(`/alerts-and-warnings/${slugify(place?.name)}${queryString}`).permanent() + } + + const data = await request.server.methods.flood.getFloods() + floods = new Floods(data) + model = new ViewModel({ location, floods }) + return h.view('alerts-and-warnings', { model }) +} + +async function locationRouteHandler (request, h) { + const canonicalUrl = request.url.origin + request.url.pathname + const location = util.cleanseLocation(request.params.location) + const direction = request.query.direction === 'downstream' ? 'd' : 'u' + + const station = request.query.station + ? await request.server.methods.flood.getStationById(request.query.station, direction) + : null + + const [place] = await locationService.find(location) + + let model, floods + + if (station) { + const warningsAlerts = await request.server.methods.flood.getWarningsAlertsWithinStationBuffer(station.rloi_id) + floods = new Floods({ floods: warningsAlerts }) + model = new ViewModel({ location, place, floods, station, canonical: canonicalUrl, q: request.yar.get('q') }) + return h.view('alerts-and-warnings', { model }) + } + + if (location.match(/^england$/i)) { + return h.redirect('/alerts-and-warnings') + } + + if (slugify(place?.name) !== location) { + return boom.notFound(`Location ${location} not found`) + } + + if (!place.isEngland.is_england) { + request.logger.warn({ + situation: 'Location search error: Valid response but location not in England.' + }) + + if (request.method === 'get') { + return boom.notFound(`Location ${location} not found`) + } else { + return h.view('location-not-found', { pageTitle: 'Error: Find location - Check for flooding', href: 'alerts-and-warnings', location }).takeover() + } + } + + // Data passed to floods model so the schema is the same as cached floods + const data = await request.server.methods.flood.getFloodsWithin(place.bbox2k) + floods = new Floods(data) + model = new ViewModel({ location, place, floods, station, canonical: canonicalUrl, q: request.yar.get('q') }) + return h.view('alerts-and-warnings', { model }) +} + +function failActionHandler (request, h) { + request.logger.warn({ + situation: 'Location search error: Invalid or no string input.' + }) + + const location = request.query.q || request.query.location || request.payload?.location + + if (!location) { + return h.redirect('alerts-and-warnings').takeover() + } else { + return h.view('location-not-found', { pageTitle: 'Error: Find location - Check for flooding', href: 'alerts-and-warnings', location }).takeover() + } +} + +module.exports = [{ + method: 'GET', + path: '/alerts-and-warnings', + handler: routeHandler, options: { validate: { query: joi.object({ - q: joi.string().allow('').trim().max(200), + q: joi.string().trim().max(200), station: joi.string(), btn: joi.string(), ext: joi.string(), fid: joi.string(), lyr: joi.string(), v: joi.string() - }) + }), + failAction: failActionHandler } } -}, { +}, +{ + method: 'GET', + path: '/alerts-and-warnings/{location}', + handler: locationRouteHandler, + options: { + validate: { + params: joi.object({ + location: joi.string().lowercase() + }), + query: joi.object({ + station: joi.string(), + btn: joi.string(), + ext: joi.string(), + fid: joi.string(), + lyr: joi.string(), + v: joi.string() + }), + failAction: failActionHandler + } + } +}, +{ method: 'POST', path: '/alerts-and-warnings', - handler: async (request, h) => { - const { location } = request.payload - if (location === '') { - return h.redirect(`/alerts-and-warnings?q=${location}`) - } - return h.redirect(`/alerts-and-warnings?q=${encodeURIComponent(util.cleanseLocation(location))}`) - }, + handler: routeHandler, options: { validate: { payload: joi.object({ location: joi.string().allow('').trim().max(200).required() }), - failAction: (request, h, _err) => { - return h.view('alerts-and-warnings').takeover() - } + failAction: failActionHandler } } }] diff --git a/server/views/layout.html b/server/views/layout.html index f9e35eb87..60dada24d 100644 --- a/server/views/layout.html +++ b/server/views/layout.html @@ -30,7 +30,7 @@ {% if metaCanonical %} - + {% endif %}