Réécriture en style objet du script gèrant les résultats de l'utilisateur aux quizs.
This commit is contained in:
parent
1730fa08df
commit
8eef92c9aa
@ -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+="<br><br>"+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+="<br><br>"+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);
|
||||
}
|
||||
});
|
@ -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+="<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);
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
})
|
||||
}
|
449
front/public/src/tools/userQuizsResults.js
Normal file
449
front/public/src/tools/userQuizsResults.js
Normal file
@ -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+="<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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user