mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
feat: Ajout de la commande npm permettant de creer un etablissement
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
La documentation doit être en français et claire pour les utilisateurs francophones.
|
La documentation doit être en français et claire pour les utilisateurs francophones.
|
||||||
Toutes la documentation doit être dans le dossier docs/
|
Toutes la documentation doit être dans le dossier docs/ à la racine.
|
||||||
Seul les fichiers README.md, CHANGELOG.md doivent être à la racine.
|
Seul les fichiers README.md, CHANGELOG.md doivent être à la racine.
|
||||||
La documentation doit être conscise et pertinente, sans répétitions inutiles entre les documents.
|
La documentation doit être conscise et pertinente, sans répétitions inutiles entre les documents.
|
||||||
Tout ce qui concerne la gestion de projet, roadmap ne doit pas apparaître dans la documentation.
|
Tout ce qui concerne la gestion de projet, roadmap ne doit pas apparaître dans la documentation.
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ node_modules/
|
|||||||
hardcoded-strings-report.md
|
hardcoded-strings-report.md
|
||||||
backend.env
|
backend.env
|
||||||
*.log
|
*.log
|
||||||
|
.claude/worktrees/*
|
||||||
1
.husky/commit-msg
Normal file → Executable file
1
.husky/commit-msg
Normal file → Executable file
@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
npx --no -- commitlint --edit $1
|
npx --no -- commitlint --edit $1
|
||||||
1
.husky/pre-commit
Normal file → Executable file
1
.husky/pre-commit
Normal file → Executable file
@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
cd $(dirname "$0")/../Front-End/ && npm run lint-light
|
cd $(dirname "$0")/../Front-End/ && npm run lint-light
|
||||||
1
.husky/prepare-commit-msg
Normal file → Executable file
1
.husky/prepare-commit-msg
Normal file → Executable file
@ -1 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
#node scripts/prepare-commit-msg.js "$1" "$2"
|
#node scripts/prepare-commit-msg.js "$1" "$2"
|
||||||
@ -5,7 +5,8 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"update-version": "node scripts/update-version.js",
|
"update-version": "node scripts/update-version.js",
|
||||||
"format": "prettier --write Front-End"
|
"format": "prettier --write Front-End",
|
||||||
|
"create-establishment": "node scripts/create-establishment.js"
|
||||||
},
|
},
|
||||||
"standard-version": {
|
"standard-version": {
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
437
scripts/create-establishment.js
Normal file
437
scripts/create-establishment.js
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI pour créer un ou plusieurs établissements N3WT.
|
||||||
|
*
|
||||||
|
* Usage interactif :
|
||||||
|
* node scripts/create-establishment.js
|
||||||
|
*
|
||||||
|
* Usage batch (fichier JSON) :
|
||||||
|
* node scripts/create-establishment.js --file batch.json
|
||||||
|
*
|
||||||
|
* Format du fichier batch :
|
||||||
|
* {
|
||||||
|
* "backendUrl": "http://localhost:8080", // optionnel (fallback : URL_DJANGO dans conf/backend.env)
|
||||||
|
* "apiKey": "TOk3n1234!!", // optionnel (fallback : WEBHOOK_API_KEY dans conf/backend.env)
|
||||||
|
* "establishments": [
|
||||||
|
* {
|
||||||
|
* "name": "École Dupont",
|
||||||
|
* "address": "1 rue de la Paix, Paris",
|
||||||
|
* "total_capacity": 300,
|
||||||
|
* "establishment_type": [1, 2], // 1=Maternelle, 2=Primaire, 3=Secondaire
|
||||||
|
* "evaluation_frequency": 1, // 1=Trimestre, 2=Semestre, 3=Année
|
||||||
|
* "licence_code": "LIC001", // optionnel
|
||||||
|
* "directeur": {
|
||||||
|
* "email": "directeur@dupont.fr",
|
||||||
|
* "password": "motdepasse123",
|
||||||
|
* "last_name": "Dupont",
|
||||||
|
* "first_name": "Jean"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
const readline = require("readline");
|
||||||
|
const http = require("http");
|
||||||
|
const https = require("https");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
// ── Lecture de conf/backend.env ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
function loadBackendEnv() {
|
||||||
|
const envPath = path.join(__dirname, "..", "conf", "backend.env");
|
||||||
|
if (!fs.existsSync(envPath)) return;
|
||||||
|
|
||||||
|
const lines = fs.readFileSync(envPath, "utf8").split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
const eqIdx = trimmed.indexOf("=");
|
||||||
|
if (eqIdx === -1) continue;
|
||||||
|
const key = trimmed.slice(0, eqIdx).trim();
|
||||||
|
let value = trimmed.slice(eqIdx + 1).trim();
|
||||||
|
if (
|
||||||
|
(value.startsWith('"') && value.endsWith('"')) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
// Ne pas écraser les variables déjà définies dans l'environnement
|
||||||
|
if (!(key in process.env)) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers readline ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let rl;
|
||||||
|
|
||||||
|
function getRL() {
|
||||||
|
if (!rl) {
|
||||||
|
rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ask(question, defaultValue) {
|
||||||
|
const suffix =
|
||||||
|
defaultValue != null && defaultValue !== "" ? ` (${defaultValue})` : "";
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
getRL().question(`${question}${suffix}: `, (answer) => {
|
||||||
|
resolve(
|
||||||
|
answer.trim() || (defaultValue != null ? String(defaultValue) : ""),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function askRequired(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const prompt = () => {
|
||||||
|
getRL().question(`${question}: `, (answer) => {
|
||||||
|
if (!answer.trim()) {
|
||||||
|
console.log(" ⚠ Ce champ est obligatoire.");
|
||||||
|
prompt();
|
||||||
|
} else {
|
||||||
|
resolve(answer.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
prompt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function askChoices(question, choices) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log(`\n${question}`);
|
||||||
|
choices.forEach((c, i) => console.log(` ${i + 1}. ${c.label}`));
|
||||||
|
const prompt = () => {
|
||||||
|
getRL().question(
|
||||||
|
"Choix (numéros séparés par des virgules): ",
|
||||||
|
(answer) => {
|
||||||
|
const nums = answer
|
||||||
|
.split(",")
|
||||||
|
.map((s) => parseInt(s.trim(), 10))
|
||||||
|
.filter((n) => n >= 1 && n <= choices.length);
|
||||||
|
if (nums.length === 0) {
|
||||||
|
console.log(" ⚠ Sélectionnez au moins une option.");
|
||||||
|
prompt();
|
||||||
|
} else {
|
||||||
|
resolve(nums.map((n) => choices[n - 1].value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
prompt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function askChoice(question, choices, defaultIndex) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log(`\n${question}`);
|
||||||
|
choices.forEach((c, i) =>
|
||||||
|
console.log(
|
||||||
|
` ${i + 1}. ${c.label}${i === defaultIndex ? " (défaut)" : ""}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const prompt = () => {
|
||||||
|
getRL().question(`Choix (1-${choices.length}): `, (answer) => {
|
||||||
|
if (!answer.trim() && defaultIndex != null) {
|
||||||
|
resolve(choices[defaultIndex].value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = parseInt(answer.trim(), 10);
|
||||||
|
if (n >= 1 && n <= choices.length) {
|
||||||
|
resolve(choices[n - 1].value);
|
||||||
|
} else {
|
||||||
|
console.log(" ⚠ Choix invalide.");
|
||||||
|
prompt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
prompt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTTP ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function postJSON(url, data, apiKey) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const mod = parsed.protocol === "https:" ? https : http;
|
||||||
|
const body = JSON.stringify(data);
|
||||||
|
|
||||||
|
const req = mod.request(
|
||||||
|
{
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port,
|
||||||
|
path: parsed.pathname,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": Buffer.byteLength(body),
|
||||||
|
"X-API-Key": apiKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let raw = "";
|
||||||
|
res.on("data", (chunk) => (raw += chunk));
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode, data: JSON.parse(raw) });
|
||||||
|
} catch {
|
||||||
|
resolve({ status: res.statusCode, data: raw });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on("error", reject);
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Création d'un établissement ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function createEstablishment(backendUrl, apiKey, payload) {
|
||||||
|
const url = `${backendUrl.replace(/\/$/, "")}/Establishment/establishments`;
|
||||||
|
const res = await postJSON(url, payload, apiKey);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Validation basique d'un enregistrement batch ──────────────────────────────
|
||||||
|
|
||||||
|
function validateRecord(record, index) {
|
||||||
|
const errors = [];
|
||||||
|
const label = record.name ? `"${record.name}"` : `#${index + 1}`;
|
||||||
|
|
||||||
|
if (!record.name) errors.push("name manquant");
|
||||||
|
if (!record.address) errors.push("address manquant");
|
||||||
|
if (!record.total_capacity) errors.push("total_capacity manquant");
|
||||||
|
if (
|
||||||
|
!Array.isArray(record.establishment_type) ||
|
||||||
|
record.establishment_type.length === 0
|
||||||
|
)
|
||||||
|
errors.push("establishment_type manquant ou vide");
|
||||||
|
if (!record.directeur) errors.push("directeur manquant");
|
||||||
|
else {
|
||||||
|
if (!record.directeur.email) errors.push("directeur.email manquant");
|
||||||
|
if (!record.directeur.password) errors.push("directeur.password manquant");
|
||||||
|
if (!record.directeur.last_name)
|
||||||
|
errors.push("directeur.last_name manquant");
|
||||||
|
if (!record.directeur.first_name)
|
||||||
|
errors.push("directeur.first_name manquant");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`Établissement ${label} invalide : ${errors.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode batch ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runBatch(filePath) {
|
||||||
|
const absPath = path.resolve(filePath);
|
||||||
|
if (!fs.existsSync(absPath)) {
|
||||||
|
console.error(`❌ Fichier introuvable : ${absPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch;
|
||||||
|
try {
|
||||||
|
batch = JSON.parse(fs.readFileSync(absPath, "utf8"));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Fichier JSON invalide : ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendUrl =
|
||||||
|
batch.backendUrl || process.env.URL_DJANGO || "http://localhost:8080";
|
||||||
|
const apiKey = batch.apiKey || process.env.WEBHOOK_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error(
|
||||||
|
"❌ apiKey manquant dans le fichier batch ou la variable WEBHOOK_API_KEY.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const establishments = batch.establishments;
|
||||||
|
if (!Array.isArray(establishments) || establishments.length === 0) {
|
||||||
|
console.error("❌ Le fichier batch ne contient aucun établissement.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation préalable de tous les enregistrements
|
||||||
|
establishments.forEach((record, i) => validateRecord(record, i));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\n📋 Batch : ${establishments.length} établissement(s) à créer sur ${backendUrl}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
let failure = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < establishments.length; i++) {
|
||||||
|
const record = establishments[i];
|
||||||
|
const label = `[${i + 1}/${establishments.length}] ${record.name}`;
|
||||||
|
process.stdout.write(` ${label} ... `);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await createEstablishment(backendUrl, apiKey, record);
|
||||||
|
if (res.status === 201) {
|
||||||
|
console.log(`✅ (ID: ${res.data.id})`);
|
||||||
|
success++;
|
||||||
|
} else {
|
||||||
|
console.log(`❌ HTTP ${res.status}`);
|
||||||
|
console.error(` ${JSON.stringify(res.data)}`);
|
||||||
|
failure++;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`❌ Erreur réseau : ${err.message}`);
|
||||||
|
failure++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nRésultat : ${success} créé(s), ${failure} échec(s).`);
|
||||||
|
if (failure > 0) process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode interactif ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function runInteractive() {
|
||||||
|
console.log("╔══════════════════════════════════════════╗");
|
||||||
|
console.log("║ Création d'un établissement N3WT ║");
|
||||||
|
console.log("╚══════════════════════════════════════════╝\n");
|
||||||
|
|
||||||
|
const backendUrl = await ask(
|
||||||
|
"URL du backend Django",
|
||||||
|
process.env.URL_DJANGO || "http://localhost:8080",
|
||||||
|
);
|
||||||
|
const apiKey = await ask(
|
||||||
|
"Clé API webhook (WEBHOOK_API_KEY)",
|
||||||
|
process.env.WEBHOOK_API_KEY || "",
|
||||||
|
);
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error("❌ La clé API est obligatoire.");
|
||||||
|
getRL().close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Établissement ---
|
||||||
|
console.log("\n── Établissement ──");
|
||||||
|
const name = await askRequired("Nom de l'établissement");
|
||||||
|
const address = await askRequired("Adresse");
|
||||||
|
const totalCapacity = parseInt(await askRequired("Capacité totale"), 10);
|
||||||
|
|
||||||
|
const establishmentType = await askChoices("Type(s) de structure:", [
|
||||||
|
{ label: "Maternelle", value: 1 },
|
||||||
|
{ label: "Primaire", value: 2 },
|
||||||
|
{ label: "Secondaire", value: 3 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const evaluationFrequency = await askChoice(
|
||||||
|
"Fréquence d'évaluation:",
|
||||||
|
[
|
||||||
|
{ label: "Trimestre", value: 1 },
|
||||||
|
{ label: "Semestre", value: 2 },
|
||||||
|
{ label: "Année", value: 3 },
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const licenceCode = await ask("Code licence (optionnel)", "");
|
||||||
|
|
||||||
|
// --- Directeur (admin) ---
|
||||||
|
console.log("\n── Compte directeur (admin) ──");
|
||||||
|
const directorEmail = await askRequired("Email du directeur");
|
||||||
|
const directorPassword = await askRequired("Mot de passe");
|
||||||
|
const directorLastName = await askRequired("Nom de famille");
|
||||||
|
const directorFirstName = await askRequired("Prénom");
|
||||||
|
|
||||||
|
// --- Récapitulatif ---
|
||||||
|
console.log("\n── Récapitulatif ──");
|
||||||
|
console.log(` Établissement : ${name}`);
|
||||||
|
console.log(` Adresse : ${address}`);
|
||||||
|
console.log(` Capacité : ${totalCapacity}`);
|
||||||
|
console.log(` Type(s) : ${establishmentType.join(", ")}`);
|
||||||
|
console.log(` Évaluation : ${evaluationFrequency}`);
|
||||||
|
console.log(
|
||||||
|
` Directeur : ${directorFirstName} ${directorLastName} <${directorEmail}>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirm = await ask("\nConfirmer la création ? (o/n)", "o");
|
||||||
|
if (confirm.toLowerCase() !== "o") {
|
||||||
|
console.log("Annulé.");
|
||||||
|
getRL().close();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
total_capacity: totalCapacity,
|
||||||
|
establishment_type: establishmentType,
|
||||||
|
evaluation_frequency: evaluationFrequency,
|
||||||
|
...(licenceCode && { licence_code: licenceCode }),
|
||||||
|
directeur: {
|
||||||
|
email: directorEmail,
|
||||||
|
password: directorPassword,
|
||||||
|
last_name: directorLastName,
|
||||||
|
first_name: directorFirstName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("\nCréation en cours...");
|
||||||
|
try {
|
||||||
|
const res = await createEstablishment(backendUrl, apiKey, payload);
|
||||||
|
if (res.status === 201) {
|
||||||
|
console.log("\n✅ Établissement créé avec succès !");
|
||||||
|
console.log(` ID : ${res.data.id}`);
|
||||||
|
console.log(` Nom : ${res.data.name}`);
|
||||||
|
} else {
|
||||||
|
console.error(`\n❌ Erreur (HTTP ${res.status}):`);
|
||||||
|
console.error(JSON.stringify(res.data, null, 2));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("\n❌ Erreur réseau:", err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRL().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Point d'entrée ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
loadBackendEnv();
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const fileIndex = args.indexOf("--file");
|
||||||
|
|
||||||
|
if (fileIndex !== -1) {
|
||||||
|
const filePath = args[fileIndex + 1];
|
||||||
|
if (!filePath) {
|
||||||
|
console.error(
|
||||||
|
"❌ Argument --file manquant. Usage : --file <chemin/vers/batch.json>",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await runBatch(filePath);
|
||||||
|
} else {
|
||||||
|
await runInteractive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("❌ Erreur inattendue:", err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user