Skip to content

Commit

Permalink
Merge pull request #2385 from sophiemoustard/dev
Browse files Browse the repository at this point in the history
MES 23/04/2024
  • Loading branch information
manonpalin committed Apr 23, 2024
2 parents f451085 + 400e8cd commit e63598d
Show file tree
Hide file tree
Showing 18 changed files with 1,737 additions and 182 deletions.
2 changes: 1 addition & 1 deletion src/controllers/questionnaireController.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const getFollowUp = async (req) => {

const getQRCode = async (req) => {
try {
const qrCode = await QuestionnaireHelper.generateQRCode(req.params._id, req.query.course);
const qrCode = await QuestionnaireHelper.generateQRCode(req.query.course);

return { message: translate[language].questionnaireQRCodeGenerated, data: { qrCode } };
} catch (e) {
Expand Down
8 changes: 8 additions & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,14 @@ module.exports = {
EXPECTATIONS: 'expectations',
END_OF_COURSE: 'end_of_course',
SELF_POSITIONNING: 'self_positionning',
// QUESTIONNAIRE_HISTORY
START_COURSE: 'start_course',
END_COURSE: 'end_course',
get TIMELINE_OPTIONS() { return [this.START_COURSE, this.END_COURSE, this.UNKNOWN]; },
// COURSE TIMELINE
BEFORE_MIDDLE_COURSE_END_DATE: 'before_middle_course_end_date',
BETWEEN_MID_AND_END_COURSE: 'between_mid_and_end_course',
ENDED: 'ended',
// tests end2end
PLANNING: 'planning',
AUTHENTICATION: 'authentication',
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/holdings.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ const Holding = require('../models/Holding');

exports.create = async payload => Holding.create(payload);

exports.list = async () => Holding.find({}, { _id: 1, name: 1 }).lean();
exports.list = async () => Holding
.find({}, { _id: 1, name: 1 })
.populate({ path: 'companies' })
.lean();

exports.update = async (holdingId, payload) => {
if (payload.company) return CompanyHolding.create({ holding: holdingId, company: payload.company });
Expand Down
49 changes: 44 additions & 5 deletions src/helpers/questionnaireHistories.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
const Boom = require('@hapi/boom');
const QuestionnaireHistory = require('../models/QuestionnaireHistory');
const { COURSE, TRAINEE } = require('./constants');
const Questionnaire = require('../models/Questionnaire');
const {
COURSE,
TRAINEE,
SELF_POSITIONNING,
START_COURSE,
END_COURSE,
BEFORE_MIDDLE_COURSE_END_DATE,
ENDED,
UNKNOWN,
} = require('./constants');
const CourseHistoriesHelper = require('./courseHistories');
const QuestionnaireHelper = require('./questionnaires');
const translate = require('./translate');

const { language } = translate;

exports.addQuestionnaireHistory = async (payload) => {
const traineesCompanyAtCourseRegistrationList = await CourseHistoriesHelper.getCompanyAtCourseRegistrationList(
{ key: COURSE, value: payload.course }, { key: TRAINEE, value: [payload.user] }
);
const { user: userId, questionnaire: questionnaireId, course: courseId } = payload;
const traineesCompanyAtCourseRegistrationList = await CourseHistoriesHelper
.getCompanyAtCourseRegistrationList({ key: COURSE, value: courseId }, { key: TRAINEE, value: [userId] });

const questionnaire = await Questionnaire.findOne({ _id: questionnaireId }, { type: 1 }).lean();

let timeline;
if (questionnaire.type === SELF_POSITIONNING) {
const { courseTimeline } = await QuestionnaireHelper.getCourseInfos(courseId);

return QuestionnaireHistory.create({ ...payload, company: traineesCompanyAtCourseRegistrationList[0].company });
switch (courseTimeline) {
case BEFORE_MIDDLE_COURSE_END_DATE:
timeline = START_COURSE;
break;
case ENDED:
timeline = END_COURSE;
break;
default:
timeline = UNKNOWN;
}
}

const questionnaireHistoryExists = await QuestionnaireHistory
.countDocuments({ course: courseId, user: userId, questionnaire: questionnaireId, ...(timeline && { timeline }) });
if (questionnaireHistoryExists) throw Boom.conflict(translate[language].questionnaireHistoryConflict);

return QuestionnaireHistory.create(
{ ...payload, company: traineesCompanyAtCourseRegistrationList[0].company, ...(timeline && { timeline }) }
);
};
150 changes: 113 additions & 37 deletions src/helpers/questionnaires.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,90 @@ const Questionnaire = require('../models/Questionnaire');
const Course = require('../models/Course');
const Card = require('../models/Card');
const CardHelper = require('./cards');
const { EXPECTATIONS, PUBLISHED, STRICTLY_E_LEARNING, END_OF_COURSE, INTRA } = require('./constants');
const {
EXPECTATIONS,
PUBLISHED,
STRICTLY_E_LEARNING,
END_OF_COURSE,
INTRA,
SELF_POSITIONNING,
START_COURSE,
END_COURSE,
DAY,
BEFORE_MIDDLE_COURSE_END_DATE,
BETWEEN_MID_AND_END_COURSE,
ENDED,
} = require('./constants');
const DatesUtilsHelper = require('./dates/utils');
const { CompaniDate } = require('./dates/companiDates');

exports.create = async payload => Questionnaire.create(payload);

const getCourseTimeline = (course) => {
const sortedSlots = [...course.slots].sort(DatesUtilsHelper.ascendingSortBy('startDate'));

if (!sortedSlots.length) return BEFORE_MIDDLE_COURSE_END_DATE;

const allSlots = [...sortedSlots, ...course.slotsToPlan];
const middleSlotIndex = Math.ceil(allSlots.length / 2) - 1;
if (!get(sortedSlots[middleSlotIndex], 'endDate')) return BEFORE_MIDDLE_COURSE_END_DATE;

const isBeforeMiddleCourseEndDate = CompaniDate().isBefore(get(sortedSlots[middleSlotIndex], 'endDate'));
if (isBeforeMiddleCourseEndDate) return BEFORE_MIDDLE_COURSE_END_DATE;

if (get(course, 'slotsToPlan.length')) return BETWEEN_MID_AND_END_COURSE;

const lastSlotStartOfDay = get(sortedSlots[sortedSlots.length - 1], 'startDate')
? CompaniDate(get(sortedSlots[sortedSlots.length - 1], 'startDate')).startOf(DAY)
: null;
if (CompaniDate().isAfter(lastSlotStartOfDay)) return ENDED;

return BETWEEN_MID_AND_END_COURSE;
};

exports.getCourseInfos = async (courseId) => {
const course = await Course.findOne({ _id: courseId })
.populate({ path: 'slots', select: '-__v -createdAt -updatedAt' })
.populate({ path: 'slotsToPlan', select: '_id' })
.populate({ path: 'subProgram', select: 'program', populate: { path: 'program', select: '_id' } })
.lean({ virtuals: true });

return course.format === STRICTLY_E_LEARNING
? { isStrictlyELearning: true }
: { programId: course.subProgram.program._id, courseTimeline: getCourseTimeline(course) };
};

exports.list = async (credentials, query = {}) => {
const isVendorUser = !!get(credentials, 'role.vendor');
const { course: courseId } = query;

if (!courseId) {
return Questionnaire.find(query).populate({ path: 'historiesCount', options: { isVendorUser } }).lean();
}

return Questionnaire.find(query).populate({ path: 'historiesCount', options: { isVendorUser } }).lean();
const { isStrictlyELearning, courseTimeline, programId } = await exports.getCourseInfos(courseId);

if (isStrictlyELearning) return [];

switch (courseTimeline) {
case BETWEEN_MID_AND_END_COURSE:
return [];
case BEFORE_MIDDLE_COURSE_END_DATE:
case ENDED: {
const qType = courseTimeline === BEFORE_MIDDLE_COURSE_END_DATE ? [EXPECTATIONS] : [END_OF_COURSE];

return Questionnaire
.find({
type: { $in: [...qType, SELF_POSITIONNING] },
$or: [{ program: { $exists: false } }, { program: programId }],
status: PUBLISHED,
})
.populate({ path: 'cards', select: '-__v -createdAt -updatedAt' })
.lean();
}
default:
return [];
}
};

exports.getQuestionnaire = async id => Questionnaire.findOne({ _id: id })
Expand All @@ -33,44 +107,49 @@ exports.removeCard = async (cardId) => {
if (get(card, 'media.publicId')) await CardHelper.deleteMedia(cardId, card.media.publicId);
};

exports.findQuestionnaire = async (course, credentials, type) => Questionnaire
.findOne({ type, status: PUBLISHED }, { type: 1, name: 1 })
.populate({
path: 'histories',
match: { course: course._id, user: credentials._id },
options: { requestingOwnInfos: true },
select: { _id: 1 },
})
.lean({ virtuals: true });
const findQuestionnaires = (questionnaireConditions, historiesConditions) => {
const { typeList, program } = questionnaireConditions;
const { course, user, timeline } = historiesConditions;

exports.getUserQuestionnaires = async (courseId, credentials) => {
const course = await Course.findOne({ _id: courseId })
.populate({ path: 'slots', select: '-__v -createdAt -updatedAt' })
.populate({ path: 'slotsToPlan', select: '_id' })
.lean({ virtuals: true });

if (course.format === STRICTLY_E_LEARNING) return [];
const findQuestionnaireQuery = {
type: { $in: typeList },
$or: [{ program: { $exists: false } }, { program }],
status: PUBLISHED,
};

const sortedCourseSlots = course.slots.sort(DatesUtilsHelper.ascendingSortBy('startDate'));
const matchHistoriesQuery = { course, user, $or: [{ timeline: { $exists: false } }, { timeline }] };

const middleCourseSlotIndex = Math.ceil(sortedCourseSlots.length / 2) - 1;
return Questionnaire
.find(findQuestionnaireQuery, { type: 1, name: 1 })
.populate({
path: 'histories',
match: matchHistoriesQuery,
options: { requestingOwnInfos: true },
select: { _id: 1, timeline: 1 },
})
.lean({ virtuals: true });
};

const isBeforeMiddleCourse = !sortedCourseSlots.length ||
CompaniDate().isBefore(sortedCourseSlots[middleCourseSlotIndex].endDate);
if (isBeforeMiddleCourse) {
const questionnaire = await this.findQuestionnaire(course, credentials, EXPECTATIONS);
exports.getUserQuestionnaires = async (courseId, credentials) => {
const { isStrictlyELearning, courseTimeline, programId } = await exports.getCourseInfos(courseId);

return !questionnaire || questionnaire.histories.length ? [] : [questionnaire];
}
if (isStrictlyELearning) return [];

if (get(course, 'slotsToPlan.length')) return [];
switch (courseTimeline) {
case BETWEEN_MID_AND_END_COURSE:
return [];
case BEFORE_MIDDLE_COURSE_END_DATE:
case ENDED: {
const qType = courseTimeline === BEFORE_MIDDLE_COURSE_END_DATE ? [EXPECTATIONS] : [END_OF_COURSE];
const timeline = courseTimeline === BEFORE_MIDDLE_COURSE_END_DATE ? START_COURSE : END_COURSE;

const isCourseEnded = sortedCourseSlots.length &&
CompaniDate().isAfter(sortedCourseSlots[sortedCourseSlots.length - 1].startDate);
if (isCourseEnded) {
const questionnaire = await this.findQuestionnaire(course, credentials, END_OF_COURSE);
const questionnaires = await findQuestionnaires(
{ typeList: [...qType, SELF_POSITIONNING], program: programId },
{ course: courseId, user: credentials._id, timeline }
);

return !questionnaire || questionnaire.histories.length ? [] : [questionnaire];
return questionnaires.filter(q => q && !q.histories.length);
}
}

return [];
Expand Down Expand Up @@ -133,12 +212,9 @@ exports.getFollowUp = async (id, courseId, credentials) => {
return courseId ? formatQuestionnaireAnswersWithCourse(courseId, questionnaireAnswers) : questionnaireAnswers;
};

exports.generateQRCode = async (questionnaireId, courseId) => {
exports.generateQRCode = async (courseId) => {
const qrCode = await QRCode
.toDataURL(
`${process.env.WEBSITE_HOSTNAME}/ni/questionnaires/${questionnaireId}?courseId=${courseId}`,
{ margin: 0 }
);
.toDataURL(`${process.env.WEBSITE_HOSTNAME}/ni/questionnaires?courseId=${courseId}`, { margin: 0 });

return qrCode;
};
3 changes: 2 additions & 1 deletion src/models/QuestionnaireHistory.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const mongoose = require('mongoose');
const { validateQuery, validateAggregation, formatQuery, queryMiddlewareList } = require('./preHooks/validate');
const { ORIGIN_OPTIONS, MOBILE } = require('../helpers/constants');
const { ORIGIN_OPTIONS, MOBILE, TIMELINE_OPTIONS } = require('../helpers/constants');

const QuestionnaireHistorySchema = mongoose.Schema({
course: { type: mongoose.Schema.Types.ObjectId, ref: 'Course', required: true },
Expand All @@ -12,6 +12,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 },
}, { timestamps: true });

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

const { language } = translate;

exports.authorizeAddQuestionnaireHistory = async (req) => {
const { user: userId, questionnaire: questionnaireId, course: courseId, questionnaireAnswersList } = req.payload;
Expand All @@ -17,10 +13,6 @@ exports.authorizeAddQuestionnaireHistory = async (req) => {

if (!questionnaire || !user || !isCourseFollowedByUser) throw Boom.notFound();

const questionnaireHistory = await QuestionnaireHistory
.countDocuments({ course: courseId, user: userId, questionnaire: questionnaireId });
if (questionnaireHistory) return Boom.conflict(translate[language].questionnaireHistoryConflict);

if (questionnaireAnswersList) await checkQuestionnaireAnswersList(questionnaireAnswersList, questionnaireId);

return null;
Expand Down
10 changes: 9 additions & 1 deletion src/routes/preHandlers/questionnaires.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,19 @@ exports.authorizeQuestionnaireQRCodeGet = async (req) => {
};

exports.authorizeGetList = async (req) => {
const { program: programId } = req.query;
const { program: programId, course: courseId } = req.query;
if (programId) {
const program = await Program.countDocuments({ _id: programId });
if (!program) throw Boom.notFound();
}

if (courseId) {
const course = await Course.countDocuments({ _id: courseId });
if (!course) throw Boom.notFound();
} else {
const loggedUserVendorRole = get(req, 'auth.credentials.role.vendor.name');
if (!loggedUserVendorRole) throw Boom.forbidden();
}

return null;
};
21 changes: 18 additions & 3 deletions src/routes/questionnaires.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ exports.plugin = {
options: {
validate: {
query: Joi.object({
status: Joi.string().valid(PUBLISHED),
course: Joi.objectId(),
program: Joi.objectId(),
}),
// [temporary] This line can be removed when mobile versions prior to 2.25.0 have been deprecated.
status: Joi.string().valid(PUBLISHED),
}).oxor('course', 'program'),
},
auth: { scope: ['questionnaires:read'] },
auth: { mode: 'optional' },
pre: [{ method: authorizeGetList }],
},
handler: list,
Expand Down Expand Up @@ -178,5 +180,18 @@ exports.plugin = {
},
handler: getQRCode,
});

server.route({
method: 'GET',
path: '/qrcode',
options: {
auth: { scope: ['questionnaires:read'] },
validate: {
query: Joi.object({ course: Joi.objectId().required() }),
},
pre: [{ method: authorizeQuestionnaireQRCodeGet }],
},
handler: getQRCode,
});
},
};
2 changes: 1 addition & 1 deletion tests/integration/cards.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('CARDS ROUTES - PUT /cards/{_id}', () => {
});
});

describe('Survey #tag', () => {
describe('Survey', () => {
const requests = [
{ msg: 'Unset first label', payload: { labels: { 1: '' } }, code: 200 },
{ msg: 'Unset last label', payload: { labels: { 5: '' } }, code: 200 },
Expand Down
Loading

0 comments on commit e63598d

Please sign in to comment.