Skip to content

Commit 7ebf272

Browse files
PAF-11: create attachments
- Add upload model for uploading attachments functionality - Add save, delete and disable file behaviours for uploading attachments functionality - Add views for uploading attachments - Add validation for number of attachments added - Add ui and unit tests
1 parent d49985a commit 7ebf272

27 files changed

+728
-11
lines changed
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = superclass => class extends superclass {
2+
locals(req, res) {
3+
const locals = super.locals(req, res);
4+
const images = req.sessionModel.get('images');
5+
if (images && images.length >= 3) {
6+
// disable file upload if attachment limit reached.
7+
req.form.options.fields['other-info-file-upload'].attributes = [{attribute: 'disabled'}];
8+
return locals;
9+
}
10+
req.form.options.fields['other-info-file-upload'].attributes = [];
11+
return locals;
12+
}
13+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = superclass => class LimitDocs extends superclass {
2+
validate(req, res, next) {
3+
const images = req.sessionModel.get('images');
4+
if (images && images.length >= 3 && req.form.values['other-info-file-uploads-add-another'] === 'yes') {
5+
return next({
6+
'other-info-file-uploads-add-another': new this.ValidationError(
7+
'other-info-file-uploads-add-another',
8+
{
9+
type: 'tooMany'
10+
}
11+
)
12+
});
13+
} super.validate(req, res, next);
14+
return next;
15+
}
16+
};

apps/paf/behaviours/remove-file.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = superclass => class extends superclass {
4+
configure(req, res, next) {
5+
if (req.query.delete) {
6+
const images = req.sessionModel.get('images') || [];
7+
const remaining = images.filter(i => i.id !== req.query.delete);
8+
req.log('info', `Reference: ${req.sessionModel.get('reference')}, Removing image: ${req.query.delete}`);
9+
req.sessionModel.set('images', remaining);
10+
const path = req.baseUrl + req.path;
11+
return res.redirect(path);
12+
}
13+
return super.configure(req, res, next);
14+
}
15+
};

apps/paf/behaviours/save-file.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
const Model = require('../models/file-upload');
5+
6+
module.exports = name => superclass => class extends superclass {
7+
process(req) {
8+
if (req.files && req.files[name]) {
9+
// set image name on values for filename extension validation
10+
// N:B validation controller gets values from
11+
// req.form.values and not on req.files
12+
req.form.values[name] = req.files[name].name;
13+
req.log('info', `Reference: ${req.sessionModel.get('reference')},
14+
Processing image: ${req.form.values[name]}`);
15+
}
16+
super.process.apply(this, arguments);
17+
}
18+
19+
locals(req, res, next) {
20+
if (!Object.keys(req.form.errors).length) {
21+
req.form.values['other-info-file-upload'] = null;
22+
}
23+
return super.locals(req, res, next);
24+
}
25+
26+
saveValues(req, res, next) {
27+
const images = req.sessionModel.get('images') || [];
28+
if (req.files && req.files[name]) {
29+
req.log('info', `Reference: ${req.sessionModel.get('reference')}, Saving image: ${req.files[name].name}`);
30+
const image = _.pick(req.files[name], ['name', 'data', 'mimetype']);
31+
const model = new Model(image);
32+
return model.save()
33+
.then(() => {
34+
req.sessionModel.set('images', [...images, model.toJSON()]);
35+
return super.saveValues(req, res, next);
36+
})
37+
.catch(next);
38+
}
39+
return super.saveValues.apply(this, arguments);
40+
}
41+
};

apps/paf/fields/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,16 @@ module.exports = {
11671167
value: 'yes'
11681168
}
11691169
},
1170+
'other-info-file-upload': {
1171+
mixin: 'input-file',
1172+
className: 'govuk-file-upload',
1173+
attributes: []
1174+
},
1175+
'add-other-info-file-upload': {
1176+
isPageHeading: true,
1177+
mixin: 'radio-group',
1178+
options: ['yes', 'no']
1179+
},
11701180
'about-you-first-name': {
11711181
mixin: 'input-text'
11721182
},

apps/paf/index.js

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
'use strict';
2+
const saveImage = require('./behaviours/save-file');
3+
const removeImage = require('./behaviours/remove-file');
4+
const CombineAndLoopFields = require('hof').components.combineAndLoopFields;
5+
const limitDocs = require('./behaviours/limit-documents');
6+
const disableUpload = require('./behaviours/disable-file-upload');
27
const SummaryPageBehaviour = require('hof').components.summary;
38
const transportBehaviour = require('./behaviours/transport-behaviour');
49
const Aggregate = require('./behaviours/aggregator');
@@ -356,7 +361,7 @@ module.exports = {
356361
field: 'report-person-occupation',
357362
value: 'yes'
358363
}
359-
},
364+
}
360365
]
361366
},
362367
'/report-person-occupation-type': {
@@ -662,6 +667,36 @@ module.exports = {
662667
next: '/other-info-file-upload'
663668
},
664669
'/other-info-file-upload': {
670+
behaviours: [saveImage('other-info-file-upload'), disableUpload],
671+
fields: ['other-info-file-upload'],
672+
continueOnEdit: true,
673+
forks: [{
674+
target: '/add-other-info-file-upload',
675+
condition: req => {
676+
if (req.form.values['other-info-file-upload']) {
677+
return true
678+
}
679+
return false;
680+
}
681+
}],
682+
next: '/about-you'
683+
},
684+
'/add-other-info-file-upload': {
685+
template: 'list-add-looped-items',
686+
behaviours: [CombineAndLoopFields({
687+
groupName: 'other-info-file-uploads',
688+
fieldsToGroup: [
689+
'other-info-file-upload'
690+
],
691+
groupOptional: true,
692+
removePrefix: 'other-',
693+
combineValuesToSingleField: 'record',
694+
returnTo: '/other-info-file-upload'
695+
}), removeImage, limitDocs],
696+
next: '/about-you',
697+
locals: {
698+
section: 'other-info-file-upload'
699+
}
665700
},
666701
'/about-you': {
667702
fields: ['how-did-you-find-out-about-the-crime'],

apps/paf/models/file-upload.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
const url = require('url');
4+
const Model = require('hof').model;
5+
const jimp = require('jimp');
6+
const uuid = require('uuid').v4;
7+
const fs = require('fs');
8+
const noPreview = 'data:image/png;base64,' + fs.readFileSync('assets/images/no-preview.png', {encoding: 'base64'});
9+
const config = require('../../../config');
10+
11+
module.exports = class UploadModel extends Model {
12+
constructor(...args) {
13+
super(...args);
14+
this.set('id', uuid());
15+
}
16+
17+
save() {
18+
return new Promise((resolve, reject) => {
19+
const attributes = {
20+
url: config.upload.hostname
21+
};
22+
const reqConf = url.parse(this.url(attributes));
23+
reqConf.formData = {
24+
document: {
25+
value: this.get('data'),
26+
options: {
27+
filename: this.get('name'),
28+
contentType: this.get('mimetype')
29+
}
30+
}
31+
};
32+
reqConf.method = 'POST';
33+
this.request(reqConf, (err, data) => {
34+
if (err) {
35+
return reject(err);
36+
}
37+
resolve(data);
38+
});
39+
})
40+
.then(result => {
41+
return this.set({ url: result.url });
42+
})
43+
.then(() => {
44+
return this.thumbnail();
45+
})
46+
.then(() => {
47+
return this.unset('data');
48+
});
49+
}
50+
51+
thumbnail() {
52+
return jimp.read(this.get('data'))
53+
.then(image => {
54+
image.resize(300, jimp.AUTO);
55+
return new Promise((resolve, reject) => {
56+
image.getBase64(this.get('mimetype'), (e, data) => {
57+
return e ? reject(e) : resolve(data);
58+
});
59+
});
60+
})
61+
.then(data => {
62+
this.set('thumbnail', data);
63+
})
64+
.catch(() => {
65+
this.set('thumbnail', noPreview);
66+
});
67+
}
68+
69+
auth() {
70+
if (!config.keycloak.token) {
71+
// eslint-disable-next-line no-console
72+
console.error('keycloak token url is not defined');
73+
return Promise.resolve({
74+
bearer: 'abc123'
75+
});
76+
}
77+
const tokenReq = {
78+
url: config.keycloak.token,
79+
form: {
80+
username: config.keycloak.username,
81+
password: config.keycloak.password,
82+
grant_type: 'password',
83+
client_id: config.keycloak.clientId,
84+
client_secret: config.keycloak.secret
85+
},
86+
method: 'POST'
87+
};
88+
89+
return new Promise((resolve, reject) => {
90+
this._request(tokenReq, (err, response) => {
91+
const body = JSON.parse(response.body);
92+
93+
if (err || body.error) {
94+
return reject(err || new Error(`${body.error} - ${body.error_description}`));
95+
}
96+
97+
resolve({
98+
bearer: JSON.parse(response.body).access_token
99+
});
100+
});
101+
});
102+
}
103+
};

apps/paf/sections/summary-data-sections.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,17 @@ module.exports = {
221221
'other-info': [
222222
'other-info-description',
223223
'other-info-another-crime',
224-
'other-info-another-crime-description'
224+
'other-info-another-crime-description',
225+
{
226+
step: '/add-other-info-file-upload',
227+
field: 'images',
228+
parse: (list, req) => {
229+
if (!req.sessionModel.get('steps').includes('/add-other-info-file-upload')) {
230+
return null;
231+
}
232+
return list && list.map(a => a.name).join('\n————————————————\n') || 'None';
233+
}
234+
}
225235
],
226236
'about-you': [
227237
'how-did-you-find-out-about-the-crime',

apps/paf/translations/src/en/fields.json

+17
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,20 @@
10871087
},
10881088
"legendClassName": "govuk-fieldset__legend"
10891089
},
1090+
"other-info-file-upload": {
1091+
"label": "Attachment"
1092+
},
1093+
"other-info-file-uploads-add-another": {
1094+
"options": {
1095+
"yes": {
1096+
"label": "Yes"
1097+
},
1098+
"no": {
1099+
"label": "No"
1100+
}
1101+
},
1102+
"legendClassName": "govuk-fieldset__legend"
1103+
},
10901104
"persons": {
10911105
"label": "Additional People",
10921106
"changeLinkDescription": "Additional People"
@@ -1356,5 +1370,8 @@
13561370
},
13571371
"when-to-contact": {
13581372
"label": "When would you like us to contact you?"
1373+
},
1374+
"images": {
1375+
"label": "Attachments"
13591376
}
13601377
}

apps/paf/translations/src/en/pages.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,15 @@
122122
"other-info-description": {
123123
"header": "Other information"
124124
},
125-
"other-info-file-upload": {
126-
"header": "Please attach any documents which may help us investigate this crime - TO DO"
125+
"other-info-file-upload":{
126+
"header": "Please attach any documents which may help us investigate this crime"
127+
},
128+
"add-other-info-file-upload": {
129+
"header": "Do you need to add any other documents?",
130+
"hint": "You can submit up to 3 attachments"
131+
},
132+
"other-info-file-uploads": {
133+
"header": "Attachments"
127134
},
128135
"about-you": {
129136
"header": "About You"

apps/paf/translations/src/en/validation.json

+4
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,9 @@
2525
},
2626
"company-website": {
2727
"url": "Enter a website address in the correct format, like www.example.com"
28+
},
29+
"other-info-file-uploads-add-another": {
30+
"required": "Please select an option",
31+
"tooMany": "You can only submit up to 3 attachments."
2832
}
2933
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{{<partials-page}}
2+
{{$page-content}}
3+
<p class="form-hint">{{#t}}pages.add-{{section}}.hint{{/t}}</p>
4+
5+
{{>partials-looped-items}}
6+
7+
{{/page-content}}
8+
{{/partials-page}}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{{<partials-page}}
2+
{{$encoding}}enctype="multipart/form-data"{{/encoding}}
3+
{{$page-content}}
4+
{{#fields}}
5+
{{#renderField}}{{/renderField}}
6+
{{/fields}}
7+
8+
{{#input-submit}}continue{{/input-submit}}
9+
{{/page-content}}
10+
{{/partials-page}}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{{#fields}}
2+
{{#renderField}}{{/renderField}}
3+
{{/fields}}
4+
{{#input-submit}}continue{{/input-submit}}
5+
6+
<div id="{{section}}-summary">
7+
{{^values.images}}
8+
<h2 class="govuk-heading-m">No files added</h2>
9+
{{/values.images}}
10+
{{#values.images.length}}
11+
<h2>{{#t}}pages.{{field}}.header{{/t}}</h2>
12+
{{/values.images.length}}
13+
<table class="table-{{section}}-loop">
14+
<tbody>
15+
{{#values.images}}
16+
<tr class="{{section}}-records">
17+
<td><pre class="looped-records">{{name}}</pre></td>
18+
<td><a href="?delete={{id}}">Delete</a></td>
19+
</tr>
20+
{{/values.images}}
21+
</tbody>
22+
</table>
23+
</div>

apps/paf/views/partials/side-nav.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
Organisation</a>
4343
</li>
4444
<li class="app-navigation__list-item">
45-
<a role="link" class="govuk-link--no-visited-state govuk-link--no-underline" aria-disabled="true">Other Info</a>
45+
<a role="link" class="govuk-link--no-visited-state govuk-link--no-underline" aria-disabled="true">Other Information</a>
4646
</li>
4747
<li class="app-navigation__list-item">
4848
<a role="link" class="govuk-link--no-visited-state govuk-link--no-underline" aria-disabled="true">About You</a>

0 commit comments

Comments
 (0)