Architecture Data
Mécanique d'Upsert (Update/Insert)
- Objectif : Autoriser les étudiants à corriger leur travail. Si l'étudiant a fait une erreur dans son fichier PDF, il peut soumettre une nouvelle version tant que la SAE est active.
- Éviter les doublons : Un étudiant ne doit avoir qu'une seule ligne de rendu par SAE. La combinaison `sae_id` + `etudiant_id` agit comme un identifiant unique virtuel.
- Stratégie : Le backend vérifie si un enregistrement existe. S'il existe, les anciens fichiers sont écrasés par les nouveaux (Update). Sinon, une nouvelle ligne est insérée (Insert). C'est ce qu'on appelle un comportement "Upsert".
/* Le principe conceptuel (Pseudo-code SQL) */
-- Étape 1 : Vérification
SELECT id FROM Rendus
WHERE sae_id = 15 AND etudiant_id = 42;
-- Étape 2A : Si existe, on écrase les anciens documents
UPDATE Rendus SET documents = '["nouveau.pdf"]', date_soumission = '...'
WHERE id = ?;
-- Étape 2B : Si n'existe pas, on initialise la note
INSERT INTO Rendus (sae_id, etudiant_id, date_soumission, documents)
VALUES (15, 42, '...', '["fichier.pdf"]');
Node.js / backend/server.js
Interception via Multer
- Middleware (
multer) : Pour recevoir des fichiers, l'API utilise upload.array('fichiers', 5), limitant l'upload à 5 fichiers simultanés par rendu pour éviter de saturer le serveur.
- Sérialisation : SQLite ne possède pas de type "Array". Les noms des fichiers générés sur le disque dur sont extraits de
req.files, transformés en tableau, puis sérialisés en une chaîne de caractères JSON (JSON.stringify) pour être stockés dans le champ texte documents.
/* Route protégée avec injection du middleware d'upload */
app.post('/api/sae/:id/rendu', verifierToken, upload.array('fichiers', 5), async (req, res) => {
// 1. Contrôle d'accès : Seul un étudiant peut rendre un travail
if (req.user.role !== 'etudiant') {
return res.status(403).json({ message: "Non autorisé." });
}
// 2. Traitement des fichiers interceptés par Multer
const fichiersNoms = req.files ? req.files.map(f => f.filename) : [];
// 3. Conversion du tableau de chaînes en une seule chaîne JSON
const documentsStr = JSON.stringify(fichiersNoms);
// Ex: '["169...-devoir.pdf", "169...-code.zip"]'
/* ... Suite du traitement en BDD ... */
});
Node.js / backend/server.js
Implémentation de l'Upsert
- Paramètres garantis : Le
sae_id provient de l'URL (req.params.id), tandis que l'etudiant_id provient du Token de sécurité (req.user.id). Il est impossible de soumettre au nom d'un autre élève.
- Horodatage (Timestamp) : La date de soumission est figée par le serveur au format ISO, garantissant que le fuseau horaire du navigateur de l'élève ne puisse pas falsifier l'heure de rendu de la SAE.
app.post('/api/sae/:id/rendu', /* ... */ async (req, res) => {
const sae_id = req.params.id;
const etudiant_id = req.user.id;
const date_soumission = new Date().toISOString();
try {
// Exécution de l'Upsert
const existing = await db.get('SELECT id FROM Rendus WHERE sae_id = ? AND etudiant_id = ?', [sae_id, etudiant_id]);
if (existing) {
await db.run('UPDATE Rendus SET date_soumission = ?, documents = ? WHERE id = ?',
[date_soumission, documentsStr, existing.id]);
} else {
await db.run('INSERT INTO Rendus (sae_id, etudiant_id, date_soumission, documents) VALUES (?, ?, ?, ?)',
[sae_id, etudiant_id, date_soumission, documentsStr]);
}
res.status(201).json({ message: "Travail rendu avec succès !" });
} catch (error) { /* ... */ }
});
React / frontend/src/App.jsx
Client : FormData et Mise à jour de l'UI
- Structure Complexe : Pour envoyer des fichiers binaires via une requête HTTP, React construit un objet
FormData. Il itère sur le State fichiersRendu pour ajouter chaque fichier avec la clé fichiers attendue par Multer.
- Réactivité : Après une soumission réussie, l'application vide le champ de sélection de fichiers et rappelle les méthodes API pour mettre à jour instantanément les détails de la SAE et le tableau de bord (Rafraîchissement de l'UI).
const handleSubmitRendu = async (e) => {
e.preventDefault(); setErreur(null); setSucces(null);
// Prévention d'une requête vide
if (fichiersRendu.length === 0) return setErreur("Joignez au moins un fichier.");
try {
// 1. Préparation du package binaire (Multipart/form-data)
const formData = new FormData();
fichiersRendu.forEach(f => formData.append('fichiers', f));
// 2. Envoi via le service
await saeService.soumettreRendu(selectedSae.id, formData, token);
setSucces("Travail rendu avec succès !");
setFichiersRendu([]); // Nettoyage de l'input
// 3. Hydratation de l'UI avec les nouvelles données
const data = await saeService.getSaeDetails(selectedSae.id, token);
setSelectedRendu(data.rendu);
const newList = await saeService.getListeSae(token);
setSaes(newList);
} catch(err) { setErreur(err.message); }
};