Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #24 from ministryofjustice/feature/refine-file-upload
Browse files Browse the repository at this point in the history
Feature/refine file upload
  • Loading branch information
solidgoldpig committed Apr 15, 2019
2 parents 8d13cf7 + 4194bd1 commit 8844540
Show file tree
Hide file tree
Showing 40 changed files with 1,557 additions and 3,332 deletions.
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
FROM node:10.11-slim
FROM node:10.15-slim

# derived from https://github.com/alekzonder/docker-puppeteer/blob/master/Dockerfile
RUN apt-get update && \
apt-get install -yq git && \
apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
Expand All @@ -12,6 +13,9 @@ wget https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_
dpkg -i dumb-init_*.deb && rm -f dumb-init_*.deb && \
apt-get clean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*

RUN mkdir /runner
WORKDIR /runner

COPY package.json package-lock.json ./
RUN npm install
# --ignore-scripts
Expand All @@ -20,6 +24,9 @@ RUN npm install
COPY bin ./bin
COPY lib ./lib

RUN useradd -ms /bin/bash runner
USER runner

ENTRYPOINT ["dumb-init", "--"]
EXPOSE 3000
CMD [ "node", "bin/start.js" ]
118 changes: 83 additions & 35 deletions lib/format/format.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
const parseArgs = require('./parse-args')
const {
getInstanceLangProperty
} = require('../service-data/service-data')

const bytes = require('./formatter/bytes')
const {conjoinFormatter} = require('./formatter/conjoin')
const {dateFormatter} = require('./formatter/date')

// Marked chokes on parentheses inside link parantheses delimiters
// const Markdown = require('markdown').markdown.toHTML
const MarkdownIt = require('markdown-it')()
Expand All @@ -11,42 +20,73 @@ const Markdown = (str) => {
const MessageFormat = require('messageformat')

const msgFormats = {}
msgFormats['en-GB'] = new MessageFormat('en-GB')
const defaultLocale = 'en-GB'
msgFormats['en-GB'].addFormatters({
concat: (input, ...rest) => {
if (!Array.isArray(input)) {
return input

msgFormats['en'] = new MessageFormat('en')
msgFormats['en'].addFormatters({
and: conjoinFormatter(' and '),
or: conjoinFormatter(' or '),
bytes,
date: dateFormatter()
})

msgFormats['cy'] = new MessageFormat('cy')
msgFormats['cy'].addFormatters({
and: conjoinFormatter(' ac '),
or: conjoinFormatter(' neu '),
bytes,
date: dateFormatter()
})

const defaultLocale = 'en'

// // TODO - initialise this after data is loaded
// const defaultLocale = getInstanceProperty('service', 'languageDefault') || 'en'
// const languages = getInstanceProperty('service', 'languages') || [defaultLocale]

// const languageMap = {
// en: 'en-GB'
// }
// // load these from metadata
// const words = {
// and: {
// cy: 'ac'
// },
// or: {
// cy: 'neu'
// }
// }

// languages.forEach(language => {
// msgFormats[language] = new MessageFormat(languageMap[language] || language)
// msgFormats[language].addFormatters({
// concat: i18nConcat(` ${words.and[language] || 'and'} `),
// orconcat: i18nConcat(` ${words.or[language] || 'or'} `),
// bytes: i18nBytes
// })
// })

const resolveNestedStrings = (value, args, options = {}) => {
const embeddedOptions = Object.assign({}, options, {markdown: false})
value = value.replace(/\[%.+?%\]/g, (str) => {
str = str.replace(/^\[%\s*/, '').replace(/\s*%\]$/, '')
let [aliasStr, ...argParts] = str.split(' ')
let argStr = argParts.join(' ')
let [aliasName, aliasProperty] = aliasStr.split('#')
const result = {
aliasName,
aliasProperty
}
input = input.slice()
const args = {
comma: ', ',
and: ' and '
if (argStr) {
result.args = parseArgs(argStr)
}

const concatArgs = rest[1] ? rest[1].split('&') : undefined
if (concatArgs) {
concatArgs.forEach(arg => {
const argParts = arg.match(/(.+?)=(.+)/)
if (argParts) {
const argName = argParts[1]
const argValue = argParts[2].replace(/^"/, '').replace(/"$/, '')
args[argName] = argValue
} else {
args[arg] = true
}
})
if (args.reverse) {
input = input.reverse()
}
str = getInstanceLangProperty(result.aliasName, result.aliasProperty || 'value', embeddedOptions.lang)
if (str) {
str = format(str, Object.assign({}, args, result.args), embeddedOptions)
}
let output = ''
input.forEach((inp, index) => {
output += `${(index ? index === input.length - 1 ? args.and : args.comma : '')}${inp}`
})
return output
}
})
return str || embeddedOptions.fallback || ''
})
return value
}

const format = (value, args, options = {}) => {
let formattedValue = value
Expand All @@ -60,19 +100,27 @@ const format = (value, args, options = {}) => {
// TODO: cache output based on knowledge of expected args?
if (formattedValue.includes('{')) {
try {
formattedValue = msgFormats[locale].compile(formattedValue)(args)
let lang = options.lang || locale
if (!msgFormats[lang]) {
lang = defaultLocale
}
formattedValue = msgFormats[lang].compile(formattedValue)(args)
} catch (e) {
// allow unformatted value to be returned
}
}
}
// resolve any nested strings
if (formattedValue.includes('[%')) {
formattedValue = resolveNestedStrings(formattedValue, args, options)
}
if (markdown) {
// TODO: cache output?
formattedValue = Markdown(formattedValue).trim()
// formattedValue = formattedValue.replace(/<(\/{0,1})strong>/g, '<$1b>')
// formattedValue = formattedValue.replace(/<(\/{0,1})em>/g, '<$1i>')
if (!actualOptions.multiline) {
formattedValue = formattedValue.replace(/^<p>(.*)<\/p>$/, '$1')
formattedValue = formattedValue.replace(/^<p>([\s\S]*)<\/p>$/, '$1')
}
}
return formattedValue
Expand Down
122 changes: 104 additions & 18 deletions lib/format/format.unit.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
const test = require('tape')
const {stub} = require('sinon')
const proxyquire = require('proxyquire')
const serviceData = {
getInstanceLangProperty: () => {}
}
const getInstanceLangPropertyStub = stub(serviceData, 'getInstanceLangProperty')

const {format} = proxyquire('./format', {
'../service-data/service-data': serviceData
})

const {format} = require('./format')
test('When formatting an undefined value', t => {
const formatted = format()
t.equal(formatted, undefined, 'it should return the same value')
t.end()
})

test('When formatting a number', t => {
const formatted = format(3)
t.equal(formatted, 3, 'it should return the same value')
t.end()
})

test('When formatting a string which does not have any substitutions', t => {
const formatted = format('value')
Expand Down Expand Up @@ -44,30 +64,96 @@ test('When formatting a multiline string which does not have any substitutions',
t.end()
})

test('When formatting a string containing a concat substitution', t => {
const formattedString = format('{x, concat}', {x: 'a'})
t.equal(formattedString, 'a', 'it should return value as is if not passed an array')
test('When formatting a string containing a conjoin substitution', t => {
const formattedString = format('{x, and}', {x: ['a', 'b', 'c']})
t.equal(formattedString, 'a, b and c', 'it should insert ‘commas’ and ‘and’ parts')

const formatted3 = format('{x, concat}', {x: ['a', 'b', 'c']})
t.equal(formatted3, 'a, b and c', 'it should insert ‘commas’ and ‘and’ parts')
const formattedReverse = format('{x, and, reverse}', {x: ['a', 'b', 'c']})
t.equal(formattedReverse, 'c, b and a', 'it should reverse the items when passed')

const formatted2 = format('{x, concat}', {x: ['a', 'b']})
t.equal(formatted2, 'a and b', 'it should omit ‘commas’ when necessary')
const formattedAnd = format('{x, and, and=" und "}', {x: ['a', 'b', 'c']})
t.equal(formattedAnd, 'a, b und c', 'it should use custom ‘and’ when passed')

const formatted1 = format('{x, concat}', {x: ['a']})
t.equal(formatted1, 'a', 'it should omit ‘and’ when necessary')
const formattedSingle = format('{x, and}', {x: 'a'})
t.equal(formattedSingle, 'a', 'it should handle strings')

const formattedReverse = format('{x, concat, reverse}', {x: ['a', 'b', 'c']})
t.equal(formattedReverse, 'c, b and a', 'it should reverse the items when passed')
t.end()
})

const formattedAnd = format('{x, concat, and=" und "}', {x: ['a', 'b', 'c']})
t.equal(formattedAnd, 'a, b und c', 'it should use custom ‘and’ when passed')
test('When formatting a string containing a date substitution', t => {
const formattedString = format('{x, date}', {x: new Date('2018/12/3')})
t.equal(formattedString, '3 December 2018', 'it should format date objects')

const formattedStringFormat = format('{x, date, format="YY/DD/MM"}', {x: new Date('2018/12/3')})
t.equal(formattedStringFormat, '18/03/12', 'it should format date objects')
t.end()
})

const formattedComma = format('{x, concat, comma=" ;; "}', {x: ['a', 'b', 'c']})
t.equal(formattedComma, 'a ;; b and c', 'it should use custom ‘comma’ when passed')
test('When formatting a string which contains a nested string', t => {
getInstanceLangPropertyStub.resetHistory()
getInstanceLangPropertyStub.callsFake(() => 'bar')
const formatted = format('[% foo %]')
t.deepEqual(getInstanceLangPropertyStub.getCall(0).args, ['foo', 'value', undefined], 'it should use the value property for the string lookup')
t.equal(formatted, 'bar', 'it should return the nested string')
getInstanceLangPropertyStub.restore()
t.end()
})

test('When formatting a string which contains a nested string which itself contains a nested string', t => {
getInstanceLangPropertyStub.resetHistory()
getInstanceLangPropertyStub.callsFake((x) => {
const values = {
foo: '[% bar %]',
bar: 'baz'
}
return values[x]
})
const formatted = format('[% foo %]')
t.equal(formatted, 'baz', 'it should return the nested string')

getInstanceLangPropertyStub.restore()
t.end()
})

test('When formatting a string which contains a nested string with arguments', t => {
getInstanceLangPropertyStub.resetHistory()
getInstanceLangPropertyStub.callsFake(() => {
return 'Hello {bar} {count, select, 1{one} 2{two} other{other}} {boolean} {f}'
})
const formatted = format('[% foo bar="baz" boolean f=false count=2 %]')
t.equal(formatted, 'Hello baz two true false', 'it should return the nested string processed with the args')

getInstanceLangPropertyStub.restore()
t.end()
})

test('When formatting a string which contains a nested string referencing an explicit property', t => {
getInstanceLangPropertyStub.resetHistory()
getInstanceLangPropertyStub.callsFake(() => {})

format('[% foo#bar %]')
t.deepEqual(getInstanceLangPropertyStub.getCall(0).args, ['foo', 'bar', undefined], 'it should use the explicit property to retrieve the referenced string')

getInstanceLangPropertyStub.restore()
t.end()
})

test('When formatting a string containing a nested string which does not exist', t => {
getInstanceLangPropertyStub.resetHistory()
getInstanceLangPropertyStub.callsFake(() => {})
const formatted = format('[% foo %]')
t.equal(formatted, '', 'it should return an empty string')

getInstanceLangPropertyStub.restore()
t.end()
})

const formattedAndComma = format('{x, concat, and=" und "&comma=" ;; "}', {x: ['a', 'b', 'c']})
t.equal(formattedAndComma, 'a ;; b und c', 'it should use custom ‘and’ and ‘comma’ when passed')
test('When formatting a string containing a nested string which does not exist and a fallback value has been provided', t => {
getInstanceLangPropertyStub.resetHistory()
getInstanceLangPropertyStub.callsFake(() => {})
const formatted = format('[% foo %]', {}, {fallback: 'fallback value'})
t.equal(formatted, 'fallback value', 'it should return the fallback value')

getInstanceLangPropertyStub.restore()
t.end()
})
25 changes: 25 additions & 0 deletions lib/format/formatter/bytes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const bytes = require('bytes')

const formatBytes = (size) => {
if (typeof size === 'string') {
return size
}
let formattedSize = bytes(size)
formattedSize = formattedSize.replace(/(\d+)\.(\d+)(\w+)/, (m, int, decimal, unit) => {
const unitType = unit.toUpperCase()
const multiplier = unitType === 'KB' ? 100 : 10
const halfMultiplier = 5 * multiplier
decimal = parseInt((decimal * multiplier + halfMultiplier) / 100, 10)
if (decimal >= 10) {
decimal = ''
int++
}
if (unitType === 'KB') {
decimal = ''
}
return `${int}${decimal ? `.${decimal}` : ''}${unit}`
})
return formattedSize
}

module.exports = formatBytes
26 changes: 26 additions & 0 deletions lib/format/formatter/bytes.unit.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const test = require('tape')

const bytes = require('bytes')
const formatBytes = require('./bytes')

const getFormattedSize = (size) => {
return formatBytes(bytes(size))
}

test('When formatting files sizes', t => {
t.equal(getFormattedSize('19MB'), '19MB', 'it should return exact MB values without decimal points')
t.equal(getFormattedSize('19.77MB'), '19.8MB', 'it should round up MB values to the nearest decimal point')
t.equal(getFormattedSize('19.71MB'), '19.7MB', 'it should round down MB values to the nearest decimal point')
t.equal(getFormattedSize('19.97MB'), '20MB', 'it should round up MB values to the next integer')
t.equal(getFormattedSize('19.03MB'), '19MB', 'it should round down MB values to the next integer')
t.equal(getFormattedSize('19.8KB'), '20KB', 'it should round up KB values to the next integer')
t.equal(getFormattedSize('19.3KB'), '19KB', 'it should round down KB values to the next integer')
t.equal(getFormattedSize('19B'), '19B', 'it should B values exactly')

t.end()
})

test('When formatting files sizes passed as strings', t => {
t.equal(formatBytes('19.71MB'), '19.71MB', 'it should return the value as is')
t.end()
})

0 comments on commit 8844540

Please sign in to comment.