3 Commits

33 changed files with 645 additions and 819 deletions

View File

@ -236,7 +236,6 @@ def makeToken(user):
"establishment__name": role.establishment.name,
"establishment__evaluation_frequency": role.establishment.evaluation_frequency,
"establishment__total_capacity": role.establishment.total_capacity,
"establishment__api_docuseal": role.establishment.api_docuseal,
"establishment__logo": logo_url,
})

View File

@ -1 +0,0 @@
# This file is intentionally left blank to make this directory a Python package.

View File

@ -1,9 +0,0 @@
from django.urls import path, re_path
from .views import generate_jwt_token, clone_template, remove_template, download_template
urlpatterns = [
re_path(r'generateToken$', generate_jwt_token, name='generate_jwt_token'),
re_path(r'cloneTemplate$', clone_template, name='clone_template'),
re_path(r'removeTemplate/(?P<id>[0-9]+)$', remove_template, name='remove_template'),
re_path(r'downloadTemplate/(?P<slug>[\w-]+)$', download_template, name='download_template')
]

View File

@ -1,200 +0,0 @@
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
import jwt
import datetime
import requests
from Establishment.models import Establishment
@csrf_exempt
@api_view(['POST'])
def generate_jwt_token(request):
# Récupérer l'établissement concerné (par ID ou autre info transmise)
establishment_id = request.data.get('establishment_id')
if not establishment_id:
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
try:
establishment = Establishment.objects.get(id=establishment_id)
except Establishment.DoesNotExist:
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
# Vérifier la clé API reçue dans le header
api_key = request.headers.get('X-Auth-Token')
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# Récupérer les données de la requête
user_email = request.data.get('user_email')
documents_urls = request.data.get('documents_urls', [])
template_id = request.data.get('id')
if not user_email:
return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST)
# Utiliser la clé API de l'établissement comme secret JWT
jwt_secret = establishment.api_docuseal
jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM']
expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA']
payload = {
'user_email': user_email,
'documents_urls': documents_urls,
'template_id': template_id,
'exp': datetime.datetime.utcnow() + expiration_delta
}
token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm)
return Response({'token': token}, status=status.HTTP_200_OK)
@csrf_exempt
@api_view(['POST'])
def clone_template(request):
# Récupérer l'établissement concerné
establishment_id = request.data.get('establishment_id')
print(f"establishment_id : {establishment_id}")
if not establishment_id:
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
try:
establishment = Establishment.objects.get(id=establishment_id)
except Establishment.DoesNotExist:
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
# Vérifier la clé API reçue dans le header
api_key = request.headers.get('X-Auth-Token')
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# Récupérer les données de la requête
document_id = request.data.get('templateId')
email = request.data.get('email')
is_required = request.data.get('is_required')
# Vérifier les données requises
if not document_id:
return Response({'error': 'template ID is required'}, status=status.HTTP_400_BAD_REQUEST)
# URL de l'API de DocuSeal pour cloner le template
clone_url = f'https://docuseal.com/api/templates/{document_id}/clone'
# Faire la requête pour cloner le template
try:
response = requests.post(clone_url, headers={
'Content-Type': 'application/json',
'X-Auth-Token': establishment.api_docuseal
})
if response.status_code != status.HTTP_200_OK:
return Response({'error': 'Failed to clone template'}, status=response.status_code)
data = response.json()
if is_required:
# URL de l'API de DocuSeal pour créer une submission
submission_url = f'https://docuseal.com/api/submissions'
try:
clone_id = data['id']
response = requests.post(submission_url, json={
'template_id': clone_id,
'send_email': False,
'submitters': [{'email': email}]
}, headers={
'Content-Type': 'application/json',
'X-Auth-Token': establishment.api_docuseal
})
if response.status_code != status.HTTP_200_OK:
return Response({'error': 'Failed to create submission'}, status=response.status_code)
data = response.json()
data[0]['id'] = clone_id
return Response(data[0], status=status.HTTP_200_OK)
except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
print(f'NOT REQUIRED -> on ne crée pas de submission')
return Response(data, status=status.HTTP_200_OK)
except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@csrf_exempt
@api_view(['DELETE'])
def remove_template(request, id):
# Récupérer l'établissement concerné
establishment_id = request.GET.get('establishment_id')
if not establishment_id:
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
try:
establishment = Establishment.objects.get(id=establishment_id)
except Establishment.DoesNotExist:
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
# Vérifier la clé API reçue dans le header
api_key = request.headers.get('X-Auth-Token')
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# URL de l'API de DocuSeal pour supprimer le template
clone_url = f'https://docuseal.com/api/templates/{id}'
try:
response = requests.delete(clone_url, headers={
'X-Auth-Token': establishment.api_docuseal
})
if response.status_code != status.HTTP_200_OK:
return Response({'error': 'Failed to remove template'}, status=response.status_code)
data = response.json()
return Response(data, status=status.HTTP_200_OK)
except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@csrf_exempt
@api_view(['GET'])
def download_template(request, slug):
# Récupérer l'établissement concerné
establishment_id = request.GET.get('establishment_id')
if not establishment_id:
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
try:
establishment = Establishment.objects.get(id=establishment_id)
except Establishment.DoesNotExist:
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
# Vérifier la clé API reçue dans le header
api_key = request.headers.get('X-Auth-Token')
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# Vérifier les données requises
if not slug:
return Response({'error': 'slug is required'}, status=status.HTTP_400_BAD_REQUEST)
# URL de l'API de DocuSeal pour télécharger le template
download_url = f'https://docuseal.com/submitters/{slug}/download'
try:
response = requests.get(download_url, headers={
'Content-Type': 'application/json',
'X-Auth-Token': establishment.api_docuseal
})
if response.status_code != status.HTTP_200_OK:
return Response({'error': 'Failed to download template'}, status=response.status_code)
data = response.json()
return Response(data, status=status.HTTP_200_OK)
except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -27,7 +27,6 @@ class Establishment(models.Model):
licence_code = models.CharField(max_length=100, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
api_docuseal = models.CharField(max_length=255, blank=True, null=True)
logo = models.FileField(
upload_to=registration_logo_upload_to,
null=True,

View File

@ -349,13 +349,6 @@ SIMPLE_JWT = {
'TOKEN_TYPE_CLAIM': 'token_type',
}
# Configuration for DocuSeal JWT
DOCUSEAL_JWT = {
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'EXPIRATION_DELTA': timedelta(hours=1)
}
# Django Channels Configuration
ASGI_APPLICATION = 'N3wtSchool.asgi.application'

View File

@ -46,7 +46,6 @@ urlpatterns = [
path("GestionEmail/", include(("GestionEmail.urls", 'GestionEmail'), namespace='GestionEmail')),
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
path("School/", include(("School.urls", 'School'), namespace='School')),
path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')),
path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')),
path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')),
path("Settings/", include(("Settings.urls", 'Settings'), namespace='Settings')),

View File

@ -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,16 +300,27 @@ 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 ########################
#############################################################
####### DocuSeal masters (documents école, à signer ou pas) #######
####### 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)
def __str__(self):
return f'{self.group.name} - {self.id}'
@ -321,14 +342,14 @@ def registration_school_file_upload_to(instance, filename):
def registration_parent_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
####### DocuSeal templates (par dossier d'inscription) #######
####### 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)
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
formTemplateData = models.JSONField(default=list, blank=True, null=True)
def __str__(self):
return self.name

View File

@ -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<id>[0-9]+)/archive$', archive, name="archive"),

View File

@ -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 dinscription.

View File

@ -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',

View File

@ -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)

View File

@ -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)

Binary file not shown.

View File

@ -42,14 +42,9 @@ const nextConfig = {
NEXT_PUBLIC_USE_FAKE_DATA: process.env.NEXT_PUBLIC_USE_FAKE_DATA || 'false',
AUTH_SECRET: process.env.AUTH_SECRET || 'false',
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
DOCUSEAL_API_KEY: process.env.DOCUSEAL_API_KEY,
},
async rewrites() {
return [
{
source: '/api/documents/:path*',
destination: 'https://api.docuseal.com/v1/documents/:path*',
},
{
source: '/api/auth/:path*',
destination: '/api/auth/:path*', // Exclure les routes NextAuth des réécritures de proxy

View File

@ -8,7 +8,6 @@
"name": "n3wt-school-front-end",
"version": "0.0.3",
"dependencies": {
"@docuseal/react": "^1.0.56",
"@radix-ui/react-dialog": "^1.1.2",
"@tailwindcss/forms": "^0.5.9",
"date-fns": "^4.1.0",
@ -537,11 +536,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@docuseal/react": {
"version": "1.0.66",
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.66.tgz",
"integrity": "sha512-rYG58gv8Uw1cTtjbHdgWgWBWpLMbIwDVsS3kN27w4sz/eDJilZieePUDS4eLKJ8keBN05BSjxD/iWQpaTBKZLg=="
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@ -11269,11 +11263,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@docuseal/react": {
"version": "1.0.66",
"resolved": "https://registry.npmjs.org/@docuseal/react/-/react-1.0.66.tgz",
"integrity": "sha512-rYG58gv8Uw1cTtjbHdgWgWBWpLMbIwDVsS3kN27w4sz/eDJilZieePUDS4eLKJ8keBN05BSjxD/iWQpaTBKZLg=="
},
"@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",

View File

@ -14,7 +14,6 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@docuseal/react": "^1.0.56",
"@radix-ui/react-dialog": "^1.1.2",
"@tailwindcss/forms": "^0.5.9",
"date-fns": "^4.1.0",

View File

@ -36,7 +36,6 @@ export default function DashboardPage() {
const {
selectedEstablishmentId,
selectedEstablishmentTotalCapacity,
apiDocuseal,
} = useEstablishment();
const [statusDistribution, setStatusDistribution] = useState([
@ -165,25 +164,6 @@ export default function DashboardPage() {
return (
<div key={selectedEstablishmentId} className="p-6">
<div className="flex items-center gap-3 mb-6">
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
apiDocuseal
? 'bg-green-100 text-green-700 border border-green-300'
: 'bg-red-100 text-red-700 border border-red-300'
}`}
>
{apiDocuseal ? (
<CheckCircle2 className="w-4 h-4 mr-2 text-green-500" />
) : (
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
)}
{apiDocuseal
? 'Clé API Docuseal renseignée'
: 'Clé API Docuseal manquante'}
</span>
</div>
{/* Statistiques principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard

View File

@ -52,7 +52,7 @@ export default function Page() {
);
const csrfToken = useCsrfToken();
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => {
if (selectedEstablishmentId) {
@ -353,7 +353,6 @@ export default function Page() {
<FilesGroupsManagement
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
apiDocuseal={apiDocuseal}
/>
</div>
),

View File

@ -35,7 +35,6 @@ import {
fetchRegistrationFileGroups,
fetchRegistrationSchoolFileMasters,
fetchRegistrationParentFileMasters,
cloneTemplate,
createRegistrationSchoolFileTemplate,
createRegistrationParentFileTemplate,
} from '@/app/actions/registerFileGroupAction';
@ -96,7 +95,7 @@ export default function CreateSubscriptionPage() {
const { getNiveauLabel } = useClasses();
const formDataRef = useRef(formData);
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const { selectedEstablishmentId } = useEstablishment();
const csrfToken = useCsrfToken();
const router = useRouter();
@ -522,128 +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)
);
const clonePromises = masters.map((templateMaster) =>
cloneTemplate(
templateMaster.id,
formDataRef.current.guardianEmail,
templateMaster.is_required,
selectedEstablishmentId,
apiDocuseal
)
.then((clonedDocument) => {
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'
);
});
})
.catch((error) => {
setIsLoading(false);
logger.error('Error during cloning or sending:', error);
showNotification(
"Erreur lors de la création du dossier d'inscription",
'error',
'Erreur',
'ERR_ADM_SUB_05'
);
})
);
// 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);
});
}
};

View File

@ -19,7 +19,7 @@ export default function Page() {
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken();
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const { selectedEstablishmentId } = useEstablishment();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = (data) => {
@ -59,7 +59,6 @@ export default function Page() {
studentId={studentId}
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
apiDocuseal = {apiDocuseal}
onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
errors={formErrors}

View File

@ -17,7 +17,7 @@ export default function Page() {
const enable = searchParams.get('enabled') === 'true';
const router = useRouter();
const csrfToken = useCsrfToken();
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const { selectedEstablishmentId } = useEstablishment();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = (data) => {
@ -53,7 +53,6 @@ export default function Page() {
studentId={studentId}
csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId}
apiDocuseal = {apiDocuseal}
onSubmit={handleSubmit}
cancelUrl={FE_PARENTS_HOME_URL}
enable={enable}

View File

@ -3,11 +3,7 @@ import {
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL,
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL,
FE_API_DOCUSEAL_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN,
FE_API_DOCUSEAL_DELETE_URL
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL
} from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
@ -327,62 +323,3 @@ export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
}
);
};
// API requests
export const removeTemplate = (templateId, selectedEstablishmentId, apiDocuseal) => {
return fetch(`${FE_API_DOCUSEAL_DELETE_URL}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId,
establishment_id :selectedEstablishmentId,
apiDocuseal
}),
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const cloneTemplate = (templateId, email, is_required, selectedEstablishmentId, apiDocuseal) => {
return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId,
email,
is_required,
establishment_id :selectedEstablishmentId,
apiDocuseal
}),
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const downloadTemplate = (slug, selectedEstablishmentId, apiDocuseal) => {
const url = `${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}?establishment_id=${selectedEstablishmentId}&apiDocuseal=${apiDocuseal}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
};
export const generateToken = (email, id = null, selectedEstablishmentId, apiDocuseal) => {
return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_email: email, id, establishment_id :selectedEstablishmentId, apiDocuseal }),
})
.then(requestResponseHandler)
.catch(errorHandler);
};

View File

@ -24,8 +24,7 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
setSelectedEstablishmentEvaluationFrequency,
setSelectedEstablishmentTotalCapacity,
selectedEstablishmentLogo,
setSelectedEstablishmentLogo,
setApiDocuseal
setSelectedEstablishmentLogo
} = useEstablishment();
const { isConnected, connectionStatus } = useChatConnection();
const [dropdownOpen, setDropdownOpen] = useState(false);
@ -41,8 +40,6 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
user.roles[roleId].establishment__total_capacity;
const establishmentLogo =
user.roles[roleId].establishment__logo;
const establishmentApiDocuseal =
user.roles[roleId].establishment__api_docuseal;
setProfileRole(role);
setSelectedEstablishmentId(establishmentId);
setSelectedEstablishmentEvaluationFrequency(
@ -50,7 +47,6 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
);
setSelectedEstablishmentTotalCapacity(establishmentTotalCapacity);
setSelectedEstablishmentLogo(establishmentLogo);
setApiDocuseal(establishmentApiDocuseal);
setSelectedRoleId(roleId);
if (onRoleChange) {
onRoleChange(roleId);

View File

@ -2,10 +2,7 @@ import React, { useState, useEffect } from 'react';
import {
fetchRegistrationFileGroups,
createRegistrationSchoolFileTemplate,
cloneTemplate,
generateToken,
} from '@/app/actions/registerFileGroupAction';
import { DocusealBuilder } from '@docuseal/react';
import logger from '@/utils/logger';
import MultiSelect from '@/components/Form/MultiSelect'; // Import du composant MultiSelect
import { useCsrfToken } from '@/context/CsrfContext';
@ -19,18 +16,13 @@ export default function FileUploadDocuSeal({
onSuccess,
}) {
const [groups, setGroups] = useState([]);
const [token, setToken] = useState(null);
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, apiDocuseal } = useEstablishment();
const { selectedEstablishmentId, user } = useEstablishment();
useEffect(() => {
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
@ -47,31 +39,10 @@ export default function FileUploadDocuSeal({
if (!user && !user?.email) {
return;
}
const id = fileToEdit ? fileToEdit.id : null;
generateToken(user?.email, id, selectedEstablishmentId, apiDocuseal)
.then((data) => {
setToken(data.token);
})
.catch((error) =>
logger.error('Erreur lors de la génération du token:', error)
);
}, [fileToEdit]);
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) => {
@ -118,45 +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);
cloneTemplate(
templateMaster?.id,
guardian.email,
is_required,
selectedEstablishmentId,
apiDocuseal
)
.then((clonedDocument) => {
// Sauvegarde des schoolFileTemplates clonés dans la base de données
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
);
});
// Logique pour envoyer chaque template au submitter
logger.debug('Sending template to:', guardian.email);
})
.catch((error) => {
logger.error('Error during cloning or sending:', error);
});
});
}
};
@ -212,32 +144,7 @@ export default function FileUploadDocuSeal({
{/* Zone de configuration des documents */}
<div className="col-span-8 bg-white p-6 rounded-lg shadow-md border border-gray-200">
{token && (
<div className="h-full overflow-auto">
{/* Description de l'étape */}
<p className="text-gray-700 text-base font-medium mb-4">
Étape 2 - Sélectionnez un document
</p>
<DocusealBuilder
token={token}
headers={{
Authorization: `Bearer ${token}`,
}}
withSendButton={false}
withSignYourselfButton={false}
autosave={false}
withDocumentsList={false}
language={'fr'}
onLoad={handleLoad}
onUpload={handleUpload}
onChange={handleChange}
onSave={handleSubmit}
className="h-full overflow-auto"
style={{ maxHeight: '65vh' }}
/>
</div>
)}
</div>
</div>
)}

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { Download, Edit3, Trash2, FolderPlus, Signature, AlertTriangle } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import { BASE_URL } from '@/utils/Url';
import {
// GET
@ -21,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';
@ -33,11 +30,11 @@ import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import { useNotification } from '@/context/NotificationContext';
import AlertMessage from '@/components/AlertMessage';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
export default function FilesGroupsManagement({
csrfToken,
selectedEstablishmentId,
apiDocuseal
selectedEstablishmentId
}) {
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
@ -116,72 +113,24 @@ export default function FilesGroupsManagement({
);
setRemovePopupOnConfirm(() => () => {
setIsLoading(true);
// Supprimer les clones associés via l'API DocuSeal
const removeClonesPromises = [
...schoolFileTemplates
.filter((template) => template.master === templateMaster.id)
.map((template) =>
removeTemplate(template.id, selectedEstablishmentId, apiDocuseal)
),
removeTemplate(templateMaster.id, selectedEstablishmentId, apiDocuseal),
];
// Supprimer le template master de la base de données
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
.then((response) => {
if (response.ok) {
setSchoolFileMasters(
schoolFileMasters.filter(
(fichier) => fichier.id !== templateMaster.id
)
);
showNotification(
`Le document "${templateMaster.name}" a été correctement supprimé.`,
'success',
'Succès'
);
// Attendre que toutes les suppressions dans DocuSeal soient terminées
Promise.all(removeClonesPromises)
.then((responses) => {
const allSuccessful = responses.every((response) => response && response.id);
if (allSuccessful) {
logger.debug('Master et clones supprimés avec succès de DocuSeal.');
// Supprimer le template master de la base de données
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
.then((response) => {
if (response.ok) {
setSchoolFileMasters(
schoolFileMasters.filter(
(fichier) => fichier.id !== templateMaster.id
)
);
showNotification(
`Le document "${templateMaster.name}" a été correctement supprimé.`,
'success',
'Succès'
);
setRemovePopupVisible(false);
setIsLoading(false);
} else {
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
setIsLoading(false);
}
})
.catch((error) => {
logger.error('Error deleting file from database:', error);
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
setIsLoading(false);
});
} else {
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
setIsLoading(false);
}
})
.catch((error) => {
logger.error('Error removing template from DocuSeal:', error);
setRemovePopupVisible(false);
setIsLoading(false);
} else {
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
@ -189,7 +138,18 @@ export default function FilesGroupsManagement({
);
setRemovePopupVisible(false);
setIsLoading(false);
});
}
})
.catch((error) => {
logger.error('Error deleting file from database:', error);
showNotification(
`Erreur lors de la suppression du document "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
setIsLoading(false);
});
});
};
@ -542,25 +502,13 @@ export default function FilesGroupsManagement({
icon={Signature}
title="Formulaires à remplir"
description="Gérez les formulaires nécessitant une signature électronique."
button={apiDocuseal}
button={true}
buttonOpeningModal={true}
onClick={() => {
setIsModalOpen(true);
setIsEditing(false);
}}
/>
<div className="mb-4">
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
!apiDocuseal && 'bg-red-100 text-red-700 border border-red-300'
}`}
>
{!apiDocuseal && (
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
)}
{!apiDocuseal && 'Clé API Docuseal manquante'}
</span>
</div>
<Table
data={filteredFiles}
columns={columnsFiles}

View File

@ -46,10 +46,6 @@ export const EstablishmentProvider = ({ children }) => {
const storedUser = sessionStorage.getItem('user');
return storedUser ? JSON.parse(storedUser) : null;
});
const [apiDocuseal, setApiDocusealState] = useState(() => {
const storedApiDocuseal = sessionStorage.getItem('apiDocuseal');
return storedApiDocuseal ? JSON.parse(storedApiDocuseal) : null;
});
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
return storedLogo ? JSON.parse(storedLogo) : null;
@ -94,11 +90,6 @@ export const EstablishmentProvider = ({ children }) => {
sessionStorage.setItem('user', JSON.stringify(user));
};
const setApiDocuseal = (api) => {
setApiDocusealState(api);
sessionStorage.setItem('apiDocuseal', JSON.stringify(api));
};
const setSelectedEstablishmentLogo = (logo) => {
setSelectedEstablishmentLogoState(logo);
sessionStorage.setItem('selectedEstablishmentLogo', JSON.stringify(logo));
@ -122,7 +113,6 @@ export const EstablishmentProvider = ({ children }) => {
name: role.establishment__name,
evaluation_frequency: role.establishment__evaluation_frequency,
total_capacity: role.establishment__total_capacity,
api_docuseal: role.establishment__api_docuseal,
logo: role.establishment__logo,
role_id: i,
role_type: role.role_type,
@ -143,9 +133,6 @@ export const EstablishmentProvider = ({ children }) => {
setSelectedEstablishmentTotalCapacity(
userEstablishments[roleIndexDefault].total_capacity
);
setApiDocuseal(
userEstablishments[roleIndexDefault].api_docuseal
);
setSelectedEstablishmentLogo(
userEstablishments[roleIndexDefault].logo
);
@ -168,7 +155,6 @@ export const EstablishmentProvider = ({ children }) => {
setUserState(null);
setSelectedEstablishmentEvaluationFrequencyState(null);
setSelectedEstablishmentTotalCapacityState(null);
setApiDocusealState(null);
setSelectedEstablishmentLogoState(null);
sessionStorage.clear();
};
@ -184,8 +170,6 @@ export const EstablishmentProvider = ({ children }) => {
setSelectedEstablishmentEvaluationFrequency,
selectedEstablishmentTotalCapacity,
setSelectedEstablishmentTotalCapacity,
apiDocuseal,
setApiDocuseal,
selectedEstablishmentLogo,
setSelectedEstablishmentLogo,
selectedRoleId,

View File

@ -1,41 +0,0 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_CLONE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'POST') {
const { templateId, email, is_required, establishment_id, apiDocuseal } = req.body;
fetch(BE_DOCUSEAL_CLONE_TEMPLATE, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': apiDocuseal,
},
body: JSON.stringify({
templateId,
email,
is_required,
establishment_id,
}),
})
.then((response) => {
if (!response.ok) {
return response.json().then((err) => {
throw new Error(err.message);
});
}
return response.json();
})
.then((data) => {
logger.debug('Template cloned successfully:', data);
res.status(200).json(data);
})
.catch((error) => {
logger.error('Error cloning template:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -1,35 +0,0 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_DOWNLOAD_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'GET') {
const { slug, establishment_id, apiDocuseal } = req.query;
logger.debug('slug : ', slug);
fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}?establishment_id=${establishment_id}`, {
method: 'GET',
headers: {
'X-Auth-Token': apiDocuseal,
},
})
.then((response) => {
if (!response.ok) {
return response.json().then((err) => {
throw new Error(err.message);
});
}
return response.json();
})
.then((data) => {
logger.debug('Template downloaded successfully:', data);
res.status(200).json(data);
})
.catch((error) => {
logger.error('Error downloading template:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -1,34 +0,0 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_GET_JWT } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'POST') {
const { apiDocuseal, ...rest } = req.body;
fetch(BE_DOCUSEAL_GET_JWT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': apiDocuseal,
},
body: JSON.stringify(rest),
})
.then((response) => {
logger.debug('Response status:', response.status);
return response
.json()
.then((data) => ({ status: response.status, data }));
})
.then(({ status, data }) => {
logger.debug('Response data:', data);
res.status(status).json(data);
})
.catch((error) => {
logger.error('Error:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -1,34 +0,0 @@
import logger from '@/utils/logger';
import { BE_DOCUSEAL_REMOVE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) {
if (req.method === 'DELETE') {
const { templateId, establishment_id, apiDocuseal } = req.body;
fetch(`${BE_DOCUSEAL_REMOVE_TEMPLATE}/${templateId}?establishment_id=${establishment_id}`, {
method: 'DELETE',
headers: {
'X-Auth-Token': apiDocuseal,
},
})
.then((response) => {
if (!response.ok) {
return response.json().then((err) => {
throw new Error(err.message);
});
}
return response.json();
})
.then((data) => {
logger.debug('Template removed successfully:', data);
res.status(200).json(data);
})
.catch((error) => {
logger.error('Error removing template:', error);
res.status(500).json({ error: 'Internal Server Error' });
});
} else {
res.setHeader('Allow', ['DELETE']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

View File

@ -4,12 +4,6 @@ export const WS_BASE_URL = process.env.NEXT_PUBLIC_WSAPI_URL;
//URL-Back-End
// GESTION DocuSeal
export const BE_DOCUSEAL_GET_JWT = `${BASE_URL}/DocuSeal/generateToken`;
export const BE_DOCUSEAL_CLONE_TEMPLATE = `${BASE_URL}/DocuSeal/cloneTemplate`;
export const BE_DOCUSEAL_REMOVE_TEMPLATE = `${BASE_URL}/DocuSeal/removeTemplate`;
export const BE_DOCUSEAL_DOWNLOAD_TEMPLATE = `${BASE_URL}/DocuSeal/downloadTemplate`;
// GESTION LOGIN
export const BE_AUTH_NEW_PASSWORD_URL = `${BASE_URL}/Auth/newPassword`;
export const BE_AUTH_REGISTER_URL = `${BASE_URL}/Auth/subscribe`;
@ -131,12 +125,6 @@ export const FE_PARENTS_HOME_URL = '/parents';
export const FE_PARENTS_MESSAGERIE_URL = '/parents/messagerie';
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription';
// API DOCUSEAL
export const FE_API_DOCUSEAL_GENERATE_TOKEN = '/api/docuseal/generateToken';
export const FE_API_DOCUSEAL_CLONE_URL = '/api/docuseal/cloneTemplate';
export const FE_API_DOCUSEAL_DOWNLOAD_URL = '/api/docuseal/downloadTemplate';
export const FE_API_DOCUSEAL_DELETE_URL = '/api/docuseal/removeTemplate';
/**
* Fonction pour obtenir l'URL de redirection en fonction du rôle
* @param {RIGHTS} role

View File

@ -10,10 +10,10 @@ services:
database:
image: "postgres:latest"
expose:
- 5432
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- postgres-data:/var/lib/postgresql
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres