Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/lib/validation/form/no-csv-external-datasets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { XPATH_MODEL, getNodes } = require('../../forms-utils');

// Enketo (and so the CHT) does not support CSV external datasets. Forms referencing them load without
// any error, but no data is available for the select_one_from_file/select_many_from_file questions.
const CSV_SRC_PREFIX = 'jr://file-csv/';

module.exports = {
requiresInstance: false,
skipFurtherValidation: false,
execute: async({ xformPath, xmlDoc }) => {
const errors = [];

const csvSources = getNodes(xmlDoc, `${XPATH_MODEL}/instance[@src]`)
.map(instance => instance.getAttribute('src').trim())
.filter(src => src.startsWith(CSV_SRC_PREFIX));
if (csvSources.length) {
errors.push(
`Form at ${xformPath} contains the following external data sources referencing CSV files: `
+ `[${csvSources.join(', ')}]. The CHT only supports XML files for `
+ `select_one_from_file/select_many_from_file questions. Convert the data to XML and reference it as `
+ `'jr://file/<name>.xml': `
+ `https://docs.communityhealthtoolkit.org/building/forms/app/#select-choice-from-file`
);
}
return { errors, warnings: [] };
}
};
5 changes: 5 additions & 0 deletions test/lib/validate-forms.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ describe('validate-forms', () => {
expect(deprecatedTelType.requiresInstance).to.equal(true);
expect(deprecatedTelType.skipFurtherValidation).to.equal(false);

const noCsvExternalDatasets = validations.shift();
expect(noCsvExternalDatasets.name).to.equal('no-csv-external-datasets.js');
expect(noCsvExternalDatasets.requiresInstance).to.equal(false);
expect(noCsvExternalDatasets.skipFurtherValidation).to.equal(false);

const noRequiredNotes = validations.shift();
expect(noRequiredNotes.name).to.equal('no-required-notes.js');
expect(noRequiredNotes.requiresInstance).to.equal(false);
Expand Down
116 changes: 116 additions & 0 deletions test/lib/validation/form/no-csv-external-datasets.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const { expect } = require('chai');
const { DOMParser } = require('@xmldom/xmldom');
const noCsvExternalDatasets = require('../../../../src/lib/validation/form/no-csv-external-datasets');

const domParser = new DOMParser();

// Simple XML helper - only focuses on what we need to test: secondary instances with src attributes
const getXml = (secondaryInstances = []) => `
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml">
<h:head>
<h:title>Test Form</h:title>
<model>
<instance>
<data>
<favorite_color/>
</data>
</instance>
${secondaryInstances.map(({ id, src }) => `<instance id="${id}" src="${src}" />`).join('')}
<instance id="inline">
<root><item><name>red</name><label>Red</label></item></root>
</instance>
<bind nodeset="/data/favorite_color" type="string" />
</model>
</h:head>
<h:body>
<select1 ref="/data/favorite_color">
<label>Favorite color</label>
<itemset nodeset="instance('colors')/root/item">
<value ref="name" />
<label ref="label" />
</itemset>
</select1>
</h:body>
</h:html>`;

const getXmlDoc = (secondaryInstances) => domParser.parseFromString(getXml(secondaryInstances));
const xformPath = '/my/form/path/form.xml';

const getExpectedError = (sources) => `Form at ${xformPath} contains the following external data sources `
+ `referencing CSV files: [${sources.join(', ')}]. The CHT only supports XML files for `
+ `select_one_from_file/select_many_from_file questions. Convert the data to XML and reference it as `
+ `'jr://file/<name>.xml': `
+ `https://docs.communityhealthtoolkit.org/building/forms/app/#select-choice-from-file`;

const assertEmpty = (output) => {
expect(output.warnings).is.empty;
expect(output.errors).is.empty;
};

describe('no-csv-external-datasets', () => {
it('resolves OK when the form has no external data sources', () => {
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc() })
.then(output => assertEmpty(output));
});

it('resolves OK when the external data source is an XML file', () => {
const instances = [{ id: 'colors', src: 'jr://file/colors.xml' }];
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc(instances) })
.then(output => assertEmpty(output));
});

it('returns an error when the external data source is a CSV file', () => {
const instances = [{ id: 'colors', src: 'jr://file-csv/colors.csv' }];
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc(instances) })
.then(output => {
expect(output.warnings).is.empty;
expect(output.errors).to.deep.equal([getExpectedError(['jr://file-csv/colors.csv'])]);
});
});

it('returns one error listing every CSV data source', () => {
const instances = [
{ id: 'colors', src: 'jr://file-csv/colors.csv' },
{ id: 'sizes', src: 'jr://file-csv/sizes.csv' }
];
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc(instances) })
.then(output => {
expect(output.errors).to.deep.equal([
getExpectedError(['jr://file-csv/colors.csv', 'jr://file-csv/sizes.csv'])
]);
});
});

it('ignores XML data sources when reporting CSV data sources', () => {
const instances = [
{ id: 'colors', src: 'jr://file/colors.xml' },
{ id: 'sizes', src: 'jr://file-csv/sizes.csv' }
];
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc(instances) })
.then(output => {
expect(output.errors).to.deep.equal([getExpectedError(['jr://file-csv/sizes.csv'])]);
});
});

it('resolves OK when the src only starts with the CSV prefix', () => {
const instances = [{ id: 'colors', src: 'jr://file-csv-backup/colors.xml' }];
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc(instances) })
.then(output => assertEmpty(output));
});

it('detects a CSV data source with surrounding whitespace', () => {
const instances = [{ id: 'colors', src: ' jr://file-csv/colors.csv ' }];
return noCsvExternalDatasets
.execute({ xformPath, xmlDoc: getXmlDoc(instances) })
.then(output => {
expect(output.errors).to.deep.equal([getExpectedError(['jr://file-csv/colors.csv'])]);
});
});
});