diff --git a/Gruntfile.js b/Gruntfile.js
index 3ebc1233fa1..cfff48044aa 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -274,7 +274,6 @@ module.exports = function(grunt) {
'webapp/src/ts/**/*.component.html',
];
const ignore = [
- 'webapp/src/ts/providers/xpath-element-path.provider.ts',
'webapp/src/js/bootstrap-tour-standalone.js',
'api/src/public/login/lib-bowser.js',
'api/extracted-resources/**/*',
diff --git a/api/server.js b/api/server.js
index b5f3e03f6e7..c50affbb77d 100644
--- a/api/server.js
+++ b/api/server.js
@@ -59,7 +59,7 @@ process
const checkInstall = require('./src/services/setup/check-install');
const configWatcher = require('./src/services/config-watcher');
const migrations = require('./src/migrations');
- const generateXform = require('./src/services/generate-xform');
+ const updateXform = require('./src/services/update-xform');
const serverUtils = require('./src/server-utils');
const generateServiceWorker = require('./src/generate-service-worker');
const manifest = require('./src/services/manifest');
@@ -92,7 +92,7 @@ process
logger.info('Service worker generated successfully');
logger.info('Updating xforms…');
- await generateXform.updateAll();
+ await updateXform.updateAll();
logger.info('xforms updated successfully');
} catch (err) {
diff --git a/api/src/services/config-watcher.js b/api/src/services/config-watcher.js
index 6c3e09a28dc..5949b8ba13a 100644
--- a/api/src/services/config-watcher.js
+++ b/api/src/services/config-watcher.js
@@ -5,7 +5,7 @@ const tombstoneUtils = require('@medic/tombstone-utils');
const viewMapUtils = require('@medic/view-map-utils');
const settingsService = require('./settings');
const translations = require('../translations');
-const generateXform = require('./generate-xform');
+const updateXform = require('./update-xform');
const generateServiceWorker = require('../generate-service-worker');
const manifest = require('./manifest');
const config = require('../config');
@@ -105,7 +105,7 @@ const handleFormChange = (change) => {
return Promise.resolve();
}
logger.info('Detected form change - generating attachments');
- return generateXform.update(change.id).catch(err => {
+ return updateXform.update(change.id).catch(err => {
logger.error('Failed to update xform: %o', err);
});
};
diff --git a/api/src/services/generate-xform.js b/api/src/services/generate-xform.js
index f7396798dff..6bab1c9bd8d 100644
--- a/api/src/services/generate-xform.js
+++ b/api/src/services/generate-xform.js
@@ -3,20 +3,15 @@
* @module generate-xform
*/
const childProcess = require('child_process');
-const path = require('path');
const htmlParser = require('node-html-parser');
const logger = require('../logger');
-const db = require('../db');
-const formsService = require('./forms');
const markdown = require('../enketo-transformer/markdown');
+const { FORM_STYLESHEET, MODEL_STYLESHEET } = require('../xsl/xsl-paths');
const MODEL_ROOT_OPEN = '';
const ROOT_CLOSE = '';
const JAVAROSA_SRC = / src="jr:\/\//gi;
const MEDIA_SRC_ATTR = ' data-media-src="';
-
-const FORM_STYLESHEET = path.join(__dirname, '../xsl/openrosa2html5form.xsl');
-const MODEL_STYLESHEET = path.join(__dirname, '../enketo-transformer/xsl/openrosa2xmlmodel.xsl');
const XSLTPROC_CMD = 'xsltproc';
const processErrorHandler = (xsltproc, err, reject) => {
@@ -161,109 +156,13 @@ const generateModel = formXml => {
});
};
-const getEnketoForm = doc => {
- const collect = doc.context && doc.context.collect;
- return !collect && formsService.getXFormAttachment(doc);
-};
-
const generate = formXml => {
return Promise.all([ generateForm(formXml), generateModel(formXml) ])
.then(([ form, model ]) => ({ form, model }));
};
-const updateAttachment = (doc, updated, name, type) => {
- const attachmentData = doc._attachments &&
- doc._attachments[name] &&
- doc._attachments[name].data &&
- doc._attachments[name].data.toString();
- if (attachmentData === updated) {
- return false;
- }
- doc._attachments[name] = {
- data: Buffer.from(updated),
- content_type: type
- };
- return true;
-};
-
-const updateAttachmentsIfRequired = (doc, updated) => {
- const formUpdated = updateAttachment(doc, updated.form, 'form.html', 'text/html');
- const modelUpdated = updateAttachment(doc, updated.model, 'model.xml', 'text/xml');
- return formUpdated || modelUpdated;
-};
-
-const updateAttachments = (accumulator, doc) => {
- return accumulator.then(results => {
- const form = getEnketoForm(doc);
- if (!form) {
- results.push(null); // not an enketo form - no update required
- return results;
- }
- logger.debug(`Generating html and xml model for enketo form "${doc._id}"`);
- return module.exports.generate(form.data.toString()).then(result => {
- results.push(result);
- return results;
- });
- });
-};
-
-// Returns array of docs that need saving.
-const updateAllAttachments = docs => {
- // spawn the child processes in series so we don't smash the server
- return docs.reduce(updateAttachments, Promise.resolve([])).then(results => {
- return docs.filter((doc, i) => {
- return results[i] && updateAttachmentsIfRequired(doc, results[i]);
- });
- });
-};
-
module.exports = {
- /**
- * Updates the model and form attachments of the given form if necessary.
- * @param {string} docId - The db id of the doc defining the form.
- */
- update: docId => {
- return db.medic.get(docId, { attachments: true, binary: true })
- .then(doc => updateAllAttachments([ doc ]))
- .then(docs => {
- const doc = docs.length && docs[0];
- if (doc) {
- logger.info(`Updating form with ID "${docId}"`);
- return db.medic.put(doc);
- } else {
- logger.info(`Form with ID "${docId}" does not need to be updated.`);
- }
- });
- },
-
- /**
- * Updates the model and form attachments for all forms if necessary.
- */
- updateAll: () => {
- return formsService.getFormDocs()
- .then(docs => {
- if (!docs.length) {
- return [];
- }
- return updateAllAttachments(docs);
- })
- .then(toSave => {
- logger.info(`Updating ${toSave.length} enketo form${toSave.length === 1 ? '' : 's'}`);
- if (!toSave.length) {
- return;
- }
- return db.medic.bulkDocs(toSave).then(results => {
- const failures = results.filter(result => !result.ok);
- if (failures.length) {
- logger.error('Bulk save failed with: %o', failures);
- throw new Error('Failed to save updated xforms to the database');
- }
- });
- });
-
- },
-
/**
* @param formXml The XML form string
* @returns a promise with the XML form transformed following
diff --git a/api/src/services/update-xform.js b/api/src/services/update-xform.js
new file mode 100644
index 00000000000..6dcd1566abb
--- /dev/null
+++ b/api/src/services/update-xform.js
@@ -0,0 +1,103 @@
+const db = require('../db');
+const logger = require('../logger');
+const formsService = require('./forms');
+const generatexFormService = require('./generate-xform');
+
+const getEnketoForm = doc => {
+ const collect = doc.context && doc.context.collect;
+ return !collect && formsService.getXFormAttachment(doc);
+};
+
+const updateAttachment = (doc, updated, name, type) => {
+ const attachmentData = doc._attachments &&
+ doc._attachments[name] &&
+ doc._attachments[name].data &&
+ doc._attachments[name].data.toString();
+ if(attachmentData === updated) {
+ return false;
+ }
+ doc._attachments[name] = {
+ data: Buffer.from(updated),
+ content_type: type
+ };
+ return true;
+};
+
+const updateAttachmentsIfRequired = (doc, updated) => {
+ const formUpdated = updateAttachment(doc, updated.form, 'form.html', 'text/html');
+ const modelUpdated = updateAttachment(doc, updated.model, 'model.xml', 'text/xml');
+ return formUpdated || modelUpdated;
+};
+
+const updateAttachments = (accumulator, doc) => {
+ return accumulator.then(results => {
+ const form = getEnketoForm(doc);
+ if(!form) {
+ results.push(null); // not an enketo form - no update required
+ return results;
+ }
+ logger.debug(`Generating html and xml model for enketo form "${doc._id}"`);
+ return generatexFormService.generate(form.data.toString()).then(result => {
+ results.push(result);
+ return results;
+ });
+ });
+};
+
+// Returns array of docs that need saving.
+const updateAllAttachments = docs => {
+ // spawn the child processes in series so we don't smash the server
+ return docs.reduce(updateAttachments, Promise.resolve([])).then(results => {
+ return docs.filter((doc, i) => {
+ return results[i] && updateAttachmentsIfRequired(doc, results[i]);
+ });
+ });
+};
+
+module.exports = {
+
+ /**
+ * Updates the model and form attachments of the given form if necessary.
+ * @param {string} docId - The db id of the doc defining the form.
+ */
+ update: docId => {
+ return db.medic.get(docId, { attachments: true, binary: true })
+ .then(doc => updateAllAttachments([doc]))
+ .then(docs => {
+ const doc = docs.length && docs[0];
+ if(doc) {
+ logger.info(`Updating form with ID "${docId}"`);
+ return db.medic.put(doc);
+ } else {
+ logger.info(`Form with ID "${docId}" does not need to be updated.`);
+ }
+ });
+ },
+
+ /**
+ * Updates the model and form attachments for all forms if necessary.
+ */
+ updateAll: () => {
+ return formsService.getFormDocs()
+ .then(docs => {
+ if(!docs.length) {
+ return [];
+ }
+ return updateAllAttachments(docs);
+ })
+ .then(toSave => {
+ logger.info(`Updating ${toSave.length} enketo form${toSave.length === 1 ? '' : 's'}`);
+ if(!toSave.length) {
+ return;
+ }
+ return db.medic.bulkDocs(toSave).then(results => {
+ const failures = results.filter(result => !result.ok);
+ if(failures.length) {
+ logger.error('Bulk save failed with: %o', failures);
+ throw new Error('Failed to save updated xforms to the database');
+ }
+ });
+ });
+
+ }
+};
diff --git a/api/src/xsl/xsl-paths.js b/api/src/xsl/xsl-paths.js
new file mode 100644
index 00000000000..1824b0da287
--- /dev/null
+++ b/api/src/xsl/xsl-paths.js
@@ -0,0 +1,6 @@
+const path = require('path');
+
+module.exports = {
+ FORM_STYLESHEET: path.join(__dirname, './openrosa2html5form.xsl'),
+ MODEL_STYLESHEET: path.join(__dirname, '../enketo-transformer/xsl/openrosa2xmlmodel.xsl')
+};
diff --git a/api/tests/mocha/services/config-watcher.spec.js b/api/tests/mocha/services/config-watcher.spec.js
index 4d705816f90..803b6593c79 100644
--- a/api/tests/mocha/services/config-watcher.spec.js
+++ b/api/tests/mocha/services/config-watcher.spec.js
@@ -7,7 +7,7 @@ const db = require('../../../src/db');
const settingsService = require('../../../src/services/settings');
const translations = require('../../../src/translations');
const generateServiceWorker = require('../../../src/generate-service-worker');
-const generateXform = require('../../../src/services/generate-xform');
+const updateXform = require('../../../src/services/update-xform');
const config = require('../../../src/config');
const bootstrap = require('../../../src/services/config-watcher');
const manifest = require('../../../src/services/manifest');
@@ -248,35 +248,35 @@ describe('Configuration', () => {
describe('form changes', () => {
it('handles xform changes', () => {
- sinon.stub(generateXform, 'update').resolves();
+ sinon.stub(updateXform, 'update').resolves();
return emitChange({ id: 'form:something:something' }).then(() => {
- chai.expect(generateXform.update.callCount).to.equal(1);
- chai.expect(generateXform.update.args[0]).to.deep.equal(['form:something:something']);
+ chai.expect(updateXform.update.callCount).to.equal(1);
+ chai.expect(updateXform.update.args[0]).to.deep.equal(['form:something:something']);
});
});
it('should not terminate the process on form gen errors', () => {
- sinon.stub(generateXform, 'update').rejects();
+ sinon.stub(updateXform, 'update').rejects();
return emitChange({ id: 'form:id' }).then(() => {
- chai.expect(generateXform.update.callCount).to.equal(1);
- chai.expect(generateXform.update.args[0]).to.deep.equal(['form:id']);
+ chai.expect(updateXform.update.callCount).to.equal(1);
+ chai.expect(updateXform.update.args[0]).to.deep.equal(['form:id']);
});
});
it('should handle deletions gracefully - #7608', () => {
- sinon.stub(generateXform, 'update');
+ sinon.stub(updateXform, 'update');
return emitChange({ id: 'form:id', deleted: true }).then(() => {
- chai.expect(generateXform.update.callCount).to.equal(0);
+ chai.expect(updateXform.update.callCount).to.equal(0);
});
});
it('should ignore tombstones - #7608', () => {
- sinon.stub(generateXform, 'update');
+ sinon.stub(updateXform, 'update');
const tombstoneId = 'form:pnc_danger_sign_follow_up_mother____3-336f91959e14966f9baec1c3dd1c7fa2____tombstone';
return emitChange({ id: tombstoneId }).then(() => {
- chai.expect(generateXform.update.callCount).to.equal(0);
+ chai.expect(updateXform.update.callCount).to.equal(0);
});
});
});
diff --git a/api/tests/mocha/services/generate-xform.spec.js b/api/tests/mocha/services/generate-xform.spec.js
index a77c55ccad1..dace5f8a3d5 100644
--- a/api/tests/mocha/services/generate-xform.spec.js
+++ b/api/tests/mocha/services/generate-xform.spec.js
@@ -6,7 +6,6 @@ const { assert, expect } = require('chai');
const sinon = require('sinon');
const childProcess = require('child_process');
const markdown = require('../../../src/enketo-transformer/markdown');
-const db = require('../../../src/db');
const service = require('../../../src/services/generate-xform');
const FILES = {
@@ -17,15 +16,6 @@ const FILES = {
expectedModel: 'model.expected.xml',
};
-const expectAttachments = (doc, form, model) => {
- const formAttachment = doc._attachments['form.html'];
- expect(formAttachment.data.toString()).to.equal(form);
- expect(formAttachment.content_type).to.equal('text/html');
- const modelAttachment = doc._attachments['model.xml'];
- expect(modelAttachment.data.toString()).to.equal(model);
- expect(modelAttachment.content_type).to.equal('text/xml');
-};
-
afterEach(() => sinon.restore());
describe('generate-xform service', () => {
@@ -250,218 +240,6 @@ describe('generate-xform service', () => {
});
- describe('update', () => {
-
- it('should fail when no form found', done => {
- sinon.stub(db.medic, 'get').rejects('boom');
- service.update('form:missing')
- .then(() => done(new Error('expected error to be thrown')))
- .catch(err => {
- expect(err.name).to.equal('boom');
- expect(db.medic.get.callCount).to.equal(1);
- expect(db.medic.get.args[0][0]).to.equal('form:missing');
- done();
- });
- });
-
- it('should do nothing when doc does not have form attachment', () => {
- sinon.stub(db.medic, 'get').resolves({ _attachments: { image: {} } });
- sinon.stub(db.medic, 'put');
- return service.update('form:exists').then(() => {
- expect(db.medic.get.callCount).to.equal(1);
- expect(db.medic.put.callCount).to.equal(0);
- });
- });
-
- it('should do nothing when the attachments are up to date', () => {
- const formXml = '';
- const currentForm = '
';
- const currentModel = '';
- sinon.stub(db.medic, 'get').resolves({ _attachments: {
- 'xform.xml': { data: Buffer.from(formXml) },
- 'form.html': { data: Buffer.from(currentForm) },
- 'model.xml': { data: Buffer.from(currentModel) }
- } });
- sinon.stub(service, 'generate').resolves({ form: currentForm, model: currentModel });
- sinon.stub(db.medic, 'put');
- return service.update('form:exists').then(() => {
- expect(service.generate.callCount).to.equal(1);
- expect(service.generate.args[0][0]).to.equal(formXml);
- expect(db.medic.put.callCount).to.equal(0);
- });
- });
-
- it('should update doc when attachments do not exist', () => {
- const formXml = '';
- const newForm = 'Hello';
- const newModel = '';
- sinon.stub(db.medic, 'get').resolves({ _attachments: {
- xml: { data: Buffer.from(formXml) }
- } });
- sinon.stub(service, 'generate').resolves({ form: newForm, model: newModel });
- sinon.stub(db.medic, 'put');
- return service.update('form:exists').then(() => {
- expect(db.medic.put.callCount).to.equal(1);
- expectAttachments(db.medic.put.args[0][0], newForm, newModel);
- });
- });
-
- it('should update doc when attachments have changed', () => {
- const formXml = '';
- const currentForm = '