Premières évolutions liées à la nouvelle version, dont création du formulaire d'édition des groupes de quizs.

This commit is contained in:
Fabrice PENHOËT 2020-10-12 17:51:35 +02:00
parent 7a23a76f8e
commit 9a6bb5d882
40 changed files with 3912 additions and 820 deletions

2
app.js
View File

@ -11,6 +11,7 @@ const userRoutes = require("./routes/user");
const userPausesRoutes = require("./routes/pause");
const userPaymentsRoutes = require("./routes/payment");
const questionnairesRoutes = require("./routes/questionnaire");
const groupsRoutes = require("./routes/group");
const questionsRoutes = require("./routes/question");
const choicesRoutes = require("./routes/choice");
const illustrationRoutes = require("./routes/illustration");
@ -39,6 +40,7 @@ app.use("/api/user", userRoutes);
app.use("/api/pause", userPausesRoutes);
app.use("/api/payment", userPaymentsRoutes);
app.use("/api/questionnaire", questionnairesRoutes);
app.use("/api/group", groupsRoutes);
app.use("/api/questionnaire", tagRoutes);
app.use("/api/question", questionsRoutes);
app.use("/api/question", choicesRoutes);

View File

@ -40,7 +40,10 @@ module.exports =
hourGiveNewQuestionnaireEnd: 8, // idem
numberNewQuestionnaireAtSameTime: 50, // for mass mailing sending new quiz
minSearchQuestionnaires: 3,
fieldNewQuestionnaires : "publishingAt", // field to be used to create the list of the last questionnaires, can be "createdAt", "updatedAt" or "publishingAt"
fieldNewQuestionnaires : "publishingAt", // field to be used to create the list of the last questionnaires, can be "createdAt", "updatedAt" or "publishingAt"
// Groups :
nbQuestionnairesByGroupMin: 2,
nbQuestionnairesByGroupMax: 0,
// Illustrations:
nbIllustrationsMin: 0,
nbIllustrationsMax: 1,
@ -63,6 +66,6 @@ module.exports =
dirCacheQuestionnaires: questionnaires.dirCacheQuestionnaires,
dirCacheQuestions: questionnaires.dirCacheQuestions,
dirCacheUsersQuestionnaires: questionnaires.dirCacheUsersQuestionnaires,
dirHTMLQuestionnaire: questionnaires.dirHTMLQuestionnaire,
dirWebQuestionnaire: questionnaires.dirWebQuestionnaire
dirHTMLQuestionnaires: questionnaires.dirHTMLQuestionnaires,
dirWebQuestionnaires: questionnaires.dirWebQuestionnaires
};

View File

@ -1,34 +1,43 @@
module.exports =
{
// API'routes (after "apiUrl" defined in instance.js)
questionnaireRoutes: "/questionnaire",
questionnaireRoutes: "/questionnaire",// la base à laquelle s'ajoute les routes suivantes
getListNextQuestionnaires: "/getlistnextquestionnaires/",
getQuestionnaireRoutes: "/get",
getRandomQuestionnairesRoute : "/getrandom",
getStatsQuestionnaires : "/stats/",
previewQuestionnaireRoutes: "/preview",
publishedQuestionnaireRoutes: "/quiz/",
saveAnswersRoute: "/answer/",
getStatsQuestionnaires : "/stats/",
searchQuestionnairesRoute : "/search",
getRandomQuestionnairesRoute : "/getrandom",
searchAdminQuestionnairesRoute : "/searchadmin",
getListNextQuestionnaires: "/getlistnextquestionnaires/",
regenerateHTML: "/htmlregenerated",
saveAnswersRoute: "/answer/",
searchAdminQuestionnairesRoute : "/searchadmin",
searchQuestionnairesRoute : "/search",
// -- groupes :
groupRoutes: "/group",
getGroupRoute: "/get/",
searchGroupsRoute : "/search",
// -- questions & choices :
questionsRoute: "/question/",
// -- tags :
tagsSearchRoute: "/tags/search/",
// -- answers :
getQuestionnairesWithoutAnswer: "/withoutanswer/user/",
getPreviousAnswers: "/user/answers/",
getStatsAnswers : "/user/anwswers/stats/",
getAdminStats: "/getadminstats/",
getPreviousAnswers: "/user/answers/",
getQuestionnairesWithoutAnswer: "/withoutanswer/user/",
getStatsAnswers : "/user/anwswers/stats/",
// forms : à compléter avec valeurs par défaut, etc. cf modèle
Questionnaire :
{
title: { maxlength: 255, required: true },
slug: { maxlength: 150 }, // champ requis mais calculé à partir du titre qd vide
slug: { maxlength: 150 }, // champ requis mais calculé à partir du titre qd laissé vide dans le formulaire
introduction: { required: true }
},
searchQuestionnaires : { minlength: 3, required: true },
Group :
{
title: { maxlength: 255, required: true },
slug: { maxlength: 150 }, // champ requis mais calculé à partir du titre qd laissé vide dans le formulaire
},
Question :
{
text: { maxlength: 255, required: true },
@ -39,20 +48,27 @@ module.exports =
text: { maxlength: 255, required: true }
},
search: { minlength: 3, required: true },
/* valeurs en fait définies dans instance.js donc à supprimer quand plus utilisées ailleurs */
searchGroups: { minlength: 3, required: true },
// Emplacement des fichiers JSON générés :
dirCacheGroups : "datas/questionnaires/groups",
dirCacheQuestionnaires : "datas/questionnaires",
dirCacheQuestions : "datas/questionnaires/questions",
dirCacheTags : "datas/questionnaires/tags",
dirCacheUsersQuestionnaires : "datas/users/questionnaires",
// Emplacement des fichiers HTML générés :
dirHTMLGroups : "front/public/quizs/gp",
dirHTMLQuestionnaires : "front/public/quiz",
dirHTMLTags : "front/public/quizs",
// Idem mais pour urls :
dirWebGroups : "quizs/gp",
dirWebQuestionnaires : "quiz",
dirWebTags : "quizs/",
// limite des résultat du moteur de recherche, quand demande de résultats au hasard :
nbRandomResults : 3,
/* Valeurs en fait définies dans instance.js donc à supprimer quand plus utilisées ailleurs : */
nbQuestionsMin: 1,
nbQuestionsMax: 0,
nbChoicesMax: 10,
nbTagsMin: 0,
nbTagsMax: 0, // 0 = not max
// JSON and HTML dir
dirCacheQuestionnaires : "datas/questionnaires",
dirCacheQuestions : "datas/questionnaires/questions",
dirCacheUsersQuestionnaires : "datas/users/questionnaires",
dirCacheTags : "datas/questionnaires/tags",
dirHTMLQuestionnaire : "front/public/quiz",
dirHTMLTags : "front/public/quizs",
dirWebQuestionnaire : "quiz",//pour url page
dirWebTags : "quizs/",// idem
nbRandomResults : 3// limite les résultat du moteur de recherche quand demande de résultats au hasard
nbTagsMax: 0, // 0 = not max
};

385
controllers/group.js Normal file
View File

@ -0,0 +1,385 @@
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);
}
}
// 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 }, 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))
creaGroupHTML(id);
else // dans le cas contraire, on supprime l'éventuel fichier préexistant
toolFile.deleteFile(config.dirHTMLGroups, Group.slug+".html");
// + 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) =>
{
// 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)
return false;
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
{
config: config,
configQuestionnaires: configQuestionnaires,
configTpl: configTpl,
tool: tool,
txtGeneral: txtGeneral,
txtGroups: txtGroups,
pageLang: group.language,
metaDescription: tool.shortenIfLongerThan(config.siteName+" : "+striptags(groupe.introduction.replace("<br>", " ").replace("</p>", " ")), 200),
author: group.CreatorName,
pageTitle: txtGroups.groupsName+" "+group.title,
contentTitle: group.title+"("+txtGroups.groupsName+")",
group: group,
linkCanonical: config.siteUrl+"/"+config.dirWebGroups+"/"+group.slug+".html"
}
const html=await compiledFunction(pageDatas);
if(preview === false)
{
await toolFile.createHTML(configQuestionnaires.dirHTMLGroups, 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;
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(questionnaire.Questions[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;

View File

@ -91,50 +91,11 @@ 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+")";
const del=deleteQuestionnaire(req.params.id, req);
if(typeof del===Error)
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;
}
}
res.status(200).json({ message: txtGeneral.deleteOkMessage });
next();
}
catch(e)
@ -403,7 +364,7 @@ exports.checkQuestionnairesNeedToBePublished= async (req, res, next) =>
}
// FONCTIONS UTILITAIRES
// FONCTIONS PARTAGÉES
const creaQuestionnaireJson = async (id) =>
{
@ -435,7 +396,7 @@ const creaQuestionnaireJson = async (id) =>
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");
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é
@ -451,6 +412,52 @@ const creaQuestionnaireJson = async (id) =>
}
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 : req.params.id }, limit:1 });
if(nb===1)// = json existant, bien que sql déjà supprimé
{
toolFile.deleteJSON(configQuestionnaires.dirCacheQuestionnaires, req.params.id);
// + HTML :
toolFile.deleteFile(configQuestionnaires.dirHTMLQuestionnaires, questionnaire.Questionnaire.slug+".html");
// 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();
return true;
}
}
}
exports.deleteQuestionnaire = deleteQuestionnaire;
const checkQuestionnaireIsPublishable = (datas, checkDate=true) =>
{
if(checkDate)
@ -502,12 +509,12 @@ const creaQuestionnaireHTML = async (id, preview=false) =>
pageTitle: questionnaire.Questionnaire.title+" ("+txtQuestionnaire.questionnairesName+")",
contentTitle: questionnaire.Questionnaire.title,
questionnaire: questionnaire,
linkCanonical: config.siteUrl+"/"+config.dirWebQuestionnaire+"/"+questionnaire.Questionnaire.slug+".html"
linkCanonical: config.siteUrl+"/"+config.dirWebQuestionnaires+"/"+questionnaire.Questionnaire.slug+".html"
}
const html=await compiledFunction(pageDatas);
if(preview===false)
{
await toolFile.createHTML(config.dirHTMLQuestionnaire, questionnaire.Questionnaire.slug, html);
await toolFile.createHTML(config.dirHTMLQuestionnaires, questionnaire.Questionnaire.slug, html);
return true;
}
else
@ -575,7 +582,7 @@ const checkQuestionnairesPublishedHaveHTML = async (regenerate=false) =>
await creaQuestionnaireHTML(questionnaires[i].id);
nb++;
}
else if(await toolFile.checkIfFileExist(config.dirHTMLQuestionnaire, questionnaires[i].slug+".html")===false)
else if(await toolFile.checkIfFileExist(config.dirHTMLQuestionnaires, questionnaires[i].slug+".html")===false)
{
await creaQuestionnaireHTML(questionnaires[i].id);
nb++;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1555
front/public/JS/group.app.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<title>Gestion des groupes de quizs</title>
<!-- Version lisible des scripts : https://gitlab.com/lefablab/wikilerni/-/tree/master/front/src -->
<script src="/JS/polyfill.app.js" defer></script>
<script src="/JS/manageGroups.app.js" defer></script>
<link rel="shortcut icon" href="/img/favicon.ico">
<link rel="stylesheet" href="/themes/wikilerni/css/style.css">
</head>
<body class="cardboard">
<!-- En tête -->
<header class="cardboard">
<a href="/" title="Page d'accueil WikLerni"><img src="/themes/wikilerni/img/wikilerni-purple-2-128.png" alt="WikiLerni (logo)" title="Accéder à la page d'accueil de WikiLerni" /></a>
<ul id="headLinks">
<li><a href="/contact.html" rel="nofollow">Contact</a></li>
<li><a href="/quizs/" id="indexHeadLink" title="Les derniers quizs">Parcourir</a></li>
<li><a href="/connexion.html" id="accountHeadLink">Mon compte</a></li>
<li><a href="/a-propos.html">À propos</a></li>
<li><a href="/" title="Page d'accueil de WikiLerni">Accueil</a></li>
</ul>
</header>
<div id="crash"></div>
<section id="main-content" class="needJS">
<ul id="menu" class="cardboard">
<li><a href="/gestion.html">Gestion WikiLerni</a></li>
<li><a href="/gestion-quizs.html" title="Publication des quizs">Les quizs</a></li>
<li><a href="/gestion-utilisateurs.html" title="Les comptes utilisateurs">Les abonné(e)s</a></li>
<li><a href="/sortie.html">Me déconnecter</a></li>
</ul>
<div id="account" class="cardboard">
<h1 class="cardboard" id="infos">Les groupes de quizs</h1>
<h2>Chercher un groupe</h2>
<form id="search" method="POST">
<input id="searchGroups" type="text" name="searchGroups" placeholder="Votre recherche" class="cardboard" />
<div class="input_wrapper"><input type="submit" value="Chercher" class="cardboard" /></div>
<div id="searchResult"></div>
</form>
<h2 id="infos">Informations du groupe</h2>
<div id="groupIntro"></div>
<div id="message"></div>
<form id="groups" method="POST">
<fieldset><label for="title">Titre</label><input id="title" type="text" name="title" class="cardboard"></fieldset>
<fieldset><label for="slug">Slug</label><input id="slug" type="text" name="slug" class="cardboard"></fieldset>
<fieldset><label for="introduction">Introduction</label><textarea id="introduction" name="introduction" rows="10" class="cardboard"></textarea></fieldset>
<fieldset><label for="publishingAt">Date de publication</label><input id="publishingAt" type="date" name="publishingAt" class="cardboard"></fieldset>
<ul class="checkbox_li">
<li class="checkbox_li">
<label for="deleteOk" class="check" id="deleteOkLabel"><input type="checkbox" id="deleteOk" name="deleteOk" value="true" /><div class="checkbox_override"></div> <span class="error">Je souhaite supprimer ce groupe.</span></label>
</li>
</ul>
<input type="hidden" name="id" id="id" value="">
<div class="input_wrapper"><input type="submit" value="Valider" class="cardboard" id="submitDatas" /></div>
<div class="input_wrapper"><a href="#groups" class="button cardboard" id="wantNewGroup">Vider</a></div>
<div id="response"></div>
</form>
<div id="response"></div>
</section>
<footer class="cardboard">
<ul id="footLinks">
<li><a href="https://framasphere.org/people/7e54b7a0b53201389eef2a0000053625" title="Blog WikiLerni sur diaspora*">Blog</a></li>
<li><a href="/credits.html">Crédits</a></li>
<li><a href="/mentions-legales.html" rel="nofollow">Mentions légales</a></li>
<li><a href="/donnees.html">Données personnelles</a></li>
<li><a href="/CGV-CGU.html" rel="nofollow">CGV &amp; CGU</a></li>
</ul>
</footer>
</body>
</html>

View File

@ -45,11 +45,11 @@
<div class="input_wrapper"><input type="submit" value="Chercher" class="cardboard" /></div>
<div id="searchResult"></div>
</form>
<div id="message"></div>
<form id="questionnaires" method="POST">
<h2>Informations du quiz</h2>
<div class="input_wrapper"><a class="button cardboard" href="/gestion-groups.html">Gérer les groupes de quizs.</a></div>
<fieldset><label for="title">Titre</label><input id="title" type="text" name="title" class="cardboard"></fieldset>
<fieldset><label for="slug">Slug</label><input id="slug" type="text" name="slug" class="cardboard"></fieldset>
<fieldset><label for="introduction">Introduction</label><textarea id="introduction" name="introduction" rows="10" class="cardboard"></textarea></fieldset>

View File

@ -46,19 +46,16 @@
<div id="emailMessage"></div>
</fieldset>
<fieldset>
<label for="password">Mot de passe : </label><input id="password" type="password" name="password" placeholder="Mot de passe de votre choix" class="cardboard">
<div id="passwordMessage"><span class="info">Au moins 8 caractères. <a href="#password" id="getPassword">Générer un mot de passe</a>.</span></div>
</fieldset>
<fieldset>
<label for="codeGodfather">Code ou e-mail parrain : </label><input id="codeGodfather" type="text" name="codeGodfather" placeholder="Code ou email de votre parrain." class="cardboard">
<div id="codeGodfatherMessage"><span class="info">Facultatif.</span></div>
</fieldset>
<ul class="checkbox_li">
<li class="checkbox_li">
<label for="cguOk" class="check"><input type="checkbox" id="cguOk" name="cguOk" value="true" /><div class="checkbox_override"></div> J'accepte <a href="/CGV-CGU.html" target="_blank" rel="noopener" title="À lire :)">les Conditions Générale d'Utilisation</a> du site (requis).</label>

View File

@ -417,7 +417,7 @@ margin:3em 0;
}
#questionsList p a
{
font-size:1.5em;
font-size:1.5em;
}
@ -597,9 +597,10 @@ font-size:1em;
{
padding:0.2em 1em;
}
#searchQuestionnaires
#searchQuestionnaires, #searchUsers, #searchGroups
{
width:65%;
margin:auto;
}
#manageQuestionnaires #questionnairesList li
{
@ -1399,7 +1400,7 @@ font-size:0.9em;
}
#signup h2, #login h2, #account h2, #manageQuestionnaires h2
{
margin-bottom: 0;
margin-bottom: 0.5em;
}
#signup p, #login p, #account p, #manageQuestionnaires p
{

View File

@ -119,7 +119,10 @@ myForm.addEventListener("submit", function(e)
}
else if (response.errors)
{
response.errors = response.errors.join("<br>");
if(Array.isArray(response.errors))
response.errors = response.errors.join("<br>");
else
response.errors = serverError;
addElement(divResponse, "p", response.errors, "", ["error"]);
}
else

181
front/src/group.js Normal file
View File

@ -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("<br>");
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);
}
});

235
front/src/manageGroups.js Normal file
View File

@ -0,0 +1,235 @@
// -- GESTION DU FORMULAIRE PERMETTANT DE SAISIR / ÉDITER LES INFOS DES GROUPES DE QUIZS
/// Vérifie que l'utilisateur est bien connecté, a le bon statut et le redirige vers le formulaire d'inscription si ce n'est pas le cas.
/// Si c'est ok, propose un moteur de recherche permettant de chercher un groupe.
/// Si un id est passé par l'url on affiche les informations du groupe dans un formulaire permettant de l'éditer/supprimer.
/// Si pas d'id passé par l'url, on affiche un formulaire vide permettant d'en saisir un nouveau.
// Fichier de configuration côté client :
import { apiUrl, availableLangs, theme } from "../../config/instance.js";
const lang=availableLangs[0];
const configQuestionnaires = require("../../config/questionnaires.js");
const configTemplate = require("../../views/"+theme+"/config/"+lang+".js");
// Fonctions utiles au script :
import { getLocaly, removeLocaly } from "./tools/clientstorage.js";
import { addElement } from "./tools/dom.js";
import { helloDev, updateAccountLink } from "./tools/everywhere.js";
import { empyForm, getDatasFromInputs, setAttributesToInputs } from "./tools/forms.js";
import { dateFormat, isEmpty, replaceAll } from "../../tools/main";
import { getUrlParams } from "./tools/url.js";
import { checkSession } from "./tools/users.js";
// Dictionnaires :
const { addOkMessage, serverError } = require("../../lang/"+lang+"/general");
const { infosGroupForAdmin, searchWithoutResult } = require("../../lang/"+lang+"/group");
const { needBeConnected } = require("../../lang/"+lang+"/user");
// Principaux éléments du DOM manipulés :
const divMain = document.getElementById("main-content");
const divMessage = document.getElementById("message");
const divResponse = document.getElementById("response");
const divCrash = document.getElementById("crash");
const divGroupIntro = document.getElementById("groupIntro");
const formGroup = document.getElementById("groups");
const deleteCheckBox = document.getElementById("deleteOkLabel");
const btnNewGroup = document.getElementById("wantNewGroup");
const formSearch = document.getElementById("search");
const divSearchResult = document.getElementById("searchResult");
helloDev();
const initialise = async () =>
{
try
{
const isConnected=await checkSession(["manager", "admin"], "/"+configTemplate.connectionPage, { message: needBeConnected, color:"error" }, window.location);
if(isConnected)
{
const user=getLocaly("user", true);
updateAccountLink(user.status, configTemplate);
divMain.style.display="block";
if(!isEmpty(getLocaly("message")))
{
addElement(divMessage, "p", getLocaly("message", true).message, "", [getLocaly("message", true).color], "", false);
removeLocaly("message");
}
// Initialisation du formulaire de recherche :
setAttributesToInputs(configQuestionnaires, formSearch);
// Fonction utile pour vider le formulaire, y compris les champs hidden, etc.
// Cache aussi certains champs en mode création
const emptyGroupForm = () =>
{
empyForm(formGroup);
// Case de suppression cachée par défaut, car inutile pour formulaire de création
deleteCheckBox.style.display="none";
}
emptyGroupForm();
// Initialise les contraintes du formulaire :
setAttributesToInputs(configQuestionnaires.Group, formGroup);
// Fonction affichant les infos connues concernant un utilisateur et son abonnement
const showFormGroupInfos = (id) =>
{
// on commence par tout vider, des fois que... :
emptyGroupForm();
const xhrGetInfos = new XMLHttpRequest();
xhrGetInfos.open("GET", apiUrl+configQuestionnaires.groupRoutes+configQuestionnaires.getGroupRoute+id);
xhrGetInfos.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200 && response.Group != undefined)
{
const mapText =
{
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
};
const groupIntro=replaceAll(infosGroupForAdmin, mapText);
addElement(divGroupIntro, "p", groupIntro, "", ["info"]);
for(let data in response.Group)
{
if(formGroup.elements[data]!==undefined)
{
if(data==="publishingAt" && response.Group[data]!==null)
formGroup.elements[data].value=dateFormat(response.Group[data], "form");// !! revoir car format pouvant poser soucis si navigateur ne gère pas les champs de type "date"
else
formGroup.elements[data].value=response.Group[data];
}
}
deleteCheckBox.style.display="block";
}
}
}
xhrGetInfos.setRequestHeader("Authorization", "Bearer "+user.token);
xhrGetInfos.send();
}
// Si un id est passé par l'url, on essaye d'afficher les infos :
let urlDatas=getUrlParams();
if(urlDatas && urlDatas.id!==undefined)
showFormGroupInfos(urlDatas.id);
// Besoin d'un coup de Kärcher ?
btnNewGroup.addEventListener("click", function(e)
{
emptyGroupForm();
});
// Envoi du formulaire des infos du groupe
formGroup.addEventListener("submit", function(e)
{
e.preventDefault();
divResponse.innerHTML="";
let datas=getDatasFromInputs(formGroup);
const xhrGroupDatas = new XMLHttpRequest();
if(!isEmpty(datas.id) && (datas.deleteOk!==undefined))
xhrGroupDatas.open("DELETE", apiUrl+configQuestionnaires.groupRoutes+"/"+datas.id);
else if(!isEmpty(datas.id))
xhrGroupDatas.open("PUT", apiUrl+configQuestionnaires.groupRoutes+"/"+datas.id);
else
xhrGroupDatas.open("POST", apiUrl+configQuestionnaires.groupRoutes);
xhrGroupDatas.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 201 && response.id!=undefined)
{
addElement(divResponse, "p", addOkMessage, "", ["success"]);
datas.id=response.id;
}
else if (this.status === 200 && response.message!=undefined)
{
if(Array.isArray(response.message))
response.message = response.message.join("<br>");
else
response.message = response.message;
addElement(divResponse, "p", response.message, "", ["success"]);
}
else if (response.errors)
{
if(Array.isArray(response.errors))
response.errors = response.errors.join("<br>");
else
response.errors = serverError;
addElement(divResponse, "p", response.errors, "", ["error"]);
}
else
addElement(divResponse, "p", serverError, "", ["error"]);
if(isEmpty(response.errors))
{
if(datas.deleteOk===undefined)
showFormGroupInfos(datas.id);
else
emptyGroupForm();
}
}
}
xhrGroupDatas.setRequestHeader("Content-Type", "application/json");
xhrGroupDatas.setRequestHeader("Authorization", "Bearer "+user.token);
if(datas)
xhrGroupDatas.send(JSON.stringify(datas));
});
// Traitement du lancement d'une recherche
formSearch.addEventListener("submit", function(e)
{
e.preventDefault();
let datas=getDatasFromInputs(formSearch);
const xhrSearch = new XMLHttpRequest();
xhrSearch.open("POST", apiUrl+configQuestionnaires.groupRoutes+configQuestionnaires.searchGroupsRoute);
xhrSearch.onreadystatechange = function()
{
if (this.readyState == XMLHttpRequest.DONE)
{
let response=JSON.parse(this.responseText);
if (this.status === 200 && Array.isArray(response))
{
if(response.length===0)
addElement(divSearchResult, "p", searchWithoutResult, "", ["info"]);// vérifier et importer texte
else
{
let selectHTML="<option value=''></option>";
for(let i in response)
selectHTML+="<option value='"+response[i].id+"'>"+response[i].title+"</option>";
addElement(divSearchResult, "select", selectHTML, "selectSearch");
const searchSelect=document.getElementById("selectSearch");
searchSelect.addEventListener("change", function()
{
if(searchSelect.value!=="")
showFormGroupInfos(searchSelect.value);
});
}
}
else if (response.errors)
{
if(Array.isArray(response.errors))
response.errors = response.errors.join("<br>");
else
response.errors = serverError;
addElement(divSearchResult, "p", response.errors, "", ["error"]);
}
else
addElement(divSearchResult, "p", serverError, "", ["error"]);
}
}
xhrSearch.setRequestHeader("Content-Type", "application/json");
xhrSearch.setRequestHeader("Authorization", "Bearer "+user.token);
if(datas)
xhrSearch.send(JSON.stringify(datas));
});
}
}
catch(e)
{
addElement(divCrash, "p", serverError, "", ["error"]);
console.error(e);
}
}
initialise();