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 tool = require("../tools/main"); const toolError = require("../tools/error"); const toolFile = require("../tools/file"); const questionnaireCtrl = require("./questionnaire"); const userCtrl = require("./user"); const txtGeneral = require("../lang/"+config.adminLang+"/general"); const txtGroups = require("../lang/"+config.adminLang+"/group"); exports.create = async (req, res, next) => { try { const db = require("../models/index"); req.body.CreatorId=req.connectedUser.User.id; const group=await db["Group"].create({ ...req.body }, { fields: ["title", "slug", "introduction", "publishingAt", "language", "CreatorId"] }); creaGroupJson(group.id); res.status(201).json({ message: txtGeneral.addedOkMessage, id: group.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 group=await searchGroupById(req.params.id); if(!group) { const Err=new Error; error.status=404; error.message=txtGroups.notFound.replace("#SEARCH", req.params.id); throw Err; } else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==group.CreatorId) res.status(401).json({ errors: txtGeneral.notAllowed }); else await db["Group"].update({ ...req.body }, { where: { id : req.params.id } , fields: ["title", "slug", "introduction", "publishingAt", "language"], limit:1 }); await creaGroupJson(req.params.id); res.status(200).json({ message: txtGeneral.updateOkMessage }); 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 group=await searchGroupById(req.params.id); if(!group) { const Err=new Error; error.status=404; error.message=txtGroups.notFound.replace("#SEARCH", req.params.id); throw Err; } else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==group.CreatorId) res.status(401).json({ errors: txtGeneral.notAllowed }); else { // La suppression sera bloquée par SQL si des quizs dépendent de ce groupe // Donc il faut d'abord supprimer tous les quizs du groupe : for(let i in group.Questionnaires) await questionnaireCtrl.deleteQuestionnaireById(group.Questionnaires[i].id); const nb=await db["Group"].destroy( { where: { id : req.params.id }, limit:1 }); if(nb===1) { toolFile.deleteJSON(configQuestionnaires.dirCacheGroups, req.params.id); toolFile.deleteFile(configQuestionnaires.dirHTMLGroups, group.Group.slug+".html"); creaStatsGroupsJson(); res.status(200).json({ message: txtGeneral.deleteOkMessage }); } else { const Err=new Error; error.status=404; error.message=txtGroups.deleteFailMessage.replace("#ID", req.params.id); throw Err; } } next(); } catch(e) { next(e); } } // Recherche par mots-clés parmis tous les groupes (y compris ceux non publiés). exports.searchGroups = async (req, res, next) => { try { let search=tool.trimIfNotNull(req.body.searchGroups); if(search === null || search === "" || search.length < configQuestionnaires.searchGroups.minlength) res.status(400).json(txtGroups.searchIsNotLongEnough.replace("#MIN", configQuestionnaires.searchGroups.minlength)); else { const db = require("../models/index"); const getGroups=await db.sequelize.query("SELECT `id`,`title` FROM `Groups` WHERE (`title` LIKE :search OR `introduction` LIKE :search) ORDER BY `title` ASC", { replacements: { search: "%"+search+"%" }, type: QueryTypes.SELECT }); res.status(200).json(getGroups); } next(); } catch(e) { next(e); } } exports.getOneById = async (req, res, next) => { try { const datas=await searchGroupById(req.params.id); if(datas) res.status(200).json(datas); else res.status(404).json({ message:txtGroups.notFound.replace("#SEARCH", req.params.id) }); next(); } catch(e) { next(e); } } 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) => { try { const stats=await getStatsGroups(); res.status(200).json(stats); } catch(e) { next(e); } } // CRONS // Supprime fichiers json de groupes n'existant plus. exports.deleteJsonFiles = async (req, res, next) => { try { const db = require("../models/index"); const groups=await db["Group"].findAll({ attributes: ["id"] }); for(let i in groups) saveFiles.push(groups[i].id+".json"); const deleteFiles = await toolFile.deleteFilesInDirectory(configQuestionnaires.dirCacheGroups, saveFiles); res.status(200).json(deleteFiles); next(); } catch(e) { next(e); } } // Teste si des groupes doivent être publiés exports.checkGroupsNeedToBePublished = async (req, res, next) => { try { const nb=await checkGroupsNeedToBePublished(); res.status(200).json(txtGroups.haveBeenPublished.replace("#NB", nb)); next(); } catch(e) { next(e); } } // FONCTIONS PARTAGÉES const creaGroupJson = async (id) => { const db = require("../models/index"); const Group=await db["Group"].findByPk(id); if(Group) { let datas={ Group }; 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 : if(checkGroupIsPublishable(datas)) 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; } else return false; } exports.creaGroupJson = creaGroupJson; const checkGroupIsPublishable = (datas, checkDate=true) => { if(checkDate) { if(datas.Group.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.Group.publishingAt); if (publishingAt.getTime() > today.getTime()) return false; } } if(datas.Questionnaires === undefined || datas.Questionnaires.length < config.nbQuestionnairesByGroupMin) return false; return true; } const creaGroupHTML = async (id, preview = false) => { const group=await searchGroupById(id, true); if(!group) return 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 = { config: config, configQuestionnaires: configQuestionnaires, configTpl: configTpl, tool: tool, txtGeneral: txtGeneral, txtGroups: txtGroups, 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.Group.slug+".html" } const html=await compiledFunction(pageDatas); if(preview === false) { await toolFile.createHTML(configQuestionnaires.dirHTMLGroups, group.Group.slug, html); return true; } else return html; } // Remonte toutes les données du groupe + les données des questionnaires y étant classés si reassemble=true const searchGroupById = async (id, reassemble=false) => { let group=await toolFile.readJSON(configQuestionnaires.dirCacheGroups, id); if(!group) group=await creaGroupJson(id); if(!group) return false; group.Group.isPublishable=checkGroupIsPublishable(group); if(reassemble) { let questionnaire; Questionnaires=[]; const author=await userCtrl.searchUserById(group.Group.CreatorId); if(author) group.Group.CreatorName=author.User.name; for(let i in group.Questionnaires) { questionnaire=await questionnaireCtrl.searchQuestionnaireById(group.Questionnaires[i].id, true); if(questionnaire) Questionnaires.push(questionnaire); } group.Questionnaires=Questionnaires; } return group; } exports.searchGroupById = searchGroupById; // Cherche si il y a des groupes de questionnaires dont la date de publication est passée mais qui ne sont pas publiés // Vérifie si ils sont publiables et si oui génère le HTML // Si regenerate=true, tous les fichiers sont (ré)générés, même s'ils existent déjà (évolution template...) // Retourne le nombre de fichiers ayant été (ré)générés const checkGroupsNeedToBePublished = async (regenerate=false) => { const db = require("../models/index"); const groups = await db.sequelize.query("SELECT `id`,`slug` FROM `groups` WHERE `publishingAt` < NOW()", { type: QueryTypes.SELECT }); let publishedOk=false, nb=0; for(let i in groups) { if(regenerate===false) { if(await toolFile.checkIfFileExist(configQuestionnaires.dirHTMLGroups, group.Group.slug+".html")===false) { publishedOk=await creaGroupHTML(groups[i].id);// creaGroupHTLM contrôle que le groupe est publiable if(publishedOk) nb++; } } else { publishedOk=await creaGroupHTML(groups[i].id); if(publishedOk) nb++; } } return nb; } // Compte le nombre total de groupes et le stocke const creaStatsGroupsJson = async () => { const db = require("../models/index"); const Groups=await db["Group"].findAll({ attributes: ["id"] }); const GroupsPublished=await db.sequelize.query("SELECT `id` FROM `Groups` WHERE `publishingAt` < NOW()", { type: QueryTypes.SELECT }); const QuestionnairesInGroups=await db.sequelize.query("SELECT DISTINCT `id` FROM `Questionnaires` WHERE `GroupId` IS NOT NULL", { type: QueryTypes.SELECT }); if(Groups && GroupsPublished && QuestionnairesInGroups) { const stats = { nbTot : Groups.length, nbPublished : GroupsPublished.length, // ! en fait, peuvent être passé de date et non publié : À REVOIR QUAND DES EXEMPLES EN BD! nbQuestionnaires : QuestionnairesInGroups.length } await toolFile.createJSON(configQuestionnaires.dirCacheGroups, "stats", stats); return stats; } else return false; } exports.creaStatsGroupsJson = creaStatsGroupsJson; // Retourne les données créées par la fonction précédente const getStatsGroups = async () => { let stats=await toolFile.readJSON(configQuestionnaires.dirCacheGroups, "stats"); if(!stats) stats=await creaStatsGroupsJson(); if(!stats) return false; else return stats; } exports.getStatsGroups = getStatsGroups;