Skip to content

Commit 59b5458

Browse files
committed
Copy forward enketoId from existing draft
Addresses the "existing draft is updated" case of getodk/central#385.
1 parent 7cbe643 commit 59b5458

File tree

4 files changed

+76
-21
lines changed

4 files changed

+76
-21
lines changed

lib/model/query/forms.js

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
const config = require('config');
1111
const { sql } = require('slonik');
12-
const { map, compose } = require('ramda');
12+
const { map } = require('ramda');
1313
const { Frame, into } = require('../frame');
1414
const { Actor, Blob, Form, Dataset } = require('../frames');
1515
const { getFormFields, merge, compare } = require('../../data/schema');
@@ -18,7 +18,7 @@ const { generateToken } = require('../../util/crypto');
1818
const { unjoiner, extender, updater, equals, insert, insertMany, markDeleted, markUndeleted, QueryOptions } = require('../../util/db');
1919
const { resolve, reject } = require('../../util/promise');
2020
const { splitStream } = require('../../util/stream');
21-
const { construct, noop } = require('../../util/util');
21+
const { construct, noop, pickAll } = require('../../util/util');
2222
const Option = require('../../util/option');
2323
const Problem = require('../../util/problem');
2424

@@ -42,7 +42,7 @@ const fromXls = (stream, contentType, formIdFallback, ignoreWarnings) => ({ Blob
4242
});
4343

4444
////////////////////////////////////////////////////////////////////////////////
45-
// CREATING NEW FORMS+VERSIONS
45+
// CREATING NEW FORMS
4646

4747
const _createNew = (form, def, project, publish) => ({ oneFirst, Actees, Forms }) =>
4848
Actees.provision('form', project)
@@ -117,6 +117,32 @@ createNew.audit = (form, partial, _, publish) => (log) =>
117117
: null));
118118
createNew.audit.withResult = true;
119119

120+
////////////////////////////////////////////////////////////////////////////////
121+
// CREATING NEW VERSIONS
122+
123+
// Inserts a new form def into the database for createVersion() below, setting
124+
// fields on the def according to whether the def will be the current def or the
125+
// draft def.
126+
const _createNewDef = (partial, form, publish, data) => ({ one }) => {
127+
const insertWith = (moreData) => one(insert(partial.def.with({
128+
formId: form.id,
129+
xlsBlobId: partial.xls.xlsBlobId,
130+
xml: partial.xml,
131+
...data,
132+
...moreData
133+
})));
134+
135+
if (publish === true) return insertWith({ publishedAt: new Date() });
136+
137+
// Check whether there is an existing draft that we have access to. If not,
138+
// generate a draft token.
139+
if (form.def.id == null || form.def.id !== form.draftDefId)
140+
return insertWith({ draftToken: generateToken() });
141+
142+
// Copy forward fields from the existing draft.
143+
return insertWith(pickAll(['draftToken', 'enketoId'], form.def));
144+
};
145+
120146
// creates a new version (formDef) of an existing form.
121147
//
122148
// if publish is true, the new version supplants the published (currentDefId)
@@ -126,20 +152,15 @@ createNew.audit.withResult = true;
126152
// exists.
127153
//
128154
// if field paths/types collide, the database will complain.
129-
130-
const _getDraftToken = (form) => {
131-
if ((form.def.id != null) && (form.draftDefId === form.def.id)) return form.def.draftToken;
132-
return generateToken();
133-
};
134-
155+
//
135156
// Parameters:
136157
// ===========
137158
// partial: Partial form definition of the new version
138159
// form: Form frame of existing form
139160
// publish: set true if you want new version to be published
140161
// duplicating: set true if copying form definition from previously uploaded definition, in that cases we don't check for structural change
141162
// as user has already been warned otherwise set false
142-
const createVersion = (partial, form, publish = false, duplicating = false) => async ({ run, one, Datasets, FormAttachments, Forms, Keys }) => {
163+
const createVersion = (partial, form, publish, duplicating = false) => async ({ run, Datasets, FormAttachments, Forms, Keys }) => {
143164
// Check xmlFormId match
144165
if (form.xmlFormId !== partial.xmlFormId)
145166
return reject(Problem.user.unexpectedValue({ field: 'xmlFormId', value: partial.xmlFormId, reason: 'does not match the form you are updating' }));
@@ -185,20 +206,13 @@ const createVersion = (partial, form, publish = false, duplicating = false) => a
185206
}
186207
}
187208

188-
// Make sure our new def is in the database, and mark it as either draft or current.
189-
let def = partial.def.with({ formId: form.id, keyId, xlsBlobId: partial.xls.xlsBlobId });
190-
def = ((publish === true)
191-
? def.with({ publishedAt: new Date(), xml: partial.xml })
192-
: def.with({ draftToken: _getDraftToken(form), xml: partial.xml }));
193-
const savedDef = await compose(one, insert)(def);
209+
const savedDef = await Forms._createNewDef(partial, form, publish, { schemaId, keyId });
194210

195211
// Update corresponding form
196212
await ((publish === true)
197213
? Forms._update(form, { currentDefId: savedDef.id })
198214
: Forms._update(form, { draftDefId: savedDef.id }));
199215

200-
await Forms._updateDef(new Form.Def(savedDef), { schemaId });
201-
202216
// Prepare the form fields
203217
const ids = { formId: form.id, schemaId };
204218
const fieldsForInsert = new Array(fields.length);
@@ -691,7 +705,7 @@ const _newSchema = () => ({ one }) =>
691705
.then((s) => s.id);
692706

693707
module.exports = {
694-
fromXls, _createNew, createNew, createVersion,
708+
fromXls, _createNew, createNew, _createNewDef, createVersion,
695709
publish, clearDraft,
696710
_update, update, _updateDef, del, restore, purge,
697711
clearUnneededDrafts,

lib/worker/form.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const pushDraftToEnketo = ({ Forms, enketo, env }, event) =>
1515
// if there was no draft or this form isn't the draft anymore just bail.
1616
if ((form.def.id == null) || (form.draftDefId !== form.def.id)) return;
1717

18+
// if the enketoId has been carried forward from the previous draft, then
19+
// bail.
20+
if (form.def.enketoId != null) return;
21+
1822
// if this form doesn't have a draft testing key something is broken
1923
// and wrong. still want to log a fail but bail early.
2024
if (form.def.draftToken == null) throw new Error('Could not find a draft token!');

test/integration/api/forms/draft.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('api: /projects/:id/forms (drafts)', () => {
138138
body.version.should.equal('drafty2');
139139
})))));
140140

141-
it('should keep the draft token while replacing the draft version', testService((service) =>
141+
it('should keep the draft token while replacing the draft', testService((service) =>
142142
service.login('alice', (asAlice) =>
143143
asAlice.post('/v1/projects/1/forms/simple/draft')
144144
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty"'))
@@ -161,6 +161,43 @@ describe('api: /projects/:id/forms (drafts)', () => {
161161
}));
162162
})))));
163163

164+
it('should keep the enketoId while replacing the draft', testService(async (service) => {
165+
const asAlice = await service.login('alice');
166+
await asAlice.post('/v1/projects/1/forms/simple/draft')
167+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty"'))
168+
.set('Content-Type', 'application/xml')
169+
.expect(200);
170+
await exhaust(container);
171+
const { body: draft1 } = await asAlice.get('/v1/projects/1/forms/simple/draft')
172+
.expect(200);
173+
draft1.enketoId.should.equal('::abcdefgh');
174+
await asAlice.post('/v1/projects/1/forms/simple/draft')
175+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty2"'))
176+
.set('Content-Type', 'application/xml')
177+
.expect(200);
178+
const { body: draft2 } = await asAlice.get('/v1/projects/1/forms/simple/draft')
179+
.expect(200);
180+
draft2.enketoId.should.equal('::abcdefgh');
181+
}));
182+
183+
it('should not request an enketoId from the worker while replacing the draft', testService(async (service, container) => {
184+
const asAlice = await service.login('alice');
185+
await asAlice.post('/v1/projects/1/forms/simple/draft')
186+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty"'))
187+
.set('Content-Type', 'application/xml')
188+
.expect(200);
189+
await exhaust(container);
190+
await asAlice.post('/v1/projects/1/forms/simple/draft')
191+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty2"'))
192+
.set('Content-Type', 'application/xml')
193+
.expect(200);
194+
global.enketoToken = '::ijklmnop';
195+
await exhaust(container);
196+
const { body: draft } = await asAlice.get('/v1/projects/1/forms/simple/draft')
197+
.expect(200);
198+
draft.enketoId.should.equal('::abcdefgh');
199+
}));
200+
164201
it('should copy the published form definition if not given one', testService((service) =>
165202
service.login('alice', (asAlice) =>
166203
asAlice.post('/v1/projects/1/forms/simple/draft')

test/integration/api/submissions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2724,7 +2724,7 @@ one,h,/data/h,2000-01-01T00:06,2000-01-01T00:07,-5,-6,,ee,ff
27242724
.replace(/PublicKey="[a-z0-9+/]+"/i, 'PublicKey="keytwo"')
27252725
.replace('working3', 'working4'))
27262726
]))
2727-
.then(([ form, partial ]) => Forms.createVersion(partial, form))
2727+
.then(([ form, partial ]) => Forms.createVersion(partial, form, false))
27282728
.then(() => asAlice.get('/v1/projects/1/forms/encrypted/submissions/keys')
27292729
.expect(200)
27302730
.then(({ body }) => {

0 commit comments

Comments
 (0)