Ajout de la possibilité de répondre à un groupe de questions.

This commit is contained in:
Fabrice PENHOËT 2020-10-22 17:47:05 +02:00
parent fe713480a0
commit ca4be71a89
12 changed files with 279 additions and 267 deletions

View File

@ -9,7 +9,6 @@ module.exports =
previewQuestionnaireRoutes: "/preview", previewQuestionnaireRoutes: "/preview",
publishedQuestionnaireRoutes: "/quiz/", publishedQuestionnaireRoutes: "/quiz/",
regenerateHTML: "/htmlregenerated", regenerateHTML: "/htmlregenerated",
saveAnswersRoute: "/answer/",
searchAdminQuestionnairesRoute : "/searchadmin", searchAdminQuestionnairesRoute : "/searchadmin",
searchQuestionnairesRoute : "/search", searchQuestionnairesRoute : "/search",
// -- groupes : // -- groupes :
@ -24,8 +23,9 @@ module.exports =
// -- answers : // -- answers :
getAdminStats: "/getadminstats/", getAdminStats: "/getadminstats/",
getPreviousAnswers: "/user/answers/", getPreviousAnswers: "/user/answers/",
getQuestionnairesWithoutAnswer: "/withoutanswer/user/", /// getQuestionnairesWithoutAnswer: "/withoutanswer/user/", -> ne sert plus ! à remplacer pour liste derniers quizs
getStatsAnswers : "/user/anwswers/stats/", getStatsAnswers : "/user/anwswers/stats/",// fonctionne aussi pour les groupes
saveAnswersRoute: "/answer/",// idem
// forms : à compléter avec valeurs par défaut, etc. cf modèle // forms : à compléter avec valeurs par défaut, etc. cf modèle
Questionnaire : Questionnaire :
{ {

View File

@ -6,10 +6,11 @@ const configTpl = require("../views/"+config.theme+"/config/"+config.availableLa
const tool = require("../tools/main"); const tool = require("../tools/main");
const toolFile = require("../tools/file"); const toolFile = require("../tools/file");
const subscriptionCtrl = require("./subscription"); const groupCtrl = require("./group");
const questionnaireCtrl = require("./questionnaire"); const questionnaireCtrl = require("./questionnaire");
const subscriptionCtrl = require("./subscription");
const txt = require("../lang/"+config.adminLang+"/answer"); const txtAnswers = require("../lang/"+config.adminLang+"/answer");
const txtGeneral = require("../lang/"+config.adminLang+"/general"); const txtGeneral = require("../lang/"+config.adminLang+"/general");
// Enregistrement d'une réponse à un questionnaire // Enregistrement d'une réponse à un questionnaire
@ -18,33 +19,39 @@ exports.create = async (req, res, next) =>
try try
{ {
const db = require("../models/index"); const db = require("../models/index");
const checkQuestionnaireAccess=await subscriptionCtrl.checkQuestionnaireAccess(req.connectedUser.User.id, req.body.QuestionnaireId);
req.body.UserId=req.connectedUser.User.id; req.body.UserId=req.connectedUser.User.id;
if(checkQuestionnaireAccess) // l'utilisateur a déjà accès à ce questionnaire await saveAnswerToQuestionnaire(req.body);
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] }); // J'en profite pour remettre les pendules à l'heure !
else
{
await Promise.all([
db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] }),
db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] })
]);
}
// j'en profite pour remettre les pendules à l'heure !
db["User"].update({ timeDifference: req.body.timeDifference }, { where: { id : req.connectedUser.User.id }, limit:1 }); db["User"].update({ timeDifference: req.body.timeDifference }, { where: { id : req.connectedUser.User.id }, limit:1 });
creaUserStatsAnwsersJson(req.body.UserId); res.status(201).json({ message: txtAnswers.responseSavedMessage });
creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
creaUserAnswersJson(req.body.UserId);
res.status(201).json({ message: txt.responseSavedMessage });
next(); next();
} }
catch(e) catch(e)
{ // à priori, l'utilisateur ne peut pas avoir envoyé de données incorrectes, donc erreur application pour admin {
next(e);
}
}
// Enregistrement d'une réponse à un groupe de questionnaires
exports.createInGroup = async (req, res, next) =>
{
try
{
const db = require("../models/index");
req.body.UserId=req.connectedUser.User.id;
await saveAnswerToGroup(req.body);
// J'en profite pour remettre les pendules à l'heure !
db["User"].update({ timeDifference: req.body.timeDifference }, { where: { id : req.connectedUser.User.id }, limit:1 });
res.status(201).json({ message: txtAnswers.responseSavedMessage });
next();
}
catch(e)
{
next(e); next(e);
} }
} }
// Retourne les réponses d'un utilisateur pour un questionnaire donné // Retourne les réponses d'un utilisateur pour un questionnaire donné
// Si fichier réponses devient trop gros, passer par sql ?
exports.getAnswersByQuestionnaire = async(req, res, next) => exports.getAnswersByQuestionnaire = async(req, res, next) =>
{ {
try try
@ -59,14 +66,30 @@ exports.getAnswersByQuestionnaire = async(req, res, next) =>
} }
} }
// Retourne les réponses d'un utilisateur pour un groupe de questionnaires donné
exports.getAnswersByGroup = async(req, res, next) =>
{
try
{
const answers=await getUserAnswersByGroup(req.params.userId, req.params.groupId);
res.status(200).json(answers);
next();
}
catch(e)
{
next(e);
}
}
// Retourne les statistiques de l'utilisateur // Retourne les statistiques de l'utilisateur
exports.getStatsByUser = async(req, res, next) => exports.getStatsByUser = async(req, res, next) =>
{ {
try try
{ {
const stats=await getUserStatsAnswers(req.params.userId); const stats=await getUserStatsAnswers(req.params.userId);
// J'ajoute les stats générales des questionnaires pour comparaison : // + stats générales des questionnaires et groupes pour comparaison :
stats.general=await questionnaireCtrl.getStatsQuestionnaires(); stats.questionnaires=await questionnaireCtrl.getStatsQuestionnaires();
stats.groups=await groupCtrl.getStatsGroups();
res.status(200).json(stats); res.status(200).json(stats);
} }
catch(e) catch(e)
@ -75,71 +98,58 @@ exports.getStatsByUser = async(req, res, next) =>
} }
} }
// Retourne la liste des questionnaires auxquels un utilisateur a accès, mais n'a pas répondu
// Ils sont listés par ordre de fraîcheur, les + récents étant en début de liste
// Un questionnaire de début et un nombre de questionnaires à retourner doivent être fournis (pagination).
exports.getQuestionnairesWithouAnswerByUser = async(req, res, next) =>
{
try
{
let datas;
if(req.params.id===undefined || req.params.begin===undefined || req.params.nb===undefined)
{
const err=new Error;
err.message=txtGeneral.neededParams;
throw err;
}
else
datas=await getUserQuestionnairesWithoutAnswer(req.params.id, req.params.begin, req.params.nb);
if(datas!==false)
{
if(req.params.output!==undefined && req.params.output=="html")
{
if(datas.questionnaires.length!=0)
{
const pug = require("pug");
const striptags = require("striptags");
const txtIllustration= require("../lang/"+config.adminLang+"/illustration");
const compiledFunction = pug.compileFile("./views/"+config.theme+"/includes/listing-questionnaires.pug");
const pageDatas=
{
tool: tool,
striptags: striptags,
txtGeneral: txtGeneral,
txtIllustration: txtIllustration,
questionnaires: datas.questionnaires,
nbQuestionnairesList:configTpl.nbQuestionnairesUserHomePage
}
datas.html=await compiledFunction(pageDatas);
}
else
datas.html="";
res.status(200).json(datas);
}
else
res.status(200).json(datas);
}
else
res.status(404).json(txtQuestionnaire.notFound);
next();
}
catch(e)
{
next(e);
}
}
// FONCTIONS UTILITAIRES // FONCTIONS UTILITAIRES
// Enregistre la réponse à un questionnaire
const saveAnswerToQuestionnaire = async (req) =>
{
const db = require("../models/index");
const checkQuestionnaireAccess=await subscriptionCtrl.checkQuestionnaireAccess(req.UserId, req.QuestionnaireId);
if(checkQuestionnaireAccess) // L'utilisateur a déjà accès à ce questionnaire, j'enregistre juste sa réponse
await db["Answer"].create({ ...req }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
else
{
await Promise.all([
db["QuestionnaireAccess"].create({ ...req }, { fields: ["QuestionnaireId", "UserId"] }),
db["Answer"].create({ ...req }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] })
]);
}
await creaUserStatsAnwsersJson(req.UserId);
await creaUserAnswersJson(req.UserId);
return true;
}
exports.saveAnswerToQuestionnaire = saveAnswerToQuestionnaire;
// Enregistre la réponse à un groupe de questionnaires
const saveAnswerToGroup = async (req) =>
{
const db = require("../models/index");
const answer = await db["Answer"].create({ ...req }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "GroupId", "UserId"] });
const group = groupCtrl.searchGroupById(req.GroupId);
for(let i in group.Questionnaires)
{
if(await subscriptionCtrl.checkQuestionnaireAccess(req.UserId, group.Questionnaires[i].id) === false)
{
req.QuestionnaireId = group.Questionnaires[i].id;
await db["QuestionnaireAccess"].create({ ...req }, { fields: ["QuestionnaireId", "UserId"] });
}
}
await creaUserStatsAnwsersJson(req.UserId);
await creaUserAnswersJson(req.UserId);
return true;
}
exports.saveAnswerToGroup = saveAnswerToGroup;
// Créer la liste des réponses d'un utilisateur // Créer la liste des réponses d'un utilisateur
// !! à surveiller car fichier pouvant devenir gros ! mais utile pour future SVG côté client
const creaUserAnswersJson = async (UserId) => const creaUserAnswersJson = async (UserId) =>
{ {
const db = require("../models/index"); const db = require("../models/index");
const userAnswers=await db.sequelize.query("SELECT `QuestionnaireId`,`nbQuestions`,`nbCorrectAnswers`,`duration`,`createdAt` FROM `Answers` WHERE `UserId`=:id ORDER BY `QuestionnaireId` DESC, `createdAt` DESC", { replacements: { id: UserId }, type: QueryTypes.SELECT }); const userAnswers=await db.sequelize.query("SELECT `QuestionnaireId`, `GroupId`, `nbQuestions`, `nbCorrectAnswers`, `duration`, `createdAt` FROM `Answers` WHERE `UserId`=:id ORDER BY `GroupId` DESC, `QuestionnaireId` DESC, `createdAt` DESC", { replacements: { id: UserId }, type: QueryTypes.SELECT });
if(userAnswers) if(userAnswers)
{ {
await toolFile.createJSON(config.dirCacheUsersAnswers, UserId, userAnswers);// à surveiller car fichier pouvant devenir gros ! mais utile pour SVG côté client await toolFile.createJSON(config.dirCacheUsersAnswers, UserId, userAnswers);
return userAnswers; return userAnswers;
} }
else else
@ -147,7 +157,7 @@ const creaUserAnswersJson = async (UserId) =>
} }
exports.creaUserAnswersJson = creaUserAnswersJson; exports.creaUserAnswersJson = creaUserAnswersJson;
// Retourne les réponses d'un utilisateurs à un questionnaire // Retourne les réponses d'un utilisateurs à un questionnaire simple.
const getUserAnswersByQuestionnaire = async (UserId, QuestionnaireId) => const getUserAnswersByQuestionnaire = async (UserId, QuestionnaireId) =>
{ {
let userAnswers=await toolFile.readJSON(config.dirCacheUsersAnswers, UserId); let userAnswers=await toolFile.readJSON(config.dirCacheUsersAnswers, UserId);
@ -158,29 +168,49 @@ const getUserAnswersByQuestionnaire = async (UserId, QuestionnaireId) =>
const answers=[]; const answers=[];
for(let i in userAnswers) for(let i in userAnswers)
{ {
if(userAnswers[i].QuestionnaireId==QuestionnaireId)// pas "===" car type de données pouvant être différents if(userAnswers[i].QuestionnaireId == QuestionnaireId) // pas "===", car type de données pouvant être différents
answers.push(userAnswers[i]); answers.push(userAnswers[i]);
else if(answers.length!==0)// les réponses étant classées par QuestionnaireId, je peux sortir de la boucle else if(answers.length !== 0) // les réponses étant classées par QuestionnaireId, je peux sortir de la boucle
break; break;
} }
return answers; return answers;
} }
// À combien de questionnaire l'utilisateur a-t'il répondu, quelle est son résultat moyen ? // Retourne les réponses d'un utilisateurs à un groupe de questionnaires.
const getUserAnswersByGroup = async (UserId, GroupId) =>
{
let userAnswers=await toolFile.readJSON(config.dirCacheUsersAnswers, UserId);
if(!userAnswers)
userAnswers=await creaUserAnswersJson(UserId);
if(!userAnswers)
return false;
const answers=[];
for(let i in userAnswers)
{
if(userAnswers[i].GroupId == GroupId)// pas "===" car type de données pouvant être différents
answers.push(userAnswers[i]);
else if(answers.length !== 0) // les réponses étant classées par GroupId, je peux sortir de la boucle
break;
}
return answers;
}
// À combien de questionnaire l'utilisateur a-t-il répondu ? et quel est son résultat moyen ?
const creaUserStatsAnwsersJson = async (UserId) => const creaUserStatsAnwsersJson = async (UserId) =>
{ {
const db = require("../models/index"); const db = require("../models/index");
const getUserAnswers = await db["Answer"].findAll({ where: { UserId : UserId }, attributes: ["id"] }); const getUserAnswers = await db["Answer"].findAll({ where: { UserId : UserId }, attributes: ["id"] });
const getUserQuestionnaires = await db["Answer"].findAll({ attributes: [[db.sequelize.fn('DISTINCT', db.sequelize.col('QuestionnaireId')), 'id']], where: { UserId : UserId }}); const getUserQuestionnaires = await db.sequelize.query("SELECT DISTINCT QuestionnaireId FROM Answers WHERE UserId=:id AND QuestionnaireId IS NOT NULL", { replacements: { id: UserId }, type: QueryTypes.SELECT });
const getUserStats = await db.sequelize.query("SELECT ROUND(AVG(nbCorrectAnswers/nbQuestions) *100) as avgCorrectAnswers, ROUND(AVG(duration)) as avgDuration FROM Answers GROUP BY UserId HAVING UserId=:id", { replacements: { id: UserId }, type: QueryTypes.SELECT }); const getUserQuestionnairesGroup = await db.sequelize.query("SELECT DISTINCT GroupId FROM Answers WHERE UserId=:id AND GroupId IS NOT NULL", { replacements: { id: UserId }, type: QueryTypes.SELECT });
if(getUserAnswers && getUserQuestionnaires) const getUserStats = await db.sequelize.query("SELECT ROUND(AVG(nbCorrectAnswers/nbQuestions) *100) as avgCorrectAnswers, ROUND(AVG(duration)) as avgDuration FROM Answers GROUP BY UserId HAVING UserId=:id", { replacements: { id: UserId }, type: QueryTypes.SELECT });
if(getUserAnswers && getUserQuestionnaires && getUserQuestionnairesGroup)
{ {
const stats = const stats =
{ {
nbAnswers : getUserAnswers.length, nbAnswers : getUserAnswers.length,
nbQuestionnaires : getUserQuestionnaires.length nbQuestionnaires : getUserQuestionnaires.length+getUserQuestionnairesGroup.length
} }
if(getUserStats && getUserAnswers.length!=0) if(getUserStats && getUserAnswers.length !== 0)
{ {
stats.avgCorrectAnswers=getUserStats[0].avgCorrectAnswers; stats.avgCorrectAnswers=getUserStats[0].avgCorrectAnswers;
stats.avgDuration=getUserStats[0].avgDuration; stats.avgDuration=getUserStats[0].avgDuration;
@ -193,7 +223,7 @@ const creaUserStatsAnwsersJson = async (UserId) =>
} }
exports.creaUserStatsAnwsersJson = creaUserStatsAnwsersJson; exports.creaUserStatsAnwsersJson = creaUserStatsAnwsersJson;
// Retourne les données créées par la fonction précédente // Retourne les données créées par la fonction précédente :
const getUserStatsAnswers = async (UserId) => const getUserStatsAnswers = async (UserId) =>
{ {
let userStats=await toolFile.readJSON(config.dirCacheUsersAnswers, "stats"+UserId); let userStats=await toolFile.readJSON(config.dirCacheUsersAnswers, "stats"+UserId);
@ -205,7 +235,7 @@ const getUserStatsAnswers = async (UserId) =>
return userStats; return userStats;
} }
// À combien de questionnaire les utilisateurs ont-ils répondu ces dernières 24 ? depuis le début ? // À combien de questionnaire les utilisateurs ont-ils répondu ces dernières 24H ? depuis le début ?
const getStatsAnswers = async () => const getStatsAnswers = async () =>
{ {
const db = require("../models/index"); const db = require("../models/index");
@ -225,7 +255,9 @@ const getStatsAnswers = async () =>
} }
exports.getStatsAnswers = getStatsAnswers; exports.getStatsAnswers = getStatsAnswers;
// Créer la liste des questionnaires proposés à l'utilisateur, mais auxquels il n'a pas encore répondu /*
// Créer la liste des questionnaires/éléments de questionnaire proposés à l'utilisateur, mais auxquels il n'a pas encore répondu
// remplacer pour la liste des derniers éléments ou quizs envoyés ?
const creaUserQuestionnairesWithoutAnswerJson = async (UserId) => const creaUserQuestionnairesWithoutAnswerJson = async (UserId) =>
{ {
UserId=tool.trimIfNotNull(UserId); UserId=tool.trimIfNotNull(UserId);
@ -271,3 +303,61 @@ const getUserQuestionnairesWithoutAnswer = async (UserId, begin=0, nb=10) =>
return { nbTot: nbTot, questionnaires: Questionnaires }; return { nbTot: nbTot, questionnaires: Questionnaires };
} }
exports.getUserQuestionnairesWithoutAnswer = getUserQuestionnairesWithoutAnswer; exports.getUserQuestionnairesWithoutAnswer = getUserQuestionnairesWithoutAnswer;
* */
/// S'inspirer de la fonction ci-dessous pour la liste des questionnaires proposés à l'utilisateur.
/*
// Retourne la liste des questionnaires auxquels un utilisateur a accès, mais n'a pas répondu
// Ils sont listés par ordre de fraîcheur, les + récents étant en début de liste
// Un questionnaire de début et un nombre de questionnaires à retourner doivent être fournis (pagination).
exports.getQuestionnairesWithouAnswerByUser = async(req, res, next) =>
{
try
{
let datas;
if(req.params.id === undefined || req.params.begin === undefined || req.params.nb === undefined)
{
const err=new Error;
err.message=txtGeneral.neededParams;
throw err;
}
else
datas=await getUserQuestionnairesWithoutAnswer(req.params.id, req.params.begin, req.params.nb);
if(datas!==false)
{
if(req.params.output!==undefined && req.params.output=="html")
{
if(datas.questionnaires.length!=0)
{
const pug = require("pug");
const striptags = require("striptags");
const txtIllustration= require("../lang/"+config.adminLang+"/illustration");
const compiledFunction = pug.compileFile("./views/"+config.theme+"/includes/listing-questionnaires.pug");
const pageDatas=
{
tool: tool,
striptags: striptags,
txtGeneral: txtGeneral,
txtIllustration: txtIllustration,
questionnaires: datas.questionnaires,
nbQuestionnairesList:configTpl.nbQuestionnairesUserHomePage
}
datas.html=await compiledFunction(pageDatas);
}
else
datas.html="";
res.status(200).json(datas);
}
else
res.status(200).json(datas);
}
else
res.status(404).json(txtQuestionnaire.notFound);
next();
}
catch(e)
{
next(e);
}
}
* */

View File

@ -62,7 +62,8 @@ exports.getGodfatherId = async (req, res, next) =>
} }
// Contrôleur traitant les données envoyées pour une inscription // Contrôleur traitant les données envoyées pour une inscription
// Les CGU doivent être acceptées et une adresse e-mail envoyée. Le reste peut être adapté sur la page de validation. // Les CGU doivent être acceptées et une adresse e-mail envoyée.
// Le reste peut être adapté sur la page de validation de l'inscription.
exports.signup = async (req, res, next) => exports.signup = async (req, res, next) =>
{ {
try try
@ -76,7 +77,6 @@ exports.signup = async (req, res, next) =>
{ {
// Un mot de passe temporaire et non communiqué est généré : // Un mot de passe temporaire et non communiqué est généré :
req.body.passwordTemp=tool.getPassword(8, 12); req.body.passwordTemp=tool.getPassword(8, 12);
console.log(req.body.passwordTemp);
req.body.password=await bcrypt.hash(req.body.passwordTemp, config.bcryptSaltRounds); req.body.password=await bcrypt.hash(req.body.passwordTemp, config.bcryptSaltRounds);
// Un pseudo temporaire est créé en utilisant la partie de l'e-mail précédant le "@" : // Un pseudo temporaire est créé en utilisant la partie de l'e-mail précédant le "@" :
const lastIndex=req.body.email.indexOf("@"); const lastIndex=req.body.email.indexOf("@");
@ -85,14 +85,11 @@ exports.signup = async (req, res, next) =>
req.body.name=req.body.email.substring(0, lastIndex); req.body.name=req.body.email.substring(0, lastIndex);
const user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "timeDifference"] }); const user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "timeDifference"] });
req.body.UserId=user.id; req.body.UserId=user.id;
// si l'utilisateur a répondu à un quiz avant de créer son compte, on enregistre son résultat : // Si l'utilisateur a répondu à un quiz (ou groupe de quizs) avant de créer son compte, on enregistre son résultat :
if(req.body.QuestionnaireId) if(req.body.QuestionnaireId)
{ await answerCtrl.saveAnswerToQuestionnaire(req.body);
await Promise.all([ else if(req.body.GroupId)
db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] }), await answerCtrl.saveAnswerToGroup(req.body);
db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] })
]); // pas nécessaire ici de créer le cache JSON, car il sera créé lors de la première connexion au compte.
}
await sendValidationLink(user); await sendValidationLink(user);
res.status(201).json({ message: txt.mailValidationMessage }); res.status(201).json({ message: txt.mailValidationMessage });
next(); next();
@ -312,23 +309,18 @@ exports.login = async (req, res, next) =>
const now=new Date(); const now=new Date();
const timeDifference=req.body.timeDifference; const timeDifference=req.body.timeDifference;
db["User"].update({ connectedAt: now, timeDifference: timeDifference }, { where: { id : user.id }, limit:1 }); db["User"].update({ connectedAt: now, timeDifference: timeDifference }, { where: { id : user.id }, limit:1 });
creaUserJson(user.id); await creaUserJson(user.id);
// Connexion à rallonge uniquement possible pour utilisateur de base : // Connexion à rallonge uniquement possible pour utilisateur de base :
let loginTime=config.tokenConnexionMinTimeInHours; let loginTime=config.tokenConnexionMinTimeInHours;
if((req.body.keepConnected==="true") && (user.status==="user")) if((req.body.keepConnected === "true") && (user.status === "user"))
loginTime=config.tokenConnexionMaxTimeInDays; loginTime=config.tokenConnexionMaxTimeInDays;
// Si des données concernant un quiz ont été transmises, on les enregistre ici : // Si l'utilisateur a répondu à un quiz (ou groupe de quizs) avant de se connecter, on enregistre son résultat.
// Uniquement pour les utilisateurs de base.
req.body.UserId=user.id; req.body.UserId=user.id;
if(req.body.QuestionnaireId) if(req.body.QuestionnaireId && user.status === "user")
{ await answerCtrl.saveAnswerToQuestionnaire(req.body);
const access=await subscriptionCtrl.checkQuestionnaireAccess(user.id, req.body.QuestionnaireId); else if(req.body.GroupId && user.status === "user")
if(!access) await answerCtrl.saveAnswerToGroup(req.body);
await db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] });
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
answerCtrl.creaUserAnswersJson(req.body.UserId);
answerCtrl.creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
answerCtrl.creaUserStatsAnwsersJson(req.body.UserId);
}
res.status(200).json( res.status(200).json(
{ {
userId: user.id, userId: user.id,
@ -404,8 +396,8 @@ exports.checkLoginLink = async (req, res, next) =>
try try
{ {
const db = require("../models/index"); const db = require("../models/index");
const userDatas= await checkTokenUser(req.body.t); const userDatas = await checkTokenUser(req.body.t);
if(userDatas.User.status!=="user") if(userDatas.User.status !== "user")
res.status(403).json({ errors: [txtGeneral.notAllowed] }); res.status(403).json({ errors: [txtGeneral.notAllowed] });
else if(userDatas.Subscription) else if(userDatas.Subscription)
{ {
@ -416,18 +408,12 @@ exports.checkLoginLink = async (req, res, next) =>
let loginTime=config.tokenConnexionMinTimeInHours; let loginTime=config.tokenConnexionMinTimeInHours;
if(userDatas.decodedToken.keepConnected===true) if(userDatas.decodedToken.keepConnected===true)
loginTime=config.tokenConnexionMaxTimeInDays; loginTime=config.tokenConnexionMaxTimeInDays;
// si des données concernant un quiz ont été transmises, je les enregistre ici : // Si l'utilisateur a répondu à un quiz (ou groupe de quizs) avant de se connecter, on enregistre son résultat :
req.body.UserId=userDatas.User.id;
if(req.body.QuestionnaireId) if(req.body.QuestionnaireId)
{ await answerCtrl.saveAnswerToQuestionnaire(req.body);
req.body.UserId=userDatas.User.id; else if(req.body.GroupId)
const access=await subscriptionCtrl.checkQuestionnaireAccess(userDatas.User.id, req.body.QuestionnaireId); await answerCtrl.saveAnswerToGroup(req.body);
if(!access)
await db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] });
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
answerCtrl.creaUserAnswersJson(req.body.UserId);
answerCtrl.creaUserStatsAnwsersJson(req.body.UserId);
answerCtrl.creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
}
res.status(200).json( res.status(200).json(
{ {
userId: userDatas.User.id, userId: userDatas.User.id,

View File

@ -1,4 +1,4 @@
// -- GESTION DU FORMULAIRE PERMETTANT D'AFFICHER ET RÉPONDRE À UN QUIZ // -- GESTION DU FORMULAIRE PERMETTANT D'AFFICHER ET RÉPONDRE À UN GROUPE DE QUIZS
/// Il n'est pas nécessaire d'être connecté pour répondre au quiz et voir son résultat. /// Il n'est pas nécessaire d'être connecté pour répondre au quiz et voir son résultat.
/// Mais si pas connecté, on propose à l'internaute de se connecter ou de créer un compte pour sauvegarder son résultat. /// Mais si pas connecté, on propose à l'internaute de se connecter ou de créer un compte pour sauvegarder son résultat.
@ -8,7 +8,7 @@
// Fichier de configuration tirés du backend : // Fichier de configuration tirés du backend :
import { apiUrl, availableLangs, theme } from "../../config/instance.js"; import { apiUrl, availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0]; const lang=availableLangs[0];
import { getPreviousAnswers, questionnaireRoutes, saveAnswersRoute } from "../../config/questionnaires.js"; import { getPreviousAnswers, groupRoutes, saveAnswersRoute } from "../../config/questionnaires.js";
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js"); const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
import { checkAnswerOuput, saveAnswer } from "./tools/answers.js"; import { checkAnswerOuput, saveAnswer } from "./tools/answers.js";
@ -25,19 +25,28 @@ const { noPreviousAnswer, previousAnswersLine, previousAnswersStats, previousAns
const { serverError } = require("../../lang/"+lang+"/general"); const { serverError } = require("../../lang/"+lang+"/general");
// Principaux éléments du DOM manipulés : // Principaux éléments du DOM manipulés :
const myForm = document.getElementById("questionnaire");
const divResponse = document.getElementById("response");
const btnShow = document.getElementById("showQuestionnaire");
const btnSubmit = document.getElementById("checkResponses"); const btnSubmit = document.getElementById("checkResponses");
const divResponse = document.getElementById("response");
const explanationsTitle = document.getElementById("explanationsTitle"); const explanationsTitle = document.getElementById("explanationsTitle");
const explanationsContent = document.getElementById("explanationsContent"); const explanationsContent = document.getElementById("explanationsContent");
const myForm = document.getElementById("group");
// Affiche le bouton de soumission + déclenche le chronomètre mesurant la durée de la réponse.
let chronoBegin=0;
const beginAnswer = () =>
{
chronoBegin=Date.now();
btnSubmit.style.display="block";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview".
}
let isConnected, user; let isConnected, user;
const initialise = async () => const initialise = async () =>
{ {
try try
{ {
btnShow.style.display="inline";// bouton caché si JS inactif, car JS nécessaire pour vérifier les réponses // Si JS activé, on affiche le bouton de soumission du formulaire :
beginAnswer();
isConnected=await checkSession(["user"]);// "user" car seuls les utilisateurs de base peuvent enregistrer leurs réponses aux quizs isConnected=await checkSession(["user"]);// "user" car seuls les utilisateurs de base peuvent enregistrer leurs réponses aux quizs
// Si l'utilisateur est connecté et a déjà répondu à ce quiz, on affiche ses précédentes réponses à la place du texte servant à expliquer le topo aux nouveaux // Si l'utilisateur est connecté et a déjà répondu à ce quiz, on affiche ses précédentes réponses à la place du texte servant à expliquer le topo aux nouveaux
if(isConnected) if(isConnected)
@ -57,40 +66,6 @@ const initialise = async () =>
initialise(); initialise();
helloDev(); helloDev();
// Affichage du questionnaire quand l'utilisateur clique sur le bouton ou si l'id du formulaire est passée par l'url.
// Déclenche en même temps le chronomètre mesurant la durée de la réponse aux questions.
const showQuestionnaire = () =>
{
chronoBegin=Date.now();
myForm.style.display="block";
btnShow.style.display="none";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview".
if(window.location.hash!=="")
{
window.location.hash="";// ! le "#" reste
window.location.assign(here+"questionnaire");
}
else
window.location.assign(here+"#questionnaire");
}
let chronoBegin=0;
btnShow.addEventListener("click", function(e)
{
try
{
e.preventDefault();
showQuestionnaire();
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
});
// Lien passé par mail pour voir directement le quiz
if(location.hash!="" && location.hash==="#questionnaire")
showQuestionnaire();
// Traitement de l'envoi de la réponse de l'utilisateur : // Traitement de l'envoi de la réponse de l'utilisateur :
let answer = {}; let answer = {};
myForm.addEventListener("submit", function(e) myForm.addEventListener("submit", function(e)
@ -99,12 +74,12 @@ myForm.addEventListener("submit", function(e)
{ {
e.preventDefault(); e.preventDefault();
btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :) btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :)
divResponse.innerHTML="";// supprime les éventuels messages déjà affichés divResponse.innerHTML="";// supprime les éventuels messages déjà affichés.
const userResponses=getDatasFromInputs(myForm); const userResponses=getDatasFromInputs(myForm);
answer.duration=Math.round((Date.now()-chronoBegin)/1000); answer.duration=Math.round((Date.now()-chronoBegin)/1000);
answer.nbQuestions=0; answer.nbQuestions=0;
answer.nbCorrectAnswers=0; answer.nbCorrectAnswers=0;
answer.QuestionnaireId=document.getElementById("questionnaireId").value; answer.GroupId=document.getElementById("groupId").value;
// Les réponses sont regroupées par question, donc quand idQuestion change, on connaît le résultat pour la question précédente. // Les réponses sont regroupées par question, donc quand idQuestion change, on connaît le résultat pour la question précédente.
// Pour qu'une réponse soit bonne, il faut cocher toutes les bonnes réponses (si QCM) à la question ET cocher aucune des mauvaises. // Pour qu'une réponse soit bonne, il faut cocher toutes les bonnes réponses (si QCM) à la question ET cocher aucune des mauvaises.
let idChoice, idQuestion="", goodResponse=false; let idChoice, idQuestion="", goodResponse=false;
@ -113,32 +88,31 @@ myForm.addEventListener("submit", function(e)
if(item.startsWith("isCorrect_response_"))// = Nouvelle réponse possible. if(item.startsWith("isCorrect_response_"))// = Nouvelle réponse possible.
{ {
idChoice = item.substring(item.lastIndexOf("_") + 1); idChoice = item.substring(item.lastIndexOf("_") + 1);
// si on change de queston if(userResponses["question_id_response_"+idChoice] != idQuestion) // = on commence à traiter une nouvelle question.
if(userResponses["question_id_response_"+idChoice]!=idQuestion) // on commence à traiter une nouvelle question
{ {
idQuestion=userResponses["question_id_response_"+idChoice]; idQuestion=userResponses["question_id_response_"+idChoice];
answer.nbQuestions++; answer.nbQuestions++;
if(goodResponse) // résultat de la question précédente if(goodResponse) // = pas d'erreur à la question précédente
answer.nbCorrectAnswers++; answer.nbCorrectAnswers++;
goodResponse=true;// réponse bonne jusqu'à la première erreur... goodResponse=true;// La réponse est considérée comme bonne, jusqu'à la première erreur...
} }
if(userResponses[item]=="true") if(userResponses[item] == "true")
{ {
document.getElementById("response_"+idChoice).parentNode.classList.add("isCorrect"); document.getElementById("response_"+idChoice).parentNode.classList.add("isCorrect");
if(userResponses["response_"+idChoice]===undefined)// une bonne réponse n'a pas été sélectionnée if(userResponses["response_"+idChoice] === undefined)// = une bonne réponse n'a pas été sélectionnée
goodResponse=false; goodResponse=false;
} }
else else
{ {
if(userResponses["response_"+idChoice]==="on")// réponse cochée ne faisant pas partie des bonnes if(userResponses["response_"+idChoice] === "on")
{ {
goodResponse=false; goodResponse=false; // = une mauvaise réponse a été sélectionnée
document.getElementById("response_"+idChoice).parentNode.classList.add("isNotCorrect"); document.getElementById("response_"+idChoice).parentNode.classList.add("isNotCorrect");
} }
} }
} }
} }
// si j'ai bien répondu à la dernière question, il faut le compter ici, car je suis sorti de la boucle : // Si j'ai bien répondu à la dernière question, il faut le compter ici, car on est sorti de la boucle :
if(goodResponse) if(goodResponse)
answer.nbCorrectAnswers++; answer.nbCorrectAnswers++;
@ -146,9 +120,9 @@ myForm.addEventListener("submit", function(e)
let getOuput=checkAnswerOuput(answer); let getOuput=checkAnswerOuput(answer);
if(isConnected) if(isConnected)
{ {
// Si l'utilisateur est connecté, on enregistre son résultat sur le serveur. // Si l'utilisateur est connecté, on passe son résultat au serveur pour le sauvegarder.
const xhrSaveAnswer = new XMLHttpRequest(); const xhrSaveAnswer = new XMLHttpRequest();
xhrSaveAnswer.open("POST", apiUrl+questionnaireRoutes+saveAnswersRoute); xhrSaveAnswer.open("POST", apiUrl+groupRoutes+saveAnswersRoute);
xhrSaveAnswer.onreadystatechange = function() xhrSaveAnswer.onreadystatechange = function()
{ {
if (this.readyState == XMLHttpRequest.DONE) if (this.readyState == XMLHttpRequest.DONE)
@ -161,7 +135,7 @@ myForm.addEventListener("submit", function(e)
} }
else else
getOuput+="<br>"+responseSavedError.replace("#URL", configTemplate.userHomePage); getOuput+="<br>"+responseSavedError.replace("#URL", configTemplate.userHomePage);
// on redirige vers le résultat // Puis on le redirige vers son résultat :
window.location.hash=""; window.location.hash="";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"... const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"...
window.location.assign(here+"explanations"); window.location.assign(here+"explanations");
@ -169,31 +143,29 @@ myForm.addEventListener("submit", function(e)
} }
xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token); xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token);
xhrSaveAnswer.setRequestHeader("Content-Type", "application/json"); xhrSaveAnswer.setRequestHeader("Content-Type", "application/json");
answer.timeDifference=getTimeDifference();// on en profite pour mettre les pendules à l'heure. answer.timeDifference=getTimeDifference();// On en profite pour mettre les pendules à l'heure.
xhrSaveAnswer.send(JSON.stringify(answer)); xhrSaveAnswer.send(JSON.stringify(answer));
} }
else else
{ // si pas connecté, on enregistre le résultat côté client pour permettre de le retrouver au moment de la création du compte ou de la connexion. { // Si internaute non connecté, on enregistre le résultat côté client pour permettre de le retrouver au moment de la création du compte ou de la connexion.
if(saveAnswer(answer)) if(saveAnswer(answer))
{ {
getOuput+="<br><br>"+wantToSaveResponses; getOuput+="</p><p>"+wantToSaveResponses+"</p>";
addElement(divResponse, "p", getOuput, "", ["info"]); addElement(divResponse, "p", getOuput, "", ["success"]);
document.querySelector(".subscribeBtns").style.display="block"; document.querySelector(".subscribeBtns").style.display="block";
} }
else // inutile de proposer de créer un compte si le stockage local ne fonctionne pas else // Mais inutile de proposer de créer un compte si le stockage local ne fonctionne pas
addElement(divResponse, "p", getOuput, "", ["info"]); addElement(divResponse, "p", getOuput, "", ["success"]);
// on redirige vers le résultat // Puis on le redirige vers son résultat :
window.location.hash=""; window.location.hash="";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"... const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"...
window.location.assign(here+"response"); window.location.assign(here+"response");
} }
// + affichage des textes d'explications pour chaque question // + Affichage des textes d'explications pour chaque question
const explanations=document.querySelectorAll(".help"); const explanations=document.querySelectorAll(".help");
for(let i in explanations) for(let i in explanations)
{ if(explanations[i].style !== undefined) // sinon, la console affiche une erreur "TypeError: explanations[i].style is undefined", bien que tout fonctionne (?)
if(explanations[i].style!=undefined) // sinon, la console affiche une erreur "TypeError: explanations[i].style is undefined", bien que tout fonctionne (?)
explanations[i].style.display="block"; explanations[i].style.display="block";
}
} }
catch(e) catch(e)
{ {
@ -202,12 +174,12 @@ myForm.addEventListener("submit", function(e)
} }
}) })
// Fonction vérifiant les précédentes réponses de l'utilisateur // Fonction vérifiant les précédentes réponses de l'utilisateur.
// Utile si connecté lors du premier chargement de la page, puis après une nouvelle réponse // Utile si connecté lors du premier chargement de la page, puis après une nouvelle réponse.
const checkPreviousResponses = (user) => const checkPreviousResponses = (user) =>
{ {
const xhrPreviousRes = new XMLHttpRequest(); const xhrPreviousRes = new XMLHttpRequest();
xhrPreviousRes.open("GET", apiUrl+questionnaireRoutes+getPreviousAnswers+user.id+"/"+document.getElementById("questionnaireId").value); xhrPreviousRes.open("GET", apiUrl+groupRoutes+getPreviousAnswers+user.id+"/"+document.getElementById("groupId").value);
xhrPreviousRes.onreadystatechange = function() xhrPreviousRes.onreadystatechange = function()
{ {
if (this.readyState == XMLHttpRequest.DONE) if (this.readyState == XMLHttpRequest.DONE)
@ -247,7 +219,6 @@ const checkPreviousResponses = (user) =>
addElement(explanationsContent, "ul", noPreviousAnswer); addElement(explanationsContent, "ul", noPreviousAnswer);
// dans un cas comme dans l'autre, bouton pour revenir à l'accueil du compte // dans un cas comme dans l'autre, bouton pour revenir à l'accueil du compte
addElement(explanationsContent, "p", "<a href=\"/"+configTemplate.userHomePage+"\" class=\"button cardboard\">"+configTemplate.userHomePageTxt+"</a>", "", ["btn"], "", false); addElement(explanationsContent, "p", "<a href=\"/"+configTemplate.userHomePage+"\" class=\"button cardboard\">"+configTemplate.userHomePageTxt+"</a>", "", ["btn"], "", false);
} }
} }
} }

View File

@ -86,6 +86,7 @@ const initialise = async () =>
xhrStats.setRequestHeader("Authorization", "Bearer "+user.token); xhrStats.setRequestHeader("Authorization", "Bearer "+user.token);
xhrStats.send(); xhrStats.send();
/*
// Par défaut, on affiche des derniers quizs proposés sans réponse : // Par défaut, on affiche des derniers quizs proposés sans réponse :
const xhrLastQuizs = new XMLHttpRequest(); const xhrLastQuizs = new XMLHttpRequest();
xhrLastQuizs.open("GET", apiUrl+questionnaireRoutes+getQuestionnairesWithoutAnswer+""+user.id+"/"+0+"/"+configTemplate.nbQuestionnairesUserHomePage+"/html"); xhrLastQuizs.open("GET", apiUrl+questionnaireRoutes+getQuestionnairesWithoutAnswer+""+user.id+"/"+0+"/"+configTemplate.nbQuestionnairesUserHomePage+"/html");
@ -113,7 +114,7 @@ const initialise = async () =>
} }
} }
xhrLastQuizs.setRequestHeader("Authorization", "Bearer "+user.token); xhrLastQuizs.setRequestHeader("Authorization", "Bearer "+user.token);
xhrLastQuizs.send(); xhrLastQuizs.send();*/
// Traitement du lancement d'une recherche // Traitement du lancement d'une recherche
// La recherche peut être lancée via la bouton submit ou un lien de pagination // La recherche peut être lancée via la bouton submit ou un lien de pagination

View File

@ -8,7 +8,7 @@ const txt = require("../../../lang/"+configFrontEnd.lang+"/answer");
// Enregistrement côté client du dernier résultat à un quiz en attendant d'être connecté // Enregistrement côté client du dernier résultat à un quiz en attendant d'être connecté
export const saveAnswer = (answer) => export const saveAnswer = (answer) =>
{ {
if(!isEmpty(answer.duration) && !isEmpty(answer.nbCorrectAnswers) && !isEmpty(answer.nbQuestions) && !isEmpty(answer.QuestionnaireId)) if(!isEmpty(answer.duration) && !isEmpty(answer.nbCorrectAnswers) && !isEmpty(answer.nbQuestions) && (!isEmpty(answer.QuestionnaireId) || !isEmpty(answer.GroupId)))
{ {
saveLocaly("lastAnswer", answer); saveLocaly("lastAnswer", answer);
return true; return true;
@ -44,48 +44,3 @@ export const checkAnswerOuput = (answer) =>
else else
return ""; return "";
} }
/*
export const checkSession = async (config) =>
{
return new Promise((resolve, reject) =>
{
if(isEmpty(localStorage.getItem("user")))
resolve(false);
else
{
const user=JSON.parse(localStorage.getItem("user"));
if(user.duration===undefined || user.duration < Date.now())
{
localStorage.removeItem("user");
resolve(false);
}
else
{
const xhr = new XMLHttpRequest();
xhr.open("GET", configFrontEnd.apiUrl+config.userRoutes+config.checkLoginRoute+user.token);
xhr.onload = () =>
{
let response=JSON.parse(xhr.responseText);
if (xhr.status === 200 && response.isValid && response.id != undefined)
{
if(response.id===user.id)
resolve(true);
else
{
localStorage.removeItem("user");
resolve(false);
}
}
else
{
localStorage.removeItem("user");
resolve(false);
}
}
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
}
}
});
}*/

View File

@ -31,7 +31,7 @@ export const setSession = (userId, token, durationTS) =>
saveLocaly("user", storageUser); saveLocaly("user", storageUser);
} }
// Vérifie qu'il y a des données locales concernant le résultat d'un quiz // Vérifie qu'il y a des données locales concernant le résultat d'un quiz ou d'un groupe de quizs
// Et les ajoute aux données envoyées par les formulaires d'inscription/connexion si c'est le cas // Et les ajoute aux données envoyées par les formulaires d'inscription/connexion si c'est le cas
export const checkAnswerDatas = (datas) => export const checkAnswerDatas = (datas) =>
{ {
@ -39,12 +39,15 @@ export const checkAnswerDatas = (datas) =>
if(!isEmpty(lastAnswer)) if(!isEmpty(lastAnswer))
{ {
const answer=JSON.parse(lastAnswer); const answer=JSON.parse(lastAnswer);
if(!isEmpty(answer.duration) && !isEmpty(answer.nbCorrectAnswers) && !isEmpty(answer.QuestionnaireId) && !isEmpty(answer.nbQuestions)) if(!isEmpty(answer.duration) && !isEmpty(answer.nbCorrectAnswers) && !isEmpty(answer.nbQuestions) && (!isEmpty(answer.QuestionnaireId) || !isEmpty(answer.GroupId)))
{ {
datas.duration=answer.duration; datas.duration=answer.duration;
datas.nbCorrectAnswers=answer.nbCorrectAnswers; datas.nbCorrectAnswers=answer.nbCorrectAnswers;
datas.QuestionnaireId=answer.QuestionnaireId;
datas.nbQuestions=answer.nbQuestions; datas.nbQuestions=answer.nbQuestions;
if(!isEmpty(answer.QuestionnaireId))
datas.QuestionnaireId=answer.QuestionnaireId;
else
datas.GroupId=answer.GroupId;
} }
} }
return datas; return datas;

View File

@ -1,8 +1,8 @@
module.exports = module.exports =
{ {
checkResponsesOuputFail : "Vous avez répondu en DURATION secondes et avez NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions. C'est certain, vous ferez mieux la prochaine fois !", checkResponsesOuputFail : "Vous avez répondu en DURATION secondes et avez <u><b>NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions</b></u>. C'est certain, vous ferez mieux la prochaine fois !",
checkResponsesOuputMedium : "Vous avez répondu en DURATION secondes et avez NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions. C'est pas mal du tout !", checkResponsesOuputMedium : "Vous avez répondu en DURATION secondes et avez <u><b>NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions</b></u>. C'est pas mal du tout !",
checkResponsesOuputSuccess : "Vous avez répondu en DURATION secondes et avez NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions. Bravo ! Rien ne vous échappe !", checkResponsesOuputSuccess : "Vous avez répondu en DURATION secondes et avez <u><b>NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions</b></u>. Bravo ! Rien ne vous échappe !",
nbQuestionnaireWithoudAnswer: "Il y a #NB quizs qui vous ont été proposés et auxquels vous n'avez pas répondu. Voici les derniers :!", nbQuestionnaireWithoudAnswer: "Il y a #NB quizs qui vous ont été proposés et auxquels vous n'avez pas répondu. Voici les derniers :!",
needIntegerNumberCorrectResponses : "Le nombre de réponses correctes doit être un nombre entier.", needIntegerNumberCorrectResponses : "Le nombre de réponses correctes doit être un nombre entier.",
needIntegerNumberSecondesResponse : "La durée de la réponse doit être un nombre entier de secondes.", needIntegerNumberSecondesResponse : "La durée de la réponse doit être un nombre entier de secondes.",
@ -22,5 +22,5 @@ module.exports =
responseSavedError : "Cependant une erreur a été rencontrée durant l'enregistrement de votre résultat. <a href='/#URL'>Accèder à tous vos quizs</a>.", responseSavedError : "Cependant une erreur a été rencontrée durant l'enregistrement de votre résultat. <a href='/#URL'>Accèder à tous vos quizs</a>.",
responseSavedMessage : "Votre résultat a été enregistré. <a href='/#URL'>Accèder à tous vos quizs</a>.", responseSavedMessage : "Votre résultat a été enregistré. <a href='/#URL'>Accèder à tous vos quizs</a>.",
statsUser: "Vous avez enregistré NBANSWERS réponses à <b>NBQUESTIONNAIRES questionnaires différents</b> sur les NBTOTQUESTIONNAIRES proposés par le site.<br>En moyenne, vous avez mis AVGDURATION secondes à répondre et avez <b>correctement répondu à AVGCORRECTANSWERS % des questions</b>.", statsUser: "Vous avez enregistré NBANSWERS réponses à <b>NBQUESTIONNAIRES questionnaires différents</b> sur les NBTOTQUESTIONNAIRES proposés par le site.<br>En moyenne, vous avez mis AVGDURATION secondes à répondre et avez <b>correctement répondu à AVGCORRECTANSWERS % des questions</b>.",
wantToSaveResponses: "Si vous le souhaitez, vous pouvez sauvegarder votre résultat <u>en créant votre compte ci-dessous</u>.<br><b>Cela vous permettra aussi de recevoir régulièrement de nouveaux quizs par e-mail</b>.", wantToSaveResponses: "Si vous le souhaitez, vous pouvez <u><b>sauvegarder votre résultat</b></u> en créant votre compte ci-dessous. Cela vous permettra aussi de <u><b>recevoir régulièrement de nouvelles \"graines de culture\"</b></u> directement sur votre e-mail.",
}; };

View File

@ -2,7 +2,7 @@ module.exports =
{ {
btnSendResponse: "Testez vos réponses.", btnSendResponse: "Testez vos réponses.",
btnShareQuizTxt: "Partager ce quiz sur ", btnShareQuizTxt: "Partager ce quiz sur ",
commonIntroTxt: "Ce quiz vous permet tester ce que vous avez retenu des textes proposés à la lecture. Au besoin, cliquez sur le bouton suivant pour les relire :", commonIntroTxt: "Ce quiz vous permet tester ce que vous avez retenu des textes proposés à la lecture. Au besoin, cliquez sur le bouton suivant pour les relire.",
correctAnswerTxt: "Bonne réponse", correctAnswerTxt: "Bonne réponse",
groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues
haveBeenPublished: "#NB nouveaux groupes de quizs ont été publiés.", haveBeenPublished: "#NB nouveaux groupes de quizs ont été publiés.",

View File

@ -1,16 +1,21 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const auth = require("../middleware/authAdmin"); const auth = require("../middleware/auth");
const authAdmin = require("../middleware/authAdmin");
const answerCtrl = require("../controllers/answer");
const groupCtrl = require("../controllers/group"); const groupCtrl = require("../controllers/group");
router.post("/search", auth, groupCtrl.searchGroups); router.post("/search", authAdmin, groupCtrl.searchGroups);
router.post("/", auth, groupCtrl.create); router.post("/", authAdmin, groupCtrl.create);
router.get("/stats", auth, groupCtrl.getStatsGroups); router.get("/stats", authAdmin, groupCtrl.getStatsGroups);
router.put("/:id", auth, groupCtrl.modify); router.put("/:id", authAdmin, groupCtrl.modify);
router.delete("/:id", auth, groupCtrl.delete); router.delete("/:id", authAdmin, groupCtrl.delete);
router.get("/get/:id", auth, groupCtrl.getOneById); router.get("/get/:id", authAdmin, groupCtrl.getOneById);
router.get("/preview/:id/:token", groupCtrl.showOneGroupById);// prévisualisation HTML, même si groupe "incomplet" router.get("/preview/:id/:token", groupCtrl.showOneGroupById);// prévisualisation HTML, même si groupe "incomplet"
router.post("/answer/", auth, answerCtrl.createInGroup);
router.get("/user/answers/:userId/:groupId", auth, answerCtrl.getAnswersByGroup);
module.exports = router; module.exports = router;

View File

@ -25,6 +25,6 @@ router.get("/preview/:id/:token", questionnaireCtrl.showOneQuestionnaireById);//
router.post("/answer/", auth, answerCtrl.create); router.post("/answer/", auth, answerCtrl.create);
router.get("/user/anwswers/stats/:userId", auth, answerCtrl.getStatsByUser); router.get("/user/anwswers/stats/:userId", auth, answerCtrl.getStatsByUser);
router.get("/user/answers/:userId/:questionnaireId", auth, answerCtrl.getAnswersByQuestionnaire); router.get("/user/answers/:userId/:questionnaireId", auth, answerCtrl.getAnswersByQuestionnaire);
router.get("/withoutanswer/user/:id/:begin/:nb/:output", auth, answerCtrl.getQuestionnairesWithouAnswerByUser); //router.get("/withoutanswer/user/:id/:begin/:nb/:output", auth, answerCtrl.getQuestionnairesWithouAnswerByUser);
module.exports = router; module.exports = router;

View File

@ -57,8 +57,8 @@ block content
noscript noscript
div div
strong #{configTpl.noJSNotification} strong #{configTpl.noJSNotification}
// à cacher si pas de JS !
form(id="group" method="POST" class="needJS") form(id="group" method="POST")
h2 #{group.Group.title} h2 #{group.Group.title}
div#response div#response
div(class="subscribeBtns") div(class="subscribeBtns")
@ -70,7 +70,7 @@ block content
for question in questionnaire.Questions for question in questionnaire.Questions
p(id="question_"+question.Question.id) #{question.Question.text} p(id="question_"+question.Question.id) #{question.Question.text}
if(question.Question.explanation) if(question.Question.explanation)
blockquote(class="help" id="help_"+question.Question.id cite=questionnaire.Links[0].url) #{txtexplanationBeforeTxt} #{question.Question.explanation} blockquote(class="help" id="help_"+question.Question.id cite="/"+configQuestionnaires.dirWebQuestionnaires+"/"+questionnaire.Questionnaire.slug+".html") #{txtexplanationBeforeTxt} #{question.Question.explanation}
ul(class="checkbox_li") ul(class="checkbox_li")
for response in question.Choices for response in question.Choices
li(class="checkbox_li") li(class="checkbox_li")
@ -85,9 +85,10 @@ block content
input(type="hidden" name="isCorrect_response_"+response.id id="isCorrect_response_"+response.id value=""+response.isCorrect) input(type="hidden" name="isCorrect_response_"+response.id id="isCorrect_response_"+response.id value=""+response.isCorrect)
input(type="hidden" name="question_id_response_"+response.id id="question_id_response_"+response.id value=question.Question.id) input(type="hidden" name="question_id_response_"+response.id id="question_id_response_"+response.id value=question.Question.id)
input(name="groupId" id="groupId" value=group.Group.id type="hidden") input(name="groupId" id="groupId" value=group.Group.id type="hidden")
// Bouton submit caché si pas de JS, car nécessaire au traitement de la réponse
p p
span(class="input_wrapper") span(class="input_wrapper")
input(id="checkResponses" type="submit" value=txtGroups.btnSendResponse class="cardboard" title=txtGroups.btnSendResponse) input(id="checkResponses" type="submit" value=txtGroups.btnSendResponse class="cardboard needJS" title=txtGroups.btnSendResponse)
div#zerozozio div#zerozozio
a(href="http://sharetodiaspora.github.io/?url="+linkCanonical+"&title="+group.Group.title rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" diaspora* ("+txtGeneral.alertNewWindow+")" target="_blank") a(href="http://sharetodiaspora.github.io/?url="+linkCanonical+"&title="+group.Group.title rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" diaspora* ("+txtGeneral.alertNewWindow+")" target="_blank")