Skip to content

Commit

Permalink
Merge pull request #701 from DEFRA/feature/FSR-1207
Browse files Browse the repository at this point in the history
FSR-1207 | alerts and warnings search
  • Loading branch information
ashleyjtaylor committed Jun 26, 2024
2 parents 80e4766 + 7048cfb commit 0c7c827
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 245 deletions.
41 changes: 22 additions & 19 deletions server/models/views/alerts-and-warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
211 changes: 147 additions & 64 deletions server/routes/alerts-and-warnings.js
Original file line number Diff line number Diff line change
@@ -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
}
}
}]
2 changes: 1 addition & 1 deletion server/services/lib/bing-results-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
2 changes: 1 addition & 1 deletion server/views/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@


{% if metaCanonical %}
<link rel="canonical" href="{{fullUrl}}"/>
<link rel="canonical" href="{{ model.canonicalUrl if model.canonicalUrl else fullUrl }}"/>
{% endif %}

<script>
Expand Down
2 changes: 1 addition & 1 deletion server/views/location-not-found.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h1 class="govuk-heading-xl">
<li>check the spelling</li>
<li>enter a broader location</li>
</ul>
<p class="govuk-body"><a href="/">Go back to search a location</a></p>
<p class="govuk-body"><a href="{{ href if href else '/' }}">Go back to search a location</a></p>
<p class="govuk-body">If you want to search for a place outside England, you should go to:</p>
<ul class="govuk-list govuk-list--bullet">
<li><a href="https://www.sepa.org.uk/environment/water/flooding/">Scottish Environment Protection Agency</a></li>
Expand Down
2 changes: 1 addition & 1 deletion test/models/alerts-and-warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ lab.test('Test exposed data placeBox is blank if not a place in England', async
const Result = await new ViewModel({ location, place, floods, station })

Code.expect(Result.expose.placeBbox.length).to.equal(0)
Code.expect(Result.pageTitle).to.equal('Flood alerts and warnings')
Code.expect(Result.pageTitle).to.equal('Kinghorn - flood alerts and warnings')
})

lab.test('Test count floods function returns correct number of floods', async () => {
Expand Down
Loading

0 comments on commit 0c7c827

Please sign in to comment.