const { Op, QueryTypes } = require("sequelize");// pour certaines requêtes sql const pug = require("pug"); const striptags = require("striptags"); const config = require("../config/main.js"); const configQuestionnaire = require("../config/questionnaires.js"); const ctrlQuestionnaire = require("./questionnaire"); const tool = require("../tools/main"); const toolError = require("../tools/error"); const toolFile = require("../tools/file"); const txtGeneral = require("../lang/"+config.adminLang+"/general"); const txtIllustration = require("../lang/"+config.adminLang+"/illustration"); const txtQuestionnaire = require("../lang/"+config.adminLang+"/questionnaire"); const txtTag = require("../lang/"+config.adminLang+"/tag"); // Actualise le classement d'un questionnaire suivant les tags envoyés exports.checkTags = async (req, res, next) => { try { if(req.body.QuestionnaireId==undefined) throw { message: txtTag.neededParams }; const tagsCurrent=await getUsedTagsQuestionnaire(req.body.QuestionnaireId); if(tagsCurrent===false) throw { message: txtTag.tagsForQuestionnaireNotFound }; const tagsReceived=req.body.classification.split(",");// ! peut être vide si pas/plus de classement souhaité for(i in tagsReceived) tagsReceived[i]=tagsReceived[i].trim().toLowerCase();// ! gestion de la casse différente pour JS, pas pour Mysql // les tags jusqu'ici associés sont-ils toujours utilisés ? let deleteLink; for (i in tagsCurrent) { if(tagsReceived.indexOf(tagsCurrent[i].name.toLowerCase())===-1) deleteLink=await unlinkTagQuestionnaire(tagsCurrent[i].id, req.body.QuestionnaireId); } // parmis les tags envoyés, certains sont-ils nouveaux pour ce questionnaire ? let findTag=false, creaLink; for(i in tagsReceived) { for(j in tagsCurrent) { if(tagsCurrent[j].name.toLowerCase()===tagsReceived[i]) { findTag=true; break; } } if(!findTag && tagsReceived[i]!=="") // revoir : remettre les majuscules lors de l'enregistrement ? { creaLink=await linkTagQuestionnaire(tagsReceived[i], req.body.QuestionnaireId); if(creaLink!==true) throw creaLink;// nécessaire pour retourner les erreurs du modèle (mais uniquement "tag trop long" possible) } findTag=false; } await ctrlQuestionnaire.creaQuestionnaireJson(req.body.QuestionnaireId);// attendre avant de répondre pour que cela soit pris en compte au réaffichage. if(req.method=="PUT") res.status(200).json({ message: txtGeneral.updateOkMessage }); else if(req.method=="POST") res.status(201).json({ message: txtGeneral.addOkMessage, id:req.body.QuestionnaireId }); next(); } catch(e) { const returnAPI=toolError.returnSequelize(e); if(returnAPI.messages) { res.status(returnAPI.status).json({ errors : returnAPI.messages }); next(); } else next(e); } } // Retourne la liste des tags déjà connus débutant par l'expression donnée : exports.getTagsBeginningBy = async (req, res, next) => { try { let tags=await getAllTags(), tagsOk=[], search=req.body.search.trim().toLowerCase(); if(search.length >= 2) { for(let i in tags) { let item=tags[i].name.toLowerCase();// il reste le problème des accents if(item.startsWith(search)) tagsOk.push(tags[i]); } } res.status(200).json(tagsOk); next(); } catch(e) { next(e); } } // Fais suite à la même fonction dans questionnaire.js // Va récupérer la liste des tags utilisés pour classer au moins un questionnaire publié // Puis regénère les fichiers HTML pour chacun de ces tags exports.HTMLRegenerate= async (req, res, next) => { try { const tagsUsed=await getUsedTags(); for(let i in tagsUsed) await creaQuestionnairesTagJson(tagsUsed[i].id);// provoque la regénération du json + du html res.status(200).json({ message: res.messageToNext.replace("#NB2", tagsUsed.length) }); res.messageToNext=null; next(); } catch(e) { next(e); } } // UTILITAIRES // Créer la liste de tous les tags présents dans la base de données const creaAllTagsJson = async () => { const db = require("../models/index"); const tags=await db["Tag"].findAll(); await toolFile.createJSON(configQuestionnaire.dirCacheTags, "all", tags); return tags; } // Retourne la liste de tous les tags const getAllTags = async () => { const tags=await toolFile.readJSON(configQuestionnaire.dirCacheTags, "all"); if(tags) return tags; else return await creaAllTagsJson(); } // Créer la liste de tous tags utilisés pour classer au moins un quiz publié /// Ajouter l'actualisation de la page "parcourir" du site const creaUsedTagsJson = async () => { const db = require("../models/index"); const tags = await db.sequelize.query("SELECT DISTINCT `Tags`.`id`,`Tags`.`name`,`Tags`.`slug` FROM `Tags` INNER JOIN `QuestionnaireClassifications` INNER JOIN `Questionnaires` WHERE `Tags`.`id`=`QuestionnaireClassifications`.`TagId` AND `QuestionnaireClassifications`.`QuestionnaireId`=`Questionnaires`.`id` AND `Questionnaires`.`isPublished`=true ORDER BY `name` ASC", { type: QueryTypes.SELECT , model: db["Tag"], mapToModel: true }); await toolFile.createJSON(configQuestionnaire.dirCacheTags, "used", tags); if(tags.length !== 0) creaUsedTagsHTML(tags); return tags; } exports.creaUsedTagsJson = creaUsedTagsJson;// utile pour actualiser en cas de publication/dépublication d'un quiz // Créer la page HTML listant tous les tags const creaUsedTagsHTML = async (tags) => { // + la page listant les X derniers quizs + liste des tags const compiledFunction=pug.compileFile("./views/"+config.theme+"/tagsList.pug"); const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js"); let pageDatas = { config: config, configTpl: configTpl, tool: tool, pageLang: config.adminLang, metaDescription: configTpl.tagListMetaDesc, pageTitle: configTpl.tagListTitle, contentTitle: config.siteName, tags: tags, linkCanonical: config.siteUrl+"/"+configQuestionnaire.dirWebTags } html=await compiledFunction(pageDatas); await toolFile.createHTML(configQuestionnaire.dirHTMLTags, "index", html); return true; } // Retourne la liste des tags utilisés const getUsedTags = async () => { const tags=await toolFile.readJSON(configQuestionnaire.dirCacheTags, "used"); if(tags) return tags; else return await creaUsedTagsJson(); } exports.getUsedTags = getUsedTags; // Retourne la liste des tags d'un questionnaire avec leurs noms, slugs, etc. const getUsedTagsQuestionnaire = async (id) => { id=tool.trimIfNotNull(id); if(id === null) return false; const questionnaire=await ctrlQuestionnaire.searchQuestionnaireById(id); if(!questionnaire) throw { message: txtQuestionnaire.notFound }; else { const allTags=await getAllTags(); let tagsQuestionnaire=[]; for(let i in questionnaire.Tags) { for(j in allTags) { if(allTags[j].id === questionnaire.Tags[i].TagId) { tagsQuestionnaire.push(allTags[j]); break; } } } return tagsQuestionnaire; } } exports.getTagsQuestionnaire = getUsedTagsQuestionnaire; // Créer la liste complète des questionnaires publiés, classés par un tag const creaQuestionnairesTagJson = async (id) => { id=tool.trimIfNotNull(id); if(id === null) return false; const db = require("../models/index"); const questionnaires = await db.sequelize.query("SELECT id FROM `Questionnaires` INNER JOIN `QuestionnaireClassifications` ON `Questionnaires`.`id`=`QuestionnaireClassifications`.`QuestionnaireId` AND `Questionnaires`.`isPublished`=1 AND `QuestionnaireClassifications`.`TagId`=:id ORDER BY "+config.fieldNewQuestionnaires+" DESC", { replacements: { id: id }, type: QueryTypes.SELECT , model: db["Questionnaire"], mapToModel: true }); await toolFile.createJSON(configQuestionnaire.dirCacheTags, "liste-"+id, questionnaires);// le supprimer si liste vide ? creaUsedTagsJson(); creaQuestionnairesTagHTML(id, questionnaires);// pas await, car potentiellement long ! return questionnaires; } exports.creaQuestionnairesTagJson = creaQuestionnairesTagJson; const creaQuestionnairesTagHTML = async (id, Questionnaires) => { id=tool.trimIfNotNull(id); if(id === null || Questionnaires === null) return false; const tag=await searchTagById(id); if(Questionnaires.length === 0) { // plus aucun quiz classé ici. Il faudrait idéalement envoyer des erreurs 404/410, etc. // revoir aussi l'intérêt ou non de supprimer les pages suivantes, sachant qu'elles ne sont pas indexables ? ou les rendre dynamiques ? await toolFile.deleteFile(configQuestionnaire.dirHTMLTags, tag.slug+".html"); return true; } const compiledFunction = pug.compileFile("./views/"+config.theme+"/tag.pug"); const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js"); const pageDatas = { config: config, configTpl: configTpl, tool: tool, striptags: striptags, txtGeneral : txtGeneral, txtQuestionnaire: txtQuestionnaire, txtIllustration: txtIllustration, pageLang: config.adminLang, metaDescription: config.siteName+" : "+txtTag.tagMetaDescription+tag.name, pageTitle: config.siteName+" - "+tag.name, contentTitle: config.siteName+" - "+tag.name, tagInfos: tag, linkCanonical: config.siteUrl+"/"+configQuestionnaire.dirWebTags+"/"+tag.slug+".html" } const nbPages=Math.ceil(Questionnaires.length / configTpl.maxQuestionnairesByPage); pageDatas.nbPages=nbPages; let debut=0; for (let i = 1; i <= nbPages; i++) { const questionnairesPage = Questionnaires.slice(debut, debut+configTpl.maxQuestionnairesByPage), questionnairesInfos=[]; for(let j in questionnairesPage) questionnairesInfos.push(await ctrlQuestionnaire.searchQuestionnaireById(questionnairesPage[j].id)); pageDatas.questionnaires=questionnairesInfos; pageDatas.page=i; let url=tag.slug; if(i!==1) { url=url+"-"+i; pageDatas.metaRobots="noindex,follow"; pageDatas.linkCanonical=null; } const html=await compiledFunction(pageDatas); await toolFile.createHTML(configQuestionnaire.dirHTMLTags, url, html); debut+=configTpl.maxQuestionnairesByPage; } return true; } // Retourne un tag, si il existe const searchTagByName = async (name) => { name=tool.trimIfNotNull(name); if(name===null) return false; const db = require("../models/index"); const tag=await db["Tag"].findOne({ where: { name: name } }); // utile de faire en sql pour les problèmes de majuscules / minuscules if(tag) return tag; else return false; } const searchTagById = async (id) => { id=tool.trimIfNotNull(id); if(id===null) return false; const db = require("../models/index"); const tag=await db["Tag"].findByPk(id); if(tag) return tag; else return false; } // Supprime l'association entre un tag et un questionnaire + actualise la liste des questionnaires pour le tag concerné // La mise à jour du json/HTML du questionnaire est appelée par le contrôleur appelant cette fonction const unlinkTagQuestionnaire = async (tagId, questionnaireId) => { const db = require("../models/index"); await db["QuestionnaireClassification"].destroy( { where: { TagId: tagId, QuestionnaireId : questionnaireId }, limit:1 }); creaQuestionnairesTagJson(tagId);// peut être lent ! return true; } // Créer l'association entre un tag et un questionnaire + actualise la liste des questionnaires / tag // La mise à jour du json/HTML du questionnaire est appelée par le contrôleur appelant cette fonction const linkTagQuestionnaire = async (tagName, questionnaireId) => { const db = require("../models/index"); // ce tag est-il nouveau ? let tagLinked=await searchTagByName(tagName); if(tagLinked === false) { tagLinked=await db["Tag"].create({ name: tagName, slug: null }, { fields: ["name", "slug"] });// "slug : null" pour laisser le modèle le générer creaAllTagsJson(); } if(tagLinked) { await db["QuestionnaireClassification"].create({ TagId: tagLinked.id, QuestionnaireId: questionnaireId }); creaQuestionnairesTagJson(tagLinked.id);// peut être lent ! } return true; }