From 8eef92c9aa925e32209244de0d3dbd0524cb8bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20PENHO=C3=8BT?= Date: Mon, 21 Mar 2022 17:08:58 +0100 Subject: [PATCH] =?UTF-8?q?R=C3=A9=C3=A9criture=20en=20style=20objet=20du?= =?UTF-8?q?=20script=20g=C3=A8rant=20les=20r=C3=A9sultats=20de=20l'utilisa?= =?UTF-8?q?teur=20aux=20quizs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/public/src/quiz.js | 102 +++-- front/public/src/tools/answers.js | 350 ---------------- front/public/src/tools/userQuizsResults.js | 449 +++++++++++++++++++++ 3 files changed, 494 insertions(+), 407 deletions(-) delete mode 100644 front/public/src/tools/answers.js create mode 100644 front/public/src/tools/userQuizsResults.js diff --git a/front/public/src/quiz.js b/front/public/src/quiz.js index 71c0d11..7277170 100644 --- a/front/public/src/quiz.js +++ b/front/public/src/quiz.js @@ -2,27 +2,28 @@ /// 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, il est proposé au répondant de sauvegarder durablement son résultat. -/// Dans ce but son résultat est d'abord stocké temporairement. +/// Dans ce but, son résultat est d'abord 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 } from "../../../config/instance.js"; const lang=availableLangs[0]; +// Textes : +const { wantToSaveResponses, wantToSeaPreviousResults }=require("../../../lang/"+lang+"/answer"); +const { serverError }=require("../../../lang/"+lang+"/general"); + // 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"); +// Classe s'occupant du stockage des résultats aux quizs : +import { userQuizsResults} from "./tools/userQuizsResults"; -// Informations du quiz, utile pour enregistrement dans base de donnée locale : -const myForm=document.getElementById("quiz");// quiz +// Informations du quiz en cours, utiles pour les enregistrements dans la base de donnée locale : +const myForm=document.getElementById("quiz"); const quizInfos= { url: window.location.pathname, @@ -31,41 +32,29 @@ const quizInfos= title: myForm.dataset.title }; -// Autres éléments du DOM manipulés : -const propose2Save=document.getElementById("propose2Save"); +// Éléments du DOM manipulés : const btnSave=document.getElementById("want2Save"); const btnSubmit=document.getElementById("checkResponses"); -const divResponse=document.getElementById("response"); +const propose2Save=document.getElementById("propose2Save"); +const responseTxt=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, ce qui déclenche le compteur - -let userDB, allPreviousAnswers=[]; +let userDB, allPreviousAnswers=[], btnShow, myResults; 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) + // Pour les quizs uniques (non groupés), un bouton permet à l'utilisateur d'afficher le quiz après avoir lu le contenu Wikipédia proposé, ce qui déclenche le chrono : + if(quizInfos.url.indexOf("/gp/") == -1) { - // 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); - // Si oui, on affiche ceux enregistrés pour ce quiz : - if(allPreviousAnswers.length !== 0) - await showPreviousResults(userDB, quizInfos.QuestionnaireId, quizInfos.GroupId); - } + btnShow=document.getElementById("showQuestionnaire"); + btnShow.style.display="inline"; // Le bouton est caché si le JS est inactif, car le JS est nécessaire pour la suite... } + // Instanciation de la classe s'occupant du stockage des résultats aux quizs : + myResults=await userQuizsResults.initialise("myResults", 1); + // Si la base de données est fonctionnel et que des résultats sont déjà enregistrés, on affiche ceux pour le quiz en cours : + if(myResults.allResults.length !== 0) + await myResults.showPreviousResultsForId(quizInfos.QuestionnaireId, quizInfos.GroupId); + // Statistiques : loadMatomo(); } catch(e) @@ -76,8 +65,7 @@ const initialise = async () => 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. +// Fonction affichant le quiz, quand il est caché par défaut+ déclenchant le chronomètre mesurant la durée de réponse aux questions. const showQuestionnaire = () => { chronoBegin=Date.now(); @@ -93,10 +81,9 @@ const showQuestionnaire = () => window.location.assign(here+"#questionnaire"); } -let chronoBegin=0; +let chronoBegin; if(btnShow) { - // L'utilisateur demande à voir le quiz : btnShow.addEventListener("click", function(e) { try @@ -106,7 +93,7 @@ if(btnShow) } catch(e) { - addElement(divResponse, "p", serverError, "", ["error"]); + addElement(responseTxt, "p", serverError, "", ["error"]); console.error(e); } }); @@ -130,34 +117,34 @@ myForm.addEventListener("submit", async function(e) { 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); + responseTxt.innerHTML=""; // supprime les éventuels messages déjà affichés + answer=userQuizsResults.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); + // Enregistrement et affichage du résultat, suivant les cas : + let getOuput=userQuizsResults.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) + if(myResults.allResults.length !== 0) { - const saveResponses=await saveResult(userDB, answer); + const saveResponses=await myResults.addResult(answer); if(saveResponses) - await showPreviousResults(userDB, quizInfos.QuestionnaireId, quizInfos.GroupId); + await myResults.showPreviousResultsForId(quizInfos.QuestionnaireId, quizInfos.GroupId); getOuput+="

"+wantToSeaPreviousResults.replace("URL","#explanations"); // Nouveau quiz pour cette personne ? - await saveNewQuiz(userDB, quizInfos); + await myResults.saveNewQuiz(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)) + if(myResults.saveResultTemp(answer) && myResults.dbIsReady) { getOuput+="

"+wantToSaveResponses; propose2Save.style.display="block"; } } - addElement(divResponse, "p", getOuput, "", ["info"]); + addElement(responseTxt, "p", getOuput, "", ["info"]); // On redirige vers le résultat : const here=window.location; if(window.location.hash !== "") @@ -167,6 +154,7 @@ myForm.addEventListener("submit", async function(e) } else window.location.assign(here+"#response"); + // + Affichage des textes d'explication pour chaque question const explanations=document.querySelectorAll(".help"); for(let i in explanations) @@ -177,10 +165,10 @@ myForm.addEventListener("submit", async function(e) } catch(e) { - addElement(divResponse, "p", serverError, "", ["error"]); + addElement(responseTxt, "p", serverError, "", ["error"]); console.error(e); } -}) +}); // L'utilisateur demande à sauvegarder son résultat : btnSave.addEventListener("click", async function(e) @@ -188,15 +176,15 @@ 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... + if(!isEmpty(myResults.dbIsReady) && !isEmpty(answer)) // On ne devrait pas m'avoir proposé d'enregistrer dans ce cas, mais... { - const saveResponses=await saveResult(userDB, answer); + const saveResponses=await myResults.addResult(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); + await myResults.showPreviousResultsForId(quizInfos.QuestionnaireId, quizInfos.GroupId); + // Nouveau quiz (ce qui doit être le cas, mais...) : + await myResults.saveNewQuiz(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 !== "") @@ -212,7 +200,7 @@ btnSave.addEventListener("click", async function(e) } catch(e) { - addElement(divResponse, "p", serverError, "", ["error"]); + addElement(responseTxt, "p", serverError, "", ["error"]); console.error(e); } }); \ No newline at end of file diff --git a/front/public/src/tools/answers.js b/front/public/src/tools/answers.js deleted file mode 100644 index a112245..0000000 --- a/front/public/src/tools/answers.js +++ /dev/null @@ -1,350 +0,0 @@ -// 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"; - -// 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"); - -// Vérification des réponses de l'utilisateur au quiz -export const checkUserAnswers = (myForm) => -{ - // 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) - { - 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; -} - -// Enregistrement temporaire du dernier résultat à un quiz en attendant de savoir si l'utilisateur souhaite une sauvegarde durable : -export const saveResultTemp = (result) => -{ - if(checkIfResultIsComplete(result)) - { - saveLocaly("lastResult", result); // écrasera l'éventuel résultat précédent. - return true; - } - else - { - 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+="
  • "+replaceAll(previousResultsLine, mapLineContent)+"
  • "; - } - mapLineContent= - { - AVGDURATION: Math.round(totDuration/nbPrevious), - AVGCORRECTANSWERS: Math.round(totNbCorrectAnswers/totNbQuestions*100) - }; - previousResultsContent="
    "+replaceAll(previousResultsStats, mapLineContent)+"
    "+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", ""+configTemplate.userHomePageTxt+"", "", ["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); - }; - }) -} - -// Ajoute le quiz à la base de donnée de l'utilisateur, s'il n'y est 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); - } - }) -} \ No newline at end of file diff --git a/front/public/src/tools/userQuizsResults.js b/front/public/src/tools/userQuizsResults.js new file mode 100644 index 0000000..e0dff24 --- /dev/null +++ b/front/public/src/tools/userQuizsResults.js @@ -0,0 +1,449 @@ +// Classe gérant le stockage des résultats aux quizs de l'utilisateur. +// Dépendance assumée à IndexedDB... + +// 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 { saveIsReady } from "./clientstorage.js"; + +// 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"); + +export class userQuizsResults +{ + dbName; + dbVersion; + #db; + #dbIsReady; + #allQuizs; + #allResults; + + set db(value) + { + this.#db=value; + } + + set dbIsReady(value) + { + this.#dbIsReady=value; + } + + set allQuizs(array) + { + this.#allQuizs=array; + } + + set allResults(array) + { + this.#allResults=array; + } + + get db() + { + return this.#db; + } + + get dbIsReady() + { + return this.#dbIsReady; + } + + get allQuizs() + { + return this.#allQuizs; + } + + get allResults() + { + return this.#allResults; + } + + // Vérification des réponses de l'utilisateur au quiz + static checkUserAnswers(myForm) + { + // 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, result={ nbCorrectAnswers:0, nbQuestions:0 }; + for(let item in userResponses) + { + 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]; + result.nbQuestions++; + if(goodResponse) // Résultat de la question précédente + result.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) + result.nbCorrectAnswers++; + return result; + } + + // Retourne un texte suivant le nombre de bonnes réponses + static 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 ""; + } + + // Ne pouvant déclarer constructor() comme async, on passe par une méthode dédiée : + static async initialise(dbName, dbVersion) + { + const myInstance=new userQuizsResults(); + myInstance.dbName=dbName; + myInstance.dbVersion=dbVersion; + myInstance.dbIsReady=saveIsReady(); + if(myInstance.dbIsReady === true) + { + // On essaye ensuite de se connecter à la base de données (et de la créer, si elle est inexistante) : + await myInstance.getOpenDb(myInstance.dbName, myInstance.dbVersion); + if(myInstance.db === undefined) + { + console.error(localDBConnexionFail); // information mais pas d'exception pour éviter blocage, car on peut répondre aux quizs sans sauvegarder les résultats + myInstance.dbIsReady=false; + } + else + { + // Récupère la liste des quizs auxquels, cet utilisateur a déjà répondu : + await myInstance.getAllQuizs(); + // + L'ensemble de ses résultats : + await myInstance.getAllResults(); + } + } + return myInstance; + } + + // Retourne la base de données de sauvegarde des résultats + la créée ou met à jour si besoin + getOpenDb () + { + return new Promise( (resolve, reject) => + { + let req=indexedDB.open(this.dbName,this.dbVersion); + req.onupgradeneeded= (e) => + { + if (e.oldVersion < 1) // Voir : https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/onupgradeneeded + { + // Stockage des quizs auxquels l'utilisateur a répondu : + let store=e.currentTarget.result.createObjectStore("userQuizs", { keyPath: "id", autoIncrement: true }); + store.createIndex("url", "url", { unique: true }); + store.createIndex("QuestionnaireId", "id", { unique: false }); // = simple quiz, sinon "0" donc doublons possibles + store.createIndex("GroupId", "id", { unique: false }); // = quiz après lecture d'un ou +sieurs articles, sinon "0" + store.createIndex("title", "title", { unique: false }); + // 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: false }); + store.createIndex("nbCorrectAnswers", "nbCorrectAnswers", { unique: false }); + store.createIndex("nbQuestions", "nbQuestions", { unique: false }); + store.createIndex("date", "date", { unique: false }); // bien que doublons peu probables... + } + }; + req.onsuccess= (e) => + { + this.db=req.result; + resolve(this.db); + }; + req.onerror= (e) => + { + console.error(e); + resolve(undefined); + }; + }) + } + + // Fonction retournant tous les quizs enregistrés pour cette personne + async getAllQuizs () + { + await this.getOpenDb(); + return new Promise( (resolve, reject) => + { + const quizsStore=getStore(this.db, "userQuizs", "readonly"); + const getquizs=quizsStore.getAll(); + getquizs.onsuccess = (e) => + { + this.db.close(); + resolve(e.target.result); + }; + getquizs.onerror= (e) => + { + this.db.close(); + reject(e); + }; + }) + } + + // Retourne tous les résultats déjà enregistrés + async getAllResults() + { + await this.getOpenDb(); + return new Promise( (resolve, reject) => + { + const resultsStore=getStore(this.db, "userResults", "readonly"); + const getResults=resultsStore.getAll(); + getResults.onsuccess = (e) => + { + this.allResults=e.target.result; + this.db.close(); + resolve(this.allResults); + }; + getResults.onerror = (e) => + { + this.db.close(); + reject(e); + }; + }) + } + + // Retourne les résultats déjà enregistrés pour un quiz donné : + async checkPreviousResultsForId(QuestionnaireId=0, GroupId=0) + { + await this.getOpenDb(); + return new Promise( (resolve, reject) => + { + const resultsStore=getStore(this.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(new Error(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 + { + this.db.close(); + resolve(answers); + } + }; + getResults.onerror= (e) => + { + this.db.close(); + reject(e); + }; + }) + } + + // Contrôle que les données fournies pour un résultat sont complètes + checkIfResultIsComplete(result) + { + if(!isEmpty(result.duration) && !isEmpty(result.nbCorrectAnswers) && !isEmpty(result.nbQuestions) && (!isEmpty(result.QuestionnaireId) || !isEmpty(result.GroupId))) + return true; + else + return false; + } + + // Enregistrement temporaire du dernier résultat à un quiz. + // En attendant de savoir si l'utilisateur souhaite une sauvegarde durable. + saveResultTemp (result) + { + if(this.checkIfResultIsComplete(result)) + { + saveLocaly("lastResult", result); // écrasera l'éventuel résultat précédent. + return true; + } + else + { + throw new Error(localDBNeedDatas); + return false; + } + } + + // Enregistrement durable d'un résultat : + async addResult(result) + { + await this.getOpenDb(); + return new Promise( (resolve, reject) => + { + if(this.checkIfResultIsComplete(result)) + { + const resultsStore=getStore(this.db, "userResults", "readwrite"); + let req; + try + { + result.date=new Date(); + req=resultsStore.add(result); + } + catch (e) + { + console.error(e); + this.db.close(); + reject(e); + } + req.onsuccess= (e) => + { + this.db.close(); + resolve(true); + }; + req.onerror= (e) => + { + this.db.close(); + reject(e); + }; + } + else + { + this.db.close(); + reject(new Error(localDBNeedDatas)); + } + }) + } + + // Enregistre le quiz, s'il n'existe pas déjà : + async saveNewQuiz(quizInfos) + { + const getUserQuizs=await this.getAllQuizs(); + const checkQuizExist=getUserQuizs.find(quiz => quiz.url == quizInfos.url); + let result; + if(checkQuizExist === undefined) + result=await this.saveQuiz(quizInfos); + return result; + } + + async saveQuiz(quiz) + { + await this.getOpenDb(); + return new Promise( (resolve, reject) => + { + if(!isEmpty(quiz.url) && !isEmpty(quiz.title) && (!isEmpty(quiz.QuestionnaireId) || !isEmpty(quiz.GroupId))) + { + const quizsStore=getStore(this.db, "userQuizs", "readwrite"); + let req; + try + { + req=quizsStore.add(quiz); + } + catch (e) + { + this.db.close(); + reject(e); + } + req.onsuccess= (e) => + { + this.db.close(); + resolve(true); + }; + req.onerror= (e) => + { + this.db.close(); + reject(e); + }; + } + else + { + this.db.close(); + reject(new Error(localDBNeedDatas)); + } + }) + } + + // Fonction affichant les précédents résultats connus pour le quiz encours : + async showPreviousResultsForId(QuestionnaireId, GroupId, txtContentId="explanationsContent", txtTitleId="explanationsTitle") + { + if((isEmpty(QuestionnaireId) && isEmpty(GroupId))) + throw new Error(localDBNeedDatas); + + // Recherche dans la base de données : + const previousResults=await this.checkPreviousResultsForId(QuestionnaireId, GroupId); + if(previousResults === undefined) // Peut être un tableau vide si ancun résultat enregistré, mais pas undefined. + throw new Error(localDBGetPreviousResultsFail); + else + { + const explanationsContent=document.getElementById(txtContentId); + const explanationsTitle=document.getElementById(txtTitleId); + // 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; // ! le nombre de questions peut évoluer, si le quiz est actualisé. + 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+="
  • "+replaceAll(previousResultsLine, mapLineContent)+"
  • "; + } + mapLineContent= + { + AVGDURATION: Math.round(totDuration/nbPrevious), + AVGCORRECTANSWERS: Math.round(totNbCorrectAnswers/totNbQuestions*100) + }; + previousResultsContent="
    "+replaceAll(previousResultsStats, mapLineContent)+"
    "+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", ""+configTemplate.userHomePageTxt+"", "", ["btn"], "", false); + } + } +} \ No newline at end of file