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

This commit is contained in:
Fabrice PENHOËT 2020-10-20 11:01:52 +02:00
parent 9a4c4aa122
commit e092c31e43
19 changed files with 785 additions and 281 deletions

View File

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

View File

@ -9,7 +9,8 @@ const questionnaireCtrl = require("./questionnaire");
const txt = require("../lang/"+config.adminLang+"/choice"); const txt = require("../lang/"+config.adminLang+"/choice");
const txtQuestion = require("../lang/"+config.adminLang+"/question"); 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) => exports.create = async (req, res, next) =>
{ {
try try
@ -51,8 +52,8 @@ exports.create = async (req, res, next) =>
for(let i in choices) for(let i in choices)
await db["Choice"].create(choices[i], { fields: ["text", "isCorrect", "QuestionId"] }); 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 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 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 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 }); res.status(201).json({ message: txtQuestion.addOkMessage , questionnaire: questionnaire });
} }
next(); 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 }); await db["Choice"].update(choicesUpdated[i], { where: { id: choicesUpdated[i].id } , fields: ["text", "isCorrect"], limit:1 });
for(let i in choicesAdded) for(let i in choicesAdded)
await db["Choice"].create(choicesAdded[i], { fields: ["text", "isCorrect", "QuestionId"] }); await db["Choice"].create(choicesAdded[i], { fields: ["text", "isCorrect", "QuestionId"] });
question=await questionCtrl.creaQuestionJson(req.params.id);// attendre pour pouvoir tout retourner 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 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 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 }); res.status(200).json({ message: txtQuestion.updateOkMessage , questionnaire: questionnaire });
} }
next(); next();

View File

@ -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 // Retourne les statistiques concernant les groupes de questionnaires
exports.getStats = async(req, res, next) => exports.getStats = async(req, res, next) =>
{ {
@ -218,15 +250,18 @@ const creaGroupJson = async (id) =>
if(Group) if(Group)
{ {
let datas={ 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) if(Questionnaires)
datas.Questionnaires=Questionnaires; datas.Questionnaires=Questionnaires;
await toolFile.createJSON(configQuestionnaires.dirCacheGroups, id, datas); 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)) 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 else // dans le cas contraire, on supprime l'éventuel fichier préexistant
toolFile.deleteFile(config.dirHTMLGroups, Group.slug+".html"); 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 : // + mise à jour des statistiques :
creaStatsGroupsJson(); creaStatsGroupsJson();
return datas; return datas;
@ -258,15 +293,15 @@ const checkGroupIsPublishable = (datas, checkDate=true) =>
const creaGroupHTML = async (id, preview = false) => 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); const group=await searchGroupById(id, true);
if(!group) if(!group)
return false; return false;
if(checkGroupIsPublishable(group)===false && preview===false) if(group.isPublishable === false && preview === false)
return false; return false;
const txtIllustration = require("../lang/"+config.adminLang+"/illustration");
const compiledFunction = pug.compileFile("./views/"+config.theme+"/quiz-group.pug"); const compiledFunction = pug.compileFile("./views/"+config.theme+"/quiz-group.pug");
const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js"); 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, config: config,
configQuestionnaires: configQuestionnaires, configQuestionnaires: configQuestionnaires,
@ -274,18 +309,19 @@ const creaGroupHTML = async (id, preview = false) =>
tool: tool, tool: tool,
txtGeneral: txtGeneral, txtGeneral: txtGeneral,
txtGroups: txtGroups, txtGroups: txtGroups,
pageLang: group.language, txtIllustration: txtIllustration,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(groupe.introduction.replace("<br>", " ").replace("</p>", " ")), 200), pageLang: group.Group.language,
author: group.CreatorName, metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(group.Group.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
pageTitle: txtGroups.groupsName+" "+group.title, author: group.Group.CreatorName,
contentTitle: group.title+"("+txtGroups.groupsName+")", pageTitle: txtGroups.groupsName+" "+group.Group.title,
contentTitle: group.Group.title+"("+txtGroups.groupsName+")",
group: group, group: group,
linkCanonical: config.siteUrl+"/"+config.dirWebGroups+"/"+group.slug+".html" linkCanonical: config.siteUrl+"/"+config.dirWebGroups+"/"+group.Group.slug+".html"
} }
const html=await compiledFunction(pageDatas); const html=await compiledFunction(pageDatas);
if(preview === false) if(preview === false)
{ {
await toolFile.createHTML(configQuestionnaires.dirHTMLGroups, group.slug, html); await toolFile.createHTML(configQuestionnaires.dirHTMLGroups, group.Group.slug, html);
return true; return true;
} }
else else
@ -300,6 +336,7 @@ const searchGroupById = async (id, reassemble=false) =>
group=await creaGroupJson(id); group=await creaGroupJson(id);
if(!group) if(!group)
return false; return false;
group.Group.isPublishable=checkGroupIsPublishable(group);
if(reassemble) if(reassemble)
{ {
let questionnaire; Questionnaires=[]; let questionnaire; Questionnaires=[];
@ -308,7 +345,7 @@ const searchGroupById = async (id, reassemble=false) =>
group.Group.CreatorName=author.User.name; group.Group.CreatorName=author.User.name;
for(let i in group.Questionnaires) 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) if(questionnaire)
Questionnaires.push(questionnaire); Questionnaires.push(questionnaire);
} }

View File

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

View File

@ -15,6 +15,7 @@ const toolError = require("../tools/error");
const toolFile = require("../tools/file"); const toolFile = require("../tools/file");
const toolMail = require("../tools/mail"); const toolMail = require("../tools/mail");
const groupCtrl = require("./group");
const questionCtrl = require("./question"); const questionCtrl = require("./question");
const illustrationCtrl = require("./illustration"); const illustrationCtrl = require("./illustration");
const tagCtrl = require("./tag"); const tagCtrl = require("./tag");
@ -29,13 +30,39 @@ exports.create = async (req, res, next) =>
{ {
try try
{ {
const db = require("../models/index"); if(tool.isEmpty(req.body.GroupId) && !tool.isEmpty(req.body.rankInGroup))
req.body.CreatorId=req.connectedUser.User.id; res.status(400).json({ errors : [txtQuestionnaire.needGroupIfRank] });
const questionnaire=await db["Questionnaire"].create({ ...req.body }, { fields: ["title", "slug", "introduction", "keywords", "publishingAt", "language", "estimatedTime", "CreatorId"] }); else
creaStatsQuestionnairesJson(); {
//utile au middleware suivant (classement tags) qui s'occupe aussi de retourner une réponse si ok : const db = require("../models/index");
req.body.QuestionnaireId=questionnaire.id; req.body.CreatorId=req.connectedUser.User.id;
next(); 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) 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) else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.CreatorId)
res.status(401).json({ errors: txtGeneral.notAllowed }); 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 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é 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; req.body.QuestionnaireId=req.params.id;
next(); next();
} }
@ -107,7 +149,7 @@ exports.delete = async (req, res, next) =>
exports.getOneQuestionnaireById = async (req, res, next) => exports.getOneQuestionnaireById = async (req, res, next) =>
{ {
try try
{ {
const datas=await searchQuestionnaireById(req.params.id, true); const datas=await searchQuestionnaireById(req.params.id, true);
if(datas) if(datas)
res.status(200).json(datas); res.status(200).json(datas);
@ -125,9 +167,9 @@ exports.showOneQuestionnaireById = async (req, res, next) =>
{ {
try 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); const connectedUser=await userCtrl.checkTokenUser(req.params.token);
if(connectedUser===false) if(connectedUser === false)
res.status(403).json({ errors:txtGeneral.failAuthToken }); res.status(403).json({ errors:txtGeneral.failAuthToken });
else else
{ {
@ -301,16 +343,15 @@ exports.getListNextQuestionnaires = async(req, res, next) =>
} }
} }
// test si des questionnaires doivent être publiés // Test si des questionnaires doivent être publiés puis (re)génère tous les fichiers HTML des questionnaires + les pages accueil + news
// 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
// la requête est ensuite passé aux tags qui font la même chose
exports.HTMLRegenerate= async (req, res, next) => exports.HTMLRegenerate= async (req, res, next) =>
{ {
try try
{ {
await checkQuestionnairesNeedToBePublished(); await checkQuestionnairesNeedToBePublished();
const nb=await checkQuestionnairesPublishedHaveHTML(true); 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); res.messageToNext=txtQuestionnaire.haveBeenRegenerated.replace("#NB1", nb);
next(); next();
} }
@ -368,7 +409,7 @@ exports.checkQuestionnairesNeedToBePublished= async (req, res, next) =>
const creaQuestionnaireJson = async (id) => const creaQuestionnaireJson = async (id) =>
{ {
const db = require("../models/index"); const db=require("../models/index");
const Questionnaire=await db["Questionnaire"].findByPk(id); const Questionnaire=await db["Questionnaire"].findByPk(id);
if(Questionnaire) if(Questionnaire)
{ {
@ -387,24 +428,36 @@ const creaQuestionnaireJson = async (id) =>
datas.Questions=Questions; datas.Questions=Questions;
const wasPublished=datas.Questionnaire.isPublished; const wasPublished=datas.Questionnaire.isPublished;
datas.Questionnaire.isPublished=checkQuestionnaireIsPublishable(datas); 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); 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 }); 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(); 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) if(wasPublished)
toolFile.deleteFile(config.dirHTMLQuestionnaires, Questionnaire.slug+".html"); toolFile.deleteFile(config.dirHTMLQuestionnaires, Questionnaire.slug+".html");
} }
// peut impacter la liste des derniers si des informations affichées ont changé // Peut impacter la liste des derniers quizs si des informations affichées ont changé
creaNewQuestionnairesJson();// peut avoir été impacté creaNewQuestionnairesJson();
// + les listes de quizs / tags : // + les listes de quizs / tags :
for(let i in Tags) for(let i in Tags)
tagCtrl.creaQuestionnairesTagJson(Tags[i].TagId) // ! Json + HTML, donc potentiellement long. tagCtrl.creaQuestionnairesTagJson(Tags[i].TagId) // ! Json + HTML, donc potentiellement long.
if(datas.Questionnaire.isPublished) if(datas.Questionnaire.isPublished)
await creaQuestionnaireHTML(id); 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; return datas;
} }
else else
@ -440,18 +493,29 @@ const deleteQuestionnaire = async (id, req) =>
for(let i in questionnaire.Illustrations) for(let i in questionnaire.Illustrations)
await illustrationCtrl.deleteIllustrationById(questionnaire.Illustrations[i].id); await illustrationCtrl.deleteIllustrationById(questionnaire.Illustrations[i].id);
const nb=await db["Questionnaire"].destroy( { where: { id : id }, limit:1 }); 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); toolFile.deleteJSON(configQuestionnaires.dirCacheQuestionnaires, id);
// + HTML : // + HTML :
toolFile.deleteFile(configQuestionnaires.dirHTMLQuestionnaires, questionnaire.Questionnaire.slug+".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. // Actualisation de liste des questionnaires pour les tags concernés.
// Ici au contraire, les enregistrements doivent être supprimés avant. // Ici au contraire, les enregistrements doivent être supprimés avant.
for(let i in questionnaire.Tags) for(let i in questionnaire.Tags)
tagCtrl.creaQuestionnairesTagJson(questionnaire.Tags[i].TagId); tagCtrl.creaQuestionnairesTagJson(questionnaire.Tags[i].TagId);
// La suppression peut éventuellement concerner un des derniers questionnaires, donc : // La suppression peut éventuellement concerner un des derniers questionnaires, donc :
creaNewQuestionnairesJson(); await creaNewQuestionnairesJson();
creaStatsQuestionnairesJson(); await creaStatsQuestionnairesJson();
return true; return true;
} }
} }
@ -473,42 +537,101 @@ const checkQuestionnaireIsPublishable = (datas, checkDate=true) =>
return false; 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 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; return false;
if(datas.Links==undefined || datas.Links.length < config.nbLinksMin) if(datas.Links === undefined || datas.Links.length < config.nbLinksMin)
return false; return false;
if(datas.Illustrations==undefined || datas.Illustrations.length < config.nbIllustrationsMin) if(datas.Illustrations === undefined || datas.Illustrations.length < config.nbIllustrationsMin)
return false; return false;
return true; return true;
} }
const creaQuestionnaireHTML = async (id, preview=false) => const creaQuestionnaireHTML = async (id, preview=false) =>
{ {
// besoin de toutes les infos concernant le questionnaire pour les transmettre à la vue // deux possibilités :
// à 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 // -- 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); const questionnaire=await searchQuestionnaireById(id, true);
if(!questionnaire) if(!questionnaire)
return false; return false;
if(questionnaire.Questionnaire.isPublished===false && preview===false) if(questionnaire.Questionnaire.isPublished===false && preview===false)
return 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 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= const pageDatas=
{ {
config: config, config: config,
configQuestionnaires: configQuestionnaires,
configTpl: configTpl, configTpl: configTpl,
tool: tool, tool: tool,
txtGeneral : txtGeneral, txtGeneral : txtGeneral,
txtQuestionnaire: txtQuestionnaire, txtQuestionnaire: txtQuestionnaire,
txtIllustration: txtIllustration, txtIllustration: txtIllustration,
txtUser: txtUser,
pageLang: questionnaire.Questionnaire.language, pageLang: questionnaire.Questionnaire.language,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(questionnaire.Questionnaire.introduction.replace("<br>", " ").replace("</p>", " ")), 200), metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(questionnaire.Questionnaire.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
author: questionnaire.Questionnaire.CreatorName, author: questionnaire.Questionnaire.CreatorName,
pageTitle: questionnaire.Questionnaire.title+" ("+txtQuestionnaire.questionnairesName+")", pageTitle: questionnaire.Questionnaire.title+" ("+txtQuestionnaire.questionnairesName+")",
contentTitle: questionnaire.Questionnaire.title, contentTitle: questionnaire.Questionnaire.title,
questionnaire: questionnaire, questionnaire: questionnaire,
group: groupInfos,
nextQuestionnaire: nextQuestionnaire,
linkCanonical: config.siteUrl+"/"+config.dirWebQuestionnaires+"/"+questionnaire.Questionnaire.slug+".html" linkCanonical: config.siteUrl+"/"+config.dirWebQuestionnaires+"/"+questionnaire.Questionnaire.slug+".html"
} }
const html=await compiledFunction(pageDatas); const html=await compiledFunction(pageDatas);
@ -530,22 +653,24 @@ const searchQuestionnaireById = async (id, reassemble=false) =>
return false; return false;
if(reassemble) if(reassemble)
{ {
let question; Questions=[];
const author=await userCtrl.searchUserById(questionnaire.Questionnaire.CreatorId); const author=await userCtrl.searchUserById(questionnaire.Questionnaire.CreatorId);
if(author) if(author)
questionnaire.Questionnaire.CreatorName=author.User.name; 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); questionDatas=await questionCtrl.searchQuestionById(questionnaire.Questions[i].id);
if(question) if(questionDatas)
Questions.push(question); questionsDatas.push(questionDatas);
} }
questionnaire.Questions=Questions; questionnaire.Questions=questionsDatas;
const tags=await tagCtrl.getTagsQuestionnaire(id); const tags=await tagCtrl.getTagsQuestionnaire(id);
if(tags) if(tags)
questionnaire.Tags=tags; questionnaire.Tags=tags;
} }
return questionnaire; return questionnaire;
} }
exports.searchQuestionnaireById = searchQuestionnaireById; exports.searchQuestionnaireById = searchQuestionnaireById;

View File

@ -62,6 +62,7 @@
<input type="hidden" name="id" id="id" value=""> <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"><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="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> <div id="response"></div>
</form> </form>
<div id="response"></div> <div id="response"></div>

View File

@ -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 /// Il n'est pas nécessaire d'être connecté pour répondre au quiz et voir son résultat.
/// Des ce cas il faut enregistrer son résultat en même temps que les informations de son compte /// 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 : // Fichier de configuration tirés du backend :
import { apiUrl, availableLangs, theme } from "../../config/instance.js"; import { apiUrl, availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0]; 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] import { checkAnswerOuput, saveAnswer } from "./tools/answers.js";
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 { addElement } from "./tools/dom.js";
import { helloDev } from "./tools/everywhere.js"; import { helloDev, updateAccountLink } from "./tools/everywhere.js";
import { getDatasFromInputs, setAttributesToInputs } from "./tools/forms.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 { loadMatomo } from "./tools/matomo.js";
import { checkAnswerDatas, checkSession, getPassword, getTimeDifference } from "./tools/users.js"; import { checkSession, getTimeDifference } from "./tools/users.js";
// Dictionnaires : // Dictionnaires :
const { notRequired, serverError } = require("../../lang/"+lang+"/general"); const { noPreviousAnswer, previousAnswersLine, previousAnswersStats, previousAnswersTitle, responseSavedError, wantToSaveResponses } = require("../../lang/"+lang+"/answer");
const { alreadyConnected, godfatherFound, godfatherNotFound, needUniqueEmail, passwordCopied } = require("../../lang/"+lang+"/user"); const { serverError } = require("../../lang/"+lang+"/general");
// Principaux éléments du DOM manipulés : // Principaux éléments du DOM manipulés :
const myForm=document.getElementById("subscription"); const myForm = document.getElementById("questionnaire");
const divResponse=document.getElementById("response"); const divResponse = document.getElementById("response");
const passwordInput=document.getElementById("password"); const btnShow = document.getElementById("showQuestionnaire");
const passwordLink=document.getElementById("getPassword"); const btnSubmit = document.getElementById("checkResponses");
const passwordHelp=document.getElementById("passwordMessage"); const explanationsTitle = document.getElementById("explanationsTitle");
const emailInput=document.getElementById("email"); const explanationsContent = document.getElementById("explanationsContent");
const btnSubmit=document.getElementById("submitDatas");
const codeGodfatherInput=document.getElementById("codeGodfather");
helloDev(); let isConnected, user;
// Test de connexion de l'utilisateur + affichage formulaire d'inscription.
const initialise = async () => const initialise = async () =>
{ {
try 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) if(isConnected)
{ {
saveLocaly("message", { message: alreadyConnected, color:"info" });// pour l'afficher sur la page suivante user=getLocaly("user", true);
const user=getLocaly("user", true); updateAccountLink(user.status, configTemplate);// lien vers le compte adapté pour les utilisateurs connectés
const homePage=user.status+"HomePage"; checkPreviousResponses(user);
window.location.assign("/"+configTemplate[homePage]);
} }
else else
{
loadMatomo(); loadMatomo();
setAttributesToInputs(configUsers, myForm);
myForm.style.display="block";
}
} }
catch(e) catch(e)
{ {
addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e); console.error(e);
} }
} }
initialise(); initialise();
helloDev();
// Générateur de mot de passe "aléatoire" // Affichage du questionnaire quand l'utilisateur clique sur le bouton ou si l'id du formulaire est passée par l'url.
passwordLink.addEventListener("click", function(e) // Déclenche en même temps le chronomètre mesurant la durée de la réponse aux questions.
const showQuestionnaire = () =>
{ {
e.preventDefault(); chronoBegin=Date.now();
passwordInput.type="text"; myForm.style.display="block";
passwordInput.value=getPassword(8, 12); btnShow.style.display="none";
// Copie du mot de passe généré dans le "presse-papier" de l'ordinateur : const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview".
passwordInput.select(); if(window.location.hash!=="")
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(); window.location.hash="";// ! le "#" reste
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.checkIfIsEmailfreeRoute); window.location.assign(here+"questionnaire");
xhr.onreadystatechange = function() }
{ else
if (this.readyState == XMLHttpRequest.DONE) window.location.assign(here+"#questionnaire");
{ }
let response=JSON.parse(this.responseText); let chronoBegin=0;
if (this.status === 200 && response.free!==undefined && response.free === false) btnShow.addEventListener("click", function(e)
{ {
addElement(document.getElementById("emailMessage"), "div", needUniqueEmail.replace("#URL", configTemplate.connectionPage), "", ["error"]); try
btnSubmit.setAttribute("disabled", true); {
} e.preventDefault();
else showQuestionnaire();
btnSubmit.removeAttribute("disabled"); }
} catch(e)
} {
xhr.setRequestHeader("Content-Type", "application/json"); addElement(divResponse, "p", serverError, "", ["error"]);
const datas={ emailTest:emailValue }; console.error(e);
xhr.send(JSON.stringify(datas));
} }
}); });
// Lien passé par mail pour voir directement le quiz
if(location.hash!="" && location.hash==="#questionnaire")
showQuestionnaire();
// Vérification que le code/e-mail de parrainage saisi est valide. // Traitement de l'envoi de la réponse de l'utilisateur :
codeGodfatherInput.addEventListener("focus", function(e) let answer = {};
{ // 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) myForm.addEventListener("submit", function(e)
{ {
try try
{ {
e.preventDefault(); e.preventDefault();
const xhr = new XMLHttpRequest(); btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :)
xhr.open("POST", apiUrl+configUsers.userRoutes+configUsers.subscribeRoute); divResponse.innerHTML="";// supprime les éventuels messages déjà affichés
xhr.onreadystatechange = function() 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); idChoice = item.substring(item.lastIndexOf("_") + 1);
if (this.status === 201) // si on change de queston
if(userResponses["question_id_response_"+idChoice]!=idQuestion) // on commence à traiter une nouvelle question
{ {
myForm.style.display="none"; idQuestion=userResponses["question_id_response_"+idChoice];
addElement(divResponse, "p", response.message, "", ["success"]); answer.nbQuestions++;
removeLocaly("lastAnswer");// ! important pour ne pas enregister plusieurs fois le résultat. 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>"); document.getElementById("response_"+idChoice).parentNode.classList.add("isCorrect");
addElement(divResponse, "p", response.errors, "", ["error"]); if(userResponses["response_"+idChoice]===undefined)// une bonne réponse n'a pas été sélectionnée
goodResponse=false;
} }
else 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"); // si j'ai bien répondu à la dernière question, il faut le compter ici, car je suis sorti de la boucle :
let datas=getDatasFromInputs(myForm); if(goodResponse)
if(datas) 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 est connecté, on enregistre son résultat sur le serveur.
// si l'utilisateur a précédement répondu à un quiz, j'ajoute les infos de son résultat : const xhrSaveAnswer = new XMLHttpRequest();
datas=checkAnswerDatas(datas); xhrSaveAnswer.open("POST", apiUrl+questionnaireRoutes+saveAnswersRoute);
xhr.send(JSON.stringify(datas)); 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) catch(e)
@ -178,4 +200,57 @@ myForm.addEventListener("submit", function(e)
addElement(divResponse, "p", serverError, "", ["error"]); addElement(divResponse, "p", serverError, "", ["error"]);
console.error(e); 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 Normal file
View File

@ -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);
}
});

View File

@ -6,7 +6,7 @@
/// Si pas d'id passé par l'url, on affiche un formulaire vide permettant d'en saisir un nouveau. /// Si pas d'id passé par l'url, on affiche un formulaire vide permettant d'en saisir un nouveau.
// Fichiers de configuration : // 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 lang=availableLangs[0];
const configQuestionnaires = require("../../config/questionnaires.js"); const configQuestionnaires = require("../../config/questionnaires.js");
const configTemplate = require("../../views/"+theme+"/config/"+lang+".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 : // Principaux éléments du DOM manipulés :
const btnNewGroup = document.getElementById("wantNewGroup"); const btnNewGroup = document.getElementById("wantNewGroup");
const btnPreviewGroup = document.getElementById("previewGroup");
const deleteCheckBox = document.getElementById("deleteOkLabel"); const deleteCheckBox = document.getElementById("deleteOkLabel");
const divCrash = document.getElementById("crash"); const divCrash = document.getElementById("crash");
const divGroupIntro = document.getElementById("groupIntro"); const divGroupIntro = document.getElementById("groupIntro");
@ -41,8 +42,9 @@ const formSearch = document.getElementById("search");
const emptyGroupForm = () => const emptyGroupForm = () =>
{ {
empyForm(formGroup); 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"; deleteCheckBox.style.display="none";
btnPreviewGroup.style.display="none";
// Intro à vider ! // Intro à vider !
divGroupIntro.innerHTML=""; divGroupIntro.innerHTML="";
} }
@ -66,7 +68,7 @@ const showFormGroupInfos = (id, token) =>
GROUP_ID : response.Group.id, GROUP_ID : response.Group.id,
DATE_CREA : dateFormat(response.Group.createdAt), DATE_CREA : dateFormat(response.Group.createdAt),
DATE_UPDATE : dateFormat(response.Group.updatedAt), 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); const groupIntro=replaceAll(infosGroupForAdmin, mapText);
addElement(divGroupIntro, "p", groupIntro, "", ["info"]); addElement(divGroupIntro, "p", groupIntro, "", ["info"]);
@ -81,6 +83,13 @@ const showFormGroupInfos = (id, token) =>
} }
} }
deleteCheckBox.style.display="block"; 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 ??? }// ajout gestion erreur 404 ???
} }
} }

View File

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

View File

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

View File

@ -7,8 +7,8 @@ module.exports =
groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues
haveBeenPublished: "#NB nouveaux groupes de quizs ont été publiés.", 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.", 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.", linkFirstElementGroup: "Retour à la première leçon.",
lastUpdated: "Dernière mise à jour, le ", lastUpdated: "Dernière mise à jour le ",
needCorrectPublishingDate: "La date de publication fournie n'a pas un format valide.", needCorrectPublishingDate: "La date de publication fournie n'a pas un format valide.",
needLanguage: "Vous devez sélectionner la langue de ce groupe de quizs.", 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.", 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.", needUniqueUrl: "L'url du groupe de quizs doit être unique.",
needUrl: "Merci de fournir l'url à ce groupe de quizs.", needUrl: "Merci de fournir l'url à ce groupe de quizs.",
notFound: "Le groupe de quizs (#SEARCH) n'a pas été trouvé.", notFound: "Le groupe de quizs (#SEARCH) n'a pas été trouvé.",
publishedAt: ", le", publishedAt: " le",
publishedBy: "Quiz publié par", publishedBy: "Quiz publié par",
searchIsNotLongEnough: "Vous devez saisir au moins #MIN caractères pour votre recherche.", searchIsNotLongEnough: "Vous devez saisir au moins #MIN caractères pour votre recherche.",
searchWithoutResult: "Aucun groupe n'a été trouvé pour votre recherche.", searchWithoutResult: "Aucun groupe n'a été trouvé pour votre recherche.",

View File

@ -17,8 +17,11 @@ module.exports =
haveBeenPublished : ":NB nouveaux questionnaires ont été publiés.", haveBeenPublished : ":NB nouveaux questionnaires ont été publiés.",
haveBeenRegenerated : "Les fichiers HTML de #NB1 questionnaires et #NB2 rubriques ont été regénérés.", haveBeenRegenerated : "Les fichiers HTML de #NB1 questionnaires et #NB2 rubriques ont été regénérés.",
lastUpdated: "Dernière mise à jour, le ", lastUpdated: "Dernière mise à jour, le ",
linkGoToNextElement: "Leçon suivante",
linkGoToQuiz: "Accéder au quiz",
needCorrectPublishingDate: "La date de publication fournie n'a pas un format valide.", needCorrectPublishingDate: "La date de publication fournie n'a pas un format valide.",
needEstimatedTime: "Merci de sélectionner une estimation de la durée de ce quiz.", needEstimatedTime: "Merci de sélectionner une estimation de la durée de ce quiz.",
needGroupIfRank: "Vous avez saisi un rang de classement, sans sélectionner le groupe du quiz.",
needIntroduction: "Merci de fournir un texte d'introduction à votre quiz.", needIntroduction: "Merci de fournir un texte d'introduction à votre quiz.",
needKnowIfIsPublished: "Il faut savoir si ce quiz est publié.", needKnowIfIsPublished: "Il faut savoir si ce quiz est publié.",
needLanguage: "Vous devez sélectionner la langue de ce quiz.", needLanguage: "Vous devez sélectionner la langue de ce quiz.",

View File

@ -120,6 +120,7 @@ module.exports = (sequelize, DataTypes) =>
{ {
type: DataTypes.INTEGER(2).UNSIGNED, allowNull: true, type: DataTypes.INTEGER(2).UNSIGNED, allowNull: true,
comment: "Allows you to classify the questionnaire if it belongs to a group.", comment: "Allows you to classify the questionnaire if it belongs to a group.",
set(value) { this.setDataValue("rankInGroup", tool.trimIfNotNull((value))); },
validate: validate:
{ {
isInt: { msg: txt.needNumberForRank }, isInt: { msg: txt.needNumberForRank },
@ -129,6 +130,11 @@ module.exports = (sequelize, DataTypes) =>
msg: txt.needNumberForRank msg: txt.needNumberForRank
} }
} }
},
GroupId:
{
type: DataTypes.INTEGER(11).UNSIGNED, allowNull: true,
set(value) { this.setDataValue("GroupId", tool.trimIfNotNull((value))); }
} }
}, },
{ {

View File

@ -11,5 +11,6 @@ router.get("/stats", auth, groupCtrl.getStatsGroups);
router.put("/:id", auth, groupCtrl.modify); router.put("/:id", auth, groupCtrl.modify);
router.delete("/:id", auth, groupCtrl.delete); router.delete("/:id", auth, groupCtrl.delete);
router.get("/get/:id", auth, groupCtrl.getOneById); router.get("/get/:id", auth, groupCtrl.getOneById);
router.get("/preview/:id/:token", groupCtrl.showOneGroupById);// prévisualisation HTML, même si groupe "incomplet"
module.exports = router; module.exports = router;

View File

@ -38,6 +38,7 @@ module.exports =
siteSlogan: "Cultivons notre jardin !", siteSlogan: "Cultivons notre jardin !",
noJSNotification: "Désolé, mais pour l'instant, l'utilisation de WikiLerni nécessite l'activation du JavaScript.", noJSNotification: "Désolé, mais pour l'instant, l'utilisation de WikiLerni nécessite l'activation du JavaScript.",
mailRecipientTxt: "Message envoyé à :", mailRecipientTxt: "Message envoyé à :",
licenceTxt: "@copyleft Le contenu de ce site <a href=\"/credits.html\" title=\"En savoir plus ?\">est libre</a> et vous offert sans publicité. Vous pouvez <a href=\"/participer-financement.html\" title=\"Financement participatif\">participer à son financement en cliquant ici</a>.",
/* Page d'accueil */ /* Page d'accueil */
homePageTxt: "Page d'accueil", homePageTxt: "Page d'accueil",
homeTitle1: "De nature curieuse ?", homeTitle1: "De nature curieuse ?",
@ -55,7 +56,8 @@ module.exports =
quizElementSubcriptionFormTitle: "Recevez les prochains WikiLerni", quizElementSubcriptionFormTitle: "Recevez les prochains WikiLerni",
explanationTitle: "Vous découvrez WikiLerni ?", explanationTitle: "Vous découvrez WikiLerni ?",
explanationTxt: "<p>Le principe est simple : <b>vous commencez par lire l'article Wikipédia dont le lien vous est proposé</b>.<br>Puis vous <b>afficher le quiz pour vérifier ce que vous avez retenu de votre lecture</b>.</p><p>Suivant les questions, <b>une ou plusieurs réponses peuvent être correctes</b> et doivent donc être cochées.<br>C'est toujours <b>le contenu de l'article Wikipédia qui fait foi</b> concernant les \"bonnes\" réponses.<br>Mais les articles de Wikipédia peuvent évoluer, donc n'hésitez pas <a href='/contact.html'>à me signaler une erreur</a>.</p><h3>Pas le temps de lire l'article Wikipédia ?</h3><p>Il est vrai que certains sont longs ! :-)<br>Dans ce cas, <b>essayez simplement de répondre avec vos connaissances actuelles</b>.<br>Il n'est pas nécessaire de répondre à toutes les questions pour obtenir les réponses.<br>Après validation, vous verrez apparaître les bonnes réponses + un extrait de l'article Wikipédia.<br>Vous pouvez ainsi <b>apprendre de nouvelles choses en quelques minutes</b>.</p><p>Une autre possibilité est d'afficher le quiz avant d'aller chercher les réponses dans l'article Wikipédia... Elles y sont toutes !</p><p><b>Il n'y a pas de bonne façon de faire</b>, et dans tous les cas <b>vous apprenez des choses sur des sujets très variés, ce qui est le but de WikiLerni</b>.</p><p>Quand le sujet s'y prête, ne vous étonnez pas si certaines des réponses proposées peuvent être un peu décalées, absurdes... On peut apprendre avec le sourire, non ? :)</p><p>Une fois votre résultat obtenu, il vous sera proposé de créer un compte pour le sauvegarder. Ce compte vous permettra de <b>tester de nouveau ce quiz</b> pour vérifier ce que vous en avez retenu après plusieurs jours, semaines, mois... Grâce à ce compte, vous pourrez aussi <b>recevoir régulièrement de nouveaux quizs</b> pour continuer à \"cultiver votre jardin\".</p>", explanationTxt: "<p>Le principe est simple : <b>vous commencez par lire l'article Wikipédia dont le lien vous est proposé</b>.<br>Puis vous <b>afficher le quiz pour vérifier ce que vous avez retenu de votre lecture</b>.</p><p>Suivant les questions, <b>une ou plusieurs réponses peuvent être correctes</b> et doivent donc être cochées.<br>C'est toujours <b>le contenu de l'article Wikipédia qui fait foi</b> concernant les \"bonnes\" réponses.<br>Mais les articles de Wikipédia peuvent évoluer, donc n'hésitez pas <a href='/contact.html'>à me signaler une erreur</a>.</p><h3>Pas le temps de lire l'article Wikipédia ?</h3><p>Il est vrai que certains sont longs ! :-)<br>Dans ce cas, <b>essayez simplement de répondre avec vos connaissances actuelles</b>.<br>Il n'est pas nécessaire de répondre à toutes les questions pour obtenir les réponses.<br>Après validation, vous verrez apparaître les bonnes réponses + un extrait de l'article Wikipédia.<br>Vous pouvez ainsi <b>apprendre de nouvelles choses en quelques minutes</b>.</p><p>Une autre possibilité est d'afficher le quiz avant d'aller chercher les réponses dans l'article Wikipédia... Elles y sont toutes !</p><p><b>Il n'y a pas de bonne façon de faire</b>, et dans tous les cas <b>vous apprenez des choses sur des sujets très variés, ce qui est le but de WikiLerni</b>.</p><p>Quand le sujet s'y prête, ne vous étonnez pas si certaines des réponses proposées peuvent être un peu décalées, absurdes... On peut apprendre avec le sourire, non ? :)</p><p>Une fois votre résultat obtenu, il vous sera proposé de créer un compte pour le sauvegarder. Ce compte vous permettra de <b>tester de nouveau ce quiz</b> pour vérifier ce que vous en avez retenu après plusieurs jours, semaines, mois... Grâce à ce compte, vous pourrez aussi <b>recevoir régulièrement de nouveaux quizs</b> pour continuer à \"cultiver votre jardin\".</p>",
questionnaireLicenceTxt: "Ce quiz <a href=\"/credits.html\">est libre</a>, mais il n'est pas gratuit. Vous pouvez <a href=\"/participer-financement.html\">participer à son financement en cliquant ici</a>.", explanationElementTxt: "<p>WikiLerni vous propose de recevoir par e-mail plusieurs cours tirés d'articles Wikipédia. À la fin, un quiz vous permettra de tester ce que vous avez retenu de votre lecture.</p>",
explanationGroupTxt: "<p>Ce quiz porte sur plusieurs leçons que vous avez pu lire précédemment. Si ce n'est pas le cas, utilisez le lien pour accéder au premier cours.</p>",
/* Autres */ /* Autres */
illustrationDir : "/img/quizs/", illustrationDir : "/img/quizs/",
twitterAccount: "WikiLerni", twitterAccount: "WikiLerni",

View File

@ -26,7 +26,7 @@ block content
const publishedAtTxt=tool.dateFormat(questionnaire.Questionnaire.publishingAt, questionnaire.Questionnaire.language); const publishedAtTxt=tool.dateFormat(questionnaire.Questionnaire.publishingAt, questionnaire.Questionnaire.language);
const updatedAtTxt=tool.dateFormat(questionnaire.Questionnaire.updatedAt, questionnaire.Questionnaire.language); const updatedAtTxt=tool.dateFormat(questionnaire.Questionnaire.updatedAt, questionnaire.Questionnaire.language);
if(questionnaire.Illustrations!=undefined && questionnaire.Illustrations.length!==0) if(questionnaire.Illustrations != undefined && questionnaire.Illustrations.length !== 0)
div(id="content-picture" class="cardboard") div(id="content-picture" class="cardboard")
div(style="background-image: url('/img/quizs/"+questionnaire.Illustrations[0].url+"');") div(style="background-image: url('/img/quizs/"+questionnaire.Illustrations[0].url+"');")
img(src="/img/quizs/"+questionnaire.Illustrations[0].url)&attributes(imgAttributes) img(src="/img/quizs/"+questionnaire.Illustrations[0].url)&attributes(imgAttributes)
@ -39,41 +39,62 @@ block content
h1(class="cardboard") h1(class="cardboard")
img(id="required-time-icon" src="/themes/wikilerni/img/time-required-"+questionnaire.Questionnaire.estimatedTime+".png" title=txtQuestionnaire.estimatedTime+" "+txtQuestionnaire.estimatedTimeOption[questionnaire.Questionnaire.estimatedTime]) img(id="required-time-icon" src="/themes/wikilerni/img/time-required-"+questionnaire.Questionnaire.estimatedTime+".png" title=txtQuestionnaire.estimatedTime+" "+txtQuestionnaire.estimatedTimeOption[questionnaire.Questionnaire.estimatedTime])
span #{questionnaire.Questionnaire.title} span #{questionnaire.Questionnaire.title}
if(questionnaire.Illustrations!=undefined && questionnaire.Illustrations.length!==0) if(questionnaire.Illustrations != undefined && questionnaire.Illustrations.length !== 0)
a(href="/img/quizs/"+questionnaire.Illustrations[0].url target="_blank" rel="noopener") a(href="/img/quizs/"+questionnaire.Illustrations[0].url target="_blank" rel="noopener")
img(src="/img/quizs/min/"+questionnaire.Illustrations[0].url class="thumb")&attributes(imgAttributes) img(src="/img/quizs/min/"+questionnaire.Illustrations[0].url class="thumb")&attributes(imgAttributes)
div(id="content-title-corner") div(id="content-title-corner")
div(id="content" class="cardboard") div(id="content" class="cardboard")
p(id="author-date") #{txtQuestionnaire.publishedBy} #{author}#{txtQuestionnaire.publishedAt} #{publishedAtTxt}. #{txtQuestionnaire.lastUpdated}#{updatedAtTxt}. p(id="author-date") #{txtQuestionnaire.publishedBy} #{author}#{txtQuestionnaire.publishedAt} #{publishedAtTxt}. #{txtQuestionnaire.lastUpdated}#{updatedAtTxt}.
//- Important : ici, on garde volontairement le html ! //- Important : ici, on garde volontairement le html !
if(questionnaire.Questionnaire.introduction) if(questionnaire.Questionnaire.introduction)
div#introduction !{questionnaire.Questionnaire.introduction} div#introduction !{questionnaire.Questionnaire.introduction}
div#links
h4 #{configTpl.quizElementLinksIntro}
ul#quizElementLinks
for link in questionnaire.Links
li
a(href=link.url target="_blank" rel="noopener" title=link.anchor+" ("+txtGeneral.alertNewWindow+")") #{link.anchor}
// - lien vers élément suivant ou quiz si dernier ? //- Les sources de l'article
//div#links if(questionnaire.Links != undefined && questionnaire.Links.length !== 0)
// a(href=link.url class="button cardboard" target="_blank" rel="noopener" title=link.anchor+" ("+txtGeneral.alertNewWindow+")") #{link.anchor} div#elementLinks
h4 #{configTpl.quizElementLinksIntro}
ul#quizElementLinks
for link in questionnaire.Links
li
a(href=link.url target="_blank" rel="noopener" title=link.anchor+" ("+txtGeneral.alertNewWindow+")") #{link.anchor}
// Lien vers l'élément suivant ou le quiz du groupe, si je suis arrivé à la fin :
-
let nextLink={};
if(nextQuestionnaire != null && nextQuestionnaire.Questionnaire.isPublished)
{
nextLink.href="/"+configQuestionnaires.dirWebQuestionnaires+"/"+nextQuestionnaire.Questionnaire.slug+".html";
nextLink.title=nextQuestionnaire.Questionnaire.title;
nextLink.anchor=txtQuestionnaire.linkGoToNextElement;
}
else if(group.Group.isPublishable)
{
nextLink.href="/"+configQuestionnaires.dirWebGroups+"/"+group.Group.slug+".html";
nextLink.title=group.Group.title;
nextLink.anchor=txtQuestionnaire.linkGoToQuiz;
}
if(nextLink.href!=undefined)
div#nextLink
a(href=nextLink.href class="button cardboard" title=nextLink.title) !{"&#10132; "+nextLink.anchor}
div#licence div#licence
p !{configTpl.questionnaireLicenceTxt} p !{configTpl.licenceTxt}
noscript noscript
div div
strong #{configTpl.noJSNotification} strong #{configTpl.noJSNotification}
// Formulaire d'inscription :
- -
const cguOkLabel = txtUsers.formsEmailLabel.replace("#link", "/"+configTpl.cguPage); const cguOkLabel = txtUser.formsEmailLabel.replace("#link", "/"+configTpl.cguPage); /// remettre class="needJS" au formulaire ci-dessous
div#signup div#signupForm
form(id="subscription" method="POST" class="needJS") form(id="subscription" method="POST")
h3 #{configTpl.quizElementSubcriptionFormTitle} h3 #{configTpl.quizElementSubcriptionFormTitle}
fieldset fieldset
label(for="email") #{txtUsers.formsEmailLabel} label(for="email") #{txtUser.formsEmailLabel}
input(id="email" type="email" name="email" placeholder=txtUsers.formsEmailPlaceholder class="cardboard") input(id="email" type="email" name="email" placeholder=txtUser.formsEmailPlaceholder class="cardboard")
div#emailMessage div#emailMessage
ul(class="checkbox_li") ul(class="checkbox_li")
li(class="checkbox_li") li(class="checkbox_li")
@ -82,7 +103,7 @@ block content
div(class="checkbox_override") div(class="checkbox_override")
span #{cguOkLabel} span #{cguOkLabel}
div(class="input_wrapper") div(class="input_wrapper")
input(id="submitDatas" type="submit" value=txtUsers.formsSubmitTxt class="cardboard") input(id="submitDatas" type="submit" value=txtUser.formsSubmitTxt class="cardboard")
div(id="response") div(id="response")
div#zerozozio div#zerozozio
@ -96,4 +117,4 @@ block content
div#explanations(class="engraved framed") div#explanations(class="engraved framed")
h3#explanationsTitle #{configTpl.explanationTitle} h3#explanationsTitle #{configTpl.explanationTitle}
div#explanationsContent div#explanationsContent
p !{configTpl.explanationTxt} p !{configTpl.explanationElementTxt}

View File

@ -5,30 +5,62 @@ block append scripts
script(src="/JS/group.app.js" defer) script(src="/JS/group.app.js" defer)
block content block content
// Il n'y a pas d'illustration spécifique au groupe, mais je reprends celle du premier élément du groupe de quizs
-
const imgAttributes = { alt: txtIllustration.defaultAlt, style: "opacity: 0.0;" };
if(group.Questionnaires[0].Illustrations!=undefined && group.Questionnaires[0].Illustrations.length!==0)
{
if (tool.isEmpty(group.Questionnaires[0].Illustrations[0].alt)===false)
imgAttributes.alt=group.Questionnaires[0].Illustrations[0].alt;
if(tool.isEmpty(group.Questionnaires[0].Illustrations[0].title)===false)
imgAttributes.title=group.Questionnaires[0].Illustrations[0].title;
}
const publishedAtTxt=tool.dateFormat(group.Group.publishingAt, group.Group.language);
const updatedAtTxt=tool.dateFormat(group.Group.updatedAt, group.Group.language);
if(group.Questionnaires[0].Illustrations!=undefined && group.Questionnaires[0].Illustrations.length!==0)
div(id="content-picture" class="cardboard")
div(style="background-image: url('/img/quizs/"+group.Questionnaires[0].Illustrations[0].url+"');")
img(src="/img/quizs/"+group.Questionnaires[0].Illustrations[0].url)&attributes(imgAttributes)
//- Important : ici, on garde volontairement le html saisi car lien possible vers auteur de l'illustration :
if(group.Questionnaires[0].Illustrations[0].caption)
p !{group.Questionnaires[0].Illustrations[0].caption}
div(id="content-side") div(id="content-side")
div(id="content-title") div(id="content-title")
h1(class="cardboard") h1(class="cardboard")
span #{group.title} img(id="required-time-icon" src="/themes/wikilerni/img/time-required-medium.png")
span #{group.Group.title}
if(group.Questionnaires[0].Illustrations!=undefined && group.Questionnaires[0].Illustrations.length!==0)
a(href="/img/quizs/"+group.Questionnaires[0].Illustrations[0].url target="_blank" rel="noopener")
img(src="/img/quizs/min/"+group.Questionnaires[0].Illustrations[0].url class="thumb")&attributes(imgAttributes)
div(id="content-title-corner") div(id="content-title-corner")
div(id="content" class="cardboard") div(id="content" class="cardboard")
p(id="author-date") #{txtGroups.publishedBy} #{author}#{txtGroups.publishedAt} #{publishedAtTxt}. #{txtGroups.lastUpdated}#{updatedAtTxt}. p(id="author-date") #{txtGroups.publishedBy} #{author} #{txtGroups.publishedAt} #{publishedAtTxt}. #{txtGroups.lastUpdated} #{updatedAtTxt}.
div#introduction //- Important : ici, on garde volontairement le html, car cela est accepté pour l'introduction.
if(group.introduction)
div !{group.introduction}//- Important : ici, on garde volontairement le html, car cela est accepté pour l'introduction. div#introduction
div txtGroups.commonIntroTxt if(group.Group.introduction)
// - lien vers premier élément du groupe div !{group.Group.introduction}
div #{txtGroups.commonIntroTxt}
// - lien vers premier élément du groupe (html autorisé pour permettre les symboles unicodes)
div#links div#links
a(href=="/"+configQuestionnaires.dirdirWebQuestionnaires+group.Questionnaires[0].slug+".html" class="button cardboard" title=txtGroups.linkFirstElementGroup) #{txtGroups.linkFirstElementGroup} a(href="/"+configQuestionnaires.dirWebQuestionnaires+"/"+group.Questionnaires[0].Questionnaire.slug+".html" class="button cardboard" title=group.Questionnaires[0].Questionnaire.title) !{"&#8592; "+txtGroups.linkFirstElementGroup}
div#licence div#licence
p !{configTpl.groupLicenceTxt} p !{configTpl.licenceTxt}
noscript noscript
div div
strong #{configTpl.noJSNotification} strong #{configTpl.noJSNotification}
form(id="group" method="POST" class="needJS") // à cacher si pas de JS !
h2 #{group.title} form(id="group" method="POST")
h2 #{group.Group.title}
div#response div#response
div(class="subscribeBtns") div(class="subscribeBtns")
p p
@ -39,7 +71,7 @@ block content
for question in questionnaire.Questions for question in questionnaire.Questions
p(id="question_"+question.Question.id) #{question.Question.text} p(id="question_"+question.Question.id) #{question.Question.text}
if(question.Question.explanation) if(question.Question.explanation)
blockquote(class="help" id="help_"+question.Question.id cite=group.Links[0].url) #{txtexplanationBeforeTxt} #{question.Question.explanation} blockquote(class="help" id="help_"+question.Question.id cite=questionnaire.Links[0].url) #{txtexplanationBeforeTxt} #{question.Question.explanation}
ul(class="checkbox_li") ul(class="checkbox_li")
for response in question.Choices for response in question.Choices
li(class="checkbox_li") li(class="checkbox_li")
@ -53,20 +85,20 @@ block content
em #{response.text} em #{response.text}
input(type="hidden" name="isCorrect_response_"+response.id id="isCorrect_response_"+response.id value=""+response.isCorrect) input(type="hidden" name="isCorrect_response_"+response.id id="isCorrect_response_"+response.id value=""+response.isCorrect)
input(type="hidden" name="question_id_response_"+response.id id="question_id_response_"+response.id value=question.Question.id) input(type="hidden" name="question_id_response_"+response.id id="question_id_response_"+response.id value=question.Question.id)
input(name="groupId" id="groupId" value=group.id type="hidden") input(name="groupId" id="groupId" value=group.Group.id type="hidden")
p p
span(class="input_wrapper") span(class="input_wrapper")
input(id="checkResponses" type="submit" value=txtGroups.btnSendResponse class="cardboard" title=txtGroups.btnSendResponse) input(id="checkResponses" type="submit" value=txtGroups.btnSendResponse class="cardboard" title=txtGroups.btnSendResponse)
div#zerozozio div#zerozozio
a(href="http://sharetodiaspora.github.io/?url="+linkCanonical+"&title="+group.title rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" diaspora* ("+txtGeneral.alertNewWindow+")" target="_blank") a(href="http://sharetodiaspora.github.io/?url="+linkCanonical+"&title="+group.Group.title rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" diaspora* ("+txtGeneral.alertNewWindow+")" target="_blank")
img(src="/themes/wikilerni/img/diaspora.png" alt=txtGroups.btnShareQuizTxt+" diaspora*") img(src="/themes/wikilerni/img/diaspora.png" alt=txtGroups.btnShareQuizTxt+" diaspora*")
a(href="https://www.facebook.com/sharer.php?u="+linkCanonical rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" facebook ("+txtGeneral.alertNewWindow+")" target="_blank") a(href="https://www.facebook.com/sharer.php?u="+linkCanonical rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" facebook ("+txtGeneral.alertNewWindow+")" target="_blank")
img(src="/themes/wikilerni/img/facebook.png" alt=txtGroups.btnShareQuizTxt+" facebook") img(src="/themes/wikilerni/img/facebook.png" alt=txtGroups.btnShareQuizTxt+" facebook")
a(href="https://twitter.com/intent/tweet?url="+linkCanonical+"&text="+group.title+" via @"+configTpl.twitterAccount rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" twitter ("+txtGeneral.alertNewWindow+")" target="_blank") a(href="https://twitter.com/intent/tweet?url="+linkCanonical+"&text="+group.Group.title+" via @"+configTpl.twitterAccount rel="nofollow noopener" title=txtGroups.btnShareQuizTxt+" twitter ("+txtGeneral.alertNewWindow+")" target="_blank")
img(src="/themes/wikilerni/img/twitter.png" alt=txtGroups.btnShareQuizTxt+" twitter") img(src="/themes/wikilerni/img/twitter.png" alt=txtGroups.btnShareQuizTxt+" twitter")
div#explanations(class="engraved framed") div#explanations(class="engraved framed")
h3#explanationsTitle #{configTpl.explanationTitle} h3#explanationsTitle #{configTpl.explanationTitle}
div#explanationsContent div#explanationsContent
p !{configTpl.explanationTxt} p !{configTpl.explanationGroupTxt}

View File

@ -50,7 +50,7 @@ block content
if(questionnaire.Questionnaire.introduction) if(questionnaire.Questionnaire.introduction)
div#introduction !{questionnaire.Questionnaire.introduction} div#introduction !{questionnaire.Questionnaire.introduction}
div#licence div#licence
p !{configTpl.questionnaireLicenceTxt} p !{configTpl.licenceTxt}
div#links div#links
for link in questionnaire.Links for link in questionnaire.Links