From ca4be71a896f814105216ad588553e7d07a75d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20PENHO=C3=8BT?= Date: Thu, 22 Oct 2020 17:47:05 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20de=20la=20possibilit=C3=A9=20de=20r?= =?UTF-8?q?=C3=A9pondre=20=C3=A0=20un=20groupe=20de=20questions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/questionnaires.js | 6 +- controllers/answer.js | 270 ++++++++++++++++++++++----------- controllers/user.js | 56 +++---- front/src/group.js | 109 +++++-------- front/src/homeUser.js | 5 +- front/src/tools/answers.js | 49 +----- front/src/tools/users.js | 9 +- lang/fr/answer.js | 10 +- lang/fr/group.js | 2 +- routes/group.js | 19 ++- routes/questionnaire.js | 2 +- views/wikilerni/quiz-group.pug | 9 +- 12 files changed, 279 insertions(+), 267 deletions(-) diff --git a/config/questionnaires.js b/config/questionnaires.js index 2d926c5..94796ff 100644 --- a/config/questionnaires.js +++ b/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 : { diff --git a/controllers/answer.js b/controllers/answer.js index 18807fe..55e9abe 100644 --- a/controllers/answer.js +++ b/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 }); - 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); + } +} + +// 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); } } // 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,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 exports.getStatsByUser = 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(); + // + 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) @@ -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 +// 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; \ No newline at end of file +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); + } +} +* */ \ No newline at end of file diff --git a/controllers/user.js b/controllers/user.js index 26bd2b9..6cb2499 100644 --- a/controllers/user.js +++ b/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, diff --git a/front/src/group.js b/front/src/group.js index dcf7de6..dfec5db 100644 --- a/front/src/group.js +++ b/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+="
"+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+="

"+wantToSaveResponses; - addElement(divResponse, "p", getOuput, "", ["info"]); + getOuput+="

"+wantToSaveResponses+"

"; + 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", ""+configTemplate.userHomePageTxt+"", "", ["btn"], "", false); - } } } diff --git a/front/src/homeUser.js b/front/src/homeUser.js index b2bc0c7..e95ee13 100644 --- a/front/src/homeUser.js +++ b/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 diff --git a/front/src/tools/answers.js b/front/src/tools/answers.js index 81183e2..c8defd5 100644 --- a/front/src/tools/answers.js +++ b/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(); - } - } - }); -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/front/src/tools/users.js b/front/src/tools/users.js index 812da29..ca00b99 100644 --- a/front/src/tools/users.js +++ b/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; diff --git a/lang/fr/answer.js b/lang/fr/answer.js index 7e5b2f9..132e0f0 100644 --- a/lang/fr/answer.js +++ b/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 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 !", 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. Accèder à tous vos quizs.", responseSavedMessage : "Votre résultat a été enregistré. Accèder à tous vos quizs.", statsUser: "Vous avez enregistré NBANSWERS réponses à NBQUESTIONNAIRES questionnaires différents sur les NBTOTQUESTIONNAIRES proposés par le site.
En moyenne, vous avez mis AVGDURATION secondes à répondre et avez correctement répondu à AVGCORRECTANSWERS % des questions.", - wantToSaveResponses: "Si vous le souhaitez, vous pouvez sauvegarder votre résultat en créant votre compte ci-dessous.
Cela vous permettra aussi de recevoir régulièrement de nouveaux quizs par e-mail.", -}; + wantToSaveResponses: "Si vous le souhaitez, vous pouvez sauvegarder votre résultat en créant votre compte ci-dessous. Cela vous permettra aussi de recevoir régulièrement de nouvelles \"graines de culture\" directement sur votre e-mail.", +}; \ No newline at end of file diff --git a/lang/fr/group.js b/lang/fr/group.js index 4c6de88..a10230c 100644 --- a/lang/fr/group.js +++ b/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.", diff --git a/routes/group.js b/routes/group.js index c0b5b0e..e16c7d8 100644 --- a/routes/group.js +++ b/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; \ No newline at end of file diff --git a/routes/questionnaire.js b/routes/questionnaire.js index b7acec7..3bf0ef7 100644 --- a/routes/questionnaire.js +++ b/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; \ No newline at end of file diff --git a/views/wikilerni/quiz-group.pug b/views/wikilerni/quiz-group.pug index 302d979..dde1a4c 100644 --- a/views/wikilerni/quiz-group.pug +++ b/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")