diff --git a/Back-End/Dockerfile b/Back-End/Dockerfile index 7f46014..afa5421 100644 --- a/Back-End/Dockerfile +++ b/Back-End/Dockerfile @@ -7,6 +7,7 @@ FROM python:3.12.7 # Allows docker to cache installed dependencies between builds COPY requirements.txt requirements.txt RUN pip install -r requirements.txt +RUN pip install pymupdf # Mounts the application code to the image COPY . . diff --git a/Back-End/N3wtSchool/bdd.py b/Back-End/N3wtSchool/bdd.py index 1fe62ef..6c4b58b 100644 --- a/Back-End/N3wtSchool/bdd.py +++ b/Back-End/N3wtSchool/bdd.py @@ -92,6 +92,7 @@ def searchObjects(_objectName, _searchTerm=None, _excludeStates=None): def delete_object(model_class, object_id, related_field=None): try: obj = model_class.objects.get(id=object_id) + if related_field and hasattr(obj, related_field): related_obj = getattr(obj, related_field) if related_obj: @@ -103,5 +104,3 @@ def delete_object(model_class, object_id, related_field=None): return JsonResponse({'error': f'L\'objet {model_class.__name__} n\'existe pas avec cet ID'}, status=404, safe=False) except Exception as e: return JsonResponse({'error': f'Une erreur est survenue : {str(e)}'}, status=500, safe=False) - - diff --git a/Back-End/School/models.py b/Back-End/School/models.py index 31d2a80..a992eac 100644 --- a/Back-End/School/models.py +++ b/Back-End/School/models.py @@ -3,6 +3,9 @@ from Auth.models import Profile from django.db.models import JSONField from django.dispatch import receiver from django.contrib.postgres.fields import ArrayField +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError + LEVEL_CHOICES = [ (1, 'Très Petite Section (TPS)'), @@ -47,7 +50,7 @@ class SchoolClass(models.Model): number_of_students = models.PositiveIntegerField(blank=True) teaching_language = models.CharField(max_length=255, blank=True) school_year = models.CharField(max_length=9, blank=True) - updated_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) teachers = models.ManyToManyField(Teacher, blank=True) levels = ArrayField(models.IntegerField(choices=LEVEL_CHOICES), default=list) type = models.IntegerField(choices=PLANNING_TYPE_CHOICES, default=1) @@ -64,3 +67,56 @@ class Planning(models.Model): def __str__(self): return f'Planning for {self.level} of {self.school_class.atmosphere_name}' + +class Discount(models.Model): + name = models.CharField(max_length=255, unique=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class Fee(models.Model): + name = models.CharField(max_length=255, unique=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class TuitionFee(models.Model): + class PaymentOptions(models.IntegerChoices): + SINGLE_PAYMENT = 0, _('Paiement en une seule fois') + FOUR_TIME_PAYMENT = 1, _('Paiement en 4 fois') + TEN_TIME_PAYMENT = 2, _('Paiement en 10 fois') + + name = models.CharField(max_length=255, unique=True) + description = models.TextField(blank=True) + base_amount = models.DecimalField(max_digits=10, decimal_places=2) + currency = models.CharField(max_length=3, default='EUR') + discounts = models.ManyToManyField('Discount', blank=True) + validity_start_date = models.DateField() + validity_end_date = models.DateField() + payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT) + is_active = models.BooleanField(default=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + def clean(self): + if self.validity_end_date <= self.validity_start_date: + raise ValidationError(_('La date de fin de validité doit être après la date de début de validité.')) + + def calculate_final_amount(self): + amount = self.base_amount + + # Apply fees (supplements and taxes) + # for fee in self.fees.all(): + # amount += fee.amount + + # Apply discounts + for discount in self.discounts.all(): + amount -= discount.amount + + return amount diff --git a/Back-End/School/serializers.py b/Back-End/School/serializers.py index d3bae13..2248861 100644 --- a/Back-End/School/serializers.py +++ b/Back-End/School/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES +from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, TuitionFee, Fee from Subscriptions.models import RegistrationForm from Subscriptions.serializers import StudentSerializer from Auth.serializers import ProfileSerializer @@ -172,4 +172,57 @@ class SchoolClassSerializer(serializers.ModelSerializer): utc_time = timezone.localtime(obj.updated_date) local_tz = pytz.timezone(settings.TZ_APPLI) local_time = utc_time.astimezone(local_tz) - return local_time.strftime("%d-%m-%Y %H:%M") \ No newline at end of file + return local_time.strftime("%d-%m-%Y %H:%M") + +class DiscountSerializer(serializers.ModelSerializer): + class Meta: + model = Discount + fields = '__all__' + +class FeeSerializer(serializers.ModelSerializer): + class Meta: + model = Fee + fields = '__all__' + +class TuitionFeeSerializer(serializers.ModelSerializer): + discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True) + final_amount = serializers.SerializerMethodField() + + class Meta: + model = TuitionFee + fields = '__all__' + + def get_final_amount(self, obj): + return obj.calculate_final_amount() + + def create(self, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Create the TuitionFee instance + tuition_fee = TuitionFee.objects.create(**validated_data) + + # Add discounts if provided + for discount in discounts_data: + tuition_fee.discounts.add(discount) + + return tuition_fee + + def update(self, instance, validated_data): + discounts_data = validated_data.pop('discounts', []) + + # Update the TuitionFee instance + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.base_amount = validated_data.get('base_amount', instance.base_amount) + instance.currency = validated_data.get('currency', instance.currency) + instance.validity_start_date = validated_data.get('validity_start_date', instance.validity_start_date) + instance.validity_end_date = validated_data.get('validity_end_date', instance.validity_end_date) + instance.payment_option = validated_data.get('payment_option', instance.payment_option) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.save() + + # Update discounts if provided + if discounts_data: + instance.discounts.set(discounts_data) + + return instance \ No newline at end of file diff --git a/Back-End/School/urls.py b/Back-End/School/urls.py index 77897c3..4ccae46 100644 --- a/Back-End/School/urls.py +++ b/Back-End/School/urls.py @@ -1,6 +1,21 @@ from django.urls import path, re_path -from School.views import TeachersView, TeacherView, SpecialitiesView, SpecialityView, ClassesView, ClasseView, PlanningsView, PlanningView +from School.views import ( + TeachersView, + TeacherView, + SpecialitiesView, + SpecialityView, + ClassesView, + ClasseView, + PlanningsView, + PlanningView, + FeesView, + FeeView, + TuitionFeesView, + TuitionFeeView, + DiscountsView, + DiscountView, +) urlpatterns = [ re_path(r'^specialities$', SpecialitiesView.as_view(), name="specialities"), @@ -18,4 +33,16 @@ urlpatterns = [ re_path(r'^plannings$', PlanningsView.as_view(), name="plannings"), re_path(r'^planning$', PlanningView.as_view(), name="planning"), re_path(r'^planning/([0-9]+)$', PlanningView.as_view(), name="planning"), + + re_path(r'^fees$', FeesView.as_view(), name="fees"), + re_path(r'^fee$', FeeView.as_view(), name="fee"), + re_path(r'^fee/([0-9]+)$', FeeView.as_view(), name="fee"), + + re_path(r'^tuitionFees$', TuitionFeesView.as_view(), name="tuitionFees"), + re_path(r'^tuitionFee$', TuitionFeeView.as_view(), name="tuitionFee"), + re_path(r'^tuitionFee/([0-9]+)$', TuitionFeeView.as_view(), name="tuitionFee"), + + re_path(r'^discounts$', DiscountsView.as_view(), name="discounts"), + re_path(r'^discount$', DiscountView.as_view(), name="discount"), + re_path(r'^discount/([0-9]+)$', DiscountView.as_view(), name="discount"), ] \ No newline at end of file diff --git a/Back-End/School/views.py b/Back-End/School/views.py index 7ba4984..e06e944 100644 --- a/Back-End/School/views.py +++ b/Back-End/School/views.py @@ -5,46 +5,41 @@ from rest_framework.parsers import JSONParser from rest_framework.views import APIView from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from .models import Teacher, Speciality, SchoolClass, Planning -from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer +from .models import Teacher, Speciality, SchoolClass, Planning, Discount, TuitionFee, Fee +from .serializers import TeacherSerializer, SpecialitySerializer, SchoolClassSerializer, PlanningSerializer, DiscountSerializer, TuitionFeeSerializer, FeeSerializer from N3wtSchool import bdd +from N3wtSchool.bdd import delete_object, getAllObjects, getObject @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialitiesView(APIView): def get(self, request): - specialitiesList=bdd.getAllObjects(Speciality) - specialities_serializer=SpecialitySerializer(specialitiesList, many=True) - + specialitiesList = getAllObjects(Speciality) + specialities_serializer = SpecialitySerializer(specialitiesList, many=True) return JsonResponse(specialities_serializer.data, safe=False) def post(self, request): - specialities_data=JSONParser().parse(request) + specialities_data = JSONParser().parse(request) all_valid = True for speciality_data in specialities_data: speciality_serializer = SpecialitySerializer(data=speciality_data) - if speciality_serializer.is_valid(): speciality_serializer.save() else: all_valid = False break if all_valid: - specialitiesList = bdd.getAllObjects(Speciality) + specialitiesList = getAllObjects(Speciality) specialities_serializer = SpecialitySerializer(specialitiesList, many=True) - return JsonResponse(specialities_serializer.data, safe=False) - return JsonResponse(speciality_serializer.errors, safe=False) - @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class SpecialityView(APIView): - def get (self, request, _id): - speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) - speciality_serializer=SpecialitySerializer(speciality) - + def get(self, request, _id): + speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id) + speciality_serializer = SpecialitySerializer(speciality) return JsonResponse(speciality_serializer.data, safe=False) def post(self, request): @@ -59,7 +54,7 @@ class SpecialityView(APIView): def put(self, request, _id): speciality_data=JSONParser().parse(request) - speciality = bdd.getObject(_objectName=Speciality, _columnName='id', _value=_id) + speciality = getObject(_objectName=Speciality, _columnName='id', _value=_id) speciality_serializer = SpecialitySerializer(speciality, data=speciality_data) if speciality_serializer.is_valid(): speciality_serializer.save() @@ -68,11 +63,62 @@ class SpecialityView(APIView): return JsonResponse(speciality_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(Speciality, _id) + return delete_object(Speciality, _id) + +# Vues pour les réductions (Discount) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountsView(APIView): + def get(self, request): + discountsList = Discount.objects.all() + discounts_serializer = DiscountSerializer(discountsList, many=True) + return JsonResponse(discounts_serializer.data, safe=False) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class DiscountView(APIView): + def get(self, request, _id): + try: + discount = Discount.objects.get(id=_id) + discount_serializer = DiscountSerializer(discount) + return JsonResponse(discount_serializer.data, safe=False) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + discount_data = JSONParser().parse(request) + discount_serializer = DiscountSerializer(data=discount_data) + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False, status=201) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + discount_data = JSONParser().parse(request) + try: + discount = Discount.objects.get(id=_id) + except Discount.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + discount_serializer = DiscountSerializer(discount, data=discount_data, partial=True) # Utilisation de partial=True + if discount_serializer.is_valid(): + discount_serializer.save() + return JsonResponse(discount_serializer.data, safe=False) + return JsonResponse(discount_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Discount, _id) class TeachersView(APIView): def get(self, request): - teachersList=bdd.getAllObjects(Teacher) + teachersList=getAllObjects(Teacher) teachers_serializer=TeacherSerializer(teachersList, many=True) return JsonResponse(teachers_serializer.data, safe=False) @@ -81,7 +127,7 @@ class TeachersView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class TeacherView(APIView): def get (self, request, _id): - teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id) + teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id) teacher_serializer=TeacherSerializer(teacher) return JsonResponse(teacher_serializer.data, safe=False) @@ -99,7 +145,7 @@ class TeacherView(APIView): def put(self, request, _id): teacher_data=JSONParser().parse(request) - teacher = bdd.getObject(_objectName=Teacher, _columnName='id', _value=_id) + teacher = getObject(_objectName=Teacher, _columnName='id', _value=_id) teacher_serializer = TeacherSerializer(teacher, data=teacher_data) if teacher_serializer.is_valid(): teacher_serializer.save() @@ -108,13 +154,13 @@ class TeacherView(APIView): return JsonResponse(teacher_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(Teacher, _id, related_field='associated_profile') + return delete_object(Teacher, _id, related_field='associated_profile') @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class ClassesView(APIView): def get(self, request): - classesList=bdd.getAllObjects(SchoolClass) + classesList=getAllObjects(SchoolClass) classes_serializer=SchoolClassSerializer(classesList, many=True) return JsonResponse(classes_serializer.data, safe=False) @@ -131,7 +177,7 @@ class ClassesView(APIView): break if all_valid: - classesList = bdd.getAllObjects(SchoolClass) + classesList = getAllObjects(SchoolClass) classes_serializer = SchoolClassSerializer(classesList, many=True) return JsonResponse(classes_serializer.data, safe=False) @@ -142,7 +188,7 @@ class ClassesView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class ClasseView(APIView): def get (self, request, _id): - schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id) + schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id) classe_serializer=SchoolClassSerializer(schoolClass) return JsonResponse(classe_serializer.data, safe=False) @@ -159,7 +205,7 @@ class ClasseView(APIView): def put(self, request, _id): classe_data=JSONParser().parse(request) - schoolClass = bdd.getObject(_objectName=SchoolClass, _columnName='id', _value=_id) + schoolClass = getObject(_objectName=SchoolClass, _columnName='id', _value=_id) classe_serializer = SchoolClassSerializer(schoolClass, data=classe_data) if classe_serializer.is_valid(): classe_serializer.save() @@ -168,14 +214,14 @@ class ClasseView(APIView): return JsonResponse(classe_serializer.errors, safe=False) def delete(self, request, _id): - return bdd.delete_object(SchoolClass, _id) + return delete_object(SchoolClass, _id) @method_decorator(csrf_protect, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch') class PlanningsView(APIView): def get(self, request): - schedulesList=bdd.getAllObjects(Planning) + schedulesList=getAllObjects(Planning) schedules_serializer=PlanningSerializer(schedulesList, many=True) return JsonResponse(schedules_serializer.data, safe=False) @@ -183,7 +229,7 @@ class PlanningsView(APIView): @method_decorator(ensure_csrf_cookie, name='dispatch') class PlanningView(APIView): def get (self, request, _id): - planning = bdd.getObject(_objectName=Planning, _columnName='classe__id', _value=_id) + planning = getObject(_objectName=Planning, _columnName='classe__id', _value=_id) planning_serializer=PlanningSerializer(planning) return JsonResponse(planning_serializer.data, safe=False) @@ -215,3 +261,98 @@ class PlanningView(APIView): return JsonResponse(planning_serializer.data, safe=False) return JsonResponse(planning_serializer.errors, safe=False) + + +# Vues pour les frais (Fee) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class FeesView(APIView): + def get(self, request): + feesList = Fee.objects.all() + fees_serializer = FeeSerializer(feesList, many=True) + return JsonResponse(fees_serializer.data, safe=False) + + def post(self, request): + fee_data = JSONParser().parse(request) + fee_serializer = FeeSerializer(data=fee_data) + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False, status=201) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class FeeView(APIView): + def get(self, request, _id): + try: + fee = Fee.objects.get(id=_id) + fee_serializer = FeeSerializer(fee) + return JsonResponse(fee_serializer.data, safe=False) + except Fee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + fee_data = JSONParser().parse(request) + fee_serializer = FeeSerializer(data=fee_data) + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False, status=201) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + fee_data = JSONParser().parse(request) + try: + fee = Fee.objects.get(id=_id) + except Fee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + fee_serializer = FeeSerializer(fee, data=fee_data, partial=True) # Utilisation de partial=True + if fee_serializer.is_valid(): + fee_serializer.save() + return JsonResponse(fee_serializer.data, safe=False) + return JsonResponse(fee_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(Fee, _id) + +# Vues pour les frais de scolarité (TuitionFee) +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class TuitionFeesView(APIView): + def get(self, request): + tuitionFeesList = TuitionFee.objects.all() + tuitionFees_serializer = TuitionFeeSerializer(tuitionFeesList, many=True) + return JsonResponse(tuitionFees_serializer.data, safe=False) + +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(ensure_csrf_cookie, name='dispatch') +class TuitionFeeView(APIView): + def get(self, request, _id): + try: + tuitionFee = TuitionFee.objects.get(id=_id) + tuitionFee_serializer = TuitionFeeSerializer(tuitionFee) + return JsonResponse(tuitionFee_serializer.data, safe=False) + except TuitionFee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + + def post(self, request): + tuitionFee_data = JSONParser().parse(request) + tuitionFee_serializer = TuitionFeeSerializer(data=tuitionFee_data) + if tuitionFee_serializer.is_valid(): + tuitionFee_serializer.save() + return JsonResponse(tuitionFee_serializer.data, safe=False, status=201) + return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) + + def put(self, request, _id): + tuitionFee_data = JSONParser().parse(request) + try: + tuitionFee = TuitionFee.objects.get(id=_id) + except TuitionFee.DoesNotExist: + return JsonResponse({'error': 'No object found'}, status=404) + tuitionFee_serializer = TuitionFeeSerializer(tuitionFee, data=tuitionFee_data, partial=True) # Utilisation de partial=True + if tuitionFee_serializer.is_valid(): + tuitionFee_serializer.save() + return JsonResponse(tuitionFee_serializer.data, safe=False) + return JsonResponse(tuitionFee_serializer.errors, safe=False, status=400) + + def delete(self, request, _id): + return delete_object(TuitionFee, _id) \ No newline at end of file diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py index 766577d..b183107 100644 --- a/Back-End/Subscriptions/models.py +++ b/Back-End/Subscriptions/models.py @@ -9,6 +9,9 @@ from School.models import SchoolClass from datetime import datetime class RegistrationFee(models.Model): + """ + Représente un tarif ou frais d’inscription avec différentes options de paiement. + """ class PaymentOptions(models.IntegerChoices): SINGLE_PAYMENT = 0, _('Paiement en une seule fois') MONTHLY_PAYMENT = 1, _('Paiement mensuel') @@ -27,6 +30,9 @@ class RegistrationFee(models.Model): return self.name class Language(models.Model): + """ + Représente une langue parlée par l’élève. + """ id = models.AutoField(primary_key=True) label = models.CharField(max_length=200, default="") @@ -34,6 +40,9 @@ class Language(models.Model): return "LANGUAGE" class Guardian(models.Model): + """ + Représente un responsable légal (parent/tuteur) d’un élève. + """ last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") birth_date = models.CharField(max_length=200, default="", blank=True) @@ -47,6 +56,9 @@ class Guardian(models.Model): return self.last_name + "_" + self.first_name class Sibling(models.Model): + """ + Représente un frère ou une sœur d’un élève. + """ id = models.AutoField(primary_key=True) last_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="") @@ -56,7 +68,9 @@ class Sibling(models.Model): return "SIBLING" class Student(models.Model): - + """ + Représente l’élève inscrit ou en cours d’inscription. + """ class StudentGender(models.IntegerChoices): NONE = 0, _('Sélection du genre') MALE = 1, _('Garçon') @@ -95,6 +109,9 @@ class Student(models.Model): # Many-to-Many Relationship siblings = models.ManyToManyField(Sibling, blank=True) + # Many-to-Many Relationship + registration_files = models.ManyToManyField('RegistrationFile', blank=True, related_name='students') + # Many-to-Many Relationship spoken_languages = models.ManyToManyField(Language, blank=True) @@ -105,21 +122,39 @@ class Student(models.Model): return self.last_name + "_" + self.first_name def getSpokenLanguages(self): + """ + Retourne la liste des langues parlées par l’élève. + """ return self.spoken_languages.all() def getMainGuardian(self): + """ + Retourne le responsable légal principal de l’élève. + """ return self.guardians.all()[0] def getGuardians(self): + """ + Retourne tous les responsables légaux de l’élève. + """ return self.guardians.all() def getProfiles(self): + """ + Retourne les profils utilisateurs liés à l’élève. + """ return self.profiles.all() def getSiblings(self): + """ + Retourne les frères et sœurs de l’élève. + """ return self.siblings.all() def getNumberOfSiblings(self): + """ + Retourne le nombre de frères et sœurs. + """ return self.siblings.count() @property @@ -148,7 +183,9 @@ class Student(models.Model): return None class RegistrationForm(models.Model): - + """ + Gère le dossier d’inscription lié à un élève donné. + """ class RegistrationFormStatus(models.IntegerChoices): RF_ABSENT = 0, _('Pas de dossier d\'inscription') RF_CREATED = 1, _('Dossier d\'inscription créé') @@ -171,9 +208,53 @@ class RegistrationForm(models.Model): return "RF_" + self.student.last_name + "_" + self.student.first_name class RegistrationFileTemplate(models.Model): + """ + Modèle pour stocker les fichiers "templates" d’inscription. + """ name = models.CharField(max_length=255) - file = models.FileField(upload_to='registration_files/') + file = models.FileField(upload_to='templates_files/', blank=True, null=True) + order = models.PositiveIntegerField(default=0) # Ajout du champ order date_added = models.DateTimeField(auto_now_add=True) + is_required = models.BooleanField(default=False) + + @property + def formatted_date_added(self): + if self.date_added: + return self.date_added.strftime('%d-%m-%Y') + return None def __str__(self): return self.name + +def registration_file_upload_to(instance, filename): + return f"registration_files/dossier_rf_{instance.register_form.pk}/{filename}" + +class RegistrationFile(models.Model): + """ + Fichier lié à un dossier d’inscription particulier. + """ + name = models.CharField(max_length=255) + file = models.FileField(upload_to=registration_file_upload_to) + date_added = models.DateTimeField(auto_now_add=True) + template = models.OneToOneField(RegistrationFileTemplate, on_delete=models.CASCADE) + register_form = models.ForeignKey('RegistrationForm', on_delete=models.CASCADE, related_name='registration_files') + + @property + def formatted_date_added(self): + if self.date_added: + return self.date_added.strftime('%d-%m-%Y') + return None + + def __str__(self): + return self.name + + @staticmethod + def get_files_from_rf(register_form_id): + """ + Récupère tous les fichiers liés à un dossier d’inscription donné. + """ + registration_files = RegistrationFile.objects.filter(register_form_id=register_form_id).order_by('template__order') + filenames = [] + for reg_file in registration_files: + filenames.append(reg_file.file.path) + return filenames diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py index 99538d7..b4a996c 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, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee +from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee from School.models import SchoolClass from Auth.models import Profile from Auth.serializers import ProfileSerializer @@ -10,17 +10,22 @@ from django.utils import timezone import pytz from datetime import datetime -class RegistrationFileTemplateSerializer(serializers.ModelSerializer): - class Meta: - model = RegistrationFileTemplate - fields = '__all__' - class RegistrationFeeSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: model = RegistrationFee fields = '__all__' +class RegistrationFileSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFile + fields = '__all__' + +class RegistrationFileTemplateSerializer(serializers.ModelSerializer): + class Meta: + model = RegistrationFileTemplate + fields = '__all__' + class LanguageSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: @@ -47,6 +52,7 @@ class GuardianSerializer(serializers.ModelSerializer): class StudentSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) guardians = GuardianSerializer(many=True, required=False) siblings = SiblingSerializer(many=True, required=False) @@ -126,7 +132,7 @@ class RegistrationFormSerializer(serializers.ModelSerializer): registration_file = serializers.FileField(required=False) status_label = serializers.SerializerMethodField() formatted_last_update = serializers.SerializerMethodField() - + registration_files = RegistrationFileSerializer(many=True, required=False) class Meta: model = RegistrationForm fields = '__all__' diff --git a/Back-End/Subscriptions/urls.py b/Back-End/Subscriptions/urls.py index 53647fa..0e7fbf8 100644 --- a/Back-End/Subscriptions/urls.py +++ b/Back-End/Subscriptions/urls.py @@ -1,36 +1,44 @@ from django.urls import path, re_path from . import views -from Subscriptions.views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView +from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView, RegistrationFileView urlpatterns = [ - re_path(r'^registerForms/([a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"), + 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/([0-9]+)$', 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/([0-9]+)$', StudentView.as_view(), name="students"), + 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/([0-9]+)$', views.send, name="send"), + re_path(r'^send/(?P<_id>[0-9]+)$', views.send, name="send"), # Archivage d'un dossier d'inscription - re_path(r'^archive/([0-9]+)$', views.archive, name="archive"), + re_path(r'^archive/(?P<_id>[0-9]+)$', views.archive, name="archive"), # Envoi d'une relance de dossier d'inscription - re_path(r'^sendRelance/([0-9]+)$', views.relance, name="sendRelance"), + re_path(r'^sendRelance/(?P<_id>[0-9]+)$', views.relance, name="sendRelance"), # Page PARENT - Liste des children - re_path(r'^children/([0-9]+)$', ChildrenListView.as_view(), name="children"), + re_path(r'^children/(?P<_id>[0-9]+)$', ChildrenListView.as_view(), name="children"), # Page INSCRIPTION - Liste des élèves re_path(r'^students$', StudentListView.as_view(), name="students"), # Frais d'inscription re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"), + + # modèles de fichiers d'inscription re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'), - re_path(r'^registrationFileTemplates/([0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"), + 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"), + ] \ No newline at end of file diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py index 9ae17a9..e76d06b 100644 --- a/Back-End/Subscriptions/util.py +++ b/Back-End/Subscriptions/util.py @@ -16,52 +16,95 @@ from enum import Enum import random import string from rest_framework.parsers import JSONParser +import pymupdf def recupereListeFichesInscription(): + """ + Retourne la liste complète des fiches d’inscription. + """ context = { "ficheInscriptions_list": bdd.getAllObjects(RegistrationForm), } return context def recupereListeFichesInscriptionEnAttenteSEPA(): - + """ + Retourne les fiches d’inscription avec paiement SEPA en attente. + """ ficheInscriptionsSEPA_list = RegistrationForm.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=RegistrationForm.RegistrationFormStatus['SEPA_ENVOYE']) return ficheInscriptionsSEPA_list def _now(): + """ + Retourne la date et l’heure en cours, avec fuseau. + """ return datetime.now(ZoneInfo(settings.TZ_APPLI)) def convertToStr(dateValue, dateFormat): + """ + Convertit un objet datetime en chaîne selon un format donné. + """ return dateValue.strftime(dateFormat) def convertToDate(date_time): + """ + Convertit une chaîne en objet datetime selon le format '%d-%m-%Y %H:%M'. + """ format = '%d-%m-%Y %H:%M' datetime_str = datetime.strptime(date_time, format) return datetime_str def convertTelephone(telephoneValue, separator='-'): + """ + Reformate un numéro de téléphone en y insérant un séparateur donné. + """ return f"{telephoneValue[:2]}{separator}{telephoneValue[2:4]}{separator}{telephoneValue[4:6]}{separator}{telephoneValue[6:8]}{separator}{telephoneValue[8:10]}" def genereRandomCode(length): + """ + Génère un code aléatoire de longueur spécifiée. + """ return ''.join(random.choice(string.ascii_letters) for i in range(length)) def calculeDatePeremption(_start, nbDays): + """ + Calcule la date de fin à partir d’un point de départ et d’un nombre de jours. + """ return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT) # Fonction permettant de retourner la valeur du QueryDict # QueryDict [ index ] -> Dernière valeur d'une liste # dict (QueryDict [ index ]) -> Toutes les valeurs de la liste def _(liste): + """ + Retourne la première valeur d’une liste extraite d’un QueryDict. + """ return liste[0] def getArgFromRequest(_argument, _request): + """ + Extrait la valeur d’un argument depuis la requête (JSON). + """ resultat = None data=JSONParser().parse(_request) resultat = data[_argument] return resultat -def rfToPDF(registerForm): +def merge_files_pdf(filenames, output_filename): + """ + Insère plusieurs fichiers PDF dans un seul document de sortie. + """ + merger = pymupdf.open() + for filename in filenames: + merger.insert_file(filename) + merger.save(output_filename) + merger.close() + +def rfToPDF(registerForm,filename): + """ + Génère le PDF d’un dossier d’inscription et l’associe au RegistrationForm. + """ # Ajout du fichier d'inscriptions data = { 'pdf_title': "Dossier d'inscription de %s"%registerForm.student.first_name, @@ -69,14 +112,12 @@ def rfToPDF(registerForm): 'signatureTime': convertToStr(_now(), '%H:%M'), 'student':registerForm.student, } - + PDFFileName = filename pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) - - PDFFileName = "Dossier_Inscription_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) - pathFichier = Path(settings.DOCUMENT_DIR + "/" + PDFFileName) + pathFichier = Path(filename) if os.path.exists(str(pathFichier)): print(f'File exists : {str(pathFichier)}') os.remove(str(pathFichier)) - receipt_file = BytesIO(pdf.content) - registerForm.fichierInscription = File(receipt_file, PDFFileName) \ No newline at end of file + registerForm.fichierInscription = File(receipt_file, PDFFileName) + registerForm.fichierInscription.save() \ No newline at end of file diff --git a/Back-End/Subscriptions/views.py b/Back-End/Subscriptions/views.py index 47a085d..5a29bd8 100644 --- a/Back-End/Subscriptions/views.py +++ b/Back-End/Subscriptions/views.py @@ -10,6 +10,8 @@ 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 @@ -18,18 +20,22 @@ from io import BytesIO import Subscriptions.mailManager as mailer import Subscriptions.util as util -from Subscriptions.serializers import RegistrationFormSerializer, RegistrationFileTemplateSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer -from Subscriptions.pagination import CustomPagination -from Subscriptions.signals import clear_cache -from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine +from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer +from .pagination import CustomPagination +from .signals import clear_cache +from .models import Student, Guardian, RegistrationForm, RegistrationFee, 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): @@ -47,8 +53,18 @@ class RegisterFormListView(APIView): 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) @@ -84,6 +100,11 @@ class RegisterFormListView(APIView): 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: @@ -104,14 +125,23 @@ class RegisterFormListView(APIView): @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') @@ -137,21 +167,33 @@ class RegisterFormView(APIView): return JsonResponse(studentForm_serializer.data, safe=False) - return JsonResponse(studentForm_serializer.errors, safe=False, status=400) + return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) - def put(self, request, id): + def put(self, request, _id): + """ + Modifie un dossier d'inscription donné. + """ studentForm_data=JSONParser().parse(request) - status = studentForm_data.pop('status', 0) + _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) + registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=_id) - if status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: + if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: # Le parent a complété le dossier d'inscription, il est soumis à validation par l'école json.dumps(studentForm_data) - util.rfToPDF(registerForm) + #Génération de la fiche d'inscription au format PDF + PDFFileName = "rf_%s_%s.pdf"%(registerForm.student.last_name, registerForm.student.first_name) + path = Path(f"registration_files/dossier_rf_{registerForm.pk}/{PDFFileName}") + registerForm.fichierInscription = util.rfToPDF(registerForm, path) + # Récupération des fichiers d'inscription + fileNames = RegistrationFile.get_files_from_rf(registerForm.pk) + fileNames.insert(0,path) + # Création du fichier PDF Fusionné avec le dossier complet + output_path = f"registration_files/dossier_rf_{registerForm.pk}/dossier_{registerForm.pk}.pdf" + util.merge_files_pdf(fileNames, output_path) # Mise à jour de l'automate updateStateMachine(registerForm, 'saisiDI') - elif status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: + elif _status == RegistrationForm.RegistrationFormStatus.RF_VALIDATED: # L'école a validé le dossier d'inscription # Mise à jour de l'automate updateStateMachine(registerForm, 'valideDI') @@ -162,34 +204,49 @@ class RegisterFormView(APIView): studentForm_serializer.save() return JsonResponse(studentForm_serializer.data, safe=False) - return JsonResponse(studentForm_serializer.errors, safe=False, status=400) + 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=400) + 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): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +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() @@ -199,24 +256,31 @@ def send(request, id): 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=400) + 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=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) -def archive(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +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=400) + 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=400) + return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False, status=status.HTTP_404_NOT_FOUND) -def relance(request, id): - register_form = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id) +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() @@ -227,12 +291,15 @@ def relance(request, id): register_form.last_update=util.convertToStr(util._now(), '%d-%m-%Y %H:%M') register_form.save() - return JsonResponse({"errorMessage":errorMessage}, safe=False, status=400) + 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=400) + 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, _idProfile): @@ -242,6 +309,9 @@ class ChildrenListView(APIView): # 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) @@ -250,20 +320,52 @@ class StudentListView(APIView): # API utilisée pour la vue de personnalisation des frais d'inscription pour la structure class RegistrationFeeView(APIView): + """ + Liste les frais d’inscription. + """ def get(self, request): tarifs = bdd.getAllObjects(RegistrationFee) tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True) return JsonResponse(tarifs_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): - fichiers = RegistrationFileTemplate.objects.all() - serializer = RegistrationFileTemplateSerializer(fichiers, many=True) - return Response(serializer.data) + 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() @@ -271,10 +373,71 @@ class RegistrationFileTemplateView(APIView): 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) + 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=400) + 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/start.py b/Back-End/start.py index 0b0aff0..be242c4 100644 --- a/Back-End/start.py +++ b/Back-End/start.py @@ -13,12 +13,12 @@ def run_command(command): commands = [ ["python", "manage.py", "collectstatic", "--noinput"], ["python", "manage.py", "flush", "--noinput"], - ["python", "manage.py", "makemigrations", "Subscriptions"], - ["python", "manage.py", "makemigrations", "GestionNotification"], - ["python", "manage.py", "makemigrations", "GestionMessagerie"], - ["python", "manage.py", "makemigrations", "Auth"], - ["python", "manage.py", "makemigrations", "School"], - ["python", "manage.py", "migrate"] + ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"], + ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"], + ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"], + ["python", "manage.py", "makemigrations", "Auth", "--noinput"], + ["python", "manage.py", "makemigrations", "School", "--noinput"], + ["python", "manage.py", "migrate", "--noinput"] ] for command in commands: diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 365a30f..2f111a2 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -1,28 +1,22 @@ 'use client' import React, { useState, useEffect } from 'react'; -import { School, Calendar } from 'lucide-react'; -import TabsStructure from '@/components/Structure/Configuration/TabsStructure'; -import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement' -import StructureManagement from '@/components/Structure/Configuration/StructureManagement' -import { BE_SCHOOL_SPECIALITIES_URL, - BE_SCHOOL_SCHOOLCLASSES_URL, - BE_SCHOOL_TEACHERS_URL, - BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url'; -import DjangoCSRFToken from '@/components/DjangoCSRFToken' +import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign +import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; +import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; +import FeesManagement from '@/components/Structure/Configuration/FeesManagement'; +import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import useCsrfToken from '@/hooks/useCsrfToken'; import { ClassesProvider } from '@/context/ClassesContext'; -import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules } from '@/app/lib/schoolAction'; +import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchFees, fetchTuitionFees } from '@/app/lib/schoolAction'; +import SidebarTabs from '@/components/SidebarTabs'; export default function Page() { const [specialities, setSpecialities] = useState([]); const [classes, setClasses] = useState([]); const [teachers, setTeachers] = useState([]); - const [schedules, setSchedules] = useState([]); - const [activeTab, setActiveTab] = useState('Configuration'); - const tabs = [ - { id: 'Configuration', title: "Configuration de l'école", icon: School }, - { id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar }, - ]; + const [fees, setFees] = useState([]); + const [discounts, setDiscounts] = useState([]); + const [tuitionFees, setTuitionFees] = useState([]); const csrfToken = useCsrfToken(); @@ -38,6 +32,15 @@ export default function Page() { // Fetch data for schedules handleSchedules(); + + // Fetch data for fees + handleFees(); + + // Fetch data for discounts + handleDiscounts(); + + // Fetch data for TuitionFee + handleTuitionFees(); }, []); const handleSpecialities = () => { @@ -45,9 +48,7 @@ export default function Page() { .then(data => { setSpecialities(data); }) - .catch(error => { - console.error('Error fetching specialities:', error); - }); + .catch(error => console.error('Error fetching specialities:', error)); }; const handleTeachers = () => { @@ -55,9 +56,7 @@ export default function Page() { .then(data => { setTeachers(data); }) - .catch(error => { - console.error('Error fetching teachers:', error); - }); + .catch(error => console.error('Error fetching teachers:', error)); }; const handleClasses = () => { @@ -65,9 +64,7 @@ export default function Page() { .then(data => { setClasses(data); }) - .catch(error => { - console.error('Error fetching classes:', error); - }); + .catch(error => console.error('Error fetching classes:', error)); }; const handleSchedules = () => { @@ -75,13 +72,35 @@ export default function Page() { .then(data => { setSchedules(data); }) - .catch(error => { - console.error('Error fetching classes:', error); - }); + .catch(error => console.error('Error fetching schedules:', error)); }; - const handleCreate = (url, newData, setDatas) => { - fetch(url, { + const handleFees = () => { + fetchFees() + .then(data => { + setFees(data); + }) + .catch(error => console.error('Error fetching fees:', error)); + }; + + const handleDiscounts = () => { + fetchDiscounts() + .then(data => { + setDiscounts(data); + }) + .catch(error => console.error('Error fetching discounts:', error)); + }; + + const handleTuitionFees = () => { + fetchTuitionFees() + .then(data => { + setTuitionFees(data); + }) + .catch(error => console.error('Error fetching tuition fees', error)); + }; + + const handleCreate = (url, newData, setDatas, setErrors) => { + return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -90,18 +109,28 @@ export default function Page() { body: JSON.stringify(newData), credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { - console.log('Succes :', data); setDatas(prevState => [...prevState, data]); + setErrors({}); + return data; }) .catch(error => { - console.error('Erreur :', error); + setErrors(error); + console.error('Error creating data:', error); + throw error; }); }; - const handleEdit = (url, id, updatedData, setDatas) => { - fetch(`${url}/${id}`, { + const handleEdit = (url, id, updatedData, setDatas, setErrors) => { + return fetch(`${url}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -110,15 +139,41 @@ export default function Page() { body: JSON.stringify(updatedData), credentials: 'include' }) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw errorData; + }); + } + return response.json(); + }) .then(data => { setDatas(prevState => prevState.map(item => item.id === id ? data : item)); + setErrors({}); + return data; }) .catch(error => { - console.error('Erreur :', error); + setErrors(error); + console.error('Error editing data:', error); + throw error; }); }; + const handleDelete = (url, id, setDatas) => { + fetch(`${url}/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include' + }) + .then(response => response.json()) + .then(data => { + setDatas(prevState => prevState.filter(item => item.id !== id)); + }) + .catch(error => console.error('Error deleting data:', error)); + }; const handleUpdatePlanning = (url, planningId, updatedData) => { fetch(`${url}/${planningId}`, { method: 'PUT', @@ -139,35 +194,11 @@ export default function Page() { }); }; - const handleDelete = (url, id, setDatas) => { - fetch(`${url}/${id}`, { - method:'DELETE', - headers: { - 'Content-Type':'application/json', - 'X-CSRFToken': csrfToken - }, - credentials: 'include' - }) - .then(response => response.json()) - .then(data => { - console.log('Success:', data); - setDatas(prevState => prevState.filter(item => item.id !== id)); - }) - .catch(error => { - console.error('Error fetching data:', error); - error = error.errorMessage; - console.log(error); - }); - }; - - return ( -