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 groupCtrl = require("./group");
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
{
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)
{
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 if(tool.isEmpty(req.body.GroupId) && !tool.isEmpty(req.body.rankInGroup))
res.status(400).json({ errors : [txtQuestionnaire.needGroupIfRank] });
else
{
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);
}
}
}
// 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();
}
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 del=deleteQuestionnaire(req.params.id, req);
if(typeof del===Error)
throw Err;
else
res.status(200).json({ message: txtGeneral.deleteOkMessage });
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 route :
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 `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 `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();
const dayWithoutPublication=[0,6];// à déclarer dans config + gérer cas où aucun jour n'est obligatoire
let dateNeeded="", questionnairesDatas, dateQuestionnaireTS, previousDayNeededTS, previousDayTS;
for(let i in questionnaires)
{
questionnairesDatas=await searchQuestionnaireById(questionnaires[i].id);
questionnaires[i].isPublishable=checkQuestionnaireIsPublishable(questionnairesDatas, false); // le questionnaire est-il complet ?
dateQuestionnaireTS=new Date(questionnaires[i].datePublishing).getTime();
if(dateNeeded==="")
{
// je commence par chercher le jour précédent pour lequel je dois avoir publié quelque chose :
previousDayTS=new Date(new Date(dateQuestionnaireTS-3600*1000*24));
previousDayNeededTS=0;
while (previousDayNeededTS===0)
{
if(dayWithoutPublication.indexOf(previousDayTS.getDay())===-1)
previousDayNeededTS=previousDayTS;
else
previousDayTS=new Date(previousDayTS.getTime()-3600*1000*24);
}
// si il n'y a pas de quiz précédent, cette date est celle que je cherche
if(!questionnaires[i-1])
{
if(previousDayNeededTS >= Date.now())
dateNeeded=previousDayNeededTS;
}
else
{ // sinon je compare la date du précédent quiz à celle pour laquelle j'ai besoin d'un quiz
if(new Date(questionnaires[i-1].datePublishing).getTime() < previousDayNeededTS)
dateNeeded=previousDayNeededTS;
}
}
}
if(questionnaires.length > 0 && dateNeeded==="")
dateNeeded=new Date(dateQuestionnaireTS+3600*1000*24);// le jour suivant celui du dernier questionnaire
else
dateNeeded=new Date(Date.now()+3600*1000*24);// mais il est possible que rien n'ai été publié ce jour, le quiz du jour étant absent de la liste traitée
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);
await creaNewQuestionnairesJson();// provoque mise à jour du HTLM, RSS, etc.
res.messageToNext=txtQuestionnaire.haveBeenRegenerated.replace("#NB1", nb);
next();
}
catch(e)
{
next(e);
}
}
// CRONS
// Supprime fichiers json de questionnaires n'existant plus.
exports.deleteJsonFiles= async (req, res, next) =>
{
try
{
const db = require("../models/index");
const questionnaires=await db["Questionnaire"].findAll({ attributes: ["id"] });
let saveFiles=["last.json","stats.json"];// dans le même répertoir et à garder.
for(let i in questionnaires)
saveFiles.push(questionnaires[i].id+".json");
const deleteFiles = await toolFile.deleteFilesInDirectory(configQuestionnaires.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));
next();
}
catch(e)
{
next(e);
}
}
// FONCTIONS PARTAGÉES
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 fonctions appelées par la suite
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 });
// + Peut impacter la liste des tags utilisés :
tagCtrl.creaUsedTagsJson();
// 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 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
return false;
}
exports.creaQuestionnaireJson = creaQuestionnaireJson;
// Supprime un questionnaire et toutes ses dépendances
const deleteQuestionnaire = async (id, req) =>
{
const db = require("../models/index");
const questionnaire=await searchQuestionnaireById(id);
if(!questionnaire)
{
const Err=new Error;
error.status=404;
error.message=txtQuestionnaire.notFound+" ("+req.params.id+")";
return Err;
}
else if(req.connectedUser.User.status==="creator" && req.connectedUser.User.id!==questionnaire.CreatorId)
{
const Err=new Error;
error.status=401;
error.message=txtGeneral.notAllowed;
return Err;
}
else
{
// Suppression des fichiers associés en plus du sql. Inutile pour link qui n'a pas de fichier.
// À faire avant la suppression SQL du questionnaire qui entraîne des suppressions en cascade dans la base de données.
for(let i in questionnaire.Questions)
await questionCtrl.deleteQuestionById(questionnaire.Questions[i].id);
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é ?
{
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 :
await creaNewQuestionnairesJson();
await creaStatsQuestionnairesJson();
return true;
}
}
}
exports.deleteQuestionnaire = deleteQuestionnaire;
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)
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)
return false;
if(datas.Links === undefined || datas.Links.length < config.nbLinksMin)
return false;
if(datas.Illustrations === undefined || datas.Illustrations.length < config.nbIllustrationsMin)
return false;
return true;
}
const creaQuestionnaireHTML = async (id, preview=false) =>
{
// 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;
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("