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 = [] # 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 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}" # 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) else: # Générer le PDF du template à partir du JSON du master try: pdf_file = generate_form_json_pdf(register_form, m.formMasterData) file_to_attach = pdf_file except Exception as e: logger.error(f"Erreur lors de la génération du PDF pour le template: {e}") file_to_attach = None tmpl = RegistrationSchoolFileTemplate.objects.create( master=m, registration_form=register_form, name=m.name or "", formTemplateData=m.formMasterData or [], slug=slug, file=file_to_attach, ) created.append(tmpl) logger.info("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))