Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SW-130: Metadata - Statistical Quality #36

Merged
merged 4 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions src/controllers/publish.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import { snakeCase } from 'lodash';
import { FieldValidationError } from 'express-validator';

import { generateViewErrors } from '../utils/generate-view-errors';
import { collectionValidator, descriptionValidator, hasError, titleValidator } from '../validators';
import {
collectionValidator,
descriptionValidator,
hasError,
qualityValidator,
roundingAppliedValidator,
roundingDescriptionValidator,
titleValidator
} from '../validators';
import { ViewError } from '../dtos/view-error';
import { logger } from '../utils/logger';
import { ViewDTO, ViewErrDTO } from '../dtos/view-dto';
Expand Down Expand Up @@ -235,7 +245,7 @@ export const provideSummary = async (req: Request, res: Response, next: NextFunc
res.redirect(req.buildUrl(`/publish/${dataset.id}/tasklist`, req.language));
return;
} catch (err) {
const error: ViewError = { field: 'title', tag: { name: 'errors.description.missing' } };
const error: ViewError = { field: 'description', tag: { name: 'errors.description.missing' } };
errors = generateViewErrors(undefined, 400, [error]);
}
}
Expand All @@ -262,10 +272,49 @@ export const provideCollection = async (req: Request, res: Response, next: NextF
res.redirect(req.buildUrl(`/publish/${dataset.id}/tasklist`, req.language));
return;
} catch (err) {
const error: ViewError = { field: 'title', tag: { name: 'errors.collection.missing' } };
const error: ViewError = { field: 'collection', tag: { name: 'errors.collection.missing' } };
errors = generateViewErrors(undefined, 400, [error]);
}
}

res.render('publish/collection', { collection, errors });
};

export const provideQuality = async (req: Request, res: Response, next: NextFunction) => {
let errors: ViewErrDTO | undefined;
const dataset = singleLangDataset(res.locals.dataset, req.language);
let datasetInfo = dataset?.datasetInfo!;

if (req.method === 'POST') {
try {
datasetInfo = {
quality: req.body.quality,
rounding_applied: req.body.rounding_applied ? req.body.rounding_applied === 'true' : undefined,
rounding_description: req.body.rounding_applied === 'true' ? req.body.rounding_description : ''
};

for (const validator of [qualityValidator(), roundingAppliedValidator(), roundingDescriptionValidator()]) {
const result = await validator.run(req);
if (!result.isEmpty()) {
res.status(400);
const error = result.array()[0] as FieldValidationError;
throw error;
}
}

await req.swapi.updateDatasetInfo(dataset.id, { ...datasetInfo, language: req.language });
res.redirect(req.buildUrl(`/publish/${dataset.id}/tasklist`, req.language));
return;
} catch (err: any) {
if (err.path) {
const error: ViewError = { field: err.path, tag: { name: `errors.${snakeCase(err.path)}.missing` } };
errors = generateViewErrors(dataset.id, 400, [error]);
} else {
next(new UnknownException());
return;
}
}
}

res.render('publish/quality', { ...datasetInfo, errors });
};
2 changes: 2 additions & 0 deletions src/dtos/dataset-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export interface DatasetInfoDTO {
description?: string;
collection?: string;
quality?: string;
rounding_applied?: boolean;
rounding_description?: string;
}
30 changes: 30 additions & 0 deletions src/middleware/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@
"explain_3": "both of the above",
"language": "This should be entered in the language in which you're viewing this service."
},
"quality": {
"heading": "What is the statistical quality of this dataset?",
"explain": "In short, simple sentences explain:",
"explain_1": "any issues or methodological changes related to the dataset including any:",
"explain_1a": "notes relevant to all data values in the dataset",
"explain_1b": "custom explanations needed to clarify any note codes used",
"explain_2": "a high-level summary of the statistical quality report, if available",
"language": "This should be entered in the language in which you're viewing this service.",
"rounding": {
"heading": "Has rounding been applied to the data values?",
"options": {
"yes": {
"label": "Yes",
"note": "In short, simple sentences explain what rounding has been applied."
},
"no": {
"label": "No"
}
}
}
},
"upload": {
"title": "Upload the data table",
"note": "The file should be in a CSV format"
Expand Down Expand Up @@ -254,6 +275,15 @@
"collection": {
"missing": "Either a collection method was not entered or there is a problem."
},
"quality": {
"missing": "You need to provide the statistical quality of the dataset"
},
"rounding_applied": {
"missing": "You need to provide whether rounding has been applied to the data values"
},
"rounding_description": {
"missing": "You need to provide a description of the rounding applied"
},
"upload": {
"no_csv": "No CSV data provided",
"no_csv_data": "No CSV data available"
Expand Down
6 changes: 5 additions & 1 deletion src/routes/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
changeData,
redirectToTasklist,
provideSummary,
provideCollection
provideCollection,
provideQuality
} from '../controllers/publish';

export const publish = Router();
Expand Down Expand Up @@ -48,3 +49,6 @@ publish.post('/:datasetId/summary', fetchDataset, upload.none(), provideSummary)

publish.get('/:datasetId/collection', fetchDataset, provideCollection);
publish.post('/:datasetId/collection', fetchDataset, upload.none(), provideCollection);

publish.get('/:datasetId/quality', fetchDataset, provideQuality);
publish.post('/:datasetId/quality', fetchDataset, upload.none(), provideQuality);
7 changes: 6 additions & 1 deletion src/validators/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request } from 'express';
import { body, param, query, ValidationChain } from 'express-validator';
import { body, param, ValidationChain } from 'express-validator';

export const hasError = async (validator: ValidationChain, req: Request) => {
return !(await validator.run(req)).isEmpty();
Expand All @@ -12,3 +12,8 @@ export const importIdValidator = () => param('importId').trim().notEmpty().isUUI
export const titleValidator = () => body('title').trim().notEmpty();
export const descriptionValidator = () => body('description').trim().notEmpty();
export const collectionValidator = () => body('collection').trim().notEmpty();

export const qualityValidator = () => body('quality').trim().notEmpty();
export const roundingAppliedValidator = () => body('rounding_applied').notEmpty().isBoolean();
export const roundingDescriptionValidator = () =>
body('rounding_description').if(body('rounding_applied').equals('true')).trim().notEmpty();
78 changes: 78 additions & 0 deletions src/views/publish/quality.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<%- include("../partials/header"); %>
<div class="govuk-width-container app-width-container">
<!-- <a href="#" class="govuk-back-link">Back</a> -->
<main class="govuk-main-wrapper" id="main-content" role="main">

<div class="top-links">
<div class="govuk-width-container">
<a href="javascript:history.back()" class="govuk-back-link"><%= t('buttons.back') %></a>
<a href="<%= buildUrl(`/publish/${locals.datasetId}/tasklist`, i18n.language) %>" class="govuk-link return-link"><%= t('publish.header.overview') %></a>
</div>
</div>

<div class="govuk-width-container">
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl"><%= t('publish.quality.heading') %></h1>

<form enctype="multipart/form-data" method="post">
<%- include("../partials/error-handler"); %>

<p class="govuk-body"><%= t('publish.quality.explain') %></p>

<ul class="govuk-list govuk-list--bullet">
<li><%= t('publish.quality.explain_1') %></li>
<ul class="govuk-list govuk-list--bullet">
<li><%= t('publish.quality.explain_1a') %></li>
<li><%= t('publish.quality.explain_1b') %></li>
</ul>
<li><%= t('publish.quality.explain_2') %></li>
</ul>

<div class="govuk-hint">
<%= t('publish.quality.language') %>
</div>


<div class="govuk-form-group">
<textarea class="govuk-textarea" id="quality" name="quality" rows="4"><%= locals.quality %></textarea>
</div>

<h2 class="govuk-heading-s"><%= t('publish.quality.rounding.heading') %></h2>

<div class="govuk-form-group">
<fieldset class="govuk-fieldset" aria-describedby="rounding">
<div class="govuk-radios" data-module="govuk-radios">
<div class="govuk-radios__item">
<input class="govuk-radios__input" id="roundingApplied1" name="rounding_applied" data-aria-controls="conditional-rounding1" type="radio" value="true" <%= locals.rounding_applied === true ? 'checked' : '' %> />
<label class="govuk-label govuk-radios__label" for="roundingApplied1"><%= t('publish.quality.rounding.options.yes.label') %></label>
</div>
<div class="govuk-radios__conditional govuk-radios__conditional--hidden" id="conditional-rounding1">
<div class="govuk-form-group">
<fieldset class="govuk-fieldset" role="group" aria-describedby="roundingApplied1">
<legend class="govuk-fieldset__legend govuk-fieldset__legend--s">
<p><%= t('publish.quality.rounding.options.yes.note') %></p>
</legend>
<div class="govuk-hint"><%= t('publish.quality.language') %></div>
<textarea class="govuk-textarea" id="roundingDescription" name="rounding_description" rows="4" aria-describedby="roundingDescription-hint"><%= locals.rounding_description %></textarea>
</fieldset>
</div>
</div>
<div class="govuk-radios__item">
<input class="govuk-radios__input" id="roundingApplied2" name="rounding_applied" type="radio" value="false" <%= locals.rounding_applied === false ? 'checked' : '' %> />
<label class="govuk-label govuk-radios__label" for="roundingApplied2"><%= t('publish.quality.rounding.options.no.label') %></label>
</div>
</div>
</fieldset>
</div>

<button type="submit" class="govuk-button" data-module="govuk-button"><%= t('buttons.continue') %></button>
<a class="govuk-button govuk-button--secondary" href="javascript:history.back()"><%= t('buttons.cancel')%></a>
</form>
</div>
</div>
</div>
</main>
</div>

<%- include("../partials/footer"); %>
Loading