diff --git a/src/controllers/questionnaireHistoryController.js b/src/controllers/questionnaireHistoryController.js index 9f7645914..03b9380d8 100644 --- a/src/controllers/questionnaireHistoryController.js +++ b/src/controllers/questionnaireHistoryController.js @@ -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 }; diff --git a/src/data/rights.js b/src/data/rights.js index 0ca507f76..2740427ca 100644 --- a/src/data/rights.js +++ b/src/data/rights.js @@ -49,6 +49,7 @@ const rights = [ { permission: 'programs:read', rolesConcerned: [VENDOR_ADMIN, TRAINING_ORGANISATION_MANAGER], description: 'Consulter les données des programmes' }, { permission: 'questionnaires:edit', rolesConcerned: [VENDOR_ADMIN, TRAINING_ORGANISATION_MANAGER], description: 'Editer les questionnaires' }, { permission: 'questionnaires:read', rolesConcerned: [VENDOR_ADMIN, TRAINING_ORGANISATION_MANAGER, TRAINER], description: 'Consulter les données des questionnaires' }, + { permission: 'questionnairehistories:edit', rolesConcerned: [VENDOR_ADMIN, TRAINING_ORGANISATION_MANAGER, TRAINER], description: 'Editer les historiques de questionnaire' }, { permission: 'roles:read', rolesConcerned: [VENDOR_ADMIN, TRAINING_ORGANISATION_MANAGER, CLIENT_ADMIN, COACH], description: 'Consulter les roles' }, { permission: 'taxcertificates:edit', rolesConcerned: [CLIENT_ADMIN, COACH], subscription: ERP, description: 'Éditer une attestation fiscale' }, { permission: 'taxcertificates:read', rolesConcerned: [CLIENT_ADMIN, COACH], subscription: ERP, description: 'Consulter une attestation fiscale' }, diff --git a/src/helpers/questionnaireHistories.js b/src/helpers/questionnaireHistories.js index 20a06a6c3..5550ff3d4 100644 --- a/src/helpers/questionnaireHistories.js +++ b/src/helpers/questionnaireHistories.js @@ -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 } }); diff --git a/src/helpers/questionnaires.js b/src/helpers/questionnaires.js index dab9f50e5..eb8e88efe 100644 --- a/src/helpers/questionnaires.js +++ b/src/helpers/questionnaires.js @@ -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']; + const followUp = questionnaire.histories.map(h => pick(h, fieldsToPick)); const course = await Course.findOne({ _id: courseId }) .select('subProgram companies misc type holding trainees') diff --git a/src/helpers/translate.js b/src/helpers/translate.js index 2c74141dc..c32a132e2 100644 --- a/src/helpers/translate.js +++ b/src/helpers/translate.js @@ -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.', @@ -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.', diff --git a/src/models/QuestionnaireHistory.js b/src/models/QuestionnaireHistory.js index d5e9c6c19..17129b74d 100644 --- a/src/models/QuestionnaireHistory.js +++ b/src/models/QuestionnaireHistory.js @@ -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); diff --git a/src/routes/preHandlers/questionnaireHistories.js b/src/routes/preHandlers/questionnaireHistories.js index 8e8fef652..579e4e8df 100644 --- a/src/routes/preHandlers/questionnaireHistories.js +++ b/src/routes/preHandlers/questionnaireHistories.js @@ -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) => { @@ -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; +}; diff --git a/src/routes/questionnaireHistories.js b/src/routes/questionnaireHistories.js index d3262c05a..c2fdeea16 100644 --- a/src/routes/questionnaireHistories.js +++ b/src/routes/questionnaireHistories.js @@ -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', @@ -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() })), + }), + }, + auth: { scope: ['questionnairehistories:edit'] }, + pre: [{ method: authorizeQuestionnaireHistoryUpdate }], + }, + handler: update, + }); }, }; diff --git a/tests/integration/questionnaireHistories.test.js b/tests/integration/questionnaireHistories.test.js index 8b2824b09..2f235696b 100644 --- a/tests/integration/questionnaireHistories.test.js +++ b/tests/integration/questionnaireHistories.test.js @@ -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'); @@ -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 () => { + 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); + }); + }); +}); diff --git a/tests/integration/seed/questionnaireHistoriesSeed.js b/tests/integration/seed/questionnaireHistoriesSeed.js index 33d93c5c6..b94352aea 100644 --- a/tests/integration/seed/questionnaireHistoriesSeed.js +++ b/tests/integration/seed/questionnaireHistoriesSeed.js @@ -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, @@ -23,6 +23,7 @@ const { SELF_POSITIONNING, ON_SITE, END_COURSE, + START_COURSE, } = require('../../../src/helpers/constants'); const { authCompany, companyWithoutSubscription } = require('../../seed/authCompaniesSeed'); @@ -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]], @@ -111,6 +113,7 @@ const coursesList = [ const questionnaireHistoriesList = [ { + _id: new ObjectId(), course: coursesList[0]._id, user: questionnaireHistoriesUsersList[2], questionnaire: questionnairesList[0]._id, @@ -118,12 +121,22 @@ const questionnaireHistoriesList = [ 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'] }], }, ]; @@ -211,4 +224,5 @@ module.exports = { coursesList, questionnaireHistoriesUsersList, cardsList, + questionnaireHistoriesList, }; diff --git a/tests/unit/helpers/authorization.test.js b/tests/unit/helpers/authorization.test.js index 39b4070e5..5194ecf53 100644 --- a/tests/unit/helpers/authorization.test.js +++ b/tests/unit/helpers/authorization.test.js @@ -123,6 +123,7 @@ describe('validate', () => { 'programs:read', 'questionnaires:edit', 'questionnaires:read', + 'questionnairehistories:edit', 'trainermissions:edit', 'trainermissions:read', `company-${companyId}`, @@ -384,6 +385,7 @@ describe('validate', () => { 'attendances:edit', 'holdings:read', 'questionnaires:read', + 'questionnairehistories:edit', ], role: { client: { name: 'coach' }, vendor: { name: 'trainer' } }, holding: null, diff --git a/tests/unit/helpers/questionnaireHistories.test.js b/tests/unit/helpers/questionnaireHistories.test.js index ac6bde7e8..aadadd6ca 100644 --- a/tests/unit/helpers/questionnaireHistories.test.js +++ b/tests/unit/helpers/questionnaireHistories.test.js @@ -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 } }); + }); +}); diff --git a/tests/unit/helpers/questionnaires.test.js b/tests/unit/helpers/questionnaires.test.js index ac5909546..047519033 100644 --- a/tests/unit/helpers/questionnaires.test.js +++ b/tests/unit/helpers/questionnaires.test.js @@ -1330,13 +1330,14 @@ describe('getFollowUp', () => { ], }; const cardsIds = [new ObjectId(), new ObjectId()]; + const historiesIds = [new ObjectId(), new ObjectId()]; const questionnaire = { _id: questionnaireId, type: SELF_POSITIONNING, name: 'questionnaire', histories: [ { - _id: new ObjectId(), + _id: historiesIds[0], course: course._id, company: companyId, user: trainees[0], @@ -1365,7 +1366,7 @@ describe('getFollowUp', () => { timeline: START_COURSE, }, { - _id: new ObjectId(), + _id: historiesIds[1], course: course._id, company: companyId, user: trainees[1], @@ -1392,6 +1393,7 @@ describe('getFollowUp', () => { }, ], timeline: END_COURSE, + isValidated: true, }, ], }; @@ -1417,6 +1419,7 @@ describe('getFollowUp', () => { }, followUp: [ { + _id: historiesIds[0], user: trainees[0], timeline: START_COURSE, questionnaireAnswersList: [ @@ -1443,8 +1446,10 @@ describe('getFollowUp', () => { ], }, { + _id: historiesIds[1], user: trainees[1], timeline: END_COURSE, + isValidated: true, questionnaireAnswersList: [ { card: {