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

Com 3718 #2391

Merged
merged 7 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion src/controllers/questionnaireHistoryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,15 @@ const addQuestionnaireHistory = async (req) => {
}
};

module.exports = { addQuestionnaireHistory };
const update = async (req) => {
try {
await QuestionnaireHistoryHelper.updateQuestionnaireHistory(req.params._id);

return { message: translate[language].questionnaireHistoryUpdated };
} catch (e) {
req.log('error', e);
return Boom.isBoom(e) ? e : Boom.badImplementation(e);
}
};

module.exports = { addQuestionnaireHistory, update };
1 change: 1 addition & 0 deletions src/data/rights.js

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

3 changes: 3 additions & 0 deletions src/helpers/questionnaireHistories.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ exports.addQuestionnaireHistory = async (payload) => {
{ ...payload, company: traineesCompanyAtCourseRegistrationList[0].company, ...(timeline && { timeline }) }
);
};

exports.updateQuestionnaireHistory = async questionnaireHistoryId => QuestionnaireHistory
.updateOne({ _id: questionnaireHistoryId }, { $set: { isValidated: true } });
3 changes: 2 additions & 1 deletion src/helpers/questionnaires.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ const formatQuestionnaireAnswersWithCourse = async (courseId, questionnaireAnswe
};

const getFollowUpForReview = async (questionnaire, courseId) => {
const followUp = questionnaire.histories.map(h => pick(h, ['user', 'questionnaireAnswersList', 'timeline']));
const fieldsToPick = ['user', 'questionnaireAnswersList', 'timeline', '_id', 'isValidated'];
ulysseferreira marked this conversation as resolved.
Show resolved Hide resolved
const followUp = questionnaire.histories.map(h => pick(h, fieldsToPick));

const course = await Course.findOne({ _id: courseId })
.select('subProgram companies misc type holding trainees')
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ module.exports = {
publishedQuestionnaireWithSameTypeExists: 'A questionnaire with the same type is already published.',
questionnaireUpdated: 'questionnaire updated.',
questionnaireHistoryCreated: 'Questionnaire history created.',
questionnaireHistoryUpdated: 'Questionnaire history updated.',
questionnaireQRCodeGenerated: 'Questionnaire QR Code generated.',
/* QuestionnaireHistories */
questionnaireHistoryConflict: 'A questionnaire history already exists.',
Expand Down Expand Up @@ -610,6 +611,7 @@ module.exports = {
publishedQuestionnaireWithSameTypeExists: 'Un questionnaire du même type est déjà publié.',
questionnaireUpdated: 'Questionnaire mis à jour.',
questionnaireHistoryCreated: 'Historique de questionnaire créé.',
questionnaireHistoryUpdated: 'Historique de questionnaire mis à jour.',
questionnaireQRCodeGenerated: 'QR Code généré.',
/* QuestionnaireHistories */
questionnaireHistoryConflict: 'Vous avez déjà répondu à ce questionnaire.',
Expand Down
1 change: 1 addition & 0 deletions src/models/QuestionnaireHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const QuestionnaireHistorySchema = mongoose.Schema({
company: { type: mongoose.Schema.Types.ObjectId, ref: 'Company', required: true },
origin: { type: String, enum: ORIGIN_OPTIONS, required: true, immutable: true, default: MOBILE },
timeline: { type: String, enum: TIMELINE_OPTIONS, immutable: true },
isValidated: { type: Boolean },
}, { timestamps: true });

QuestionnaireHistorySchema.pre('find', validateQuery);
Expand Down
34 changes: 34 additions & 0 deletions src/routes/preHandlers/questionnaireHistories.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const Boom = require('@hapi/boom');
const get = require('lodash/get');
const Questionnaire = require('../../models/Questionnaire');
const User = require('../../models/User');
const Course = require('../../models/Course');
const QuestionnaireHistory = require('../../models/QuestionnaireHistory');
const { END_COURSE } = require('../../helpers/constants');
const UtilsHelper = require('../../helpers/utils');
const { checkQuestionnaireAnswersList } = require('./utils');

exports.authorizeAddQuestionnaireHistory = async (req) => {
Expand All @@ -17,3 +21,33 @@ exports.authorizeAddQuestionnaireHistory = async (req) => {

return null;
};

exports.authorizeQuestionnaireHistoryUpdate = async (req) => {
const { _id: questionnaireHistoryId } = req.params;
const { trainerAnswers } = req.payload;
const credentials = get(req, 'auth.credentials');

const questionnaireHistory = await QuestionnaireHistory
.findOne(
{ _id: questionnaireHistoryId, timeline: END_COURSE },
{ questionnaire: 1, questionnaireAnswersList: 1, course: 1 }
)
.populate({ path: 'course', select: 'trainer' })
.lean();
if (!questionnaireHistory) throw Boom.notFound();

const courseTrainer = questionnaireHistory.course.trainer;
const loggedUserIsCourseTrainer = UtilsHelper.areObjectIdsEquals(courseTrainer, credentials._id);
if (!loggedUserIsCourseTrainer) throw Boom.forbidden();

const cardIds = trainerAnswers.map(answer => answer.card);
const questionnaire = await Questionnaire
.findOne({ _id: questionnaireHistory.questionnaire, cards: { $in: cardIds } })
.lean();
if (!questionnaire) throw Boom.notFound();

const answersHasGoodLength = trainerAnswers.length === questionnaireHistory.questionnaireAnswersList.length;
if (!answersHasGoodLength) throw Boom.badRequest();

return null;
};
22 changes: 20 additions & 2 deletions src/routes/questionnaireHistories.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

const Joi = require('joi');
Joi.objectId = require('joi-objectid')(Joi);
const { addQuestionnaireHistory } = require('../controllers/questionnaireHistoryController');
const { addQuestionnaireHistory, update } = require('../controllers/questionnaireHistoryController');
const { WEBAPP } = require('../helpers/constants');
const { authorizeAddQuestionnaireHistory } = require('./preHandlers/questionnaireHistories');
const {
authorizeAddQuestionnaireHistory,
authorizeQuestionnaireHistoryUpdate,
} = require('./preHandlers/questionnaireHistories');

exports.plugin = {
name: 'routes-questionnaire-histories',
Expand All @@ -30,5 +33,20 @@ exports.plugin = {
},
handler: addQuestionnaireHistory,
});
server.route({
method: 'PUT',
path: '/{_id}',
options: {
validate: {
params: Joi.object({ _id: Joi.objectId().required() }),
payload: Joi.object({
trainerAnswers: Joi.array().items(Joi.object({ card: Joi.objectId() })),
ulysseferreira marked this conversation as resolved.
Show resolved Hide resolved
}),
},
auth: { scope: ['questionnairehistories:edit'] },
pre: [{ method: authorizeQuestionnaireHistoryUpdate }],
},
handler: update,
});
},
};
117 changes: 116 additions & 1 deletion tests/integration/questionnaireHistories.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const {
questionnaireHistoriesUsersList,
cardsList,
coursesList,
questionnaireHistoriesList,
} = require('./seed/questionnaireHistoriesSeed');
const { getTokenByCredentials } = require('./helpers/authentication');
const { getTokenByCredentials, getToken } = require('./helpers/authentication');
const { companyWithoutSubscription } = require('../seed/authCompaniesSeed');
const { noRoleNoCompany } = require('../seed/authUsersSeed');
const QuestionnaireHistory = require('../../src/models/QuestionnaireHistory');
Expand Down Expand Up @@ -440,3 +441,117 @@ describe('QUESTIONNAIRE HISTORIES ROUTES - POST /questionnairehistories', () =>
});
});
});

describe('QUESTIONNAIRE HISTORIES ROUTES - PUT /questionnairehistories/{_id}', () => {
let authToken;
beforeEach(populateDB);
const endSelfPositionningQuestionnaireHistoryId = questionnaireHistoriesList[1]._id;

describe('TRAINER', () => {
beforeEach(async () => {
authToken = await getToken('trainer');
});

it('should update questionnaireHistory', async () => {
const payload = { trainerAnswers: [{ card: cardsList[1]._id }] };
const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${endSelfPositionningQuestionnaireHistoryId}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(200);
const questionnaireHistory = await QuestionnaireHistory
.countDocuments({ _id: endSelfPositionningQuestionnaireHistoryId, timeline: END_COURSE, isValidated: true });
expect(questionnaireHistory).toBe(1);
});

it('should return 404 if questionnaireHistory doesn\'t exist', async () => {
ulysseferreira marked this conversation as resolved.
Show resolved Hide resolved
const payload = { trainerAnswers: [{ card: cardsList[1]._id }] };
const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${new ObjectId()}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(404);
});

it('should return 404 if questionnaireHistory has START_COURSE timeline', async () => {
const startSelfPositionningQuestionnaireHistoryId = questionnaireHistoriesList[2]._id;

const payload = { trainerAnswers: [{ card: cardsList[1]._id }] };
const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${startSelfPositionningQuestionnaireHistoryId}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(404);
});

it('should return 404 if card is not in questionnaire', async () => {
const payload = { trainerAnswers: [{ card: cardsList[2]._id }] };
const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${endSelfPositionningQuestionnaireHistoryId}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(404);
});

it('should return 400 if trainerAnswers has not good number of elements', async () => {
const payload = { trainerAnswers: [{ card: cardsList[1]._id }, { card: cardsList[3]._id }] };
const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${endSelfPositionningQuestionnaireHistoryId}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(400);
});
});

describe('OTHER ROLES', () => {
const roles = [
{ name: 'helper', expectedCode: 403 },
{ name: 'planning_referent', expectedCode: 403 },
{ name: 'coach', expectedCode: 403 },
];
roles.forEach((role) => {
it(`should return ${role.expectedCode} as user is ${role.name}`, async () => {
authToken = await getToken(role.name);
const payload = { trainerAnswers: [{ card: cardsList[1]._id }] };

const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${endSelfPositionningQuestionnaireHistoryId}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(role.expectedCode);
});
});

it('should return 403 if user is not course trainer', async () => {
authToken = await getToken('training_organisation_manager');

const payload = { trainerAnswers: [{ card: cardsList[1]._id }] };
const response = await app.inject({
method: 'PUT',
url: `/questionnairehistories/${endSelfPositionningQuestionnaireHistoryId}`,
payload,
headers: { Cookie: `alenvi_token=${authToken}` },
});

expect(response.statusCode).toBe(403);
});
});
});
18 changes: 16 additions & 2 deletions tests/integration/seed/questionnaireHistoriesSeed.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Step = require('../../../src/models/Step');
const SubProgram = require('../../../src/models/SubProgram');
const CourseSlot = require('../../../src/models/CourseSlot');
const Program = require('../../../src/models/Program');
const { userList, trainerOrganisationManager, vendorAdmin } = require('../../seed/authUsersSeed');
const { userList, trainerOrganisationManager, vendorAdmin, trainer } = require('../../seed/authUsersSeed');
const { deleteNonAuthenticationSeeds } = require('../helpers/db');
const {
INTER_B2B,
Expand All @@ -23,6 +23,7 @@ const {
SELF_POSITIONNING,
ON_SITE,
END_COURSE,
START_COURSE,
} = require('../../../src/helpers/constants');
const { authCompany, companyWithoutSubscription } = require('../../seed/authCompaniesSeed');

Expand Down Expand Up @@ -102,6 +103,7 @@ const coursesList = [
_id: new ObjectId(),
format: 'blended',
subProgram: subProgramsList[1]._id,
trainer: trainer._id,
type: INTER_B2B,
operationsRepresentative: vendorAdmin._id,
trainees: [questionnaireHistoriesUsersList[1]],
Expand All @@ -111,19 +113,30 @@ const coursesList = [

const questionnaireHistoriesList = [
{
_id: new ObjectId(),
course: coursesList[0]._id,
user: questionnaireHistoriesUsersList[2],
questionnaire: questionnairesList[0]._id,
company: authCompany._id,
questionnaireAnswersList: [{ card: cardsList[3]._id, answerList: ['blabla'] }],
},
{
_id: new ObjectId(),
course: coursesList[1]._id,
user: questionnaireHistoriesUsersList[1],
questionnaire: questionnairesList[2]._id,
company: authCompany._id,
timeline: END_COURSE,
questionnaireAnswersList: [{ card: cardsList[3]._id, answerList: ['blabla'] }],
questionnaireAnswersList: [{ card: cardsList[1]._id, answerList: ['2'] }],
},
{
_id: new ObjectId(),
course: coursesList[1]._id,
user: questionnaireHistoriesUsersList[1],
questionnaire: questionnairesList[2]._id,
company: authCompany._id,
timeline: START_COURSE,
questionnaireAnswersList: [{ card: cardsList[1]._id, answerList: ['1'] }],
},
];

Expand Down Expand Up @@ -211,4 +224,5 @@ module.exports = {
coursesList,
questionnaireHistoriesUsersList,
cardsList,
questionnaireHistoriesList,
};
2 changes: 2 additions & 0 deletions tests/unit/helpers/authorization.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('validate', () => {
'programs:read',
'questionnaires:edit',
'questionnaires:read',
'questionnairehistories:edit',
'trainermissions:edit',
'trainermissions:read',
`company-${companyId}`,
Expand Down Expand Up @@ -384,6 +385,7 @@ describe('validate', () => {
'attendances:edit',
'holdings:read',
'questionnaires:read',
'questionnairehistories:edit',
],
role: { client: { name: 'coach' }, vendor: { name: 'trainer' } },
holding: null,
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/helpers/questionnaireHistories.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,22 @@ describe('addQuestionnaireHistory', () => {
);
});
});

describe('updateQuestionnaireHistory', () => {
const questionnaireHistoryId = new ObjectId();
let updateOne;

beforeEach(() => {
updateOne = sinon.stub(QuestionnaireHistory, 'updateOne');
});

afterEach(() => {
updateOne.restore();
});

it('should update questionnaireHistory', async () => {
await QuestionnaireHistoriesHelper.updateQuestionnaireHistory(questionnaireHistoryId);

sinon.assert.calledWithExactly(updateOne, { _id: questionnaireHistoryId }, { $set: { isValidated: true } });
});
});
Loading
Loading