Skip to content

Commit 30effbb

Browse files
committed
Move webformsEnabled to form_defs
1 parent dc66ebd commit 30effbb

7 files changed

Lines changed: 140 additions & 15 deletions

File tree

lib/model/frames/form.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
// As with Users/Actors, this information is merged together upon return via the
2626
// API. This is partly for legacy reasons: Forms and FormsDef did not used to
2727

28-
const { Frame, table, readable, writable, into, embedded, fieldTypes } = require('../frame');
28+
const { Frame, table, readable, writable, into, embedded, fieldTypes, from } = require('../frame');
2929
const { injectPublicKey, addVersionSuffix } = require('../../data/schema');
3030
const { Key } = require('./key');
3131
const { md5sum, shasum, sha256sum, digestWith, generateVersionSuffix } = require('../../util/crypto');
@@ -61,12 +61,12 @@ class Form extends Frame.define(
6161
'enketoOnceId', readable, 'acteeId',
6262
'createdAt', readable, 'updatedAt', readable,
6363
'deletedAt',
64-
'webformsEnabled', readable, writable,
6564
embedded('createdBy'), embedded('publishedBy')
6665
) {
6766
get def() { return this.aux.def; }
6867
get xls() { return this.aux.xls || {}; } // TODO/SL sort of a hack but let's see how it goes.
6968
get xml() { return this.aux.xml?.xml; }
69+
get associated() { return this.aux.formAssociated; }
7070

7171
isCurrentDraft() {
7272
return this.draftDefId && this.def?.id === this.draftDefId;
@@ -98,11 +98,15 @@ class Form extends Frame.define(
9898
? this.enketoOnceId
9999
: null;
100100

101+
// Sometime we don't request def or there is no (draft) def, in those cases we read the latest
102+
// `webformsEnabled`;
103+
const webformsEnabled = this.def?.webformsEnabled ?? this.associated?.webformsEnabled;
104+
101105
// Include deletedAt in response only if it is not null (most likely on resources about soft-deleted forms)
102106
// and also include the numeric form id (used to restore)
103107
const deletedAtIdFields = this.deletedAt ? { deletedAt: this.deletedAt, id: this.id } : null;
104108

105-
return Object.assign(Frame.prototype.forApi.call(this), { enketoId, enketoOnceId }, deletedAtIdFields);
109+
return Object.assign(Frame.prototype.forApi.call(this), { enketoId, enketoOnceId, webformsEnabled }, deletedAtIdFields);
106110
}
107111

108112
// Given an XML string, returns Promise[Object]. If the Promise rejects, the XML
@@ -155,6 +159,10 @@ class Form extends Frame.define(
155159

156160
Form.Partial = class extends Form {};
157161

162+
Form.Associated = Frame.define(
163+
from('formAssociated'), into('formAssociated'),
164+
'webformsEnabled', readable, writable
165+
);
158166

159167
////////////////////////////////////////////////////////////////////////////////
160168
// EXTENDED FORM
@@ -202,7 +210,8 @@ Form.Def = class extends Frame.define(
202210
'sha256', readable, 'draftToken', readable,
203211
'enketoId', readable, 'createdAt',
204212
'publishedAt', readable, 'xlsBlobId',
205-
'name', readable, 'schemaId'
213+
'name', readable, 'schemaId',
214+
'webformsEnabled', readable, writable
206215
) {
207216
isDraft() {
208217
return this.publishedAt == null;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const up = async (db) => {
2+
await db.raw(`
3+
ALTER TABLE form_defs
4+
ADD COLUMN "webformsEnabled" boolean;
5+
`);
6+
7+
await db.raw(`
8+
UPDATE form_defs
9+
SET "webformsEnabled" = forms."webformsEnabled"
10+
FROM forms
11+
WHERE form_defs.id = forms."currentDefId"
12+
`);
13+
14+
await db.raw(`
15+
ALTER TABLE forms
16+
DROP COLUMN "webformsEnabled";
17+
`);
18+
};
19+
20+
const down = async (db) => {
21+
await db.raw(`
22+
ALTER TABLE forms
23+
ADD COLUMN "webformsEnabled" boolean NOT NULL DEFAULT FALSE;
24+
`);
25+
26+
await db.raw(`
27+
UPDATE forms
28+
SET "webformsEnabled" =
29+
(SELECT fd."webformsEnabled" FROM form_defs fd WHERE fd.id = forms."currentDefId");
30+
`);
31+
32+
await db.raw(`
33+
ALTER TABLE form_defs
34+
DROP COLUMN "webformsEnabled";
35+
`);
36+
};
37+
38+
module.exports = { up, down };

lib/model/query/analytics.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,8 @@ group by (p."id", f."state")`);
323323
const countFormsWebformsEnabled = () => ({ all }) => all(sql`
324324
select f."projectId" as "projectId", count(f."id") as total
325325
from forms as f
326-
where f."webformsEnabled" is true
326+
join form_defs as fd on fd.id = f."currentDefId"
327+
where fd."webformsEnabled" is true
327328
group by f."projectId"`);
328329

329330
// This query checks the audit log for 'form.delete' actions

lib/model/query/forms.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ with sch as
110110
values (default)
111111
returning *),
112112
def as
113-
(insert into form_defs ("formId", "schemaId", xml, name, hash, sha, sha256, version, "keyId", "xlsBlobId", "draftToken", "enketoId", "createdAt")
114-
select nextval(pg_get_serial_sequence('forms', 'id')), sch.id, ${form.xml}, ${def.name}, ${def.hash}, ${def.sha}, ${def.sha256}, ${def.version}, ${def.keyId}, ${form.xls.xlsBlobId || null}, ${def.draftToken || null}, ${def.enketoId || null}, clock_timestamp()
113+
(insert into form_defs ("formId", "schemaId", xml, name, hash, sha, sha256, version, "keyId", "xlsBlobId", "draftToken", "enketoId", "createdAt", "webformsEnabled")
114+
select nextval(pg_get_serial_sequence('forms', 'id')), sch.id, ${form.xml}, ${def.name}, ${def.hash}, ${def.sha}, ${def.sha256}, ${def.version}, ${def.keyId}, ${form.xls.xlsBlobId || null}, ${def.draftToken || null}, ${def.enketoId || null}, clock_timestamp(), ${def.webformsEnabled}
115115
from sch
116116
returning *),
117117
form as
118-
(insert into forms (id, "xmlFormId", state, "projectId", "draftDefId", "acteeId", "enketoId", "enketoOnceId", "createdAt", "webformsEnabled")
119-
select def."formId", ${form.xmlFormId}, ${form.state || 'open'}, ${project.id}, def.id, ${form.acteeId}, ${form.enketoId || null}, ${form.enketoOnceId || null}, def."createdAt", ${form.webformsEnabled || DEFAULT_WEBFORMS_ENABLED} from def
118+
(insert into forms (id, "xmlFormId", state, "projectId", "draftDefId", "acteeId", "enketoId", "enketoOnceId", "createdAt")
119+
select def."formId", ${form.xmlFormId}, ${form.state || 'open'}, ${project.id}, def.id, ${form.acteeId}, ${form.enketoId || null}, ${form.enketoOnceId || null}, def."createdAt" from def
120120
returning forms.*)
121121
select id from form`)
122122
.then(() => Forms.getByProjectAndXmlFormId(project.id, form.xmlFormId, Form.DraftVersion))
@@ -149,10 +149,12 @@ const createNew = (partial, project) => async ({ Actees, Datasets, FormAttachmen
149149
0.5
150150
);
151151

152+
const webformsEnabled = keyId ? false : DEFAULT_WEBFORMS_ENABLED;
153+
152154
// Save draft form
153155
const savedForm = await Forms._createNew(
154156
partial.with({ acteeId }),
155-
partial.def.with({ keyId, draftToken, enketoId }),
157+
partial.def.with({ keyId, draftToken, enketoId, webformsEnabled }),
156158
project
157159
);
158160

@@ -273,7 +275,9 @@ const createVersion = (partial, form, publish, duplicating = false) => async ({
273275
// will just copy over whatever enketo ids do or don't exist already.
274276

275277
// Save draft def
276-
const savedDef = await Forms._createNewDef(partial, form, publish, { draftToken, enketoId, schemaId, keyId });
278+
// In production, form.associated should never be null but in testing code we call Form.createVersion directly
279+
const webformsEnabled = keyId ? false : (form.associated?.webformsEnabled ?? DEFAULT_WEBFORMS_ENABLED);
280+
const savedDef = await Forms._createNewDef(partial, form, publish, { draftToken, enketoId, schemaId, keyId, webformsEnabled });
277281

278282
// Prepare the form fields
279283
if (!match)
@@ -655,6 +659,10 @@ const _getSql = ((fields, extend, options, defVersion, deleted = false, actorId)
655659
defVersion === Form.WithoutDef ? sql`${fields} FROM forms` :
656660
sql`${fields} FROM forms LEFT OUTER JOIN form_defs on ${versionJoinCondition(defVersion)}`
657661
}
662+
JOIN (
663+
SELECT forms.id, form_defs."webformsEnabled" FROM forms
664+
JOIN form_defs ON form_defs.id=coalesce(forms."currentDefId", forms."draftDefId")
665+
) AS "formAssociated" ON "formAssociated".id = forms.id
658666
${extend|| sql`
659667
left outer join
660668
(select "formId", count(id)::integer as "submissions", max("createdAt") as "lastSubmission",
@@ -707,7 +715,7 @@ const _get = (exec, options, defVersion, xml, deleted, actorId) => {
707715
if (xml !== Form.IncludeXml && xml !== Form.WithoutXml) throw new Error('Must use Form.WithoutXml or Form.IncludeXml; actually got: ' + xml.toString());
708716
if (defVersion === Form.WithoutDef && xml === Form.IncludeXml) throw new Error('Cannot request xml without a form def');
709717

710-
const frames = [ Form ];
718+
const frames = [ Form, Form.Associated ];
711719
if (defVersion !== Form.WithoutDef) frames.push(Form.Def);
712720
if (xml === Form.IncludeXml) frames.push(Form.Xml);
713721
return extender(...frames)(Form.Extended, Actor.into('createdBy'))(_getSql)(exec, options, defVersion, deleted, actorId);

lib/resources/forms.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,19 @@ module.exports = (service, endpoint, anonymousEndpoint) => {
246246
// already that there is no blob hash.
247247
FormAttachments.update(form, attachment, null, body.dataset ? dataset.id : null))))));
248248

249+
service.patch('/projects/:projectId/forms/:xmlFormId/draft', endpoint(async ({ Forms }, { auth, params, body }) => {
250+
const form = await Forms.getByProjectAndXmlFormId(params.projectId, params.xmlFormId, Form.DraftVersion)
251+
.then(getOrNotFound);
252+
253+
await auth.canOrReject('form.update', form);
254+
255+
const defFromRequest = Form.Def.fromApi(body);
256+
257+
await Forms._updateDef(form.def, defFromRequest);
258+
259+
const webformsEnabled = defFromRequest.webformsEnabled ?? form.def.webformsEnabled;
260+
return { ...form.forApi(), webformsEnabled };
261+
}));
249262

250263
service.delete('/projects/:projectId/forms/:xmlFormId/draft', endpoint(({ Audits, Forms, Submissions }, { params, auth }) =>
251264
Forms.getByProjectAndXmlFormId(params.projectId, params.xmlFormId, Form.WithoutDef)
@@ -367,10 +380,13 @@ module.exports = (service, endpoint, anonymousEndpoint) => {
367380
// PRIMARY-FORM SPECIFIC ENDPOINTS
368381

369382
service.patch('/projects/:projectId/forms/:xmlFormId', endpoint(({ Forms }, { auth, params, body }) =>
370-
Forms.getByProjectAndXmlFormId(params.projectId, params.xmlFormId, Form.WithoutDef)
383+
Forms.getByProjectAndXmlFormId(params.projectId, params.xmlFormId, Form.AnyVersion)
371384
.then(getOrNotFound)
372385
.then((form) => auth.canOrReject('form.update', form))
373-
.then((form) => Forms.update(form, Form.fromApi(body)))
386+
.then((form) => Promise.all([
387+
Forms.update(form, Form.fromApi(body)),
388+
Forms._updateDef(form.def, Form.Def.fromApi(body))
389+
]))
374390
// TODO: sucks to have to re-request but this shouldn't be a perf-critical path.
375391
.then(() => Forms.getByProjectAndXmlFormId(params.projectId, params.xmlFormId, Form.AnyVersion))
376392
.then(getOrNotFound)));

lib/util/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const PURGE_DAY_RANGE = config.has('default.taskSchedule.purge')
1515

1616
const DEFAULT_WEBFORMS_ENABLED = config.has('default.webformsEnabled')
1717
? config.get('default.webformsEnabled')
18-
: false;
18+
: true;
1919

2020
module.exports = {
2121
PURGE_DAY_RANGE,

test/integration/api/forms/forms.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,59 @@ describe('api: /projects/:id/forms (create, read, update)', () => {
17721772
body.webformsEnabled.should.equal(true);
17731773
})))));
17741774

1775+
it('should update allowed fields', testService(async (service) => {
1776+
const asAlice = await service.login('alice');
1777+
1778+
await asAlice.post('/v1/projects/1/forms/simple/draft').expect(200);
1779+
1780+
// default is true now.
1781+
await asAlice.get('/v1/projects/1/forms/simple/draft').expect(200)
1782+
.then(({ body }) => {
1783+
body.webformsEnabled.should.be.true();
1784+
});
1785+
1786+
await asAlice.patch('/v1/projects/1/forms/simple/draft')
1787+
.send({ webformsEnabled: false })
1788+
.expect(200)
1789+
.then(({ body }) => {
1790+
body.webformsEnabled.should.be.false();
1791+
});
1792+
1793+
await asAlice.get('/v1/projects/1/forms/simple/draft').expect(200)
1794+
.then(({ body }) => {
1795+
body.webformsEnabled.should.be.false();
1796+
});
1797+
}));
1798+
1799+
it('should inherit webformsEnabled when creating new draft from published', testService(async (service) => {
1800+
const asAlice = await service.login('alice');
1801+
1802+
// First create a draft and enable webforms
1803+
await asAlice.post('/v1/projects/1/forms/simple/draft')
1804+
.expect(200);
1805+
await asAlice.patch('/v1/projects/1/forms/simple')
1806+
.send({ webformsEnabled: true })
1807+
.expect(200);
1808+
await asAlice.post('/v1/projects/1/forms/simple/draft/publish?version=v2')
1809+
.expect(200);
1810+
1811+
// Verify published version has webformsEnabled: true
1812+
await asAlice.get('/v1/projects/1/forms/simple')
1813+
.expect(200)
1814+
.then(({ body }) => {
1815+
body.webformsEnabled.should.equal(true);
1816+
});
1817+
1818+
// Create a new draft - should inherit webformsEnabled from published
1819+
await asAlice.post('/v1/projects/1/forms/simple/draft')
1820+
.expect(200);
1821+
await asAlice.get('/v1/projects/1/forms/simple/draft')
1822+
.expect(200)
1823+
.then(({ body }) => {
1824+
body.webformsEnabled.should.equal(true);
1825+
});
1826+
}));
1827+
17751828
it('should reject if state is invalid', testService((service) =>
17761829
service.login('alice', (asAlice) =>
17771830
asAlice.patch('/v1/projects/1/forms/simple')

0 commit comments

Comments
 (0)