diff --git a/config/questionnaires.js b/config/questionnaires.js index 590cce9..2d926c5 100644 --- a/config/questionnaires.js +++ b/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 : diff --git a/controllers/choice.js b/controllers/choice.js index c5bf6c8..18fc21f 100644 --- a/controllers/choice.js +++ b/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(); diff --git a/controllers/group.js b/controllers/group.js index 118754e..c2f19f5 100644 --- a/controllers/group.js +++ b/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("
", " ").replace("

", " ")), 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("
", " ").replace("

", " ")), 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); } diff --git a/controllers/question.js b/controllers/question.js index d5b2ba5..5fcd4da 100644 --- a/controllers/question.js +++ b/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; diff --git a/controllers/questionnaire.js b/controllers/questionnaire.js index a036706..f0518b4 100644 --- a/controllers/questionnaire.js +++ b/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("
", " ").replace("

", " ")), 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("
", " ").replace("

", " ")), 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; diff --git a/front/public/gestion-groups.html b/front/public/gestion-groups.html index 37bc3d9..878965c 100644 --- a/front/public/gestion-groups.html +++ b/front/public/gestion-groups.html @@ -62,6 +62,7 @@
Vider
+
Voir le quiz
diff --git a/front/src/group.js b/front/src/group.js index 27c5a07..dcf7de6 100644 --- a/front/src/group.js +++ b/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"); +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"); -helloDev(); - -// 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) +// 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 = () => { - 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!=="") + 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"); + } + else + window.location.assign(here+"#questionnaire"); +} +let chronoBegin=0; +btnShow.addEventListener("click", function(e) +{ + try + { + 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(); -// 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 : +// 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("
"); - 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+="
"+xhrResponse.message.replace("#URL", configTemplate.userHomePage); + checkPreviousResponses(user); + } + else + getOuput+="
"+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+="

"+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); } -}); \ No newline at end of file +}) + +// 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+="
  • "+replaceAll(previousAnswersLine, mapLineContent)+"
  • "; + } + mapLineContent = + { + AVGDURATION : Math.round(totDuration/nbResponses), + AVGCORRECTANSWERS : Math.round(totNbCorrectAnswers/totNbQuestions*100) + }; + previousAnswersContent="
    "+replaceAll(previousAnswersStats, mapLineContent)+"
    "+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", ""+configTemplate.userHomePageTxt+"", "", ["btn"], "", false); + + } + } + } + xhrPreviousRes.setRequestHeader("Authorization", "Bearer "+user.token); + xhrPreviousRes.send(); +} \ No newline at end of file diff --git a/front/src/groupElement.js b/front/src/groupElement.js new file mode 100644 index 0000000..1201d90 --- /dev/null +++ b/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("
    "); + 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); + } +}); \ No newline at end of file diff --git a/front/src/manageGroups.js b/front/src/manageGroups.js index 995fa8b..6504905 100644 --- a/front/src/manageGroups.js +++ b/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 ??? } } diff --git a/front/src/manageQuestionnaires.js b/front/src/manageQuestionnaires.js index 053dedb..e475c5f 100644 --- a/front/src/manageQuestionnaires.js +++ b/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) + if (this.readyState == XMLHttpRequest.DONE) { - 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("
    "); + 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("
    "); + 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("
    "); + else + response.errors = serverError; + addElement(divResponse, "p", response.errors, "", ["error"]); + } 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("
    "); - 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) { diff --git a/front/webpack.config.js b/front/webpack.config.js index ce70721..ced0aa8 100644 --- a/front/webpack.config.js +++ b/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", diff --git a/lang/fr/group.js b/lang/fr/group.js index a061128..49a7ae0 100644 --- a/lang/fr/group.js +++ b/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.
    Son identifiant est GROUP_ID. 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", searchIsNotLongEnough: "Vous devez saisir au moins #MIN caractères pour votre recherche.", searchWithoutResult: "Aucun groupe n'a été trouvé pour votre recherche.", diff --git a/lang/fr/questionnaire.js b/lang/fr/questionnaire.js index e7e17f6..25a7540 100644 --- a/lang/fr/questionnaire.js +++ b/lang/fr/questionnaire.js @@ -17,8 +17,11 @@ module.exports = haveBeenPublished : ":NB nouveaux questionnaires ont été publiés.", haveBeenRegenerated : "Les fichiers HTML de #NB1 questionnaires et #NB2 rubriques ont été regénérés.", 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.", 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.", needKnowIfIsPublished: "Il faut savoir si ce quiz est publié.", needLanguage: "Vous devez sélectionner la langue de ce quiz.", diff --git a/models/Questionnaire.js b/models/Questionnaire.js index 1f6cfa3..e638716 100644 --- a/models/Questionnaire.js +++ b/models/Questionnaire.js @@ -120,6 +120,7 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.INTEGER(2).UNSIGNED, allowNull: true, comment: "Allows you to classify the questionnaire if it belongs to a group.", + set(value) { this.setDataValue("rankInGroup", tool.trimIfNotNull((value))); }, validate: { isInt: { msg: txt.needNumberForRank }, @@ -129,6 +130,11 @@ module.exports = (sequelize, DataTypes) => msg: txt.needNumberForRank } } + }, + GroupId: + { + type: DataTypes.INTEGER(11).UNSIGNED, allowNull: true, + set(value) { this.setDataValue("GroupId", tool.trimIfNotNull((value))); } } }, { diff --git a/routes/group.js b/routes/group.js index b59d12f..c0b5b0e 100644 --- a/routes/group.js +++ b/routes/group.js @@ -11,5 +11,6 @@ router.get("/stats", auth, groupCtrl.getStatsGroups); router.put("/:id", auth, groupCtrl.modify); router.delete("/:id", auth, groupCtrl.delete); 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; \ No newline at end of file diff --git a/views/wikilerni/config/fr.js b/views/wikilerni/config/fr.js index d6623bb..1ff0051 100644 --- a/views/wikilerni/config/fr.js +++ b/views/wikilerni/config/fr.js @@ -38,6 +38,7 @@ module.exports = siteSlogan: "Cultivons notre jardin !", noJSNotification: "Désolé, mais pour l'instant, l'utilisation de WikiLerni nécessite l'activation du JavaScript.", mailRecipientTxt: "Message envoyé à :", + licenceTxt: "@copyleft Le contenu de ce site est libre et vous offert sans publicité. Vous pouvez participer à son financement en cliquant ici.", /* Page d'accueil */ homePageTxt: "Page d'accueil", homeTitle1: "De nature curieuse ?", @@ -55,7 +56,8 @@ module.exports = quizElementSubcriptionFormTitle: "Recevez les prochains WikiLerni", explanationTitle: "Vous découvrez WikiLerni ?", explanationTxt: "

    Le principe est simple : vous commencez par lire l'article Wikipédia dont le lien vous est proposé.
    Puis vous afficher le quiz pour vérifier ce que vous avez retenu de votre lecture.

    Suivant les questions, une ou plusieurs réponses peuvent être correctes et doivent donc être cochées.
    C'est toujours le contenu de l'article Wikipédia qui fait foi concernant les \"bonnes\" réponses.
    Mais les articles de Wikipédia peuvent évoluer, donc n'hésitez pas à me signaler une erreur.

    Pas le temps de lire l'article Wikipédia ?

    Il est vrai que certains sont longs ! :-)
    Dans ce cas, essayez simplement de répondre avec vos connaissances actuelles.
    Il n'est pas nécessaire de répondre à toutes les questions pour obtenir les réponses.
    Après validation, vous verrez apparaître les bonnes réponses + un extrait de l'article Wikipédia.
    Vous pouvez ainsi apprendre de nouvelles choses en quelques minutes.

    Une autre possibilité est d'afficher le quiz avant d'aller chercher les réponses dans l'article Wikipédia... Elles y sont toutes !

    Il n'y a pas de bonne façon de faire, et dans tous les cas vous apprenez des choses sur des sujets très variés, ce qui est le but de WikiLerni.

    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 ? :)

    Une fois votre résultat obtenu, il vous sera proposé de créer un compte pour le sauvegarder. Ce compte vous permettra de tester de nouveau ce quiz pour vérifier ce que vous en avez retenu après plusieurs jours, semaines, mois... Grâce à ce compte, vous pourrez aussi recevoir régulièrement de nouveaux quizs pour continuer à \"cultiver votre jardin\".

    ", - questionnaireLicenceTxt: "Ce quiz est libre, mais il n'est pas gratuit. Vous pouvez participer à son financement en cliquant ici.", + explanationElementTxt: "

    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.

    ", + explanationGroupTxt: "

    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.

    ", /* Autres */ illustrationDir : "/img/quizs/", twitterAccount: "WikiLerni", diff --git a/views/wikilerni/quiz-element.pug b/views/wikilerni/quiz-element.pug index 52368b0..cb16014 100644 --- a/views/wikilerni/quiz-element.pug +++ b/views/wikilerni/quiz-element.pug @@ -26,7 +26,7 @@ block content const publishedAtTxt=tool.dateFormat(questionnaire.Questionnaire.publishingAt, 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(style="background-image: url('/img/quizs/"+questionnaire.Illustrations[0].url+"');") img(src="/img/quizs/"+questionnaire.Illustrations[0].url)&attributes(imgAttributes) @@ -39,41 +39,62 @@ block content 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]) 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") img(src="/img/quizs/min/"+questionnaire.Illustrations[0].url class="thumb")&attributes(imgAttributes) div(id="content-title-corner") + div(id="content" class="cardboard") p(id="author-date") #{txtQuestionnaire.publishedBy} #{author}#{txtQuestionnaire.publishedAt} #{publishedAtTxt}. #{txtQuestionnaire.lastUpdated}#{updatedAtTxt}. + //- Important : ici, on garde volontairement le html ! if(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 ? - //div#links - // a(href=link.url class="button cardboard" target="_blank" rel="noopener" title=link.anchor+" ("+txtGeneral.alertNewWindow+")") #{link.anchor} + //- Les sources de l'article + if(questionnaire.Links != undefined && questionnaire.Links.length !== 0) + 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) !{"➔ "+nextLink.anchor} + div#licence - p !{configTpl.questionnaireLicenceTxt} + p !{configTpl.licenceTxt} noscript div strong #{configTpl.noJSNotification} + + // Formulaire d'inscription : - - const cguOkLabel = txtUsers.formsEmailLabel.replace("#link", "/"+configTpl.cguPage); - div#signup - form(id="subscription" method="POST" class="needJS") + const cguOkLabel = txtUser.formsEmailLabel.replace("#link", "/"+configTpl.cguPage); /// remettre class="needJS" au formulaire ci-dessous + div#signupForm + form(id="subscription" method="POST") h3 #{configTpl.quizElementSubcriptionFormTitle} fieldset - label(for="email") #{txtUsers.formsEmailLabel} - input(id="email" type="email" name="email" placeholder=txtUsers.formsEmailPlaceholder class="cardboard") + label(for="email") #{txtUser.formsEmailLabel} + input(id="email" type="email" name="email" placeholder=txtUser.formsEmailPlaceholder class="cardboard") div#emailMessage ul(class="checkbox_li") li(class="checkbox_li") @@ -82,7 +103,7 @@ block content div(class="checkbox_override") span #{cguOkLabel} 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#zerozozio @@ -96,4 +117,4 @@ block content div#explanations(class="engraved framed") h3#explanationsTitle #{configTpl.explanationTitle} div#explanationsContent - p !{configTpl.explanationTxt} \ No newline at end of file + p !{configTpl.explanationElementTxt} \ No newline at end of file diff --git a/views/wikilerni/quiz-group.pug b/views/wikilerni/quiz-group.pug index fdcb423..e195686 100644 --- a/views/wikilerni/quiz-group.pug +++ b/views/wikilerni/quiz-group.pug @@ -5,30 +5,62 @@ block append scripts script(src="/JS/group.app.js" defer) 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-title") 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" class="cardboard") - p(id="author-date") #{txtGroups.publishedBy} #{author}#{txtGroups.publishedAt} #{publishedAtTxt}. #{txtGroups.lastUpdated}#{updatedAtTxt}. - div#introduction - if(group.introduction) - div !{group.introduction}//- Important : ici, on garde volontairement le html, car cela est accepté pour l'introduction. - div txtGroups.commonIntroTxt - // - lien vers premier élément du groupe + p(id="author-date") #{txtGroups.publishedBy} #{author} #{txtGroups.publishedAt} #{publishedAtTxt}. #{txtGroups.lastUpdated} #{updatedAtTxt}. + //- Important : ici, on garde volontairement le html, car cela est accepté pour l'introduction. + + div#introduction + if(group.Group.introduction) + div !{group.Group.introduction} + div #{txtGroups.commonIntroTxt} + + // - lien vers premier élément du groupe (html autorisé pour permettre les symboles unicodes) 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) !{"← "+txtGroups.linkFirstElementGroup} + div#licence - p !{configTpl.groupLicenceTxt} + p !{configTpl.licenceTxt} noscript div strong #{configTpl.noJSNotification} - - form(id="group" method="POST" class="needJS") - h2 #{group.title} + + // à cacher si pas de JS ! + form(id="group" method="POST") + h2 #{group.Group.title} div#response div(class="subscribeBtns") p @@ -39,7 +71,7 @@ block content for question in questionnaire.Questions p(id="question_"+question.Question.id) #{question.Question.text} 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") for response in question.Choices li(class="checkbox_li") @@ -53,20 +85,20 @@ block content em #{response.text} 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(name="groupId" id="groupId" value=group.id type="hidden") + input(name="groupId" id="groupId" value=group.Group.id type="hidden") p span(class="input_wrapper") input(id="checkResponses" type="submit" value=txtGroups.btnSendResponse class="cardboard" title=txtGroups.btnSendResponse) 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*") 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") - 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") div#explanations(class="engraved framed") h3#explanationsTitle #{configTpl.explanationTitle} div#explanationsContent - p !{configTpl.explanationTxt} \ No newline at end of file + p !{configTpl.explanationGroupTxt} \ No newline at end of file diff --git a/views/wikilerni/quiz.pug b/views/wikilerni/quiz.pug index 82e61a0..eafe271 100644 --- a/views/wikilerni/quiz.pug +++ b/views/wikilerni/quiz.pug @@ -50,7 +50,7 @@ block content if(questionnaire.Questionnaire.introduction) div#introduction !{questionnaire.Questionnaire.introduction} div#licence - p !{configTpl.questionnaireLicenceTxt} + p !{configTpl.licenceTxt} div#links for link in questionnaire.Links