diff --git a/src/content/entityIssueMessages.csv b/src/content/entityIssueMessages.csv index 50d34491..f35ccbd6 100644 --- a/src/content/entityIssueMessages.csv +++ b/src/content/entityIssueMessages.csv @@ -1,36 +1,36 @@ issue_type,singular_message,plural_message -"future entry date", one entry has a future entry date, {num_entries} entries have a future entry date -"invalid coordinates", one entry has invalid coordinates, {num_entries} entries have invalid coordinates -"invalid date", one entry has an invalid date, {num_entries} entries have invalid dates -"invalid decimal", one entry has an invalid decimal, {num_entries} entries have invalid decimals -"invalid flag", one entry has an invalid flag, {num_entries} entries have invalid flags -"invalid geometry - not fixable", one entry has an invalid geometry that cannot be fixed, {num_entries} entries have invalid geometries that cannot be fixed -"invalid integer", one entry has an invalid integer, {num_entries} entries have invalid integers -"invalid organisation", one entry has an invalid organisation, {num_entries} entries have invalid organisations -"invalid URI", one entry has an invalid URI, {num_entries} entries have invalid URIs -"invalid WKT", one entry has invalid WKT, {num_entries} entries have invalid WKT -"missing value", one entry is missing a value, {num_entities} entities have missing values -"OSGB out of bounds of custom boundary", one entry has OSGB coordinates that are out of bounds of the custom boundary, {num_entries} entries have OSGB coordinates that are out of bounds of the custom boundary -"OSGB out of bounds of England", one entry has OSGB coordinates that are out of bounds of England, {num_entries} entries have OSGB coordinates that are out of bounds of England -"too large", one entry is too large, {num_entries} entries are too large -"too small", one entry is too small, {num_entries} entries are too small -"Unexpected geom type", one entry has an unexpected geometry type, {num_entries} entries have unexpected geometry types -"Unexpected geom type within GeometryCollection", one entry has an unexpected geometry type within a GeometryCollection, {num_entries} entries have unexpected geometry types within GeometryCollections -"unknown entity - missing reference", one entry has an unknown entity with a missing reference, {num_entries} entries have unknown entities with missing references -"WGS84 out of bounds", one entry has WGS84 coordinates that are out of bounds, {num_entries} entries have WGS84 coordinates that are out of bounds -"WGS84 out of bounds of custom boundary", one entry has WGS84 coordinates that are out of bounds of the custom boundary, {num_entries} entries have WGS84 coordinates that are out of bounds of the custom boundary -"WGS84 out of bounds of England", one entry has WGS84 coordinates that are out of bounds of England, {num_entries} entries have WGS84 coordinates that are out of bounds of England -"invalid geometry - fixed", one entry has an invalid geometry that has been fixed, {num_entries} entries have invalid geometries that have been fixed -"invalid type geojson", one entry has an invalid GeoJSON type, {num_entries} entries have invalid GeoJSON types -"Mercator", one entry uses Mercator coordinates, {num_entries} entries use Mercator coordinates -"Mercator flipped", one entry uses flipped Mercator coordinates, {num_entries} entries use flipped Mercator coordinates -"OSGB", one entry uses OSGB coordinates, {num_entries} entries use OSGB coordinates -"OSGB flipped", one entry uses flipped OSGB coordinates, {num_entries} entries use flipped OSGB coordinates -"WGS84 flipped", one entry uses flipped WGS84 coordinates, {num_entries} entries use flipped WGS84 coordinates -"combined-value", one entry has a combined value, {num_entries} entries have combined values -"default-field", one entry has a default value from another field, {num_entries} entries have default values from other fields -"default-value", one entry has a default value, {num_entries} entries have default values -"patch", one entry has been patched, {num_entries} entries have been patched -"removed URI prefix", one entry has a URI prefix that has been removed, {num_entries} entries have URI prefixes that have been removed -"unknown entity", one entry has an unknown entity, {num_entries} entries have unknown entities -"reference values are not unique", one entry has reference values that are not unique, {num_entries} entries have reference values that are not unique +"future entry date", 1 entry has a future entry date, {num_entries} entries have a future entry date +"invalid coordinates", 1 entry has invalid coordinates, {num_entries} entries have invalid coordinates +"invalid date", 1 entry has an invalid date, {num_entries} entries have invalid dates +"invalid decimal", 1 entry has an invalid decimal, {num_entries} entries have invalid decimals +"invalid flag", 1 entry has an invalid flag, {num_entries} entries have invalid flags +"invalid geometry - not fixable", 1 entry has an invalid geometry that cannot be fixed, {num_entries} entries have invalid geometries that cannot be fixed +"invalid integer", 1 entry has an invalid integer, {num_entries} entries have invalid integers +"invalid organisation", 1 entry has an invalid organisation, {num_entries} entries have invalid organisations +"invalid URI", 1 entry has an invalid URI, {num_entries} entries have invalid URIs +"invalid WKT", 1 entry has invalid WKT, {num_entries} entries have invalid WKT +"missing value", 1 entry is missing a value, {num_entries} entities have missing values +"OSGB out of bounds of custom boundary", 1 entry has OSGB coordinates that are out of bounds of the custom boundary, {num_entries} entries have OSGB coordinates that are out of bounds of the custom boundary +"OSGB out of bounds of England", 1 entry has OSGB coordinates that are out of bounds of England, {num_entries} entries have OSGB coordinates that are out of bounds of England +"too large", 1 entry is too large, {num_entries} entries are too large +"too small", 1 entry is too small, {num_entries} entries are too small +"Unexpected geom type", 1 entry has an unexpected geometry type, {num_entries} entries have unexpected geometry types +"Unexpected geom type within GeometryCollection", 1 entry has an unexpected geometry type within a GeometryCollection, {num_entries} entries have unexpected geometry types within GeometryCollections +"unknown entity - missing reference", 1 entry has an unknown entity with a missing reference, {num_entries} entries have unknown entities with missing references +"WGS84 out of bounds", 1 entry has WGS84 coordinates that are out of bounds, {num_entries} entries have WGS84 coordinates that are out of bounds +"WGS84 out of bounds of custom boundary", 1 entry has WGS84 coordinates that are out of bounds of the custom boundary, {num_entries} entries have WGS84 coordinates that are out of bounds of the custom boundary +"WGS84 out of bounds of England", 1 entry has WGS84 coordinates that are out of bounds of England, {num_entries} entries have WGS84 coordinates that are out of bounds of England +"invalid geometry - fixed", 1 entry has an invalid geometry that has been fixed, {num_entries} entries have invalid geometries that have been fixed +"invalid type geojson", 1 entry has an invalid GeoJSON type, {num_entries} entries have invalid GeoJSON types +"Mercator", 1 entry uses Mercator coordinates, {num_entries} entries use Mercator coordinates +"Mercator flipped", 1 entry uses flipped Mercator coordinates, {num_entries} entries use flipped Mercator coordinates +"OSGB", 1 entry uses OSGB coordinates, {num_entries} entries use OSGB coordinates +"OSGB flipped", 1 entry uses flipped OSGB coordinates, {num_entries} entries use flipped OSGB coordinates +"WGS84 flipped", 1 entry uses flipped WGS84 coordinates, {num_entries} entries use flipped WGS84 coordinates +"combined-value", 1 entry has a combined value, {num_entries} entries have combined values +"default-field", 1 entry has a default value from another field, {num_entries} entries have default values from other fields +"default-value", 1 entry has a default value, {num_entries} entries have default values +"patch", 1 entry has been patched, {num_entries} entries have been patched +"removed URI prefix", 1 entry has a URI prefix that has been removed, {num_entries} entries have URI prefixes that have been removed +"unknown entity", 1 entry has an unknown entity, {num_entries} entries have unknown entities +"reference values are not unique", 1 entry has reference values that are not unique, {num_entries} entries have reference values that are not unique diff --git a/src/content/fieldIssueMessages.csv b/src/content/fieldIssueMessages.csv index caa7cfff..1291d884 100644 --- a/src/content/fieldIssueMessages.csv +++ b/src/content/fieldIssueMessages.csv @@ -1,36 +1,36 @@ issue_type,singular_message,plural_message -"future entry date", one field has a future entry date, {num_issues} fields have future entry dates -"invalid coordinates", one field has invalid coordinates, {num_issues} fields have invalid coordinates -"invalid date", one field has an invalid date, {num_issues} fields have invalid dates -"invalid decimal", one field has an invalid decimal, {num_issues} fields have invalid decimals -"invalid flag", one field has an invalid flag, {num_issues} fields have invalid flags -"invalid geometry - not fixable", one field has an invalid geometry that cannot be fixed, {num_issues} fields have invalid geometries that cannot be fixed -"invalid integer", one field has an invalid integer, {num_issues} fields have invalid integers -"invalid organisation", one field has an invalid organisation, {num_issues} fields have invalid organisations -"invalid URI", one field has an invalid URI, {num_issues} fields have invalid URIs -"invalid WKT", one field has invalid WKT, {num_issues} fields have invalid WKT -"missing value", one field is missing a value, {num_issues} fields are missing values -"OSGB out of bounds of custom boundary", one field has OSGB coordinates that are out of bounds of the custom boundary, {num_issues} fields have OSGB coordinates that are out of bounds of the custom boundary -"OSGB out of bounds of England", one field has OSGB coordinates that are out of bounds of England, {num_issues} fields have OSGB coordinates that are out of bounds of England -"too large", one field is too large, {num_issues} fields are too large -"too small", one field is too small, {num_issues} fields are too small -"Unexpected geom type", one field has an unexpected geometry type, {num_issues} fields have unexpected geometry types -"Unexpected geom type within GeometryCollection", one field has an unexpected geometry type within a GeometryCollection, {num_issues} fields have unexpected geometry types within GeometryCollections -"unknown entity - missing reference", one field has an unknown entity with a missing reference, {num_issues} fields have unknown entities with missing references -"WGS84 out of bounds", one field has WGS84 coordinates that are out of bounds, {num_issues} fields have WGS84 coordinates that are out of bounds -"WGS84 out of bounds of custom boundary", one field has WGS84 coordinates that are out of bounds of the custom boundary, {num_issues} fields have WGS84 coordinates that are out of bounds of the custom boundary -"WGS84 out of bounds of England", one field has WGS84 coordinates that are out of bounds of England, {num_issues} fields have WGS84 coordinates that are out of bounds of England -"invalid geometry - fixed", one field has an invalid geometry that has been fixed, {num_issues} fields have invalid geometries that have been fixed -"invalid type geojson", one field has an invalid GeoJSON type, {num_issues} fields have invalid GeoJSON types -"Mercator", one field uses Mercator coordinates, {num_issues} fields use Mercator coordinates -"Mercator flipped", one field uses flipped Mercator coordinates, {num_issues} fields use flipped Mercator coordinates -"OSGB", one field uses OSGB coordinates, {num_issues} fields use OSGB coordinates -"OSGB flipped", one field uses flipped OSGB coordinates, {num_issues} fields use flipped OSGB coordinates -"WGS84 flipped", one field uses flipped WGS84 coordinates, {num_issues} fields use flipped WGS84 coordinates -"combined-value", one field has a combined value, {num_issues} fields have combined values -"default-field", one field has a default value from another field, {num_issues} fields have default values from other fields -"default-value", one field has a default value, {num_issues} fields have default values -"patch", one field has been patched, {num_issues} fields have been patched -"removed URI prefix", one field has a URI prefix that has been removed, {num_issues} fields have URI prefixes that have been removed -"unknown entity", one field has an unknown entity, {num_issues} fields have unknown entities -"reference values are not unique", one field has reference values that are not unique, {num_issues} fields have reference values that are not unique +"future entry date", 1 field has a future entry date, {num_issues} fields have future entry dates +"invalid coordinates", 1 field has invalid coordinates, {num_issues} fields have invalid coordinates +"invalid date", 1 field has an invalid date, {num_issues} fields have invalid dates +"invalid decimal", 1 field has an invalid decimal, {num_issues} fields have invalid decimals +"invalid flag", 1 field has an invalid flag, {num_issues} fields have invalid flags +"invalid geometry - not fixable", 1 field has an invalid geometry that cannot be fixed, {num_issues} fields have invalid geometries that cannot be fixed +"invalid integer", 1 field has an invalid integer, {num_issues} fields have invalid integers +"invalid organisation", 1 field has an invalid organisation, {num_issues} fields have invalid organisations +"invalid URI", 1 field has an invalid URI, {num_issues} fields have invalid URIs +"invalid WKT", 1 field has invalid WKT, {num_issues} fields have invalid WKT +"missing value", 1 field is missing a value, {num_issues} fields are missing values +"OSGB out of bounds of custom boundary", 1 field has OSGB coordinates that are out of bounds of the custom boundary, {num_issues} fields have OSGB coordinates that are out of bounds of the custom boundary +"OSGB out of bounds of England", 1 field has OSGB coordinates that are out of bounds of England, {num_issues} fields have OSGB coordinates that are out of bounds of England +"too large", 1 field is too large, {num_issues} fields are too large +"too small", 1 field is too small, {num_issues} fields are too small +"Unexpected geom type", 1 field has an unexpected geometry type, {num_issues} fields have unexpected geometry types +"Unexpected geom type within GeometryCollection", 1 field has an unexpected geometry type within a GeometryCollection, {num_issues} fields have unexpected geometry types within GeometryCollections +"unknown entity - missing reference", 1 field has an unknown entity with a missing reference, {num_issues} fields have unknown entities with missing references +"WGS84 out of bounds", 1 field has WGS84 coordinates that are out of bounds, {num_issues} fields have WGS84 coordinates that are out of bounds +"WGS84 out of bounds of custom boundary", 1 field has WGS84 coordinates that are out of bounds of the custom boundary, {num_issues} fields have WGS84 coordinates that are out of bounds of the custom boundary +"WGS84 out of bounds of England", 1 field has WGS84 coordinates that are out of bounds of England, {num_issues} fields have WGS84 coordinates that are out of bounds of England +"invalid geometry - fixed", 1 field has an invalid geometry that has been fixed, {num_issues} fields have invalid geometries that have been fixed +"invalid type geojson", 1 field has an invalid GeoJSON type, {num_issues} fields have invalid GeoJSON types +"Mercator", 1 field uses Mercator coordinates, {num_issues} fields use Mercator coordinates +"Mercator flipped", 1 field uses flipped Mercator coordinates, {num_issues} fields use flipped Mercator coordinates +"OSGB", 1 field uses OSGB coordinates, {num_issues} fields use OSGB coordinates +"OSGB flipped", 1 field uses flipped OSGB coordinates, {num_issues} fields use flipped OSGB coordinates +"WGS84 flipped", 1 field uses flipped WGS84 coordinates, {num_issues} fields use flipped WGS84 coordinates +"combined-value", 1 field has a combined value, {num_issues} fields have combined values +"default-field", 1 field has a default value from another field, {num_issues} fields have default values from other fields +"default-value", 1 field has a default value, {num_issues} fields have default values +"patch", 1 field has been patched, {num_issues} fields have been patched +"removed URI prefix", 1 field has a URI prefix that has been removed, {num_issues} fields have URI prefixes that have been removed +"unknown entity", 1 field has an unknown entity, {num_issues} fields have unknown entities +"reference values are not unique", 1 field has reference values that are not unique, {num_issues} fields have reference values that are not unique diff --git a/src/controllers/OrganisationsController.js b/src/controllers/OrganisationsController.js index 1132b68e..9ff6adeb 100644 --- a/src/controllers/OrganisationsController.js +++ b/src/controllers/OrganisationsController.js @@ -12,6 +12,28 @@ const availableDatasets = Object.values(dataSubjects) .map(dataset => dataset.value) ) +/** + * Returns a status tag object with a text label and a CSS class based on the status. + * + * @param {string} status - The status to generate a tag for (e.g. "Error", "Needs fixing", etc.) + * @returns {object} - An object with a `tag` property containing the text label and CSS class. + */ +function getStatusTag (status) { + const statusToTagClass = { + Error: 'govuk-tag--red', + 'Needs fixing': 'govuk-tag--yellow', + Warning: 'govuk-tag--blue', + Issue: 'govuk-tag--blue' + } + + return { + tag: { + text: status, + classes: statusToTagClass[status] + } + } +} + const organisationsController = { /** * Get LPA overview data and render the overview page @@ -103,9 +125,18 @@ const organisationsController = { } }, + /** + * Handles GET requests for the "Get Started" page. + * + * @param {Express.Request} req - The incoming request object. + * @param {Express.Response} res - The response object to send back to the client. + * @param {Express.NextFunction} next - The next function in the middleware chain. + * + * Retrieves the organisation and dataset names from the database and renders the "Get Started" page with the organisation and dataset details. + */ async getGetStarted (req, res, next) { try { - // get the organisation name + // get the organisation name const lpa = req.params.lpa const organisationResult = await datasette.runQuery(`SELECT name FROM organisation WHERE organisation = '${lpa}'`) const organisation = organisationResult.formattedData[0] @@ -127,6 +158,16 @@ const organisationsController = { } }, + /** + * Handles GET requests for the dataset task list page. + * + * @param {Express.Request} req - The incoming request object. + * @param {Express.Response} res - The response object to send back to the client. + * @param {Express.NextFunction} next - The next function in the middleware chain. + * + * Retrieves the organisation and dataset names from the database, fetches the issues for the given LPA and dataset, + * and renders the dataset task list page with the list of tasks and organisation and dataset details. + */ async getDatasetTaskList (req, res, next) { const lpa = req.params.lpa const datasetId = req.params.dataset @@ -140,7 +181,15 @@ const organisationsController = { const issues = await performanceDbApi.getLpaDatasetIssues(lpa, datasetId) - const taskList = performanceDbApi.getTaskList(issues) + const taskList = issues.map((issue) => { + return { + title: { + text: performanceDbApi.getTaskMessage(issue.issue_type, issue.num_issues) + }, + href: `/organisations/${lpa}/${datasetId}/${issue.issue_type}`, + status: getStatusTag(issue.status) + } + }) const params = { taskList, @@ -155,72 +204,124 @@ const organisationsController = { } }, + /** + * Handles GET requests for the issue details page. + * + * @param {Express.Request} req - The incoming request object. + * @param {Express.Response} res - The response object to send back to the client. + * @param {Express.NextFunction} next - The next function in the middleware chain. + * + * Retrieves the organisation, dataset, and issue details from the database, and renders the issue details page + * with the list of issues, entry data, and organisation and dataset details. + * + * @throws {Error} If there is an error fetching the data or rendering the page. + */ async getIssueDetails (req, res, next) { const { lpa, dataset: datasetId, issue_type: issueType } = req.params + let { resourceId, entityNumber } = req.params - const organisationResult = await datasette.runQuery(`SELECT name FROM organisation WHERE organisation = '${lpa}'`) - const organisation = organisationResult.formattedData[0] - - const datasetResult = await datasette.runQuery(`SELECT name FROM dataset WHERE dataset = '${datasetId}'`) - const dataset = datasetResult.formattedData[0] + try { + entityNumber = entityNumber ? parseInt(entityNumber) : 1 - const issueCount = 5 + const organisationResult = await datasette.runQuery(`SELECT name FROM organisation WHERE organisation = '${lpa}'`) + const organisation = organisationResult.formattedData[0] - const errorHeading = performanceDbApi.getTaskMessage(issueType, issueCount, true) + const datasetResult = await datasette.runQuery(`SELECT name FROM dataset WHERE dataset = '${datasetId}'`) + const dataset = datasetResult.formattedData[0] - const issueItems = [ - { - html: '2 fields are missing values in entry 949', - href: 'todo' - }, - { - html: '3 fields are missing values in entry 950', - href: 'todo' + if (!resourceId) { + const resource = await performanceDbApi.getLatestResource(lpa, datasetId) + resourceId = resource.resource } - ] - const entry = { - title: '20 and 20A Whitbourne Springs', - fields: [ - { - key: { - text: 'description' - }, - value: { - html: '20 and 20A Whitbourne Springs' - }, - classes: '' - }, - { - key: { - text: 'document-url' - }, - value: { - html: '
' - }, - classes: 'dl-summary-card-list__row--error' - }, - { + const issues = await performanceDbApi.getIssues(resourceId, issueType, datasetId) + + const issuesByEntryNumber = issues.reduce((acc, current) => { + acc[current.entry_number] = acc[current.entry_number] || [] + acc[current.entry_number].push(current) + return acc + }, {}) + + const errorHeading = performanceDbApi.getTaskMessage(issueType, Object.keys(issuesByEntryNumber).length, true) + + const issueItems = Object.entries(issuesByEntryNumber).map(([entryNumber, issues]) => { + return { + html: performanceDbApi.getTaskMessage(issueType, issues.length) + ` in record ${entryNumber}`, + href: `/organisations/${lpa}/${datasetId}/${issueType}/${entryNumber}` + } + }) + + const entryData = await performanceDbApi.getEntry(resourceId, entityNumber, datasetId) + + const fields = entryData.map((row) => { + let hasError = false + let issueIndex + if (issuesByEntryNumber[entityNumber]) { + issueIndex = issuesByEntryNumber[entityNumber].findIndex(issue => issue.field === row.field) + if (issueIndex >= 0) { + hasError = true + } + } + + let valueHtml = '' + let classes = '' + if (hasError) { + const message = issuesByEntryNumber[entityNumber][issueIndex].message || issueType + valueHtml += ` ` + classes += 'dl-summary-card-list__row--error' + } + valueHtml += row.value + + return { key: { - text: 'documentation-url' + text: row.field }, value: { - html: ' ' + html: valueHtml }, - classes: 'dl-summary-card-list__row--error' + classes } - ] - } + }) - const params = { - organisation, - dataset, - errorHeading, - issueItems, - entry - } + if (issuesByEntryNumber[entityNumber]) { + issuesByEntryNumber[entityNumber].forEach((issue) => { + if (!fields.find(field => field.key.text === issue.field)) { + const errorMessage = issue.message || issueType + + const valueHtml = ` ${issue.value}` + const classes = 'dl-summary-card-list__row--error' + + fields.push({ + key: { + text: issue.field + }, + value: { + html: valueHtml + }, + classes + }) + } + }) + } + + const entry = { + title: `entry: ${entityNumber}`, + fields + } - res.render('organisations/issueDetails.html', params) + const params = { + organisation, + dataset, + errorHeading, + issueItems, + entry + } + + res.render('organisations/issueDetails.html', params) + } catch (e) { + logger.warn(`getIssueDetails() failed for lpa='${lpa}', datasetId='${datasetId}', issue=${issueType}, entityNumber=${entityNumber}, resourceId=${resourceId}`, { type: types.App }) + next(e) + } } } diff --git a/src/routes/organisations.js b/src/routes/organisations.js index 4268ece3..5a015fff 100644 --- a/src/routes/organisations.js +++ b/src/routes/organisations.js @@ -4,6 +4,7 @@ import OrganisationsController from '../controllers/OrganisationsController.js' const router = express.Router() router.get('/:lpa/:dataset/get-started', OrganisationsController.getGetStarted) +router.get('/:lpa/:dataset/:issue_type/:entityNumber', OrganisationsController.getIssueDetails) router.get('/:lpa/:dataset/:issue_type', OrganisationsController.getIssueDetails) router.get('/:lpa/:dataset', OrganisationsController.getDatasetTaskList) router.get('/:lpa', OrganisationsController.getOverview) diff --git a/src/services/datasette.js b/src/services/datasette.js index 85018e24..0048269b 100644 --- a/src/services/datasette.js +++ b/src/services/datasette.js @@ -2,7 +2,6 @@ import axios from 'axios' import logger from '../utils/logger.js' const datasetteUrl = 'https://datasette.planning.data.gov.uk' -const database = 'digital-land' export default { /** @@ -14,7 +13,7 @@ export default { * - `formattedData`: The formatted data, with columns and rows parsed into a usable format. * @throws {Error} If the query fails or there is an error communicating with Datasette. */ - runQuery: async (query) => { + runQuery: async (query, database = 'digital-land') => { const encodedQuery = encodeURIComponent(query) const url = `${datasetteUrl}/${database}.json?sql=${encodedQuery}` try { diff --git a/src/services/performanceDbApi.js b/src/services/performanceDbApi.js index a2c6278d..807d35ae 100644 --- a/src/services/performanceDbApi.js +++ b/src/services/performanceDbApi.js @@ -29,30 +29,14 @@ fs.createReadStream('src/content/entityIssueMessages.csv') .on('data', (row) => { messages[row.issue_type] = { ...messages[row.issue_type], - entities_singular: row.singular_message.replace('{num_entities}', '{}'), - entities_plural: row.plural_message.replace('{num_entities}', '{}') + entities_singular: row.singular_message.replace('{num_entries}', '{}'), + entities_plural: row.plural_message.replace('{num_entries}', '{}') } }) .on('end', () => { // Messages object is now populated }) -function getStatusTag (status) { - const statusToTagClass = { - Error: 'govuk-tag--red', - 'Needs fixing': 'govuk-tag--yellow', - Warning: 'govuk-tag--blue', - Issue: 'govuk-tag--blue' - } - - return { - tag: { - text: status, - classes: statusToTagClass[status] - } - } -} - // =========================================== /** @@ -214,18 +198,6 @@ ORDER BY }) }, - getTaskList: (issues) => { - return issues.map((issue) => { - return { - title: { - text: this.getTaskMessage(issue.issue_type, issue.num_issues) - }, - href: 'toDo', - status: getStatusTag(issue.status) - } - }) - }, - getTaskMessage (issueType, issueCount, entityLevel = false) { if (!messages[issueType]) { throw new Error(`Unknown issue type: ${issueType}`) @@ -238,5 +210,61 @@ ORDER BY message = issueCount === 1 ? messages[issueType].singular : messages[issueType].plural } return message.replace('{}', issueCount) + }, + + async getLatestResource (lpa, dataset) { + const sql = ` + SELECT rle.resource, rle.status, rle.endpoint, rle.endpoint_url, rle.status, rle.days_since_200, rle.exception + FROM reporting_latest_endpoints rle + LEFT JOIN resource_organisation ro ON rle.resource = ro.resource + LEFT JOIN organisation o ON REPLACE(ro.organisation, '-eng', '') = o.organisation + WHERE REPLACE(ro.organisation, '-eng', '') = '${lpa}' + AND rle.pipeline = '${dataset}'` + + const result = await datasette.runQuery(sql) + + return result.formattedData[0] + }, + + async getIssues (resource, issueType, database = 'digital-land') { + const sql = ` + SELECT i.field, i.line_number, entry_number, message, issue_type, value + FROM issue i + WHERE i.resource = '${resource}' + AND issue_type = '${issueType}' + ` + + const result = await datasette.runQuery(sql, database) + + return result.formattedData + }, + + async getEntry (resourceId, entryNumber, dataset) { + const sql = ` + select + fr.rowid, + fr.end_date, + fr.fact, + fr.entry_date, + fr.entry_number, + fr.resource, + fr.start_date, + ft.entity, + ft.field, + ft.entry_date, + ft.start_date, + ft.value + from + fact_resource fr + left join fact ft on fr.fact = ft.fact + where + fr.resource = '${resourceId}' + and fr.entry_number = ${entryNumber} + order by + fr.rowid` + + const result = await datasette.runQuery(sql, dataset) + + return result.formattedData } } diff --git a/test/unit/organisationsController.test.js b/test/unit/organisationsController.test.js index d3994870..58e4932c 100644 --- a/test/unit/organisationsController.test.js +++ b/test/unit/organisationsController.test.js @@ -206,16 +206,32 @@ describe('OrganisationsController.js', () => { }) vi.mocked(performanceDbApi.getLpaDatasetIssues).mockResolvedValue([ - { issue: 'Example issue' } + { + issue: 'Example issue 1', + issue_type: 'Example issue type 1', + num_issues: 1, + status: 'Error' + } ]) - vi.mocked(performanceDbApi.getTaskList).mockReturnValue([{ task: 'Example task' }]) + vi.mocked(performanceDbApi.getTaskMessage).mockReturnValueOnce('task message 1') await organisationsController.getDatasetTaskList(req, res, next) expect(res.render).toHaveBeenCalledTimes(1) expect(res.render).toHaveBeenCalledWith('organisations/datasetTaskList.html', { - taskList: [{ task: 'Example task' }], + taskList: [{ + title: { + text: 'task message 1' + }, + href: '/organisations/example-lpa/example-dataset/Example issue type 1', + status: { + tag: { + classes: 'govuk-tag--red', + text: 'Error' + } + } + }], organisation: { name: 'Example Organisation' }, dataset: { name: 'Example Dataset' } }) @@ -233,22 +249,51 @@ describe('OrganisationsController.js', () => { }) vi.mocked(performanceDbApi.getLpaDatasetIssues).mockResolvedValue([ - { issue: 'Example issue 1' }, - { issue: 'Example issue 2' } + { + issue: 'Example issue 1', + issue_type: 'Example issue type 1', + num_issues: 1, + status: 'Error' + }, + { + issue: 'Example issue 2', + issue_type: 'Example issue type 2', + num_issues: 1, + status: 'Needs fixing' + } ]) - vi.mocked(performanceDbApi.getTaskList).mockReturnValue([ - { task: 'Example task 1' }, - { task: 'Example task 2' } - ]) + vi.mocked(performanceDbApi.getTaskMessage).mockReturnValueOnce('task message 1').mockReturnValueOnce('task message 2') await organisationsController.getDatasetTaskList(req, res, next) expect(res.render).toHaveBeenCalledTimes(1) expect(res.render).toHaveBeenCalledWith('organisations/datasetTaskList.html', { taskList: [ - { task: 'Example task 1' }, - { task: 'Example task 2' } + { + title: { + text: 'task message 1' + }, + href: '/organisations/example-lpa/example-dataset/Example issue type 1', + status: { + tag: { + classes: 'govuk-tag--red', + text: 'Error' + } + } + }, + { + title: { + text: 'task message 2' + }, + href: '/organisations/example-lpa/example-dataset/Example issue type 2', + status: { + tag: { + classes: 'govuk-tag--yellow', + text: 'Needs fixing' + } + } + } ], organisation: { name: 'Example Organisation' }, dataset: { name: 'Example Dataset' } @@ -273,10 +318,101 @@ describe('OrganisationsController.js', () => { }) describe('issue details', () => { - it.todo('should call render with the issue details page') + it('should call render with the issue details page and the correct params', async () => { + const req = { + params: { + lpa: 'test-lpa', + dataset: 'test-dataset', + issue_type: 'test-issue-type', + resourceId: 'test-resource-id', + entityNumber: '1' + } + } + const res = { + render: vi.fn() + } + const next = vi.fn() + + vi.mocked(datasette.runQuery) + .mockReturnValueOnce({ formattedData: [{ name: 'mock lpa' }] }) + .mockReturnValueOnce({ formattedData: [{ name: 'mock dataset' }] }) + + vi.mocked(performanceDbApi.getLatestResource).mockResolvedValueOnce({ resource: 'mockResourceId' }) + + const issues = [ + { + entry_number: 0, + field: 'start-date', + value: '02-02-2022' + } + ] + + vi.mocked(performanceDbApi.getIssues).mockResolvedValueOnce(issues) + + vi.mocked(performanceDbApi.getTaskMessage).mockReturnValueOnce('mock task message 1') + + issues.forEach(issue => { + vi.mocked(performanceDbApi.getTaskMessage).mockReturnValueOnce(`mockMessageFor: ${issue.entry_number}`) + }) + + vi.mocked(performanceDbApi.getEntry).mockResolvedValueOnce([ + { + field: 'start-date', + value: '02-02-2022' + } + ]) + + await organisationsController.getIssueDetails(req, res, next) + + expect(res.render).toHaveBeenCalledTimes(1) + expect(res.render).toHaveBeenCalledWith('organisations/issueDetails.html', { + organisation: { + name: 'mock lpa' + }, + dataset: { + name: 'mock dataset' + }, + errorHeading: 'mock task message 1', + issueItems: [ + { + html: 'mockMessageFor: 0 in record 0', + href: '/organisations/test-lpa/test-dataset/test-issue-type/0' + } + ], + entry: { + title: 'entry: 1', + fields: [ + { + key: { text: 'start-date' }, + value: { html: '02-02-2022' }, + classes: '' + } + ] + } + }) + }) - it.todo('should fetch the issue details and pass the on to the template') + it('should catch errors and pass them onto the next function', async () => { + const req = { + params: { + lpa: 'test-lpa', + dataset: 'test-dataset', + issue_type: 'test-issue-type', + resourceId: 'test-resource-id', + entityNumber: '1' + } + } + const res = { + render: vi.fn() + } + const next = vi.fn() - it.todo('should catch errors and pass them onto the next function ') + vi.mocked(performanceDbApi.getLatestResource).mockRejectedValue(new Error('Test error')) + + await organisationsController.getIssueDetails(req, res, next) + + expect(next).toHaveBeenCalledTimes(1) + expect(next).toHaveBeenCalledWith(expect.any(Error)) + }) }) })