Browse Source

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

master
Fabrice PENHOËT 11 months ago
parent
commit
ca4be71a89
  1. 6
      config/questionnaires.js
  2. 258
      controllers/answer.js
  3. 56
      controllers/user.js
  4. 109
      front/src/group.js
  5. 5
      front/src/homeUser.js
  6. 49
      front/src/tools/answers.js
  7. 9
      front/src/tools/users.js
  8. 10
      lang/fr/answer.js
  9. 2
      lang/fr/group.js
  10. 19
      routes/group.js
  11. 2
      routes/questionnaire.js
  12. 9
      views/wikilerni/quiz-group.pug

6
config/questionnaires.js

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

258
controllers/answer.js

@ -6,10 +6,11 @@ const configTpl = require("../views/"+config.theme+"/config/"+config.availableLa
const tool = require("../tools/main");
const toolFile = require("../tools/file");
const subscriptionCtrl = require("./subscription");
const groupCtrl = require("./group");
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");
// Enregistrement d'une réponse à un questionnaire
@ -18,33 +19,39 @@ exports.create = async (req, res, next) =>
try
{
const db = require("../models/index");
const checkQuestionnaireAccess=await subscriptionCtrl.checkQuestionnaireAccess(req.connectedUser.User.id, req.body.QuestionnaireId);
req.body.UserId=req.connectedUser.User.id;
if(checkQuestionnaireAccess) // l'utilisateur a déjà accès à ce questionnaire
await db["Answer"].create({ ...req.body }, { fields: ["nbQuestions", "nbCorrectAnswers", "duration", "QuestionnaireId", "UserId"] });
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 !
await saveAnswerToQuestionnaire(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);
}
}
// 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 });
creaUserStatsAnwsersJson(req.body.UserId);
creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
creaUserAnswersJson(req.body.UserId);
res.status(201).json({ message: txt.responseSavedMessage });
res.status(201).json({ message: txtAnswers.responseSavedMessage });
next();
}
catch(e)
{ // à priori, l'utilisateur ne peut pas avoir envoyé de données incorrectes, donc erreur application pour admin
{
next(e);
}
}
// 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) =>
{
try
@ -59,15 +66,14 @@ exports.getAnswersByQuestionnaire = async(req, res, next) =>
}
}
// Retourne les statistiques de l'utilisateur
exports.getStatsByUser = 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 stats=await getUserStatsAnswers(req.params.userId);
// J'ajoute les stats générales des questionnaires pour comparaison :
stats.general=await questionnaireCtrl.getStatsQuestionnaires();
res.status(200).json(stats);
const answers=await getUserAnswersByGroup(req.params.userId, req.params.groupId);
res.status(200).json(answers);
next();
}
catch(e)
{
@ -75,53 +81,16 @@ 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) =>
// Retourne les statistiques de l'utilisateur
exports.getStatsByUser = 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();
const stats=await getUserStatsAnswers(req.params.userId);
// + stats générales des questionnaires et groupes pour comparaison :
stats.questionnaires=await questionnaireCtrl.getStatsQuestionnaires();
stats.groups=await groupCtrl.getStatsGroups();
res.status(200).json(stats);
}
catch(e)
{
@ -132,14 +101,55 @@ exports.getQuestionnairesWithouAnswerByUser = async(req, res, next) =>
// 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
// !! à surveiller car fichier pouvant devenir gros ! mais utile pour future SVG côté client
const creaUserAnswersJson = async (UserId) =>
{
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)
{
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;
}
else
@ -147,7 +157,7 @@ const creaUserAnswersJson = async (UserId) =>
}
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) =>
{
let userAnswers=await toolFile.readJSON(config.dirCacheUsersAnswers, UserId);
@ -158,29 +168,49 @@ const getUserAnswersByQuestionnaire = async (UserId, QuestionnaireId) =>
const answers=[];
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]);
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;
}
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 db = require("../models/index");
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 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)
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 getUserQuestionnairesGroup = await db.sequelize.query("SELECT DISTINCT GroupId FROM Answers WHERE UserId=:id AND GroupId 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 });
if(getUserAnswers && getUserQuestionnaires && getUserQuestionnairesGroup)
{
const stats =
{
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.avgDuration=getUserStats[0].avgDuration;
@ -193,7 +223,7 @@ const creaUserStatsAnwsersJson = async (UserId) =>
}
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) =>
{
let userStats=await toolFile.readJSON(config.dirCacheUsersAnswers, "stats"+UserId);
@ -205,7 +235,7 @@ const getUserStatsAnswers = async (UserId) =>
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 db = require("../models/index");
@ -225,7 +255,9 @@ const getStatsAnswers = async () =>
}
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) =>
{
UserId=tool.trimIfNotNull(UserId);
@ -270,4 +302,62 @@ const getUserQuestionnairesWithoutAnswer = async (UserId, begin=0, nb=10) =>
}
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);
}
}
* */

56
controllers/user.js

@ -62,7 +62,8 @@ exports.getGodfatherId = async (req, res, next) =>
}
// 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) =>
{
try
@ -76,7 +77,6 @@ exports.signup = async (req, res, next) =>
{
// Un mot de passe temporaire et non communiqué est généré :
req.body.passwordTemp=tool.getPassword(8, 12);
console.log(req.body.passwordTemp);
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 "@" :
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);
const user=await db["User"].create({ ...req.body }, { fields: ["name", "email", "password", "timeDifference"] });
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)
{
await Promise.all([
db["QuestionnaireAccess"].create({ ...req.body }, { fields: ["QuestionnaireId", "UserId"] }),
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 answerCtrl.saveAnswerToQuestionnaire(req.body);
else if(req.body.GroupId)
await answerCtrl.saveAnswerToGroup(req.body);
await sendValidationLink(user);
res.status(201).json({ message: txt.mailValidationMessage });
next();
@ -312,23 +309,18 @@ exports.login = async (req, res, next) =>
const now=new Date();
const timeDifference=req.body.timeDifference;
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 :
let loginTime=config.tokenConnexionMinTimeInHours;
if((req.body.keepConnected==="true") && (user.status==="user"))
if((req.body.keepConnected === "true") && (user.status === "user"))
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;
if(req.body.QuestionnaireId)
{
const access=await subscriptionCtrl.checkQuestionnaireAccess(user.id, req.body.QuestionnaireId);
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.creaUserQuestionnairesWithoutAnswerJson(req.body.UserId);
answerCtrl.creaUserStatsAnwsersJson(req.body.UserId);
}
if(req.body.QuestionnaireId && user.status === "user")
await answerCtrl.saveAnswerToQuestionnaire(req.body);
else if(req.body.GroupId && user.status === "user")
await answerCtrl.saveAnswerToGroup(req.body);
res.status(200).json(
{
userId: user.id,
@ -404,8 +396,8 @@ exports.checkLoginLink = async (req, res, next) =>
try
{
const db = require("../models/index");
const userDatas= await checkTokenUser(req.body.t);
if(userDatas.User.status!=="user")
const userDatas = await checkTokenUser(req.body.t);
if(userDatas.User.status !== "user")
res.status(403).json({ errors: [txtGeneral.notAllowed] });
else if(userDatas.Subscription)
{
@ -416,18 +408,12 @@ exports.checkLoginLink = async (req, res, next) =>
let loginTime=config.tokenConnexionMinTimeInHours;
if(userDatas.decodedToken.keepConnected===true)
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)
{
req.body.UserId=userDatas.User.id;
const access=await subscriptionCtrl.checkQuestionnaireAccess(userDatas.User.id, req.body.QuestionnaireId);
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);
}
await answerCtrl.saveAnswerToQuestionnaire(req.body);
else if(req.body.GroupId)
await answerCtrl.saveAnswerToGroup(req.body);
res.status(200).json(
{
userId: userDatas.User.id,

109
front/src/group.js

@ -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.
/// 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 :
import { apiUrl, availableLangs, theme } from "../../config/instance.js";
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");
import { checkAnswerOuput, saveAnswer } from "./tools/answers.js";
@ -25,19 +25,28 @@ const { noPreviousAnswer, previousAnswersLine, previousAnswersStats, previousAns
const { serverError } = require("../../lang/"+lang+"/general");
// 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 divResponse = document.getElementById("response");
const explanationsTitle = document.getElementById("explanationsTitle");
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;
const initialise = async () =>
{
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
// 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)
@ -57,40 +66,6 @@ const initialise = async () =>
initialise();
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 :
let answer = {};
myForm.addEventListener("submit", function(e)
@ -99,12 +74,12 @@ myForm.addEventListener("submit", function(e)
{
e.preventDefault();
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);
answer.duration=Math.round((Date.now()-chronoBegin)/1000);
answer.nbQuestions=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.
// 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;
@ -113,32 +88,31 @@ myForm.addEventListener("submit", function(e)
if(item.startsWith("isCorrect_response_"))// = Nouvelle réponse possible.
{
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];
answer.nbQuestions++;
if(goodResponse) // résultat de la question précédente
if(goodResponse) // = pas d'erreur à la question précédente
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");
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;
}
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");
}
}
}
}
// 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)
answer.nbCorrectAnswers++;
@ -146,9 +120,9 @@ myForm.addEventListener("submit", function(e)
let getOuput=checkAnswerOuput(answer);
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();
xhrSaveAnswer.open("POST", apiUrl+questionnaireRoutes+saveAnswersRoute);
xhrSaveAnswer.open("POST", apiUrl+groupRoutes+saveAnswersRoute);
xhrSaveAnswer.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
@ -161,7 +135,7 @@ myForm.addEventListener("submit", function(e)
}
else
getOuput+="<br>"+responseSavedError.replace("#URL", configTemplate.userHomePage);
// on redirige vers le résultat
// Puis on le redirige vers son résultat :
window.location.hash="";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"...
window.location.assign(here+"explanations");
@ -169,31 +143,29 @@ myForm.addEventListener("submit", function(e)
}
xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token);
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));
}
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))
{
getOuput+="<br><br>"+wantToSaveResponses;
addElement(divResponse, "p", getOuput, "", ["info"]);
getOuput+="</p><p>"+wantToSaveResponses+"</p>";
addElement(divResponse, "p", getOuput, "", ["success"]);
document.querySelector(".subscribeBtns").style.display="block";
}
else // inutile de proposer de créer un compte si le stockage local ne fonctionne pas
addElement(divResponse, "p", getOuput, "", ["info"]);
// on redirige vers le résultat
else // Mais inutile de proposer de créer un compte si le stockage local ne fonctionne pas
addElement(divResponse, "p", getOuput, "", ["success"]);
// Puis on le redirige vers son résultat :
window.location.hash="";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"...
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");
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";
}
}
catch(e)
{
@ -202,12 +174,12 @@ myForm.addEventListener("submit", function(e)
}
})
// 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
// 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.
const checkPreviousResponses = (user) =>
{
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()
{
if (this.readyState == XMLHttpRequest.DONE)
@ -247,7 +219,6 @@ const checkPreviousResponses = (user) =>
addElement(explanationsContent, "ul", noPreviousAnswer);
// 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);
}
}
}

5
front/src/homeUser.js

@ -85,7 +85,8 @@ const initialise = async () =>
}
xhrStats.setRequestHeader("Authorization", "Bearer "+user.token);
xhrStats.send();
/*
// Par défaut, on affiche des derniers quizs proposés sans réponse :
const xhrLastQuizs = new XMLHttpRequest();
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.send();
xhrLastQuizs.send();*/
// Traitement du lancement d'une recherche
// La recherche peut être lancée via la bouton submit ou un lien de pagination

49
front/src/tools/answers.js

@ -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é
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);
return true;
@ -43,49 +43,4 @@ export const checkAnswerOuput = (answer) =>
}
else
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();
}
}
});
}*/
}

9
front/src/tools/users.js

@ -31,7 +31,7 @@ export const setSession = (userId, token, durationTS) =>
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
export const checkAnswerDatas = (datas) =>
{
@ -39,12 +39,15 @@ export const checkAnswerDatas = (datas) =>
if(!isEmpty(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.nbCorrectAnswers=answer.nbCorrectAnswers;
datas.QuestionnaireId=answer.QuestionnaireId;
datas.nbQuestions=answer.nbQuestions;
if(!isEmpty(answer.QuestionnaireId))
datas.QuestionnaireId=answer.QuestionnaireId;
else
datas.GroupId=answer.GroupId;
}
}
return datas;

10
lang/fr/answer.js

@ -1,8 +1,8 @@
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 !",
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 !",
checkResponsesOuputSuccess : "Vous avez répondu en DURATION secondes et avez NBCORRECTANSWERS bonne(s) réponse(s) sur NBQUESTIONS questions. Bravo ! Rien ne vous échappe !",
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 <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 <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 :!",
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.",
@ -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>.",
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>.",
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.",
};

2
lang/fr/group.js

@ -2,7 +2,7 @@ module.exports =
{
btnSendResponse: "Testez vos réponses.",
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",
groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues
haveBeenPublished: "#NB nouveaux groupes de quizs ont été publiés.",

19
routes/group.js

@ -1,16 +1,21 @@
const express = require("express");
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");
router.post("/search", auth, groupCtrl.searchGroups);
router.post("/", auth, groupCtrl.create);
router.get("/stats", auth, groupCtrl.getStatsGroups);
router.put("/:id", auth, groupCtrl.modify);
router.delete("/:id", auth, groupCtrl.delete);
router.get("/get/:id", auth, groupCtrl.getOneById);
router.post("/search", authAdmin, groupCtrl.searchGroups);
router.post("/", authAdmin, groupCtrl.create);
router.get("/stats", authAdmin, groupCtrl.getStatsGroups);
router.put("/:id", authAdmin, groupCtrl.modify);
router.delete("/:id", authAdmin, groupCtrl.delete);
router.get("/get/:id", authAdmin, groupCtrl.getOneById);
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;

2
routes/questionnaire.js

@ -25,6 +25,6 @@ router.get("/preview/:id/:token", questionnaireCtrl.showOneQuestionnaireById);//
router.post("/answer/", auth, answerCtrl.create);
router.get("/user/anwswers/stats/:userId", auth, answerCtrl.getStatsByUser);
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;

9
views/wikilerni/quiz-group.pug

@ -57,8 +57,8 @@ block content
noscript
div
strong #{configTpl.noJSNotification}
// à cacher si pas de JS !
form(id="group" method="POST" class="needJS")
form(id="group" method="POST")
h2 #{group.Group.title}
div#response
div(class="subscribeBtns")
@ -70,7 +70,7 @@ block content
for question in questionnaire.Questions
p(id="question_"+question.Question.id) #{question.Question.text}
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")
for response in question.Choices
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="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")
// Bouton submit caché si pas de JS, car nécessaire au traitement de la réponse
p
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
a(href="http://sharetodiaspora.github.io/?url="+linkCanonical+"&title="+group.Group.title rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" diaspora* ("+txtGeneral.alertNewWindow+")" target="_blank")

Loading…
Cancel
Save