Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: proposal pdf download fails when instrument picker question is used in proposal question template #663

Merged
merged 28 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
51d4189
fix for pdf download issue when instrument picker question is present…
Bhaswati1148 Jul 19, 2024
a4c8279
added a new test case to test the pdf download scenario when instrume…
Bhaswati1148 Jul 19, 2024
bded21b
1.Allow only integers in request time field
Bhaswati1148 Jul 19, 2024
9cc2ad6
a small fix for the newly added test case
Bhaswati1148 Jul 19, 2024
774b437
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 19, 2024
265ca64
fix test
Bhaswati1148 Jul 19, 2024
d172ff1
Merge branch '1111-pdf-download-failing' of https://github.com/UserOf…
Bhaswati1148 Jul 19, 2024
e27989c
Merge branch 'develop' into 1111-pdf-download-failing
martin-trajanovski Jul 22, 2024
bcf2304
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 22, 2024
c47d207
sql query to update old instrument picker answers
Bhaswati1148 Jul 23, 2024
b00c352
removing the sql block to update multiple instrument ids answers as e…
Bhaswati1148 Jul 23, 2024
7fc9fbc
added pl/sql block to update answers with multiple instrument ids
Bhaswati1148 Jul 24, 2024
f5911b7
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 24, 2024
f2589bb
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 24, 2024
f6b01a9
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 25, 2024
06ccde6
modifying the update query for single instrument id
Bhaswati1148 Jul 25, 2024
0c9ccc9
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 26, 2024
4ab6e3c
Merge branch 'develop' into 1111-pdf-download-failing
martin-trajanovski Jul 29, 2024
1e2a1d0
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Jul 31, 2024
b802d1b
Merge branch 'develop' into 1111-pdf-download-failing
martin-trajanovski Aug 2, 2024
8452849
Merge branch 'develop' into 1111-pdf-download-failing
martin-trajanovski Aug 5, 2024
56184e6
Merge branch 'develop' into 1111-pdf-download-failing
martin-trajanovski Aug 5, 2024
40210d0
Merge branch 'develop' into 1111-pdf-download-failing
martin-trajanovski Aug 6, 2024
6a36ca3
removing check so that test case runs on e2e scenario
Bhaswati1148 Aug 6, 2024
31eec0f
adding a check so that download pdf test case skips for stfc environment
Bhaswati1148 Aug 7, 2024
732e890
duo-validation version upgrade
Bhaswati1148 Aug 7, 2024
b72e94a
duo-validation version upgrade
Bhaswati1148 Aug 7, 2024
6cb1c46
Merge branch 'develop' into 1111-pdf-download-failing
Bhaswati1148 Aug 7, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
DO
$$
BEGIN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to run this against our develop database and get back with approval if everything works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Bhaswati1148 after running this migration locally I noticed that it works well when we had value just as number for example:
this answer:

{
  "value": 4
}

was converted to this:

{
  "value": {
    "instrumentId": "4",
    "timeRequested": null
  }
}

but it seems to have a problem when converting some other cases like for example:
this one:

{
  "value": [4]
}

was converted to this:

{
  "value": {
    "instrumentId": "[4]",
    "timeRequested": null
  }
}

which I think it is wrong. It should be something like:

{
  "value": [{
    "instrumentId": "4",
    "timeRequested": null
  }]
}

And then another example is where we have:

{
  "value": [19, 20, 21]
}

converted to:

{
  "value": {
    "instrumentId": "[19, 20, 21]",
    "timeRequested": null
  }
}

which I also think it is wrong and it should be:

{
  "value": [{
    "instrumentId": "19",
    "timeRequested": null
  },
  {
      "instrumentId": "20",
      "timeRequested": null
   },
  {
      "instrumentId": "21",
      "timeRequested": null
  }]
}

IF register_patch('0158_UpdateOldInstrumentPickerAnswers.sql', 'Bhaswati Dey', 'Update old instrument picker answers', '2024-07-23') THEN
BEGIN
--QUERY TO UPDATE ANSWERS WITH SINGLE INSTRUMENT ID
UPDATE answers
SET answer = jsonb_set(answer, '{value}', jsonb_build_object('instrumentId', answer->>'value','timeRequested',null))
WHERE jsonb_typeof(answer->'value')='number' and question_id IN (SELECT question_id FROM questions WHERE data_type='INSTRUMENT_PICKER' AND
(default_config->'isMultipleSelect' IS NULL OR default_config->>'isMultipleSelect' = 'false'));

--SQL BLOCK TO UPDATE ANSWERS WITH MULTIPLE INSTRUMENT IDS
DECLARE rec record;
BEGIN
FOR rec IN
WITH item AS (
SELECT ('{value,' || pos - 1 || '}')::text[] AS path, answer->'value'->((pos - 1)::int) AS instrumentId, answer_id FROM answers,
jsonb_array_elements(answer->'value') WITH ordinality arr(item, pos) WHERE question_id IN (
SELECT question_id FROM questions WHERE data_type='INSTRUMENT_PICKER' AND default_config->>'isMultipleSelect' = 'true')
)SELECT * FROM item

LOOP
IF jsonb_typeof(rec.instrumentId)='number' THEN
UPDATE answers a
SET answer = jsonb_set(answer, rec.path, jsonb_build_object('instrumentId',rec.instrumentId::text,'timeRequested',null))
WHERE a.answer_id = rec.answer_id;
END IF;
END LOOP;
END;
END;
END IF;
END;
$$
LANGUAGE plpgsql;
9 changes: 5 additions & 4 deletions apps/backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@user-office-software/duo-localisation": "^1.2.0",
"@user-office-software/duo-logger": "^2.1.1",
"@user-office-software/duo-message-broker": "^1.6.0",
"@user-office-software/duo-validation": "^5.1.5",
"@user-office-software/duo-validation": "^5.1.8",
"@user-office-software/openid": "^1.4.0",
"await-to-js": "^2.1.1",
"bcryptjs": "^2.4.3",
Expand Down
85 changes: 66 additions & 19 deletions apps/backend/src/factory/pdf/proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { ReviewDataSource } from '../../datasources/ReviewDataSource';
import { SampleDataSource } from '../../datasources/SampleDataSource';
import { UserDataSource } from '../../datasources/UserDataSource';
import { DownloadOptions } from '../../middlewares/factory/factoryServices';
import { Call } from '../../models/Call';
import { GenericTemplate } from '../../models/GenericTemplate';
import { Instrument } from '../../models/Instrument';
import { Proposal } from '../../models/Proposal';
import { getTopicActiveAnswers } from '../../models/ProposalModelFunctions';
import { QuestionaryStep } from '../../models/Questionary';
import { Answer, QuestionaryStep } from '../../models/Questionary';
import { isRejection } from '../../models/Rejection';
import { Review } from '../../models/Review';
import { Sample } from '../../models/Sample';
Expand All @@ -27,6 +29,7 @@ import {
} from '../../models/TechnicalReview';
import { DataType } from '../../models/Template';
import { BasicUserDetails, UserWithRole } from '../../models/User';
import { InstrumentPickerConfig } from '../../resolvers/types/FieldConfig';
import { PdfTemplate } from '../../resolvers/types/PdfTemplate';
import { getFileAttachments, Attachment } from '../util';
import {
Expand Down Expand Up @@ -100,6 +103,43 @@ const getQuestionary = async (questionaryId: number) => {
return questionary;
};

const instrumentPickerAnswer = (
answer: Answer,
instruments: Instrument[],
call: Call
): string => {
const instrumentPickerConfig = answer.config as InstrumentPickerConfig;
if (instrumentPickerConfig.requestTime) {
const instrumentWithTime = instruments?.map((i) => {
const filtered = Array.isArray(answer.value)
? answer.value.find(
(v: {
instrumentId: string;
instrumentName: string;
timeRequested: number;
}) => Number(v.instrumentId) == i.id
)
: answer.value;

return (
i.name +
' (' +
filtered.timeRequested +
' ' +
call.allocationTimeUnit +
') '
);
});

return instrumentWithTime?.join(', ');
} else {
const instrumentWithTime = instruments?.length
? instruments.map((instrument) => instrument.name).join(', ')
: '';

return instrumentWithTime;
}
};
const addTopicInformation = async (
proposalPDFData: ProposalPDFData,
questionarySteps: QuestionaryStep[],
Expand Down Expand Up @@ -147,17 +187,23 @@ const addTopicInformation = async (
)
.map((genericTemplate) => genericTemplate);
} else if (answer.question.dataType === DataType.INSTRUMENT_PICKER) {
const instrumentIds = Array.isArray(answer.value)
? answer.value
: [answer.value];
const ids = Array.isArray(answer.value)
? answer.value.map((v: { instrumentId: string }) =>
Number(v.instrumentId)
)
: [Number(answer.value?.instrumentId || '0')];
const instrumentDataSource = container.resolve<InstrumentDataSource>(
Tokens.InstrumentDataSource
);
const instruments =
await instrumentDataSource.getInstrumentsByIds(instrumentIds);
answer.value = instruments?.length
? instruments.map((instrument) => instrument.name).join(', ')
: '';
const callDataSource = container.resolve<CallDataSource>(
Tokens.CallDataSource
);
const instruments = await instrumentDataSource.getInstrumentsByIds(ids);

const call = await callDataSource.getCallByQuestionId(
answer.question.id
);
answer.value = instrumentPickerAnswer(answer, instruments, call);
}
}

Expand Down Expand Up @@ -333,17 +379,18 @@ export const collectProposalPDFData = async (
)
.map((genericTemplate) => genericTemplate);
} else if (answer.question.dataType === DataType.INSTRUMENT_PICKER) {
const instrumentIds = Array.isArray(answer.value)
? answer.value
: [answer.value];
const ids = Array.isArray(answer.value)
? answer.value.map((v: { instrumentId: string }) =>
Number(v.instrumentId)
)
: [Number(answer.value?.instrumentId || '0')];
const instruments =
await baseContext.queries.instrument.getInstrumentsByIds(
user,
instrumentIds
);
answer.value = instruments?.length
? instruments.map((instrument) => instrument.name).join(', ')
: '';
await baseContext.queries.instrument.getInstrumentsByIds(user, ids);
const call = await baseContext.queries.call.getCallByQuestionId(
user,
answer.question.id
);
answer.value = instrumentPickerAnswer(answer, instruments, call);
}
}

Expand Down
156 changes: 156 additions & 0 deletions apps/e2e/cypress/e2e/proposals.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FeatureId,
} from '@user-office-software-libs/shared-types';
import { DateTime } from 'luxon';
import PdfParse from 'pdf-parse';

import featureFlags from '../support/featureFlags';
import initialDBData from '../support/initialDBData';
Expand Down Expand Up @@ -41,6 +42,8 @@ context('Proposal tests', () => {
let createdWorkflowId: number;
let createdProposalPk: number;
let createdProposalId: string;
let createdCallId: number;
let createdTemplateId: number;
const textQuestion = faker.random.words(2);

const currentDayStart = DateTime.now().startOf('day');
Expand Down Expand Up @@ -1650,4 +1653,157 @@ context('Proposal tests', () => {
).should('exist');
});
});
describe('Proposal PDF generation with instrument picker question', () => {
const instrumentPickerQuestion = 'Select your Instrument';
const instrument = {
name: 'Instrument 1',
shortCode: 'Instrument 1',
description: 'Instrument 1',
managerUserId: initialDBData.users.user1.id,
};
const instrument2 = {
name: 'Instrument 2',
shortCode: 'Instrument 2',
description: 'Instrument 2',
managerUserId: initialDBData.users.user1.id,
};
let topicId: number;
let instrumentPickerQuestionId: string;

beforeEach(() => {
// NOTE: Stop the web application and clearly separate the end-to-end tests by visiting the blank about page after each test.
// This prevents flaky tests with some long-running network requests from one test to finish in the next and unexpectedly update the app.
cy.window().then((win) => {
win.location.href = 'about:blank';
});
cy.resetDB();
cy.getAndStoreFeaturesEnabled();
cy.createTemplate({
name: 'Proposal Template with Instrument Picker',
groupId: TemplateGroupId.PROPOSAL,
}).then((result) => {
if (result.createTemplate) {
createdTemplateId = result.createTemplate.templateId;

cy.createTopic({
templateId: createdTemplateId,
sortOrder: 1,
}).then((topicResult) => {
if (!topicResult.createTopic) {
throw new Error('Can not create topic');
}

topicId =
topicResult.createTopic.steps[
topicResult.createTopic.steps.length - 1
].topic.id;
cy.createQuestion({
categoryId: TemplateCategoryId.PROPOSAL_QUESTIONARY,
dataType: DataType.INSTRUMENT_PICKER,
}).then((result) => {
instrumentPickerQuestionId = result.createQuestion.id;
cy.updateQuestion({
id: instrumentPickerQuestionId,
question: instrumentPickerQuestion,
config: `{"variant":"dropdown","isMultipleSelect":false,"required":true,"requestTime":true}`,
});
cy.createQuestionTemplateRelation({
questionId: instrumentPickerQuestionId,
templateId: createdTemplateId,
sortOrder: 0,
topicId: topicId,
});
});
});
}
});
cy.createCall({
...newCall,
allocationTimeUnit: AllocationTimeUnits.DAY,
proposalWorkflowId: initialDBData.workflows.defaultWorkflow.id,
templateId: createdTemplateId,
}).then((response) => {
if (response.createCall) {
createdCallId = response.createCall.id;
cy.createInstrument(instrument).then((result) => {
cy.assignInstrumentToCall({
callId: createdCallId,
instrumentFapIds: [{ instrumentId: result.createInstrument.id }],
});
});
cy.createInstrument(instrument2).then((result) => {
cy.assignInstrumentToCall({
callId: createdCallId,
instrumentFapIds: [{ instrumentId: result.createInstrument.id }],
});
});
}
});
cy.login('officer');
cy.visit('/');
});
it('Should be able to download proposal pdf for a proposal which contains instrument picker question', function () {
if (!featureFlags.getEnabledFeatures().get(FeatureId.SCHEDULER)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason we are not running these in e2e mode and only stfc mode?

Copy link
Contributor Author

@Bhaswati1148 Bhaswati1148 Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case was failing without this check and now I don't remember the message shown for the failed test case.
However when I checked in other places like here and here I found this check was there, so I added that in the new test case and it worked.
Sorry I don't know the reason why this needs to be added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flag is often used to check which config we are running in (e2e vs stfc) so we are skipping the test in e2e mode (which aligns more closly with ess config) since this is not something that is stfc specific (correct me if im wrong) I think we should have it working in both modes

/*temporarily skipping, there is some issue on github actions which fails
when downloading pdfs specifically in stfc enviroment
(error message - Bad status code : 500)
This test passes in local environment.
*/
this.skip();
}
cy.createProposal({ callId: createdCallId }).then((result) => {
if (result.createProposal) {
createdProposalPk = result.createProposal.primaryKey;
createdProposalId = result.createProposal.proposalId;
const proposal = result.createProposal;
cy.updateProposal({
proposalPk: result.createProposal.primaryKey,
proposerId: proposer.id,
title: title,
abstract: abstract,
});
cy.answerTopic({
isPartialSave: false,
questionaryId: proposal.questionaryId,
topicId: topicId,
answers: [
{
questionId: instrumentPickerQuestionId,
value: `{"value":{"instrumentId": "1", "timeRequested": "1"}}`,
},
],
});
cy.submitProposal({
proposalPk: result.createProposal.primaryKey,
});

const token = window.localStorage.getItem('token');

if (!token) {
throw new Error('Token not provided');
}
const currentYear = new Date().getFullYear();
const downloadedFileName = `${createdProposalId}_${initialDBData.users.officer.lastName}_${currentYear}.pdf`;
const downloadsFolder = Cypress.config('downloadsFolder');
const downloadFilePath = `${downloadsFolder}/${downloadedFileName}`;

cy.task('downloadFile', {
url: `${Cypress.config(
'baseUrl'
)}/download/pdf/proposal/${createdProposalPk}`,
token: token,
filename: downloadedFileName,
downloadsFolder: downloadsFolder,
});

cy.task('readPdf', downloadFilePath).then((args) => {
const { text } = args as PdfParse.Result;
const instrumentPickerAnswer = 'Instrument 1 (1 day)';
expect(text).to.include(title);
expect(text).to.include(instrumentPickerAnswer);
});
}
});
});
});
});
Loading
Loading