Browse Source

Ajout choix groupe pour les questionnaire et prise en compte création fichiers HTML

master
Fabrice PENHOËT 11 months ago
parent
commit
e092c31e43
  1. 5
      config/questionnaires.js
  2. 13
      controllers/choice.js
  3. 65
      controllers/group.js
  4. 4
      controllers/question.js
  5. 205
      controllers/questionnaire.js
  6. 1
      front/public/gestion-groups.html
  7. 319
      front/src/group.js
  8. 181
      front/src/groupElement.js
  9. 15
      front/src/manageGroups.js
  10. 100
      front/src/manageQuestionnaires.js
  11. 1
      front/webpack.config.js
  12. 6
      lang/fr/group.js
  13. 3
      lang/fr/questionnaire.js
  14. 6
      models/Questionnaire.js
  15. 1
      routes/group.js
  16. 4
      views/wikilerni/config/fr.js
  17. 61
      views/wikilerni/quiz-element.pug
  18. 68
      views/wikilerni/quiz-group.pug
  19. 2
      views/wikilerni/quiz.pug

5
config/questionnaires.js

@ -15,6 +15,7 @@ module.exports =
// -- groupes :
groupRoutes: "/group",
getGroupRoute: "/get/",
previewGroupRoutes: "/preview",
searchGroupsRoute : "/search",
// -- questions & choices :
questionsRoute: "/question/",
@ -56,11 +57,11 @@ module.exports =
dirCacheTags : "datas/questionnaires/tags",
dirCacheUsersQuestionnaires : "datas/users/questionnaires",
// Emplacement des fichiers HTML générés :
dirHTMLGroups : "front/public/quizs/gp",
dirHTMLGroups : "front/public/quiz/gp",
dirHTMLQuestionnaires : "front/public/quiz",
dirHTMLTags : "front/public/quizs",
// Idem mais pour urls :
dirWebGroups : "quizs/gp",
dirWebGroups : "quiz/gp",
dirWebQuestionnaires : "quiz",
dirWebTags : "quizs/",
// limite des résultat du moteur de recherche, quand demande de résultats au hasard :

13
controllers/choice.js

@ -9,7 +9,8 @@ const questionnaireCtrl = require("./questionnaire");
const txt = require("../lang/"+config.adminLang+"/choice");
const txtQuestion = require("../lang/"+config.adminLang+"/question");
// J'arrive aux deux contrôleurs suivants après être passé par les contrôleurs de "question" qui leur passe la main via next()
// J'arrive aux deux contrôleurs suivants après être les contrôleurs de "question" qui leur passe la main via next()
exports.create = async (req, res, next) =>
{
try
@ -51,8 +52,8 @@ exports.create = async (req, res, next) =>
for(let i in choices)
await db["Choice"].create(choices[i], { fields: ["text", "isCorrect", "QuestionId"] });
question=await questionCtrl.creaQuestionJson(req.body.QuestionId);// besoin de ces données pour la réponse
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId,true);// pour le cache + HTML
questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId,true);// nécessaire réaffichage après ajout
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId, true);// pour le cache + HTML
questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId, true);// nécessaire au réaffichage après ajout
res.status(201).json({ message: txtQuestion.addOkMessage , questionnaire: questionnaire });
}
next();
@ -121,9 +122,9 @@ exports.modify = async (req, res, next) =>
await db["Choice"].update(choicesUpdated[i], { where: { id: choicesUpdated[i].id } , fields: ["text", "isCorrect"], limit:1 });
for(let i in choicesAdded)
await db["Choice"].create(choicesAdded[i], { fields: ["text", "isCorrect", "QuestionId"] });
question=await questionCtrl.creaQuestionJson(req.params.id);// attendre pour pouvoir tout retourner
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId,true);// pour le cache + HTML
questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId, true);// nécessaire réaffichage après enregistrement
question=await questionCtrl.creaQuestionJson(req.params.id);// nécessaire d'attendre pour pouvoir tout retourner ensuite
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId, true);// pour le cache + HTML
questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId, true);// nécessaire au réaffichage après enregistrement
res.status(200).json({ message: txtQuestion.updateOkMessage , questionnaire: questionnaire });
}
next();

65
controllers/group.js

@ -157,6 +157,38 @@ exports.getOneById = async (req, res, next) =>
}
}
exports.showOneGroupById = async (req, res, next) =>
{
try
{
// Seuls certains utilisateurs peuvent avoir accès à cette page
const connectedUser=await userCtrl.checkTokenUser(req.params.token);
if(connectedUser===false)
res.status(403).json({ errors:txtGeneral.failAuthToken });
else
{
if(["admin", "manager", "creator"].indexOf(connectedUser.User.status) === -1)
res.status(403).json({ errors:txtGeneral.notAllowed });
else
{
const HTML=await creaGroupHTML(req.params.id, true);
if(HTML)
{
res.setHeader("Content-Type", "text/html");
res.send(HTML);
}
else
res.status(404).json({ errors:txtGeneral.notFound.replace("#SEARCH", req.params.id) });
}
}
next();
}
catch(e)
{
next(e);
}
}
// Retourne les statistiques concernant les groupes de questionnaires
exports.getStats = async(req, res, next) =>
{
@ -218,15 +250,18 @@ const creaGroupJson = async (id) =>
if(Group)
{
let datas={ Group };
const Questionnaires=await db["Questionnaire"].findAll({ where: { GroupId: Group.id }, order: [["rankInGroup", "ASC"], ["createdAt", "ASC"]], attributes: ["id"] });
const Questionnaires=await db["Questionnaire"].findAll({ where: { GroupId: Group.id, isPublished : true }, order: [["rankInGroup", "ASC"], ["createdAt", "ASC"]], attributes: ["id"] });
if(Questionnaires)
datas.Questionnaires=Questionnaires;
await toolFile.createJSON(configQuestionnaires.dirCacheGroups, id, datas);
// si le groupe est publiable on génère/actualise la page HTML :
// Si le groupe est publiable on génère/actualise la page HTML :
if(checkGroupIsPublishable(datas))
creaGroupHTML(id);
await creaGroupHTML(id);// !!! await important car sinon bug, plusieurs fonctions accédant au même fichier (à revoir car pas clair !)
else // dans le cas contraire, on supprime l'éventuel fichier préexistant
toolFile.deleteFile(config.dirHTMLGroups, Group.slug+".html");
// Dans certains cas les fichiers HTML des quizs du groupe peuvent aussi être impactés (notamment le dernier)
for(let i in Questionnaires)
await questionnaireCtrl.creaQuestionnaireHTML(Questionnaires[i].id, false);
// + mise à jour des statistiques :
creaStatsGroupsJson();
return datas;
@ -258,15 +293,15 @@ const checkGroupIsPublishable = (datas, checkDate=true) =>
const creaGroupHTML = async (id, preview = false) =>
{
// J'ai besoin de toutes les infos concernant le groupe et ses éléments pour les transmettre à la vue
const group=await searchGroupById(id, true);
if(!group)
return false;
if(checkGroupIsPublishable(group)===false && preview===false)
if(group.isPublishable === false && preview === false)
return false;
const txtIllustration = require("../lang/"+config.adminLang+"/illustration");
const compiledFunction = pug.compileFile("./views/"+config.theme+"/quiz-group.pug");
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js");
const pageDatas = // à revoir en regardant les besoins de la vue
const pageDatas =
{
config: config,
configQuestionnaires: configQuestionnaires,
@ -274,18 +309,19 @@ const creaGroupHTML = async (id, preview = false) =>
tool: tool,
txtGeneral: txtGeneral,
txtGroups: txtGroups,
pageLang: group.language,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(groupe.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
author: group.CreatorName,
pageTitle: txtGroups.groupsName+" "+group.title,
contentTitle: group.title+"("+txtGroups.groupsName+")",
txtIllustration: txtIllustration,
pageLang: group.Group.language,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(group.Group.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
author: group.Group.CreatorName,
pageTitle: txtGroups.groupsName+" "+group.Group.title,
contentTitle: group.Group.title+"("+txtGroups.groupsName+")",
group: group,
linkCanonical: config.siteUrl+"/"+config.dirWebGroups+"/"+group.slug+".html"
linkCanonical: config.siteUrl+"/"+config.dirWebGroups+"/"+group.Group.slug+".html"
}
const html=await compiledFunction(pageDatas);
if(preview === false)
{
await toolFile.createHTML(configQuestionnaires.dirHTMLGroups, group.slug, html);
await toolFile.createHTML(configQuestionnaires.dirHTMLGroups, group.Group.slug, html);
return true;
}
else
@ -300,6 +336,7 @@ const searchGroupById = async (id, reassemble=false) =>
group=await creaGroupJson(id);
if(!group)
return false;
group.Group.isPublishable=checkGroupIsPublishable(group);
if(reassemble)
{
let questionnaire; Questionnaires=[];
@ -308,7 +345,7 @@ const searchGroupById = async (id, reassemble=false) =>
group.Group.CreatorName=author.User.name;
for(let i in group.Questionnaires)
{
questionnaire=await questionnaireCtrl.searchQuestionnaireById(questionnaire.Questions[i].id, true);
questionnaire=await questionnaireCtrl.searchQuestionnaireById(group.Questionnaires[i].id, true);
if(questionnaire)
Questionnaires.push(questionnaire);
}

4
controllers/question.js

@ -20,12 +20,12 @@ exports.create = async (req, res, next) =>
const questionnaire=await questionnaireCtrl.searchQuestionnaireById(req.body.QuestionnaireId);
if(!questionnaire)
throw txt.needQuestionnaire;
else if(config.nbQuestionsMax!==0 && questionnaire.Questions.length>=config.nbQuestionsMax)
else if(config.nbQuestionsMax !== 0 && questionnaire.Questions.length >= config.nbQuestionsMax)
res.status(400).json({ errors: [txt.needMaxQuestions+config.nbQuestionsMax] });
else
{
const question=await db["Question"].create({ ...req.body }, { fields: ["text", "explanation", "rank", "QuestionnaireId"] });
questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId);
await questionnaireCtrl.creaQuestionnaireJson(req.body.QuestionnaireId);
req.body.QuestionId=question.id;
next();// je passe la main au contrôleur qui gère les réponses possibles saisies pour cette question
return true;

205
controllers/questionnaire.js

@ -15,6 +15,7 @@ const toolError = require("../tools/error");
const toolFile = require("../tools/file");
const toolMail = require("../tools/mail");
const groupCtrl = require("./group");
const questionCtrl = require("./question");
const illustrationCtrl = require("./illustration");
const tagCtrl = require("./tag");
@ -29,13 +30,39 @@ exports.create = async (req, res, next) =>
{
try
{
const db = require("../models/index");
req.body.CreatorId=req.connectedUser.User.id;
const questionnaire=await db["Questionnaire"].create({ ...req.body }, { fields: ["title", "slug", "introduction", "keywords", "publishingAt", "language", "estimatedTime", "CreatorId"] });
creaStatsQuestionnairesJson();
//utile au middleware suivant (classement tags) qui s'occupe aussi de retourner une réponse si ok :
req.body.QuestionnaireId=questionnaire.id;
next();
if(tool.isEmpty(req.body.GroupId) && !tool.isEmpty(req.body.rankInGroup))
res.status(400).json({ errors : [txtQuestionnaire.needGroupIfRank] });
else
{
const db = require("../models/index");
req.body.CreatorId=req.connectedUser.User.id;
if(!tool.isEmpty(req.body.GroupId) && tool.isEmpty(req.body.rankInGroup))
req.body.rankInGroup=1;
const questionnaire=await db["Questionnaire"].create({ ...req.body }, { fields: ["title", "slug", "introduction", "keywords", "publishingAt", "language", "estimatedTime", "CreatorId", "GroupId", "rankInGroup"] });
creaStatsQuestionnairesJson();
// Si un groupe a été sélectionné, il faut mettre à jour les fichiers du groupe :
if(!tool.isEmpty(req.body.GroupId))
{
const groupInfos = await groupCtrl.creaGroupJson(req.body.GroupId);
// Si le nouveau quiz est publiable, le fichier HTML de l'éventuel quiz/élément précédent du groupe doit être actualisé
// Si le nouveau n'est pas publiable, il ne sera pas listé dans groupInfos.Questionnaires donc pas d'incidence
if(groupInfos !== false)
{
for(let i in groupInfos.Questionnaires)
{
if(groupInfos.Questionnaires[i].id === questionnaire.id && i != 0)
{
let j=parseInt(i)-1;// !! "i" n'est pas un nombre !
if(groupInfos.Questionnaires[j] !== undefined)
await creaQuestionnaireHTML(groupInfos.Questionnaires[j].id);
}
}
}
}
// id utile au middleware suivant (classement tags) qui s'occupe aussi de retourner une réponse si ok :
req.body.QuestionnaireId=questionnaire.id;
next();
}
}
catch(e)
{
@ -65,12 +92,27 @@ exports.modify = async (req, res, next) =>
}
else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.CreatorId)
res.status(401).json({ errors: txtGeneral.notAllowed });
else if(tool.isEmpty(req.body.GroupId) && !tool.isEmpty(req.body.rankInGroup))
res.status(400).json({ errors : [txtQuestionnaire.needGroupIfRank] });
else
{
await db["Questionnaire"].update({ ...req.body }, { where: { id : req.params.id } , fields: ["title", "slug", "introduction", "keywords", "publishingAt", "language", "estimatedTime"], limit:1 }); // voir si admin aura le droit de changer le créateur ? et ajouter une gestion des redirection si slug change ?
if(!tool.isEmpty(req.body.GroupId) && tool.isEmpty(req.body.rankInGroup))
req.body.rankInGroup=1;
await db["Questionnaire"].update({ ...req.body }, { where: { id : req.params.id } , fields: ["title", "slug", "introduction", "keywords", "publishingAt", "language", "estimatedTime", "GroupId", "rankInGroup"], limit:1 });
creaStatsQuestionnairesJson();// le nombre de quizs publiés peut avoir changé
// Si le questionnaire a été déplacé d'un groupe, il faut actualiser les fichiers de l'ancien groupe :
if(req.body.GroupId != questionnaire.Questionnaire.GroupId && !tool.isEmpty(questionnaire.Questionnaire.GroupId))
{ //(ne pas utiliser "!==" car types différents)
const groupInfos = await groupCtrl.creaGroupJson(questionnaire.Questionnaire.GroupId);
// Peut aussi avoir un impact sur les autres élement dans l'ancien groupe, notamment celui qui le précédait
if(groupInfos !== false)
{
for(let i in groupInfos.Questionnaires)
await creaQuestionnaireHTML(groupInfos.Questionnaires[i].id);
}
}
}
//utile au middleware suivant (classement tags) qui s'occupe aussi de retourner une réponse si ok :
// id utile au middleware suivant (classement tags) qui s'occupe aussi de retourner une réponse si ok :
req.body.QuestionnaireId=req.params.id;
next();
}
@ -107,7 +149,7 @@ exports.delete = async (req, res, next) =>
exports.getOneQuestionnaireById = async (req, res, next) =>
{
try
{
{
const datas=await searchQuestionnaireById(req.params.id, true);
if(datas)
res.status(200).json(datas);
@ -125,9 +167,9 @@ exports.showOneQuestionnaireById = async (req, res, next) =>
{
try
{
// Seuls certains utilisateurs peuvent avoir accès à cette page
// Seuls certains utilisateurs peuvent avoir accès à cette route :
const connectedUser=await userCtrl.checkTokenUser(req.params.token);
if(connectedUser===false)
if(connectedUser === false)
res.status(403).json({ errors:txtGeneral.failAuthToken });
else
{
@ -301,16 +343,15 @@ exports.getListNextQuestionnaires = async(req, res, next) =>
}
}
// test si des questionnaires doivent être publiés
// puis (re)génère tous les fichiers HTML des questionnaires + les pages accueil + news
// la requête est ensuite passé aux tags qui font la même chose
// Test si des questionnaires doivent être publiés puis (re)génère tous les fichiers HTML des questionnaires + les pages accueil + news
// La requête est ensuite passé aux tags qui font la même chose
exports.HTMLRegenerate= async (req, res, next) =>
{
try
{
await checkQuestionnairesNeedToBePublished();
const nb=await checkQuestionnairesPublishedHaveHTML(true);
creaNewQuestionnairesJson();// provoque mise à jour du HTLM, RSS, etc.
await creaNewQuestionnairesJson();// provoque mise à jour du HTLM, RSS, etc.
res.messageToNext=txtQuestionnaire.haveBeenRegenerated.replace("#NB1", nb);
next();
}
@ -368,7 +409,7 @@ exports.checkQuestionnairesNeedToBePublished= async (req, res, next) =>
const creaQuestionnaireJson = async (id) =>
{
const db = require("../models/index");
const db=require("../models/index");
const Questionnaire=await db["Questionnaire"].findByPk(id);
if(Questionnaire)
{
@ -387,24 +428,36 @@ const creaQuestionnaireJson = async (id) =>
datas.Questions=Questions;
const wasPublished=datas.Questionnaire.isPublished;
datas.Questionnaire.isPublished=checkQuestionnaireIsPublishable(datas);
// important d'écrire le fichier ici, car il est nécessaire aux autres fonctions appelées ci-dessous
// Important d'écrire le fichier ici, car il est nécessaire aux fonctions appelées par la suite
await toolFile.createJSON(config.dirCacheQuestionnaires, id, datas);
if(datas.Questionnaire.isPublished!==wasPublished)
if(datas.Questionnaire.isPublished !== wasPublished)
{
await db["Questionnaire"].update({ isPublished:datas.Questionnaire.isPublished }, { where: { id : id } , fields: ["isPublished"], limit:1 });
// + liste des tags utilisés :
// + Peut impacter la liste des tags utilisés :
tagCtrl.creaUsedTagsJson();
// si le quiz était publié jusqu'ici, il me faut supprimer son fichier HTML (revenir pour réactiver)
// Et si le quiz était publié jusqu'ici, il me faut supprimer son fichier HTML.
if(wasPublished)
toolFile.deleteFile(config.dirHTMLQuestionnaires, Questionnaire.slug+".html");
}
// peut impacter la liste des derniers si des informations affichées ont changé
creaNewQuestionnairesJson();// peut avoir été impacté
// Peut impacter la liste des derniers quizs si des informations affichées ont changé
creaNewQuestionnairesJson();
// + les listes de quizs / tags :
for(let i in Tags)
tagCtrl.creaQuestionnairesTagJson(Tags[i].TagId) // ! Json + HTML, donc potentiellement long.
if(datas.Questionnaire.isPublished)
await creaQuestionnaireHTML(id);
// Si le questionnaire est l'élément d'un groupe, il faut actualiser les fichiers du groupe
// Il faut le faire ici et non dans le contrôleur de mise à jour, car des chgts des éléments annexes (liens, questions...) peuvent aussi impacter les fichiers du groupe ou avoir rendu le questionnaire publiable, etc.
if(!tool.isEmpty(Questionnaire.GroupId))
{
const groupInfos = await groupCtrl.creaGroupJson(Questionnaire.GroupId);
// Idem pour le HTML des éventuels autres quizs du groupe car title, slug, rank, etc. peuvent avoir changé
if(groupInfos !== false)
{
for(let i in groupInfos.Questionnaires)
await creaQuestionnaireHTML(groupInfos.Questionnaires[i].id);
}
}
return datas;
}
else
@ -440,18 +493,29 @@ const deleteQuestionnaire = async (id, req) =>
for(let i in questionnaire.Illustrations)
await illustrationCtrl.deleteIllustrationById(questionnaire.Illustrations[i].id);
const nb=await db["Questionnaire"].destroy( { where: { id : id }, limit:1 });
if(nb===1)// = json existant, bien que sql déjà supprimé
if(nb===1)// = json existant, bien que sql déjà supprimé ?
{
toolFile.deleteJSON(configQuestionnaires.dirCacheQuestionnaires, id);
// + HTML :
toolFile.deleteFile(configQuestionnaires.dirHTMLQuestionnaires, questionnaire.Questionnaire.slug+".html");
// Si ce questionnaire était l'élément d'un groupe, il faut actualiser ses fichiers :
if(!tool.isEmpty(questionnaire.Questionnaire.GroupId))
{
const groupInfos=await groupCtrl.creaGroupJson(questionnaire.Questionnaire.GroupId);
// Idem pour le HTML des éventuels autres quizs du groupe, notamment si un d'entre eux le précédait
if(groupInfos !== false)
{
for(let i in groupInfos.Questionnaires)
await creaQuestionnaireHTML(groupInfos.Questionnaires[i].id);
}
}
// Actualisation de liste des questionnaires pour les tags concernés.
// Ici au contraire, les enregistrements doivent être supprimés avant.
for(let i in questionnaire.Tags)
tagCtrl.creaQuestionnairesTagJson(questionnaire.Tags[i].TagId);
// La suppression peut éventuellement concerner un des derniers questionnaires, donc :
creaNewQuestionnairesJson();
creaStatsQuestionnairesJson();
await creaNewQuestionnairesJson();
await creaStatsQuestionnairesJson();
return true;
}
}
@ -473,42 +537,101 @@ const checkQuestionnaireIsPublishable = (datas, checkDate=true) =>
return false;
}
}
if(datas.Questions==undefined || datas.Questions.length < config.nbQuestionsMin)
if(datas.Questions === undefined || datas.Questions.length < config.nbQuestionsMin)
return false;// le nombre de réponses mini étant contrôlé au moment de l'enregistrement de la question
if(datas.Tags==undefined || datas.Tags.length < config.nbTagsMin)
if(datas.Tags === undefined || datas.Tags.length < config.nbTagsMin)
return false;
if(datas.Links==undefined || datas.Links.length < config.nbLinksMin)
if(datas.Links === undefined || datas.Links.length < config.nbLinksMin)
return false;
if(datas.Illustrations==undefined || datas.Illustrations.length < config.nbIllustrationsMin)
if(datas.Illustrations === undefined || datas.Illustrations.length < config.nbIllustrationsMin)
return false;
return true;
}
const creaQuestionnaireHTML = async (id, preview=false) =>
{
// besoin de toutes les infos concernant le questionnaire pour les transmettre à la vue
// à terme : séparer la création de la partie body pouvant être retournée pour recharger page, de la génération complète pour créer le fichier html
// deux possibilités :
// -- si élément d'un groupe de quiz : juste le texte sans les questions
// -- si quiz automone : toutes les infos
const questionnaire=await searchQuestionnaireById(id, true);
if(!questionnaire)
return false;
if(questionnaire.Questionnaire.isPublished===false && preview===false)
return false;
const compiledFunction = pug.compileFile("./views/"+config.theme+"/quiz.pug");
if(!tool.isEmpty(questionnaire.Questionnaire.GroupId))
return creaQuestionnaireInGroupHTML(questionnaire, preview);
else
{
const compiledFunction = pug.compileFile("./views/"+config.theme+"/quiz.pug");
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js");
const pageDatas =
{
config: config,
configTpl: configTpl,
tool: tool,
txtGeneral : txtGeneral,
txtQuestionnaire: txtQuestionnaire,
txtIllustration: txtIllustration,
pageLang: questionnaire.Questionnaire.language,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(questionnaire.Questionnaire.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
author: questionnaire.Questionnaire.CreatorName,
pageTitle: questionnaire.Questionnaire.title+" ("+txtQuestionnaire.questionnairesName+")",
contentTitle: questionnaire.Questionnaire.title,
questionnaire: questionnaire,
linkCanonical: config.siteUrl+"/"+config.dirWebQuestionnaires+"/"+questionnaire.Questionnaire.slug+".html"
}
const html=await compiledFunction(pageDatas);
if(preview===false)
{
await toolFile.createHTML(config.dirHTMLQuestionnaires, questionnaire.Questionnaire.slug, html);
return true;
}
else
return html;
}
}
exports.creaQuestionnaireHTML = creaQuestionnaireHTML;
const creaQuestionnaireInGroupHTML = async (questionnaire, preview=false) =>
{
if(questionnaire === undefined)
return false;
if(questionnaire.Questionnaire.isPublished === false && preview === false)
return false;
const compiledFunction = pug.compileFile("./views/"+config.theme+"/quiz-element.pug");
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js");
const txtUser = require("../lang/"+config.adminLang+"/user");
// J'ai aussi besoin de certaines informations du groupe :
const groupInfos=await groupCtrl.searchGroupById(questionnaire.Questionnaire.GroupId, false);
if(!groupInfos)
return false;
// + Certaines infos de l'élément suivant du groupe, s'il existe :
let nextQuestionnaire=null;
for(let i in groupInfos.Questionnaires)
if(groupInfos.Questionnaires[i].id === questionnaire.Questionnaire.id)
{
let j=parseInt(i)+1;// !! "i" n'est pas un nombre !
if(groupInfos.Questionnaires[j] !== undefined)
nextQuestionnaire=await searchQuestionnaireById(groupInfos.Questionnaires[j].id, false);
}
const pageDatas=
{
config: config,
configQuestionnaires: configQuestionnaires,
configTpl: configTpl,
tool: tool,
txtGeneral : txtGeneral,
txtQuestionnaire: txtQuestionnaire,
txtIllustration: txtIllustration,
txtUser: txtUser,
pageLang: questionnaire.Questionnaire.language,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(questionnaire.Questionnaire.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
author: questionnaire.Questionnaire.CreatorName,
pageTitle: questionnaire.Questionnaire.title+" ("+txtQuestionnaire.questionnairesName+")",
contentTitle: questionnaire.Questionnaire.title,
questionnaire: questionnaire,
group: groupInfos,
nextQuestionnaire: nextQuestionnaire,
linkCanonical: config.siteUrl+"/"+config.dirWebQuestionnaires+"/"+questionnaire.Questionnaire.slug+".html"
}
const html=await compiledFunction(pageDatas);
@ -530,22 +653,24 @@ const searchQuestionnaireById = async (id, reassemble=false) =>
return false;
if(reassemble)
{
let question; Questions=[];
const author=await userCtrl.searchUserById(questionnaire.Questionnaire.CreatorId);
if(author)
questionnaire.Questionnaire.CreatorName=author.User.name;
for(i in questionnaire.Questions)
if(!tool.isEmpty(questionnaire.Questionnaire.GroupId))
questionnaire.Group=await groupCtrl.searchGroupById(questionnaire.Questionnaire.GroupId, false);// !! false, sinon on risque de tourner en rond !
let questionDatas; questionsDatas=[];
for(let i in questionnaire.Questions)
{
question=await questionCtrl.searchQuestionById(questionnaire.Questions[i].id);
if(question)
Questions.push(question);
questionDatas=await questionCtrl.searchQuestionById(questionnaire.Questions[i].id);
if(questionDatas)
questionsDatas.push(questionDatas);
}
questionnaire.Questions=Questions;
questionnaire.Questions=questionsDatas;
const tags=await tagCtrl.getTagsQuestionnaire(id);
if(tags)
questionnaire.Tags=tags;
}
return questionnaire;
return questionnaire;
}
exports.searchQuestionnaireById = searchQuestionnaireById;

1
front/public/gestion-groups.html

@ -62,6 +62,7 @@
<input type="hidden" name="id" id="id" value="">
<div class="input_wrapper"><input type="submit" value="Valider" class="cardboard" id="submitDatas" /></div>
<div class="input_wrapper"><a href="#groups" class="button cardboard" id="wantNewGroup">Vider</a></div>
<div class="input_wrapper"><a href="#groups" class="button cardboard" id="previewGroup" target="_blank">Voir le quiz</a></div>
<div id="response"></div>
</form>
<div id="response"></div>

319
front/src/group.js

@ -1,176 +1,198 @@
// -- GESTION DU FORMULAIRE PERMETTANT DE CRÉER SON COMPTE
// -- GESTION DU FORMULAIRE PERMETTANT D'AFFICHER ET RÉPONDRE À UN QUIZ
/// L'utilisateur peut avoir répondu à un quiz avant d'arriver sur la page d'inscription
/// Des ce cas il faut enregistrer son résultat en même temps que les informations de son compte
/// Il n'est pas nécessaire d'être connecté pour répondre au quiz et voir son résultat.
/// Mais si pas connecté, on propose à l'internaute de se connecter ou de créer un compte pour sauvegarder son résultat.
/// Dans ce but son résultat est stocké dans son navigateur.
/// Si il est connecté, l'enregistrement de son résultat se fait automatiquement côté serveur et ses éventuels précédents résultats sont affichés.
// Fichier de configuration tirés du backend :
import { apiUrl, availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0];
import { getPreviousAnswers, questionnaireRoutes, saveAnswersRoute } from "../../config/questionnaires.js";
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");// besoin de toutes les déclarations, car appel dynamique : configTemplate[homePage]
const configUsers = require("../../config/users");// idem pour configurer formulaire
// Importation des fonctions utiles au script :
import { getLocaly, removeLocaly, saveLocaly } from "./tools/clientstorage.js";
import { checkAnswerOuput, saveAnswer } from "./tools/answers.js";
import { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js";
import { getDatasFromInputs, setAttributesToInputs } from "./tools/forms.js";
import { helloDev, updateAccountLink } from "./tools/everywhere.js";
import { getLocaly } from "./tools/clientstorage.js";
import { getDatasFromInputs } from "./tools/forms.js";
import { dateFormat, replaceAll } from "../../tools/main";
import { loadMatomo } from "./tools/matomo.js";
import { checkAnswerDatas, checkSession, getPassword, getTimeDifference } from "./tools/users.js";
import { checkSession, getTimeDifference } from "./tools/users.js";
// Dictionnaires :
const { notRequired, serverError } = require("../../lang/"+lang+"/general");
const { alreadyConnected, godfatherFound, godfatherNotFound, needUniqueEmail, passwordCopied } = require("../../lang/"+lang+"/user");
const { noPreviousAnswer, previousAnswersLine, previousAnswersStats, previousAnswersTitle, responseSavedError, wantToSaveResponses } = require("../../lang/"+lang+"/answer");
const { serverError } = require("../../lang/"+lang+"/general");
// Principaux éléments du DOM manipulés :
const myForm=document.getElementById("subscription");
const divResponse=document.getElementById("response");
const passwordInput=document.getElementById("password");
const passwordLink=document.getElementById("getPassword");
const passwordHelp=document.getElementById("passwordMessage");
const emailInput=document.getElementById("email");
const btnSubmit=document.getElementById("submitDatas");
const codeGodfatherInput=document.getElementById("codeGodfather");
helloDev();
const myForm = document.getElementById("questionnaire");
const divResponse = document.getElementById("response");
const btnShow = document.getElementById("showQuestionnaire");
const btnSubmit = document.getElementById("checkResponses");
const explanationsTitle = document.getElementById("explanationsTitle");
const explanationsContent = document.getElementById("explanationsContent");
// Test de connexion de l'utilisateur + affichage formulaire d'inscription.
let isConnected, user;
const initialise = async () =>
{
try
{
const isConnected=await checkSession();
btnShow.style.display="inline";// bouton caché si JS inactif, car JS nécessaire pour vérifier les réponses
isConnected=await checkSession(["user"]);// "user" car seuls les utilisateurs de base peuvent enregistrer leurs réponses aux quizs
// Si l'utilisateur est connecté et a déjà répondu à ce quiz, on affiche ses précédentes réponses à la place du texte servant à expliquer le topo aux nouveaux
if(isConnected)
{
saveLocaly("message", { message: alreadyConnected, color:"info" });// pour l'afficher sur la page suivante
const user=getLocaly("user", true);
const homePage=user.status+"HomePage";
window.location.assign("/"+configTemplate[homePage]);
user=getLocaly("user", true);
updateAccountLink(user.status, configTemplate);// lien vers le compte adapté pour les utilisateurs connectés
checkPreviousResponses(user);
}
else
{
loadMatomo();
setAttributesToInputs(configUsers, myForm);
myForm.style.display="block";
}
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
}
initialise();
helloDev();
// Générateur de mot de passe "aléatoire"
passwordLink.addEventListener("click", function(e)
{
e.preventDefault();
passwordInput.type="text";
passwordInput.value=getPassword(8, 12);
// Copie du mot de passe généré dans le "presse-papier" de l'ordinateur :
passwordInput.select();
document.execCommand("copy");
addElement(passwordHelp, "div", passwordCopied, "", ["success"]);
});
// Test si l'e-mail saisi est déjà utilisé par un autre compte.
// Si c'est le cas, la validation du formulaire est bloquée.
emailInput.addEventListener("focus", function(e)
{
document.getElementById("emailMessage").innerHTML="";// pour supprimer l'éventuel message d'erreur déjà affiché
});
emailInput.addEventListener("blur", function(e)
// Affichage du questionnaire quand l'utilisateur clique sur le bouton ou si l'id du formulaire est passée par l'url.
// Déclenche en même temps le chronomètre mesurant la durée de la réponse aux questions.
const showQuestionnaire = () =>
{
const emailValue=emailInput.value.trim();
if(emailValue!=="")
chronoBegin=Date.now();
myForm.style.display="block";
btnShow.style.display="none";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview".
if(window.location.hash!=="")
{
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.checkIfIsEmailfreeRoute);
xhr.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200 && response.free!==undefined && response.free === false)
{
addElement(document.getElementById("emailMessage"), "div", needUniqueEmail.replace("#URL", configTemplate.connectionPage), "", ["error"]);
btnSubmit.setAttribute("disabled", true);
}
else
btnSubmit.removeAttribute("disabled");
}
}
xhr.setRequestHeader("Content-Type", "application/json");
const datas={ emailTest:emailValue };
xhr.send(JSON.stringify(datas));
window.location.hash="";// ! le "#" reste
window.location.assign(here+"questionnaire");
}
});
// Vérification que le code/e-mail de parrainage saisi est valide.
codeGodfatherInput.addEventListener("focus", function(e)
{ // on efface l'éventuel message d'erreur si on revient sur le champ pour tester un autre code.
addElement(document.getElementById("codeGodfatherMessage"), "i", notRequired);
});
codeGodfatherInput.addEventListener("blur", function(e)
else
window.location.assign(here+"#questionnaire");
}
let chronoBegin=0;
btnShow.addEventListener("click", function(e)
{
const codeValue=codeGodfatherInput.value.trim();
if(codeValue!=="")
try
{
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.getGodfatherRoute);
xhr.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
if (this.status === 204)
addElement(document.getElementById("codeGodfatherMessage"), "div", godfatherNotFound, "", ["error"]);
else
addElement(document.getElementById("codeGodfatherMessage"), "div", godfatherFound, "", ["success"]);
}
}
xhr.setRequestHeader("Content-Type", "application/json");
const datas={ codeTest:codeValue };
xhr.send(JSON.stringify(datas));
e.preventDefault();
showQuestionnaire();
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
});
// Lien passé par mail pour voir directement le quiz
if(location.hash!="" && location.hash==="#questionnaire")
showQuestionnaire();
// Traitement de l'envoi des données d'inscription :
// Traitement de l'envoi de la réponse de l'utilisateur :
let answer = {};
myForm.addEventListener("submit", function(e)
{
try
{
e.preventDefault();
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.subscribeRoute);
xhr.onreadystatechange = function()
e.preventDefault();
btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :)
divResponse.innerHTML="";// supprime les éventuels messages déjà affichés
const userResponses=getDatasFromInputs(myForm);
answer.duration=Math.round((Date.now()-chronoBegin)/1000);
answer.nbQuestions=0;
answer.nbCorrectAnswers=0;
answer.QuestionnaireId=document.getElementById("questionnaireId").value;
// Les réponses sont regroupées par question, donc quand idQuestion change, on connaît le résultat pour la question précédente.
// Pour qu'une réponse soit bonne, il faut cocher toutes les bonnes réponses (si QCM) à la question ET cocher aucune des mauvaises.
let idChoice, idQuestion="", goodResponse=false;
for(let item in userResponses)
{
if (this.readyState == XMLHttpRequest.DONE)
if(item.startsWith("isCorrect_response_"))// = Nouvelle réponse possible.
{
let response=JSON.parse(this.responseText);
if (this.status === 201)
idChoice = item.substring(item.lastIndexOf("_") + 1);
// si on change de queston
if(userResponses["question_id_response_"+idChoice]!=idQuestion) // on commence à traiter une nouvelle question
{
myForm.style.display="none";
addElement(divResponse, "p", response.message, "", ["success"]);
removeLocaly("lastAnswer");// ! important pour ne pas enregister plusieurs fois le résultat.
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...
}
else if (response.errors)
if(userResponses[item]=="true")
{
response.errors = response.errors.join("<br>");
addElement(divResponse, "p", response.errors, "", ["error"]);
document.getElementById("response_"+idChoice).parentNode.classList.add("isCorrect");
if(userResponses["response_"+idChoice]===undefined)// une bonne réponse n'a pas été sélectionnée
goodResponse=false;
}
else
addElement(divResponse, "p", serverError, "", ["error"]);
{
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");
}
}
}
}
xhr.setRequestHeader("Content-Type", "application/json");
let datas=getDatasFromInputs(myForm);
if(datas)
// si j'ai bien répondu à la dernière question, il faut le compter ici, car je suis sorti de la boucle :
if(goodResponse)
answer.nbCorrectAnswers++;
// Affichage du résultat, suivant si l'utilisateur est connecté ou pas et son score :
let getOuput=checkAnswerOuput(answer);
if(isConnected)
{
datas.timeDifference=getTimeDifference(configUsers);
// si l'utilisateur a précédement répondu à un quiz, j'ajoute les infos de son résultat :
datas=checkAnswerDatas(datas);
xhr.send(JSON.stringify(datas));
// Si l'utilisateur est connecté, on enregistre son résultat sur le serveur.
const xhrSaveAnswer = new XMLHttpRequest();
xhrSaveAnswer.open("POST", apiUrl+questionnaireRoutes+saveAnswersRoute);
xhrSaveAnswer.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let xhrResponse=JSON.parse(this.responseText);
if (this.status === 201 && (xhrResponse.message))
{
getOuput+="<br>"+xhrResponse.message.replace("#URL", configTemplate.userHomePage);
checkPreviousResponses(user);
}
else
getOuput+="<br>"+responseSavedError.replace("#URL", configTemplate.userHomePage);
// on redirige vers le résultat
window.location.hash="";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"...
window.location.assign(here+"explanations");
}
}
xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token);
xhrSaveAnswer.setRequestHeader("Content-Type", "application/json");
answer.timeDifference=getTimeDifference();// on en profite pour mettre les pendules à l'heure.
xhrSaveAnswer.send(JSON.stringify(answer));
}
else
{ // si pas connecté, on enregistre le résultat côté client pour permettre de le retrouver au moment de la création du compte ou de la connexion.
if(saveAnswer(answer))
{
getOuput+="<br><br>"+wantToSaveResponses;
addElement(divResponse, "p", getOuput, "", ["info"]);
document.querySelector(".subscribeBtns").style.display="block";
}
else // inutile de proposer de créer un compte si le stockage local ne fonctionne pas
addElement(divResponse, "p", getOuput, "", ["info"]);
// on redirige vers le résultat
window.location.hash="";
const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"...
window.location.assign(here+"response");
}
// + affichage des textes d'explications pour chaque question
const explanations=document.querySelectorAll(".help");
for(let i in explanations)
{
if(explanations[i].style!=undefined) // sinon, la console affiche une erreur "TypeError: explanations[i].style is undefined", bien que tout fonctionne (?)
explanations[i].style.display="block";
}
}
catch(e)
@ -178,4 +200,57 @@ myForm.addEventListener("submit", function(e)
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
});
})
// Fonction vérifiant les précédentes réponses de l'utilisateur
// Utile si connecté lors du premier chargement de la page, puis après une nouvelle réponse
const checkPreviousResponses = (user) =>
{
const xhrPreviousRes = new XMLHttpRequest();
xhrPreviousRes.open("GET", apiUrl+questionnaireRoutes+getPreviousAnswers+user.id+"/"+document.getElementById("questionnaireId").value);
xhrPreviousRes.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200)
{
const nbResponses=response.length;
let previousAnswersContent="";
addElement(explanationsTitle, "span", previousAnswersTitle.replace("#NOM", user.name));
if(nbResponses!==0)
{
let totNbQuestions=0, totNbCorrectAnswers=0, totDuration=0, mapLineContent;
for(let i in response)
{
totNbQuestions+=response[i].nbQuestions;// ! on ne peut se baser sur la version actuelle du quiz, car le nombre de questions a pu évoluer.
totNbCorrectAnswers+=response[i].nbCorrectAnswers;
totDuration+=response[i].duration;
mapLineContent =
{
DATEANSWER : dateFormat(response[i].createdAt, lang),
NBCORRECTANSWERS : response[i].nbCorrectAnswers,
NBQUESTIONS : response[i].nbQuestions,
AVGDURATION : response[i].duration
};
previousAnswersContent+="<li>"+replaceAll(previousAnswersLine, mapLineContent)+"</li>";
}
mapLineContent =
{
AVGDURATION : Math.round(totDuration/nbResponses),
AVGCORRECTANSWERS : Math.round(totNbCorrectAnswers/totNbQuestions*100)
};
previousAnswersContent="<h5>"+replaceAll(previousAnswersStats, mapLineContent)+"</h5>"+previousAnswersContent;
addElement(explanationsContent, "ul", previousAnswersContent);
}
else
addElement(explanationsContent, "ul", noPreviousAnswer);
// dans un cas comme dans l'autre, bouton pour revenir à l'accueil du compte
addElement(explanationsContent, "p", "<a href=\"/"+configTemplate.userHomePage+"\" class=\"button cardboard\">"+configTemplate.userHomePageTxt+"</a>", "", ["btn"], "", false);
}
}
}
xhrPreviousRes.setRequestHeader("Authorization", "Bearer "+user.token);
xhrPreviousRes.send();
}

181
front/src/groupElement.js

@ -0,0 +1,181 @@
// -- GESTION DU FORMULAIRE PERMETTANT DE CRÉER SON COMPTE
/// L'utilisateur peut avoir répondu à un quiz avant d'arriver sur la page d'inscription
/// Des ce cas il faut enregistrer son résultat en même temps que les informations de son compte
// Fichier de configuration tirés du backend :
import { apiUrl, availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0];
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");// besoin de toutes les déclarations, car appel dynamique : configTemplate[homePage]
const configUsers = require("../../config/users");// idem pour configurer formulaire
// Importation des fonctions utiles au script :
import { getLocaly, removeLocaly, saveLocaly } from "./tools/clientstorage.js";
import { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js";
import { getDatasFromInputs, setAttributesToInputs } from "./tools/forms.js";
import { loadMatomo } from "./tools/matomo.js";
import { checkAnswerDatas, checkSession, getPassword, getTimeDifference } from "./tools/users.js";
// Dictionnaires :
const { notRequired, serverError } = require("../../lang/"+lang+"/general");
const { alreadyConnected, godfatherFound, godfatherNotFound, needUniqueEmail, passwordCopied } = require("../../lang/"+lang+"/user");
// Principaux éléments du DOM manipulés :
const myForm=document.getElementById("subscription");
const divResponse=document.getElementById("response");
const passwordInput=document.getElementById("password");
const passwordLink=document.getElementById("getPassword");
const passwordHelp=document.getElementById("passwordMessage");
const emailInput=document.getElementById("email");
const btnSubmit=document.getElementById("submitDatas");
const codeGodfatherInput=document.getElementById("codeGodfather");
helloDev();
// Test de connexion de l'utilisateur + affichage formulaire d'inscription.
const initialise = async () =>
{
try
{
const isConnected=await checkSession();
if(isConnected)
{
//saveLocaly("message", { message: alreadyConnected, color:"info" });// pour l'afficher sur la page suivante
//const user=getLocaly("user", true);
//const homePage=user.status+"HomePage";
//window.location.assign("/"+configTemplate[homePage]);
}
else
{
loadMatomo();
setAttributesToInputs(configUsers, myForm);
myForm.style.display="block";
}
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
}
initialise();
// Générateur de mot de passe "aléatoire"
passwordLink.addEventListener("click", function(e)
{
e.preventDefault();
passwordInput.type="text";
passwordInput.value=getPassword(8, 12);
// Copie du mot de passe généré dans le "presse-papier" de l'ordinateur :
passwordInput.select();
document.execCommand("copy");
addElement(passwordHelp, "div", passwordCopied, "", ["success"]);
});
// Test si l'e-mail saisi est déjà utilisé par un autre compte.
// Si c'est le cas, la validation du formulaire est bloquée.
emailInput.addEventListener("focus", function(e)
{
document.getElementById("emailMessage").innerHTML="";// pour supprimer l'éventuel message d'erreur déjà affiché
});
emailInput.addEventListener("blur", function(e)
{
const emailValue=emailInput.value.trim();
if(emailValue!=="")
{
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.checkIfIsEmailfreeRoute);
xhr.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200 && response.free!==undefined && response.free === false)
{
addElement(document.getElementById("emailMessage"), "div", needUniqueEmail.replace("#URL", configTemplate.connectionPage), "", ["error"]);
btnSubmit.setAttribute("disabled", true);
}
else
btnSubmit.removeAttribute("disabled");
}
}
xhr.setRequestHeader("Content-Type", "application/json");
const datas={ emailTest:emailValue };
xhr.send(JSON.stringify(datas));
}
});
// Vérification que le code/e-mail de parrainage saisi est valide.
codeGodfatherInput.addEventListener("focus", function(e)
{ // on efface l'éventuel message d'erreur si on revient sur le champ pour tester un autre code.
addElement(document.getElementById("codeGodfatherMessage"), "i", notRequired);
});
codeGodfatherInput.addEventListener("blur", function(e)
{
const codeValue=codeGodfatherInput.value.trim();
if(codeValue!=="")
{
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.getGodfatherRoute);
xhr.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
if (this.status === 204)
addElement(document.getElementById("codeGodfatherMessage"), "div", godfatherNotFound, "", ["error"]);
else
addElement(document.getElementById("codeGodfatherMessage"), "div", godfatherFound, "", ["success"]);
}
}
xhr.setRequestHeader("Content-Type", "application/json");
const datas={ codeTest:codeValue };
xhr.send(JSON.stringify(datas));
}
});
// Traitement de l'envoi des données d'inscription :
myForm.addEventListener("submit", function(e)
{
try
{
e.preventDefault();
const xhr = new XMLHttpRequest();
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.subscribeRoute);
xhr.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 201)
{
myForm.style.display="none";
addElement(divResponse, "p", response.message, "", ["success"]);
removeLocaly("lastAnswer");// ! important pour ne pas enregister plusieurs fois le résultat.
}
else if (response.errors)
{
response.errors = response.errors.join("<br>");
addElement(divResponse, "p", response.errors, "", ["error"]);
}
else
addElement(divResponse, "p", serverError, "", ["error"]);
}
}
xhr.setRequestHeader("Content-Type", "application/json");
let datas=getDatasFromInputs(myForm);
if(datas)
{
datas.timeDifference=getTimeDifference(configUsers);
// si l'utilisateur a précédement répondu à un quiz, j'ajoute les infos de son résultat :
datas=checkAnswerDatas(datas);
xhr.send(JSON.stringify(datas));
}
}
catch(e)
{
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e);
}
});

15
front/src/manageGroups.js

@ -6,7 +6,7 @@
/// Si pas d'id passé par l'url, on affiche un formulaire vide permettant d'en saisir un nouveau.
// Fichiers de configuration :
import { apiUrl, availableLangs, theme } from "../../config/instance.js";
import { apiUrl, availableLangs, siteUrl, theme } from "../../config/instance.js";
const lang=availableLangs[0];
const configQuestionnaires = require("../../config/questionnaires.js");
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
@ -27,6 +27,7 @@ const { needBeConnected } = require("../../lang/"+lang+"/user");
// Principaux éléments du DOM manipulés :
const btnNewGroup = document.getElementById("wantNewGroup");
const btnPreviewGroup = document.getElementById("previewGroup");
const deleteCheckBox = document.getElementById("deleteOkLabel");
const divCrash = document.getElementById("crash");
const divGroupIntro = document.getElementById("groupIntro");
@ -41,8 +42,9 @@ const formSearch = document.getElementById("search");
const emptyGroupForm = () =>
{
empyForm(formGroup);
// Case de suppression inutile en mode création :
// Case de suppression et bouton visualisation inutiles en mode création :
deleteCheckBox.style.display="none";
btnPreviewGroup.style.display="none";
// Intro à vider !
divGroupIntro.innerHTML="";
}
@ -66,7 +68,7 @@ const showFormGroupInfos = (id, token) =>
GROUP_ID : response.Group.id,
DATE_CREA : dateFormat(response.Group.createdAt),
DATE_UPDATE : dateFormat(response.Group.updatedAt),
NB_ELEMENTS : (response.Group.Questionnaires!==undefined) ? response.Group.Questionnaires.length : 0
NB_ELEMENTS : (response.Questionnaires!==undefined) ? response.Questionnaires.length : 0
};
const groupIntro=replaceAll(infosGroupForAdmin, mapText);
addElement(divGroupIntro, "p", groupIntro, "", ["info"]);
@ -81,6 +83,13 @@ const showFormGroupInfos = (id, token) =>
}
}
deleteCheckBox.style.display="block";
btnPreviewGroup.style.display="block";
console.log(response.Group);
if(response.Group["isPublishable"] === false)
btnPreviewGroup.setAttribute("href", apiUrl+configQuestionnaires.groupRoutes+configQuestionnaires.previewGroupRoutes+"/"+id+"/"+token);
else
btnPreviewGroup.setAttribute("href", siteUrl+"/"+configQuestionnaires.dirWebGroups+"/"+response.Group["slug"]+".html");
console.log(btnPreviewGroup);
}// ajout gestion erreur 404 ???
}
}

100
front/src/manageQuestionnaires.js

@ -29,7 +29,7 @@ const { addOkMessage, deleteBtnTxt, serverError, updateBtnTxt } = require("../..
const { addIllustrationTxt, defaultAlt, introNoIllustration, introTitleForIllustration } = require("../../lang/"+lang+"/illustration");
const { addLinkTxt, defaultValueForLink, introNoLink, introTitleForLink } = require("../../lang/"+lang+"/link");
const { addQuestionTxt, introNoQuestion, introTitleForQuestion } = require("../../lang/"+lang+"/question");
const { nextDateWithoutQuestionnaire, nextQuestionnairesList, questionnaireNeedBeCompleted, searchQuestionnaireWithNoResult } = require("../../lang/"+lang+"/questionnaire");
const { needGroupIfRank, nextDateWithoutQuestionnaire, nextQuestionnairesList, questionnaireNeedBeCompleted, searchQuestionnaireWithNoResult } = require("../../lang/"+lang+"/questionnaire");
const { needBeConnected } = require("../../lang/"+lang+"/user");
// Principaux éléments du DOM manipulés :
@ -431,6 +431,8 @@ const showFormQuestionnaireInfos = (id, token) =>
}
formQuestionnaire.elements["classification"].value=classification;
}
if(!isEmpty(response.Group))
formQuestionnaire.elements["group"].value=response.Group.Group.title+" ("+response.Group.Group.id+")";
divLinks.style.display="block";
divQuestions.style.display="block";
divIllustrations.style.display="block";
@ -650,59 +652,65 @@ const initialise = async () =>
e.preventDefault();
divResponse.innerHTML="";
let datas=getDatasFromInputs(formQuestionnaire);
console.log(datas);
const xhrQuestionnaireDatas = new XMLHttpRequest();
if(!isEmpty(datas.id) && (datas.deleteOk!==undefined))
xhrQuestionnaireDatas.open("DELETE", apiUrl+configQuestionnaires.questionnaireRoutes+"/"+datas.id);
else if(!isEmpty(datas.id))
xhrQuestionnaireDatas.open("PUT", apiUrl+configQuestionnaires.questionnaireRoutes+"/"+datas.id);
if(!isEmpty(datas.rankInGroup) && isEmpty(datas.GroupId))
addElement(divResponse, "p", needGroupIfRank, "", ["error"]);
else
xhrQuestionnaireDatas.open("POST", apiUrl+configQuestionnaires.questionnaireRoutes+"/");
xhrQuestionnaireDatas.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
if(isEmpty(datas.rankInGroup) && !isEmpty(datas.GroupId))
datas.rankInGroup=1;
const xhrQuestionnaireDatas = new XMLHttpRequest();
if(!isEmpty(datas.id) && (datas.deleteOk!==undefined))
xhrQuestionnaireDatas.open("DELETE", apiUrl+configQuestionnaires.questionnaireRoutes+"/"+datas.id);
else if(!isEmpty(datas.id))
xhrQuestionnaireDatas.open("PUT", apiUrl+configQuestionnaires.questionnaireRoutes+"/"+datas.id);
else
xhrQuestionnaireDatas.open("POST", apiUrl+configQuestionnaires.questionnaireRoutes+"/");
xhrQuestionnaireDatas.onreadystatechange = function()
{
let response=JSON.parse(this.responseText);
if (this.status === 201 && response.id!=undefined)
{
addElement(divResponse, "p", addOkMessage, "", ["success"]);
datas.id=response.id;
showNextQuestionnaires(user.token);// peut avoir évolué suivant ce qui s'est passé
}
else if (this.status === 200 && response.message!=undefined)
{
if(Array.isArray(response.message))
response.message = response.message.join("<br>");
else
response.message = response.message;
addElement(divResponse, "p", response.message, "", ["success"]);
showNextQuestionnaires(user.token);// peut avoir évolué suivant ce qui s'est passé
}
else if (response.errors)
if (this.readyState == XMLHttpRequest.DONE)
{
if(Array.isArray(response.errors))
response.errors = response.errors.join("<br>");
let response=JSON.parse(this.responseText);
if (this.status === 201 && response.id != undefined)
{
addElement(divResponse, "p", addOkMessage, "", ["success"]);
datas.id=response.id;
showNextQuestionnaires(user.token);// peut avoir évolué suivant ce qui s'est passé
}
else if (this.status === 200 && response.message != undefined)
{
if(Array.isArray(response.message))
response.message = response.message.join("<br>");
else
response.message = response.message;
addElement(divResponse, "p", response.message, "", ["success"]);
showNextQuestionnaires(user.token);// peut avoir évolué suivant ce qui s'est passé
}
else if (response.errors)
{
if(Array.isArray(response.errors))
response.errors = response.errors.join("<br>");
else
response.errors = serverError;
addElement(divResponse, "p", response.errors, "", ["error"]);
}
else
response.errors = serverError;
addElement(divResponse, "p", response.errors, "", ["error"]);
}
else
addElement(divResponse, "p", serverError, "", ["error"]);
if(datas.deleteOk===undefined)
showFormQuestionnaireInfos(datas.id, user.token);
else
{
formQuestionnaire.reset();
divLinks.innerHTML="";
divIllustrations.innerHTML="";
divQuestions.innerHTML="";
addElement(divResponse, "p", serverError, "", ["error"]);
if(datas.deleteOk === undefined && response.errors === undefined)
showFormQuestionnaireInfos(datas.id, user.token);// on actualise les données
else if (response.errors === undefined)
{
formQuestionnaire.reset();
divLinks.innerHTML="";
divIllustrations.innerHTML="";
divQuestions.innerHTML="";
}
}
}
xhrQuestionnaireDatas.setRequestHeader("Content-Type", "application/json");
xhrQuestionnaireDatas.setRequestHeader("Authorization", "Bearer "+user.token);
if(datas)
xhrQuestionnaireDatas.send(JSON.stringify(datas));
}
xhrQuestionnaireDatas.setRequestHeader("Content-Type", "application/json");
xhrQuestionnaireDatas.setRequestHeader("Authorization", "Bearer "+user.token);
if(datas)
xhrQuestionnaireDatas.send(JSON.stringify(datas));
});
formLink.addEventListener("submit", function(e)
{

1
front/webpack.config.js

@ -11,6 +11,7 @@ module.exports =
deconnection: "./src/deconnection.js",
deleteValidation: "./src/deleteValidation.js",
group: "./src/group.js",
groupElement: "./src/groupElement.js",
homeManager: "./src/homeManager.js",
homeUser: "./src/homeUser.js",
index: "./src/index.js",

6
lang/fr/group.js

@ -7,8 +7,8 @@ module.exports =
groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues
haveBeenPublished: "#NB nouveaux groupes de quizs ont été publiés.",
infosGroupForAdmin: "Ce groupe de quizs a été créé le DATE_CREA, mise à jour la dernière fois le DATE_UPDATE.<br>Son identifiant est <b>GROUP_ID</b>. Il regroupe actuellement les questions de NB_ELEMENTS quizs.",
linkFirstElementGroup: "Retour à la première page.",
lastUpdated: "Dernière mise à jour, le ",
linkFirstElementGroup: "Retour à la première leçon.",
lastUpdated: "Dernière mise à jour le ",
needCorrectPublishingDate: "La date de publication fournie n'a pas un format valide.",
needLanguage: "Vous devez sélectionner la langue de ce groupe de quizs.",
needNotTooLongTitle: "Le titre du groupe de quizs ne doit pas compter plus de 255 caractères.",
@ -16,7 +16,7 @@ module.exports =
needUniqueUrl: "L'url du groupe de quizs doit être unique.",
needUrl: "Merci de fournir l'url à ce groupe de quizs.",
notFound: "Le groupe de quizs (#SEARCH) n'a pas été trouvé.",
publishedAt: ", le",
publishedAt: " le",
publishedBy: "Quiz publié par",