Browse Source

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

master
Fabrice PENHOËT 8 months ago
parent
commit
9a6bb5d882
  1. 2
      app.js
  2. 9
      config/instance-example.js
  3. 58
      config/questionnaires.js
  4. 385
      controllers/group.js
  5. 101
      controllers/questionnaire.js
  6. 113
      front/public/JS/accountUser.app.js
  7. 65
      front/public/JS/deconnection.app.js
  8. 113
      front/public/JS/deleteValidation.app.js
  9. 1555
      front/public/JS/group.app.js
  10. 114
      front/public/JS/homeManager.app.js
  11. 114
      front/public/JS/homeUser.app.js
  12. 109
      front/public/JS/index.app.js
  13. 113
      front/public/JS/loginLink.app.js
  14. 114
      front/public/JS/manageQuestionnaires.app.js
  15. 113
      front/public/JS/manageUsers.app.js
  16. 113
      front/public/JS/newLoginValidation.app.js
  17. 109
      front/public/JS/paymentPage.app.js
  18. 109
      front/public/JS/questionnaire.app.js
  19. 113
      front/public/JS/subscribe.app.js
  20. 113
      front/public/JS/subscribeValidation.app.js
  21. 109
      front/public/JS/unsubscribe.app.js
  22. 80
      front/public/gestion-groups.html
  23. 2
      front/public/gestion-quizs.html
  24. 3
      front/public/inscription.html
  25. 7
      front/public/themes/wikilerni/css/style.css
  26. 5
      front/src/connection.js
  27. 181
      front/src/group.js
  28. 235
      front/src/manageGroups.js
  29. 2
      front/webpack.config.js
  30. 4
      lang/fr/general.js
  31. 28
      lang/fr/group.js
  32. 4
      lang/fr/questionnaire.js
  33. 4
      lang/fr/user.js
  34. 14
      models/Group.js
  35. 4
      routes/cron.js
  36. 15
      routes/group.js
  37. 45
      views/wikilerni/config/fr.js
  38. 99
      views/wikilerni/quiz-element.pug
  39. 72
      views/wikilerni/quiz-group.pug
  40. 2
      views/wikilerni/quiz.pug

2
app.js

@ -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);

9
config/instance-example.js

@ -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
};

58
config/questionnaires.js

@ -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/",
regenerateHTML: "/htmlregenerated",
saveAnswersRoute: "/answer/",
getStatsQuestionnaires : "/stats/",
searchQuestionnairesRoute : "/search",
getRandomQuestionnairesRoute : "/getrandom",
searchAdminQuestionnairesRoute : "/searchadmin",
getListNextQuestionnaires: "/getlistnextquestionnaires/",
regenerateHTML: "/htmlregenerated",
searchQuestionnairesRoute : "/search",
// -- groupes :
groupRoutes: "/group",
getGroupRoute: "/get/",
searchGroupsRoute : "/search",
// -- questions & choices :
questionsRoute: "/question/",
// -- tags :
tagsSearchRoute: "/tags/search/",
// -- answers :
getQuestionnairesWithoutAnswer: "/withoutanswer/user/",
getAdminStats: "/getadminstats/",
getPreviousAnswers: "/user/answers/",
getQuestionnairesWithoutAnswer: "/withoutanswer/user/",
getStatsAnswers : "/user/anwswers/stats/",
getAdminStats: "/getadminstats/",
// 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 */
nbQuestionsMin: 1,
nbQuestionsMax: 0,
nbChoicesMax: 10,
nbTagsMin: 0,
nbTagsMax: 0, // 0 = not max
// JSON and HTML dir
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",
dirCacheTags : "datas/questionnaires/tags",
dirHTMLQuestionnaire : "front/public/quiz",
// Emplacement des fichiers HTML générés :
dirHTMLGroups : "front/public/quizs/gp",
dirHTMLQuestionnaires : "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
// 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
};

385
controllers/group.js

@ -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;

101
controllers/questionnaire.js

@ -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++;

113
front/public/JS/accountUser.app.js
File diff suppressed because it is too large
View File

65
front/public/JS/deconnection.app.js
File diff suppressed because it is too large
View File

113
front/public/JS/deleteValidation.app.js
File diff suppressed because it is too large
View File

1555
front/public/JS/group.app.js
File diff suppressed because it is too large
View File

114
front/public/JS/homeManager.app.js
File diff suppressed because it is too large
View File

114
front/public/JS/homeUser.app.js
File diff suppressed because it is too large
View File

109
front/public/JS/index.app.js
File diff suppressed because it is too large
View File

113
front/public/JS/loginLink.app.js
File diff suppressed because it is too large
View File

114
front/public/JS/manageQuestionnaires.app.js
File diff suppressed because it is too large
View File

113
front/public/JS/manageUsers.app.js
File diff suppressed because it is too large
View File

113
front/public/JS/newLoginValidation.app.js
File diff suppressed because it is too large
View File

109
front/public/JS/paymentPage.app.js
File diff suppressed because it is too large
View File

109
front/public/JS/questionnaire.app.js
File diff suppressed because it is too large
View File

113
front/public/JS/subscribe.app.js
File diff suppressed because it is too large
View File

113
front/public/JS/subscribeValidation.app.js
File diff suppressed because it is too large
View File

109
front/public/JS/unsubscribe.app.js
File diff suppressed because it is too large
View File

80
front/public/gestion-groups.html

@ -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>

2
front/public/gestion-quizs.html

@ -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>

3
front/public/inscription.html

@ -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>

7
front/public/themes/wikilerni/css/style.css

@ -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
{

5
front/src/connection.js

@ -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

@ -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

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

2
front/webpack.config.js

@ -10,10 +10,12 @@ module.exports =
connection: "./src/connection.js",
deconnection: "./src/deconnection.js",
deleteValidation: "./src/deleteValidation.js",
group: "./src/group.js",
homeManager: "./src/homeManager.js",
homeUser: "./src/homeUser.js",
index: "./src/index.js",
loginLink: "./src/loginLink.js",
manageGroups: "./src/manageGroups.js",
manageQuestionnaires: "./src/manageQuestionnaires.js",
manageUsers: "./src/manageUsers.js",
newLoginValidation: "./src/newLoginValidation.js",

4
lang/fr/general.js

@ -5,7 +5,10 @@ module.exports =
alertNewWindow: "nouvelle fenêtre",
badUrl : "Tentative d'accès à une page n'existant pas :",
btnLinkToQuestionnaire : "Aller au quiz !",
btnProposeConnection: "Je me connecte.",
btnProposeSubscribe: "Je crée mon compte.",
deleteBtnTxt: "Supprimer",
deleteFailMessage : "La suppression de l'enregistrement #ID a échoué.",
deleteOkMessage : "La suppression a bien été enregistrée.",
failAuth : "Erreur d'authentification.",
failAuthCron : "Tentative de lancement d'un cron sans le bon token.",
@ -25,6 +28,7 @@ module.exports =
scriptTimingAlert : "*** Script lent : SCRIPT_TIMING millisecondes, route : SCRIPT_URL",
scriptTimingInfo : "Durée de la réponse : SCRIPT_TIMING millisecondes, route : SCRIPT_URL",
statsAdmin : "Durant les dernières 24h : NB_USERS_24H comptes ont été créés, NB_SUBSCRIPTIONS_24H validés et NB_USERS_DELETED_24H supprimés. NB_ANSWERS_24H réponses aux quizs ont été enregistrées.<br>En tout, il y a : NB_USERS_TOT comptes, dont NB_SUBSCRIPTIONS_TOT validés et NB_SUBSCRIPTIONS_PREMIUM comptes prémium. NB_ANSWERS_TOT réponses aux quizs ont été enregistrées.<br>Parmi les NB_USERS_DELETED_TOT comptes supprimés, NB_USERS_DELETED_VALIDED avaient validé leur compte et NB_USERS_DELETED_PREMIUM avaient souscrit un compte prémium.",
subscriptionCall: "Inscrivez-vous !",
updateBtnTxt: "Modifier",
updateOkMessage : "La mise à jour à jour a bien été enregistrée."
};

28
lang/fr/group.js

@ -1,14 +1,24 @@
module.exports =
{
btnSendResponse: "Testez vos réponses.",
btnShareQuizTxt: "Partager ce quiz sur ",
commonIntroTxt: "Ce quiz vous permet de tester ce que vous avez retenu des textes qui vous ont été proposés à la lecture. Au besoin, cliquez sur le bouton précédent pour les relire.",
correctAnswerTxt: "Bonne réponse",
groupsName: "Quiz",// nom d'un groupe pour l'affichage dans les vues
haveBeenPublished: "#NB nouveaux groupes de quizs ont été publiés.",
infosGroupForAdmin: "Ce groupe de quizs a été créé le DATE_CREA, mise à jour la dernière fois le DATE_UPDATE.<br>Son identifiant est <b>GROUP_ID</b>. Il regroupe actuellement les questions de NB_ELEMENTS quizs.",
linkFirstElementGroup: "Retour à la première page.",
lastUpdated: "Dernière mise à jour, le ",
needCorrectPublishingDate: "La date de publication fournie n'a pas un format valide.",
needLanguage: "Vous devez sélectionner la langue de ce quiz.",
needNotTooLongTitle: "Le titre du quiz ne doit pas compter plus de 255 caractères.",
needTitle: "Merci de fournir un titre à votre quiz.",
needUniqueUrl: "L'url du quiz doit être unique.",
needUrl: "Merci de fournir l'url à votre quiz.",
/*
questionnairesName: "quiz",
questionnaireNeedBeCompleted: "Quiz incomplet",
needLanguage: "Vous devez sélectionner la langue de ce groupe de quizs.",
needNotTooLongTitle: "Le titre du groupe de quizs ne doit pas compter plus de 255 caractères.",
needTitle: "Merci de fournir un titre à ce groupe de quizs.",
needUniqueUrl: "L'url du groupe de quizs doit être unique.",
needUrl: "Merci de fournir l'url à ce groupe de quizs.",
notFound: "Le groupe de quizs (#SEARCH) n'a pas été trouvé.",
publishedAt: ", le",
publishedBy: "Quiz publié par"*/
publishedBy: "Quiz publié par",
searchIsNotLongEnough: "Vous devez saisir au moins #MIN caractères pour votre recherche.",
searchWithoutResult: "Aucun groupe n'a été trouvé pour votre recherche.",
wrongAnswerTxt: "Mauvaise réponse"
};

4
lang/fr/questionnaire.js

@ -1,7 +1,7 @@
module.exports =
{
btnProposeConnection: "Je me connecte.",
btnProposeSubscribe: "Je crée mon compte.",
btnProposeConnection: "Je me connecte.",// déplacé dans general.js
btnProposeSubscribe: "Je crée mon compte.",//idem
btnSendResponse: "Testez vos réponses.",
btnShareQuizTxt: "Partager ce quiz sur ",
btnShowQuestionnaire: "Afficher le quiz !",

4
lang/fr/user.js

@ -11,6 +11,10 @@ module.exports =
deleteInactiveUsersMessage: " comptes utilisateurs inactifs ont été supprimés.",
deleteOkMessage: "L'utilisateur a bien été supprimé.",
emailNotFound: "Aucun compte utilisateur n'a été trouvé pour cette adresse e-mail.",
formsEmailLabel: "E-mail :",
formsEmailPlaceholder: "Votre adresse e-mail",
formsCGUOkLabel: "J'accepte <a href=#link target=\"_blank\" rel=\"noopener\" title=\"À lire :)\">les Conditions Générale d'Utilisation</a> du site (requis).",
formsSubmitTxt: "Je m'abonne !",
godfatherFound: "Votre \"parrain\" a bien été trouvé :)",
godfatherNotFound: "Désolé mais aucun utilisateur n'a été trouvé pour ce code/e-mail parrain :(",
infosAdminGodfather: "Cet utilisateur a été parrainé par ",

14
models/Group.js

@ -12,7 +12,7 @@ const txtGeneral = require("../lang/"+config.adminLang+"/general");
module.exports = (sequelize, DataTypes) =>
{
const Group = sequelize.define("Groupe",
const Group = sequelize.define("Group",
{
title:
{
@ -28,7 +28,7 @@ module.exports = (sequelize, DataTypes) =>
{
args: [1, 255],
msg: txt.needNotTooLongTitle
}
}
}
},
slug:
@ -50,7 +50,7 @@ module.exports = (sequelize, DataTypes) =>
validate:
{
notNull: { msg: txt.needUrl }
}
}
},
</