mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
942 lines
37 KiB
Python
942 lines
37 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
|
||
import base64
|
||
from reportlab.pdfgen import canvas
|
||
from reportlab.lib.pagesizes import A4
|
||
from reportlab.lib.utils import ImageReader
|
||
from reportlab.graphics import renderPDF
|
||
from django.core.files.base import ContentFile
|
||
from django.core.files import File
|
||
from pathlib import Path
|
||
import os
|
||
from enum import Enum
|
||
from urllib.parse import unquote_to_bytes
|
||
|
||
import random
|
||
import string
|
||
from rest_framework.parsers import JSONParser
|
||
from PyPDF2 import PdfMerger, PdfReader, PdfWriter
|
||
from PyPDF2.errors import PdfReadError
|
||
|
||
import shutil
|
||
import logging
|
||
|
||
import json
|
||
from django.http import QueryDict
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from svglib.svglib import svg2rlg
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _draw_signature_data_url(cnv, data_url, x, y, width, height):
|
||
"""
|
||
Dessine une signature issue d'un data URL dans un canvas ReportLab.
|
||
Supporte les images raster (PNG/JPEG/...) et SVG.
|
||
Retourne True si la signature a pu etre dessinee.
|
||
"""
|
||
if not isinstance(data_url, str) or not data_url.startswith("data:image"):
|
||
return False
|
||
|
||
try:
|
||
header, payload = data_url.split(',', 1)
|
||
except ValueError:
|
||
return False
|
||
|
||
is_base64 = ';base64' in header
|
||
mime_type = header.split(':', 1)[1].split(';', 1)[0] if ':' in header else ''
|
||
|
||
try:
|
||
raw_bytes = base64.b64decode(payload) if is_base64 else unquote_to_bytes(payload)
|
||
except Exception as e:
|
||
logger.error(f"[_draw_signature_data_url] Decodage impossible: {e}")
|
||
return False
|
||
|
||
# Support SVG via svglib (deja present dans requirements)
|
||
if mime_type == 'image/svg+xml':
|
||
try:
|
||
drawing = svg2rlg(BytesIO(raw_bytes))
|
||
if drawing is None:
|
||
return False
|
||
|
||
src_w = float(getattr(drawing, 'width', 0) or 0)
|
||
src_h = float(getattr(drawing, 'height', 0) or 0)
|
||
if src_w <= 0 or src_h <= 0:
|
||
return False
|
||
|
||
scale = min(width / src_w, height / src_h)
|
||
draw_w = src_w * scale
|
||
draw_h = src_h * scale
|
||
offset_x = x + (width - draw_w) / 2
|
||
offset_y = y + (height - draw_h) / 2
|
||
|
||
cnv.saveState()
|
||
cnv.translate(offset_x, offset_y)
|
||
cnv.scale(scale, scale)
|
||
renderPDF.draw(drawing, cnv, 0, 0)
|
||
cnv.restoreState()
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"[_draw_signature_data_url] Rendu SVG impossible: {e}")
|
||
return False
|
||
|
||
# Support images raster classiques
|
||
try:
|
||
img_reader = ImageReader(BytesIO(raw_bytes))
|
||
cnv.drawImage(
|
||
img_reader,
|
||
x,
|
||
y,
|
||
width=width,
|
||
height=height,
|
||
preserveAspectRatio=True,
|
||
mask='auto',
|
||
)
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"[_draw_signature_data_url] Rendu raster impossible: {e}")
|
||
return False
|
||
|
||
def save_file_replacing_existing(file_field, filename, content, save=True):
|
||
"""
|
||
Sauvegarde un fichier en écrasant l'existant s'il porte le même nom.
|
||
Évite les suffixes automatiques Django (ex: fichier_N5QdZpk.pdf).
|
||
|
||
Args:
|
||
file_field: Le FileField Django (ex: registerForm.registration_file)
|
||
filename: Le nom du fichier à sauvegarder
|
||
content: Le contenu du fichier (File, BytesIO, ContentFile, etc.)
|
||
save: Si True, sauvegarde l'instance parente
|
||
"""
|
||
# Supprimer l'ancien fichier s'il existe
|
||
if file_field and file_field.name:
|
||
try:
|
||
if hasattr(file_field, 'path') and os.path.exists(file_field.path):
|
||
os.remove(file_field.path)
|
||
logger.debug(f"[save_file] Ancien fichier supprimé: {file_field.path}")
|
||
except Exception as e:
|
||
logger.error(f"[save_file] Erreur suppression ancien fichier: {e}")
|
||
|
||
# Sauvegarder le nouveau fichier
|
||
file_field.save(filename, content, save=save)
|
||
|
||
def save_file_field_without_suffix(instance, field_name, filename, content, save=False):
|
||
"""
|
||
Sauvegarde un fichier dans un FileField Django en ecrasant le precedent,
|
||
sans laisser Django generer de suffixe (_abc123).
|
||
|
||
Args:
|
||
instance: instance Django portant le FileField
|
||
field_name: nom du FileField (ex: 'file')
|
||
filename: nom de fichier cible (basename)
|
||
content: contenu fichier (ContentFile, File, etc.)
|
||
save: si True, persiste immediatement l'instance
|
||
"""
|
||
file_field = getattr(instance, field_name)
|
||
field = instance._meta.get_field(field_name)
|
||
storage = file_field.storage
|
||
|
||
target_name = field.generate_filename(instance, filename)
|
||
|
||
# Supprimer le fichier actuellement reference si different
|
||
if file_field and file_field.name and file_field.name != target_name:
|
||
try:
|
||
if storage.exists(file_field.name):
|
||
storage.delete(file_field.name)
|
||
except Exception as e:
|
||
logger.error(f"[save_file_field_without_suffix] Erreur suppression ancien fichier ({file_field.name}): {e}")
|
||
|
||
# Supprimer explicitement la cible si elle existe deja
|
||
try:
|
||
if storage.exists(target_name):
|
||
storage.delete(target_name)
|
||
except Exception as e:
|
||
logger.error(f"[save_file_field_without_suffix] Erreur suppression cible ({target_name}): {e}")
|
||
|
||
# Sauvegarde: la cible n'existe plus, donc pas de suffixe
|
||
file_field.save(filename, content, save=save)
|
||
|
||
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}"
|
||
|
||
is_dynamic_master = (
|
||
isinstance(m.formMasterData, dict)
|
||
and bool(m.formMasterData.get("fields"))
|
||
)
|
||
|
||
# Formulaire dynamique: toujours générer le PDF final depuis le JSON
|
||
# (aperçu admin) au lieu de copier le fichier source brut (PNG/PDF).
|
||
if is_dynamic_master:
|
||
base_pdf_content = None
|
||
base_file_ext = None
|
||
if m.file and hasattr(m.file, 'name') and m.file.name:
|
||
base_file_ext = os.path.splitext(m.file.name)[1].lower()
|
||
try:
|
||
m.file.open('rb')
|
||
base_pdf_content = m.file.read()
|
||
m.file.close()
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture fichier source master dynamique: {e}")
|
||
|
||
try:
|
||
generated_pdf = generate_form_json_pdf(
|
||
register_form,
|
||
m.formMasterData,
|
||
base_pdf_content=base_pdf_content,
|
||
base_file_ext=base_file_ext,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Erreur génération PDF dynamique pour template: {e}")
|
||
generated_pdf = None
|
||
|
||
if tmpl:
|
||
try:
|
||
if tmpl.file and tmpl.file.name:
|
||
tmpl.file.delete(save=False)
|
||
except Exception:
|
||
logger.exception("Erreur suppression ancien fichier template dynamique %s", getattr(tmpl, "pk", None))
|
||
|
||
tmpl.name = m.name or ""
|
||
tmpl.slug = slug
|
||
tmpl.formTemplateData = m.formMasterData or []
|
||
if generated_pdf is not None:
|
||
output_filename = os.path.basename(generated_pdf.name)
|
||
save_file_field_without_suffix(
|
||
tmpl,
|
||
'file',
|
||
output_filename,
|
||
generated_pdf,
|
||
save=False,
|
||
)
|
||
tmpl.save()
|
||
created.append(tmpl)
|
||
logger.info(
|
||
"util.create_templates_for_registration_form - Regenerated dynamic school template %s from master %s for RF %s",
|
||
tmpl.pk,
|
||
m.pk,
|
||
register_form.pk,
|
||
)
|
||
continue
|
||
|
||
tmpl = RegistrationSchoolFileTemplate(
|
||
master=m,
|
||
registration_form=register_form,
|
||
name=m.name or "",
|
||
formTemplateData=m.formMasterData or [],
|
||
slug=slug,
|
||
)
|
||
if generated_pdf is not None:
|
||
output_filename = os.path.basename(generated_pdf.name)
|
||
save_file_field_without_suffix(
|
||
tmpl,
|
||
'file',
|
||
output_filename,
|
||
generated_pdf,
|
||
save=False,
|
||
)
|
||
tmpl.save()
|
||
created.append(tmpl)
|
||
logger.info(
|
||
"util.create_templates_for_registration_form - Created dynamic school template %s from master %s for RF %s",
|
||
tmpl.pk,
|
||
m.pk,
|
||
register_form.pk,
|
||
)
|
||
continue
|
||
|
||
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
|
||
|
||
master_file_path = m.file.path if m.file and hasattr(m.file, 'path') else None
|
||
|
||
def _build_upload_path(template_pk):
|
||
"""Génère le chemin relatif et absolu pour un template avec un pk connu."""
|
||
rel = registration_school_file_upload_to(
|
||
type("Tmp", (), {
|
||
"registration_form": register_form,
|
||
"pk": template_pk,
|
||
})(),
|
||
file_name,
|
||
)
|
||
return rel, os.path.join(settings.MEDIA_ROOT, rel)
|
||
|
||
if tmpl:
|
||
upload_rel_path, abs_path = _build_upload_path(tmpl.pk)
|
||
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 — sauvegarder d'abord pour obtenir un pk
|
||
tmpl = RegistrationSchoolFileTemplate(
|
||
master=m,
|
||
registration_form=register_form,
|
||
name=m.name or "",
|
||
formTemplateData=m.formMasterData or [],
|
||
slug=slug,
|
||
)
|
||
tmpl.save() # pk attribué ici
|
||
if file_name:
|
||
upload_rel_path, abs_path = _build_upload_path(tmpl.pk)
|
||
# Copier le fichier du master si besoin
|
||
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.
|
||
Les fichiers non-PDF (images) sont convertis en PDF avant fusion.
|
||
Les fichiers invalides sont ignorés avec un log d'erreur.
|
||
"""
|
||
merger = PdfMerger()
|
||
files_added = 0
|
||
|
||
# Extensions d'images supportées
|
||
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif'}
|
||
|
||
for file_path in file_paths:
|
||
# Vérifier que le fichier existe
|
||
if not os.path.exists(file_path):
|
||
logger.warning(f"[merge_files_pdf] Fichier introuvable, ignoré: {file_path}")
|
||
continue
|
||
|
||
file_ext = os.path.splitext(file_path)[1].lower()
|
||
|
||
# Si c'est une image, la convertir en PDF
|
||
if file_ext in image_extensions:
|
||
try:
|
||
from PIL import Image
|
||
from reportlab.lib.utils import ImageReader
|
||
|
||
img = Image.open(file_path)
|
||
img_width, img_height = img.size
|
||
|
||
# Créer un PDF en mémoire avec l'image
|
||
img_pdf = BytesIO()
|
||
c = canvas.Canvas(img_pdf, pagesize=(img_width, img_height))
|
||
c.drawImage(file_path, 0, 0, width=img_width, height=img_height)
|
||
c.save()
|
||
img_pdf.seek(0)
|
||
|
||
merger.append(img_pdf)
|
||
files_added += 1
|
||
logger.debug(f"[merge_files_pdf] Image convertie et ajoutée: {file_path}")
|
||
except Exception as e:
|
||
logger.error(f"[merge_files_pdf] Erreur lors de la conversion de l'image {file_path}: {e}")
|
||
continue
|
||
|
||
# Sinon, essayer de l'ajouter comme PDF
|
||
try:
|
||
# Valider que c'est un PDF lisible
|
||
with open(file_path, 'rb') as f:
|
||
PdfReader(f, strict=False)
|
||
|
||
# Si la validation passe, ajouter au merger
|
||
merger.append(file_path)
|
||
files_added += 1
|
||
logger.debug(f"[merge_files_pdf] PDF ajouté: {file_path}")
|
||
except PdfReadError as e:
|
||
logger.error(f"[merge_files_pdf] Fichier PDF invalide, ignoré: {file_path} - {e}")
|
||
except Exception as e:
|
||
logger.error(f"[merge_files_pdf] Erreur lors de la lecture du fichier {file_path}: {e}")
|
||
|
||
if files_added == 0:
|
||
logger.warning("[merge_files_pdf] Aucun fichier valide à fusionner")
|
||
# Retourner un PDF vide
|
||
empty_pdf = BytesIO()
|
||
c = canvas.Canvas(empty_pdf, pagesize=A4)
|
||
c.drawString(100, 750, "Aucun document à afficher")
|
||
c.save()
|
||
empty_pdf.seek(0)
|
||
return empty_pdf
|
||
|
||
# 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,
|
||
'establishment': registerForm.establishment,
|
||
'school_year': registerForm.school_year,
|
||
}
|
||
|
||
# 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.")
|
||
|
||
# Enregistrer directement le fichier dans le champ registration_file (écrase l'existant)
|
||
try:
|
||
save_file_replacing_existing(
|
||
registerForm.registration_file,
|
||
os.path.basename(filename),
|
||
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 generateRegistrationPDF(registerForm):
|
||
"""
|
||
Génère le PDF d'un dossier d'inscription à la volée et retourne le contenu binaire.
|
||
Ne sauvegarde pas le fichier sur disque.
|
||
"""
|
||
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,
|
||
'establishment': registerForm.establishment,
|
||
'school_year': registerForm.school_year,
|
||
}
|
||
pdf = renderers.render_to_pdf('pdfs/fiche_eleve.html', data)
|
||
if not pdf:
|
||
raise ValueError("Erreur lors de la génération du PDF.")
|
||
return pdf.content
|
||
|
||
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, base_pdf_content=None, base_file_ext=None, base_pdf_path=None):
|
||
"""
|
||
Génère un PDF composite du formulaire dynamique:
|
||
- le document source uploadé (PDF/image) si présent,
|
||
- puis un rendu du formulaire (similaire à l'aperçu),
|
||
- avec overlay de signature(s) sur la dernière page du document source.
|
||
"""
|
||
|
||
form_name = (form_json.get("title") or "formulaire").strip().replace(" ", "_")
|
||
filename = f"{form_name}.pdf"
|
||
fields = form_json.get("fields", []) if isinstance(form_json, dict) else []
|
||
|
||
# Compatibilité ascendante : charger depuis un chemin si nécessaire
|
||
if base_pdf_content is None and base_pdf_path and os.path.exists(base_pdf_path):
|
||
base_file_ext = os.path.splitext(base_pdf_path)[1].lower()
|
||
try:
|
||
with open(base_pdf_path, 'rb') as f:
|
||
base_pdf_content = f.read()
|
||
except Exception as e:
|
||
logger.error(f"[generate_form_json_pdf] Lecture fichier source: {e}")
|
||
|
||
writer = PdfWriter()
|
||
has_source_document = False
|
||
source_is_image = False
|
||
source_image_reader = None
|
||
source_image_size = None
|
||
|
||
# 1) Charger le document source (PDF/image) si présent
|
||
if base_pdf_content:
|
||
try:
|
||
image_exts = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
|
||
ext = (base_file_ext or '').lower()
|
||
|
||
if ext in image_exts:
|
||
# Pour les images, on les rend dans la section [fichier uploade]
|
||
# au lieu de les ajouter comme page source separee.
|
||
img_buffer = BytesIO(base_pdf_content)
|
||
source_image_reader = ImageReader(img_buffer)
|
||
source_image_size = source_image_reader.getSize()
|
||
source_is_image = True
|
||
else:
|
||
source_reader = PdfReader(BytesIO(base_pdf_content))
|
||
for page in source_reader.pages:
|
||
writer.add_page(page)
|
||
has_source_document = len(source_reader.pages) > 0
|
||
except Exception as e:
|
||
logger.error(f"[generate_form_json_pdf] Erreur chargement source: {e}")
|
||
|
||
# 2) Overlay des signatures sur la dernière page du document source
|
||
# Desactive ici pour eviter les doublons: la signature est rendue
|
||
# dans la section JSON du formulaire (et non plus en overlay source).
|
||
signatures = []
|
||
for field in fields:
|
||
if field.get("type") == "signature":
|
||
value = field.get("value")
|
||
if isinstance(value, str) and value.startswith("data:image"):
|
||
signatures.append(value)
|
||
|
||
enable_source_signature_overlay = False
|
||
if signatures and len(writer.pages) > 0 and enable_source_signature_overlay:
|
||
try:
|
||
target_page = writer.pages[len(writer.pages) - 1]
|
||
page_width = float(target_page.mediabox.width)
|
||
page_height = float(target_page.mediabox.height)
|
||
|
||
packet = BytesIO()
|
||
c_overlay = canvas.Canvas(packet, pagesize=(page_width, page_height))
|
||
|
||
sig_width = 170
|
||
sig_height = 70
|
||
margin = 36
|
||
spacing = 10
|
||
|
||
for i, data_url in enumerate(signatures[:3]):
|
||
try:
|
||
x = page_width - sig_width - margin
|
||
y = margin + i * (sig_height + spacing)
|
||
_draw_signature_data_url(c_overlay, data_url, x, y, sig_width, sig_height)
|
||
except Exception as e:
|
||
logger.error(f"[generate_form_json_pdf] Signature ignorée: {e}")
|
||
|
||
c_overlay.save()
|
||
packet.seek(0)
|
||
overlay_pdf = PdfReader(packet)
|
||
if overlay_pdf.pages:
|
||
target_page.merge_page(overlay_pdf.pages[0])
|
||
except Exception as e:
|
||
logger.error(f"[generate_form_json_pdf] Erreur overlay signature: {e}")
|
||
|
||
# 3) Rendu JSON explicite du formulaire final (toujours genere).
|
||
# Cela garantit la presence des sections H1 / FileUpload / Signature
|
||
# dans le PDF final, meme si un document source est fourni.
|
||
fields_to_render = fields
|
||
|
||
if fields_to_render:
|
||
layout_buffer = BytesIO()
|
||
c = canvas.Canvas(layout_buffer, pagesize=A4)
|
||
y = 800
|
||
|
||
c.setFont("Helvetica-Bold", 18)
|
||
c.drawString(60, y, form_json.get("title", "Formulaire"))
|
||
y -= 35
|
||
|
||
c.setFont("Helvetica", 11)
|
||
for field in fields_to_render:
|
||
ftype = field.get("type", "")
|
||
if ftype in {"heading1", "heading2", "heading3", "heading4", "heading5", "heading6", "paragraph"}:
|
||
text = field.get("text", "")
|
||
if text:
|
||
c.setFont("Helvetica-Bold" if ftype.startswith("heading") else "Helvetica", 11)
|
||
c.drawString(60, y, text[:120])
|
||
y -= 18
|
||
c.setFont("Helvetica", 11)
|
||
continue
|
||
|
||
label = field.get("label", field.get("id", "Champ"))
|
||
value = field.get("value", "")
|
||
|
||
if ftype == "file":
|
||
c.drawString(60, y, f"{label}")
|
||
y -= 18
|
||
|
||
if source_is_image and source_image_reader and source_image_size:
|
||
img_w, img_h = source_image_size
|
||
max_w = 420
|
||
max_h = 260
|
||
ratio = min(max_w / img_w, max_h / img_h)
|
||
draw_w = img_w * ratio
|
||
draw_h = img_h * ratio
|
||
|
||
if y - draw_h < 80:
|
||
c.showPage()
|
||
y = 800
|
||
c.setFont("Helvetica", 11)
|
||
|
||
c.drawImage(
|
||
source_image_reader,
|
||
60,
|
||
y - draw_h,
|
||
width=draw_w,
|
||
height=draw_h,
|
||
preserveAspectRatio=True,
|
||
mask='auto',
|
||
)
|
||
y -= draw_h + 14
|
||
elif ftype == "signature":
|
||
c.drawString(60, y, f"{label}")
|
||
sig_drawn = False
|
||
if isinstance(value, str) and value.startswith("data:image"):
|
||
try:
|
||
sig_drawn = _draw_signature_data_url(c, value, 260, y - 55, 170, 55)
|
||
except Exception as e:
|
||
logger.error(f"[generate_form_json_pdf] Signature render echoue: {e}")
|
||
if not sig_drawn:
|
||
c.rect(260, y - 55, 170, 55)
|
||
y -= 70
|
||
else:
|
||
if value not in (None, ""):
|
||
c.drawString(60, y, f"{label} [{ftype}] : {str(value)[:120]}")
|
||
else:
|
||
c.drawString(60, y, f"{label} [{ftype}]")
|
||
y -= 18
|
||
|
||
if y < 80:
|
||
c.showPage()
|
||
y = 800
|
||
c.setFont("Helvetica", 11)
|
||
|
||
c.save()
|
||
layout_buffer.seek(0)
|
||
try:
|
||
layout_reader = PdfReader(layout_buffer)
|
||
for page in layout_reader.pages:
|
||
writer.add_page(page)
|
||
except Exception as e:
|
||
logger.error(f"[generate_form_json_pdf] Erreur ajout rendu formulaire: {e}")
|
||
|
||
# 4) Fallback minimal si aucune page n'a été créée
|
||
if len(writer.pages) == 0:
|
||
fallback = BytesIO()
|
||
c_fb = canvas.Canvas(fallback, pagesize=A4)
|
||
c_fb.setFont("Helvetica-Bold", 16)
|
||
c_fb.drawString(60, 800, form_json.get("title", "Formulaire"))
|
||
c_fb.save()
|
||
fallback.seek(0)
|
||
fallback_reader = PdfReader(fallback)
|
||
for page in fallback_reader.pages:
|
||
writer.add_page(page)
|
||
|
||
out = BytesIO()
|
||
writer.write(out)
|
||
out.seek(0)
|
||
return ContentFile(out.read(), name=os.path.basename(filename))
|