// 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, noPreviousResultsAtAll, previousResultsLine, previousResultsTitle, previousResultsStats, userAnswersFail, userAnswersMedium, userAnswersSuccess }=require("../../../../lang/"+lang+"/answer"); const { localDBConnexionFail }=require("../../../../lang/"+lang+"/general"); 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.allQuizs=e.target.result; 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); } } // Retourne une liste HTML des précédents quizs avec lien vers leur page showMyQuizs(listId="quizsList") { const listElt=document.getElementById(listId); // On affiche d'abord les quizs les plus récents : const myQuizs=this.allQuizs.reverse(); let html=""; for(const quiz of myQuizs) html+=`
  • ${quiz.title}
  • `; if(html !== "") addElement(listElt, "ul", html+"ici"); else addElement(listElt, "p", noPreviousResultsAtAll); } }