diff --git a/server/models/views/alerts-and-warnings.js b/server/models/views/alerts-and-warnings.js index 65ef129d3..0a109a20f 100644 --- a/server/models/views/alerts-and-warnings.js +++ b/server/models/views/alerts-and-warnings.js @@ -2,36 +2,39 @@ 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, + station, map: station ? 'map-station' : 'map', - station: station || null, - placeName: place ? place.name : '', - placeBbox: place ? place.bbox2k : [], - placeCentre: place ? place.center : [], + placeName: place?.name || '', + placeBbox: place?.bbox2k || [], + placeCentre: place?.center || [], timestamp: Date.now(), - error: error ? true : null, + metaCanonical: canonical, + canonicalUrl: canonical, + error: !!error, displayGetWarningsLink: true, displayLongTermLink: true, - isEngland: place ? place.isEngland.is_england : null, - isDummyData: floods ? floods.isDummyData : false, + isEngland: place?.isEngland?.is_england, + isDummyData: floods?.isDummyData, floodRiskUrl: config.floodRiskUrl }) + if (this.station && this.station.agency_name) { + this.pageTitle = `${this.station.agency_name} - flood alerts and warnings` + } else { + const pageTitle = place?.name ?? location + + this.pageTitle = `${pageTitle ? pageTitle + ' - f' : 'F'}lood alerts and warnings` + } + if (error) { this.pageTitle = 'Sorry, there is currently a problem searching a location' - } else { - if (this.station && this.station.agency_name) { - this.pageTitle = `${this.station.agency_name} - flood alerts and warnings` - } else { - this.pageTitle = `${location ? location + ' - f' : 'F'}lood alerts and warnings` - } } - this.countFloods = floods ? floods.floods.length : 0 - this.floods = floods - ? floods.groups.map(item => item) - : [] + + this.countFloods = floods?.floods?.length ?? 0 + this.floods = floods?.groups?.map(item => item) this.expose = { station: this.station, diff --git a/server/routes/alerts-and-warnings.js b/server/routes/alerts-and-warnings.js index f371f2b6c..5bbccd591 100644 --- a/server/routes/alerts-and-warnings.js +++ b/server/routes/alerts-and-warnings.js @@ -1,92 +1,175 @@ +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') + +const page = 'alerts-and-warnings' +const QUERY_STRING_LOCATION_MAX_LENGTH = 200 + +function renderLocationNotFound (location, h) { + return h.view('location-not-found', { pageTitle: 'Error: Find location - Check for flooding', href: page, location }).takeover() +} + +function renderNotFound (location) { + return boom.notFound(`Location ${location} not found`) +} + +function createQueryParametersString (queryObject) { + const { q, location, ...otherParameters } = queryObject + const queryString = qs.stringify(otherParameters, { addQueryPrefix: true, encode: false }) + return queryString +} + +async function routeHandler (request, h) { + let location = request.query.q || request.payload?.location + + request.yar.set('q', { location }) + + const direction = request.query.direction === 'downstream' ? 'd' : 'u' + + let model, floods + + if (request.query.station) { + const station = await request.server.methods.flood.getStationById(request.query.station, direction) + + 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(page, { model }) + } + + if (!location) { + const data = await request.server.methods.flood.getFloods() + floods = new Floods(data) + model = new ViewModel({ location, floods }) + return h.view(page, { model }) + } + + location = util.cleanseLocation(location) + + const [place] = await locationService.find(location) + + if (!place) { + if (request.method === 'get') { + return renderNotFound(location) + } + + return renderLocationNotFound(location, h) + } + + if (!place.isEngland.is_england) { + request.logger.warn({ + situation: 'Location search error: Valid response but location not in England.' + }) + + if (request.method === 'post') { + return renderLocationNotFound(location, h) + } + } + + const queryString = createQueryParametersString(request.query) + + return h.redirect(`/${page}/${slugify(place?.name)}${queryString}`).permanent() +} + +async function locationRouteHandler (request, h) { + const canonicalUrl = request.url.origin + request.url.pathname + const location = util.cleanseLocation(request.params.location) + + const [place] = await locationService.find(location) + + if (location.match(/^england$/i)) { + return h.redirect(`/${page}`) + } + + if (slugify(place?.name) !== location) { + return renderNotFound(location) + } + + if (!place?.isEngland.is_england) { + request.logger.warn({ + situation: 'Location search error: Valid response but location not in England.' + }) + + return renderNotFound(location) + } + + // Data passed to floods model so the schema is the same as cached floods + const data = await request.server.methods.flood.getFloodsWithin(place.bbox2k) + const floods = new Floods(data) + const model = new ViewModel({ location, place, floods, canonical: canonicalUrl, q: request.yar.get('q')?.location }) + request.yar.set('q', null) + return h.view(page, { model }) +} + +function failActionHandler (request, h) { + request.logger.warn({ + situation: 'Location search error: Invalid or no string input.' + }) + + const location = request.query.q || request.payload?.location + + if (!location) { + return h.redirect(page).takeover() + } else { + return renderLocationNotFound(location, h) + } +} 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 }) - } - - if (!place) { - model = new ViewModel({ location, place, floods, station }) - return h.view('alerts-and-warnings', { model }) - } - - 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 }) - } 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 }) - } + path: `/${page}`, + handler: routeHandler, + options: { + validate: { + query: joi.object({ + q: joi.string().trim().max(QUERY_STRING_LOCATION_MAX_LENGTH), + station: joi.string(), + btn: joi.string(), + ext: joi.string(), + fid: joi.string(), + lyr: joi.string(), + v: joi.string() + }), + failAction: failActionHandler } - }, + } +}, +{ + method: 'GET', + path: `/${page}/{location}`, + handler: locationRouteHandler, options: { validate: { + params: joi.object({ + location: joi.string().lowercase() + }), query: joi.object({ - q: joi.string().allow('').trim().max(200), 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))}`) - }, + path: `/${page}`, + handler: routeHandler, options: { validate: { payload: joi.object({ - location: joi.string().allow('').trim().max(200).required() + location: joi.string().allow('').trim().max(QUERY_STRING_LOCATION_MAX_LENGTH).required() }), - failAction: (request, h, _err) => { - return h.view('alerts-and-warnings').takeover() - } + failAction: failActionHandler } } }] diff --git a/server/services/lib/bing-results-parser.js b/server/services/lib/bing-results-parser.js index 6f6f7ad55..441023941 100644 --- a/server/services/lib/bing-results-parser.js +++ b/server/services/lib/bing-results-parser.js @@ -8,7 +8,7 @@ async function bingResultsParser (bingData, getIsEngland) { const allowedConfidences = ['medium', 'high'] const allowedTypes = [ - 'populatedplace', 'postcode1', 'postcode3', 'admindivision2', 'neighborhood' + 'populatedplace', 'postcode1', 'postcode3', 'admindivision2', 'neighborhood', 'religiousstructure', 'roadblock' ] const data = set.resources .filter(r => allowedConfidences.includes(r.confidence.toLowerCase())) 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 %}