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")