mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
509 lines
21 KiB
Python
509 lines
21 KiB
Python
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()
|
||
|
||
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 ou mettre à jour les existants si le master a changé
|
||
for m in school_masters:
|
||
tmpl_qs = RegistrationSchoolFileTemplate.objects.filter(master=m, registration_form=register_form)
|
||
tmpl = tmpl_qs.first() if tmpl_qs.exists() else None
|
||
|
||
base_slug = (m.name or "master").strip().replace(" ", "_")[:40]
|
||
slug = f"{base_slug}_{register_form.pk}_{m.pk}"
|
||
|
||
file_name = None
|
||
if m.file and hasattr(m.file, 'name') and m.file.name:
|
||
file_name = os.path.basename(m.file.name)
|
||
elif m.file:
|
||
file_name = str(m.file)
|
||
else:
|
||
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
|
||
|
||
from django.core.files.base import ContentFile
|
||
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
|
||
|
||
if tmpl:
|
||
template_file_name = os.path.basename(tmpl.file.name) if tmpl.file and tmpl.file.name else None
|
||
master_file_changed = template_file_name != file_name
|
||
# --- GESTION FORM EXISTANT : suppression ancien template si nom ou contenu master changé ---
|
||
if master_file_changed or (
|
||
master_file_path and os.path.exists(master_file_path) and
|
||
(not tmpl.file or not os.path.exists(abs_path) or os.path.getmtime(master_file_path) > os.path.getmtime(abs_path))
|
||
):
|
||
# Supprimer l'ancien fichier du template (même si le nom change)
|
||
if tmpl.file and tmpl.file.name:
|
||
old_template_path = os.path.join(settings.MEDIA_ROOT, tmpl.file.name)
|
||
if os.path.exists(old_template_path):
|
||
try:
|
||
os.remove(old_template_path)
|
||
logger.info(f"util.create_templates_for_registration_form - Suppression ancien fichier template: {old_template_path}")
|
||
except Exception as e:
|
||
logger.error(f"Erreur suppression ancien fichier template: {e}")
|
||
# Copier le nouveau fichier du master (form existant)
|
||
if master_file_path and os.path.exists(master_file_path):
|
||
try:
|
||
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||
import shutil
|
||
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}")
|
||
tmpl.file.name = upload_rel_path
|
||
tmpl.name = m.name or ""
|
||
tmpl.slug = slug
|
||
tmpl.formTemplateData = m.formMasterData or []
|
||
tmpl.save()
|
||
except Exception as e:
|
||
logger.error(f"Erreur lors de la copie du fichier master pour mise à jour du template: {e}")
|
||
created.append(tmpl)
|
||
logger.info("util.create_templates_for_registration_form - Mise à jour school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
|
||
continue
|
||
|
||
# Sinon, création du template comme avant
|
||
tmpl = RegistrationSchoolFileTemplate(
|
||
master=m,
|
||
registration_form=register_form,
|
||
name=m.name or "",
|
||
formTemplateData=m.formMasterData or [],
|
||
slug=slug,
|
||
)
|
||
if file_name:
|
||
# Copier le fichier du master si besoin (form existant)
|
||
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}")
|
||
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 d’inscription.
|
||
"""
|
||
context = {
|
||
"ficheInscriptions_list": bdd.getAllObjects(RegistrationForm),
|
||
}
|
||
return context
|
||
|
||
def recupereListeFichesInscriptionEnAttenteSEPA():
|
||
"""
|
||
Retourne les fiches d’inscription 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 l’heure 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 d’un point de départ et d’un 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 d’une liste extraite d’un QueryDict.
|
||
"""
|
||
return liste[0]
|
||
|
||
def getArgFromRequest(_argument, _request):
|
||
"""
|
||
Extrait la valeur d’un 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))
|