const { Op, QueryTypes } = require("sequelize"); const pug = require("pug"); const striptags = require("striptags"); const config = require("../config/main.js"); const configQuestionnaires = require("../config/questionnaires.js"); const configTags = require("../config/tags.js"); const configLinks = require("../config/links.js"); const configIllustrations = require("../config/illustrations.js"); const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js"); const tool = require("../tools/main"); const toolError = require("../tools/error"); const toolFile = require("../tools/file"); const toolMail = require("../tools/mail"); const questionCtrl = require("./question"); const illustrationCtrl = require("./illustration"); const tagCtrl = require("./tag"); const userCtrl = require("./user"); const txtGeneral = require("../lang/"+config.adminLang+"/general"); const txtQuestionnaire = require("../lang/"+config.adminLang+"/questionnaire"); const txtQuestionnaireAccess = require("../lang/"+config.adminLang+"/questionnaireaccess"); const txtIllustration = require("../lang/"+config.adminLang+"/illustration"); 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(); } catch(e) { const returnAPI=toolError.returnSequelize(e); if(returnAPI.messages) { res.status(returnAPI.status).json({ errors : returnAPI.messages }); next(); } else next(e); } } exports.modify = async (req, res, next) => { try { const db = require("../models/index"); const questionnaire=await searchQuestionnaireById(req.params.id); if(!questionnaire) { const Err=new Error; error.status=404; error.message=txtQuestionnaire.notFound+" ("+req.params.id+")"; throw Err; } else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.CreatorId) res.status(401).json({ errors: txtGeneral.notAllowed }); 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 ? creaStatsQuestionnairesJson();// le nombre de quizs publiés peut avoir changé } //utile au middleware suivant (classement tags) qui s'occupe aussi de retourner une réponse si ok : req.body.QuestionnaireId=req.params.id; next(); } catch(e) { const returnAPI=toolError.returnSequelize(e); if(returnAPI.messages) { res.status(returnAPI.status).json({ errors : returnAPI.messages }); next(); } else next(e); } } exports.delete = async (req, res, next) => { try { const db = require("../models/index"); const questionnaire=await searchQuestionnaireById(req.params.id); if(!questionnaire) { const Err=new Error; error.status=404; error.message=txtQuestionnaire.notFound+" ("+req.params.id+")"; throw Err; } else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.CreatorId) res.status(401).json({ errors: txtGeneral.notAllowed }); else { // Permet de supprimer les fichiers associés en plus du sql. Inutile pour link qui n'a pas de fichier. // À faire avant la suppression SQL du questionnaire entraînant la suppression en cascade du reste. for(i in questionnaire.Questions) await questionCtrl.deleteQuestionById(questionnaire.Questions[i].id); for(i in questionnaire.Illustrations) await illustrationCtrl.deleteIllustrationById(questionnaire.Illustrations[i].id); const nb=await db["Questionnaire"].destroy( { where: { id : req.params.id }, limit:1 }); if(nb===1) { await toolFile.deleteJSON(config.dirCacheQuestionnaires, req.params.id); res.status(200).json({ message: txtGeneral.deleteOkMessage }); // 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(); // Éventuellement regénérer les caches listant les réponses/quizs des users ayant accès à ce questionnaire ? // ++ HTML } else { const Err=new Error; error.status=404; error.message=txtQuestionnaire.notFound+" ("+req.params.id+")"; throw Err; } } next(); } catch(e) { next(e); } } exports.getOneQuestionnaireById = async (req, res, next) => { try { const datas=await searchQuestionnaireById(req.params.id, true); if(datas) res.status(200).json(datas); else res.status(404).json({ errors:txtQuestionnaire.notFound }); next(); } catch(e) { next(e); } } exports.showOneQuestionnaireById = 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 creaQuestionnaireHTML(req.params.id, true); if(HTML) { res.setHeader("Content-Type", "text/html"); res.send(HTML); } else res.status(404).json({ errors:txtQuestionnaire.notFound }); } } next(); } catch(e) { next(e); } } // Recherche par mots-clés parmis tous les questionnaires publiés en filtrant parmi ceux auxquels l'abonné a déjà répondu ou pas // Seul un certain nombre de résultats est renvoyé (pagination) exports.searchQuestionnaires = async (req, res, next) => { try { let search=tool.trimIfNotNull(req.body.searchQuestionnaires); if(search === null || search === "" || search.length < config.minSearchQuestionnaires) res.status(400).json(txtQuestionnaireAccess.searchIsNotLongEnough); else { const db = require("../models/index"); let getQuestionnaires; if(!tool.isEmpty(req.body.onlyAnswers) && !tool.isEmpty(req.connectedUser.User.id)) getQuestionnaires=await db.sequelize.query("SELECT DISTINCT `Questionnaires`.`id` FROM `Questionnaires` INNER JOIN `Answers` WHERE `Answers`.`QuestionnaireId`=`Questionnaires`.`id` AND `Answers`.`UserId`=:id AND (`title` LIKE :search OR `introduction` LIKE :search OR `keywords` LIKE :search) AND `isPublished` = 1", { replacements: { id:req.connectedUser.User.id, search: "%"+search+"%" }, type: QueryTypes.SELECT }); else getQuestionnaires=await db.sequelize.query("SELECT `id` FROM `Questionnaires` WHERE (`title` LIKE :search OR `introduction` LIKE :search OR `keywords` LIKE :search) AND `isPublished` = 1", { replacements: { search: "%"+search+"%" }, type: QueryTypes.SELECT }); let begin=0, end, output=""; if(!tool.isEmpty(req.body.begin)) begin=parseInt(req.body.begin, 10); end=begin+configTpl.nbQuestionnairesUserHomePage-1;// tableau commence à 0 ! if(req.body.output!==undefined) output=req.body.output; datas=await getListingsQuestionnairesOuput(getQuestionnaires, begin, end, output); res.status(200).json(datas); } next(); } catch(e) { next(e); } } // Recherche aléatoire parmi tous les questionnaires publiés // De nouveau on peut filtrer ou non ceux auxquels l'utilisateur a répondu // Par contre, ici pas de pagination car on maîtrise le nombre de résultats envoyés exports.getRandomQuestionnaires = async (req, res, next) => { try { const db = require("../models/index"); let getQuestionnaires; if(!tool.isEmpty(req.body.onlyAnswers) && !tool.isEmpty(req.connectedUser.User.id)) getQuestionnaires=await db.sequelize.query("SELECT DISTINCT `Questionnaires`.`id` FROM `Questionnaires` INNER JOIN `Answers` WHERE `Answers`.`QuestionnaireId`=`Questionnaires`.`id` AND `Answers`.`UserId`=:id AND `isPublished` = 1 ORDER BY RAND() LIMIT "+configQuestionnaires.nbRandomResults, { replacements: { id:req.connectedUser.User.id }, type: QueryTypes.SELECT }); else getQuestionnaires=await db.sequelize.query("SELECT `id` FROM `Questionnaires` WHERE `isPublished` = 1 ORDER BY RAND() LIMIT "+configQuestionnaires.nbRandomResults, { type: QueryTypes.SELECT }); let begin=0, end; end=begin+configTpl.nbQuestionnairesUserHomePage-1; datas=await getListingsQuestionnairesOuput(getQuestionnaires, begin, end, "html"); res.status(200).json(datas); next(); } catch(e) { next(e); } } // Recherche par mots-clés parmis tous les questionnaires, y compris ceux non publiés exports.searchAdminQuestionnaires = async (req, res, next) => { try { let search=tool.trimIfNotNull(req.body.searchQuestionnaires); if(search === null || search === "" || search.length < config.minSearchQuestionnaires) res.status(400).json(txtQuestionnaireAccess.searchIsNotLongEnough); else { const db = require("../models/index"); const getQuestionnaires=await db.sequelize.query("SELECT `id`,`title` FROM `Questionnaires` WHERE (`title` LIKE :search OR `introduction` LIKE :search OR `keywords` LIKE :search) ORDER BY `title` ASC", { replacements: { search: "%"+search+"%" }, type: QueryTypes.SELECT }); res.status(200).json(getQuestionnaires); } next(); } catch(e) { next(e); } } // Retourne les statistiques concernant les questionnaires exports.getStats = async(req, res, next) => { try { const stats=await getStatsQuestionnaires(); res.status(200).json(stats); } catch(e) { next(e); } } // Liste des prochains questionnaires devant être publiés. // On vérifie à chaque fois si ils ont ce qu'il faut pour être publiables // On en profite pour chercher la prochaine date sans questionnaire programmé exports.getListNextQuestionnaires = async(req, res, next) => { try { let questionnaires=await getNextQuestionnaires(); let dateNeeded="", questionnairesDatas, dateSearch, dateQuestionnaire; for(let i in questionnaires) { questionnairesDatas=await searchQuestionnaireById(questionnaires[i].id); questionnaires[i].isPublishable=checkQuestionnaireIsPublishable(questionnairesDatas, false); // le questionnaire est-il complet ? if(dateNeeded==="") // si il y a plus de 24H entre deux dates de publication, c'est mal ! { if(i==0) dateSearch=new Date(Date.now()+3600*1000*24); else dateSearch=new Date(questionnaires[i-1].datePublishing).getTime()+3600*1000*24; dateQuestionnaire=new Date(questionnaires[i].datePublishing).getTime(); if(dateQuestionnaire > dateSearch) dateNeeded=dateSearch; } } if(questionnaires.length > 0 && dateNeeded==="") dateNeeded=new Date(dateQuestionnaire+3600*1000*24);// le jour suivant celui du dernier questionnaire else dateNeeded=new Date();// rien pour ce jour res.status(200).json({questionnaires: questionnaires, dateNeeded: dateNeeded }); next(); } catch(e) { next(e); } } // 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. res.messageToNext=txtQuestionnaire.haveBeenRegenerated.replace("#NB1", nb); next(); } catch(e) { next(e); } } // CRONS exports.deleteJsonFiles= async (req, res, next) => { // ajouter le suppression des fichiers HTML ? -> plutôt le faire manuellement lors de la suppression du questionnaire try { const db = require("../models/index"); const questionnaires=await db["Questionnaire"].findAll({ attributes: ["id"] }); let saveFiles=[]; for(let i in questionnaires) saveFiles.push(questionnaires[i].id+".json"); const deleteFiles = await toolFile.deleteFilesInDirectory(config.dirCacheQuestionnaires, saveFiles); res.status(200).json(deleteFiles); next(); } catch(e) { next(e); } } // test si des questionnaires doivent être publiés // + si des questionnaires publiés n'ont pas fichier html // si fichier html créé il faut aussi actualiser la page d'accueil & co exports.checkQuestionnairesNeedToBePublished= async (req, res, next) => { try { await checkQuestionnairesNeedToBePublished(); const nb=await checkQuestionnairesPublishedHaveHTML(); if(nb > 0) creaNewQuestionnairesJson();// provoque mise à jour du HTLM, RSS, etc. res.status(200).json(txtQuestionnaire.haveBeenPublished.replace(":NB", nb)); console.log(txtQuestionnaire.haveBeenPublished.replace(":NB", nb)); await toolMail.sendMail(0, config.adminEmail, "Publication articles", txtQuestionnaire.haveBeenPublished.replace(":NB", nb), "
"+txtQuestionnaire.haveBeenPublished.replace(":NB", nb)+"
"); next(); } catch(e) { next(e); } } // FONCTIONS UTILITAIRES const creaQuestionnaireJson = async (id) => { const db = require("../models/index"); const Questionnaire=await db["Questionnaire"].findByPk(id); if(Questionnaire) { let datas={ Questionnaire }; const Tags=await db["QuestionnaireClassification"].findAll({ where: { QuestionnaireId: Questionnaire.id }, attributes: ["TagId"] }); if(Tags) datas.Tags=Tags; const Illustrations=await db["Illustration"].findAll({ where: { QuestionnaireId: Questionnaire.id } }); if(Illustrations) datas.Illustrations=Illustrations; const Links=await db["Link"].findAll({ where: { QuestionnaireId: Questionnaire.id } }); if(Links) datas.Links=Links; const Questions=await db["Question"].findAll({ where: { QuestionnaireId: Questionnaire.id }, order: [["rank", "ASC"], ["createdAt", "ASC"]], attributes: ["id"] }); if(Questions) 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 await toolFile.createJSON(config.dirCacheQuestionnaires, id, datas); 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 : tagCtrl.creaUsedTagsJson(); // si le quiz était publié jusqu'ici, il me faut supprimer son fichier HTML (revenir pour réactiver) if(wasPublished) toolFile.deleteFile(config.dirHTMLQuestionnaire, Questionnaire.slug+".html"); } // peut impacter la liste des derniers si des informations affichées ont changé creaNewQuestionnairesJson();// peut avoir été impacté // + 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); return datas; } else return false; } exports.creaQuestionnaireJson = creaQuestionnaireJson; const checkQuestionnaireIsPublishable = (datas, checkDate=true) => { if(checkDate) { if(datas.Questionnaire.publishingAt===null) return false; else { const today=new Date(); today.setHours(0,0,0,0);// !! attention au décalage horaire du fait de l'enregistrement en UTC dans mysql const publishingAt=new Date(datas.Questionnaire.publishingAt); if (publishingAt.getTime() > today.getTime()) return false; } } if(datas.Questions==undefined || datas.Questions.length < config.nbQuestionsMin) // le nombre de réponses mini étant contrôlé au moment de l'enregistrement de la question return false; if(datas.Tags==undefined || datas.Tags.length < configTags.nbTagsMin) return false; if(datas.Links==undefined || datas.Links.length < configLinks.nbLinksMin) return false; if(datas.Illustrations==undefined || datas.Illustrations.length < configIllustrations.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 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"); 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("Je viens de publier le quiz : "+config.siteUrl+"/quizs/"+questionnaires[i].slug+".html
"); nb++; } } return nb; } // Liste des derniers questionnaires publiés (utile pour page d'accueil, flux rss, etc.) const creaNewQuestionnairesJson = async () => { const db = require("../models/index"); const Questionnaires=await db["Questionnaire"].findAll({ where: { isPublished : true }, order: [["updatedAt", "DESC"], ["id", "DESC"]], attributes: ["id"], limit: config.nbNewQuestionnaires }); if(Questionnaires) { await toolFile.createJSON(config.dirCacheQuestionnaires, "last", Questionnaires); creaNewQuestionnairesHTML(Questionnaires); return Questionnaires; } else return false; } exports.creaNewQuestionnairesJson = creaNewQuestionnairesJson; // Se limite à compter le nombre total de questionnaires et à le stocker pour éviter de lancer une requête sql à chaque fois const creaStatsQuestionnairesJson = async () => { const db = require("../models/index"); const Questionnaires=await db["Questionnaire"].findAll({ attributes: ["id"] }); const QuestionnairesPublished=await db["Questionnaire"].findAll({ where: { isPublished : true }, attributes: ["id"] }); if(Questionnaires && QuestionnairesPublished) { const stats = { nbTot : Questionnaires.length, nbPublished : QuestionnairesPublished.length } await toolFile.createJSON(config.dirCacheQuestionnaires, "stats", stats); return stats; } else return false; } exports.creaStatsQuestionnairesJson = creaStatsQuestionnairesJson; // Retourne les données créées par la fonction précédente const getStatsQuestionnaires = async () => { let stats=await toolFile.readJSON(config.dirCacheQuestionnaires, "stats"); if(!stats) stats=await creaStatsQuestionnairesJson(); if(!stats) return false; else return stats; } exports.getStatsQuestionnaires = getStatsQuestionnaires; // Quelle est la prochaine date pour laquelle aucun questionnaire n'a été publié ? const getDateNewQuestionnaireNeeded = async (maxDays) => { const db = require("../models/index"); let dateNeed="", checkDate, addMin=0, addMax=1; while(dateNeed==="") { checkDate = await db.sequelize.query("SELECT ADDDATE(CURDATE(), :addMin) as `dateNeeded` FROM `Questionnaires` WHERE `isPublished`=1 AND `publishingAt` > ADDDATE(CURDATE(), :addMin) AND `publishingAt`< ADDDATE(CURDATE(), :addMax) ORDER BY `publishingAt` LIMIT 1", { replacements: { addMin: addMin, addMax: addMax }, type: QueryTypes.SELECT }); if(checkDate && getSubscriptions24H.length>0) dateNeed=checkDate.dateNeeded; else { addMin++; addMax++; } if(addMin>maxDays) return false; } return dateNeed; } exports.getDateNewQuestionnaireNeeded = getDateNewQuestionnaireNeeded; // Les prochains questionnaires devant être publiés (permet aux gestionnaires de voir les dates vides) const getNextQuestionnaires = async () => { const db = require("../models/index"); const questionnaires= await db.sequelize.query("SELECT `id`,`title`, `publishingAt` as `datePublishing` FROM `Questionnaires` WHERE `publishingAt` > NOW() order by `publishingAt`", { type: QueryTypes.SELECT }); return questionnaires; } exports.getNextQuestionnaires = getNextQuestionnaires; const creaNewQuestionnairesHTML = async (Questionnaires) => { // On regénère la page d'accueil avec le(s) dernier(s) questionnaire(s) mis en avant : let compiledFunction = pug.compileFile("./views/"+config.theme+"/home.pug"); const configTpl = require("../views/"+config.theme+"/config/"+config.availableLangs[0]+".js"); let questionnaires=[]; for(let i in Questionnaires) questionnaires.push(await searchQuestionnaireById(Questionnaires[i].id)); let pageDatas= { config: config, configTpl: configTpl, tool: tool, striptags: striptags, txtGeneral : txtGeneral, txtQuestionnaire: txtQuestionnaire, txtIllustration: txtIllustration, pageLang: config.adminLang, metaDescription: txtGeneral.siteMetaDescription, pageTitle: txtGeneral.siteHTMLTitle, contentTitle: config.siteName, questionnaires: questionnaires, linkCanonical: config.siteUrl } let html=await compiledFunction(pageDatas); await toolFile.createHTML(config.dirHTML, "index", html); // + la page listant les X derniers quizs + liste des tags compiledFunction=pug.compileFile("./views/"+config.theme+"/newQuestionnaires.pug"); pageDatas.metaDescription=configTpl.newQuestionnairesIntro; pageDatas.pageTitle=configTpl.newQuestionnairesTitle; pageDatas.linkCanonical=config.siteUrl+"/"+configQuestionnaires.dirWebTags; pageDatas.tags=await tagCtrl.getUsedTags(); html=await compiledFunction(pageDatas); await toolFile.createHTML(configQuestionnaires.dirHTMLTags, "index", html); return true; } // À partir d'une liste d'id questionnaires fournis, retourne la liste complète ou partielle (pagination) avec les infos de chaque questionnaire // Retour en html ou json suivant les cas const getListingsQuestionnairesOuput = async (Questionnaires, begin=0, end=null, output="") => { const datas= { nbTot: Questionnaires.length, questionnaires: [], html: "" }; if(datas.nbTot!==0) { if(end===null) end=datas.nbTot; for (let i = begin; i <= end; i++) { if(Questionnaires[i]===undefined) break; else datas.questionnaires.push(await searchQuestionnaireById(Questionnaires[i].id)); } // Utiles pour savoir où j'en suis rendu dans le front : datas.begin=begin; datas.end=end; if(output==="html") { const compiledFunction = pug.compileFile("./views/"+config.theme+"/includes/listing-questionnaires.pug"); const pageDatas= { tool: tool, striptags: striptags, txtGeneral: txtGeneral, txtIllustration: txtIllustration, questionnaires: datas.questionnaires, nbQuestionnairesList:configTpl.nbQuestionnairesUserHomePage } datas.html=await compiledFunction(pageDatas); datas.questionnaires=null;// allège un peu le contenu retourné... } } return datas; } exports.getListingsQuestionnairesOuput = getListingsQuestionnairesOuput;