diff --git a/app/src/db/migrations/20241016164117__051_modify_submissions_data_vw.js b/app/src/db/migrations/20241016164117__051_modify_submissions_data_vw.js new file mode 100644 index 000000000..047051eff --- /dev/null +++ b/app/src/db/migrations/20241016164117__051_modify_submissions_data_vw.js @@ -0,0 +1,89 @@ +exports.up = function (knex) { + return Promise.resolve() + .then(() => knex.schema.dropViewIfExists('submissions_data_vw')) + .then(() => + knex.schema.raw(`CREATE OR REPLACE VIEW public.submissions_data_vw + AS SELECT s."confirmationId", + s."formName", + s.version, + s."createdAt", + CASE + WHEN u.id IS NULL THEN 'public'::character varying(255) + ELSE u."fullName" + END AS "fullName", + CASE + WHEN u.id IS NULL THEN 'public'::character varying(255) + ELSE u.username + END AS username, + u.email, + fs.submission -> 'data'::text AS submission, + fs."updatedAt", + fs."updatedBy", + s.deleted, + s.draft, + s."submissionId", + s."formId", + s."formVersionId", + u.id AS "userId", + u."idpUserId", + u."firstName", + u."lastName", + s."formSubmissionStatusCode" AS status, + s."formSubmissionAssignedToFullName" AS assignee, + s."formSubmissionAssignedToEmail" AS "assigneeEmail", + fss."createdAt" AS "submittedAt" + FROM submissions_vw s + JOIN form_submission fs ON s."submissionId" = fs.id + LEFT JOIN form_submission_user fsu ON s."submissionId" = fsu."formSubmissionId" AND fsu.permission::text = 'submission_create'::text + LEFT JOIN "user" u ON fsu."userId" = u.id + JOIN ( + SELECT form_submission_status."submissionId", form_submission_status."createdAt", ROW_NUMBER() OVER (PARTITION BY form_submission_status."submissionId" ORDER BY form_submission_status."createdAt" DESC) AS rn + FROM form_submission_status where form_submission_status.code='SUBMITTED' +) fss +ON s."submissionId" = fss."submissionId" WHERE fss.rn = 1 + ORDER BY s."createdAt", s."formName", s.version;`) + ); +}; + +exports.down = function (knex) { + return Promise.resolve() + .then(() => knex.schema.dropViewIfExists('submissions_data_vw')) + .then(() => + knex.schema.raw( + `CREATE OR REPLACE VIEW public.submissions_data_vw + AS SELECT s."confirmationId", + s."formName", + s.version, + s."createdAt", + CASE + WHEN u.id IS NULL THEN 'public'::character varying(255) + ELSE u."fullName" + END AS "fullName", + CASE + WHEN u.id IS NULL THEN 'public'::character varying(255) + ELSE u.username + END AS username, + u.email, + fs.submission -> 'data'::text AS submission, + fs."updatedAt", + fs."updatedBy", + s.deleted, + s.draft, + s."submissionId", + s."formId", + s."formVersionId", + u.id AS "userId", + u."idpUserId", + u."firstName", + u."lastName", + s."formSubmissionStatusCode" AS status, + s."formSubmissionAssignedToFullName" AS assignee, + s."formSubmissionAssignedToEmail" AS "assigneeEmail" + FROM submissions_vw s + JOIN form_submission fs ON s."submissionId" = fs.id + LEFT JOIN form_submission_user fsu ON s."submissionId" = fsu."formSubmissionId" AND fsu.permission::text = 'submission_create'::text + LEFT JOIN "user" u ON fsu."userId" = u.id + ORDER BY s."createdAt", s."formName", s.version;` + ) + ); +}; diff --git a/app/src/docs/v1.api-spec.yaml b/app/src/docs/v1.api-spec.yaml index 39e68e4fe..867dd677b 100755 --- a/app/src/docs/v1.api-spec.yaml +++ b/app/src/docs/v1.api-spec.yaml @@ -4052,6 +4052,10 @@ components: createdAt: type: string example: '2020-06-04T18:49:20.672Z' + submittedAt: + type: string + description: Represents the timestamp indicating when a submission was last moved to the SUBMITTED state. If a submission is revised and resubmitted multiple times, submittedAt will update each time the state changes back to SUBMITTED, reflecting the most recent submission time. + example: '2020-06-04T18:49:20.672Z' formFieldA: type: string description: A field in the submission object diff --git a/app/src/forms/form/exportService.js b/app/src/forms/form/exportService.js index 29268cdb5..e353cb609 100644 --- a/app/src/forms/form/exportService.js +++ b/app/src/forms/form/exportService.js @@ -139,7 +139,7 @@ const service = { _submissionsColumns: (form, params) => { // Custom columns not defined - return default column selection behavior - let columns = ['submissionId', 'confirmationId', 'formName', 'version', 'createdAt', 'fullName', 'username', 'email']; + let columns = ['submissionId', 'confirmationId', 'formName', 'version', 'createdAt', 'fullName', 'username', 'email', 'submittedAt']; // if form has 'status updates' enabled in the form settings include these in export if (form.enableStatusUpdates) { columns = columns.concat(['status', 'assignee', 'assigneeEmail']); diff --git a/app/tests/unit/forms/form/exportService.spec.js b/app/tests/unit/forms/form/exportService.spec.js index b57895ee2..404576902 100644 --- a/app/tests/unit/forms/form/exportService.spec.js +++ b/app/tests/unit/forms/form/exportService.spec.js @@ -81,7 +81,18 @@ describe('export', () => { describe('type 1 / multiRowEmptySpacesCSVExport', () => { const params = { emailExport: false, - fields: ['form.submissionId', 'form.confirmationId', 'form.formName', 'form.version', 'form.createdAt', 'form.fullName', 'form.username', 'form.email', 'simpletextfield'], + fields: [ + 'form.submissionId', + 'form.confirmationId', + 'form.formName', + 'form.version', + 'form.createdAt', + 'form.fullName', + 'form.username', + 'form.email', + 'form.submittedAt', + 'simpletextfield', + ], template: 'multiRowEmptySpacesCSVExport', }; @@ -93,6 +104,7 @@ describe('export', () => { formName: 'form', version: 1, createdAt: '2024-05-03T20:56:31.270Z', + submittedAt: '2024-05-03T20:56:31.270Z', fullName: 'Pat Test', username: 'PAT_TEST', email: 'pat.test@gov.bc.ca', @@ -125,6 +137,7 @@ describe('export', () => { 'form.fullName', 'form.username', 'form.email', + 'form.submittedAt', 'dataGrid', 'dataGrid.0.simpletextfield', 'dataGrid.1.simpletextfield', @@ -143,6 +156,7 @@ describe('export', () => { formName: 'form', version: 1, createdAt: '2024-05-03T20:56:31.270Z', + submittedAt: '2024-05-03T20:56:31.270Z', fullName: 'Pat Test', username: 'PAT_TEST', email: 'pat.test@gov.bc.ca', @@ -652,6 +666,7 @@ describe('', () => { 'form.fullName', 'form.username', 'form.email', + 'form.submittedAt', 'fishermansName', 'email', 'forWhichBcLakeRegionAreYouCompletingTheseQuestions', @@ -682,7 +697,7 @@ describe('', () => { expect(exportService._getData).toBeCalledTimes(1); expect(exportService._buildCsvHeaders).toBeCalledTimes(1); // test cases - expect(fields.length).toEqual(19); + expect(fields.length).toEqual(20); }); }); @@ -716,7 +731,7 @@ describe('_submissionsColumns', () => { }; const submissions = exportService._submissionsColumns(form, params); - expect(submissions.length).toEqual(9); + expect(submissions.length).toEqual(10); expect(submissions).toEqual(expect.arrayContaining(['submissionId', 'confirmationId', 'formName', 'version', 'createdAt', 'fullName', 'username', 'email', 'submission'])); }); @@ -731,7 +746,7 @@ describe('_submissionsColumns', () => { }; const submissions = exportService._submissionsColumns(form, params); - expect(submissions.length).toEqual(10); + expect(submissions.length).toEqual(11); }); it('should return right number of columns, when 1 prefered column (draft) passed as params.', async () => { @@ -745,7 +760,7 @@ describe('_submissionsColumns', () => { }; const submissions = exportService._submissionsColumns(form, params); - expect(submissions.length).toEqual(10); + expect(submissions.length).toEqual(11); }); it('should return right number of columns, when 2 prefered column (draft & deleted) passed as params.', async () => { @@ -760,7 +775,7 @@ describe('_submissionsColumns', () => { const submissions = exportService._submissionsColumns(form, params); - expect(submissions.length).toEqual(11); + expect(submissions.length).toEqual(12); }); it('should return right number of columns, when a garbage or NON-allowed column (testCol1 & testCol2) passed as params.', async () => { @@ -774,7 +789,7 @@ describe('_submissionsColumns', () => { }; const submissions = exportService._submissionsColumns(form, params); - expect(submissions.length).toEqual(9); + expect(submissions.length).toEqual(10); }); });