WikiLerni/front/public/src/tools/userQuizsResults.js

544 lines
20 KiB
JavaScript

// 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...
}
if (e.oldVersion < 2)
{
const quizsStore=req.transaction.objectStore("userQuizs");
quizsStore.createIndex("keywords", "keywords", { unique: false });
}
};
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));
}
})
}
// Importation en masse des résultats :
async saveAllResults(results)
{
await this.getOpenDb();
return new Promise( (resolve, reject) =>
{
const resultsStore=getStore(this.db, "userResults", "readwrite");
// Au commence par vider l'existant :
resultsStore.clear();
// Puis on enregistre les données fournies :
for(const result of results)
{
if(this.checkIfResultIsComplete(result))
{
let req;
req=resultsStore.add(result);
req.onerror= (e) =>
{
this.db.close();
reject(e);
};
}
}
this.db.close();
// On injecte les donnés qui ont été acceptées dans l'instance :
this.getAllResults().then( () =>
{
resolve(true);
});
})
}
// 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));
}
})
}
// Importation en masse des quizs :
async saveAllQuizs(quizs)
{
await this.getOpenDb();
return new Promise( (resolve, reject) =>
{
const quizsStore=getStore(this.db, "userQuizs", "readwrite");
// Au commence par vider l'existant :
quizsStore.clear();
// Puis on enregistre les données fournies :
for(const quiz of quizs)
{
if(!isEmpty(quiz.url) && !isEmpty(quiz.title) && (!isEmpty(quiz.QuestionnaireId) || !isEmpty(quiz.GroupId)))
{
let req;
req=quizsStore.add(quiz);
req.onerror= (e) =>
{
this.db.close();
reject(e);
};
}
}
this.db.close();
// On injecte les donnés qui ont été acceptées dans l'instance :
this.getAllQuizs().then( () =>
{
resolve(true);
});
})
}
// 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);
}
}
// 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=Object.values(this.allQuizs);
myQuizs.reverse();
let html="";
for(const quiz of myQuizs)
html+=`<li><a href="${quiz.url}#explanations">${quiz.title}</a></li>`;
if(html !== "")
addElement(listElt, "ul", html);
else
addElement(listElt, "p", noPreviousResultsAtAll);
}
// Propose à l'utilisateur de télécharger ses données dans un fichier JSON
saveMyQuizs(eltId="quizsSave", saveLinkTxt="Save your datas.", cssClass=[], linkId="")
{
const datas2Save=[JSON.stringify({ quizs: this.allQuizs, results: this.allResults })];
const datasFile=new File(datas2Save, "myDatas.json", { type: "application/json", });
const datasFileUrl=URL.createObjectURL(datasFile);
const domElt=document.getElementById(eltId);
addElement(domElt, "a", saveLinkTxt, linkId, cssClass, { href:datasFileUrl, download:"myDatas.json"});
}
}