diff --git a/config/default.yaml b/config/default.yaml
index a96168be..d45fa93d 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -4,5 +4,6 @@ logs:
app: false
error: false
api: {
- url: http://localhost:3000/api
+ url: http://127.0.0.1:8082,
+ validationEndpoint: /api/dataset/validate/file/request/
}
diff --git a/src/assets/scss/_scrollable-container.scss b/src/assets/scss/_scrollable-container.scss
new file mode 100644
index 00000000..6f3b4475
--- /dev/null
+++ b/src/assets/scss/_scrollable-container.scss
@@ -0,0 +1,79 @@
+.app-scrollable-container {
+
+ table {
+ margin-bottom: 20px;
+ th, td {
+ white-space: nowrap;
+ }
+
+ td.app-wrap {
+ // white-space: unset;
+ max-width: 500px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .app-inset-text__value {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+
+ max-height: 500px;
+ @include govuk-responsive-margin(6, $direction: "bottom");
+ padding-left: 15px;
+ padding-right: 15px;
+ overflow-x: auto;
+ border: 1px solid rgb(169, 169, 169);
+ background-image:
+ // Shadows
+ linear-gradient(to right, white, white),
+ linear-gradient(to right, white, white),
+
+ // Shadow covers
+ linear-gradient(to right, rgba(0,0,0,.25), rgba(255,255,255,0)),
+ linear-gradient(to left, rgba(0,0,0,.25), rgba(255,255,255,0));
+
+ background-position: left center, right center, left center, right center;
+ background-repeat: no-repeat;
+ background-color: white;
+ background-size: 20px 100%, 20px 100%, 10px 100%, 10px 100%;
+
+ /* Opera doesn't support this in the shorthand */
+ background-attachment: local, local, scroll, scroll;
+
+ scrollbar-width: thin;
+ scrollbar-color: #8A8A8A #DFE9EB;
+
+ /* Chrome, Edge and Safari */
+ &::-webkit-scrollbar {
+ height: 10px;
+ width: 10px;
+ }
+ &::-webkit-scrollbar-track {
+ border-radius: 5px;
+ background-color: #e8e8e8;
+ }
+
+ &::-webkit-scrollbar-track:hover {
+ background-color: #B8C0C2;
+ }
+
+ &::-webkit-scrollbar-track:active {
+ background-color: #B8C0C2;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: 0px;
+ background-color: $govuk-border-colour;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background-color: #5F5F5F;
+ }
+
+ &::-webkit-scrollbar-thumb:active {
+ background-color: #474D54;
+ }
+
+}
\ No newline at end of file
diff --git a/src/assets/scss/index.scss b/src/assets/scss/index.scss
index a6ceec3f..081eca0b 100644
--- a/src/assets/scss/index.scss
+++ b/src/assets/scss/index.scss
@@ -1,2 +1,37 @@
@import "node_modules/govuk-frontend/govuk/all";
-@import "node_modules/@x-govuk/govuk-prototype-components/x-govuk/all";
\ No newline at end of file
+@import "node_modules/@x-govuk/govuk-prototype-components/x-govuk/all";
+@import "src/assets/scss/_scrollable-container.scss";
+
+.app-inset-text---error {
+ border-left: 5px solid govuk-colour('red');
+ padding: govuk-spacing(1) govuk-spacing(2);
+ margin: 0;
+
+ .app-inset-text__value {
+ margin-bottom: govuk-spacing(1);
+ }
+
+ .app-inset-text__error {
+ color: govuk-colour('red');
+ font-weight: bold;
+ }
+
+
+ }
+
+ .app-inset-text---warning {
+ border-left: 5px solid govuk-colour('blue');
+ padding: govuk-spacing(1) govuk-spacing(2);
+ margin: 0;
+
+ .app-inset-text__value {
+ margin-bottom: govuk-spacing(1);
+ }
+
+ .app-inset-text__warning {
+ color: govuk-colour('blue');
+ font-weight: bold;
+ }
+
+
+ }
\ No newline at end of file
diff --git a/src/controllers/errorsController.js b/src/controllers/errorsController.js
new file mode 100644
index 00000000..9026b102
--- /dev/null
+++ b/src/controllers/errorsController.js
@@ -0,0 +1,56 @@
+'use strict'
+
+const { Controller } = require('hmpo-form-wizard')
+
+class ErrorsController extends Controller {
+ get (req, res, next) {
+ const json = req.sessionModel.get('validationResult')
+
+ const aggregatedIssues = {}
+ const issueCounts = {}
+
+ json['issue-log'].forEach(issue => {
+ const entryNumber = issue['entry-number']
+
+ const rowColumns = json['converted-csv'][issue['line-number'] - 1]
+ if (!(entryNumber in aggregatedIssues)) {
+ aggregatedIssues[entryNumber] = Object.keys(rowColumns).reduce((acc, key) => {
+ acc[key] = {
+ error: false,
+ value: rowColumns[key]
+ }
+ return acc
+ }, {})
+ }
+
+ if (entryNumber in aggregatedIssues) {
+ aggregatedIssues[entryNumber][issue.field] = {
+ error: this.lookupIssueType(issue['issue-type']),
+ value: rowColumns[issue.field]
+ }
+ issueCounts[issue.field] = issueCounts[issue.field] ? issueCounts[issue.field] + 1 : 1
+ }
+ })
+
+ const rows = Object.keys(aggregatedIssues).map(key => {
+ return {
+ entryNumber: key,
+ columns: aggregatedIssues[key]
+ }
+ })
+
+ req.form.options.rows = rows
+ req.form.options.issueCounts = issueCounts
+ req.form.options.dataset = req.sessionModel.get('dataset')
+ req.form.options.dataSubject = req.sessionModel.get('data-subject')
+
+ super.get(req, res, next)
+ }
+
+ lookupIssueType (issueType) {
+ // this needs to be implemented once we know what the issue types are
+ return issueType
+ }
+}
+
+module.exports = ErrorsController
diff --git a/src/controllers/uploadController.js b/src/controllers/uploadController.js
index 01c57bc9..fc00d304 100644
--- a/src/controllers/uploadController.js
+++ b/src/controllers/uploadController.js
@@ -7,7 +7,9 @@ const { Controller } = require('hmpo-form-wizard')
const { readFile } = require('node:fs/promises')
const { lookup } = require('mime-types')
-const apiRoute = 'http://127.0.0.1:8082/api/dataset/validate/file/request/'
+const config = require('../../config')
+
+const apiRoute = config.api.url + config.api.validationEndpoint
class UploadController extends Controller {
middlewareSetup () {
@@ -35,10 +37,8 @@ class UploadController extends Controller {
const json = await result.json()
- console.log(json)
-
- // send the response back to the user
- res.send(json)
+ req.sessionModel.set('validationResult', json)
+ super.post(req, res, next)
} catch (e) {
res.send(e)
}
diff --git a/src/routes/form-wizard/fields.js b/src/routes/form-wizard/fields.js
index 1a97b402..7d8a3c7f 100644
--- a/src/routes/form-wizard/fields.js
+++ b/src/routes/form-wizard/fields.js
@@ -7,5 +7,8 @@ module.exports = {
},
datafile: {
validate: 'required'
+ },
+ validationResult: {
+ validate: 'required'
}
}
diff --git a/src/routes/form-wizard/steps.js b/src/routes/form-wizard/steps.js
index 0a77de9a..6a066ded 100644
--- a/src/routes/form-wizard/steps.js
+++ b/src/routes/form-wizard/steps.js
@@ -16,7 +16,10 @@ module.exports = {
},
'/upload': {
controller: require('../../controllers/uploadController'),
- fields: ['datafile', 'path'],
- next: 'done'
+ next: 'errors'
+ },
+ '/errors': {
+ controller: require('../../controllers/errorsController'),
+ next: 'transformations'
}
}
diff --git a/src/views/errors.html b/src/views/errors.html
new file mode 100644
index 00000000..1cae3853
--- /dev/null
+++ b/src/views/errors.html
@@ -0,0 +1,141 @@
+{% extends "layouts/main.html" %}
+
+{% from 'govuk/components/back-link/macro.njk' import govukBackLink %}
+{% from 'govuk/components/button/macro.njk' import govukButton %}
+{% from 'govuk/components/radios/macro.njk' import govukRadios %}
+{% from 'govuk/components/inset-text/macro.njk' import govukInsetText %}
+
+{% set pageName = 'There’s a problem with your data' %}
+
+{% block beforeContent %}
+ {{ govukBackLink({
+ text: "Back",
+ href: "javascript:window.history.back()"
+ }) }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+ {% if options.dataset %}
+ {{options.dataset}}
+ {% else %}
+ {{options.dataSubject}}
+ {% endif %}
+
+
+ {{pageName}}
+
+
+
+ {% for issue, count in options.issueCounts %}
+ - {{count}} issue, relating to {{issue}}
+ {% endfor %}
+
+
+
+
+
+
+
+ Records with errors
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/test/acceptance/upload_data.test.js b/test/acceptance/upload_data.test.js
index f3b8c8b1..accd4261 100644
--- a/test/acceptance/upload_data.test.js
+++ b/test/acceptance/upload_data.test.js
@@ -1,6 +1,6 @@
import { test } from '@playwright/test'
-test('Upload data', async ({ page }) => {
+test('Enter form information', async ({ page }) => {
await page.goto('/')
await page.getByRole('button', { name: 'Start now' }).click()
@@ -20,5 +20,6 @@ test('Upload data', async ({ page }) => {
await page.getByText('Upload data').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles('test/testData/conservation-area.csv')
+
await page.getByRole('button', { name: 'Continue' }).click()
})
diff --git a/test/testData/API_RUN_PIPELINE_RESPONSE.json b/test/testData/API_RUN_PIPELINE_RESPONSE.json
new file mode 100644
index 00000000..bff90878
--- /dev/null
+++ b/test/testData/API_RUN_PIPELINE_RESPONSE.json
@@ -0,0 +1,23 @@
+{
+ "converted-csv": [
+ {"Reference": "CA6", "Name": "Camden Square", "Geometry": "POLYGON ((-0.125888391245 51.54316508186, -0.125891457623 51.543177267548, -0.125903428774 51.54322160042))", "Start date": "01/04/1980", "Legislation": "", "Notes": "", "Point": "POINT (-0.130484959448 51.544845663239)", "End date": "", "Document URL": "https://www.camden.gov.uk/camden-square-conservation-area-appraisal-and-management-strategy"},
+ {"Reference": "CA20", "Name": "Holly Lodge Estate", "Geometry": "POLYGON ((-0.125888391245 51.54316508186, -0.125891457623 51.543177267548, -0.125903428774 51.54322160042))", "Start date": "01/06/1992", "Legislation": "", "Notes": "", "Point": "POINT (-0.150097204178 51.564975754948)", "End date": "", "Document URL": "https://www.camden.gov.uk/holly-lodge-conservation-area"},
+ {"Reference": "CA9", "Name": "Dartmouth Park", "Geometry": "POLYGON ((-0.125888391245 51.54316508186, -0.125891457623 51.543177267548, -0.125903428774 51.54322160042))", "Start date": "01/06/1992", "Legislation": "", "Notes": "", "Point": "POINT (-0.145442349961 51.559999511433)", "End date": "", "Document URL": "https://www.camden.gov.uk/dartmouth-park-conservation-area-appraisal-and-management-strategy"}
+ ],
+ "issue-log": [
+ {"dataset": "conservation-area", "resource": "0b4284077da580a6daea59ee2227f9c7c55a9a45d57ef470d82418a4391ddf9a", "line-number": "2", "entry-number": "1", "field": "geometry", "issue-type": "OSGB", "value": ""},
+ {"dataset": "conservation-area", "resource": "0b4284077da580a6daea59ee2227f9c7c55a9a45d57ef470d82418a4391ddf9a", "line-number": "2", "entry-number": "1", "field": "organisation", "issue-type": "default-value", "value": "local-authority-eng:MAL"},
+ {"dataset": "conservation-area", "resource": "0b4284077da580a6daea59ee2227f9c7c55a9a45d57ef470d82418a4391ddf9a", "line-number": "2", "entry-number": "1", "field": "entry-date", "issue-type": "default-value", "value": "2020-09-14"}
+ ],
+ "flattened-csv": {
+ "entities": [
+ {"dataset": "conservation-area", "end-date": "", "entity": "44000001", "entry-date": "2022-04-12", "geometry": "MULTIPOLYGON (((-0.307721 51.724964,-0.307831 51.725040,-0.307919 51.725121,-0.309609 51.724308,-0.309719 51.724396)))", "name": "Shafford Mill", "organisation-entity": "16", "point": "POINT(-0.370182 51.770914)", "prefix": "conservation-area", "reference": "5071", "start-date": "1980-07-31", "typology": "geography"},
+ {"dataset": "conservation-area", "end-date": "", "entity": "44000002", "entry-date": "2022-04-12", "geometry": "MULTIPOLYGON (((-0.372417 51.774343,-0.372532 51.774138,-0.372688 51.773884,-0.372776 51.773857,-0.372692 51.773743)))", "name": "Potters Crouch", "organisation-entity": "16", "point": "POINT(-0.383726 51.734475)", "prefix": "conservation-area", "reference": "5074", "start-date": "1977-07-27", "typology": "geography"}
+ ]
+ },
+ "column-field-log": [
+ {"entry-date": "2023-10-03T10:13:45Z", "dataset": "conservation-area", "resource": "0b4284077da580a6daea59ee2227f9c7c55a9a45d57ef470d82418a4391ddf9a", "column": "WKT", "field": "geometry"},
+ {"entry-date": "2023-10-03T10:13:45Z", "dataset": "conservation-area", "resource": "0b4284077da580a6daea59ee2227f9c7c55a9a45d57ef470d82418a4391ddf9a", "column": "details", "field": "name"},
+ {"entry-date": "2023-10-03T10:13:45Z", "dataset": "conservation-area", "resource": "0b4284077da580a6daea59ee2227f9c7c55a9a45d57ef470d82418a4391ddf9a", "column": "ogc_fid", "field": "reference"}
+ ]
+}
diff --git a/test/unit/errorsController.test.js b/test/unit/errorsController.test.js
new file mode 100644
index 00000000..01f461c6
--- /dev/null
+++ b/test/unit/errorsController.test.js
@@ -0,0 +1,102 @@
+import ErrorsController from '../../src/controllers/errorsController.js'
+
+import { describe, it, expect, vi } from 'vitest'
+
+import mockApiValue from '../testData/API_RUN_PIPELINE_RESPONSE.json'
+
+describe('ErrorsController', () => {
+ const options = {
+ route: '/errors'
+ }
+ const errorsController = new ErrorsController(options)
+
+ it('correctly serves the errors page when the session data is correctly set', async () => {
+ const session = {
+ validationResult: mockApiValue,
+ dataset: 'test-dataset',
+ 'data-subject': 'test-data-subject'
+ }
+ const req = {
+ sessionModel: {
+ get: (key) => session[key]
+ },
+ form: {
+ options: {
+ }
+ }
+ }
+ const res = {}
+ const next = vi.fn()
+
+ await errorsController.get(req, res, next)
+
+ const expectedFormValues = {
+ options: {
+ rows: [
+ {
+ entryNumber: '1',
+ columns: {
+ 'Document URL': {
+ error: false,
+ value: 'https://www.camden.gov.uk/holly-lodge-conservation-area'
+ },
+ 'End date': {
+ error: false,
+ value: ''
+ },
+ Geometry: {
+ error: false,
+ value: 'POLYGON ((-0.125888391245 51.54316508186, -0.125891457623 51.543177267548, -0.125903428774 51.54322160042))'
+ },
+ Legislation: {
+ error: false,
+ value: ''
+ },
+ Name: {
+ error: false,
+ value: 'Holly Lodge Estate'
+ },
+ Notes: {
+ error: false,
+ value: ''
+ },
+ Point: {
+ error: false,
+ value: 'POINT (-0.150097204178 51.564975754948)'
+ },
+ Reference: {
+ error: false,
+ value: 'CA20'
+ },
+ 'Start date': {
+ error: false,
+ value: '01/06/1992'
+ },
+ 'entry-date': {
+ error: 'default-value',
+ value: undefined
+ },
+ geometry: {
+ error: 'OSGB',
+ value: undefined
+ },
+ organisation: {
+ error: 'default-value',
+ value: undefined
+ }
+ }
+ }
+ ],
+ issueCounts: {
+ 'entry-date': 1,
+ geometry: 1,
+ organisation: 1
+ },
+ dataset: 'test-dataset',
+ dataSubject: 'test-data-subject'
+ }
+ }
+
+ expect(req.form).toEqual(expectedFormValues)
+ })
+})
diff --git a/test/unit/errorsPage.test.js b/test/unit/errorsPage.test.js
new file mode 100644
index 00000000..05bb97b5
--- /dev/null
+++ b/test/unit/errorsPage.test.js
@@ -0,0 +1,90 @@
+import { describe, it, expect } from 'vitest'
+
+import nunjucks from 'nunjucks'
+const { govukMarkdown } = require('@x-govuk/govuk-prototype-filters')
+
+const nunjucksEnv = nunjucks.configure([
+ 'src/views',
+ 'node_modules/govuk-frontend/',
+ 'node_modules/@x-govuk/govuk-prototype-components/'
+], {
+ dev: true,
+ noCache: true,
+ watch: true
+})
+
+nunjucksEnv.addFilter('govukMarkdown', govukMarkdown)
+
+describe('errors page', () => {
+ it('renders the correct number of errors', () => {
+ const params = {
+ options: {
+ rows: [
+ {
+ entryNumber: '1',
+ columns: {
+ 'Document URL': {
+ error: false,
+ value: 'https://www.camden.gov.uk/holly-lodge-conservation-area'
+ },
+ 'End date': {
+ error: false,
+ value: ''
+ },
+ Geometry: {
+ error: 'fake error',
+ value: 'POLYGON ((-0.125888391245 51.54316508186, -0.125891457623 51.543177267548, -0.125903428774 51.54322160042))'
+ },
+ Legislation: {
+ error: false,
+ value: ''
+ },
+ Name: {
+ error: false,
+ value: 'Holly Lodge Estate'
+ },
+ Notes: {
+ error: false,
+ value: ''
+ },
+ Point: {
+ error: false,
+ value: 'POINT (-0.150097204178 51.564975754948)'
+ },
+ Reference: {
+ error: false,
+ value: 'CA20'
+ },
+ 'Start date': {
+ error: false,
+ value: '01/06/1992'
+ },
+ 'entry-date': {
+ error: 'default-value',
+ value: undefined
+ },
+ geometry: {
+ error: 'OSGB',
+ value: undefined
+ },
+ organisation: {
+ error: 'default-value',
+ value: undefined
+ }
+ }
+ }
+ ],
+ issueCounts: {
+ geography: 1
+ },
+ dataset: 'Datasubject test',
+ dataSubject: 'dataset test'
+ }
+ }
+ const html = nunjucks.render('errors.html', params).replace(/(\r\n|\n|\r)/gm, '').replace(/\t/gm, '').replace(/\s+/g, ' ')
+
+ expect(html).toContain('1 issue, relating to geography')
+ expect(html).toContain(' Datasubject test ')
+ expect(html).toContain(' POLYGON ((-0.125888391245 51.54316508186, -0.125891457623 51.543177267548, -0.125903428774 51.54322160042)) fake error | ')
+ })
+})
diff --git a/test/unit/test.test.js b/test/unit/test.test.js
deleted file mode 100644
index c17a1d5b..00000000
--- a/test/unit/test.test.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// write a hello world test in vitest
-
-// Path: test/unit/test.test.js
-
-import { describe, it, expect } from 'vitest'
-
-describe('hello world', () => {
- it('hello world', () => {
- expect('hello world').toBe('hello world')
- })
-})
diff --git a/test/unit/uploadController.test.js b/test/unit/uploadController.test.js
new file mode 100644
index 00000000..9222a783
--- /dev/null
+++ b/test/unit/uploadController.test.js
@@ -0,0 +1,39 @@
+import UploadController from '../../src/controllers/uploadController.js'
+
+import { describe, it, expect, vi } from 'vitest'
+
+import mockApiValue from '../testData/API_RUN_PIPELINE_RESPONSE.json'
+
+describe('UploadController', () => {
+ const options = {
+ route: '/upload'
+ }
+ const uploadController = new UploadController(options)
+ it('posting correct data adds the validation result to the session', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ json: vi.fn().mockResolvedValue(mockApiValue)
+ })
+
+ expect(uploadController.post).toBeDefined()
+
+ const req = {
+ file: {
+ path: 'readme.md',
+ originalname: 'conservation_area.csv'
+ },
+ sessionModel: {
+ get: () => 'test',
+ set: vi.fn()
+ }
+ }
+ const res = {
+ send: vi.fn(),
+ redirect: vi.fn()
+ }
+ const next = vi.fn()
+
+ await uploadController.post(req, res, next)
+
+ expect(req.sessionModel.set).toHaveBeenCalledWith('validationResult', mockApiValue)
+ })
+})