Files
n3wt-school/Back-End/Subscriptions/util.py
N3WT DE COMPET b4f70e6bad feat: Sauvegarde des formulaires d'école dans les bons dossiers /
utilisation des bons composants dans les modales [N3WTS-17]
2026-01-18 18:44:13 +01:00

484 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.shortcuts import render,get_object_or_404,get_list_or_404
from .models import RegistrationForm, Student, Guardian, Sibling
import time
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
from django.conf import settings
from N3wtSchool import renderers
from N3wtSchool import bdd
from io import BytesIO
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from django.core.files.base import ContentFile
from django.core.files import File
from pathlib import Path
import os
from enum import Enum
import random
import string
from rest_framework.parsers import JSONParser
from PyPDF2 import PdfMerger
import shutil
import logging
import json
from django.http import QueryDict
from rest_framework.response import Response
from rest_framework import status
logger = logging.getLogger(__name__)
def build_payload_from_request(request):
"""
Normalise la request en payload prêt à être donné au serializer.
- supporte multipart/form-data où le front envoie 'data' (JSON string) ou un fichier JSON + fichiers
- supporte application/json ou form-data simple
Retour: (payload_dict, None) ou (None, Response erreur)
"""
# Si c'est du JSON pur (Content-Type: application/json)
if hasattr(request, 'content_type') and 'application/json' in request.content_type:
try:
# request.data contient déjà le JSON parsé par Django REST
payload = dict(request.data) if hasattr(request.data, 'items') else request.data
logger.info(f"JSON payload extracted: {payload}")
return payload, None
except Exception as e:
logger.error(f'Error processing JSON: {e}')
return None, Response({'error': "Invalid JSON", 'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
# Cas multipart/form-data avec champ 'data'
data_field = request.data.get('data') if hasattr(request.data, 'get') else None
if data_field:
try:
# Si 'data' est un fichier (InMemoryUploadedFile ou fichier similaire), lire et décoder
if hasattr(data_field, 'read'):
raw = data_field.read()
if isinstance(raw, (bytes, bytearray)):
text = raw.decode('utf-8')
else:
text = raw
payload = json.loads(text)
# Si 'data' est bytes déjà
elif isinstance(data_field, (bytes, bytearray)):
payload = json.loads(data_field.decode('utf-8'))
# Si 'data' est une string JSON
elif isinstance(data_field, str):
payload = json.loads(data_field)
else:
# type inattendu
raise ValueError(f"Unsupported 'data' type: {type(data_field)}")
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
logger.error(f'Invalid JSON in "data": {e}')
return None, Response({'error': "Invalid JSON in 'data'", 'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
else:
payload = request.data.copy() if hasattr(request.data, 'copy') else dict(request.data)
if isinstance(payload, QueryDict):
payload = payload.dict()
# Attacher les fichiers présents (ex: photo, files.*, etc.), sauf 'data' (déjà traité)
for f_key, f_val in request.FILES.items():
if f_key == 'data':
# remettre le pointeur au début si besoin (déjà lu) — non indispensable ici mais sûr
try:
f_val.seek(0)
except Exception:
pass
# ne pas mettre le fichier 'data' dans le payload (c'est le JSON)
continue
payload[f_key] = f_val
return payload, None
def create_templates_for_registration_form(register_form):
"""
Idempotent:
- supprime les templates existants qui ne correspondent pas
aux masters du fileGroup courant du register_form (et supprime leurs fichiers).
- crée les templates manquants pour les masters du fileGroup courant.
Retourne la liste des templates créés.
"""
from Subscriptions.models import (
RegistrationSchoolFileMaster,
RegistrationSchoolFileTemplate,
RegistrationParentFileMaster,
RegistrationParentFileTemplate,
registration_school_file_upload_to,
)
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)
if not current_group:
# Si plus de fileGroup, supprimer tous les templates existants pour ce RF
school_existing = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form)
for t in school_existing:
try:
if getattr(t, "file", None):
logger.info("Deleted school template %s for RF %s", t.pk, register_form.pk)
t.file.delete(save=False)
except Exception:
logger.exception("Erreur suppression fichier school template %s", getattr(t, "pk", None))
t.delete()
parent_existing = RegistrationParentFileTemplate.objects.filter(registration_form=register_form)
for t in parent_existing:
try:
if getattr(t, "file", None):
t.file.delete(save=False)
except Exception:
logger.exception("Erreur suppression fichier parent template %s", getattr(t, "pk", None))
t.delete()
return created
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}
# Supprimer les school templates obsolètes
for tmpl in RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form):
if not tmpl.master_id or tmpl.master_id not in school_master_ids:
try:
if getattr(tmpl, "file", None):
tmpl.file.delete(save=False)
except Exception:
logger.exception("Erreur suppression fichier school template obsolète %s", getattr(tmpl, "pk", None))
tmpl.delete()
logger.info("Deleted obsolete school template %s for RF %s", getattr(tmpl, "pk", None), register_form.pk)
# Supprimer les parent templates obsolètes
for tmpl in RegistrationParentFileTemplate.objects.filter(registration_form=register_form):
if not tmpl.master_id or tmpl.master_id not in parent_master_ids:
try:
if getattr(tmpl, "file", None):
tmpl.file.delete(save=False)
except Exception:
logger.exception("Erreur suppression fichier parent template obsolète %s", getattr(tmpl, "pk", None))
tmpl.delete()
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:
continue
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
# --- 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 si besoin (rare ici)
try:
pdf_file = generate_form_json_pdf(register_form, m.formMasterData)
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_name = None
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,
)
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("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:
exists = RegistrationParentFileTemplate.objects.filter(master=m, registration_form=register_form).exists()
if exists:
continue
tmpl = RegistrationParentFileTemplate.objects.create(
master=m,
registration_form=register_form,
file=None,
)
created.append(tmpl)
logger.info("Created parent template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
return created
def recupereListeFichesInscription():
"""
Retourne la liste complète des fiches dinscription.
"""
context = {
"ficheInscriptions_list": bdd.getAllObjects(RegistrationForm),
}
return context
def recupereListeFichesInscriptionEnAttenteSEPA():
"""
Retourne les fiches dinscription avec paiement SEPA en attente.
"""
ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE'])
return ficheInscriptionsSEPA_list
def _now():
"""
Retourne la date et lheure en cours, avec fuseau.
"""
return datetime.now(ZoneInfo(settings.TZ_APPLI))
def convertToStr(dateValue, dateFormat):
"""
Convertit un objet datetime en chaîne selon un format donné.
"""
return dateValue.strftime(dateFormat)
def convertToDate(date_time):
"""
Convertit une chaîne en objet datetime selon le format '%d-%m-%Y %H:%M'.
"""
format = '%d-%m-%Y %H:%M'
datetime_str = datetime.strptime(date_time, format)
return datetime_str
def convertTelephone(telephoneValue, separator='-'):
"""
Reformate un numéro de téléphone en y insérant un séparateur donné.
"""
return f"{telephoneValue[:2]}{separator}{telephoneValue[2:4]}{separator}{telephoneValue[4:6]}{separator}{telephoneValue[6:8]}{separator}{telephoneValue[8:10]}"
def genereRandomCode(length):
"""
Génère un code aléatoire de longueur spécifiée.
"""
return ''.join(random.choice(string.ascii_letters) for i in range(length))
def calculeDatePeremption(_start, nbDays):
"""
Calcule la date de fin à partir dun point de départ et dun nombre de jours.
"""
return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT)
# Fonction permettant de retourner la valeur du QueryDict
# QueryDict [ index ] -> Dernière valeur d'une liste
# dict (QueryDict [ index ]) -> Toutes les valeurs de la liste
def _(liste):
"""
Retourne la première valeur dune liste extraite dun QueryDict.
"""
return liste[0]
def getArgFromRequest(_argument, _request):
"""
Extrait la valeur dun argument depuis la requête (JSON).
"""
resultat = None
data=JSONParser().parse(_request)
resultat = data[_argument]
return resultat
def merge_files_pdf(file_paths):
"""
Fusionne plusieurs fichiers PDF et retourne le contenu fusionné en mémoire.
"""
merger = PdfMerger()
# Ajouter les fichiers valides au merger
for file_path in file_paths:
merger.append(file_path)
# Sauvegarder le fichier fusionné en mémoire
merged_pdf = BytesIO()
merger.write(merged_pdf)
merger.close()
# Revenir au début du fichier en mémoire
merged_pdf.seek(0)
return merged_pdf
def rfToPDF(registerForm, filename):
"""
Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm.
"""
filename = filename.replace(" ", "_")
data = {
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
'signatureTime': convertToStr(_now(), '%H:%M'),
'student': registerForm.student,
}
# Générer le PDF
pdf = renderers.render_to_pdf('pdfs/fiche_eleve.html', data)
if not pdf:
raise ValueError("Erreur lors de la génération du PDF.")
# Vérifier si un fichier avec le même nom existe déjà et le supprimer
if registerForm.registration_file and registerForm.registration_file.name:
# Vérifiez si le chemin est déjà absolu ou relatif
if os.path.isabs(registerForm.registration_file.name):
existing_file_path = registerForm.registration_file.name
else:
existing_file_path = os.path.join(settings.MEDIA_ROOT, registerForm.registration_file.name.lstrip('/'))
# Vérifier si le fichier existe et le supprimer
if os.path.exists(existing_file_path):
os.remove(existing_file_path)
registerForm.registration_file.delete(save=False)
else:
print(f'File does not exist: {existing_file_path}')
# Enregistrer directement le fichier dans le champ registration_file
try:
registerForm.registration_file.save(
os.path.basename(filename), # Utiliser uniquement le nom de fichier
File(BytesIO(pdf.content)),
save=True
)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
raise
return registerForm.registration_file
def delete_registration_files(registerForm):
"""
Supprime le fichier et le dossier associés à un RegistrationForm.
"""
base_dir = f"registration_files/dossier_rf_{registerForm.pk}"
if registerForm.registration_file and os.path.exists(registerForm.registration_file.path):
os.remove(registerForm.registration_file.path)
registerForm.registration_file.delete(save=False)
if os.path.exists(base_dir):
shutil.rmtree(base_dir)
from datetime import datetime
def getCurrentSchoolYear():
"""
Retourne l'année scolaire en cours au format "YYYY-YYYY".
Exemple : Si nous sommes en octobre 2023, retourne "2023-2024".
"""
now = datetime.now()
current_year = now.year
current_month = now.month
# Si nous sommes avant septembre, l'année scolaire a commencé l'année précédente
start_year = current_year if current_month >= 9 else current_year - 1
return f"{start_year}-{start_year + 1}"
def getNextSchoolYear():
"""
Retourne l'année scolaire suivante au format "YYYY-YYYY".
Exemple : Si nous sommes en octobre 2023, retourne "2024-2025".
"""
current_school_year = getCurrentSchoolYear()
start_year, end_year = map(int, current_school_year.split('-'))
return f"{start_year + 1}-{end_year + 1}"
def getHistoricalYears(count=5):
"""
Retourne un tableau des années scolaires passées au format "YYYY-YYYY".
Exemple : ["2022-2023", "2021-2022", "2020-2021"].
:param count: Le nombre d'années scolaires passées à inclure.
"""
current_school_year = getCurrentSchoolYear()
start_year = int(current_school_year.split('-')[0])
historical_years = []
for i in range(1, count + 1):
historical_start_year = start_year - i
historical_years.append(f"{historical_start_year}-{historical_start_year + 1}")
return historical_years
def generate_form_json_pdf(register_form, form_json):
"""
Génère un PDF du rendu du formulaire dynamique à partir du JSON (formConfig)
et l'associe au RegistrationSchoolFileTemplate.
Le PDF contient le titre, les labels et types de champs.
Retourne un ContentFile prêt à être utilisé dans un FileField, avec uniquement le nom du fichier.
"""
# Récupérer le nom du formulaire
form_name = (form_json.get("title") or "formulaire").strip().replace(" ", "_")
filename = f"{form_name}.pdf"
# Générer le PDF
buffer = BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
y = 800
# Titre
c.setFont("Helvetica-Bold", 20)
c.drawString(100, y, form_json.get("title", "Formulaire"))
y -= 40
# Champs
c.setFont("Helvetica", 12)
fields = form_json.get("fields", [])
for field in fields:
label = field.get("label", field.get("id", ""))
ftype = field.get("type", "")
c.drawString(100, y, f"{label} [{ftype}]")
y -= 25
if y < 100:
c.showPage()
y = 800
c.save()
buffer.seek(0)
pdf_content = buffer.read()
# Si un fichier existe déjà sur le template, le supprimer (optionnel, à adapter selon usage)
if hasattr(register_form, "registration_file") and register_form.registration_file and register_form.registration_file.name:
existing_file_path = os.path.join(settings.MEDIA_ROOT, register_form.registration_file.name.lstrip('/'))
if os.path.exists(existing_file_path):
os.remove(existing_file_path)
register_form.registration_file.delete(save=False)
# Retourner le ContentFile avec uniquement le nom du fichier
return ContentFile(pdf_content, name=os.path.basename(filename))