diff --git a/Back-End/DocuSeal/__init__.py b/Back-End/DocuSeal/__init__.py deleted file mode 100644 index d64ffa4..0000000 --- a/Back-End/DocuSeal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# This file is intentionally left blank to make this directory a Python package. diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index b9abb78..296a9cf 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -277,6 +277,16 @@ class RegistrationForm(models.Model): return "RF_" + self.student.last_name + "_" + self.student.first_name def save(self, *args, **kwargs): + # Préparer le flag de création / changement de fileGroup + was_new = self.pk is None + old_fileGroup = None + if not was_new: + try: + old_instance = RegistrationForm.objects.get(pk=self.pk) + old_fileGroup = old_instance.fileGroup + except RegistrationForm.DoesNotExist: + old_fileGroup = None + # Vérifier si un fichier existant doit être remplacé if self.pk: # Si l'objet existe déjà dans la base de données try: @@ -290,6 +300,17 @@ class RegistrationForm(models.Model): # Appeler la méthode save originale super().save(*args, **kwargs) + # Après save : si nouveau ou changement de fileGroup -> créer les templates + fileGroup_changed = (self.fileGroup is not None) and (old_fileGroup is None or (old_fileGroup and old_fileGroup.id != self.fileGroup.id)) + if was_new or fileGroup_changed: + try: + import Subscriptions.util as util + created = util.create_templates_for_registration_form(self) + if created: + logger.info("Created %d templates for RegistrationForm %s", len(created), self.pk) + except Exception as e: + logger.exception("Error creating templates for RegistrationForm %s: %s", self.pk, e) + ############################################################# ####################### MASTER FILES ######################## ############################################################# @@ -297,7 +318,6 @@ class RegistrationForm(models.Model): ####### Formulaires masters (documents école, à signer ou pas) ####### class RegistrationSchoolFileMaster(models.Model): groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True) - id = models.IntegerField(primary_key=True) name = models.CharField(max_length=255, default="") is_required = models.BooleanField(default=False) formMasterData = models.JSONField(default=list, blank=True, null=True) @@ -325,7 +345,6 @@ def registration_parent_file_upload_to(instance, filename): ####### Formulaires templates (par dossier d'inscription) ####### class RegistrationSchoolFileTemplate(models.Model): master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True) - id = models.IntegerField(primary_key=True) slug = models.CharField(max_length=255, default="") name = models.CharField(max_length=255, default="") registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True) diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index f1044d3..5444187 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -24,7 +24,12 @@ from .views import ( ) from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group -from .views import registration_file_views, get_school_file_templates_by_rf, get_parent_file_templates_by_rf +from .views import ( + registration_school_file_masters_views, + registration_school_file_templates_views, + get_school_file_templates_by_rf, + get_parent_file_templates_by_rf +) urlpatterns = [ re_path(r'^registerForms/(?P[0-9]+)/archive$', archive, name="archive"), diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index cc7f479..16f2d33 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -21,8 +21,161 @@ 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) + """ + 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, + ) + + 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): + 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}" + tmpl = RegistrationSchoolFileTemplate.objects.create( + master=m, + registration_form=register_form, + name=m.name or "", + formTemplateData=m.formMasterData or [], + slug=slug, + ) + 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. diff --git a/Back-End/Subscriptions/views/__init__.py b/Back-End/Subscriptions/views/__init__.py index abab71c..dc413b4 100644 --- a/Back-End/Subscriptions/views/__init__.py +++ b/Back-End/Subscriptions/views/__init__.py @@ -1,14 +1,24 @@ -from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_school_file_templates_by_rf, get_parent_file_templates_by_rf -from .registration_file_views import ( +from .register_form_views import ( + RegisterFormView, + RegisterFormWithIdView, + send, + resend, + archive, + get_school_file_templates_by_rf, + get_parent_file_templates_by_rf +) +from .registration_school_file_masters_views import ( RegistrationSchoolFileMasterView, - RegistrationSchoolFileMasterSimpleView, - RegistrationSchoolFileTemplateView, - RegistrationSchoolFileTemplateSimpleView, + RegistrationSchoolFileMasterSimpleView, RegistrationParentFileMasterView, RegistrationParentFileMasterSimpleView, RegistrationParentFileTemplateSimpleView, RegistrationParentFileTemplateView ) +from .registration_school_file_templates_views import ( + RegistrationSchoolFileTemplateView, + RegistrationSchoolFileTemplateSimpleView, +) from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .student_views import StudentView, StudentListView, ChildrenListView, search_students from .guardian_views import GuardianView, DissociateGuardianView @@ -33,7 +43,7 @@ __all__ = [ 'RegistrationFileGroupSimpleView', 'get_registration_files_by_group', 'get_school_file_templates_by_rf', - 'get_parent_file_templates_by_rf' + 'get_parent_file_templates_by_rf', 'StudentView', 'StudentListView', 'ChildrenListView', diff --git a/Back-End/Subscriptions/views/registration_school_file_masters_views.py b/Back-End/Subscriptions/views/registration_school_file_masters_views.py new file mode 100644 index 0000000..f91afd9 --- /dev/null +++ b/Back-End/Subscriptions/views/registration_school_file_masters_views.py @@ -0,0 +1,363 @@ +from django.http.response import JsonResponse +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +import json +from django.http import QueryDict + +from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer +from Subscriptions.models import ( + RegistrationForm, + RegistrationSchoolFileMaster, + RegistrationSchoolFileTemplate, + RegistrationParentFileMaster, + RegistrationParentFileTemplate +) +from N3wtSchool import bdd +import logging +import Subscriptions.util as util + +logger = logging.getLogger(__name__) + +class RegistrationSchoolFileMasterView(APIView): + parser_classes = [MultiPartParser, FormParser] + @swagger_auto_schema( + operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné", + manual_parameters=[ + openapi.Parameter( + 'establishment_id', + openapi.IN_QUERY, + description="ID de l'établissement", + type=openapi.TYPE_INTEGER, + required=True + ) + ], + responses={200: RegistrationSchoolFileMasterSerializer(many=True)} + ) + def get(self, request): + establishment_id = request.GET.get('establishment_id') + if not establishment_id: + return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST) + + # Filtrer les masters liés à l'établissement via groups.establishment + masters = RegistrationSchoolFileMaster.objects.filter( + groups__establishment__id=establishment_id + ).distinct() + serializer = RegistrationSchoolFileMasterSerializer(masters, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau master de template d'inscription", + request_body=RegistrationSchoolFileMasterSerializer, + responses={ + 201: RegistrationSchoolFileMasterSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + logger.info(f"raw request.data: {request.data}") + + payload, resp = util.build_payload_from_request(request) + if resp: + return resp + + logger.info(f"payload for serializer: {payload}") + serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True) + if serializer.is_valid(): + obj = serializer.save() + + # Propager la création des templates côté serveur pour les RegistrationForm + try: + groups_qs = obj.groups.all() + if groups_qs.exists(): + # Tous les RegistrationForm dont fileGroup est dans les groups du master + rfs = RegistrationForm.objects.filter(fileGroup__in=groups_qs).distinct() + for rf in rfs: + try: + util.create_templates_for_registration_form(rf) + except Exception as e: + logger.exception("Error creating templates for RF %s from master %s: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e) + except Exception: + logger.exception("Error while propagating templates after master creation %s", getattr(obj, 'pk', None)) + + + return Response(RegistrationSchoolFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED) + + logger.error(f"serializer errors: {serializer.errors}") + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class RegistrationSchoolFileMasterSimpleView(APIView): + @swagger_auto_schema( + operation_description="Récupère un master de template d'inscription spécifique", + responses={ + 200: RegistrationSchoolFileMasterSerializer, + 404: "Master non trouvé" + } + ) + def get(self, request, id): + master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) + if master is None: + return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationSchoolFileMasterSerializer(master) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un master de template d'inscription existant", + request_body=RegistrationSchoolFileMasterSerializer, + responses={ + 200: RegistrationSchoolFileMasterSerializer, + 400: "Données invalides", + 404: "Master non trouvé" + } + ) + def put(self, request, id): + master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) + if master is None: + return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND) + + # snapshot des groups avant update + old_group_ids = set(master.groups.values_list('id', flat=True)) + + # Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON) + payload, resp = util.build_payload_from_request(request) + if resp: + return resp + + logger.info(f"payload for update serializer: {payload}") + serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True) + if serializer.is_valid(): + obj = serializer.save() + + # groups après update + new_group_ids = set(obj.groups.values_list('id', flat=True)) + + removed_group_ids = old_group_ids - new_group_ids + added_group_ids = new_group_ids - old_group_ids + + # Pour chaque RF appartenant aux groupes retirés -> nettoyer les templates (idempotent) + if removed_group_ids: + try: + rfs_removed = RegistrationForm.objects.filter(fileGroup__in=list(removed_group_ids)).distinct() + for rf in rfs_removed: + try: + util.create_templates_for_registration_form(rf) # supprimera les templates obsolètes + except Exception as e: + logger.exception("Error cleaning templates for RF %s after master %s group removal: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e) + except Exception: + logger.exception("Error while processing RFs for removed groups after master update %s", getattr(obj, 'pk', None)) + + # Pour chaque RF appartenant aux groupes ajoutés -> créer les templates manquants + if added_group_ids: + try: + rfs_added = RegistrationForm.objects.filter(fileGroup__in=list(added_group_ids)).distinct() + for rf in rfs_added: + try: + util.create_templates_for_registration_form(rf) # créera les templates manquants + except Exception as e: + logger.exception("Error creating templates for RF %s after master %s group addition: %s", getattr(rf, 'pk', None), getattr(obj, 'pk', None), e) + except Exception: + logger.exception("Error while processing RFs for added groups after master update %s", getattr(obj, 'pk', None)) + + return Response(serializer.data, status=status.HTTP_200_OK) + + logger.error(f"serializer errors on put: {serializer.errors}") + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un master de template d'inscription", + responses={ + 204: "Suppression réussie", + 404: "Master non trouvé" + } + ) + def delete(self, request, id): + master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) + if master is not None: + master.delete() + return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK) + else: + return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +class RegistrationParentFileMasterView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les fichiers parents pour un établissement donné", + manual_parameters=[ + openapi.Parameter( + 'establishment_id', + openapi.IN_QUERY, + description="ID de l'établissement", + type=openapi.TYPE_INTEGER, + required=True + ) + ], + responses={200: RegistrationParentFileMasterSerializer(many=True)} + ) + def get(self, request): + establishment_id = request.GET.get('establishment_id') + if not establishment_id: + return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST) + + # Filtrer les fichiers parents liés à l'établissement + templates = RegistrationParentFileMaster.objects.filter( + groups__establishment__id=establishment_id + ).distinct() + serializer = RegistrationParentFileMasterSerializer(templates, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau fichier parent", + request_body=RegistrationParentFileMasterSerializer, + responses={ + 201: RegistrationParentFileMasterSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + serializer = RegistrationParentFileMasterSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class RegistrationParentFileMasterSimpleView(APIView): + @swagger_auto_schema( + operation_description="Récupère un fichier parent spécifique", + responses={ + 200: RegistrationParentFileMasterSerializer, + 404: "Fichier parent non trouvé" + } + ) + def get(self, request, id): + template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id) + if template is None: + return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationParentFileMasterSerializer(template) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un fichier parent existant", + request_body=RegistrationParentFileMasterSerializer, + responses={ + 200: RegistrationParentFileMasterSerializer, + 400: "Données invalides", + 404: "Fichier parent non trouvé" + } + ) + def put(self, request, id): + template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id) + if template is None: + return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationParentFileMasterSerializer(template, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Fichier parent mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un fichier parent", + responses={ + 204: "Suppression réussie", + 404: "Fichier parent non trouvé" + } + ) + def delete(self, request, id): + template = bdd.getObject(_objectName=RegistrationParentFileMaster, _columnName='id', _value=id) + if template is not None: + template.delete() + return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) + else: + return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +class RegistrationParentFileTemplateView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les templates parents pour un établissement donné", + manual_parameters=[ + openapi.Parameter( + 'establishment_id', + openapi.IN_QUERY, + description="ID de l'établissement", + type=openapi.TYPE_INTEGER, + required=True + ) + ], + responses={200: RegistrationParentFileTemplateSerializer(many=True)} + ) + def get(self, request): + establishment_id = request.GET.get('establishment_id') + if not establishment_id: + return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST) + + # Filtrer les templates parents liés à l'établissement via master.groups.establishment + templates = RegistrationParentFileTemplate.objects.filter( + master__groups__establishment__id=establishment_id + ).distinct() + serializer = RegistrationParentFileTemplateSerializer(templates, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau template d'inscription", + request_body=RegistrationParentFileTemplateSerializer, + responses={ + 201: RegistrationParentFileTemplateSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + serializer = RegistrationParentFileTemplateSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class RegistrationParentFileTemplateSimpleView(APIView): + @swagger_auto_schema( + operation_description="Récupère un template d'inscription spécifique", + responses={ + 200: RegistrationParentFileTemplateSerializer, + 404: "Template non trouvé" + } + ) + def get(self, request, id): + template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id) + if template is None: + return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationParentFileTemplateSerializer(template) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un template d'inscription existant", + request_body=RegistrationParentFileTemplateSerializer, + responses={ + 200: RegistrationParentFileTemplateSerializer, + 400: "Données invalides", + 404: "Template non trouvé" + } + ) + def put(self, request, id): + template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id) + if template is None: + return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + + serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un template d'inscription", + responses={ + 204: "Suppression réussie", + 404: "Template non trouvé" + } + ) + def delete(self, request, id): + template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id) + if template is not None: + template.delete() + return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) + else: + return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Subscriptions/views/registration_file_views.py b/Back-End/Subscriptions/views/registration_school_file_templates_views.py similarity index 93% rename from Back-End/Subscriptions/views/registration_file_views.py rename to Back-End/Subscriptions/views/registration_school_file_templates_views.py index 15f22bb..70c6fac 100644 --- a/Back-End/Subscriptions/views/registration_file_views.py +++ b/Back-End/Subscriptions/views/registration_school_file_templates_views.py @@ -5,12 +5,19 @@ from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status +import json +from django.http import QueryDict from Subscriptions.serializers import RegistrationSchoolFileMasterSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileMasterSerializer, RegistrationParentFileTemplateSerializer from Subscriptions.models import RegistrationSchoolFileMaster, RegistrationSchoolFileTemplate, RegistrationParentFileMaster, RegistrationParentFileTemplate from N3wtSchool import bdd +import logging +import Subscriptions.util as util + +logger = logging.getLogger(__name__) class RegistrationSchoolFileMasterView(APIView): + parser_classes = [MultiPartParser, FormParser] @swagger_auto_schema( operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné", manual_parameters=[ @@ -45,10 +52,19 @@ class RegistrationSchoolFileMasterView(APIView): } ) def post(self, request): - serializer = RegistrationSchoolFileMasterSerializer(data=request.data) + logger.info(f"raw request.data: {request.data}") + + payload, resp = util.build_payload_from_request(request) + if resp: + return resp + + logger.info(f"payload for serializer: {payload}") + serializer = RegistrationSchoolFileMasterSerializer(data=payload, partial=True) if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + obj = serializer.save() + return Response(RegistrationSchoolFileMasterSerializer(obj).data, status=status.HTTP_201_CREATED) + + logger.error(f"serializer errors: {serializer.errors}") return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class RegistrationSchoolFileMasterSimpleView(APIView): @@ -78,11 +94,19 @@ class RegistrationSchoolFileMasterSimpleView(APIView): def put(self, request, id): master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) if master is None: - return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationSchoolFileMasterSerializer(master, data=request.data) + return JsonResponse({'erreur': "Le master de template n'a pas été trouvé"}, safe=False, status=status.HTTP_404_NOT_FOUND) + + # Normaliser payload (supporte form-data avec champ 'data' JSON ou fichier JSON) + payload, resp = util.build_payload_from_request(request) + if resp: + return resp + + logger.info(f"payload for update serializer: {payload}") + serializer = RegistrationSchoolFileMasterSerializer(master, data=payload, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) + logger.error(f"serializer errors on put: {serializer.errors}") return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @swagger_auto_schema( @@ -96,7 +120,7 @@ class RegistrationSchoolFileMasterSimpleView(APIView): master = bdd.getObject(_objectName=RegistrationSchoolFileMaster, _columnName='id', _value=id) if master is not None: master.delete() - return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) + return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_200_OK) else: return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js index 0276232..779e9cb 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js @@ -521,129 +521,23 @@ export default function CreateSubscriptionPage() { } else { // Création du dossier d'inscription createRegisterForm(data, csrfToken) - .then((data) => { - // Clonage des schoolFileTemplates - const masters = schoolFileMasters.filter((file) => - file.groups.includes(selectedFileGroup) + .then((response) => { + showNotification( + "Dossier d'inscription créé avec succès", + 'success', + 'Succès' ); - const parentMasters = parentFileMasters.filter((file) => - file.groups.includes(selectedFileGroup) - ); - createRegistrationSchoolFileTemplate( - cloneData, - csrfToken - ) - .then((response) => - logger.debug('Template enregistré avec succès:', response) - ) - .catch((error) => { - setIsLoading(false); - logger.error( - "Erreur lors de l'enregistrement du template:", - error - ); - showNotification( - "Erreur lors de la création du dossier d'inscription", - 'error', - 'Erreur', - 'ERR_ADM_SUB_03' - ); - }); - - const clonePromises = masters.map((templateMaster) => { - const cloneData = { - name: `${templateMaster.name}_${formDataRef.current.studentFirstName}_${formDataRef.current.studentLastName}`, - slug: clonedDocument.slug, - id: clonedDocument.id, - master: templateMaster.id, - registration_form: data.student.id, - }; - - return createRegistrationSchoolFileTemplate( - cloneData, - csrfToken - ) - .then((response) => - logger.debug('Template enregistré avec succès:', response) - ) - .catch((error) => { - setIsLoading(false); - logger.error( - "Erreur lors de l'enregistrement du template:", - error - ); - showNotification( - "Erreur lors de la création du dossier d'inscription", - 'error', - 'Erreur', - 'ERR_ADM_SUB_03' - ); - }); - }); - - // Clonage des parentFileTemplates - const parentClonePromises = parentMasters.map((parentMaster) => { - const parentTemplateData = { - master: parentMaster.id, - registration_form: data.student.id, - }; - - return createRegistrationParentFileTemplate( - parentTemplateData, - csrfToken - ) - .then((response) => - logger.debug( - 'Parent template enregistré avec succès:', - response - ) - ) - .catch((error) => { - setIsLoading(false); - logger.error( - "Erreur lors de l'enregistrement du parent template:", - error - ); - showNotification( - "Erreur lors de la création du dossier d'inscription", - 'error', - 'Erreur', - 'ERR_ADM_SUB_02' - ); - }); - }); - - // Attendre que tous les clones soient créés - Promise.all([...clonePromises, ...parentClonePromises]) - .then(() => { - // Redirection après succès - showNotification( - "Dossier d'inscription créé avec succès", - 'success', - 'Succès' - ); - router.push(FE_ADMIN_SUBSCRIPTIONS_URL); - }) - .catch((error) => { - setIsLoading(false); - showNotification( - "Erreur lors de la création du dossier d'inscription", - 'error', - 'Erreur', - 'ERR_ADM_SUB_04' - ); - logger.error('Error during cloning or sending:', error); - }); - }) + router.push(FE_ADMIN_SUBSCRIPTIONS_URL); + }) .catch((error) => { setIsLoading(false); + logger.error('Erreur lors de la mise à jour du dossier:', error); showNotification( "Erreur lors de la création du dossier d'inscription", 'error', 'Erreur', 'ERR_ADM_SUB_01' ); - logger.error('Error during register form creation:', error); }); } }; diff --git a/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js b/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js index 7bed94d..890d7dd 100644 --- a/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js +++ b/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js @@ -19,13 +19,9 @@ export default function FileUploadDocuSeal({ const [templateMaster, setTemplateMaster] = useState(null); const [uploadedFileName, setUploadedFileName] = useState(''); const [selectedGroups, setSelectedGroups] = useState([]); - const [guardianDetails, setGuardianDetails] = useState([]); - const [popupVisible, setPopupVisible] = useState(false); const [popupMessage, setPopupMessage] = useState(''); - const csrfToken = useCsrfToken(); - const { selectedEstablishmentId, user } = useEstablishment(); useEffect(() => { @@ -47,18 +43,6 @@ export default function FileUploadDocuSeal({ const handleGroupChange = (selectedGroups) => { setSelectedGroups(selectedGroups); - - const details = selectedGroups.flatMap((group) => - group.registration_forms.flatMap((form) => - form.guardians.map((guardian) => ({ - email: guardian.associated_profile_email, - last_name: form.last_name, - first_name: form.first_name, - registration_form: form.student_id, - })) - ) - ); - setGuardianDetails(details); // Mettre à jour la variable d'état avec les détails des guardians }; const handleLoad = (detail) => { @@ -105,29 +89,6 @@ export default function FileUploadDocuSeal({ id: templateMaster?.id, is_required: is_required, }); - - guardianDetails.forEach((guardian, index) => { - logger.debug('creation du clone avec required : ', is_required); - const data = { - name: `${uploadedFileName}_${guardian.first_name}_${guardian.last_name}`, - slug: clonedDocument.slug, - id: clonedDocument.id, - master: templateMaster?.id, - registration_form: guardian.registration_form, - }; - logger.debug('creation : ', data); - createRegistrationSchoolFileTemplate(data, csrfToken) - .then((response) => { - logger.debug('Template enregistré avec succès:', response); - onSuccess(); - }) - .catch((error) => { - logger.error( - "Erreur lors de l'enregistrement du template:", - error - ); - }); - }); } }; diff --git a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js index 7f851c3..23a6841 100644 --- a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js +++ b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js @@ -20,9 +20,7 @@ import { // DELETE deleteRegistrationFileGroup, deleteRegistrationSchoolFileMaster, - deleteRegistrationParentFileMaster, - - removeTemplate + deleteRegistrationParentFileMaster } from '@/app/actions/registerFileGroupAction'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm'; import logger from '@/utils/logger';