From 81d1dfa9a70d0cd8d80e7d951a74c9355bba5238 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Sat, 23 Nov 2024 20:02:51 +0100 Subject: [PATCH 001/106] feat: Gestion des profils des enseignants / Visualisation d'une classe [#4] --- Back-End/GestionEnseignants/models.py | 2 + Back-End/GestionEnseignants/serializers.py | 38 ++- Back-End/GestionEnseignants/views.py | 15 +- Back-End/GestionInscriptions/models.py | 51 +++- Back-End/GestionInscriptions/serializers.py | 27 ++- Back-End/GestionInscriptions/urls.py | 7 +- Back-End/GestionInscriptions/util.py | 20 ++ Back-End/GestionInscriptions/views.py | 168 ++++++-------- Back-End/GestionLogin/models.py | 8 +- Back-End/GestionLogin/serializers.py | 23 ++ Back-End/GestionLogin/views.py | 5 +- Back-End/N3wtSchool/bdd.py | 10 +- Front-End/messages/en/students.json | 2 + Front-End/messages/fr/students.json | 2 + Front-End/src/app/[locale]/admin/page.js | 28 ++- .../src/app/[locale]/admin/structure/page.js | 3 - .../src/app/[locale]/admin/students/page.js | 219 ++++++++++++++++-- .../src/app/[locale]/users/login/page.js | 16 +- .../src/components/AffectationClasseForm.js | 68 ++++++ Front-End/src/components/ClasseDetails.js | 72 ++++++ Front-End/src/components/ClassesSection.js | 41 +++- Front-End/src/components/Modal.js | 2 +- Front-End/src/components/Sidebar.js | 2 +- Front-End/src/components/Table.js | 2 +- Front-End/src/components/TeacherForm.js | 44 +++- Front-End/src/components/TeachersSection.js | 95 +++++++- 26 files changed, 792 insertions(+), 178 deletions(-) create mode 100644 Front-End/src/components/AffectationClasseForm.js create mode 100644 Front-End/src/components/ClasseDetails.js diff --git a/Back-End/GestionEnseignants/models.py b/Back-End/GestionEnseignants/models.py index 6a2afb8..ca32ef6 100644 --- a/Back-End/GestionEnseignants/models.py +++ b/Back-End/GestionEnseignants/models.py @@ -1,4 +1,5 @@ from django.db import models +from GestionLogin.models import Profil class Specialite(models.Model): nom = models.CharField(max_length=100) @@ -13,6 +14,7 @@ class Enseignant(models.Model): prenom = models.CharField(max_length=100) mail = models.EmailField(unique=True) specialite = models.ForeignKey(Specialite, on_delete=models.SET_NULL, null=True, blank=True, related_name='enseignants') + profilAssocie = models.ForeignKey(Profil, on_delete=models.CASCADE, null=True, blank=True) def __str__(self): return f"{self.nom} {self.prenom}" diff --git a/Back-End/GestionEnseignants/serializers.py b/Back-End/GestionEnseignants/serializers.py index a3e3a38..4872c6f 100644 --- a/Back-End/GestionEnseignants/serializers.py +++ b/Back-End/GestionEnseignants/serializers.py @@ -1,6 +1,10 @@ from rest_framework import serializers from .models import Enseignant, Specialite, Classe -from N3wtSchool import settings +from GestionInscriptions.models import FicheInscription +from GestionInscriptions.serializers import EleveSerializer +from GestionLogin.serializers import ProfilSerializer +from GestionLogin.models import Profil +from N3wtSchool import settings, bdd from django.utils import timezone import pytz @@ -23,10 +27,11 @@ class ClasseSerializer(serializers.ModelSerializer): dateCreation_formattee = serializers.SerializerMethodField() enseignant_principal = serializers.SerializerMethodField() enseignant_principal_id = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), source='enseignant_principal', write_only=False, read_only=False) + eleves = serializers.SerializerMethodField() class Meta: model = Classe - fields = ['id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'specialites', 'specialites_ids', 'enseignant_principal', 'enseignant_principal_id', 'annee_scolaire', 'dateCreation', 'dateCreation_formattee'] + fields = ['id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'specialites', 'specialites_ids', 'enseignant_principal', 'enseignant_principal_id', 'annee_scolaire', 'dateCreation', 'dateCreation_formattee', 'eleves'] def get_enseignant_principal(self, obj): from .serializers import EnseignantDetailSerializer @@ -59,22 +64,45 @@ class ClasseSerializer(serializers.ModelSerializer): return local_time.strftime("%d-%m-%Y %H:%M") + def get_eleves(self, obj): + elevesList = obj.eleves.all() + filtered_eleves = [] + for eleve in elevesList: + ficheInscription=bdd.getObject(FicheInscription, "eleve__id", eleve.id) + if ficheInscription.etat == ficheInscription.EtatDossierInscription.DI_VALIDE: + filtered_eleves.append(eleve) + return EleveSerializer(filtered_eleves, many=True, read_only=True).data + class EnseignantSerializer(serializers.ModelSerializer): specialite = SpecialiteSerializer(read_only=True) specialite_id = serializers.PrimaryKeyRelatedField(queryset=Specialite.objects.all(), source='specialite', write_only=False, read_only=False) + profilAssocie_id = serializers.PrimaryKeyRelatedField(queryset=Profil.objects.all(), source='profilAssocie', write_only=False, read_only=False) classes_principal = ClasseSerializer(many=True, read_only=True) + profilAssocie = ProfilSerializer(read_only=True) + DroitLabel = serializers.SerializerMethodField() + DroitValue = serializers.SerializerMethodField() class Meta: model = Enseignant - fields = ['id', 'nom', 'prenom', 'mail', 'specialite', 'specialite_id', 'classes_principal'] - + fields = ['id', 'nom', 'prenom', 'mail', 'specialite', 'specialite_id', 'classes_principal', 'profilAssocie', 'profilAssocie_id', 'DroitLabel', 'DroitValue'] + def create(self, validated_data): specialite = validated_data.pop('specialite', None) + profilAssocie = validated_data.pop('profilAssocie', None) enseignant = Enseignant.objects.create(**validated_data) - enseignant.specialite = specialite + if specialite: + enseignant.specialite = specialite + if profilAssocie: + enseignant.profilAssocie = profilAssocie enseignant.save() return enseignant + def get_DroitLabel(self, obj): + return obj.profilAssocie.get_droit_display() if obj.profilAssocie else None + + def get_DroitValue(self, obj): + return obj.profilAssocie.droit if obj.profilAssocie else None + class EnseignantDetailSerializer(serializers.ModelSerializer): specialite = SpecialiteSerializer(read_only=True) diff --git a/Back-End/GestionEnseignants/views.py b/Back-End/GestionEnseignants/views.py index 7007523..4e4054e 100644 --- a/Back-End/GestionEnseignants/views.py +++ b/Back-End/GestionEnseignants/views.py @@ -47,10 +47,15 @@ class EnseignantView(APIView): def delete(self, request, _id): enseignant = bdd.getObject(_objectName=Enseignant, _columnName='id', _value=_id) - if enseignant != None: + if enseignant is not None: + if enseignant.profilAssocie: + print('Suppression du profil associé') + enseignant.profilAssocie.delete() enseignant.delete() - - return JsonResponse("La suppression de la spécialité a été effectuée avec succès", safe=False) + return JsonResponse({'message': 'La suppression de l\'enseignant a été effectuée avec succès'}, safe=False) + else: + return JsonResponse({'erreur': 'L\'enseignant n\'a pas été trouvé'}, safe=False) + @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') @@ -175,6 +180,10 @@ class ClasseView(APIView): def delete(self, request, _id): classe = bdd.getObject(_objectName=Classe, _columnName='id', _value=_id) if classe != None: + for eleve in classe.eleves.all(): + print(f'eleve a retirer la classe : {eleve}') + eleve.classeAssociee = None + eleve.save() classe.delete() return JsonResponse("La suppression de la classe a été effectuée avec succès", safe=False) \ No newline at end of file diff --git a/Back-End/GestionInscriptions/models.py b/Back-End/GestionInscriptions/models.py index 67bd194..4404b81 100644 --- a/Back-End/GestionInscriptions/models.py +++ b/Back-End/GestionInscriptions/models.py @@ -4,6 +4,27 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from GestionLogin.models import Profil +from GestionEnseignants.models import Classe + +from datetime import datetime + +class FraisInscription(models.Model): + class OptionsPaiements(models.IntegerChoices): + PAIEMENT_1_FOIS = 0, _('Paiement en une seule fois') + PAIEMENT_MENSUEL = 1, _('Paiement mensuel') + PAIEMENT_TRIMESTRIEL = 2, _('Paiement trimestriel') + + nom = models.CharField(max_length=255, unique=True) + description = models.TextField(blank=True) + montant_de_base = models.DecimalField(max_digits=10, decimal_places=2) + reductions = models.JSONField(blank=True, null=True) + supplements = models.JSONField(blank=True, null=True) + date_debut_validite = models.DateField() + date_fin_validite = models.DateField() + options_paiement = models.IntegerField(choices=OptionsPaiements, default=OptionsPaiements.PAIEMENT_1_FOIS) + + def __str__(self): + return self.nom class Langue(models.Model): id = models.AutoField(primary_key=True) @@ -59,7 +80,7 @@ class Eleve(models.Model): niveau = models.IntegerField(choices=NiveauEleve, default=NiveauEleve.NONE, blank=True) nationalite = models.CharField(max_length=200, default="", blank=True) adresse = models.CharField(max_length=200, default="", blank=True) - dateNaissance = models.CharField(max_length=200, default="", blank=True) + dateNaissance = models.DateField(null=True, blank=True) lieuNaissance = models.CharField(max_length=200, default="", blank=True) codePostalNaissance = models.IntegerField(default=0, blank=True) medecinTraitant = models.CharField(max_length=200, default="", blank=True) @@ -77,6 +98,9 @@ class Eleve(models.Model): # Relation N-N languesParlees = models.ManyToManyField(Langue, blank=True) + # Relation 1-N + classeAssociee = models.ForeignKey(Classe, on_delete=models.SET_NULL, null=True, blank=True, related_name='eleves') + def __str__(self): return self.nom + "_" + self.prenom @@ -98,6 +122,31 @@ class Eleve(models.Model): def getNbFreres(self): return self.freres.count() + @property + def age(self): + if self.dateNaissance: + today = datetime.today() + years = today.year - self.dateNaissance.year + months = today.month - self.dateNaissance.month + if today.day < self.dateNaissance.day: + months -= 1 + if months < 0: + years -= 1 + months += 12 + + # Déterminer le format de l'âge + if months >= 6 and months <= 12: + return f"{years} ans 1/2" + else: + return f"{years} ans" + return None + + @property + def dateNaissance_formattee(self): + if self.dateNaissance: + return self.dateNaissance.strftime('%d-%m-%Y') + return None + class FicheInscription(models.Model): class EtatDossierInscription(models.IntegerChoices): diff --git a/Back-End/GestionInscriptions/serializers.py b/Back-End/GestionInscriptions/serializers.py index fbcd6e4..f3954db 100644 --- a/Back-End/GestionInscriptions/serializers.py +++ b/Back-End/GestionInscriptions/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from GestionInscriptions.models import FicheInscription, Eleve, Responsable, Frere, Langue +from GestionInscriptions.models import FicheInscription, Eleve, Responsable, Frere, Langue, FraisInscription +from GestionEnseignants.models import Classe from GestionLogin.models import Profil from GestionLogin.serializers import ProfilSerializer from GestionMessagerie.models import Messagerie @@ -7,6 +8,13 @@ from GestionNotification.models import Notification from N3wtSchool import settings from django.utils import timezone import pytz +from datetime import datetime + +class FraisInscriptionSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + class Meta: + model = FraisInscription + fields = '__all__' class LanguesSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) @@ -34,7 +42,13 @@ class EleveSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) responsables = ResponsableSerializer(many=True, required=False) freres = FrereSerializer(many=True, required=False) - langues = LanguesSerializer(many=True, required=False) + langues = LanguesSerializer(many=True, required=False) + classeAssocie_id = serializers.PrimaryKeyRelatedField(queryset=Classe.objects.all(), source='classeAssociee', required=False, write_only=False, read_only=False) + age = serializers.SerializerMethodField() + dateNaissance_formattee = serializers.SerializerMethodField() + dateNaissance = serializers.DateField(input_formats=['%d-%m-%Y', '%Y-%m-%d'], required=False, allow_null=True) + classeAssocieeName = serializers.SerializerMethodField() + class Meta: model = Eleve fields = '__all__' @@ -89,6 +103,15 @@ class EleveSerializer(serializers.ModelSerializer): return instance + def get_age(self, obj): + return obj.age + + def get_dateNaissance_formattee(self, obj): + return obj.dateNaissance_formattee + + def get_classeAssocieeName(self, obj): + return obj.classeAssociee.nom_ambiance if obj.classeAssociee else None + class FicheInscriptionSerializer(serializers.ModelSerializer): eleve = EleveSerializer(many=False, required=True) fichierInscription = serializers.FileField(required=False) diff --git a/Back-End/GestionInscriptions/urls.py b/Back-End/GestionInscriptions/urls.py index 4fd699e..1b09589 100644 --- a/Back-End/GestionInscriptions/urls.py +++ b/Back-End/GestionInscriptions/urls.py @@ -1,7 +1,7 @@ from django.urls import path, re_path from . import views -from GestionInscriptions.views import ListFichesInscriptionView, FicheInscriptionView, EleveView, ResponsableView, ListeEnfantsView, ListeElevesView +from GestionInscriptions.views import ListFichesInscriptionView, FicheInscriptionView, EleveView, ResponsableView, ListeEnfantsView, ListeElevesView, FraisInscriptionView urlpatterns = [ re_path(r'^fichesInscription/([a-zA-z]+)$', ListFichesInscriptionView.as_view(), name="listefichesInscriptions"), @@ -26,6 +26,9 @@ urlpatterns = [ # Page PARENT - Liste des enfants re_path(r'^enfants/([0-9]+)$', ListeEnfantsView.as_view(), name="enfants"), - # Page INSCRIPTION - Liste des élèves + # Page INSCRIPTION - Liste des élèves re_path(r'^eleves$', ListeElevesView.as_view(), name="enfants"), + + # Frais d'inscription + re_path(r'^tarifsInscription$', FraisInscriptionView.as_view(), name="fraisInscription"), ] \ No newline at end of file diff --git a/Back-End/GestionInscriptions/util.py b/Back-End/GestionInscriptions/util.py index 45be230..9dd556c 100644 --- a/Back-End/GestionInscriptions/util.py +++ b/Back-End/GestionInscriptions/util.py @@ -179,3 +179,23 @@ def getArgFromRequest(_argument, _request): data=JSONParser().parse(_request) resultat = data[_argument] return resultat + +def diToPDF(ficheEleve): + # Ajout du fichier d'inscriptions + data = { + 'pdf_title': "Dossier d'inscription de %s"%ficheEleve.eleve.prenom, + 'dateSignature': convertToStr(_now(), '%d-%m-%Y'), + 'heureSignature': convertToStr(_now(), '%H:%M'), + 'eleve':ficheEleve.eleve, + } + + pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) + + nomFichierPDF = "Dossier_Inscription_%s_%s.pdf"%(ficheEleve.eleve.nom, ficheEleve.eleve.prenom) + pathFichier = Path(settings.DOCUMENT_DIR + "/" + nomFichierPDF) + if os.path.exists(str(pathFichier)): + print(f'File exists : {str(pathFichier)}') + os.remove(str(pathFichier)) + + receipt_file = BytesIO(pdf.content) + ficheEleve.fichierInscription = File(receipt_file, nomFichierPDF) \ No newline at end of file diff --git a/Back-End/GestionInscriptions/views.py b/Back-End/GestionInscriptions/views.py index 410edac..4007bcc 100644 --- a/Back-End/GestionInscriptions/views.py +++ b/Back-End/GestionInscriptions/views.py @@ -17,10 +17,10 @@ from io import BytesIO import GestionInscriptions.mailManager as mailer import GestionInscriptions.util as util -from GestionInscriptions.serializers import FicheInscriptionSerializer, EleveSerializer, FicheInscriptionByParentSerializer, EleveByDICreationSerializer +from GestionInscriptions.serializers import FicheInscriptionSerializer, EleveSerializer, FicheInscriptionByParentSerializer, EleveByDICreationSerializer, FraisInscriptionSerializer from GestionInscriptions.pagination import CustomPagination from GestionInscriptions.signals import clear_cache -from .models import Eleve, Responsable, FicheInscription +from .models import Eleve, Responsable, FicheInscription, FraisInscription from GestionInscriptions.automate import Automate_DI_Inscription, load_config, getStateMachineObjectState, updateStateMachine from GestionLogin.models import Profil @@ -30,85 +30,57 @@ from N3wtSchool import settings, renderers, bdd class ListFichesInscriptionView(APIView): pagination_class = CustomPagination + def get_fiche_inscriptions(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 = [FicheInscription.EtatDossierInscription.DI_VALIDE, FicheInscription.EtatDossierInscription.DI_ARCHIVE] + return bdd.searchObjects(FicheInscription, search, _excludeStates=exclude_states) + elif _filter == 'archived': + return bdd.getObjects(FicheInscription, 'etat', FicheInscription.EtatDossierInscription.DI_ARCHIVE) + elif _filter == 'subscribed': + return bdd.getObjects(FicheInscription, 'etat', FicheInscription.EtatDossierInscription.DI_VALIDE) + return None + def get(self, request, _filter): - if _filter == 'all': - # 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 + # Récupération des paramètres + search = request.GET.get('search', '').strip() + page_size = request.GET.get('page_size', None) - cached_page_size = cache.get('N3WT_page_size') - if cached_page_size != page_size: - clear_cache() - cache.set('N3WT_page_size', page_size) + # 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) - # Gestion du cache - page_number = request.GET.get('page', 1) - cache_key = f'N3WT_ficheInscriptions_page_{page_number}_search_{search}' - 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 + ficheInscriptions_List = self.get_fiche_inscriptions(_filter, search) - # Filtrage des résultats - if search: - # Utiliser la nouvelle fonction de recherche - ficheInscriptions_List = bdd.searchObjects( - FicheInscription, - search, - _excludeState=6 # Exclure les fiches archivées - ) - else: - # Récupère toutes les fiches non archivées - ficheInscriptions_List = bdd.getObjects(FicheInscription, 'etat', 6, _reverseCondition=True) + if not ficheInscriptions_List: + return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) - # Pagination - paginator = self.pagination_class() - page = paginator.paginate_queryset(ficheInscriptions_List, request) - if page is not None: - ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True) - response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data) - cache.set(cache_key, response_data, timeout=60*15) - return JsonResponse(response_data, safe=False) + # Pagination + paginator = self.pagination_class() + page = paginator.paginate_queryset(ficheInscriptions_List, request) + if page is not None: + ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True) + response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data) + cache.set(cache_key, response_data, timeout=60*15) + return JsonResponse(response_data, safe=False) - elif _filter == 'archived' : - page_size = request.GET.get('page_size', None) - if page_size is not None: - try: - page_size = int(page_size) - except ValueError: - page_size = settings.NB_RESULT_PER_PAGE - - cached_page_size = cache.get('N3WT_archived_page_size') - - # Comparer avec le nouveau page_size - if cached_page_size != page_size: - # Appeler cached_page() et mettre à jour le cache - clear_cache() - cache.set('N3WT_archived_page_size',page_size) - - page_number = request.GET.get('page', 1) - cache_key_page = f'N3WT_ficheInscriptions_archives_page_{page_number}' - cached_page = cache.get(cache_key_page) - if cached_page: - return JsonResponse(cached_page, safe=False) - - ficheInscriptions_List=bdd.getObjects(FicheInscription, 'etat', 6) - paginator = self.pagination_class() - page = paginator.paginate_queryset(ficheInscriptions_List, request) - if page is not None: - ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True) - response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data) - cache.set(cache_key_page, response_data, timeout=60*15) - - return JsonResponse(response_data, safe=False) - - return JsonResponse(status=status.HTTP_404_NOT_FOUND) + return JsonResponse({'error' : 'aucune donnée trouvée', 'count' :0}, safe=False) def post(self, request): fichesEleve_data=JSONParser().parse(request) @@ -168,39 +140,28 @@ class FicheInscriptionView(APIView): def put(self, request, id): ficheEleve_data=JSONParser().parse(request) - admin = ficheEleve_data.pop('admin', 1) + etat = ficheEleve_data.pop('etat', 0) ficheEleve_data["dateMAJ"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M')) ficheEleve = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id) - currentState = getStateMachineObjectState(ficheEleve.etat) - if admin == 0 and currentState == FicheInscription.EtatDossierInscription.DI_ENVOYE: + + if etat == FicheInscription.EtatDossierInscription.DI_EN_VALIDATION: + # Le parent a complété le dossier d'inscription, il est soumis à validation par l'école + print('EN VALIDATION') json.dumps(ficheEleve_data) - - # Ajout du fichier d'inscriptions - data = { - 'pdf_title': "Dossier d'inscription de %s"%ficheEleve.eleve.prenom, - 'dateSignature': util.convertToStr(util._now(), '%d-%m-%Y'), - 'heureSignature': util.convertToStr(util._now(), '%H:%M'), - 'eleve':ficheEleve.eleve, - } - - pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) - - nomFichierPDF = "Dossier_Inscription_%s_%s.pdf"%(ficheEleve.eleve.nom, ficheEleve.eleve.prenom) - pathFichier = Path(settings.DOCUMENT_DIR + "/" + nomFichierPDF) - if os.path.exists(str(pathFichier)): - print(f'File exists : {str(pathFichier)}') - os.remove(str(pathFichier)) - - receipt_file = BytesIO(pdf.content) - ficheEleve.fichierInscription = File(receipt_file, nomFichierPDF) - + util.diToPDF(ficheEleve) # Mise à jour de l'automate - updateStateMachine(di, 'saisiDI') + updateStateMachine(ficheEleve, 'saisiDI') + elif etat == FicheInscription.EtatDossierInscription.DI_VALIDE: + # L'école a validé le dossier d'inscription + # Mise à jour de l'automate + print('VALIDATION') + updateStateMachine(ficheEleve, 'valideDI') + ficheEleve_serializer = FicheInscriptionSerializer(ficheEleve, data=ficheEleve_data) if ficheEleve_serializer.is_valid(): - di = ficheEleve_serializer.save() - return JsonResponse("Updated Successfully", safe=False) + ficheEleve_serializer.save() + return JsonResponse(ficheEleve_serializer.data, safe=False) return JsonResponse(ficheEleve_serializer.errors, safe=False) @@ -287,3 +248,10 @@ class ListeElevesView(APIView): students = bdd.getAllObjects(_objectName=Eleve) students_serializer = EleveByDICreationSerializer(students, many=True) return JsonResponse(students_serializer.data, safe=False) + +# API utilisée pour la vue de personnalisation des frais d'inscription pour la structure +class FraisInscriptionView(APIView): + def get(self, request): + tarifs = bdd.getAllObjects(FraisInscription) + tarifs_serializer = FraisInscriptionSerializer(tarifs, many=True) + return JsonResponse(tarifs_serializer.data, safe=False) diff --git a/Back-End/GestionLogin/models.py b/Back-End/GestionLogin/models.py index f16dcbb..cff77da 100644 --- a/Back-End/GestionLogin/models.py +++ b/Back-End/GestionLogin/models.py @@ -5,10 +5,10 @@ from django.core.validators import EmailValidator class Profil(AbstractUser): class Droits(models.IntegerChoices): - PROFIL_UNDEFINED = -1, _('Profil non défini') - PROFIL_ECOLE = 0, _('Profil école') - PROFIL_PARENT = 1, _('Profil parent') - PROFIL_ADMIN = 2, _('Profil administrateur') + PROFIL_UNDEFINED = -1, _('NON DEFINI') + PROFIL_ECOLE = 0, _('ECOLE') + PROFIL_PARENT = 1, _('PARENT') + PROFIL_ADMIN = 2, _('ADMIN') email = models.EmailField(max_length=255, unique=True, default="", validators=[EmailValidator()]) diff --git a/Back-End/GestionLogin/serializers.py b/Back-End/GestionLogin/serializers.py index e5653de..7281e41 100644 --- a/Back-End/GestionLogin/serializers.py +++ b/Back-End/GestionLogin/serializers.py @@ -26,3 +26,26 @@ class ProfilSerializer(serializers.ModelSerializer): ret = super().to_representation(instance) ret['password'] = '********' return ret + +class ProfilUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = Profil + fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active'] + extra_kwargs = { + 'password': {'write_only': True, 'required': False} + } + + def update(self, instance, validated_data): + password = validated_data.pop('password', None) + instance = super().update(instance, validated_data) + + if password: + instance.set_password(password) + instance.save() + + return instance + + def to_representation(self, instance): + ret = super().to_representation(instance) + ret['password'] = '********' + return ret diff --git a/Back-End/GestionLogin/views.py b/Back-End/GestionLogin/views.py index 76590cd..4fb0902 100644 --- a/Back-End/GestionLogin/views.py +++ b/Back-End/GestionLogin/views.py @@ -17,8 +17,8 @@ import json from . import validator from .models import Profil +from GestionLogin.serializers import ProfilSerializer, ProfilUpdateSerializer from GestionInscriptions.models import FicheInscription -from GestionInscriptions.serializers import ProfilSerializer from GestionInscriptions.signals import clear_cache import GestionInscriptions.mailManager as mailer import GestionInscriptions.util as util @@ -83,7 +83,7 @@ class ProfilView(APIView): def put(self, request, _id): data=JSONParser().parse(request) profil = Profil.objects.get(id=_id) - profil_serializer = ProfilSerializer(profil, data=data) + profil_serializer = ProfilUpdateSerializer(profil, data=data) if profil_serializer.is_valid(): profil_serializer.save() return JsonResponse("Updated Successfully", safe=False) @@ -143,6 +143,7 @@ class LoginView(APIView): 'errorFields':errorFields, 'errorMessage':retour, 'profil':user.id if user else -1, + 'droit':user.droit if user else -1, #'jwtToken':jwt_token if profil != -1 else '' }, safe=False) diff --git a/Back-End/N3wtSchool/bdd.py b/Back-End/N3wtSchool/bdd.py index ef24b59..f479358 100644 --- a/Back-End/N3wtSchool/bdd.py +++ b/Back-End/N3wtSchool/bdd.py @@ -56,19 +56,19 @@ def getLastId(_object): logging.warning("Aucun résultat n'a été trouvé - ") return result -def searchObjects(_objectName, _searchTerm, _excludeState=None): +def searchObjects(_objectName, _searchTerm=None, _excludeStates=None): """ Recherche générique sur les objets avec possibilité d'exclure certains états _objectName: Classe du modèle _searchTerm: Terme de recherche - _excludeState: État à exclure de la recherche (optionnel) + _excludeStates: Liste d'état à exclure de la recherche (optionnel) """ try: query = _objectName.objects.all() # Si on a un état à exclure - if _excludeState is not None: - query = query.filter(etat__lt=_excludeState) + if _excludeStates is not None: + query = query.exclude(etat__in=_excludeStates) # Si on a un terme de recherche if _searchTerm and _searchTerm.strip(): @@ -83,4 +83,4 @@ def searchObjects(_objectName, _searchTerm, _excludeState=None): except _objectName.DoesNotExist: logging.error(f"Aucun résultat n'a été trouvé - {_objectName.__name__} (recherche: {_searchTerm})") - return None \ No newline at end of file + return None diff --git a/Front-End/messages/en/students.json b/Front-End/messages/en/students.json index a44b858..8bfa08b 100644 --- a/Front-End/messages/en/students.json +++ b/Front-End/messages/en/students.json @@ -3,6 +3,7 @@ "addStudent": "New", "allStudents": "All Students", "pending": "Pending Registrations", + "subscribed": "Subscribed", "archived": "Archived", "name": "Name", "class": "Class", @@ -25,6 +26,7 @@ "mainContactMail":"Main contact email", "phone":"Phone", "lastUpdateDate":"Last update", + "classe":"Class", "registrationFileStatus":"Registration file status", "files":"Files" } \ No newline at end of file diff --git a/Front-End/messages/fr/students.json b/Front-End/messages/fr/students.json index 302e57d..0b0d6df 100644 --- a/Front-End/messages/fr/students.json +++ b/Front-End/messages/fr/students.json @@ -3,6 +3,7 @@ "addStudent": "Nouveau", "allStudents": "Tous les élèves", "pending": "Inscriptions en attente", + "subscribed": "Inscrits", "archived": "Archivés", "name": "Nom", "class": "Classe", @@ -25,6 +26,7 @@ "mainContactMail":"Email de contact principal", "phone":"Téléphone", "lastUpdateDate":"Dernière mise à jour", + "classe":"Classe", "registrationFileStatus":"État du dossier d'inscription", "files":"Fichiers" } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 0a113ae..0a7e5c0 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -4,6 +4,8 @@ import React, { useState, useEffect } from 'react'; import { useTranslations } from 'next-intl'; import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'lucide-react'; import Loader from '@/components/Loader'; +import { BK_GESTIONINSCRIPTION_CLASSES_URL } from '@/utils/Url'; +import ClasseDetails from '@/components/ClasseDetails'; // Composant StatCard pour afficher une statistique const StatCard = ({ title, value, icon, change, color = "blue" }) => ( @@ -54,7 +56,23 @@ export default function DashboardPage() { } }); + const [classes, setClasses] = useState([]); + + const fetchClasses = () => { + fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) + .then(response => response.json()) + .then(data => { + setClasses(data); + }) + .catch(error => { + console.error('Error fetching classes:', error); + }); + }; + useEffect(() => { + // Fetch data for classes + fetchClasses(); + // Simulation de chargement des données setTimeout(() => { setStats({ @@ -120,7 +138,7 @@ export default function DashboardPage() { {/* Événements et KPIs */} -
+
{/* Graphique des inscriptions */}

{t('inscriptionTrends')}

@@ -138,6 +156,14 @@ export default function DashboardPage() { ))}
+ +
+ {classes.map((classe) => ( +
+ +
+ ))} +
); } \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index f8ded13..c72a74f 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,10 +1,8 @@ 'use client' import React, { useState, useEffect } from 'react'; -import Table from '@/components/Table'; import SpecialitiesSection from '@/components/SpecialitiesSection' import ClassesSection from '@/components/ClassesSection' import TeachersSection from '@/components/TeachersSection'; -import { User, School } from 'lucide-react' import { BK_GESTIONINSCRIPTION_SPECIALITES_URL, BK_GESTIONINSCRIPTION_CLASSES_URL, BK_GESTIONINSCRIPTION_SPECIALITE_URL, @@ -66,7 +64,6 @@ export default function Page() { }; const handleCreate = (url, newData, setDatas) => { - console.log('SEND POST :', JSON.stringify(newData)); fetch(url, { method: 'POST', headers: { diff --git a/Front-End/src/app/[locale]/admin/students/page.js b/Front-End/src/app/[locale]/admin/students/page.js index 9c07579..7a5fdf0 100644 --- a/Front-End/src/app/[locale]/admin/students/page.js +++ b/Front-End/src/app/[locale]/admin/students/page.js @@ -13,19 +13,29 @@ import Button from '@/components/Button'; import DropdownMenu from "@/components/DropdownMenu"; import { swapFormatDate } from '@/utils/Date'; import { formatPhoneNumber } from '@/utils/Telephone'; -import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus } from 'lucide-react'; +import { MoreVertical, Send, Edit, Trash2, FileText, ChevronUp, UserPlus, CheckCircle } from 'lucide-react'; import Modal from '@/components/Modal'; import InscriptionForm from '@/components/Inscription/InscriptionForm' +import AffectationClasseForm from '@/components/AffectationClasseForm' -import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, BK_GESTIONINSCRIPTION_SEND_URL, FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, BK_GESTIONINSCRIPTION_ARCHIVE_URL } from '@/utils/Url'; +import { BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL, + BK_GESTIONINSCRIPTION_SEND_URL, + FR_ADMIN_STUDENT_EDIT_SUBSCRIBE, + BK_GESTIONINSCRIPTION_ARCHIVE_URL, + BK_GESTIONINSCRIPTION_CLASSES_URL, + BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL } from '@/utils/Url'; + +import DjangoCSRFToken from '@/components/DjangoCSRFToken' +import useCsrfToken from '@/hooks/useCsrfToken'; const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true'; export default function Page({ params: { locale } }) { const t = useTranslations('students'); const [ficheInscriptions, setFicheInscriptions] = useState([]); - const [ficheInscriptionsData, setFicheInscriptionsData] = useState([]); - const [fichesInscriptionsDataArchivees, setFicheInscriptionsDataArchivees] = useState([]); + const [fichesInscriptionsDataEnCours, setFichesInscriptionsDataEnCours] = useState([]); + const [fichesInscriptionsDataInscrits, setichesInscriptionsDataInscrits] = useState([]); + const [fichesInscriptionsDataArchivees, setFichesInscriptionsDataArchivees] = useState([]); // const [filter, setFilter] = useState('*'); const [searchTerm, setSearchTerm] = useState(''); const [alertPage, setAlertPage] = useState(false); @@ -33,22 +43,32 @@ export default function Page({ params: { locale } }) { const [ficheArchivee, setFicheArchivee] = useState(false); const [isLoading, setIsLoading] = useState(true); const [popup, setPopup] = useState({ visible: false, message: '', onConfirm: null }); - const [activeTab, setActiveTab] = useState('all'); + const [activeTab, setActiveTab] = useState('pending'); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); - const [totalStudents, setTotalStudents] = useState(0); + const [totalPending, setTotalPending] = useState(0); + const [totalSubscribed, setTotalSubscribed] = useState(0); const [totalArchives, setTotalArchives] = useState(0); const [itemsPerPage, setItemsPerPage] = useState(5); // Définir le nombre d'éléments par page const [isOpen, setIsOpen] = useState(false); + const [isOpenAffectationClasse, setIsOpenAffectationClasse] = useState(false); + const [eleve, setEleve] = useState(''); + const [classes, setClasses] = useState([]); + + const csrfToken = useCsrfToken(); const openModal = () => { setIsOpen(true); } + const openModalAssociationEleve = (eleveSelected) => { + setIsOpenAffectationClasse(true); + setEleve(eleveSelected); + } // Modifier la fonction fetchData pour inclure le terme de recherche const fetchData = (page, pageSize, search = '') => { - const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/all?page=${page}&page_size=${pageSize}&search=${search}`; + const url = `${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/pending?page=${page}&page_size=${pageSize}&search=${search}`; fetch(url, { headers: { 'Content-Type': 'application/json', @@ -58,11 +78,33 @@ export default function Page({ params: { locale } }) { setIsLoading(false); if (data) { const { fichesInscriptions, count } = data; - setFicheInscriptionsData(fichesInscriptions); + setFichesInscriptionsDataEnCours(fichesInscriptions); const calculatedTotalPages = Math.ceil(count / pageSize); - setTotalStudents(count); + setTotalPending(count); setTotalPages(calculatedTotalPages); } + console.log('Success PENDING:', data); + }) + .catch(error => { + console.error('Error fetching data:', error); + setIsLoading(false); + }); + }; + + const fetchDataSubscribed = () => { + fetch(`${BK_GESTIONINSCRIPTION_FICHESINSCRIPTION_URL}/subscribed`, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(response => response.json()) + .then(data => { + setIsLoading(false); + if (data) { + const { fichesInscriptions, count } = data; + setTotalSubscribed(count); + setichesInscriptionsDataInscrits(fichesInscriptions); + } + console.log('Success SUBSCRIBED:', data); }) .catch(error => { console.error('Error fetching data:', error); @@ -81,7 +123,7 @@ export default function Page({ params: { locale } }) { if (data) { const { fichesInscriptions, count } = data; setTotalArchives(count); - setFicheInscriptionsDataArchivees(fichesInscriptions); + setFichesInscriptionsDataArchivees(fichesInscriptions); } console.log('Success ARCHIVED:', data); }) @@ -91,14 +133,31 @@ export default function Page({ params: { locale } }) { }); }; + const fetchClasses = () => { + fetch(`${BK_GESTIONINSCRIPTION_CLASSES_URL}`) + .then(response => response.json()) + .then(data => { + setClasses(data); + console.log("classes : ", data) + }) + .catch(error => { + console.error('Error fetching classes:', error); + }); + }; + + useEffect(() => { + fetchClasses(); + }, []); + useEffect(() => { const fetchDataAndSetState = () => { if (!useFakeData) { fetchData(currentPage, itemsPerPage, searchTerm); + fetchDataSubscribed(); fetchDataArchived(); } else { setTimeout(() => { - setFicheInscriptionsData(mockFicheInscription); + setFichesInscriptionsDataEnCours(mockFicheInscription); setIsLoading(false); }, 1000); } @@ -183,13 +242,37 @@ export default function Page({ params: { locale } }) { fetchData(newPage, itemsPerPage); // Appeler fetchData directement ici }; + const validateAndAssociate = (updatedData) => { + fetch(`${BK_GESTIONINSCRIPTION_FICHEINSCRIPTION_URL}/${eleve.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(updatedData), + credentials: 'include' + }) + .then(response => response.json()) + .then(data => { + console.log('Succès :', data); + }) + .catch(error => { + console.error('Erreur :', error); + }); + } + const columns = [ { name: t('studentName'), transform: (row) => row.eleve.nom }, { name: t('studentFistName'), transform: (row) => row.eleve.prenom }, { name: t('mainContactMail'), transform: (row) => row.eleve.responsables[0].mail }, { name: t('phone'), transform: (row) => formatPhoneNumber(row.eleve.responsables[0].telephone) }, - { name: t('lastUpdateDate'), transform: (row) => swapFormatDate(row.dateMAJ, "DD-MM-YYYY hh:mm:ss", "DD/MM/YYYY hh:mm") }, - { name: t('registrationFileStatus'), transform: (row) => updateStatusAction(row.eleve.id, newStatus)} /> }, + { name: t('lastUpdateDate'), transform: (row) => row.dateMAJ_formattee}, + { name: t('registrationFileStatus'), transform: (row) => ( +
+ updateStatusAction(row.eleve.id, newStatus)} showDropdown={false} /> +
+ ) + }, { name: t('files'), transform: (row) => (