From ffc6ce8de835e9caf547b6c4a893436aa93513ba Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Mon, 10 Feb 2025 18:35:24 +0100 Subject: [PATCH] feat: Ajout des Bundles de fichiers [#24] --- Back-End/Subscriptions/models.py | 13 + Back-End/Subscriptions/serializers.py | 7 +- .../templates/pdfs/dossier_inscription.html | 234 ++++++--- Back-End/Subscriptions/urls.py | 52 +- Back-End/Subscriptions/views.py | 450 ------------------ Back-End/Subscriptions/views/__init__.py | 24 + .../Subscriptions/views/guardian_views.py | 34 ++ .../views/register_form_views.py | 385 +++++++++++++++ .../views/registration_file_group_views.py | 125 +++++ .../views/registration_file_views.py | 211 ++++++++ Back-End/Subscriptions/views/student_views.py | 83 ++++ Back-End/src/app.js | 0 Back-End/src/middleware/cors.js | 0 .../src/app/[locale]/admin/structure/page.js | 50 +- .../app/[locale]/admin/subscriptions/page.js | 177 ++----- .../src/app/lib/registerFileGroupAction.js | 74 +++ Front-End/src/app/lib/subscriptionAction.js | 77 +-- .../components/DraggableFileUpload.js | 0 .../components/FileUpload.js | 21 + .../components/Inscription/InscriptionForm.js | 59 ++- .../Inscription/InscriptionFormShared.js | 16 +- .../components/RegistrationFileGroupForm.js | 56 +++ .../components/RegistrationFileGroupList.js | 21 + .../Structure/Files/FilesManagement.js | 302 ++++++++++++ Front-End/src/utils/Url.js | 8 +- 25 files changed, 1736 insertions(+), 743 deletions(-) delete mode 100644 Back-End/Subscriptions/views.py create mode 100644 Back-End/Subscriptions/views/__init__.py create mode 100644 Back-End/Subscriptions/views/guardian_views.py create mode 100644 Back-End/Subscriptions/views/register_form_views.py create mode 100644 Back-End/Subscriptions/views/registration_file_group_views.py create mode 100644 Back-End/Subscriptions/views/registration_file_views.py create mode 100644 Back-End/Subscriptions/views/student_views.py create mode 100644 Back-End/src/app.js create mode 100644 Back-End/src/middleware/cors.js create mode 100644 Front-End/src/app/lib/registerFileGroupAction.js rename Front-End/src/{app/[locale]/admin/subscriptions => }/components/DraggableFileUpload.js (100%) rename Front-End/src/{app/[locale]/admin/subscriptions => }/components/FileUpload.js (74%) create mode 100644 Front-End/src/components/RegistrationFileGroupForm.js create mode 100644 Front-End/src/components/RegistrationFileGroupList.js create mode 100644 Front-End/src/components/Structure/Files/FilesManagement.js diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index d43dbe5..e7b2f9d 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -161,6 +161,13 @@ class Student(models.Model): return self.birth_date.strftime('%d-%m-%Y') return None +class RegistrationFileGroup(models.Model): + name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + + def __str__(self): + return self.name + def registration_file_path(instance, filename): # Génère le chemin : registration_files/dossier_rf_{student_id}/filename return f'registration_files/dossier_rf_{instance.student_id}/{filename}' @@ -196,6 +203,11 @@ class RegistrationForm(models.Model): # Many-to-Many Relationship discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms') + fileGroup = models.ForeignKey(RegistrationFileGroup, + on_delete=models.CASCADE, + related_name='file_group', + null=True, + blank=True) def __str__(self): return "RF_" + self.student.last_name + "_" + self.student.first_name @@ -209,6 +221,7 @@ class RegistrationFileTemplate(models.Model): order = models.PositiveIntegerField(default=0) # Ajout du champ order date_added = models.DateTimeField(auto_now_add=True) is_required = models.BooleanField(default=False) + group = models.ForeignKey(RegistrationFileGroup, on_delete=models.CASCADE, related_name='file_templates') @property def formatted_date_added(self): diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index b11c482..461caa0 100644 --- a/Back-End/Subscriptions/serializers.py +++ b/Back-End/Subscriptions/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language +from .models import RegistrationFileTemplate, RegistrationFile, RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language from School.models import SchoolClass, Fee, Discount, FeeType from School.serializers import FeeSerializer, DiscountSerializer from Auth.models import Profile @@ -11,6 +11,11 @@ from django.utils import timezone import pytz from datetime import datetime +class RegistrationFileGroupSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFileGroup + fields = '__all__' + class RegistrationFileSerializer(serializers.ModelSerializer): class Meta: model = RegistrationFile diff --git a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html index 4170a06..cb01e82 100644 --- a/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html +++ b/Back-End/Subscriptions/templates/pdfs/dossier_inscription.html @@ -3,95 +3,205 @@ {{ pdf_title }} - {% load myTemplateTag %} -
-
-

{{ pdf_title }}

+
+
+

{{ pdf_title }}

-
-
- Signé le : {{ signatureDate }}
- A : {{ signatureTime }} -
-

ELEVE

+ +
+

ÉLÈVE

{% with level=student|getStudentLevel %} {% with gender=student|getStudentGender %} - NOM : {{ student.last_name }}
- PRENOM : {{ student.first_name }}
- ADRESSE : {{ student.address }}
- GENRE : {{ gender }}
- NE(E) LE : {{ student.birth_date }}
- A : {{ student.birth_place }} ({{ student.birth_postal_code }})
- NATIONALITE : {{ student.nationality }}
- NIVEAU : {{ level }}
- MEDECIN TRAITANT : {{ student.attending_physician }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NOM :{{ student.last_name }}PRÉNOM :{{ student.first_name }}
ADRESSE :{{ student.address }}
GENRE :{{ gender }}NÉ(E) LE :{{ student.birth_date }}
À :{{ student.birth_place }} ({{ student.birth_postal_code }})
NATIONALITÉ :{{ student.nationality }}NIVEAU :{{ level }}
MÉDECIN TRAITANT :{{ student.attending_physician }}
{% endwith %} {% endwith %} -
-

RESPONSABLES

+
+ +
+

RESPONSABLES

{% with guardians=student.getGuardians %} - {% with siblings=student.getGuardians %} {% for guardian in guardians%} -

Guardian {{ forloop.counter }}

- NOM : {{ guardian.last_name }}
- PRENOM : {{ guardian.first_name }}
- ADRESSE : {{ guardian.address }}
- NE(E) LE : {{ guardian.birth_date }}
- MAIL : {{ guardian.email }}
- TEL : {{ guardian.phone }}
- PROFESSION : {{ guardian.profession }}
+
+

Responsable {{ forloop.counter }}

+ + + + + + + + + + + + + + + + + + + + + + + +
NOM :{{ guardian.last_name }}PRÉNOM :{{ guardian.first_name }}
ADRESSE :{{ guardian.address }}
NÉ(E) LE :{{ guardian.birth_date }}EMAIL :{{ guardian.email }}
TÉLÉPHONE :{{ guardian.phone }}PROFESSION :{{ guardian.profession }}
+
{% endfor %} -
-

FRATRIE

+ {% endwith %} +
+ +
+

FRATRIE

+ {% with siblings=student.getGuardians %} {% for sibling in siblings%} -

Frère - Soeur {{ forloop.counter }}

- NOM : {{ sibling.last_name }}
- PRENOM : {{ sibling.first_name }}
- NE(E) LE : {{ sibling.birth_date }}
+
+

Frère/Sœur {{ forloop.counter }}

+ + + + + + + + + + + +
NOM :{{ sibling.last_name }}PRÉNOM :{{ sibling.first_name }}
NÉ(E) LE :{{ sibling.birth_date }}
+
{% endfor %} -
-

MODALITES DE PAIEMENT

+ {% endwith %} +
+ +
+

MODALITÉS DE PAIEMENT

{% with paymentMethod=student|getStudentPaymentMethod %} - {{ paymentMethod }}
- {% endwith %} - {% endwith %} +

{{ paymentMethod }}

{% endwith %}
+ +
+ Fait le {{ signatureDate }} à {{ signatureTime }} +
\ No newline at end of file diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 9def08b..5b6252c 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -1,41 +1,41 @@ from django.urls import path, re_path from . import views -from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFileView + +# RF +from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive +# SubClasses +from .views import StudentView, GuardianView, ChildrenListView, StudentListView +# Files +from .views import RegistrationFileTemplateView, RegistrationFileTemplateSimpleView, RegistrationFileView, RegistrationFileSimpleView +from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group urlpatterns = [ - re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), - - re_path(r'^registerForm$', RegisterFormView.as_view(), name="registerForm"), - re_path(r'^registerForm/(?P<_id>[0-9]+)$', RegisterFormView.as_view(), name="registerForm"), - - # Page de formulaire d'inscription - ELEVE - re_path(r'^student/(?P<_id>[0-9]+)$', StudentView.as_view(), name="students"), - - # Page de formulaire d'inscription - RESPONSABLE - re_path(r'^lastGuardian$', GuardianView.as_view(), name="lastGuardian"), - - # Envoi d'un dossier d'inscription - re_path(r'^send/(?P<_id>[0-9]+)$', views.send, name="send"), - - # Archivage d'un dossier d'inscription - re_path(r'^archive/(?P<_id>[0-9]+)$', views.archive, name="archive"), - - # Envoi d'une relance de dossier d'inscription - re_path(r'^sendRelance/(?P<_id>[0-9]+)$', views.relance, name="sendRelance"), - - # Page PARENT - Liste des children - re_path(r'^children/(?P<_id>[0-9]+)$', ChildrenListView.as_view(), name="children"), + re_path(r'^registerForms/(?P[0-9]+)/archive$', archive, name="archive"), + re_path(r'^registerForms/(?P[0-9]+)/resend$', resend, name="resend"), + re_path(r'^registerForms/(?P[0-9]+)/send$', send, name="send"), + re_path(r'^registerForms/(?P[0-9]+)$', RegisterFormWithIdView.as_view(), name="registerForm"), + re_path(r'^registerForms$', RegisterFormView.as_view(), name="registerForms"), # Page INSCRIPTION - Liste des élèves re_path(r'^students$', StudentListView.as_view(), name="students"), + # Page de formulaire d'inscription - ELEVE + re_path(r'^students/(?P[0-9]+)$', StudentView.as_view(), name="students"), + # Page PARENT - Liste des children + re_path(r'^children/(?P[0-9]+)$', ChildrenListView.as_view(), name="children"), + + # Page de formulaire d'inscription - RESPONSABLE + re_path(r'^lastGuardianId$', GuardianView.as_view(), name="lastGuardianId"), # modèles de fichiers d'inscription + re_path(r'^registrationFileTemplates/(?P[0-9]+)$', RegistrationFileTemplateSimpleView.as_view(), name="registrationFileTemplate"), re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'), - re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), # fichiers d'inscription - re_path(r'^registrationFiles/(?P<_id>[0-9]+)$', RegistrationFileView.as_view(), name='registrationFiles'), - re_path(r'^registrationFiles', RegistrationFileView.as_view(), name="registrationFiles"), + re_path(r'^registrationFiles/(?P[0-9]+)$', RegistrationFileSimpleView.as_view(), name='registrationFiles'), + re_path(r'^registrationFiles$', RegistrationFileView.as_view(), name="registrationFiles"), + re_path(r'^registrationFileGroups/(?P[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'), + re_path(r'^registrationFileGroups/(?P[0-9]+)/registrationFiles$', get_registration_files_by_group, name="get_registration_files_by_group"), + re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'), ] \ No newline at end of file diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py deleted file mode 100644 index 4b48e4f..0000000 --- a/Back-End/Subscriptions/views.py +++ /dev/null @@ -1,450 +0,0 @@ -from django.http.response import JsonResponse -from django.contrib.auth import login, authenticate, get_user_model -from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect -from django.utils.decorators import method_decorator -from django.core.cache import cache -from django.core.paginator import Paginator -from django.core.files import File -from django.db.models import Q # Ajout de cet import -from rest_framework.parsers import JSONParser,MultiPartParser, FormParser -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework import status -from drf_yasg.utils import swagger_auto_schema -from drf_yasg import openapi - -import json -from pathlib import Path -import os -from io import BytesIO - -import Subscriptions.mailManager as mailer -import Subscriptions.util as util - -from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine -from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer -from .pagination import CustomPagination -from .signals import clear_cache -from .models import Student, Guardian, RegistrationForm, RegistrationFileTemplate, RegistrationFile -from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine - -from Auth.models import Profile - -from N3wtSchool import settings, renderers, bdd - -class RegisterFormListView(APIView): - """ - Gère la liste des dossiers d’inscription, lecture et création. - """ - pagination_class = CustomPagination - - def get_register_form(self, _filter, search=None): - """ - Récupère les fiches d'inscriptions en fonction du filtre passé. - _filter: Filtre pour déterminer l'état des fiches ('pending', 'archived', 'subscribed') - search: Terme de recherche (optionnel) - """ - if _filter == 'pending': - exclude_states = [RegistrationForm.RegistrationFormStatus.RF_VALIDATED, RegistrationForm.RegistrationFormStatus.RF_ARCHIVED] - return bdd.searchObjects(RegistrationForm, search, _excludeStates=exclude_states) - elif _filter == 'archived': - return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_ARCHIVED) - elif _filter == 'subscribed': - return bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) - return None - - @swagger_auto_schema( - manual_parameters=[ - openapi.Parameter('_filter', openapi.IN_PATH, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), - openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), - openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), - ], - responses={200: RegistrationFormSerializer(many=True)} - ) - def get(self, request, _filter): - """ - Récupère les fiches d'inscriptions en fonction du filtre passé. - """ - # Récupération des paramètres - search = request.GET.get('search', '').strip() - page_size = request.GET.get('page_size', None) - - # Gestion du page_size - if page_size is not None: - try: - page_size = int(page_size) - except ValueError: - page_size = settings.NB_RESULT_PER_PAGE - - # Définir le cache_key en fonction du filtre - page_number = request.GET.get('page', 1) - cache_key = f'N3WT_ficheInscriptions_{_filter}_page_{page_number}_search_{search if _filter == "pending" else ""}' - cached_page = cache.get(cache_key) - if cached_page: - return JsonResponse(cached_page, safe=False) - - # Récupérer les fiches d'inscriptions en fonction du filtre - registerForms_List = self.get_register_form(_filter, search) - - if not registerForms_List: - return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) - - # Pagination - paginator = self.pagination_class() - page = paginator.paginate_queryset(registerForms_List, request) - if page is not None: - registerForms_serializer = RegistrationFormSerializer(page, many=True) - response_data = paginator.get_paginated_response(registerForms_serializer.data) - cache.set(cache_key, response_data, timeout=60*15) - return JsonResponse(response_data, safe=False) - - return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) - - @swagger_auto_schema( - manual_parameters=[ - ], - responses={200: RegistrationFormSerializer(many=True)} - ) - def post(self, request): - studentFormList_serializer=JSONParser().parse(request) - for studentForm_data in studentFormList_serializer: - # Ajout de la date de mise à jour - studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - json.dumps(studentForm_data) - # Ajout du code d'inscription - code = util.genereRandomCode(12) - studentForm_data["codeLienInscription"] = code - studentForm_serializer = RegistrationFormSerializer(data=studentForm_data) - - if studentForm_serializer.is_valid(): - studentForm_serializer.save() - - return JsonResponse(studentForm_serializer.errors, safe=False) - - -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(ensure_csrf_cookie, name='dispatch') -class RegisterFormView(APIView): - """ - Gère la lecture, création, modification et suppression d’un dossier d’inscription. - """ - pagination_class = CustomPagination - - def get(self, request, _id): - """ - Récupère un dossier d'inscription donné. - """ - registerForm=bdd.getObject(RegistrationForm, "student__id", _id) - registerForm_serializer=RegistrationFormSerializer(registerForm) - return JsonResponse(registerForm_serializer.data, safe=False) - - def post(self, request): - """ - Crée un dossier d'inscription. - """ - studentForm_data=JSONParser().parse(request) - # Ajout de la date de mise à jour - studentForm_data["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - json.dumps(studentForm_data) - # Ajout du code d'inscription - code = util.genereRandomCode(12) - studentForm_data["codeLienInscription"] = code - - guardiansId = studentForm_data.pop('idGuardians', []) - studentForm_serializer = RegistrationFormSerializer(data=studentForm_data) - - if studentForm_serializer.is_valid(): - di = studentForm_serializer.save() - - # Mise à jour de l'automate - updateStateMachine(di, 'creationDI') - - # Récupération du reponsable associé - for guardianId in guardiansId: - guardian = Guardian.objects.get(id=guardianId) - di.student.guardians.add(guardian) - di.save() - - return JsonResponse(studentForm_serializer.data, safe=False) - - return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, _id): - """ - Modifie un dossier d'inscription donné. - """ - studentForm_data=JSONParser().parse(request) - _status = studentForm_data.pop('status', 0) - studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) - registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - - if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: - try: - # Génération de la fiche d'inscription au format PDF - base_dir = f"registration_files/dossier_rf_{registerForm.pk}" - os.makedirs(base_dir, exist_ok=True) - - # Fichier PDF initial - initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" - registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) - registerForm.save() - - # Récupération des fichiers d'inscription - fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) - if registerForm.registration_file: - fileNames.insert(0, registerForm.registration_file.path) - - # Création du fichier PDF Fusionné - merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" - util.merge_files_pdf(fileNames, merged_pdf) - - # Mise à jour du champ registration_file avec le fichier fusionné - with open(merged_pdf, 'rb') as f: - registerForm.registration_file.save( - os.path.basename(merged_pdf), - File(f), - save=True - ) - - # Mise à jour de l'automate - updateStateMachine(registerForm, 'saisiDI') - except Exception as e: - return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: - # L'école a validé le dossier d'inscription - # Mise à jour de l'automate - updateStateMachine(registerForm, 'valideDI') - - - studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data) - if studentForm_serializer.is_valid(): - studentForm_serializer.save() - return JsonResponse(studentForm_serializer.data, safe=False) - - return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, id): - """ - Supprime un dossier d'inscription donné. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) - if register_form != None: - student = register_form.student - student.guardians.clear() - student.profiles.clear() - student.registration_files.clear() - student.delete() - clear_cache() - - return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -class StudentView(APIView): - """ - Gère la lecture d’un élève donné. - """ - def get(self, request, _id): - student = bdd.getObject(_objectName=Student, _columnName='id', _value=_id) - if student is None: - return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - student_serializer = StudentSerializer(student) - return JsonResponse(student_serializer.data, safe=False) - -class GuardianView(APIView): - """ - Récupère le dernier ID de responsable légal créé. - """ - def get(self, request): - lastGuardian = bdd.getLastId(Guardian) - return JsonResponse({"lastid":lastGuardian}, safe=False) - -def send(request, _id): - """ - Envoie le dossier d’inscription par e-mail. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if register_form != None: - student = register_form.student - guardian = student.getMainGuardian() - email = guardian.email - errorMessage = mailer.sendRegisterForm(email) - if errorMessage == '': - register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - # Mise à jour de l'automate - updateStateMachine(register_form, 'envoiDI') - return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) - - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -def archive(request, _id): - """ - Archive le dossier d’inscription visé. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if register_form != None: - register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - # Mise à jour de l'automate - updateStateMachine(register_form, 'archiveDI') - - return JsonResponse({"errorMessage":''}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -def relance(request, _id): - """ - Relance un dossier d’inscription par e-mail. - """ - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if register_form != None: - student = register_form.student - guardian = student.getMainGuardian() - email = guardian.email - errorMessage = mailer.envoieRelanceDossierInscription(email, register_form.codeLienInscription) - if errorMessage == '': - register_form.status=RegistrationForm.RegistrationFormStatus.RF_SENT - register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') - register_form.save() - - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) - - return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -# API utilisée pour la vue parent -class ChildrenListView(APIView): - """ - Pour la vue parent : liste les élèves rattachés à un profil donné. - """ - # Récupération des élèves d'un parent - # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves - def get(self, request, _id): - students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=_id) - students_serializer = RegistrationFormByParentSerializer(students, many=True) - return JsonResponse(students_serializer.data, safe=False) - -# API utilisée pour la vue de création d'un DI -class StudentListView(APIView): - """ - Pour la vue de création d’un dossier d’inscription : liste les élèves disponibles. - """ - # Récupération de la liste des élèves inscrits ou en cours d'inscriptions - def get(self, request): - students = bdd.getAllObjects(_objectName=Student) - students_serializer = StudentByRFCreationSerializer(students, many=True) - return JsonResponse(students_serializer.data, safe=False) - -class RegistrationFileTemplateView(APIView): - """ - Gère les fichiers templates pour les dossiers d’inscription. - """ - parser_classes = (MultiPartParser, FormParser) - - def get(self, request, _id=None): - """ - Récupère les fichiers templates pour les dossiers d’inscription. - """ - if _id is None: - files = RegistrationFileTemplate.objects.all() - serializer = RegistrationFileTemplateSerializer(files, many=True) - return Response(serializer.data) - else : - registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) - if registationFileTemplate is None: - return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileTemplateSerializer(registationFileTemplate) - return JsonResponse(serializer.data, safe=False) - - def put(self, request, _id): - """ - Met à jour un fichier template existant. - """ - registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) - if registationFileTemplate is None: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileTemplateSerializer(registationFileTemplate,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) - - def post(self, request): - """ - Crée un fichier template pour les dossiers d’inscription. - """ - serializer = RegistrationFileTemplateSerializer(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) - - def delete(self, request, _id): - """ - Supprime un fichier template existant. - """ - registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=_id) - if registrationFileTemplate is not None: - registrationFileTemplate.file.delete() # Supprimer le fichier uploadé - registrationFileTemplate.delete() - return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) - else: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - -class RegistrationFileView(APIView): - """ - Gère la création, mise à jour et suppression de fichiers liés à un dossier d’inscription. - """ - parser_classes = (MultiPartParser, FormParser) - - def get(self, request, _id=None): - """ - Récupère les fichiers liés à un dossier d’inscription donné. - """ - if (_id is None): - files = RegistrationFile.objects.all() - serializer = RegistrationFileSerializer(files, many=True) - return Response(serializer.data) - else: - registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) - if registationFile is None: - return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileSerializer(registationFile) - return JsonResponse(serializer.data, safe=False) - - def post(self, request): - """ - Crée un RegistrationFile pour le RegistrationForm associé. - """ - serializer = RegistrationFileSerializer(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) - - - def put(self, request, fileId): - """ - Met à jour un RegistrationFile existant. - """ - registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=fileId) - if registrationFile is None: - return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - serializer = RegistrationFileSerializer(registrationFile, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response({'message': 'Fichier mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, _id): - """ - Supprime un RegistrationFile existant. - """ - registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=_id) - if registrationFile is not None: - registrationFile.file.delete() # Supprimer le fichier uploadé - registrationFile.delete() - return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False) - else: - return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) - diff --git a/Back-End/Subscriptions/views/__init__.py b/Back-End/Subscriptions/views/__init__.py new file mode 100644 index 0000000..755cfd8 --- /dev/null +++ b/Back-End/Subscriptions/views/__init__.py @@ -0,0 +1,24 @@ +from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive +from .registration_file_views import RegistrationFileTemplateView, RegistrationFileTemplateSimpleView, RegistrationFileView, RegistrationFileSimpleView +from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group +from .student_views import StudentView, StudentListView, ChildrenListView +from .guardian_views import GuardianView + +__all__ = [ + 'RegisterFormView', + 'RegisterFormWithIdView', + 'send', + 'resend', + 'archive', + 'RegistrationFileView', + 'RegistrationFileSimpleView', + 'RegistrationFileTemplateView', + 'RegistrationFileTemplateSimpleView', + 'RegistrationFileGroupView', + 'RegistrationFileGroupSimpleView', + 'get_registration_files_by_group', + 'StudentView', + 'StudentListView', + 'ChildrenListView', + 'GuardianView', +] diff --git a/Back-End/Subscriptions/views/guardian_views.py b/Back-End/Subscriptions/views/guardian_views.py new file mode 100644 index 0000000..13e0912 --- /dev/null +++ b/Back-End/Subscriptions/views/guardian_views.py @@ -0,0 +1,34 @@ +from django.http.response import JsonResponse +from rest_framework.views import APIView +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from Subscriptions.models import Guardian +from N3wtSchool import bdd + +class GuardianView(APIView): + """ + Gestion des responsables légaux. + """ + + @swagger_auto_schema( + operation_description="Récupère le dernier ID de responsable légal créé", + operation_summary="Récupèrer le dernier ID de responsable légal créé", + responses={ + 200: openapi.Response( + description="Dernier ID du responsable légal", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'lastid': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Dernier ID créé" + ) + } + ) + ) + } + ) + def get(self, request): + lastGuardian = bdd.getLastId(Guardian) + return JsonResponse({"lastid":lastGuardian}, safe=False) diff --git a/Back-End/Subscriptions/views/register_form_views.py b/Back-End/Subscriptions/views/register_form_views.py new file mode 100644 index 0000000..1464ff4 --- /dev/null +++ b/Back-End/Subscriptions/views/register_form_views.py @@ -0,0 +1,385 @@ +from django.http.response import JsonResponse +from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect +from django.utils.decorators import method_decorator +from django.core.cache import cache +from rest_framework.parsers import JSONParser +from rest_framework.views import APIView +from rest_framework.decorators import action, api_view +from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +import json +import os +from django.core.files import File + +import Subscriptions.mailManager as mailer +import Subscriptions.util as util + +from Subscriptions.serializers import RegistrationFormSerializer +from Subscriptions.pagination import CustomPagination +from Subscriptions.signals import clear_cache +from Subscriptions.models import Student, Guardian, RegistrationForm, RegistrationFile, RegistrationFileGroup +from Subscriptions.automate import updateStateMachine + +from N3wtSchool import settings, bdd + +import logging +logger = logging.getLogger(__name__) + +# /Subscriptions/registerForms +class RegisterFormView(APIView): + """ + Gère la liste des dossiers d’inscription, lecture et création. + """ + pagination_class = CustomPagination + + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter('filter', openapi.IN_QUERY, description="filtre", type=openapi.TYPE_STRING, enum=['pending', 'archived', 'subscribed'], required=True), + openapi.Parameter('search', openapi.IN_QUERY, description="search", type=openapi.TYPE_STRING, required=False), + openapi.Parameter('page_size', openapi.IN_QUERY, description="limite de page lors de la pagination", type=openapi.TYPE_INTEGER, required=False), + ], + responses={200: RegistrationFormSerializer(many=True)}, + operation_description="Récupère les dossier d'inscriptions en fonction du filtre passé.", + operation_summary="Récupérer les dossier d'inscriptions", + examples={ + "application/json": [ + { + "id": 1, + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + "status": "pending", + "last_update": "10-02-2025 10:00" + }, + { + "id": 2, + "student": { + "id": 2, + "first_name": "Jane", + "last_name": "Doe", + "date_of_birth": "2011-02-02" + }, + "status": "archived", + "last_update": "09-02-2025 09:00" + } + ] + } + ) + def get(self, request): + """ + Récupère les fiches d'inscriptions en fonction du filtre passé. + """ + # Récupération des paramètres + filter = request.GET.get('filter', '').strip() + search = request.GET.get('search', '').strip() + page_size = request.GET.get('page_size', None) + + # Gestion du page_size + if page_size is not None: + try: + page_size = int(page_size) + except ValueError: + page_size = settings.NB_RESULT_PER_PAGE + + # Définir le cache_key en fonction du filtre + page_number = request.GET.get('page', 1) + cache_key = f'N3WT_ficheInscriptions_{filter}_page_{page_number}_search_{search if filter == "pending" else ""}' + cached_page = cache.get(cache_key) + if cached_page: + return JsonResponse(cached_page, safe=False) + + # Récupérer les dossier d'inscriptions en fonction du filtre + registerForms_List = None + if filter == 'pending': + exclude_states = [RegistrationForm.RegistrationFormStatus.RF_VALIDATED, RegistrationForm.RegistrationFormStatus.RF_ARCHIVED] + registerForms_List = bdd.searchObjects(RegistrationForm, search, _excludeStates=exclude_states) + elif filter == 'archived': + registerForms_List = bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_ARCHIVED) + elif filter == 'subscribed': + registerForms_List = bdd.getObjects(RegistrationForm, 'status', RegistrationForm.RegistrationFormStatus.RF_VALIDATED) + else: + registerForms_List = None + + if not registerForms_List: + return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) + + # Pagination + paginator = self.pagination_class() + page = paginator.paginate_queryset(registerForms_List, request) + if page is not None: + registerForms_serializer = RegistrationFormSerializer(page, many=True) + response_data = paginator.get_paginated_response(registerForms_serializer.data) + cache.set(cache_key, response_data, timeout=60 * 15) + return JsonResponse(response_data, safe=False) + + return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False) + + @swagger_auto_schema( + request_body=RegistrationFormSerializer, + responses={200: RegistrationFormSerializer()}, + operation_description="Crée un dossier d'inscription.", + operation_summary="Créer un dossier d'inscription", + examples={ + "application/json": { + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + "status": "pending", + "last_update": "10-02-2025 10:00", + "codeLienInscription": "ABC123XYZ456" + } + } + ) + @method_decorator(csrf_protect, name='dispatch') + @method_decorator(ensure_csrf_cookie, name='dispatch') + def post(self, request): + """ + Crée un dossier d'inscription. + """ + regiterFormData = request.data.copy() + logger.info(f"Création d'un dossier d'inscription {request}") + # Ajout de la date de mise à jour + regiterFormData["last_update"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + # Ajout du code d'inscription + code = util.genereRandomCode(12) + regiterFormData["codeLienInscription"] = code + + guardiansId = regiterFormData.pop('idGuardians', []) + registerForm_serializer = RegistrationFormSerializer(data=regiterFormData) + fileGroupId = regiterFormData.pop('fileGroup', None) + + if registerForm_serializer.is_valid(): + di = registerForm_serializer.save() + + # Mise à jour de l'automate + updateStateMachine(di, 'creationDI') + + # Récupération du reponsable associé + for guardianId in guardiansId: + guardian = Guardian.objects.get(id=guardianId) + di.student.guardians.add(guardian) + di.save() + if fileGroupId: + di.fileGroup = RegistrationFileGroup.objects.get(id=fileGroupId) + di.save() + + return JsonResponse(registerForm_serializer.data, safe=False) + else: + logger.error(f"Erreur lors de la validation des données {regiterFormData}") + + return JsonResponse(registerForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + +# /Subscriptions/registerForms/{id} +class RegisterFormWithIdView(APIView): + """ + Gère la lecture, création, modification et suppression d’un dossier d’inscription. + """ + pagination_class = CustomPagination + + @swagger_auto_schema( + responses={200: RegistrationFormSerializer()}, + operation_description="Récupère un dossier d'inscription donné.", + operation_summary="Récupérer un dossier d'inscription", + examples={ + "application/json": { + "id": 1, + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + } + } + ) + def get(self, request, id): + """ + Récupère un dossier d'inscription donné. + """ + registerForm = bdd.getObject(RegistrationForm, "student__id", id) + if registerForm is None: + return JsonResponse({"errorMessage":'Le dossier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + registerForm_serializer = RegistrationFormSerializer(registerForm) + return JsonResponse(registerForm_serializer.data, safe=False) + + @swagger_auto_schema( + request_body=RegistrationFormSerializer, + responses={200: RegistrationFormSerializer()}, + operation_description="Modifie un dossier d'inscription donné.", + operation_summary="Modifier un dossier d'inscription", + examples={ + "application/json": { + "id": 1, + "student": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "2010-01-01" + }, + "status": "under_review", + "last_update": "10-02-2025 10:00" + } + } + ) + @method_decorator(csrf_protect, name='dispatch') + @method_decorator(ensure_csrf_cookie, name='dispatch') + def put(self, request, id): + """ + Modifie un dossier d'inscription donné. + """ + studentForm_data = JSONParser().parse(request) + _status = studentForm_data.pop('status', 0) + studentForm_data["last_update"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) + registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + + if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + try: + # Génération de la fiche d'inscription au format PDF + base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}" + os.makedirs(base_dir, exist_ok=True) + + # Fichier PDF initial + initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" + registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) + registerForm.save() + + # Récupération des fichiers d'inscription + fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) + if registerForm.registration_file: + fileNames.insert(0, registerForm.registration_file.path) + + # Création du fichier PDF Fusionné + merged_pdf = f"{base_dir}/dossier_complet_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, merged_pdf) + + # Mise à jour du champ registration_file avec le fichier fusionné + with open(merged_pdf, 'rb') as f: + registerForm.registration_file.save( + os.path.basename(merged_pdf), + File(f), + save=True + ) + + # Mise à jour de l'automate + updateStateMachine(registerForm, 'saisiDI') + except Exception as e: + return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: + # L'école a validé le dossier d'inscription + # Mise à jour de l'automate + updateStateMachine(registerForm, 'valideDI') + + studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data) + if studentForm_serializer.is_valid(): + studentForm_serializer.save() + return JsonResponse(studentForm_serializer.data, safe=False) + + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + responses={204: 'No Content'}, + operation_description="Supprime un dossier d'inscription donné.", + operation_summary="Supprimer un dossier d'inscription" + ) + @method_decorator(csrf_protect, name='dispatch') + @method_decorator(ensure_csrf_cookie, name='dispatch') + def delete(self, request, id): + """ + Supprime un dossier d'inscription donné. + """ + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + student = register_form.student + student.guardians.clear() + student.profiles.clear() + student.registration_files.clear() + student.delete() + clear_cache() + + return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False) + + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Envoie le dossier d'inscription par e-mail", + operation_summary="Envoyer un dossier d'inscription" +) +@api_view(['GET']) +def send(request,id): + """Envoie le dossier d'inscription par e-mail.""" + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + student = register_form.student + guardian = student.getMainGuardian() + email = guardian.email + errorMessage = mailer.sendRegisterForm(email) + if errorMessage == '': + register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + updateStateMachine(register_form, 'envoiDI') + return JsonResponse({"message": f"Le dossier d'inscription a bien été envoyé à l'addresse {email}"}, safe=False) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Archive le dossier d'inscription", + operation_summary="Archiver un dossier d'inscription" +) +@api_view(['GET']) +def archive(request,id): + """Archive le dossier d'inscription.""" + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + updateStateMachine(register_form, 'archiveDI') + return JsonResponse({"message": "Le dossier a été archivé avec succès"}, safe=False) + return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Relance un dossier d'inscription par e-mail", + operation_summary="Relancer un dossier d'inscription" +) +@api_view(['GET']) +def resend(request,id): + """Relance un dossier d'inscription par e-mail.""" + register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) + if register_form != None: + student = register_form.student + guardian = student.getMainGuardian() + email = guardian.email + errorMessage = mailer.envoieRelanceDossierInscription(email, register_form.codeLienInscription) + if errorMessage == '': + register_form.status=RegistrationForm.RegistrationFormStatus.RF_SENT + register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') + register_form.save() + return JsonResponse({"message": f"Le dossier a été renvoyé à l'adresse {email}"}, safe=False) + return JsonResponse({"errorMessage":errorMessage}, safe=False, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse({"errorMessage":'Dossier d\'inscription non trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Subscriptions/views/registration_file_group_views.py b/Back-End/Subscriptions/views/registration_file_group_views.py new file mode 100644 index 0000000..b71dfc5 --- /dev/null +++ b/Back-End/Subscriptions/views/registration_file_group_views.py @@ -0,0 +1,125 @@ +from django.http.response import JsonResponse +from drf_yasg.utils import swagger_auto_schema +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +from rest_framework.decorators import action, api_view +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from Subscriptions.serializers import RegistrationFileGroupSerializer +from Subscriptions.models import RegistrationFileGroup, RegistrationFileTemplate +from N3wtSchool import bdd + +class RegistrationFileGroupView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les groupes de fichiers d'inscription", + responses={200: RegistrationFileGroupSerializer(many=True)} + ) + def get(self, request): + """ + Récupère tous les groupes de fichiers d'inscription. + """ + groups = RegistrationFileGroup.objects.all() + serializer = RegistrationFileGroupSerializer(groups, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau groupe de fichiers d'inscription", + request_body=RegistrationFileGroupSerializer, + responses={ + 201: RegistrationFileGroupSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + """ + Crée un nouveau groupe de fichiers d'inscription. + """ + serializer = RegistrationFileGroupSerializer(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 RegistrationFileGroupSimpleView(APIView): + @swagger_auto_schema( + operation_description="Récupère un groupe de fichiers d'inscription spécifique", + responses={ + 200: RegistrationFileGroupSerializer, + 404: "Groupe non trouvé" + } + ) + def get(self, request, id): + """ + Récupère un groupe de fichiers d'inscription spécifique. + """ + group = bdd.getObject(_objectName=RegistrationFileGroup, _columnName='id', _value=id) + if group is None: + return JsonResponse({"errorMessage": "Le groupe de fichiers n'a pas été trouvé"}, + status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileGroupSerializer(group) + return JsonResponse(serializer.data) + + @swagger_auto_schema( + operation_description="Met à jour un groupe de fichiers d'inscription", + request_body=RegistrationFileGroupSerializer, + responses={ + 200: RegistrationFileGroupSerializer, + 400: "Données invalides", + 404: "Groupe non trouvé" + } + ) + def put(self, request, id): + """ + Met à jour un groupe de fichiers d'inscription existant. + """ + group = bdd.getObject(_objectName=RegistrationFileGroup, _columnName='id', _value=id) + if group is None: + return JsonResponse({'erreur': "Le groupe de fichiers n'a pas été trouvé"}, + status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileGroupSerializer(group, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @swagger_auto_schema( + operation_description="Supprime un groupe de fichiers d'inscription", + responses={ + 204: "Suppression réussie", + 404: "Groupe non trouvé" + } + ) + def delete(self, request, id): + """ + Supprime un groupe de fichiers d'inscription. + """ + group = bdd.getObject(_objectName=RegistrationFileGroup, _columnName='id', _value=id) + if group is not None: + group.delete() + return JsonResponse({'message': 'La suppression du groupe a été effectuée avec succès'}, + status=status.HTTP_204_NO_CONTENT) + return JsonResponse({'erreur': "Le groupe de fichiers n'a pas été trouvé"}, + status=status.HTTP_404_NOT_FOUND) + +@swagger_auto_schema( + method='get', + responses={200: openapi.Response('Success', schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'message': openapi.Schema(type=openapi.TYPE_STRING) + } + ))}, + operation_description="Récupère les fichiers d'inscription d'un groupe donné", + operation_summary="Récupèrer les fichiers d'inscription d'un groupe donné" +) +@api_view(['GET']) +def get_registration_files_by_group(request, id): + try: + group = RegistrationFileGroup.objects.get(id=id) + templates = RegistrationFileTemplate.objects.filter(group=group) + templates_data = list(templates.values()) + return JsonResponse(templates_data, safe=False) + except RegistrationFileGroup.DoesNotExist: + return JsonResponse({'error': 'Le groupe de fichiers n\'a pas été trouvé'}, status=404) \ No newline at end of file diff --git a/Back-End/Subscriptions/views/registration_file_views.py b/Back-End/Subscriptions/views/registration_file_views.py new file mode 100644 index 0000000..e115b03 --- /dev/null +++ b/Back-End/Subscriptions/views/registration_file_views.py @@ -0,0 +1,211 @@ +from django.http.response import JsonResponse +from django.core.files import File +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 os + +from Subscriptions.serializers import RegistrationFileTemplateSerializer, RegistrationFileSerializer +from Subscriptions.models import RegistrationFileTemplate, RegistrationFile +from N3wtSchool import bdd + + +class RegistrationFileTemplateView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les fichiers templates pour les dossiers d'inscription", + responses={200: RegistrationFileTemplateSerializer(many=True)} + ) + def get(self, request): + """ + Récupère les fichiers templates pour les dossiers d’inscription. + """ + files = RegistrationFileTemplate.objects.all() + serializer = RegistrationFileTemplateSerializer(files, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau fichier template pour les dossiers d'inscription", + request_body=RegistrationFileTemplateSerializer, + responses={ + 201: RegistrationFileTemplateSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + """ + Crée un fichier template pour les dossiers d’inscription. + """ + serializer = RegistrationFileTemplateSerializer(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 RegistrationFileTemplateSimpleView(APIView): + """ + Gère les fichiers templates pour les dossiers d’inscription. + """ + parser_classes = (MultiPartParser, FormParser) + + @swagger_auto_schema( + operation_description="Récupère un fichier template spécifique", + responses={ + 200: RegistrationFileTemplateSerializer, + 404: "Fichier template non trouvé" + } + ) + def get(self, request, id): + """ + Récupère les fichiers templates pour les dossiers d’inscription. + """ + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) + if registationFileTemplate is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un fichier template existant", + request_body=RegistrationFileTemplateSerializer, + responses={ + 201: RegistrationFileTemplateSerializer, + 400: "Données invalides", + 404: "Fichier template non trouvé" + } + ) + def put(self, request, id): + """ + Met à jour un fichier template existant. + """ + registationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) + if registationFileTemplate is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileTemplateSerializer(registationFileTemplate,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) + + @swagger_auto_schema( + operation_description="Supprime un fichier template", + responses={ + 204: "Suppression réussie", + 404: "Fichier template non trouvé" + } + ) + def delete(self, request, id): + """ + Supprime un fichier template existant. + """ + registrationFileTemplate = bdd.getObject(_objectName=RegistrationFileTemplate, _columnName='id', _value=id) + if registrationFileTemplate is not None: + registrationFileTemplate.file.delete() # Supprimer le fichier uploadé + registrationFileTemplate.delete() + return JsonResponse({'message': 'La suppression du fichier d\'inscription a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT) + else: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + +class RegistrationFileView(APIView): + @swagger_auto_schema( + operation_description="Récupère tous les fichiers d'inscription", + responses={200: RegistrationFileSerializer(many=True)} + ) + def get(self, request): + """ + Récupère les fichiers liés à un dossier d’inscription donné. + """ + files = RegistrationFile.objects.all() + serializer = RegistrationFileSerializer(files, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + operation_description="Crée un nouveau fichier d'inscription", + request_body=RegistrationFileSerializer, + responses={ + 201: RegistrationFileSerializer, + 400: "Données invalides" + } + ) + def post(self, request): + """ + Crée un RegistrationFile pour le RegistrationForm associé. + """ + serializer = RegistrationFileSerializer(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 RegistrationFileSimpleView(APIView): + """ + Gère la création, mise à jour et suppression de fichiers liés à un dossier d’inscription. + """ + parser_classes = (MultiPartParser, FormParser) + + @swagger_auto_schema( + operation_description="Récupère un fichier d'inscription spécifique", + responses={ + 200: RegistrationFileSerializer, + 404: "Fichier non trouvé" + } + ) + def get(self, request, id): + """ + Récupère les fichiers liés à un dossier d’inscription donné. + """ + registationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) + if registationFile is None: + return JsonResponse({"errorMessage":'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registationFile) + return JsonResponse(serializer.data, safe=False) + + @swagger_auto_schema( + operation_description="Met à jour un fichier d'inscription existant", + request_body=RegistrationFileSerializer, + responses={ + 200: openapi.Response( + description="Fichier mis à jour avec succès", + schema=RegistrationFileSerializer + ), + 400: "Données invalides", + 404: "Fichier non trouvé" + } + ) + def put(self, request, id): + """ + Met à jour un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) + if registrationFile is None: + return JsonResponse({'erreur': 'Le fichier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + serializer = RegistrationFileSerializer(registrationFile, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Fichier 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 d'inscription", + responses={ + 200: "Suppression réussie", + 404: "Fichier non trouvé" + } + ) + def delete(self, request, id): + """ + Supprime un RegistrationFile existant. + """ + registrationFile = bdd.getObject(_objectName=RegistrationFile, _columnName='id', _value=id) + if registrationFile is not None: + registrationFile.file.delete() # Supprimer le fichier uploadé + registrationFile.delete() + return JsonResponse({'message': 'La suppression du fichier a été effectuée avec succès'}, safe=False) + else: + return JsonResponse({'erreur': 'Le fichier n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) diff --git a/Back-End/Subscriptions/views/student_views.py b/Back-End/Subscriptions/views/student_views.py new file mode 100644 index 0000000..a84a65a --- /dev/null +++ b/Back-End/Subscriptions/views/student_views.py @@ -0,0 +1,83 @@ +from django.http.response import JsonResponse +from rest_framework.views import APIView +from rest_framework import status +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from Subscriptions.serializers import StudentByRFCreationSerializer, RegistrationFormByParentSerializer, StudentSerializer +from Subscriptions.models import Student, RegistrationForm + +from N3wtSchool import bdd + +class StudentView(APIView): + """ + Gère la lecture d’un élève donné. + """ + @swagger_auto_schema( + operation_summary="Récupérer les informations d'un élève", + operation_description="Retourne les détails d'un élève spécifique à partir de son ID", + responses={ + 200: openapi.Response('Détails de l\'élève', StudentSerializer), + 404: openapi.Response('Élève non trouvé') + }, + manual_parameters=[ + openapi.Parameter( + 'id', openapi.IN_PATH, + description="ID de l'élève", + type=openapi.TYPE_INTEGER, + required=True + ) + ] + ) + def get(self, request, id): + student = bdd.getObject(_objectName=Student, _columnName='id', _value=id) + if student is None: + return JsonResponse({"errorMessage":'Aucun élève trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) + student_serializer = StudentSerializer(student) + return JsonResponse(student_serializer.data, safe=False) + +# API utilisée pour la vue de création d'un DI +class StudentListView(APIView): + """ + Pour la vue de création d’un dossier d’inscription : liste les élèves disponibles. + """ + @swagger_auto_schema( + operation_summary="Lister tous les élèves", + operation_description="Retourne la liste de tous les élèves inscrits ou en cours d'inscription", + responses={ + 200: openapi.Response('Liste des élèves', StudentByRFCreationSerializer(many=True)) + } + ) + # Récupération de la liste des élèves inscrits ou en cours d'inscriptions + def get(self, request): + students = bdd.getAllObjects(_objectName=Student) + students_serializer = StudentByRFCreationSerializer(students, many=True) + return JsonResponse(students_serializer.data, safe=False) + + +# API utilisée pour la vue parent +class ChildrenListView(APIView): + """ + Pour la vue parent : liste les élèves rattachés à un profil donné. + """ + @swagger_auto_schema( + operation_summary="Lister les élèves d'un parent", + operation_description="Retourne la liste des élèves associés à un profil parent spécifique", + responses={ + 200: openapi.Response('Liste des élèves du parent', RegistrationFormByParentSerializer(many=True)) + }, + manual_parameters=[ + openapi.Parameter( + 'id', openapi.IN_PATH, + description="ID du profil parent", + type=openapi.TYPE_INTEGER, + required=True + ) + ] + ) + # Récupération des élèves d'un parent + # idProfile : identifiant du profil connecté rattaché aux fiches d'élèves + def get(self, request, id): + students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__associated_profile__id', _value=id) + students_serializer = RegistrationFormByParentSerializer(students, many=True) + return JsonResponse(students_serializer.data, safe=False) diff --git a/Back-End/src/app.js b/Back-End/src/app.js new file mode 100644 index 0000000..e69de29 diff --git a/Back-End/src/middleware/cors.js b/Back-End/src/middleware/cors.js new file mode 100644 index 0000000..e69de29 diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 46ca74e..51a7722 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -6,27 +6,35 @@ import FeesManagement from '@/components/Structure/Tarification/FeesManagement'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { createDatas, +import { createDatas, updateDatas, removeDatas, - fetchSpecialities, - fetchTeachers, - fetchClasses, - fetchSchedules, - fetchRegistrationDiscounts, - fetchTuitionDiscounts, - fetchRegistrationFees, - fetchTuitionFees } from '@/app/lib/schoolAction'; + fetchSpecialities, + fetchTeachers, + fetchClasses, + fetchSchedules, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, + fetchTuitionFees, + } from '@/app/lib/schoolAction'; import SidebarTabs from '@/components/SidebarTabs'; +import FilesManagement from '@/components/Structure/Files/FilesManagement'; + +import { fetchRegisterFormFileTemplate } from '@/app/lib/subscriptionAction'; + + export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); + const [schedules, setSchedules] = useState([]); // Add this line const [registrationDiscounts, setRegistrationDiscounts] = useState([]); const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); + const [fichiers, setFichiers] = useState([]); const csrfToken = useCsrfToken(); @@ -42,18 +50,27 @@ export default function Page() { // Fetch data for schedules handleSchedules(); - + // Fetch data for registration discounts handleRegistrationDiscounts(); - + // Fetch data for tuition discounts handleTuitionDiscounts(); - + // Fetch data for registration fees handleRegistrationFees(); - + // Fetch data for tuition fees handleTuitionFees(); + + // Fetch data for registration file templates + fetchRegisterFormFileTemplate() + .then((data)=> { + setFichiers(data) + }) + .catch(error => console.error('Error fetching files:', error)); + + }, []); const handleSpecialities = () => { @@ -96,7 +113,7 @@ export default function Page() { .catch(error => console.error('Error fetching registration discounts:', error)); }; - const handleTuitionDiscounts = () => { + const handleTuitionDiscounts = () => { fetchTuitionDiscounts() .then(data => { setTuitionDiscounts(data); @@ -224,6 +241,11 @@ export default function Page() { handleDelete={handleDelete} /> ) + }, + { + id: 'Files', + label: 'Documents d\'inscription', + content: } ]; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js index 9de4858..9c09fca 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js @@ -11,11 +11,10 @@ import Loader from '@/components/Loader'; import AlertWithModal from '@/components/AlertWithModal'; import DropdownMenu from "@/components/DropdownMenu"; import { formatPhoneNumber } from '@/utils/Telephone'; -import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus, Download } from 'lucide-react'; +import { MoreVertical, Send, Edit, Trash2, FileText, CheckCircle, Plus } from 'lucide-react'; import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' import AffectationClasseForm from '@/components/AffectationClasseForm' -import FileUpload from './components/FileUpload'; import { PENDING, @@ -26,17 +25,14 @@ import { sendRegisterForm, archiveRegisterForm, fetchRegisterFormFileTemplate, - deleteRegisterFormFileTemplate, - createRegistrationFormFileTemplate, - editRegistrationFormFileTemplate, fetchStudents, editRegisterForm } from "@/app/lib/subscriptionAction" -import { +import { fetchClasses, - fetchRegistrationDiscounts, - fetchTuitionDiscounts, - fetchRegistrationFees, + fetchRegistrationDiscounts, + fetchTuitionDiscounts, + fetchRegistrationFees, fetchTuitionFees } from '@/app/lib/schoolAction'; import { createProfile } from '@/app/lib/authAction'; @@ -47,7 +43,7 @@ import { import DjangoCSRFToken from '@/components/DjangoCSRFToken' import useCsrfToken from '@/hooks/useCsrfToken'; -import { formatDate } from '@/utils/Date'; +import { fetchRegistrationFileGroups } from '@/app/lib/registerFileGroupAction'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; @@ -77,14 +73,13 @@ export default function Page({ params: { locale } }) { const [classes, setClasses] = useState([]); const [students, setEleves] = useState([]); const [reloadFetch, setReloadFetch] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [fileToEdit, setFileToEdit] = useState(null); + const [registrationDiscounts, setRegistrationDiscounts] = useState([]); const [tuitionDiscounts, setTuitionDiscounts] = useState([]); const [registrationFees, setRegistrationFees] = useState([]); const [tuitionFees, setTuitionFees] = useState([]); + const [groups, setGroups] = useState([]); const csrfToken = useCsrfToken(); @@ -228,6 +223,11 @@ const registerFormArchivedDataHandler = (data) => { setTuitionFees(data); }) .catch(requestErrorHandler); + fetchRegistrationFileGroups() + .then(data => { + setGroups(data); + }) + .catch(error => console.error('Error fetching file groups:', error)); } else { setTimeout(() => { setRegistrationFormsDataPending(mockFicheInscription); @@ -357,7 +357,7 @@ useEffect(()=>{ const selectedRegistrationDiscountsIds = updatedData.selectedRegistrationDiscounts.map(discountId => discountId) const selectedTuitionFeesIds = updatedData.selectedTuitionFees.map(feeId => feeId) const selectedTuitionDiscountsIds = updatedData.selectedTuitionDiscounts.map(discountId => discountId) - + const selectedFileGroup = updatedData.selectedFileGroup const allFeesIds = [...selectedRegistrationFeesIds, ...selectedTuitionFeesIds]; const allDiscountsds = [...selectedRegistrationDiscountsIds, ...selectedTuitionDiscountsIds]; @@ -370,7 +370,8 @@ useEffect(()=>{ }, idGuardians: selectedGuardiansIds, fees: allFeesIds, - discounts: allDiscountsds + discounts: allDiscountsds, + fileGroup: selectedFileGroup }; createRegisterForm(data, csrfToken) @@ -567,89 +568,23 @@ const columnsSubscribed = [ ]; -const handleFileDelete = (fileId) => { - deleteRegisterFormFileTemplate(fileId,csrfToken) - .then(response => { - if (response.ok) { - setFichiers(fichiers.filter(fichier => fichier.id !== fileId)); - alert('Fichier supprimé avec succès.'); - } else { - alert('Erreur lors de la suppression du fichier.'); - } - }) - .catch(error => { - console.error('Error deleting file:', error); - alert('Erreur lors de la suppression du fichier.'); - }); -}; - -const handleFileEdit = (file) => { - setIsEditing(true); - setFileToEdit(file); - setIsModalOpen(true); -}; - -const columnsFiles = [ - { name: 'Nom du fichier', transform: (row) => row.name }, - { name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") }, - { name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' }, - { name: 'Ordre de fusion', transform: (row) => row.order }, - { name: 'Actions', transform: (row) => ( -
- { - row.file && ( - - - ) - } - - -
- ) }, -]; - -const handleFileUpload = ({file, name, is_required, order}) => { - if (!name) { - alert('Veuillez entrer un nom de fichier.'); - return; - } - - const formData = new FormData(); - if(file){ - formData.append('file', file); - } - formData.append('name', name); - formData.append('is_required', is_required); - formData.append('order', order); - - if (isEditing && fileToEdit) { - editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken) - .then(data => { - setFichiers(prevFichiers => - prevFichiers.map(f => f.id === fileToEdit.id ? data : f) - ); - setIsModalOpen(false); - setFileToEdit(null); - setIsEditing(false); - }) - .catch(error => { - console.error('Error editing file:', error); - }); - } else { - createRegistrationFormFileTemplate(formData, csrfToken) - .then(data => { - setFichiers([...fichiers, data]); - setIsModalOpen(false); - }) - .catch(error => { - console.error('Error uploading file:', error); - }); - } -}; + const tabs = [ + { + id: 'pending', + label: t('pending'), + count: totalPending + }, + { + id: 'subscribed', + label: t('subscribed'), + count: totalSubscribed + }, + { + id: 'archived', + label: t('archived'), + count: totalArchives + } + ]; if (isLoading) { return ; @@ -699,16 +634,6 @@ const handleFileUpload = ({file, name, is_required, order}) => { active={activeTab === 'archived'} onClick={() => setActiveTab('archived')} /> - - {t('subscribeFiles')} - ({fichiers.length}) - - )} - active={activeTab === 'subscribeFiles'} - onClick={() => setActiveTab('subscribeFiles')} - />
@@ -758,41 +683,6 @@ const handleFileUpload = ({file, name, is_required, order}) => {
) : null} - {/*SI STATE == subscribeFiles */} - {activeTab === 'subscribeFiles' && ( -
-
- -
- ( - - )} - /> -
- - - - )} { tuitionDiscounts={tuitionDiscounts} registrationFees={registrationFees.filter(fee => fee.is_active)} tuitionFees={tuitionFees.filter(fee => fee.is_active)} + groups={groups} onSubmit={createRF} /> )} diff --git a/Front-End/src/app/lib/registerFileGroupAction.js b/Front-End/src/app/lib/registerFileGroupAction.js new file mode 100644 index 0000000..9db0398 --- /dev/null +++ b/Front-End/src/app/lib/registerFileGroupAction.js @@ -0,0 +1,74 @@ +import { BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL } from '@/utils/Url'; + +export async function fetchRegistrationFileGroups() { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, { + credentials: 'include', + headers: { + 'Accept': 'application/json', + } + }); + if (!response.ok) { + throw new Error('Failed to fetch file groups'); + } + return response.json(); +} + +export async function createRegistrationFileGroup(groupData, csrfToken) { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify(groupData), + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to create file group'); + } + + return response.json(); +} + +export async function deleteRegistrationFileGroup(groupId, csrfToken) { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken, + }, + credentials: 'include' + }); + + return response; +} + +export const editRegistrationFileGroup = async (groupId, groupData, csrfToken) => { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + }, + body: JSON.stringify(groupData), + }); + + if (!response.ok) { + throw new Error('Erreur lors de la modification du groupe'); + } + + return response.json(); +}; + +export const fetchRegistrationFileFromGroup = async (groupId) => { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/registrationFiles`, { + credentials: 'include', + headers: { + 'Accept': 'application/json', + } + }); + if (!response.ok) { + throw new Error('Erreur lors de la récupération des fichiers associés au groupe'); + } + return response.json(); +} \ No newline at end of file diff --git a/Front-End/src/app/lib/subscriptionAction.js b/Front-End/src/app/lib/subscriptionAction.js index b593394..76cbb0c 100644 --- a/Front-End/src/app/lib/subscriptionAction.js +++ b/Front-End/src/app/lib/subscriptionAction.js @@ -1,13 +1,9 @@ import { BE_SUBSCRIPTION_STUDENTS_URL, - BE_SUBSCRIPTION_STUDENT_URL, - BE_SUBSCRIPTION_ARCHIVE_URL, - BE_SUBSCRIPTION_SEND_URL, BE_SUBSCRIPTION_CHILDRENS_URL, - BE_SUBSCRIPTION_REGISTERFORM_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_TEMPLATE_URL, - BE_SUBSCRIPTION_LAST_GUARDIAN_URL, + BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL } from '@/utils/Url'; @@ -28,10 +24,10 @@ const requestResponseHandler = async (response) => { throw error; } -export const fetchRegisterForms = (type=PENDING, page='', pageSize='', search = '') => { - let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}`; +export const fetchRegisterForms = (filter=PENDING, page='', pageSize='', search = '') => { + let url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}`; if (page !== '' && pageSize !== '') { - url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${type}?page=${page}&search=${search}`; + url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&page=${page}&search=${search}`; } return fetch(url, { headers: { @@ -40,18 +36,17 @@ export const fetchRegisterForms = (type=PENDING, page='', pageSize='', search = }).then(requestResponseHandler) }; -export const fetchRegisterForm = (id) =>{ - return fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${id}`) // Utilisation de studentId au lieu de codeDI +export const fetchRegisterForm = (id) =>{ + return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI .then(requestResponseHandler) } -export const fetchLastGuardian = () =>{ - return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_URL}`) +export const fetchLastGuardian = () =>{ + return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`) .then(requestResponseHandler) } export const editRegisterForm=(id, data, csrfToken)=>{ - - return fetch(`${BE_SUBSCRIPTION_REGISTERFORM_URL}/${id}`, { + return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -61,11 +56,11 @@ export const editRegisterForm=(id, data, csrfToken)=>{ credentials: 'include' }) .then(requestResponseHandler) - }; + export const createRegisterForm=(data, csrfToken)=>{ - const url = `${BE_SUBSCRIPTION_REGISTERFORM_URL}`; + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`; return fetch(url, { method: 'POST', headers: { @@ -78,8 +73,26 @@ export const createRegisterForm=(data, csrfToken)=>{ .then(requestResponseHandler) } +export const sendRegisterForm = (id) => { + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`; + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler) +} + +export const resendRegisterForm = (id) => { + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`; + return fetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(requestResponseHandler) + +} export const archiveRegisterForm = (id) => { - const url = `${BE_SUBSCRIPTION_ARCHIVE_URL}/${id}`; + const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`; return fetch(url, { method: 'GET', headers: { @@ -88,18 +101,6 @@ export const archiveRegisterForm = (id) => { }).then(requestResponseHandler) } -export const sendRegisterForm = (id) => { - const url = `${BE_SUBSCRIPTION_SEND_URL}/${id}`; - return fetch(url, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(requestResponseHandler) - -} - - - export const fetchRegisterFormFile = (id = null) => { let url = `${BE_SUBSCRIPTION_REGISTRATIONFORMFILE_URL}` if (id) { @@ -204,9 +205,10 @@ export const editRegistrationFormFileTemplate = (fileId, data, csrfToken) => { .then(requestResponseHandler) } -export const fetchStudents = () => { +export const fetchStudents = (id) => { + const url = (id)?`${BE_SUBSCRIPTION_STUDENTS_URL}/${id}`:`${BE_SUBSCRIPTION_STUDENTS_URL}`; const request = new Request( - `${BE_SUBSCRIPTION_STUDENTS_URL}`, + url, { method:'GET', headers: { @@ -229,4 +231,17 @@ export const fetchChildren = (id) =>{ } ); return fetch(request).then(requestResponseHandler) +} + +export async function getRegisterFormFileTemplate(fileId) { + const response = await fetch(`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`, { + credentials: 'include', + headers: { + 'Accept': 'application/json', + } + }); + if (!response.ok) { + throw new Error('Failed to fetch file template'); + } + return response.json(); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js b/Front-End/src/components/DraggableFileUpload.js similarity index 100% rename from Front-End/src/app/[locale]/admin/subscriptions/components/DraggableFileUpload.js rename to Front-End/src/components/DraggableFileUpload.js diff --git a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js b/Front-End/src/components/FileUpload.js similarity index 74% rename from Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js rename to Front-End/src/components/FileUpload.js index 92cd485..a349e83 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/components/FileUpload.js +++ b/Front-End/src/components/FileUpload.js @@ -1,18 +1,24 @@ import React, { useState, useEffect } from 'react'; import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch import DraggableFileUpload from './DraggableFileUpload'; +import { fetchRegistrationFileGroups } from '@/app/lib/registerFileGroupAction'; export default function FileUpload({ onFileUpload, fileToEdit = null }) { const [fileName, setFileName] = useState(''); const [file, setFile] = useState(null); const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired const [order, setOrder] = useState(0); + const [groups, setGroups] = useState([]); + const [selectedGroup, setSelectedGroup] = useState(''); useEffect(() => { + fetchRegistrationFileGroups().then(data => setGroups(data)); + if (fileToEdit) { setFileName(fileToEdit.name || ''); setIsRequired(fileToEdit.is_required || false); setOrder(fileToEdit.fusion_order || 0); + setSelectedGroup(fileToEdit.group_id || ''); } }, [fileToEdit]); @@ -26,11 +32,13 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) { name: fileName, is_required: isRequired, order: parseInt(order, 10), + groupId: selectedGroup || null }); setFile(null); setFileName(''); setIsRequired(false); setOrder(0); + setSelectedGroup(''); }; return ( @@ -72,6 +80,19 @@ export default function FileUpload({ onFileUpload, fileToEdit = null }) { onChange={() => setIsRequired(!isRequired)} /> +
+ + +
); } \ No newline at end of file diff --git a/Front-End/src/components/Inscription/InscriptionForm.js b/Front-End/src/components/Inscription/InscriptionForm.js index 22a23fb..9332c80 100644 --- a/Front-End/src/components/Inscription/InscriptionForm.js +++ b/Front-End/src/components/Inscription/InscriptionForm.js @@ -9,7 +9,7 @@ import DiscountsSection from '@/components/Structure/Tarification/DiscountsSecti import SectionTitle from '@/components/SectionTitle'; import ProgressStep from '@/components/ProgressStep'; -const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit, currentStep }) => { +const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, onSubmit, currentStep, groups }) => { const [formData, setFormData] = useState({ studentLastName: '', studentFirstName: '', @@ -21,7 +21,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r selectedRegistrationDiscounts: [], selectedRegistrationFees: registrationFees.map(fee => fee.id), selectedTuitionDiscounts: [], - selectedTuitionFees: [] + selectedTuitionFees: [], + selectedFileGroup: null // Ajout du groupe de fichiers sélectionné }); const [step, setStep] = useState(currentStep || 1); @@ -35,10 +36,11 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r 2: 'Nouveau Responsable', 3: "Frais d'inscription", 4: 'Frais de scolarité', - 5: 'Récapitulatif' + 5: 'Documents requis', + 6: 'Récapitulatif' }; - const steps = ['Élève', 'Responsable', 'Inscription', 'Scolarité', 'Récap']; + const steps = ['Élève', 'Responsable', 'Inscription', 'Scolarité', 'Documents', 'Récap']; const isStep1Valid = formData.studentLastName && formData.studentFirstName; const isStep2Valid = ( @@ -47,7 +49,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r ); const isStep3Valid = formData.selectedRegistrationFees.length > 0; const isStep4Valid = formData.selectedTuitionFees.length > 0; - const isStep5Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid; + const isStep5Valid = formData.selectedFileGroup !== null; + const isStep6Valid = isStep1Valid && isStep2Valid && isStep3Valid && isStep4Valid && isStep5Valid; const isStepValid = (stepNumber) => { switch (stepNumber) { @@ -61,6 +64,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r return isStep4Valid; case 5: return isStep5Valid; + case 6: + return isStep6Valid; default: return false; } @@ -464,6 +469,44 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r )} + {step === 5 && ( +
+ {groups.length > 0 ? ( +
+

Sélectionnez un groupe de documents

+ {groups.map((group) => ( +
+ setFormData({ + ...formData, + selectedFileGroup: parseInt(e.target.value) + })} + className="form-radio h-4 w-4 text-emerald-600" + /> + +
+ ))} +
+ ) : ( +

+ Attention! + Aucun groupe de documents n'a été créé. +

+ )} +
+ )} + {step === steps.length && (
@@ -553,7 +596,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r (step === 1 && !isStep1Valid) || (step === 2 && !isStep2Valid) || (step === 3 && !isStep3Valid) || - (step === 4 && !isStep4Valid) + (step === 4 && !isStep4Valid) || + (step === 5 && !isStep5Valid) ) ? "bg-gray-300 text-gray-700 cursor-not-allowed" : "bg-emerald-500 text-white hover:bg-emerald-600" @@ -563,7 +607,8 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r (step === 1 && !isStep1Valid) || (step === 2 && !isStep2Valid) || (step === 3 && !isStep3Valid) || - (step === 4 && !isStep4Valid) + (step === 4 && !isStep4Valid) || + (step === 5 && !isStep5Valid) ) } primary diff --git a/Front-End/src/components/Inscription/InscriptionFormShared.js b/Front-End/src/components/Inscription/InscriptionFormShared.js index fad9a90..221e9b6 100644 --- a/Front-End/src/components/Inscription/InscriptionFormShared.js +++ b/Front-End/src/components/Inscription/InscriptionFormShared.js @@ -8,9 +8,10 @@ import Button from '@/components/Button'; import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import Table from '@/components/Table'; import { fetchRegisterFormFileTemplate, createRegistrationFormFile, fetchRegisterForm, deleteRegisterFormFile } from '@/app/lib/subscriptionAction'; +import { fetchRegistrationFileFromGroup } from '@/app/lib/registerFileGroupAction'; import { Download, Upload, Trash2, Eye } from 'lucide-react'; import { BASE_URL } from '@/utils/Url'; -import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload'; +import DraggableFileUpload from '@/components/DraggableFileUpload'; import Modal from '@/components/Modal'; import FileStatusLabel from '@/components/FileStatusLabel'; @@ -57,6 +58,7 @@ export default function InscriptionFormShared({ // États pour la gestion des fichiers const [uploadedFiles, setUploadedFiles] = useState([]); const [fileTemplates, setFileTemplates] = useState([]); + const [fileGroup, setFileGroup] = useState(null); const [fileName, setFileName] = useState(""); const [file, setFile] = useState(""); const [showUploadModal, setShowUploadModal] = useState(false); @@ -83,15 +85,21 @@ export default function InscriptionFormShared({ }); setGuardians(data?.student?.guardians || []); setUploadedFiles(data.registration_files || []); + setFileGroup(data.fileGroup || null); }); - fetchRegisterFormFileTemplate().then((data) => { - setFileTemplates(data); - }); setIsLoading(false); } }, [studentId]); + useEffect(() => { + if(fileGroup){ + fetchRegistrationFileFromGroup(fileGroup).then((data) => { + setFileTemplates(data); + }); + } + }, [fileGroup]); + // Fonctions de gestion du formulaire et des fichiers const updateFormField = (field, value) => { setFormData(prev => ({...prev, [field]: value})); diff --git a/Front-End/src/components/RegistrationFileGroupForm.js b/Front-End/src/components/RegistrationFileGroupForm.js new file mode 100644 index 0000000..c3ac53e --- /dev/null +++ b/Front-End/src/components/RegistrationFileGroupForm.js @@ -0,0 +1,56 @@ +import React, { useState, useEffect } from 'react'; + +export default function RegistrationFileGroupForm({ onSubmit, initialData }) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + + useEffect(() => { + if (initialData) { + setName(initialData.name); + setDescription(initialData.description); + } + }, [initialData]); + + const handleSubmit = (e) => { + e.preventDefault(); + onSubmit({ name, description }); + }; + + return ( +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ +
+ +