mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 15:33:22 +00:00
feat: Sauvegarde des formulaires d'école dans les bons dossiers /
utilisation des bons composants dans les modales [N3WTS-17]
This commit is contained in:
@ -318,20 +318,27 @@ class RegistrationForm(models.Model):
|
||||
####### Formulaires masters (documents école, à signer ou pas) #######
|
||||
def registration_school_file_master_upload_to(instance, filename):
|
||||
# Stocke les fichiers masters dans un dossier dédié
|
||||
return f"registration_files/school_file_masters/{instance.pk}/{filename}"
|
||||
# Utilise l'ID si le nom n'est pas encore disponible
|
||||
est_name = None
|
||||
if instance.establishment and instance.establishment.name:
|
||||
est_name = instance.establishment.name
|
||||
else:
|
||||
# fallback si pas d'établissement (devrait être rare)
|
||||
est_name = "unknown_establishment"
|
||||
return f"{est_name}/Formulaires/{filename}"
|
||||
|
||||
class RegistrationSchoolFileMaster(models.Model):
|
||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
|
||||
name = models.CharField(max_length=255, default="")
|
||||
is_required = models.BooleanField(default=False)
|
||||
formMasterData = models.JSONField(default=list, blank=True, null=True)
|
||||
# Nouveau champ pour formulaire existant (PDF, DOC, etc.)
|
||||
file = models.FileField(
|
||||
upload_to=registration_school_file_master_upload_to,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Fichier du formulaire existant (PDF, DOC, etc.)"
|
||||
)
|
||||
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='school_file_masters', null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} - {self.id}'
|
||||
@ -342,6 +349,131 @@ class RegistrationSchoolFileMaster(models.Model):
|
||||
return self.file.url
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
import os
|
||||
affected_rf_ids = set()
|
||||
is_new = self.pk is None
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# 2. Gestion des groupes pour la synchro des templates
|
||||
if is_new:
|
||||
from Subscriptions.models import RegistrationForm
|
||||
new_groups = set(self.groups.values_list('id', flat=True))
|
||||
affected_rf_ids.update(
|
||||
RegistrationForm.objects.filter(fileGroup__in=list(new_groups)).values_list('pk', flat=True)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
old = RegistrationSchoolFileMaster.objects.get(pk=self.pk)
|
||||
old_groups = set(old.groups.values_list('id', flat=True))
|
||||
new_groups = set(self.groups.values_list('id', flat=True))
|
||||
from Subscriptions.models import RegistrationForm
|
||||
affected_rf_ids.update(
|
||||
RegistrationForm.objects.filter(fileGroup__in=list(old_groups | new_groups)).values_list('pk', flat=True)
|
||||
)
|
||||
form_data_changed = (
|
||||
old.formMasterData != self.formMasterData
|
||||
and self.formMasterData
|
||||
and isinstance(self.formMasterData, dict)
|
||||
and self.formMasterData.get("fields")
|
||||
)
|
||||
name_changed = old.name != self.name
|
||||
# --- Correction spécifique pour les fichiers existants (PDF, DOC, etc.) ---
|
||||
# Si le nom change et qu'on a un fichier existant, on doit renommer physiquement le fichier
|
||||
if old.file and not self.file and name_changed:
|
||||
old_file_path = old.file.path
|
||||
ext = os.path.splitext(old_file_path)[1]
|
||||
clean_name = (self.name or 'document').replace(' ', '_').replace('/', '_')
|
||||
new_filename = f"{clean_name}{ext}"
|
||||
new_rel_path = os.path.join(os.path.dirname(old.file.name), new_filename)
|
||||
new_abs_path = os.path.join(os.path.dirname(old_file_path), new_filename)
|
||||
logger.info(f"Renommage fichier: {old_file_path} -> {new_abs_path}")
|
||||
try:
|
||||
if not os.path.exists(new_abs_path):
|
||||
os.rename(old_file_path, new_abs_path)
|
||||
self.file.name = new_rel_path
|
||||
logger.info(f"self.file.name après renommage: {self.file.name}")
|
||||
super().save(update_fields=["file"])
|
||||
else:
|
||||
logger.info(f"Le fichier cible existe déjà: {new_abs_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du renommage du fichier master: {e}")
|
||||
elif old.file and self.file and self.file != old.file:
|
||||
old.file.delete(save=False)
|
||||
elif old.file and form_data_changed:
|
||||
old.file.delete(save=False)
|
||||
self.file = None
|
||||
except RegistrationSchoolFileMaster.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Harmonisation : gestion du fichier (dynamique ou existant) sans appeler registration_school_file_master_upload_to
|
||||
# Pour les formulaires dynamiques (PDF à générer)
|
||||
if not self.file and self.formMasterData and isinstance(self.formMasterData, dict) and self.formMasterData.get("fields"):
|
||||
try:
|
||||
from Subscriptions.util import generate_form_json_pdf
|
||||
pdf_filename = f"{self.name or 'formulaire'}.pdf"
|
||||
abs_path = os.path.join(settings.MEDIA_ROOT, self.file.field.upload_to(self, pdf_filename))
|
||||
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||
if os.path.exists(abs_path):
|
||||
try:
|
||||
os.remove(abs_path)
|
||||
logger.info(f"Suppression fichier existant: {abs_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur suppression fichier existant avant save: {e}")
|
||||
pdf_file = generate_form_json_pdf(self, self.formMasterData)
|
||||
logger.info(f"Sauvegarde PDF dynamique: {pdf_filename}")
|
||||
self.file.save(pdf_filename, pdf_file, save=False)
|
||||
logger.info(f"self.file.name après save PDF dynamique: {self.file.name}")
|
||||
super().save(update_fields=["file"])
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération automatique du PDF pour le master {self.pk}: {e}")
|
||||
# Pour les fichiers existants uploadés
|
||||
elif self.file and hasattr(self.file, 'name') and self.name:
|
||||
# On force le nom du fichier (nom du master + extension) si besoin
|
||||
ext = os.path.splitext(self.file.name)[1]
|
||||
clean_name = (self.name or 'document').replace(' ', '_').replace('/', '_')
|
||||
final_file_name = f"{clean_name}{ext}"
|
||||
# Si le nom ne correspond pas, on renomme le fichier physique et le FileField
|
||||
if os.path.basename(self.file.name) != final_file_name:
|
||||
current_path = self.file.path
|
||||
abs_path = os.path.join(settings.MEDIA_ROOT, self.file.field.upload_to(self, final_file_name))
|
||||
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||
if os.path.exists(abs_path):
|
||||
try:
|
||||
os.remove(abs_path)
|
||||
logger.info(f"Suppression fichier existant: {abs_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur suppression fichier existant avant renommage: {e}")
|
||||
try:
|
||||
os.rename(current_path, abs_path)
|
||||
self.file.name = os.path.relpath(abs_path, settings.MEDIA_ROOT)
|
||||
logger.info(f"Déplacement fichier existant: {current_path} -> {abs_path}")
|
||||
super().save(update_fields=["file"])
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du déplacement du fichier existant: {e}")
|
||||
|
||||
# 4. Synchronisation des templates pour tous les dossiers d'inscription concernés (création ou modification)
|
||||
try:
|
||||
from Subscriptions.util import create_templates_for_registration_form
|
||||
from Subscriptions.models import RegistrationForm
|
||||
for rf_id in affected_rf_ids:
|
||||
try:
|
||||
rf = RegistrationForm.objects.get(pk=rf_id)
|
||||
create_templates_for_registration_form(rf)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la synchronisation des templates pour RF {rf_id} après modification du master {self.pk}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur globale lors de la synchronisation des templates après modification du master {self.pk}: {e}")
|
||||
def delete(self, *args, **kwargs):
|
||||
# Supprimer le fichier physique du master si présent
|
||||
if self.file and hasattr(self.file, 'path') and os.path.exists(self.file.path):
|
||||
try:
|
||||
self.file.delete(save=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression du fichier master: {e}")
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
####### Parent files masters (documents à fournir par les parents) #######
|
||||
class RegistrationParentFileMaster(models.Model):
|
||||
groups = models.ManyToManyField(RegistrationFileGroup, related_name='parent_file_masters', blank=True)
|
||||
@ -354,7 +486,7 @@ class RegistrationParentFileMaster(models.Model):
|
||||
############################################################
|
||||
|
||||
def registration_school_file_upload_to(instance, filename):
|
||||
return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/school/{filename}"
|
||||
return f"{instance.registration_form.establishment.name}/dossier_{instance.registration_form.student.last_name}_{instance.registration_form.student.first_name}/{filename}"
|
||||
|
||||
def registration_parent_file_upload_to(instance, filename):
|
||||
return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||
|
||||
@ -109,6 +109,7 @@ def create_templates_for_registration_form(register_form):
|
||||
)
|
||||
|
||||
created = []
|
||||
logger.info("util.create_templates_for_registration_form - create_templates_for_registration_form")
|
||||
|
||||
# Récupérer les masters du fileGroup courant
|
||||
current_group = getattr(register_form, "fileGroup", None)
|
||||
@ -135,6 +136,7 @@ def create_templates_for_registration_form(register_form):
|
||||
|
||||
school_masters = RegistrationSchoolFileMaster.objects.filter(groups=current_group).distinct()
|
||||
parent_masters = RegistrationParentFileMaster.objects.filter(groups=current_group).distinct()
|
||||
logger.info("util.create_templates_for_registration_form - school_masters récupérés")
|
||||
|
||||
school_master_ids = {m.pk for m in school_masters}
|
||||
parent_master_ids = {m.pk for m in parent_masters}
|
||||
@ -162,6 +164,7 @@ def create_templates_for_registration_form(register_form):
|
||||
logger.info("Deleted obsolete parent template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
|
||||
|
||||
# Créer les school templates manquants
|
||||
logger.info("util.create_templates_for_registration_form - Créer les school templates manquants")
|
||||
for m in school_masters:
|
||||
exists = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
|
||||
if exists:
|
||||
@ -169,44 +172,61 @@ def create_templates_for_registration_form(register_form):
|
||||
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
||||
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
||||
|
||||
# Si le master a un fichier uploadé (formulaire existant)
|
||||
file_to_attach = None
|
||||
if m.file:
|
||||
import os
|
||||
from django.core.files import File as DjangoFile
|
||||
master_file_path = m.file.path
|
||||
if os.path.exists(master_file_path):
|
||||
filename = os.path.basename(master_file_path)
|
||||
# Générer le chemin cible pour le template élève
|
||||
dest_path = registration_school_file_upload_to(None, filename)
|
||||
dest_dir = os.path.dirname(os.path.join(settings.MEDIA_ROOT, dest_path))
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
# Copier le fichier dans le dossier cible
|
||||
dest_full_path = os.path.join(settings.MEDIA_ROOT, dest_path)
|
||||
with open(master_file_path, 'rb') as src, open(dest_full_path, 'wb') as dst:
|
||||
dst.write(src.read())
|
||||
# Préparer le File Django à attacher au template
|
||||
with open(dest_full_path, 'rb') as f:
|
||||
file_to_attach = DjangoFile(f, name=dest_path)
|
||||
# --- Correction : Générer un nom de fichier unique uniquement si le master n'a pas de fichier ---
|
||||
file_name = None
|
||||
if m.file and hasattr(m.file, 'name') and m.file.name:
|
||||
# Utiliser le nom du fichier tel qu'il est stocké dans le master (pas de suffixe aléatoire ici)
|
||||
file_name = os.path.basename(m.file.name)
|
||||
logger.info(f"util.create_templates_for_registration_form - file_name 1 : {file_name}")
|
||||
elif m.file:
|
||||
file_name = str(m.file)
|
||||
else:
|
||||
# Générer le PDF du template à partir du JSON du master
|
||||
# Générer le PDF si besoin (rare ici)
|
||||
try:
|
||||
pdf_file = generate_form_json_pdf(register_form, m.formMasterData)
|
||||
file_to_attach = pdf_file
|
||||
file_name = os.path.basename(pdf_file.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la génération du PDF pour le template: {e}")
|
||||
file_to_attach = None
|
||||
file_name = None
|
||||
|
||||
tmpl = RegistrationSchoolFileTemplate.objects.create(
|
||||
logger.info(f"util.create_templates_for_registration_form - file_name : {file_name}")
|
||||
|
||||
tmpl = RegistrationSchoolFileTemplate(
|
||||
master=m,
|
||||
registration_form=register_form,
|
||||
name=m.name or "",
|
||||
formTemplateData=m.formMasterData or [],
|
||||
slug=slug,
|
||||
file=file_to_attach,
|
||||
)
|
||||
if file_name:
|
||||
from django.core.files.base import ContentFile
|
||||
# Vérifier si le fichier existe déjà dans MEDIA_ROOT (copie du master)
|
||||
upload_rel_path = registration_school_file_upload_to(
|
||||
type("Tmp", (), {
|
||||
"registration_form": register_form,
|
||||
"establishment": getattr(register_form, "establishment", None),
|
||||
"student": getattr(register_form, "student", None)
|
||||
})(),
|
||||
file_name
|
||||
)
|
||||
abs_path = os.path.join(settings.MEDIA_ROOT, upload_rel_path)
|
||||
master_file_path = m.file.path if m.file and hasattr(m.file, 'path') else None
|
||||
|
||||
# Si le fichier n'existe pas dans le dossier cible, le copier depuis le master
|
||||
if master_file_path and not os.path.exists(abs_path):
|
||||
try:
|
||||
import shutil
|
||||
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||
shutil.copy2(master_file_path, abs_path)
|
||||
logger.info(f"util.create_templates_for_registration_form - Copie du fichier master {master_file_path} -> {abs_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la copie du fichier master pour le template: {e}")
|
||||
|
||||
# Associer le fichier existant (ou copié) au template
|
||||
tmpl.file.name = upload_rel_path
|
||||
tmpl.save()
|
||||
created.append(tmpl)
|
||||
logger.info("Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
logger.info("util.create_templates_for_registration_form - Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||||
|
||||
# Créer les parent templates manquants
|
||||
for m in parent_masters:
|
||||
|
||||
@ -139,6 +139,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
|
||||
# Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent)
|
||||
if removed_group_ids:
|
||||
logger.info("REMOVE IDs")
|
||||
try:
|
||||
rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct()
|
||||
for rf in rfs_removed:
|
||||
@ -151,6 +152,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
|
||||
# Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants
|
||||
if added_group_ids:
|
||||
logger.info("ADD IDs")
|
||||
try:
|
||||
rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct()
|
||||
for rf in rfs_added:
|
||||
|
||||
@ -106,7 +106,6 @@ export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
|
||||
body: data,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
// Pas de Content-Type, le navigateur gère pour FormData
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
@ -187,10 +186,9 @@ export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
|
||||
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
body: data,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ import {
|
||||
FileUp,
|
||||
PenTool,
|
||||
} from 'lucide-react';
|
||||
import CheckBox from '@/components/Form/CheckBox';
|
||||
|
||||
const FIELD_TYPES_ICON = {
|
||||
text: { icon: TextCursorInput },
|
||||
@ -520,29 +521,27 @@ export default function FormTemplateBuilder({
|
||||
Groupes d'inscription{' '}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||
<div className="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
|
||||
{groups && groups.length > 0 ? (
|
||||
groups.map((group) => (
|
||||
<label key={group.id} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedGroups.includes(group.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedGroups([
|
||||
...selectedGroups,
|
||||
group.id,
|
||||
]);
|
||||
} else {
|
||||
setSelectedGroups(
|
||||
selectedGroups.filter((id) => id !== group.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="mr-2 text-blue-600"
|
||||
/>
|
||||
<span className="text-sm">{group.name}</span>
|
||||
</label>
|
||||
<CheckBox
|
||||
key={group.id}
|
||||
item={{ id: group.id }}
|
||||
formData={{
|
||||
groups: selectedGroups
|
||||
}}
|
||||
handleChange={() => {
|
||||
let group_ids = selectedGroups;
|
||||
if (group_ids.includes(group.id)) {
|
||||
group_ids = group_ids.filter((id) => id !== group.id);
|
||||
} else {
|
||||
group_ids = [...group_ids, group.id];
|
||||
}
|
||||
setSelectedGroups(group_ids);
|
||||
}}
|
||||
fieldName="groups"
|
||||
itemLabelFunc={() => group.name}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import InputText from '@/components/Form/InputText';
|
||||
import Button from '@/components/Form/Button';
|
||||
|
||||
export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
|
||||
const [name, setName] = useState('');
|
||||
@ -18,38 +20,28 @@ export default function RegistrationFileGroupForm({ onSubmit, initialData }) {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nom du groupe
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/* Utilisation de InputText pour le nom du groupe */}
|
||||
<InputText
|
||||
label="Nom du groupe"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<InputText
|
||||
label="Description"
|
||||
name="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
<Button
|
||||
primary
|
||||
type="submit"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
{initialData ? 'Modifier le groupe' : 'Créer le groupe'}
|
||||
</button>
|
||||
text="Enregistrer"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user