Évolution du frontend de manière à permettre la sauvegarde des résultats aux quizs dans le navigateur (IndexDB).

This commit is contained in:
Fabrice PENHOËT 2022-03-01 17:40:41 +01:00
parent 896fabb1b7
commit 70182b481f
14 changed files with 603 additions and 544 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,228 +0,0 @@
// -- 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.
/// Dans ce but son résultat est stocké dans son navigateur.
/// Si il est connecté, l'enregistrement de son résultat se fait automatiquement côté serveur et ses éventuels précédents résultats sont affichés.
// Fichier de configuration tirés du backend :
import { apiUrl, availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0];
import { getPreviousAnswers, groupRoutes, saveAnswersRoute } from "../../config/questionnaires.js";
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
import { checkAnswerOuput, saveAnswer } from "./tools/answers.js";
import { addElement } from "./tools/dom.js";
import { helloDev, updateAccountLink } from "./tools/everywhere.js";
import { getLocaly } from "./tools/clientstorage.js";
import { getDatasFromInputs } from "./tools/forms.js";
import { dateFormat, replaceAll } from "../../tools/main";
import { loadMatomo } from "./tools/matomo.js";
import { checkSession, getTimeDifference } from "./tools/users.js";
// Dictionnaires :
const { noPreviousAnswer, previousAnswersLine, previousAnswersStats, previousAnswersTitle, responseSavedError, wantToSaveResponses } = require("../../lang/"+lang+"/answer");
const { serverError } = require("../../lang/"+lang+"/general");
// Principaux éléments du DOM manipulés :
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";
}
let isConnected, user;
const initialise = async () =>
{
try
{
// 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)
{
user=getLocaly("user", true);
updateAccountLink(user.status, configTemplate);// lien vers le compte adapté pour les utilisateurs connectés
checkPreviousResponses(user);
}
else */
loadMatomo();
}
catch(e)
{
console.error(e);
}
}
initialise();
helloDev();
// Traitement de l'envoi de la réponse de l'utilisateur :
let answer = {};
myForm.addEventListener("submit", function(e)
{
try
{
e.preventDefault();
btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :)
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.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;
for(let item in userResponses)
{
if(item.startsWith("isCorrect_response_"))// = Nouvelle réponse possible.
{
idChoice = item.substring(item.lastIndexOf("_") + 1);
if(userResponses["question_id_response_"+idChoice] != idQuestion) // = on commence à traiter une nouvelle question.
{
idQuestion=userResponses["question_id_response_"+idChoice];
answer.nbQuestions++;
if(goodResponse) // = pas d'erreur à la question précédente
answer.nbCorrectAnswers++;
goodResponse=true;// La réponse est considérée comme bonne, jusqu'à la première erreur...
}
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
goodResponse=false;
}
else
{
if(userResponses["response_"+idChoice] === "on")
{
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 on est sorti de la boucle :
if(goodResponse)
answer.nbCorrectAnswers++;
// Affichage du résultat, suivant si l'utilisateur est connecté ou pas et son score :
let getOuput=checkAnswerOuput(answer);
/*if(isConnected)
{
// Si l'utilisateur est connecté, on passe son résultat au serveur pour le sauvegarder.
const xhrSaveAnswer = new XMLHttpRequest();
xhrSaveAnswer.open("POST", apiUrl+groupRoutes+saveAnswersRoute);
xhrSaveAnswer.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let xhrResponse=JSON.parse(this.responseText);
if (this.status === 201 && (xhrResponse.message))
{
getOuput+="<br>"+xhrResponse.message.replace("#URL", configTemplate.userHomePage);
checkPreviousResponses(user);
}
else
getOuput+="<br>"+responseSavedError.replace("#URL", configTemplate.userHomePage);
// 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.hash="explanations";
}
}
xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token);
xhrSaveAnswer.setRequestHeader("Content-Type", "application/json");
answer.timeDifference=getTimeDifference();// On en profite pour mettre les pendules à l'heure.
xhrSaveAnswer.send(JSON.stringify(answer));
}
else
{ // 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+="</p><p>"+wantToSaveResponses+"</p>";
addElement(divResponse, "p", getOuput, "", ["success"]);
document.querySelector(".subscribeBtns").style.display="block";
}
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.hash="response";
//}
// + 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 (?)
explanations[i].style.display="block";
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(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.
const checkPreviousResponses = (user) =>
{
const xhrPreviousRes = new XMLHttpRequest();
xhrPreviousRes.open("GET", apiUrl+groupRoutes+getPreviousAnswers+user.id+"/"+document.getElementById("groupId").value);
xhrPreviousRes.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200)
{
const nbResponses=response.length;
let previousAnswersContent="";
addElement(explanationsTitle, "span", previousAnswersTitle.replace("#NOM", user.name));
if(nbResponses!==0)
{
let totNbQuestions=0, totNbCorrectAnswers=0, totDuration=0, mapLineContent;
for(let i in response)
{
totNbQuestions+=response[i].nbQuestions;// ! on ne peut se baser sur la version actuelle du quiz, car le nombre de questions a pu évoluer.
totNbCorrectAnswers+=response[i].nbCorrectAnswers;
totDuration+=response[i].duration;
mapLineContent =
{
DATEANSWER : dateFormat(response[i].createdAt, lang),
NBCORRECTANSWERS : response[i].nbCorrectAnswers,
NBQUESTIONS : response[i].nbQuestions,
AVGDURATION : response[i].duration
};
previousAnswersContent+="<li>"+replaceAll(previousAnswersLine, mapLineContent)+"</li>";
}
mapLineContent =
{
AVGDURATION : Math.round(totDuration/nbResponses),
AVGCORRECTANSWERS : Math.round(totNbCorrectAnswers/totNbQuestions*100)
};
previousAnswersContent="<h5>"+replaceAll(previousAnswersStats, mapLineContent)+"</h5>"+previousAnswersContent;
addElement(explanationsContent, "ul", previousAnswersContent);
}
else
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);
}
}
}
xhrPreviousRes.setRequestHeader("Authorization", "Bearer "+user.token);
xhrPreviousRes.send();
} */

View File

@ -1,259 +0,0 @@
// -- GESTION DU FORMULAIRE PERMETTANT D'AFFICHER ET RÉPONDRE À UN QUIZ
/// 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.
/// Dans ce but son résultat est stocké dans son navigateur.
/// Si il est connecté, l'enregistrement de son résultat se fait automatiquement côté serveur et ses éventuels précédents résultats sont affichés.
// 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";
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
import { checkAnswerOuput, saveAnswer } from "./tools/answers.js";
import { addElement } from "./tools/dom.js";
import { helloDev, updateAccountLink } from "./tools/everywhere.js";
import { getLocaly } from "./tools/clientstorage.js";
import { getDatasFromInputs } from "./tools/forms.js";
import { dateFormat, replaceAll } from "../../tools/main";
import { loadMatomo } from "./tools/matomo.js";
import { checkSession, getTimeDifference } from "./tools/users.js";
// Dictionnaires :
const { noPreviousAnswer, previousAnswersLine, previousAnswersStats, previousAnswersTitle, responseSavedError, wantToSaveResponses } = require("../../lang/"+lang+"/answer");
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 explanationsTitle = document.getElementById("explanationsTitle");
const explanationsContent = document.getElementById("explanationsContent");
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
/*
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)
{
user=getLocaly("user", true);
updateAccountLink(user.status, configTemplate);// lien vers le compte adapté pour les utilisateurs connectés
checkPreviousResponses(user);
}
else */
loadMatomo();
}
catch(e)
{
console.error(e);
}
}
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)
{
try
{
e.preventDefault();
btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :)
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;
// 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;
for(let item in userResponses)
{
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
{
idQuestion=userResponses["question_id_response_"+idChoice];
answer.nbQuestions++;
if(goodResponse) // résultat de la question précédente
answer.nbCorrectAnswers++;
goodResponse=true;// réponse bonne jusqu'à la première erreur...
}
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
goodResponse=false;
}
else
{
if(userResponses["response_"+idChoice]==="on")// réponse cochée ne faisant pas partie des bonnes
{
goodResponse=false;
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 :
if(goodResponse)
answer.nbCorrectAnswers++;
// Affichage du résultat, suivant si l'utilisateur est connecté ou pas et son score :
let getOuput=checkAnswerOuput(answer);
/*
if(isConnected)
{
// Si l'utilisateur est connecté, on enregistre son résultat sur le serveur.
const xhrSaveAnswer = new XMLHttpRequest();
xhrSaveAnswer.open("POST", apiUrl+questionnaireRoutes+saveAnswersRoute);
xhrSaveAnswer.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let xhrResponse=JSON.parse(this.responseText);
if (this.status === 201 && (xhrResponse.message))
{
getOuput+="<br>"+xhrResponse.message.replace("#URL", configTemplate.userHomePage);
checkPreviousResponses(user);
}
else
getOuput+="<br>"+responseSavedError.replace("#URL", configTemplate.userHomePage);
// on redirige vers le 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");
}
}
xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token);
xhrSaveAnswer.setRequestHeader("Content-Type", "application/json");
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.
if(saveAnswer(answer))
{
getOuput+="<br><br>"+wantToSaveResponses;
addElement(divResponse, "p", getOuput, "", ["info"]);
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
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
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 (?)
explanations[i].style.display="block";
}
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(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
const checkPreviousResponses = (user) =>
{
const xhrPreviousRes = new XMLHttpRequest();
xhrPreviousRes.open("GET", apiUrl+questionnaireRoutes+getPreviousAnswers+user.id+"/"+document.getElementById("questionnaireId").value);
xhrPreviousRes.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200)
{
const nbResponses=response.length;
let previousAnswersContent="";
addElement(explanationsTitle, "span", previousAnswersTitle.replace("#NOM", user.name));
if(nbResponses!==0)
{
let totNbQuestions=0, totNbCorrectAnswers=0, totDuration=0, mapLineContent;
for(let i in response)
{
totNbQuestions+=response[i].nbQuestions;// ! on ne peut se baser sur la version actuelle du quiz, car le nombre de questions a pu évoluer.
totNbCorrectAnswers+=response[i].nbCorrectAnswers;
totDuration+=response[i].duration;
mapLineContent =
{
DATEANSWER : dateFormat(response[i].createdAt, lang),
NBCORRECTANSWERS : response[i].nbCorrectAnswers,
NBQUESTIONS : response[i].nbQuestions,
AVGDURATION : response[i].duration
};
previousAnswersContent+="<li>"+replaceAll(previousAnswersLine, mapLineContent)+"</li>";
}
mapLineContent =
{
AVGDURATION : Math.round(totDuration/nbResponses),
AVGCORRECTANSWERS : Math.round(totNbCorrectAnswers/totNbQuestions*100)
};
previousAnswersContent="<h5>"+replaceAll(previousAnswersStats, mapLineContent)+"</h5>"+previousAnswersContent;
addElement(explanationsContent, "ul", previousAnswersContent);
}
else
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);
}
}
}
xhrPreviousRes.setRequestHeader("Authorization", "Bearer "+user.token);
xhrPreviousRes.send();
}*/

219
front/src/quiz.js Normal file
View File

@ -0,0 +1,219 @@
// -- GESTION DU FORMULAIRE PERMETTANT D'AFFICHER ET RÉPONDRE À UN QUIZ
/// Il n'est pas nécessaire d'avoir une base de données locale active pour répondre à un quiz.
/// Mais si ce n'est pas déjà le cas et que cela semble techniquement possible, on propose au répondant de sauvegarder son résultat.
/// Dans ce but son résultat est stocké temporairement.
/// Si la base de donnée locale existe déjà, l'enregistrement de son résultat se fait automatiquement dans IndexedDB et ses éventuels précédents résultats sont affichés.
// Configurations générales provenant du backend :
import { availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0];
const configTemplate=require("../../views/"+theme+"/config/"+lang+".js");
// Fonctions :
import { checkAllPreviousResults, checkUserAnswers, getResultOutput, resultsOpenDb, saveNewQuiz, saveResult, saveResultTemp, showPreviousResults } from "./tools/answers.js";
import { saveIsReady } from "./tools/clientstorage.js";
import { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js";
import { isEmpty } from "../../tools/main";
import { loadMatomo } from "./tools/matomo.js";
// Textes :
const { wantToSaveResponses, wantToSeaPreviousResults }=require("../../lang/"+lang+"/answer");
const { localDBConnexionFail, serverError }=require("../../lang/"+lang+"/general");
// Informations du quiz, utile pour enregistrement dans base de donnée locale :
const myForm=document.getElementById("quiz");// quiz
const quizInfos=
{
url: window.location.pathname,
GroupId: document.getElementById("groupId").value,
QuestionnaireId: document.getElementById("questionnaireId").value,
title: myForm.dataset.title
};
// Autres éléments du DOM manipulés :
const propose2Save=document.getElementById("propose2Save");
const btnSave=document.getElementById("want2Save");
const btnSubmit=document.getElementById("checkResponses");
const divResponse=document.getElementById("response");
// L'url permet de savoir si nous sommes sur un quiz unique ou groupé :
let btnShow;
if(quizInfos.url.indexOf("/gp/") == -1)
btnShow=document.getElementById("showQuestionnaire"); // le quiz est affiché directement pour les groupes
let userDB, allPreviousAnswers=[];
const initialise = async () =>
{
try
{
if(btnShow)
btnShow.style.display="inline"; // Le bouton est caché si le JS inactif, car le JS est nécessaire pour la suite...
// On vérifie si la navigateur accepte l'utilisation d'IndexedDB :
const saveIsPossible=saveIsReady();
if(saveIsPossible)
{
// On essaye ensuite de se connecter à la base de données (ce qui va la créer si inexistante) :
userDB=await resultsOpenDb("myAnswers", 1);
if(userDB === undefined)
console.error(localDBConnexionFail);
else
{
// Vérifie si l'utilisateur a déjà sauvegardé au moins un résultat (et donc est ok pour les sauvegardes) :
allPreviousAnswers=await checkAllPreviousResults(userDB);
if(allPreviousAnswers.length !== 0)
await showPreviousResults(userDB, quizInfos.QuestionnaireId, quizInfos.GroupId);
}
}
loadMatomo();
}
catch(e)
{
console.error(e);
}
}
initialise();
helloDev();
// Affichage du quiz, quand il est caché par défaut (= l'internaute doit d'abord lire un article Wikipédia).
// Déclenche en même temps le chronomètre mesurant la durée de 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");
}
// L'utilisateur demande à voir le quiz :
let chronoBegin=0;
if(btnShow)
{
btnShow.addEventListener("click", function(e)
{
try
{
e.preventDefault();
showQuestionnaire();
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
});
// Un lien peut être passé pour voir directement le quiz :
if(location.hash !== "" && location.hash === "#questionnaire")
showQuestionnaire();
}
// Dans le cas d'un quiz groupé, le chrono est lancé dès l'affichage :
if(quizInfos.GroupId != "0")
{
chronoBegin=Date.now();
btnSubmit.style.display="block";
}
// Traitement de l'envoi de la réponse de l'utilisateur :
let answer;
myForm.addEventListener("submit", async function(e)
{
try
{
e.preventDefault();
btnSubmit.style.display="none"; // seulement une réponse à la fois, SVP :)
divResponse.innerHTML=""; // supprime les éventuels messages déjà affichés
answer=checkUserAnswers(myForm);
answer.duration=Math.round((Date.now()-chronoBegin)/1000);
answer.QuestionnaireId=quizInfos.QuestionnaireId;
answer.GroupId=quizInfos.GroupId;
// Affichage du résultat, suivant les cas :
let getOuput=getResultOutput(answer);
// S'il y a déjà une réponse dans la bd, c'est que l'utilisateur est ok pour les enregister.
if(allPreviousAnswers.length !==0)
{
const saveResponses=await saveResult(userDB, answer);
if(saveResponses)
await showPreviousResults(userDB, quizInfos.QuestionnaireId, quizInfos.GroupId);
getOuput+="<br><br>"+wantToSeaPreviousResults.replace("URL","#explanations");
// Nouveau quiz pour cette personne ?
await saveNewQuiz(userDB, quizInfos);
}
else
{
// S'il n'a pas encore de données, on stocke temporairement le résultat et propose de l'enregistrer :
if(saveResultTemp(answer) && !isEmpty(userDB))
{
getOuput+="<br><br>"+wantToSaveResponses;
propose2Save.style.display="block";
}
}
addElement(divResponse, "p", getOuput, "", ["info"]);
// On redirige vers le résultat :
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+"response");
}
else
window.location.assign(here+"#response");
// + Affichage des textes d'explication 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 (?)
explanations[i].style.display="block";
}
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
})
// L'utilisateur demande à sauvegarder son résultat :
btnSave.addEventListener("click", async function(e)
{
try
{
e.preventDefault();
if(!isEmpty(userDB) && !isEmpty(answer)) // On ne devrait pas me proposer d'enregistrer dans ce cas, mais...
{
const saveResponses=await saveResult(userDB, answer);
if(saveResponses)
{
// Nouvel enregistrement = actualisation nécessaire de la liste des résultats pour ce quiz :
await showPreviousResults(userDB, quizInfos.QuestionnaireId, quizInfos.GroupId);
// Nouveau quiz
await saveNewQuiz(userDB, quizInfos);
// Redirection vers la liste des résultats :
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+"explanations");
}
else
window.location.assign(here+"#explanations");
}
propose2Save.style.display="none";
}
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
});

View File

@ -1,46 +1,350 @@
const configFrontEnd = require("../config/general");
// Fonctions externes :
import { addElement } from "./dom.js";
import { dateFormat, isEmpty, replaceAll } from "../../../tools/main";
import { saveLocaly, getStore } from "./clientstorage.js";
import { getDatasFromInputs } from "./forms.js";
import { saveLocaly } from "./clientstorage.js";
import { isEmpty, replaceAll } from "../../../tools/main";
// Textes :
import { availableLangs } from "../../../config/instance.js";
const lang=availableLangs[0];
const { localDBNeedDatas, localDBNeedQuizId, noPreviousResults, previousResultsLine, previousResultsTitle, previousResultsStats, userAnswersFail, userAnswersMedium, userAnswersSuccess }=require("../../../lang/"+lang+"/answer");
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) =>
// Vérification des réponses de l'utilisateur au quiz
export const checkUserAnswers = (myForm) =>
{
if(!isEmpty(answer.duration) && !isEmpty(answer.nbCorrectAnswers) && !isEmpty(answer.nbQuestions) && (!isEmpty(answer.QuestionnaireId) || !isEmpty(answer.GroupId)))
// 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 avoir coché toutes les bonnes réponses (si QCM) à la question ET n'avoir coché aucune des mauvaises.
const userResponses=getDatasFromInputs(myForm);
let idChoice, idQuestion="", goodResponse=false, answer={ nbCorrectAnswers:0, nbQuestions:0 };
for(let item in userResponses)
{
saveLocaly("lastAnswer", answer);
return true;
if(item.startsWith("isCorrect_response_")) // = Nouvelle réponse possible.
{
idChoice=item.substring(item.lastIndexOf("_")+1);
// Si on change de 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
answer.nbCorrectAnswers++;
goodResponse=true;// Réponse bonne jusqu'à la première erreur...
}
if(userResponses[item] == "true")
{
document.getElementById("response_"+idChoice).parentNode.classList.add("isCorrect");
if(userResponses["response_"+idChoice] === undefined) // Une des bonnes réponses 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 :(
{
goodResponse=false;
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 :
if(goodResponse)
answer.nbCorrectAnswers++;
return answer;
}
// Retourne un texte suivant le nombre de bonnes réponses
export const getResultOutput = (result) =>
{
if(!isEmpty(result.duration) && !isEmpty(result.nbCorrectAnswers) && !isEmpty(result.nbQuestions))
{
const ratio=result.nbCorrectAnswers/result.nbQuestions;
const mapObj=
{
DURATION: result.duration,
NBCORRECTANSWERS: result.nbCorrectAnswers,
NBQUESTIONS: result.nbQuestions
}
let output="";
if(ratio < 0.4)
output=replaceAll(userAnswersFail, mapObj);
else if(ratio < 0.8)
output=replaceAll(userAnswersMedium, mapObj);
else
output=replaceAll(userAnswersSuccess, mapObj);
return output;
}
else
return "";
}
const checkIfResultIsComplete = (result) =>
{
if(!isEmpty(result.duration) && !isEmpty(result.nbCorrectAnswers) && !isEmpty(result.nbQuestions) && (!isEmpty(result.QuestionnaireId) || !isEmpty(result.GroupId)))
return true;
else
return false;
}
// Retourne un texte suivant le nombre de bonnes réponses
export const checkAnswerOuput = (answer) =>
// Enregistrement temporaire du dernier résultat à un quiz en attendant de savoir si l'utilisateur souhaite une sauvegarde durable :
export const saveResultTemp = (result) =>
{
if(!isEmpty(answer.duration) && !isEmpty(answer.nbCorrectAnswers) && !isEmpty(answer.nbQuestions))
if(checkIfResultIsComplete(result))
{
const ratio=answer.nbCorrectAnswers/answer.nbQuestions;
const mapObj=
{
DURATION: answer.duration,
NBCORRECTANSWERS: answer.nbCorrectAnswers,
NBQUESTIONS: answer.nbQuestions
}
let output="";
if(ratio < 0.4)
output=replaceAll(txt.checkResponsesOuputFail, mapObj);
else if(ratio < 0.8)
output=replaceAll(txt.checkResponsesOuputMedium, mapObj);
else
output=replaceAll(txt.checkResponsesOuputSuccess, mapObj);
if(output)
return output;
else
return "";
saveLocaly("lastResult", result); // écrasera l'éventuel résultat précédent.
return true;
}
else
return "";
{
console.error(localDBNeedDatas);
console.log(result);
return false;
}
}
// Se connecte à la base de données de sauvegarde des résultats
// Et la créé si c'est la première connexion (ou mise à jour de version)
export const resultsOpenDb = (dbName, dbVersion) =>
{
return new Promise( (resolve, reject) =>
{
let req=indexedDB.open(dbName, dbVersion);
req.onupgradeneeded= (e) =>
{
// Création du stockage des quizs auxquels l'utilisateur a déjà répondu :
let store=e.currentTarget.result.createObjectStore("userQuizs", { keyPath: "id", autoIncrement: true });
store.createIndex("url", "url", { unique: true });
store.createIndex("QuestionnaireId", "id", { unique: true }); // = simple quiz
store.createIndex("GroupId", "id", { unique: true }); // = quiz après lecture d'un ou +sieurs articles.
store.createIndex("title", "title", { unique: false });
// Création du stockage des résultats :
store=e.currentTarget.result.createObjectStore("userResults", { keyPath: "id", autoIncrement: true });
store.createIndex("QuestionnaireId", "QuestionnaireId", { unique: false });
store.createIndex("GroupId", "GroupId", { unique: false });
store.createIndex("duration", "duration", { unique: true });
store.createIndex("nbCorrectAnswers", "nbCorrectAnswers", { unique: false });
store.createIndex("nbQuestions", "nbQuestions", { unique: false });
store.createIndex("date", "date", { unique: false }); // bien que doublons peu probables...
/// Revoir comment gérér les mises à jour de version de la BD sans tout casser...
};
req.onsuccess= (e) =>
{
resolve(req.result); // On retourne la base de données
};
req.onerror= (e) =>
{
console.error(e); // dans cette fonction, peut simplement planter parce que navigation privée...
resolve(undefined);
};
})
}
// Fonction cherchant les éventuels résultats déjà enregistrés pour un quiz :
export const checkPreviousResultsForId= (db, QuestionnaireId=0, GroupId=0) =>
{
return new Promise( (resolve, reject) =>
{
const resultsStore=getStore(db, "userResults", "readonly");
let myIndex, getResults;
if(QuestionnaireId != 0)
{
myIndex=resultsStore.index("QuestionnaireId");
getResults=myIndex.openCursor(QuestionnaireId);
}
else if(GroupId != 0)
{
myIndex=resultsStore.index("GroupId");
getResults=myIndex.openCursor(GroupId);
}
else
reject(localDBNeedQuizId);
const answers=[];
getResults.onsuccess = (e) =>
{
const cursor=e.target.result;
if (cursor)
{
answers.push(cursor.value);
cursor.continue();
}
else // = on a parcouru toutes les données
resolve(answers);
};
getResults.onerror= (e) =>
{
console.error(e);
reject(e);
};
})
}
// Fonction affichant les précédents résultats connus pour le quiz encours :
export const showPreviousResults = async (userDB, QuestionnaireId, GroupId) =>
{
if(isEmpty(userDB) || (isEmpty(QuestionnaireId) && isEmpty(GroupId)))
return false;
// Recherche dans la base de données :
const previousResults=await checkPreviousResultsForId(userDB, QuestionnaireId, GroupId);
if(previousResults === undefined) // Peut être un tableau vide si ancun résultat enregistré, mais pas undefined.
console.error(localDBGetPreviousResultsFail);
else
{
const explanationsContent=document.getElementById("explanationsContent");
const explanationsTitle=document.getElementById("explanationsTitle");
// Les précédents résultats sont classés par ordre d'enregistrement et sont donc à inverser :
previousResults.reverse();
const nbPrevious=previousResults.length;
let previousResultsContent="";
addElement(explanationsTitle, "span", previousResultsTitle);
if(nbPrevious !== 0)
{
let totNbQuestions=0, totNbCorrectAnswers=0, totDuration=0, mapLineContent;
for(const i in previousResults)
{
totNbQuestions+=previousResults[i].nbQuestions; // ! on ne peut se baser sur la version actuelle du quiz, car le nombre de questions peut évolué.
totNbCorrectAnswers+=previousResults[i].nbCorrectAnswers;
totDuration+=previousResults[i].duration;
mapLineContent=
{
DATEANSWER: dateFormat(previousResults[i].date, lang),
NBCORRECTANSWERS: previousResults[i].nbCorrectAnswers,
NBQUESTIONS: previousResults[i].nbQuestions,
AVGDURATION: previousResults[i].duration
};
previousResultsContent+="<li>"+replaceAll(previousResultsLine, mapLineContent)+"</li>";
}
mapLineContent=
{
AVGDURATION: Math.round(totDuration/nbPrevious),
AVGCORRECTANSWERS: Math.round(totNbCorrectAnswers/totNbQuestions*100)
};
previousResultsContent="<h5>"+replaceAll(previousResultsStats, mapLineContent)+"</h5>"+previousResultsContent;
addElement(explanationsContent, "ul", previousResultsContent);
}
else
addElement(explanationsContent, "ul", noPreviousResults);
/// Revoir : ajouter un lien vers la page listant les quizs auxquels l'utilisateur a répondu
/// addElement(explanationsContent, "p", "<a href=\"/"+configTemplate.userHomePage+"\" class=\"button cardboard\">"+configTemplate.userHomePageTxt+"</a>", "", ["btn"], "", false);
}
}
// Recherche de tous les résultats déjà enregistrés
export const checkAllPreviousResults = (db) =>
{
return new Promise( (resolve, reject) =>
{
const resultsStore=getStore(db, "userResults", "readonly");
const getResults=resultsStore.getAll();
getResults.onsuccess = (e) =>
{
resolve(e.target.result);
};
getResults.onerror = (e) =>
{
console.error(e);
reject(e);
};
})
}
// Tentative d'enregistrement d'un résultat :
export const saveResult = (db, result) =>
{
return new Promise( (resolve, reject) =>
{
if(checkIfResultIsComplete(result))
{
const resultsStore=getStore(db, "userResults", "readwrite");
let req;
try
{
result.date=new Date();
req=resultsStore.add(result);
}
catch (e)
{
console.error(e);
reject(e);
}
req.onsuccess= (e) =>
{
resolve(true);
};
req.onerror= (e) =>
{
console.error(e);
reject(e);
};
}
else
{
console.error(localDBNeedDatas);
reject(localDBNeedDatas);
}
})
}
// Fonction retournant tous les quizs enregistrés pour cette personne
export const getAllQuizs = (db) =>
{
return new Promise( (resolve, reject) =>
{
const quizsStore=getStore(db, "userQuizs", "readonly");
const getquizs=quizsStore.getAll();
getquizs.onsuccess = (e) =>
{
resolve(e.target.result);
};
getquizs.onerror= (e) =>
{
console.error(e);
reject(e);
};
})
}
// Ajout le quiz à la base de donnée de l'utilisateur, s'il n'y existe pas déjà
export const saveNewQuiz = async (userDB, quizInfos) =>
{
const getUserQuizs=await getAllQuizs(userDB);
const checkQuizExist=getUserQuizs.find(quiz => quiz.url == quizInfos.url);
if(checkQuizExist === undefined)
await saveQuiz(userDB, quizInfos);
}
// Enregistre le quiz parmi ceux auxquels l'internaute a répondu, s'il n'y est pas déjà.
export const saveQuiz = (db, quiz) =>
{
return new Promise( (resolve, reject) =>
{
if(!isEmpty(quiz.url) && !isEmpty(quiz.title) && (!isEmpty(quiz.QuestionnaireId) || !isEmpty(quiz.GroupId)))
{
const quizsStore=getStore(db, "userQuizs", "readwrite");
let req;
try
{
req=quizsStore.add(quiz);
}
catch (e)
{
console.error(e);
reject(e);
}
req.onsuccess= (e) =>
{
resolve(true);
};
req.onerror= (e) =>
{
console.error(e);
reject(e);
};
}
else
{
console.error(localDBNeedDatas);
reject(localDBNeedDatas);
}
})
}

View File

@ -1,6 +1,6 @@
// FONCTIONS UTILES AU STOCKAGE LOCAL (SESSION, COOKIES, INDEXDB, ETC.)
// Revenir pour gérer le cas où local.storage n'est pas connu pour utiliser cookie
// Éviter sessionStorage dont le contenu n'est pas gardé d'un onglet à l'autre : https://developer.mozilla.org/fr/docs/Web/API/Window/sessionStorage
export const saveLocaly = (name, data) =>
{
localStorage.setItem(name, JSON.stringify(data));
@ -17,4 +17,18 @@ export const getLocaly = (name, json=false) =>
export const removeLocaly = (name) =>
{
localStorage.removeItem(name);
}
}
export const saveIsReady = () =>
{
if (!window.indexedDB)
return false;
else
return true;
}
export const getStore = (db, store_name, mode) =>
{
const tx=db.transaction(store_name, mode);
return tx.objectStore(store_name);
}

View File

@ -53,6 +53,9 @@ export const checkAnswerDatas = (datas) =>
return datas;
}
/// La suite est toujours utile pour l'admin.
// Cette fonction teste la connexion de l'utilisateur d'une page
// On peut fournis une liste de statuts acceptés (si vide = tous), ainsi qu'une url de redirection si non connecté, un message d'erreur à afficher sur la page de destination et l'url sur laquelle revenir une fois connecté
export const checkSession = async (status=[], urlRedirection, message, urlWanted) =>

View File

@ -9,7 +9,6 @@ module.exports =
connection: "./src/connection.js",
deconnection: "./src/deconnection.js",
deleteValidation: "./src/deleteValidation.js",
group: "./src/group.js",
groupElement: "./src/groupElement.js",
homeManager: "./src/homeManager.js",
homeUser: "./src/homeUser.js",
@ -21,7 +20,7 @@ module.exports =
newLoginValidation: "./src/newLoginValidation.js",
paymentPage: "./src/paymentPage.js",
polyfill: "babel-polyfill",
questionnaire: "./src/questionnaire.js",
quiz: "./src/quiz.js",
subscribe: "./src/subscribe.js",
subscribeValidation: "./src/subscribeValidation.js",
unsubscribe: "./src/unsubscribe.js"

View File

@ -1,8 +1,8 @@
module.exports =
{
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 !",
localDBGetPreviousResultsFail: "Bug de récupération des résultats précédents.",
localDBNeedDatas: "Il manque des données nécessaires à l'enregistrement.",
localDBNeedQuizId: "Aucun identifiant n'a été fourni pour le quiz.",
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.",
needIntegerNumberUserResponses : "Le nombre de questions auxquelles l'utilisateur a répondu doit être un nombre entier.",
@ -13,12 +13,16 @@ module.exports =
needMaxNumberCorrectResponses : "Le nombre de réponses correctes ne peut être supérieur au nombre de questions.",
needMinNumberCorrectResponses : "Le nombre de réponses correctes ne peut être négatif.",
needMinNumberSecondesResponse : "La durée de la réponse ne peut être négative.",
noPreviousAnswer: "On dirait que c'est la première fois que vous répondez à ce quiz. Bonne lecture !",
previousAnswersLine: "Le DATEANSWER, vous avez répondu correctement à NBCORRECTANSWERS questions sur NBQUESTIONS en AVGDURATION secondes.",
previousAnswersStats: "En moyenne, vous avez répondu à ce quiz en AVGDURATION secondes, en ayant <b>AVGCORRECTANSWERS % de bonnes réponses</b>.",
previousAnswersTitle: "Bonjour #NOM, voici vos précédents résultats à ce quiz",
noPreviousResults: "On dirait que c'est la première fois que vous répondez à ce quiz. Bonne lecture !",
previousResultsLine: "Le DATEANSWER, vous avez répondu correctement à NBCORRECTANSWERS questions sur NBQUESTIONS en AVGDURATION secondes.",
previousResultsStats: "En moyenne, vous avez répondu à ce quiz en AVGDURATION secondes, en ayant <b>AVGCORRECTANSWERS % de bonnes réponses</b>.",
previousResultsTitle: "Voici vos précédents résultats à ce quiz",
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é <b>NBANSWERS réponseS1 à NBQUESTIONNAIRES quizS2 différentS3</b> sur les NBTOTQUESTIONNAIRES proposéS4 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 <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.",
userAnswersFail : "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 !",
userAnswersMedium : "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 !",
userAnswersSuccess : "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 !",
wantToSaveResponses: "Si vous le souhaitez, vous pouvez <b>sauvegarder vos résultats</b> (il n'est pas nécessaire de créer un compte) :",
wantToSeaPreviousResults: "<a href='URL'>Cliquez ici</a> pour tous vos résultats et vos statistiques de réussite à ce quiz."
};

View File

@ -16,6 +16,7 @@ module.exports =
failAuthHeader : "Absence de header Authorization.",
failAuthId : "Identifiant non valide : ",
failAuthToken : "Token invalide ou utilisateur non trouvé.",
localDBConnexionFail : "La connexion à la base de donnée locale est impossible.",
neededParams : "Des paramètres nécessaires manquants sont manquants.",
nextPage : "Page suivante",
notAllowed : "Vous n'avez pas les droits nécessaires pour cette action.",

View File

@ -2,6 +2,7 @@ module.exports =
{
btnProposeConnection: "Je me connecte.",// déplacé dans general.js
btnProposeSubscribe: "Je crée mon compte.",//idem
btnProposeSave: "Enregistrer mes réponses.",//idem
btnSendResponse: "Testez vos réponses.",
btnShareQuizTxt: "Partager via ",
btnShareQuizMailBody: "Bonjour,%0A%0AVoici%20un%20lien%20internet%20qui%20devrait%20t'intéresser :%0A",

View File

@ -1,7 +1,7 @@
extends layout.pug
block append scripts
script(src="/JS/polyfill.app.js" defer)
script(src="/JS/group.app.js" defer)
script(src="/JS/quiz.app.js" defer)
block content
-
if(group.Questionnaires.length !==0)
@ -41,14 +41,12 @@ block content
noscript
div
strong #{configTpl.noJSNotification}
form(id="group" method="POST")
form(id="quiz" method="POST" data-title=group.Group.title)
h2 #{group.Group.title}
div#response
// div(class="subscribeBtns")
// p
// a(class="button cardboard" href=configTpl.subscribePage) #{txtGeneral.btnProposeSubscribe}
// p
// a(class="button cardboard" href=configTpl.connectionPage) #{txtGeneral.btnProposeConnection}
div(id="propose2Save" class="needJS")
span(class="input_wrapper")
input(class="button cardboard" id="want2Save" type="button" value=txtQuestionnaire.btnProposeSave)
for questionnaire in group.Questionnaires
for question in questionnaire.Questions
p(id="question_"+question.Question.id) #{question.Question.text}
@ -69,6 +67,7 @@ block content
em #{response.text}
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="questionnaireId" id="questionnaireId" value="0" type="hidden")
input(name="groupId" id="groupId" value=group.Group.id type="hidden")
p
span(class="input_wrapper")

View File

@ -1,7 +1,7 @@
extends layout.pug
block append scripts
script(src="/JS/polyfill.app.js" defer)
script(src="/JS/questionnaire.app.js" defer)
script(src="/JS/quiz.app.js" defer)
block content
div(id="tags" class="cardboard")
ul
@ -52,9 +52,12 @@ block content
noscript
div
strong #{configTpl.noJSNotification}
form(id="questionnaire" method="POST" class="needJS")
form(id="quiz" method="POST" class="needJS" data-title=questionnaire.Questionnaire.title)
h2 #{questionnaire.Questionnaire.title}
div#response
div(id="propose2Save" class="needJS")
span(class="input_wrapper")
input(class="button cardboard" id="want2Save" type="button" value=txtQuestionnaire.btnProposeSave)
for question in questionnaire.Questions
p(id="question_"+question.Question.id) #{question.Question.text}
if(question.Question.explanation)
@ -73,9 +76,10 @@ block content
input(type="hidden" name="isCorrect_response_"+reponse.id id="isCorrect_response_"+reponse.id value=""+reponse.isCorrect)
input(type="hidden" name="question_id_response_"+reponse.id id="question_id_response_"+reponse.id value=question.Question.id)
input(name="questionnaireId" id="questionnaireId" value=questionnaire.Questionnaire.id type="hidden")
input(name="groupId" id="groupId" value="0" type="hidden")
p
span(class="input_wrapper")
input(id="checkResponses" type="submit" value=txtQuestionnaire.btnSendResponse class="cardboard" title=txtQuestionnaire.btnSendResponse)
input(id="checkResponses" type="submit" value=txtQuestionnaire.btnSendResponse class="cardboard")
div#licence
p !{configTpl.licenceTxt}
div#zerozozio