mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
Compare commits
38 Commits
WIP_Docker
...
A64_TestsA
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a2b23f260 | |||
| 0064b8d35a | |||
| ec2c1daebc | |||
| 67cea2f1c6 | |||
| 5785bfae46 | |||
| a17078709b | |||
| d58155da06 | |||
| 043d93dcc4 | |||
| 6bc24055cd | |||
| 2f6d30b85b | |||
| c161fa7e75 | |||
| 789816e986 | |||
| 6bedf715cc | |||
| 59a0d40130 | |||
| 25e2799c0f | |||
| 017c0290dd | |||
| fe2d4d4513 | |||
| f93c428259 | |||
| e61cd51ce2 | |||
| 6a0b90e98f | |||
| 8a71fa1830 | |||
| f265540da2 | |||
| 5be5f9f70d | |||
| 68a6a63c4f | |||
| af30ae33b5 | |||
| e509625811 | |||
| 3a2455f918 | |||
| e74f9c98a2 | |||
| 8f0cf16f70 | |||
| 78d96f82f9 | |||
| c117f96e52 | |||
| e4668ef1e5 | |||
| ec2630a6e4 | |||
| d65b171da8 | |||
| 4a6b7ce379 | |||
| 170f7c4fa8 | |||
| ce83e02f7b | |||
| a69498dd06 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
.env
|
||||
node_modules/
|
||||
hardcoded-strings-report.md
|
||||
backend.env
|
||||
@ -1 +1 @@
|
||||
node scripts/prepare-commit-msg.js "$1" "$2"
|
||||
#node scripts/prepare-commit-msg.js "$1" "$2"
|
||||
2
Back-End/.gitignore
vendored
2
Back-End/.gitignore
vendored
@ -5,3 +5,5 @@ data
|
||||
*.dmp
|
||||
staticfiles
|
||||
/*/Configuration/application*.json
|
||||
rapport_tests_back.json
|
||||
tests_automatiques.json
|
||||
75
Back-End/Auth/migrations/0001_initial.py
Normal file
75
Back-End/Auth/migrations/0001_initial.py
Normal file
@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProfileRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role_type', models.IntegerField(choices=[(-1, 'NON DEFINI'), (0, 'ECOLE'), (1, 'ADMIN'), (2, 'PARENT')], default=-1)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile_roles', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Directeur',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('profile_role', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='directeur_profile', to='Auth.profilerole')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('email', models.EmailField(default='', max_length=255, unique=True, validators=[django.core.validators.EmailValidator()])),
|
||||
('roleIndexLoginDefault', models.IntegerField(default=0)),
|
||||
('code', models.CharField(blank=True, default='', max_length=200)),
|
||||
('datePeremption', models.CharField(blank=True, default='', max_length=200)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profilerole',
|
||||
name='profile',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
0
Back-End/Auth/migrations/__init__.py
Normal file
0
Back-End/Auth/migrations/__init__.py
Normal file
@ -223,14 +223,29 @@ def makeToken(user):
|
||||
"""
|
||||
try:
|
||||
# Récupérer tous les rôles de l'utilisateur actifs
|
||||
roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name', 'establishment__evaluation_frequency', 'establishment__total_capacity', 'establishment__api_docuseal')
|
||||
roles_qs = ProfileRole.objects.filter(profile=user, is_active=True).select_related('establishment')
|
||||
roles = []
|
||||
for role in roles_qs:
|
||||
logo_url = ""
|
||||
if role.establishment.logo:
|
||||
# Construit l'URL complète pour le logo
|
||||
logo_url = f"{role.establishment.logo.url}"
|
||||
roles.append({
|
||||
"role_type": role.role_type,
|
||||
"establishment__id": role.establishment.id,
|
||||
"establishment__name": role.establishment.name,
|
||||
"establishment__evaluation_frequency": role.establishment.evaluation_frequency,
|
||||
"establishment__total_capacity": role.establishment.total_capacity,
|
||||
"establishment__api_docuseal": role.establishment.api_docuseal,
|
||||
"establishment__logo": logo_url,
|
||||
})
|
||||
|
||||
# Générer le JWT avec la bonne syntaxe datetime
|
||||
access_payload = {
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
||||
'roles': list(roles),
|
||||
'roles': roles,
|
||||
'type': 'access',
|
||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
|
||||
'iat': datetime.utcnow(),
|
||||
@ -361,7 +376,7 @@ class SubscribeView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
establishment_id = newProfilConnection['establishment_id']
|
||||
@ -437,7 +452,7 @@ class NewPasswordView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
|
||||
@ -487,7 +502,7 @@ class ResetPasswordView(APIView):
|
||||
}
|
||||
)
|
||||
def post(self, request, code):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
|
||||
@ -498,7 +513,7 @@ class ResetPasswordView(APIView):
|
||||
if profil:
|
||||
|
||||
if datetime.strptime(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'), '%d-%m-%Y %H:%M') > datetime.strptime(profil.datePeremption, '%d-%m-%Y %H:%M'):
|
||||
retourErreur = error.returnMessage[error.EXPIRED_URL] % (_uuid)
|
||||
retourErreur = error.returnMessage[error.EXPIRED_URL]
|
||||
elif validationOk:
|
||||
retour = error.returnMessage[error.PASSWORD_CHANGED]
|
||||
|
||||
|
||||
63
Back-End/Common/migrations/0001_initial.py
Normal file
63
Back-End/Common/migrations/0001_initial.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Cycle',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('number', models.IntegerField(unique=True)),
|
||||
('label', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('cycle', models.IntegerField(choices=[(1, 'Cycle 1'), (2, 'Cycle 2'), (3, 'Cycle 3'), (4, 'Cycle 4')])),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentModeType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, unique=True)),
|
||||
('label', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentPlanType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=50, unique=True)),
|
||||
('label', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='Common.domain')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Level',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='Common.cycle')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/Common/migrations/__init__.py
Normal file
0
Back-End/Common/migrations/__init__.py
Normal file
29
Back-End/Establishment/migrations/0001_initial.py
Normal file
29
Back-End/Establishment/migrations/0001_initial.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Establishment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('address', models.CharField(max_length=255)),
|
||||
('total_capacity', models.IntegerField()),
|
||||
('establishment_type', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(choices=[(1, 'Maternelle'), (2, 'Primaire'), (3, 'Secondaire')]), size=None)),
|
||||
('evaluation_frequency', models.IntegerField(choices=[(1, 'Trimestre'), (2, 'Semestre'), (3, 'Année')], default=1)),
|
||||
('licence_code', models.CharField(blank=True, max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-30 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='api_docuseal',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
19
Back-End/Establishment/migrations/0003_establishment_logo.py
Normal file
19
Back-End/Establishment/migrations/0003_establishment_logo.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-31 09:56
|
||||
|
||||
import Establishment.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0002_establishment_api_docuseal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='logo',
|
||||
field=models.FileField(blank=True, null=True, upload_to=Establishment.models.registration_logo_upload_to),
|
||||
),
|
||||
]
|
||||
0
Back-End/Establishment/migrations/__init__.py
Normal file
0
Back-End/Establishment/migrations/__init__.py
Normal file
@ -2,6 +2,12 @@ from django.db import models
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import os
|
||||
|
||||
def registration_logo_upload_to(instance, filename):
|
||||
ext = os.path.splitext(filename)[1]
|
||||
return f"logos/school_{instance.pk}/logo{ext}"
|
||||
|
||||
class StructureType(models.IntegerChoices):
|
||||
MATERNELLE = 1, _('Maternelle')
|
||||
PRIMAIRE = 2, _('Primaire')
|
||||
@ -22,6 +28,11 @@ class Establishment(models.Model):
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
api_docuseal = models.CharField(max_length=255, blank=True, null=True)
|
||||
logo = models.FileField(
|
||||
upload_to=registration_logo_upload_to,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -1,16 +1,19 @@
|
||||
from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
|
||||
from django.utils.decorators import method_decorator
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from .models import Establishment
|
||||
from .serializers import EstablishmentSerializer
|
||||
from N3wtSchool.bdd import delete_object, getAllObjects
|
||||
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
|
||||
from School.models import EstablishmentCompetency, Competency
|
||||
from django.db.models import Q
|
||||
from Auth.models import Profile, ProfileRole, Directeur
|
||||
from Settings.models import SMTPSettings
|
||||
import N3wtSchool.mailManager as mailer
|
||||
import os
|
||||
from N3wtSchool import settings
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
@ -41,6 +44,8 @@ class EstablishmentListCreateView(APIView):
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class EstablishmentDetailView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
def get(self, request, id=None):
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=id)
|
||||
@ -50,15 +55,20 @@ class EstablishmentDetailView(APIView):
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def put(self, request, id):
|
||||
establishment_data = JSONParser().parse(request)
|
||||
"""
|
||||
Met à jour un établissement existant.
|
||||
Accepte les données en multipart/form-data pour permettre l'upload de fichiers (ex : logo).
|
||||
"""
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=id)
|
||||
except Establishment.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
establishment_serializer = EstablishmentSerializer(establishment, data=establishment_data, partial=True)
|
||||
|
||||
# Utilise request.data pour supporter multipart/form-data (fichiers et champs classiques)
|
||||
establishment_serializer = EstablishmentSerializer(establishment, data=request.data, partial=True)
|
||||
if establishment_serializer.is_valid():
|
||||
establishment_serializer.save()
|
||||
return JsonResponse(establishment_serializer.data, safe=False)
|
||||
return JsonResponse(establishment_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, id):
|
||||
@ -66,6 +76,7 @@ class EstablishmentDetailView(APIView):
|
||||
|
||||
def create_establishment_with_directeur(establishment_data):
|
||||
# Extraction des sous-objets
|
||||
# school_name = establishment_data.get("name")
|
||||
directeur_data = establishment_data.pop("directeur", None)
|
||||
smtp_settings_data = establishment_data.pop("smtp_settings", {})
|
||||
|
||||
@ -90,6 +101,8 @@ def create_establishment_with_directeur(establishment_data):
|
||||
# Création de l'établissement
|
||||
establishment_serializer = EstablishmentSerializer(data=establishment_data)
|
||||
establishment_serializer.is_valid(raise_exception=True)
|
||||
# base_dir = os.path.join(settings.MEDIA_ROOT, f"logo/school_{school_name}")
|
||||
# os.makedirs(base_dir, exist_ok=True)
|
||||
establishment = establishment_serializer.save()
|
||||
|
||||
# Création ou récupération du ProfileRole ADMIN pour ce profil et cet établissement
|
||||
@ -97,7 +110,7 @@ def create_establishment_with_directeur(establishment_data):
|
||||
profile=profile,
|
||||
establishment=establishment,
|
||||
role_type=ProfileRole.RoleType.PROFIL_ADMIN,
|
||||
defaults={"is_active": True}
|
||||
defaults={"is_active": False}
|
||||
)
|
||||
|
||||
# Création ou mise à jour du Directeur lié à ce ProfileRole
|
||||
@ -114,4 +127,6 @@ def create_establishment_with_directeur(establishment_data):
|
||||
smtp_settings_data["establishment"] = establishment
|
||||
SMTPSettings.objects.create(**smtp_settings_data)
|
||||
|
||||
# Envoi du mail
|
||||
mailer.sendRegistrationDirector(directeur_email, establishment.pk)
|
||||
return establishment, establishment_serializer.data
|
||||
30
Back-End/GestionMessagerie/migrations/0001_initial.py
Normal file
30
Back-End/GestionMessagerie/migrations/0001_initial.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Messagerie',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('objet', models.CharField(blank=True, default='', max_length=200)),
|
||||
('corpus', models.CharField(blank=True, default='', max_length=200)),
|
||||
('date_envoi', models.DateTimeField(auto_now_add=True)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('conversation_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('destinataire', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='messages_recus', to=settings.AUTH_USER_MODEL)),
|
||||
('emetteur', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='messages_envoyes', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,87 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-30 07:40
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('GestionMessagerie', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Conversation',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('conversation_type', models.CharField(choices=[('private', 'Privée'), ('group', 'Groupe')], default='private', max_length=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('last_activity', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('content', models.TextField()),
|
||||
('message_type', models.CharField(choices=[('text', 'Texte'), ('file', 'Fichier'), ('image', 'Image'), ('system', 'Système')], default='text', max_length=10)),
|
||||
('file_url', models.URLField(blank=True, null=True)),
|
||||
('file_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('file_size', models.BigIntegerField(blank=True, null=True)),
|
||||
('file_type', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_edited', models.BooleanField(default=False)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='GestionMessagerie.conversation')),
|
||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserPresence',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('online', 'En ligne'), ('away', 'Absent'), ('busy', 'Occupé'), ('offline', 'Hors ligne')], default='offline', max_length=10)),
|
||||
('last_seen', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_typing_in', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='typing_users', to='GestionMessagerie.conversation')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='presence', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConversationParticipant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('last_read_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='GestionMessagerie.conversation')),
|
||||
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation_participants', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('conversation', 'participant')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageRead',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('read_at', models.DateTimeField(auto_now_add=True)),
|
||||
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_by', to='GestionMessagerie.message')),
|
||||
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('message', 'participant')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
Back-End/GestionMessagerie/migrations/__init__.py
Normal file
0
Back-End/GestionMessagerie/migrations/__init__.py
Normal file
28
Back-End/GestionNotification/migrations/0001_initial.py
Normal file
28
Back-End/GestionNotification/migrations/0001_initial.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.CharField(max_length=255)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('typeNotification', models.IntegerField(choices=[(0, 'Aucune notification'), (1, 'Un message a été reçu'), (2, "Le dossier d'inscription a été mis à jour")], default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/GestionNotification/migrations/__init__.py
Normal file
0
Back-End/GestionNotification/migrations/__init__.py
Normal file
@ -1,4 +1,5 @@
|
||||
from typing import Final
|
||||
from N3wtSchool import settings
|
||||
|
||||
WRONG_ID: Final = 1
|
||||
INCOMPLETE: Final = 2
|
||||
@ -8,11 +9,14 @@ DIFFERENT_PASWWORD: Final = 5
|
||||
PROFIL_NOT_EXISTS: Final = 6
|
||||
MESSAGE_REINIT_PASSWORD: Final = 7
|
||||
EXPIRED_URL: Final = 8
|
||||
PASSWORD_CHANGED: Final = 8
|
||||
WRONG_MAIL_FORMAT: Final = 9
|
||||
PROFIL_INACTIVE: Final = 10
|
||||
MESSAGE_ACTIVATION_PROFILE: Final = 11
|
||||
PROFIL_ACTIVE: Final = 12
|
||||
PASSWORD_CHANGED: Final = 9
|
||||
WRONG_MAIL_FORMAT: Final = 10
|
||||
PROFIL_INACTIVE: Final = 11
|
||||
MESSAGE_ACTIVATION_PROFILE: Final = 12
|
||||
PROFIL_ACTIVE: Final = 13
|
||||
|
||||
def get_expired_url_message():
|
||||
return f"L'URL a expiré. Effectuer à nouveau la demande de réinitialisation de mot de passe : {settings.BASE_URL}/password/new"
|
||||
|
||||
returnMessage = {
|
||||
WRONG_ID:'Identifiants invalides',
|
||||
@ -22,7 +26,7 @@ returnMessage = {
|
||||
DIFFERENT_PASWWORD: 'Les mots de passe ne correspondent pas',
|
||||
PROFIL_NOT_EXISTS: 'Aucun profil associé à cet utilisateur',
|
||||
MESSAGE_REINIT_PASSWORD: 'Un mail a été envoyé à l\'adresse \'%s\'',
|
||||
EXPIRED_URL:'L\'URL a expiré. Effectuer à nouveau la demande de réinitialisation de mot de passe : http://localhost:3000/password/reset?uuid=%s',
|
||||
EXPIRED_URL: get_expired_url_message(),
|
||||
PASSWORD_CHANGED: 'Le mot de passe a été réinitialisé',
|
||||
WRONG_MAIL_FORMAT: 'L\'adresse mail est mal formatée',
|
||||
PROFIL_INACTIVE: 'Le profil n\'est pas actif',
|
||||
|
||||
@ -101,6 +101,27 @@ def envoieReinitMotDePasse(recipients, code):
|
||||
|
||||
return errorMessage
|
||||
|
||||
def sendRegistrationDirector(recipients, establishment_id):
|
||||
errorMessage = ''
|
||||
try:
|
||||
# Préparation du contexte pour le template
|
||||
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Bienvenue dans la communauté !'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'URL_DJANGO': settings.URL_DJANGO,
|
||||
'email': recipients,
|
||||
'establishment': establishment_id
|
||||
}
|
||||
subject = EMAIL_INSCRIPTION_SUBJECT
|
||||
html_message = render_to_string('emails/subscribeDirector.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
|
||||
return errorMessage
|
||||
|
||||
|
||||
def sendRegisterForm(recipients, establishment_id):
|
||||
errorMessage = ''
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class ContentSecurityPolicyMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
response['Content-Security-Policy'] = "frame-ancestors 'self' http://localhost:3000"
|
||||
response['Content-Security-Policy'] = f"frame-ancestors 'self' {settings.BASE_URL}"
|
||||
return response
|
||||
|
||||
@ -24,18 +24,16 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
MEDIA_URL = '/data/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'data')
|
||||
|
||||
BASE_URL = os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000')
|
||||
BASE_URL = os.getenv('BASE_URL', 'http://localhost:3000')
|
||||
|
||||
|
||||
LOGIN_REDIRECT_URL = '/Subscriptions/registerForms'
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-afjm6kvigncxzx6jjjf(qb0n(*qvi#je79r=gqflcn007d_ve9'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
DEBUG = os.getenv('DJANGO_DEBUG', True)
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
@ -211,8 +209,6 @@ USE_I18N = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
DEBUG = True
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
@ -232,33 +228,18 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
########################################################################
|
||||
|
||||
|
||||
|
||||
DJANGO_SUPERUSER_PASSWORD='admin'
|
||||
DJANGO_SUPERUSER_USERNAME='admin'
|
||||
DJANGO_SUPERUSER_EMAIL='admin@n3wtschool.com'
|
||||
# Configuration de l'email de l'application
|
||||
smtp_config_file = 'N3wtSchool/Configuration/application.json'
|
||||
|
||||
if os.path.exists(smtp_config_file):
|
||||
try:
|
||||
with open(smtp_config_file, 'r') as f:
|
||||
smtpSettings = json.load(f)
|
||||
EMAIL_HOST = smtpSettings.get('hostSMTP', '')
|
||||
EMAIL_PORT = smtpSettings.get('portSMTP', 587)
|
||||
EMAIL_HOST_USER = smtpSettings.get('username', '')
|
||||
EMAIL_HOST_PASSWORD = smtpSettings.get('password', '')
|
||||
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.com')
|
||||
EMAIL_PORT = os.getenv('EMAIL_PORT', 587)
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_USE_TLS = smtpSettings.get('useTLS', True)
|
||||
EMAIL_USE_SSL = smtpSettings.get('useSSL', False)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la lecture du fichier de configuration SMTP : {e}")
|
||||
else:
|
||||
logger.error(f"Fichier de configuration SMTP introuvable : {smtp_config_file}")
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'true').lower() == 'true'
|
||||
EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', 'false').lower() == 'true'
|
||||
|
||||
DOCUMENT_DIR = 'documents'
|
||||
|
||||
# Configuration CORS temporaire pour debug
|
||||
CORS_ALLOW_ALL_HEADERS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Configuration CORS spécifique pour la production
|
||||
@ -291,20 +272,26 @@ CORS_ALLOWED_METHODS = [
|
||||
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:8080').split(',')
|
||||
|
||||
CSRF_COOKIE_HTTPONLY = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = os.getenv('CSRF_COOKIE_SECURE', 'false').lower() == 'true'
|
||||
CSRF_COOKIE_NAME = 'csrftoken'
|
||||
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN', '')
|
||||
|
||||
USE_TZ = True
|
||||
TZ_APPLI = 'Europe/Paris'
|
||||
|
||||
DB_NAME = os.getenv('DB_NAME', 'school')
|
||||
DB_USER = os.getenv('DB_USER', 'postgres')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', 'postgres')
|
||||
DB_HOST = os.getenv('DB_HOST', 'database')
|
||||
DB_PORT = os.getenv('DB_PORT', '5432')
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
"NAME": "school",
|
||||
"USER": "postgres",
|
||||
"PASSWORD": "postgres",
|
||||
"HOST": "database",
|
||||
"PORT": "5432",
|
||||
"NAME": DB_NAME,
|
||||
"USER": DB_USER,
|
||||
"PASSWORD": DB_PASSWORD,
|
||||
"HOST": DB_HOST,
|
||||
"PORT": DB_PORT,
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,14 +326,14 @@ CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_TIMEZONE = 'Europe/Paris'
|
||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
||||
|
||||
URL_DJANGO = 'http://localhost:8080/'
|
||||
URL_DJANGO = os.getenv('URL_DJANGO', 'http://localhost:8080/')
|
||||
|
||||
REDIS_HOST = 'redis'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_DB = 0
|
||||
REDIS_PASSWORD = None
|
||||
|
||||
SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3'
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3')
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||
|
||||
44
Back-End/Planning/migrations/0001_initial.py
Normal file
44
Back-End/Planning/migrations/0001_initial.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
('School', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Planning',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, default='', null=True)),
|
||||
('color', models.CharField(default='#000000', max_length=255)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Establishment.establishment')),
|
||||
('school_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='planning', to='School.schoolclass')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Events',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, default='', null=True)),
|
||||
('start', models.DateTimeField()),
|
||||
('end', models.DateTimeField()),
|
||||
('recursionType', models.IntegerField(choices=[(0, 'Aucune'), (1, 'Quotidienne'), (2, 'Hebdomadaire'), (3, 'Mensuel'), (4, 'Personnalisé')], default=0)),
|
||||
('recursionEnd', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('color', models.CharField(max_length=255)),
|
||||
('location', models.CharField(blank=True, default='', max_length=255, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('planning', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Planning.planning')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/Planning/migrations/__init__.py
Normal file
0
Back-End/Planning/migrations/__init__.py
Normal file
@ -147,7 +147,7 @@ class EventsWithIdView(APIView):
|
||||
return JsonResponse({'error': 'Event not found'}, status=404)
|
||||
|
||||
event.delete()
|
||||
return JsonResponse({'message': 'Event deleted'}, status=204)
|
||||
return JsonResponse({'message': 'Event deleted'}, status=200)
|
||||
|
||||
class UpcomingEventsView(APIView):
|
||||
def get(self, request):
|
||||
|
||||
145
Back-End/School/migrations/0001_initial.py
Normal file
145
Back-End/School/migrations/0001_initial.py
Normal file
@ -0,0 +1,145 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Auth', '0001_initial'),
|
||||
('Common', '0001_initial'),
|
||||
('Establishment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Competency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.TextField()),
|
||||
('end_of_cycle', models.BooleanField(blank=True, default=False, null=True)),
|
||||
('level', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competencies', to='Common.category')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Discount',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('discount_type', models.IntegerField(choices=[(0, 'Currency'), (1, 'Percent')], default=0)),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='discounts', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EstablishmentCompetency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('custom_name', models.TextField(blank=True, help_text='Nom de la compétence custom', null=True)),
|
||||
('is_required', models.BooleanField(default=True)),
|
||||
('competency', models.ForeignKey(blank=True, help_text='Compétence de référence (optionnelle si custom)', null=True, on_delete=django.db.models.deletion.CASCADE, to='School.competency')),
|
||||
('custom_category', models.ForeignKey(blank=True, help_text='Catégorie de la compétence custom', null=True, on_delete=django.db.models.deletion.CASCADE, to='Common.category')),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Establishment.establishment')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('establishment', 'competency', 'custom_name', 'custom_category')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='competency',
|
||||
name='establishments',
|
||||
field=models.ManyToManyField(blank=True, related_name='competencies', through='School.EstablishmentCompetency', to='Establishment.establishment'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Fee',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('base_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fees', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentMode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_modes', to='Establishment.establishment')),
|
||||
('mode', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_modes', to='Common.paymentmodetype')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaymentPlan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('due_dates', django.contrib.postgres.fields.ArrayField(base_field=models.DateField(), blank=True, null=True, size=None)),
|
||||
('type', models.IntegerField(choices=[(0, 'Registration Fee'), (1, 'Tuition Fee')], default=0)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_plans', to='Establishment.establishment')),
|
||||
('plan_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_plans', to='Common.paymentplantype')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SchoolClass',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('atmosphere_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('age_range', models.JSONField(blank=True, null=True)),
|
||||
('number_of_students', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('teaching_language', models.CharField(blank=True, max_length=255)),
|
||||
('school_year', models.CharField(blank=True, max_length=9)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('type', models.IntegerField(choices=[(1, 'Annuel'), (2, 'Semestriel'), (3, 'Trimestriel')], default=1)),
|
||||
('time_range', models.JSONField(default=list)),
|
||||
('opening_days', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), default=list, size=None)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='school_classes', to='Establishment.establishment')),
|
||||
('levels', models.ManyToManyField(blank=True, related_name='school_classes', to='Common.level')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Planning',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('level', models.IntegerField(blank=True, choices=[(1, 'Très Petite Section (TPS)'), (2, 'Petite Section (PS)'), (3, 'Moyenne Section (MS)'), (4, 'Grande Section (GS)'), (5, 'Cours Préparatoire (CP)'), (6, 'Cours Élémentaire 1 (CE1)'), (7, 'Cours Élémentaire 2 (CE2)'), (8, 'Cours Moyen 1 (CM1)'), (9, 'Cours Moyen 2 (CM2)')], null=True)),
|
||||
('schedule', models.JSONField(default=dict)),
|
||||
('school_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='plannings', to='School.schoolclass')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Speciality',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('color_code', models.CharField(default='#FFFFFF', max_length=7)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specialities', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Teacher',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('profile_role', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='teacher_profile', to='Auth.profilerole')),
|
||||
('specialities', models.ManyToManyField(blank=True, to='School.speciality')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='schoolclass',
|
||||
name='teachers',
|
||||
field=models.ManyToManyField(blank=True, to='School.teacher'),
|
||||
),
|
||||
]
|
||||
0
Back-End/School/migrations/__init__.py
Normal file
0
Back-End/School/migrations/__init__.py
Normal file
@ -33,6 +33,10 @@ from N3wtSchool.bdd import delete_object, getAllObjects, getObject
|
||||
from django.db.models import Q
|
||||
from collections import defaultdict
|
||||
from Subscriptions.models import Student, StudentCompetency
|
||||
from Subscriptions.util import getCurrentSchoolYear
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
@ -581,7 +585,6 @@ class EstablishmentCompetencyListCreateView(APIView):
|
||||
|
||||
try:
|
||||
category = Category.objects.get(id=category_id)
|
||||
# Vérifier si une compétence custom du même nom existe déjà pour cet établissement et cette catégorie
|
||||
ec_exists = EstablishmentCompetency.objects.filter(
|
||||
establishment_id=establishment_id,
|
||||
competency__isnull=True,
|
||||
@ -598,12 +601,31 @@ class EstablishmentCompetencyListCreateView(APIView):
|
||||
custom_category=category,
|
||||
is_required=False
|
||||
)
|
||||
# Associer à tous les élèves de l'établissement
|
||||
|
||||
# Récupérer l'établissement et sa fréquence d'évaluation
|
||||
establishment = ec.establishment
|
||||
evaluation_frequency = establishment.evaluation_frequency # 1=Trimestre, 2=Semestre, 3=Année
|
||||
|
||||
# Déterminer l'année scolaire courante
|
||||
school_year = getCurrentSchoolYear()
|
||||
|
||||
# Générer les périodes selon la fréquence
|
||||
periods = []
|
||||
if evaluation_frequency == 1: # Trimestre
|
||||
periods = [f"T{i+1}_{school_year}" for i in range(3)]
|
||||
elif evaluation_frequency == 2: # Semestre
|
||||
periods = [f"S{i+1}_{school_year}" for i in range(2)]
|
||||
elif evaluation_frequency == 3: # Année
|
||||
periods = [f"A_{school_year}"]
|
||||
|
||||
# Associer à tous les élèves de l'établissement pour chaque période
|
||||
students = Student.objects.filter(associated_class__establishment_id=establishment_id)
|
||||
for student in students:
|
||||
for period in periods:
|
||||
StudentCompetency.objects.get_or_create(
|
||||
student=student,
|
||||
establishment_competency=ec
|
||||
establishment_competency=ec,
|
||||
period=period
|
||||
)
|
||||
|
||||
created.append({
|
||||
|
||||
29
Back-End/Settings/migrations/0001_initial.py
Normal file
29
Back-End/Settings/migrations/0001_initial.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Establishment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SMTPSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('smtp_server', models.CharField(max_length=255)),
|
||||
('smtp_port', models.PositiveIntegerField()),
|
||||
('smtp_user', models.CharField(max_length=255)),
|
||||
('smtp_password', models.CharField(max_length=255)),
|
||||
('use_tls', models.BooleanField(default=True)),
|
||||
('use_ssl', models.BooleanField(default=False)),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
Back-End/Settings/migrations/__init__.py
Normal file
0
Back-End/Settings/migrations/__init__.py
Normal file
213
Back-End/Subscriptions/migrations/0001_initial.py
Normal file
213
Back-End/Subscriptions/migrations/0001_initial.py
Normal file
@ -0,0 +1,213 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-28 11:14
|
||||
|
||||
import Subscriptions.models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('Auth', '__first__'),
|
||||
('Common', '0001_initial'),
|
||||
('Establishment', '0001_initial'),
|
||||
('School', '__first__'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Language',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('label', models.CharField(default='', max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Student',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('photo', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_photo_upload_to)),
|
||||
('last_name', models.CharField(default='', max_length=200)),
|
||||
('first_name', models.CharField(default='', max_length=200)),
|
||||
('gender', models.IntegerField(blank=True, choices=[(0, 'Sélection du genre'), (1, 'Garçon'), (2, 'Fille')], default=0)),
|
||||
('nationality', models.CharField(blank=True, default='', max_length=200)),
|
||||
('address', models.CharField(blank=True, default='', max_length=200)),
|
||||
('birth_date', models.DateField(blank=True, null=True)),
|
||||
('birth_place', models.CharField(blank=True, default='', max_length=200)),
|
||||
('birth_postal_code', models.IntegerField(blank=True, default=0)),
|
||||
('attending_physician', models.CharField(blank=True, default='', max_length=200)),
|
||||
('associated_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='School.schoolclass')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationSchoolFileTemplate',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False)),
|
||||
('slug', models.CharField(default='', max_length=255)),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_school_file_upload_to)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sibling',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('birth_date', models.DateField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Guardian',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('birth_date', models.DateField(blank=True, null=True)),
|
||||
('address', models.CharField(blank=True, default='', max_length=200)),
|
||||
('phone', models.CharField(blank=True, default='', max_length=200)),
|
||||
('profession', models.CharField(blank=True, default='', max_length=200)),
|
||||
('profile_role', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='guardian_profile', to='Auth.profilerole')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationFileGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='file_group', to='Establishment.establishment')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationForm',
|
||||
fields=[
|
||||
('student', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='Subscriptions.student')),
|
||||
('status', models.IntegerField(choices=[(0, "Pas de dossier d'inscription"), (1, "Dossier d'inscription initialisé"), (2, "Dossier d'inscription envoyé"), (3, "Dossier d'inscription en cours de validation"), (4, "Dossier d'inscription à relancer"), (5, "Dossier d'inscription validé"), (6, "Dossier d'inscription archivé"), (7, 'Mandat SEPA envoyé'), (8, 'Mandat SEPA à envoyer')], default=0)),
|
||||
('last_update', models.DateTimeField(auto_now=True)),
|
||||
('school_year', models.CharField(blank=True, default='', max_length=9)),
|
||||
('notes', models.CharField(blank=True, max_length=200)),
|
||||
('registration_link_code', models.CharField(blank=True, default='', max_length=200)),
|
||||
('registration_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
||||
('sepa_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
||||
('fusion_file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_file_path)),
|
||||
('associated_rf', models.CharField(blank=True, default='', max_length=200)),
|
||||
('discounts', models.ManyToManyField(blank=True, related_name='register_forms', to='School.discount')),
|
||||
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='register_forms', to='Establishment.establishment')),
|
||||
('fees', models.ManyToManyField(blank=True, related_name='register_forms', to='School.fee')),
|
||||
('fileGroup', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='register_forms', to='Subscriptions.registrationfilegroup')),
|
||||
('registration_payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='registration_payment_modes_forms', to='School.paymentmode')),
|
||||
('registration_payment_plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='registration_payment_plans_forms', to='School.paymentplan')),
|
||||
('tuition_payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tuition_payment_modes_forms', to='School.paymentmode')),
|
||||
('tuition_payment_plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tuition_payment_plans_forms', to='School.paymentplan')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='guardians',
|
||||
field=models.ManyToManyField(blank=True, to='Subscriptions.guardian'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='level',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='Common.level'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='profiles',
|
||||
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='spoken_languages',
|
||||
field=models.ManyToManyField(blank=True, to='Subscriptions.language'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BilanCompetence',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_bilan_form_upload_to)),
|
||||
('period', models.CharField(help_text='Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bilans', to='Subscriptions.student')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AbsenceManagement',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('day', models.DateField(blank=True, null=True)),
|
||||
('moment', models.IntegerField(choices=[(1, 'Morning'), (2, 'Afternoon'), (3, 'Total')], default=3)),
|
||||
('reason', models.IntegerField(choices=[(1, 'Justified Absence'), (2, 'Unjustified Absence'), (3, 'Justified Late'), (4, 'Unjustified Late')], default=2)),
|
||||
('commentaire', models.TextField(blank=True, null=True)),
|
||||
('establishment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='Establishment.establishment')),
|
||||
('student', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='Subscriptions.student')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationParentFileMaster',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('description', models.CharField(blank=True, null=True)),
|
||||
('is_required', models.BooleanField(default=False)),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='parent_file_masters', to='Subscriptions.registrationfilegroup')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationSchoolFileMaster',
|
||||
fields=[
|
||||
('id', models.IntegerField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(default='', max_length=255)),
|
||||
('is_required', models.BooleanField(default=False)),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='school_file_masters', to='Subscriptions.registrationfilegroup')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='registration_files',
|
||||
field=models.ManyToManyField(blank=True, related_name='students', to='Subscriptions.registrationschoolfiletemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='registrationschoolfiletemplate',
|
||||
name='master',
|
||||
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_templates', to='Subscriptions.registrationschoolfilemaster'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='siblings',
|
||||
field=models.ManyToManyField(blank=True, to='Subscriptions.sibling'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='registrationschoolfiletemplate',
|
||||
name='registration_form',
|
||||
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='school_file_templates', to='Subscriptions.registrationform'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RegistrationParentFileTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(blank=True, null=True, upload_to=Subscriptions.models.registration_parent_file_upload_to)),
|
||||
('master', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationparentfilemaster')),
|
||||
('registration_form', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_file_templates', to='Subscriptions.registrationform')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StudentCompetency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('score', models.IntegerField(blank=True, null=True)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('period', models.CharField(blank=True, default='', help_text="Période d'évaluation ex: T1-2024_2025, S1-2024_2025, A-2024_2025", max_length=20)),
|
||||
('establishment_competency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_scores', to='School.establishmentcompetency')),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competency_scores', to='Subscriptions.student')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('student', 'establishment_competency', 'period')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.3 on 2025-05-30 07:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('Subscriptions', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='registrationparentfilemaster',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
]
|
||||
0
Back-End/Subscriptions/migrations/__init__.py
Normal file
0
Back-End/Subscriptions/migrations/__init__.py
Normal file
@ -6,6 +6,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from datetime import datetime
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("SubscriptionModels")
|
||||
|
||||
class Language(models.Model):
|
||||
"""
|
||||
@ -394,6 +397,7 @@ class RegistrationParentFileTemplate(models.Model):
|
||||
registration_files = RegistrationParentFileTemplate.objects.filter(registration_form=register_form_id)
|
||||
filenames = []
|
||||
for reg_file in registration_files:
|
||||
if reg_file.file and hasattr(reg_file.file, 'path'):
|
||||
filenames.append(reg_file.file.path)
|
||||
return filenames
|
||||
|
||||
|
||||
@ -381,16 +381,20 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
|
||||
|
||||
class StudentByParentSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
associated_class_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Student
|
||||
fields = ['id', 'last_name', 'first_name', 'level', 'photo']
|
||||
fields = ['id', 'last_name', 'first_name', 'level', 'photo', 'associated_class_name']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StudentByParentSerializer, self).__init__(*args, **kwargs)
|
||||
for field in self.fields:
|
||||
self.fields[field].required = False
|
||||
|
||||
def get_associated_class_name(self, obj):
|
||||
return obj.associated_class.atmosphere_name if obj.associated_class else None
|
||||
|
||||
class RegistrationFormByParentSerializer(serializers.ModelSerializer):
|
||||
student = StudentByParentSerializer(many=False, required=True)
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Confirmation d'inscription</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Confirmation de souscription</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
width: 120px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}/static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Confirmation de souscription</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour,</p>
|
||||
<p>Nous sommes ravis de vous compter parmi les utilisateurs de N3wt School et vous remercions pour votre confiance</p>
|
||||
<p>Vous trouverez ci-joint le lien vers la page d'authentification : <a href="{{BASE_URL}}/users/login">{{BASE_URL}}/users/login</a></p>
|
||||
<p>S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte à cette url : <a href="{{BASE_URL}}/users/subscribe?establishment_id={{establishment}}">{{BASE_URL}}/users/subscribe</a></p>
|
||||
<p>votre identifiant est : {{ email }}</p>
|
||||
<p>Notre équipe est à votre disposition pour vous aider à tirer pleinement parti des fonctionnalités offertes par Newt School.</p>
|
||||
<p>N'hésitez pas à nous contacter pour toute question ou besoin d'assistance.</p>
|
||||
<p>Cordialement,</p>
|
||||
<p>L'équipe N3wt School</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Ce message est généré automatiquement, merci de ne pas y répondre.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@ -11,28 +11,25 @@
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.6;
|
||||
color: #222;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 3px solid #4CAF50;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
font-size: 20pt;
|
||||
font-size: 22pt;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin: 0;
|
||||
@ -41,51 +38,47 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #4CAF50;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: #fefefe;
|
||||
margin-bottom: 32px; /* Espacement augmenté entre les sections */
|
||||
}
|
||||
.section-title {
|
||||
font-size: 18pt;
|
||||
font-size: 15pt;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 18px; /* Espacement sous le titre de section */
|
||||
border-bottom: 1px solid #4CAF50;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
td {
|
||||
padding: 8px;
|
||||
vertical-align: top;
|
||||
th, td {
|
||||
border: 1px solid #bbb;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f3f3f3;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
.label-cell {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
width: 35%;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
width: 30%;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
.value-cell {
|
||||
color: #333;
|
||||
width: 65%;
|
||||
text-align: left;
|
||||
}
|
||||
.phone {
|
||||
white-space: nowrap;
|
||||
width: 70%;
|
||||
}
|
||||
.signature {
|
||||
margin-top: 30px;
|
||||
@ -97,6 +90,12 @@
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.subsection-title {
|
||||
font-size: 12pt;
|
||||
color: #333;
|
||||
margin: 8px 0 4px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -106,125 +105,121 @@
|
||||
<div class="header">
|
||||
<h1 class="title">Fiche élève de {{ student.last_name }} {{ student.first_name }}</h1>
|
||||
{% if student.photo %}
|
||||
<img
|
||||
src="{{ student.get_photo_url }}"
|
||||
alt="Photo de l'élève"
|
||||
class="photo"
|
||||
/>
|
||||
<img src="{{ student.get_photo_url }}" alt="Photo de l'élève" class="photo" />
|
||||
{% else %}
|
||||
<img
|
||||
src="/static/img/default-photo.jpg"
|
||||
alt="Photo par défaut"
|
||||
class="photo"
|
||||
/>
|
||||
<img src="/static/img/default-photo.jpg" alt="Photo par défaut" class="photo" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Student Section -->
|
||||
<!-- Élève -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">ÉLÈVE</h2>
|
||||
<div class="section-title">ÉLÈVE</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom :</td>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ student.last_name }}</td>
|
||||
<td class="label-cell">Prénom :</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ student.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Adresse :</td>
|
||||
<td colspan="3" class="value-cell">{{ student.address }}</td>
|
||||
<td class="label-cell">Adresse</td>
|
||||
<td class="value-cell" colspan="3">{{ student.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Genre :</td>
|
||||
<td class="label-cell">Genre</td>
|
||||
<td class="value-cell">{{ student|getStudentGender }}</td>
|
||||
<td class="label-cell">Né(e) le :</td>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell">{{ student.birth_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">À :</td>
|
||||
<td colspan="3" class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
|
||||
<td class="label-cell">À</td>
|
||||
<td class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
|
||||
<td class="label-cell">Nationalité</td>
|
||||
<td class="value-cell">{{ student.nationality }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Nationalité :</td>
|
||||
<td class="value-cell">{{ student.nationality }}</td>
|
||||
<td class="label-cell">Niveau :</td>
|
||||
<td class="label-cell">Niveau</td>
|
||||
<td class="value-cell">{{ student|getStudentLevel }}</td>
|
||||
<td class="label-cell"></td>
|
||||
<td class="value-cell"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Guardians Section -->
|
||||
<!-- Responsables -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">RESPONSABLES</h2>
|
||||
<div class="section-title">RESPONSABLES</div>
|
||||
{% for guardian in student.getGuardians %}
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Responsable {{ forloop.counter }}</h3>
|
||||
<div>
|
||||
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom :</td>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ guardian.last_name }}</td>
|
||||
<td class="label-cell">Prénom :</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ guardian.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Adresse :</td>
|
||||
<td colspan="3" class="value-cell">{{ guardian.address }}</td>
|
||||
<td class="label-cell">Adresse</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le :</td>
|
||||
<td class="label-cell">Email</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell">{{ guardian.birth_date }}</td>
|
||||
<td class="label-cell">Email :</td>
|
||||
<td class="value-cell">{{ guardian.email }}</td>
|
||||
<td class="label-cell">Téléphone</td>
|
||||
<td class="value-cell">{{ guardian.phone|phone_format }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Téléphone :</td>
|
||||
<td class="value-cell phone">{{ guardian.phone|phone_format }}</td>
|
||||
<td class="label-cell">Profession :</td>
|
||||
<td class="value-cell">{{ guardian.profession }}</td>
|
||||
<td class="label-cell">Profession</td>
|
||||
<td class="value-cell" colspan="3">{{ guardian.profession }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Siblings Section -->
|
||||
<!-- Fratrie -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">FRATRIE</h2>
|
||||
<div class="section-title">FRATRIE</div>
|
||||
{% for sibling in student.getSiblings %}
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Frère/Soeur {{ forloop.counter }}</h3>
|
||||
<div>
|
||||
<div class="subsection-title">Frère/Soeur {{ forloop.counter }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Nom :</td>
|
||||
<td class="label-cell">Nom</td>
|
||||
<td class="value-cell">{{ sibling.last_name }}</td>
|
||||
<td class="label-cell">Prénom :</td>
|
||||
<td class="label-cell">Prénom</td>
|
||||
<td class="value-cell">{{ sibling.first_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Né(e) le :</td>
|
||||
<td colspan="3" class="value-cell">{{ sibling.birth_date }}</td>
|
||||
<td class="label-cell">Né(e) le</td>
|
||||
<td class="value-cell" colspan="3">{{ sibling.birth_date }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Payment Section -->
|
||||
<!-- Paiement -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">MODALITÉS DE PAIEMENT</h2>
|
||||
<div class="section-title">MODALITÉS DE PAIEMENT</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="label-cell">Frais d'inscription :</td>
|
||||
<td class="label-cell">Frais d'inscription</td>
|
||||
<td class="value-cell">{{ student|getRegistrationPaymentMethod }} en {{ student|getRegistrationPaymentPlan }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label-cell">Frais de scolarité :</td>
|
||||
<td class="label-cell">Frais de scolarité</td>
|
||||
<td class="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<!-- Signature -->
|
||||
<div class="signature">
|
||||
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
|
||||
</div>
|
||||
|
||||
@ -88,6 +88,7 @@ class RegisterFormView(APIView):
|
||||
filter = request.GET.get('filter', '').strip()
|
||||
page_size = request.GET.get('page_size', None)
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
search = request.GET.get('search', '').strip() # <-- Ajout du paramètre search
|
||||
|
||||
# Gestion du page_size
|
||||
if page_size is not None:
|
||||
@ -113,7 +114,14 @@ class RegisterFormView(APIView):
|
||||
registerForms_List = None
|
||||
|
||||
if registerForms_List:
|
||||
registerForms_List = registerForms_List.filter(establishment=establishment_id).order_by('-last_update')
|
||||
registerForms_List = registerForms_List.filter(establishment=establishment_id)
|
||||
# Ajout du filtre search sur le nom et prénom de l'élève
|
||||
if search:
|
||||
registerForms_List = registerForms_List.filter(
|
||||
Q(student__first_name__icontains=search) |
|
||||
Q(student__last_name__icontains=search)
|
||||
)
|
||||
registerForms_List = registerForms_List.order_by('-last_update')
|
||||
|
||||
if not registerForms_List:
|
||||
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)
|
||||
|
||||
@ -12,11 +12,27 @@ from N3wtSchool import bdd
|
||||
|
||||
class RegistrationSchoolFileMasterView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les masters de templates d'inscription",
|
||||
operation_description="Récupère tous les masters de templates d'inscription pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationSchoolFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
masters = RegistrationSchoolFileMaster.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les masters liés à l'établissement via groups.establishment
|
||||
masters = RegistrationSchoolFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationSchoolFileMasterSerializer(masters, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -86,11 +102,27 @@ class RegistrationSchoolFileMasterSimpleView(APIView):
|
||||
|
||||
class RegistrationSchoolFileTemplateView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les templates d'inscription",
|
||||
operation_description="Récupère tous les templates d'inscription pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationSchoolFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
templates = RegistrationSchoolFileTemplate.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationSchoolFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationSchoolFileTemplateSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -160,11 +192,27 @@ class RegistrationSchoolFileTemplateSimpleView(APIView):
|
||||
|
||||
class RegistrationParentFileMasterView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les fichiers parents",
|
||||
operation_description="Récupère tous les fichiers parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileMasterSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
templates = RegistrationParentFileMaster.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les fichiers parents liés à l'établissement
|
||||
templates = RegistrationParentFileMaster.objects.filter(
|
||||
groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileMasterSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@ -234,11 +282,27 @@ class RegistrationParentFileMasterSimpleView(APIView):
|
||||
|
||||
class RegistrationParentFileTemplateView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère tous les templates d'inscription",
|
||||
operation_description="Récupère tous les templates parents pour un établissement donné",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id',
|
||||
openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
responses={200: RegistrationParentFileTemplateSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
templates = RegistrationParentFileTemplate.objects.all()
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': "Paramètre 'establishment_id' requis"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Filtrer les templates parents liés à l'établissement via master.groups.establishment
|
||||
templates = RegistrationParentFileTemplate.objects.filter(
|
||||
master__groups__establishment__id=establishment_id
|
||||
).distinct()
|
||||
serializer = RegistrationParentFileTemplateSerializer(templates, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.0.3"
|
||||
|
||||
@ -70,3 +70,7 @@ xhtml2pdf==0.2.16
|
||||
channels==4.0.0
|
||||
channels-redis==4.1.0
|
||||
daphne==4.1.0
|
||||
pytest
|
||||
djangorestframework
|
||||
pytest-django
|
||||
pytest-json-report
|
||||
|
||||
@ -10,21 +10,21 @@ def run_command(command):
|
||||
print(f"stderr: {stderr.decode()}")
|
||||
return process.returncode
|
||||
|
||||
test_mode = os.getenv('TEST_MODE', 'False') == 'True'
|
||||
test_mode = os.getenv('test_mode', 'false').lower() == 'true'
|
||||
|
||||
commands = [
|
||||
["python", "manage.py", "collectstatic", "--noinput"],
|
||||
["python", "manage.py", "flush", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Common", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Settings", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Planning", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "GestionEmail", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "Auth", "--noinput"],
|
||||
["python", "manage.py", "makemigrations", "School", "--noinput"],
|
||||
#["python", "manage.py", "flush", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Common", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Settings", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Subscriptions", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Planning", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "GestionNotification", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "GestionEmail", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "GestionMessagerie", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "Auth", "--noinput"],
|
||||
# ["python", "manage.py", "makemigrations", "School", "--noinput"],
|
||||
["python", "manage.py", "migrate", "--noinput"]
|
||||
]
|
||||
|
||||
|
||||
@ -1,8 +1,42 @@
|
||||
<svg width="38" height="120" viewBox="0 0 38 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38 79.0995V40L29.9458 49.0478V72.3137L1.27073 99.134C-2.59496 103.012 3.20408 109.151 7.39397 106.243L38 79.0995Z" fill="#D9006D" fill-opacity="0.8"/>
|
||||
<path d="M0 79.0995V40L8.05422 49.0478V72.3137L36.7293 99.134C40.595 103.012 34.7959 109.151 30.606 106.243L0 79.0995Z" fill="#038ECE" fill-opacity="0.8"/>
|
||||
<path d="M19 83L12 89.6585L19 96L26 89.6585L19 83Z" fill="#011922"/>
|
||||
<path d="M14 28.8736H24V72.1264H14V28.8736Z" fill="#001821"/>
|
||||
<path d="M24 28.8736C24 31.5652 21.7614 33.7471 19 33.7471C16.2386 33.7471 14 31.5652 14 28.8736C14 26.182 16.2386 24 19 24C21.7614 24 24 26.182 24 28.8736Z" fill="#001821"/>
|
||||
<path d="M24 72.1264C24 74.818 21.7614 77 19 77C16.2386 77 14 74.818 14 72.1264C14 69.4348 16.2386 67.2529 19 67.2529C21.7614 67.2529 24 69.4348 24 72.1264Z" fill="#001821"/>
|
||||
<svg width="565" height="609" viewBox="0 0 565 609" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M170.999 374.501C167.799 383.301 172.332 402.834 174.999 411.501C189.998 452.002 218.216 463.317 223 464C237 466 238.5 464 246.5 459.5C248.1 451.9 238.333 445 234 443C224 439.8 213.833 431 210 427C246.4 422.6 255.833 416.167 256 413.5C246.8 395.9 213.5 396.834 198 399.501C181.2 372.301 172.999 371.501 170.999 374.501Z" fill="#003625"/>
|
||||
<path d="M166 84.5L208 94.5L206 112.5C203 121 196.9 138.5 196.5 140.5C196.1 142.5 174 145.333 163 146.5L112 166.5C96.3333 177.833 64.7 200.8 63.5 202C62.3 203.2 42.3333 237.5 32.5 254.5L23 320L21 389.5C29.8333 416.333 47.7 470.6 48.5 473C49.5 476 66.9995 505.5 65.9995 505.5C65.1995 505.5 92.6662 529.5 106.5 541.5C116.666 549.667 137.4 566.2 139 567C140.6 567.8 156.333 574.667 164 578L221 584.5H247.5L291.5 572.5L341 546L388 490.5L397 475L411.5 447.5L414.5 425L406.5 400L386.5 377.5L366 371L367.5 381L375 402L377 419L367.5 440.5L360.5 456.5L337 475C327.166 480.167 307.5 489.9 307.5 487.5C307.5 484.5 269 494 264 494.5C259 495 231 493 227.5 490.5C224.7 488.5 183 471.667 162.5 463.5L104 282L199 314C219.5 315.5 261.2 318.5 264 318.5C266.8 318.5 275.5 315.5 279.5 314L276 307.5L272.5 299L264 289.5C266.833 288.167 273.9 285.5 279.5 285.5C285.1 285.5 281.833 279.5 279.5 276.5L272.5 266.5L236 261L227.5 248.5V227L264 185L279.5 157.5L288.5 142.5L295 130L307.5 120.5L328 112.5L392.5 103.5L457.5 87.5H476L481 84.5V73L476 60.5L470.5 50C467 47.6667 459.5 42.4 457.5 40C455.5 37.6 449.666 33 447 31H435.5L425.5 19.5L416 16L406 11H392.5L377 16H366.5L314 19.5L264 25.5L199 55L166 84.5Z" fill="#10B981"/>
|
||||
<path d="M215 304C195.4 294 147.167 248.5 125.5 227L119.5 230.5C116.5 235.333 110.5 245.3 110.5 246.5C110.5 247.7 109.833 257 109.5 261.5C112 266.167 117.5 276.4 119.5 280C122 284.5 131.5 284.5 142 290C152.5 295.5 171 297 173.5 298C176 299 192 311 194.5 313C196.5 314.6 200.667 313.667 202.5 313L215 304Z" fill="#059669"/>
|
||||
<path d="M33.5 381.5C32 386.167 29 396.9 29 402.5V415V427.5C41.1667 449.167 65.9 493 67.5 495C69.5 497.5 84 521 87.5 524.5C91 528 105 542.5 107.5 546C109.5 548.8 124.667 560.833 132 566.5L184.5 575C281.7 595 343 545 361.5 517.5L359.5 511C275.1 571.8 199.333 562.333 172 550C178.4 550 184.333 546.333 186.5 544.5C83.3 509.3 41.5 421.167 33.5 381.5Z" fill="#059669"/>
|
||||
<path d="M346 184C315.2 203.2 291.5 245.333 283.5 264C283.5 259.599 239.5 254.166 217.5 252C232.064 267.745 261.219 267.834 273.898 267.992C273.587 267.653 273.741 267.591 274.5 268C274.304 267.997 274.103 267.995 273.898 267.992C274.473 268.621 276.642 270.201 279.5 271.5C283.9 273.5 279.5 278 274.5 276C272.1 276 265.5 279.333 262.5 281H258C250 283.8 229 272.167 219.5 266C167.1 228.8 137.333 222.5 129 224L145.5 214C149.5 210.4 184.5 230.5 201.5 241C205.9 203.4 251 177.333 273 169C251.4 135 283 116.167 301.5 111C308.7 107.4 404.5 96.1665 451.5 90.9997C464.3 86.5997 465.5 92.833 464.5 96.4997C444.9 129.7 377.334 168.666 346 184Z" fill="white"/>
|
||||
<path d="M299 33.9991C400.2 9.99913 455.167 48.3325 470 70.4991C471 68.3328 473.1 63.7 473.5 62.5C474 61 467 48.5 466 47C465 45.5 459 43 457.5 42C456 41 449.5 37.5 448 36C446.5 34.5 437 32.4987 436 32.4987C435 32.4987 430 28.4987 428.5 28.4987C427 28.4987 424 22.5 422.5 20.9987C421 19.4974 411.5 15 410.5 14C409.5 13 391.5 13 389 13C387 13 381.167 15.9991 378.5 17.4987C374.833 17.9987 366.9 18.6987 364.5 17.4987C361.5 15.9987 334 17.4987 331.5 17.4987C329 17.4987 300 20.9987 299 20.9987C298 20.9987 273 27.4987 270 28.4987C267.6 29.2987 249.667 31.4987 241 32.4987L188.5 65.9987V70.4991C223.3 46.4995 254 40.499 265 40.4987C250.2 48.8987 242.833 65.332 241 72.4987C255 53.2987 285.5 38.8323 299 33.9991Z" fill="white"/>
|
||||
<path d="M273 295.001C268.2 296.601 269.667 300.667 271 302.501C259.001 296.9 259 286.168 260.5 281.502C273.5 276.502 274.5 275.501 275 272.502C275.4 270.102 257.833 270.502 249 271.002C239.8 271.802 224.5 259.335 218 253.002C224 253.002 245.833 256.001 256 257.5C264.8 257.5 278.333 260.833 284 262.5C303.2 218.9 334.667 191 348 182.5C447.6 126.099 466.833 98.9995 464 92.5C466 88.5 454.833 90.8333 449 92.5C379 113.7 318.167 118 296.5 117.5L318.5 107C338.899 107 428.333 82.3333 470.5 70C469.7 44.8 439.834 32.5 425.001 29.5C407.001 11.5 385.834 16 377.501 20.5C341.901 19.3 323.334 21 318.5 22C226.1 32.8 185.333 67.1667 176.5 83C206.1 80.2 220.167 85.8333 223.5 89C213.5 90.6 209.667 99.3333 209 103.5C206.2 108.3 201.5 129.5 199.5 139.5L188.501 145.5C96.9026 141.9 49.001 215.668 36.5 253.002C15.7 329.003 27.8333 401.334 36.5 428C74.9 542.799 158.167 574.5 195 576C276.6 586.8 339.333 541.167 360.5 517L358.5 510C389.7 488.4 403.167 450 406 433.5C410.799 391.9 382.666 377.833 368 376C397.999 417.999 367.834 457.833 349.001 472.5C289.226 524.9 200.428 504 163.5 487C43.5 421 68.1667 314.834 95.5 270.002C95.9 203.203 141 199.834 163.5 206.5C140.7 210.9 125 227 120 234.5C97.2 262.5 125.833 278.5 143 283.001C145 281.8 167.5 290.5 178.5 295.001C179.7 293.001 193 304.501 199.5 310.501L202 309.501C207.6 295.503 218.667 301.334 223.5 306L246 348.5C248.8 360.1 242.167 362 238.5 361.5C225.3 361.9 210.667 337.667 205 325.5L206.5 350C207.7 360 202.333 363.167 199.5 363.5C185.9 363.5 180.833 340.167 180 328.5C182 313.7 173.167 311.667 168.5 312.5C156.1 322.899 153 357.5 153 373.5C157 445.899 206.667 474.333 231 479.5C311.8 500.7 354.334 451.333 365.501 424C369.101 377.2 332.334 364.167 313.5 363.5C329.899 351.1 348.667 350.667 356.001 352C422.401 360 433.001 425.667 430.001 457.5C409.201 575.899 293.667 607.833 238.5 609C96.1 601.4 31.1676 490.5 16.5015 436C-33.0985 282.8 46.1681 182.833 92.0015 152C124.001 130.4 159.668 126.333 173.501 127C185.501 127.8 188.501 122 188.501 119C192.901 106.2 165.668 105.667 151.501 107C134.701 109 134.501 98.1667 136.501 92.5C202.101 14.1 314.501 5.49998 362.501 11C404.501 -15 432.667 12.5 441.501 29.5C481.101 33.1 489.667 60 489.001 73C486.201 106.2 465.834 129.5 456.001 137C449.201 145 419.167 165.333 405.001 174.5L344.001 209L344.501 211C356.501 219 371.501 242.334 377.501 253.002C386.301 251.402 402.167 254.335 409.001 256.002C419.001 260.002 427.167 267.002 430.001 270.002V275.002C429.601 276.202 426.501 277.168 425.001 277.502C422.601 278.702 407.334 273.668 400.001 271.002H398.001V272.502L409.001 281.502C415.801 287.102 420.501 295.835 422.001 299.501C424.001 305.102 422.167 308.501 421.001 309.501C418.201 312.702 414.501 311.501 413.001 310.501L386.001 284.502L385.001 284.002L392.001 303.001C394.001 307.001 394.167 315.335 394.001 319.001C391.201 331.001 384.167 327.001 381.001 323.501L365.501 286.501L365.001 310.501C363.801 318.901 358.501 322.335 356.001 323.001C351.201 323.001 349.334 320.335 349.001 319.001C347.401 316.202 346.667 301.168 346.501 294.001C344.101 286.801 334.834 281.001 330.501 279.001C322.501 274.201 302.167 274.667 293.001 275.501C293.001 279.901 292.334 282.334 292.001 283.001L273 295.001Z" fill="#003625"/>
|
||||
<path d="M255 296.5C239.8 285.3 233.667 289.5 232.5 293C226.1 301.8 235.167 312.667 240.5 317C251.3 327 272.667 334.167 282 336.5C295.6 337.3 296 327.833 294.5 323C290.1 316.2 266.333 302.5 255 296.5Z" fill="#003625"/>
|
||||
<path d="M283.499 263.5C278.299 258.7 255.999 254.5 245.499 253C241.899 249.401 294.666 208.834 321.499 189L303.499 185.5C331.899 183.5 362.333 153.334 373.999 138.5C417.499 142 460.999 87.5004 462.999 90.5004C464.599 92.9004 464.333 95.1671 463.999 96.0004C448.799 127.2 382.666 165 351.499 180C321.499 194.8 293.666 241.834 283.499 263.5Z" fill="#EEEDE8"/>
|
||||
<path d="M201.5 147L198.5 139C191.7 143.8 184.5 143.833 182 144.5C141.6 139.3 104.5 164.5 91 176.5C37.8 218.1 21.6667 289.833 21 320.5C36.6 234.1 88.8333 186.5 112 173.5C135.2 156.3 171.667 153 187 153.5C189.8 153.5 197.833 149.167 201.5 147Z" fill="white"/>
|
||||
<path d="M331.248 39C354.848 39.4 364.414 59.5 366.248 69.5C364.081 68.8333 359.848 67.6 360.248 68C360.648 68.4 359.415 72.1667 358.748 74C352.748 89.2 336.582 93 329.248 93C311.248 92.2 303.081 77.3333 301.248 70C298.448 46 320.081 39.3333 331.248 39Z" fill="#003625"/>
|
||||
<path d="M310.749 63.4997C316.749 48.2997 331.582 50.1664 338.249 52.9997C325.849 55.7994 323.748 58.4998 324.248 59.5C329.448 70.3 323.748 73.3333 320.249 73.5C311.849 73.5 310.415 66.8331 310.749 63.4997Z" fill="white"/>
|
||||
<circle cx="226" cy="107" r="10" fill="#003625"/>
|
||||
<circle cx="226" cy="107" r="10" fill="#003625"/>
|
||||
<circle cx="432.5" cy="60.5" r="7.5" fill="#003625"/>
|
||||
<circle cx="432.5" cy="60.5" r="7.5" fill="#003625"/>
|
||||
<circle cx="209" cy="147" r="10" fill="#003625"/>
|
||||
<circle cx="209" cy="147" r="10" fill="#003625"/>
|
||||
<circle cx="214.5" cy="126.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="214.5" cy="126.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="220.5" cy="77.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="220.5" cy="77.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="228.5" cy="59.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="228.5" cy="59.5" r="4.5" fill="#003625"/>
|
||||
<ellipse cx="213" cy="63.5" rx="3" ry="3.5" fill="#003625"/>
|
||||
<ellipse cx="213" cy="63.5" rx="3" ry="3.5" fill="#003625"/>
|
||||
<ellipse cx="198.5" cy="74.5" rx="5.5" ry="3.5" fill="#003625"/>
|
||||
<ellipse cx="198.5" cy="74.5" rx="5.5" ry="3.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="169.5" cy="159.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="169.5" cy="159.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="189.5" cy="161.5" r="4.5" fill="#003625"/>
|
||||
<circle cx="133.5" cy="173.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="133.5" cy="173.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="117.5" cy="187.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="117.5" cy="187.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="102.5" cy="186.5" r="3.5" fill="#003625"/>
|
||||
<circle cx="102.5" cy="186.5" r="3.5" fill="#003625"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 9.6 KiB |
39
Back-End/test_all_endpoints.py
Normal file
39
Back-End/test_all_endpoints.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""
|
||||
Starter de tests automatiques pour valider tous les endpoints définis dans les fichiers urls.py du projet Django N3WT-SCHOOL.
|
||||
Chaque endpoint est testé pour la réponse HTTP attendue (200, 401, 403, 404, etc.).
|
||||
"""
|
||||
import pytest
|
||||
from django.urls import reverse, resolve
|
||||
from rest_framework.test import APIClient
|
||||
from django.conf import settings
|
||||
from django.urls import get_resolver
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAllEndpoints:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def get_all_url_patterns(self):
|
||||
resolver = get_resolver()
|
||||
patterns = resolver.url_patterns
|
||||
urls = []
|
||||
def extract(patterns, prefix=""):
|
||||
for p in patterns:
|
||||
if hasattr(p, 'url_patterns'):
|
||||
extract(p.url_patterns, prefix + str(p.pattern))
|
||||
else:
|
||||
urls.append(prefix + str(p.pattern))
|
||||
extract(patterns)
|
||||
return urls
|
||||
|
||||
def test_all_endpoints_anonymous(self):
|
||||
urls = self.get_all_url_patterns()
|
||||
for url in urls:
|
||||
if '<' in url: # skip dynamic urls for starter
|
||||
continue
|
||||
try:
|
||||
response = self.client.get(url)
|
||||
assert response.status_code in [200, 401, 403, 404]
|
||||
except Exception as e:
|
||||
print(f"Erreur sur {url}: {e}")
|
||||
124
Back-End/test_auth_endpoints.py
Normal file
124
Back-End/test_auth_endpoints.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""
|
||||
Tests automatiques pour les endpoints Auth de l'API N3WT-SCHOOL.
|
||||
- Teste les endpoints GET, y compris dynamiques.
|
||||
- Teste l'authentification (login JWT) et l'accès aux endpoints protégés.
|
||||
- Vérifie la structure JSON des réponses principales.
|
||||
"""
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from Auth.models import Profile, ProfileRole
|
||||
from Establishment.models import Establishment
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAuthEndpoints:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, db):
|
||||
self.client = APIClient()
|
||||
# Création d'un établissement de test
|
||||
self.establishment = Establishment.objects.create(
|
||||
name="Etablissement Test",
|
||||
address="1 rue du test",
|
||||
total_capacity=100,
|
||||
establishment_type=[1],
|
||||
evaluation_frequency=1,
|
||||
licence_code="LIC123",
|
||||
is_active=True
|
||||
)
|
||||
# Création d'un utilisateur de test
|
||||
self.test_email = 'testuser@example.com'
|
||||
self.test_password = 'testpass123'
|
||||
self.profile = Profile.objects.create(
|
||||
email=self.test_email,
|
||||
username=self.test_email,
|
||||
password=make_password(self.test_password)
|
||||
)
|
||||
self.profile_role = ProfileRole.objects.create(
|
||||
profile=self.profile,
|
||||
role_type=1, # ADMIN
|
||||
establishment=self.establishment,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
def test_csrf(self):
|
||||
response = self.client.get('/Auth/csrf')
|
||||
assert response.status_code == 200
|
||||
assert 'csrfToken' in response.json()
|
||||
|
||||
def test_login(self):
|
||||
response = self.client.post('/Auth/login', {
|
||||
'email': self.test_email,
|
||||
'password': self.test_password
|
||||
}, format='json')
|
||||
assert response.status_code in [200, 401]
|
||||
if response.status_code == 200:
|
||||
assert 'access' in response.json() or 'token' in response.json()
|
||||
|
||||
def test_profiles(self):
|
||||
# GET /Auth/profiles
|
||||
response = self.client.get(f'/Auth/profiles')
|
||||
assert response.status_code in [200, 401, 403]
|
||||
if response.status_code == 200:
|
||||
# Vérifie que le profil de test existe dans la liste
|
||||
emails = [p.get('email') for p in response.json() if isinstance(p, dict)]
|
||||
assert self.test_email in emails
|
||||
|
||||
def test_profiles_id(self):
|
||||
# GET /Auth/profiles/<id>
|
||||
response = self.client.get(f'/Auth/profiles/{self.profile.id}')
|
||||
assert response.status_code in [200, 401, 403, 404]
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert data.get('email') == self.test_email
|
||||
|
||||
def test_profile_roles(self):
|
||||
# GET /Auth/profileRoles avec paramètres requis
|
||||
params = {
|
||||
'establishment_id': self.establishment.id,
|
||||
'filter': 'school'
|
||||
}
|
||||
response = self.client.get('/Auth/profileRoles', params)
|
||||
assert response.status_code in [200, 401, 403, 400]
|
||||
if response.status_code == 200:
|
||||
results = response.json()
|
||||
# Adapter à la structure réelle de la réponse : clé 'profilesRoles'
|
||||
if isinstance(results, dict) and 'profilesRoles' in results:
|
||||
results = results['profilesRoles']
|
||||
found = any(
|
||||
r.get('profile') == self.profile.id and r.get('role_type') == 1
|
||||
for r in results if isinstance(r, dict)
|
||||
)
|
||||
assert found
|
||||
|
||||
def test_profile_roles_id(self):
|
||||
# GET /Auth/profileRoles/<id>
|
||||
response = self.client.get(f'/Auth/profileRoles/{self.profile_role.id}')
|
||||
assert response.status_code in [200, 401, 403, 404]
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert data.get('profile') == self.profile.id
|
||||
assert data.get('role_type') == 1
|
||||
|
||||
def test_reset_password(self):
|
||||
# POST /Auth/resetPassword/<code> (méthode attendue)
|
||||
response = self.client.post('/Auth/resetPassword/ABCDEF', {
|
||||
'password1': 'newpass123',
|
||||
'password2': 'newpass123'
|
||||
}, format='json')
|
||||
assert response.status_code in [200, 400, 404]
|
||||
# 400 attendu si le code est invalide ou expiré
|
||||
|
||||
def test_info_session(self):
|
||||
# GET /Auth/infoSession (protégé)
|
||||
login = self.client.post('/Auth/login', {
|
||||
'email': self.test_email,
|
||||
'password': self.test_password
|
||||
}, format='json')
|
||||
if login.status_code == 200 and ('access' in login.json() or 'token' in login.json()):
|
||||
token = login.json().get('access') or login.json().get('token')
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
|
||||
response = self.client.get('/Auth/infoSession')
|
||||
assert response.status_code in [200, 401, 403]
|
||||
else:
|
||||
pytest.skip('Impossible de s’authentifier pour tester infoSession')
|
||||
267
CHANGELOG.md
267
CHANGELOG.md
@ -0,0 +1,267 @@
|
||||
# Changelog
|
||||
|
||||
Toutes les modifications notables apportées à ce projet seront documentées dans ce fichier.
|
||||
|
||||
### [0.0.3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/compare/0.0.2...0.0.3) (2025-06-01)
|
||||
|
||||
|
||||
### Corrections de bugs
|
||||
|
||||
* Ajout d'un '/' en fin d'URL ([67cea2f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67cea2f1c6edae8eed5e024c79b1e19d08788d4c))
|
||||
|
||||
### 0.0.2 (2025-06-01)
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* mise à jour de la doc swagger ([11fc446](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/11fc446b904cc64d63154ad5c6711a8296a7fc51))
|
||||
|
||||
|
||||
### Refactorisations
|
||||
|
||||
* "registerFilesTemplates" -> "registrerFileTemplate" ([83f4d67](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/83f4d67a6fc3f786803343957b276f8419f3058d))
|
||||
* adaptation mobile ([4b8f85e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4b8f85e68dc95585d96a4cbad219ad068cbc8acf))
|
||||
* Affichage des notifications dans la partie "Users" ([af30ae3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/af30ae33b5660c55fa6824498f4325aab3de3c5a))
|
||||
* Affichage des notifications dans la partie "Users" ([e509625](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e5096258110faac37b9457705dd1b51bc231983f))
|
||||
* Augmentation du nombre de données ([95c154a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/95c154a4a2d746c4350887bb697af142152ed8d7))
|
||||
* changement de la philosophie de logging ([c7723ec](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c7723eceee650de86eea3263d44d374ad9844282))
|
||||
* Changement des IconTextInput en TextInput, modification du composant step ([a248898](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a248898203286213c3447333611e1a9981dff64a))
|
||||
* Composant *InscriptionForm* ([56e2762](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/56e27628f897920659a6ce186539ddec7e94a05a))
|
||||
* Creation d'un provider et d'un systeme de middleware ([5088479](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/508847940c8c35fd982ab935f4d69371869eed5a))
|
||||
* Création de composants et uniformisation des modales ([#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)) ([d51778b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d51778ba54e95283aa6ad7821fda673813c7c7a0))
|
||||
* Création de nouveaux composants / update formulaire de ([7acae47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7acae479da658707fb3e073ebcdfee023d18500b)), closes [#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)
|
||||
* Deplacement du JWT dans le back ([eb89a32](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb89a324abbdf69091e5c78530ec62f2c2ccbcd1))
|
||||
* Document Ecole/Parent ([7564865](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7564865d8f414fbefa0731c4ca472a100efb6036))
|
||||
* gestion des erreurs ([f3490a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f3490a4e9584b959450ca45c8e74e430396425b3))
|
||||
* Injection des env var dans le frontend ([aae5d27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/aae5d27d8c556c5687951f3a04e01d42f69f3085))
|
||||
* je suis une merde ([c4d4542](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c4d45426b520a498f409be8617c7936224195290))
|
||||
* Mise à jour de la doc swagger / URL ([4c95b6a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c95b6a83f15a9989fac5f69a9386664d25ec9f6))
|
||||
* Modification de l'url de l'api Auth ([9bf9c5f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9bf9c5f62df1a6482ba27b897da498592b57e04f))
|
||||
* Modification de la construction docker ([2d128aa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2d128aaf30e60813c0c5caa244a93ff46e3985f3))
|
||||
* Partie "School" ([58fe509](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/58fe509734a3b5dc6e0b5c6aa3fd713fd4dc821e))
|
||||
* Partie FRONT / School ([24352ef](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/24352efad304dee7418dc846681a4b38047431f6))
|
||||
* Refactoring de la section ClassSection ([1a8ef26](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1a8ef26f5883abe4855949a54aa50defb98c852d))
|
||||
* refactoring du FRONT page subscribe ([427b6c7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/427b6c758892469d07579159511e7ce1ceed20d0))
|
||||
* Refactorisation du login et de admin/subscription ([41aa9d5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/41aa9d55d388c0ddf189c7b9ab6057487f86484b))
|
||||
* Remplacement de quelques popup par les notifications ([ce83e02](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ce83e02f7b3e53ef2b859436432784d6eb69200d))
|
||||
* Renommage du menu "Eleves" en "Inscriptions" ([692e845](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/692e8454bf9840ada3f8e052d7ef13cbf1b0d9c0))
|
||||
* Revue de la modale permettant de créer un dossier ([cb3f909](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb3f909fa4e7a53148cd13cf190c13b0670d35de))
|
||||
* Revue de la modale permettant de créer un dossier ([665625e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/665625e0280683fef056e9c950fc6555d889643e))
|
||||
* SpecialitySection + TeacherSection (en cours) ([72dd769](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/72dd7699d6bd61e17b4c3dc0098ca0989a94b2c8))
|
||||
* Suppression des paramètres mail mot de passes des settings ([ec2630a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ec2630a6e40dedaaa8f41a04b44e5ec1f6b2a1e0))
|
||||
* Traduction en anglais des modules "GestionInscription" et ([2b414b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2b414b83913c2f0f81cf226b78577ad522443d7b))
|
||||
* Transformation des requetes vers le back en action ajout des ([147a701](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/147a70135d2f10ac16961c098d85da0a1bcafb38))
|
||||
* Utilisation d'une application "Common" pour tous les modèles ([e65e310](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e65e31014d2b89afe1e5f077e8d4109f07d40d0b))
|
||||
|
||||
|
||||
### Nouvelles fonctionnalités
|
||||
|
||||
* A la signature d'un document, on récupère l'URL du PDF [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([2ac4832](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2ac48329851f91f6bb02a44e02ad5a90b4ae504c))
|
||||
* Affichage d'icones dans le tableau des inscriptions dans la ([9559db5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9559db59eb418d233682217ef72f315bccc6fe1d))
|
||||
* Ajout d'un composant permettant de visualiser les fichiers signés ([7f442b9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f442b9cae008dab4f18438f9ee46be21ed037b0))
|
||||
* Ajout d'un nouveau status avec envoi de mandat SEPA + envoi de ([4c2e2f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4c2e2f87565dc6e2be501839c274a5aa6969a9ec))
|
||||
* Ajout d'un nouvel état dans l'automatique lorsqu'un mandat SEPA ([545349c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/545349c7db7d0f653f3ae06b10d441ef975b0cc0))
|
||||
* Ajout d'une colonne dans le tableau des pièces jointes indiquant ([3c0806e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c0806e26c116dbccd808bd8c8b170c5c4d9bc5b))
|
||||
* Ajout d'une fonction de dissociation entre un responsable et un ([3bcc620](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3bcc620ee103690a2ee5f79e6203aba880bda9b7))
|
||||
* Ajout d'une fonction de logout ([c2bba1a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c2bba1abbfafbb7aca1bb07e8019d7fa244a808e))
|
||||
* Ajout d'une fonction de logout ([0ef6a2b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0ef6a2b1192dbd3ecc59ce0e8cbba233ccc9c821))
|
||||
* Ajout de l'emploi du temps sur la page parent ([78d96f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/78d96f82f91ed777073250b960eee8f326cccb43))
|
||||
* Ajout de l'envoie de mail [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([99a882a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/99a882a64acfd9340d6849edd1766de5173a2341))
|
||||
* Ajout de l'option d'envoi automatique [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([a77dd8e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a77dd8ec64bd78ab9c42aad3f93a181e64719d06))
|
||||
* Ajout de la configuration des tarifs de l'école [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([5a0e65b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5a0e65bb752a80781517394d7b2a673788f7595e))
|
||||
* Ajout de la fratrie [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([4a382d5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4a382d523ccfd4cf8fa7e672e9315b86dbdbbb14))
|
||||
* Ajout de la fratrie / Gestion des index de fratrie / Gestion des ([2ab1684](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2ab1684791377804dd03c8467a94dbc1244e102f))
|
||||
* Ajout de la gestion des fichier d'inscription [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([3c27133](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c27133cdb9943c5e20b81c03f9e2fa47077dbbb))
|
||||
* Ajout de la photo pour le dossier de l'élève + correction ([5851341](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5851341235998647a4142bdf1996ddc9db21762d))
|
||||
* Ajout de la possibilité de supprimer une association ([c9350a7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9350a796b65ea4eef0e38390ab9fb1d88196210))
|
||||
* Ajout de la sélection des modes de paiements / refactoring de ([5a7661d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5a7661db93454b9a73b9f6bd46646c6135a0f203))
|
||||
* Ajout des Bundles de fichiers [[#24](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/24)] ([ffc6ce8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ffc6ce8de835e9caf547b6c4a893436aa93513ba))
|
||||
* ajout des documents d'inscription [[#20](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/20)] ([b8ef34a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b8ef34a04b14c8a8fb980fcd9255296ceb699ec6))
|
||||
* Ajout des évenements à venir ([c03fa0b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c03fa0ba42d69918501beb5bb98637a449eb2da0))
|
||||
* Ajout des frais d'inscription lors de la création d'un RF [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([ece23de](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ece23deb19483c50d9999541a482e3378db19d23))
|
||||
* Ajout des frais de scolarité dans le dossier d'inscription [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([0c2e0b9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0c2e0b92f43f223adc22db36ecad7fd864737a98))
|
||||
* Ajout des modes de paiements + création d'une commande dans le ([0c5e3aa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0c5e3aa0988a16b6f9f8c0b411c2c1b443c972a7))
|
||||
* Ajout des payementPlans dans le formulaire / ajout de la photo ([d37aed5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37aed5f6496b8c8ca5519689dfc811d9626e09e))
|
||||
* Ajout du logo de l'école ([6a0b90e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6a0b90e98fbcc707756ae7fbbff921e480f2c695))
|
||||
* Ajout du logo N3wt dans les mails ([8a71fa1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8a71fa1830d0c0fb11467208bc98dc4f71598199))
|
||||
* Ajout du suivi de version dans le footer du Front ([fb7fbaf](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fb7fbaf8394ebf41e6f3f31897e6d009c537a481))
|
||||
* Amélioration de la fiche élève pour y ajouter la fratrie et les ([256f995](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/256f995698e79572eb3d51ea60b96b6fad47d553))
|
||||
* Amélioration du dashboard ([eb48523](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb48523f7d466698faa268b8b25e6f1ed90bdfd7))
|
||||
* Amorçage de la gestion des absences [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([cb4fe74](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cb4fe74a9e316a92c6b5e1d2550aaf2b1036a744))
|
||||
* Aussi pour la table des parents tant qu'à faire ([a3182c0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a3182c0ba7c6ea9f99a4fe34a4a00079b4676d59))
|
||||
* **backend:** Ajout du logger django [[#7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/7)] ([b8511f9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b8511f94b633b9bf5bd764b3706c53b74b3a6648))
|
||||
* Bilan de compétence d'un élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([5760c89](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5760c89105f38f4481e2cc6fa788bb0c39e8caa8))
|
||||
* Champ de recherche de l'élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([eb7805e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb7805e54e41f6eaefad81fea1616f0613365e8c))
|
||||
* Configuration des compétences par cycle [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([4e5aab6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4e5aab6db74a8d1dfdfb4928f60ad47da52c89e8))
|
||||
* Configuration et gestion du planning [[#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)] ([830d9a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/830d9a48c003e1cca469b1cf4082305e16685181))
|
||||
* Création d'un annuaire / mise à jour du subscribe ([6bd5704](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bd5704983282264bc50c73677495740f7d7e8a9))
|
||||
* Création d'un profile selector [[#37](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/37),[#38](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/38)] ([89b01b7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/89b01b79db884c393db29332b95f570e47d20ed1))
|
||||
* création d'une tooltip pour les informations supplémentaires de ([9197615](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/91976157e44e319b23fd35fa89859164bab71202))
|
||||
* création de 4 JSON de compétences en attendant de les mettre en ([69405c5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/69405c577e7af3d07654fca96015d21f475e700d))
|
||||
* Création de clones lors de la création de RF [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([d1a0067](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d1a0067f7b7125e453ff6fc75efead881a7af37d))
|
||||
* Création nouveau style / pagination profils annuaires ([760ee00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/760ee0009e983776dfd500df7465ae66593dc85d))
|
||||
* Dockerisation d'un serveur docuseal + initialisation d'un compte ([8897d52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8897d523dc23fd2d89a0ec66b5cc7fa15b69db5b))
|
||||
* Envoie d'un mail de bienvue au directeur ([5be5f9f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5be5f9f70d4fcf56da29afb19187806ff2e6e428))
|
||||
* Evolution des modèles pour intégrer un planning et du m2m ([85d4c00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/85d4c007cb2091ae1911ca1998f1b830470b8310))
|
||||
* Formulaire de création RF sur une seule pag ([76f9a7d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/76f9a7dd14d7065f4add01718fda499fbb9183c7))
|
||||
* Génération d'une page de suivi pédagogique + fix utilisation ([2a6b3bd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2a6b3bdf63ddc13509b66690ea5d76eac77d1090))
|
||||
* Génération du bilan de compétence en PDF [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([0fe6c76](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0fe6c761892097d043902f4f051b9fdb5fef29d0))
|
||||
* Gestion de la création d'un nouveau guardian, de l'association ([fb73f9e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fb73f9e9a86430d7498aa8a10e5abc46325b7b2c))
|
||||
* Gestion de la mise à jour des profiles / roles / lors de l'édition ([dfd707d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dfd707d7a0c7f4514f5583f07803d20e3c2d6bd7))
|
||||
* Gestion de la sauvegarde du fichier d'inscription / affichage du ([d6edf25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d6edf250bbc1cc1a9862e26174bc24ca4f9ee4c1))
|
||||
* Gestion de la validation du dossier d'inscription ([b23264c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b23264c0d4008a4317c009a73ae11f57ee6917e2))
|
||||
* Gestion des absences du jour [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([030d19d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/030d19d411af8f0d87a3cb72cb401d9dd5fa96ce))
|
||||
* Gestion des documents nécessitant des signatures électroniques et ([e3879f5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e3879f516b81b7e4b784049668b2507f12e8155f)), closes [#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)
|
||||
* Gestion des documents parent ([59aee80](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/59aee80c2e7592a7cdb119d1d30a5ad2c8bb20b0))
|
||||
* Gestion des documents signés durant l'inscription / possibilité de ([905b95f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/905b95f3a364f0d1ce8348d086870045d942bf92))
|
||||
* gestion des no data dans les table [[#33](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/33)] ([2888f8d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2888f8dcce8d593df8f81a635eaac94af4603829))
|
||||
* Gestion des pièces à fournir par les parents (configuration école) ([a65bd47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a65bd47905cc33c44416c1def0413579b96d820d))
|
||||
* Gestion des profils ADMIN/ECOLE (création des enseignants) ([e0bfd3e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e0bfd3e11579c512aa6ad63c73e00e40be4eaf06))
|
||||
* Gestion des profils des enseignants / Visualisation d'une classe [[#4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/4)] ([81d1dfa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/81d1dfa9a70d0cd8d80e7d951a74c9355bba5238))
|
||||
* Gestion des rattachements de Guardian à des RF déjà existants ([7d1b9c5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7d1b9c5657439d2fff287f60b9aba79a5dfdf089))
|
||||
* Gestion du planning [3] ([58144ba](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/58144ba0d0f1b53e9313f4cd4d3fbc3e6bfdd274))
|
||||
* Gestion multi-profil multi-école ([1617829](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/16178296ec4dd4843be26b6e09b9c0f080df7ee4))
|
||||
* Harmonisation des fees / ajout de type de réduction / mise à jour ([5462306](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5462306a6020493cf747ea3bb8edb3240c36286f)), closes [#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)
|
||||
* Merge remote-tracking branch 'origin/WIP_style' into develop ([f887ae1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f887ae18862b740fa904d8ca04a3932eec455908))
|
||||
* Messagerie WIP [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([23a593d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23a593dbc77b6f544a17de5a451ff60316f50292))
|
||||
* Mise à jour des Dockerfile préparation d'un environnement de démo [[#12](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/12)] ([32a77c7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/32a77c780abe8c0aa9846843ac81d13e4b8cf73a))
|
||||
* Mise à jour des Teacher ([173ac47](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/173ac47fb26ba2f101802571621fc4112adb1a9f))
|
||||
* Mise à jour du modèle (possibilité d'associer une réduciton à un ([8d1a41e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8d1a41e2693c3704b68e8d75bd32c4a89a6389e5)), closes [#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)
|
||||
* mise en place de la messagerie [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([d37145b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37145b73e2012f21a329ee97a565189233ca0f8))
|
||||
* Mise en place des actions pour chaque state du RF, possibilité ([8fc9478](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8fc947878665ca04e1697fa6df140e0d80c5a672))
|
||||
* Mise en place des paiements en plusieurs fois - partie BACK [[#25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/25)] ([274db24](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/274db249aa25f2a0281638c318a68cf88a721a45))
|
||||
* Mise en place des paiements en plusieurs fois (partie BACK) [[#25](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/25)] ([23203c0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23203c0397f6247d32462cceca33d964898223a9))
|
||||
* Mise en place du Backend-messagerie [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([c6bc0d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c6bc0d0b515a5be7b0bf930ff628a5e9b5ebbb33))
|
||||
* Nommage des templates / Intégration dans formulaire d'inscription ([eb81bbb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eb81bbba9265b9f3a71e500737436ee5301b7a5e)), closes [#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)
|
||||
* Ordonnancement de l'inscription sur plusieurs pages + contrôle des ([daad12c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/daad12cf40ce3b628581892f9a894a0841baa5e3))
|
||||
* Oubli fichier [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([d7fca9e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d7fca9e942412a8d2fe379f38052f3b41ed9c0f9))
|
||||
* passage des mail au format HTML ([b97cf6e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/b97cf6e02b92ba6750662bbf9e9c3af6ad19ab38))
|
||||
* Passage par une variable d'environnement pour les CORS et CSRF ([f9e870e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f9e870e11fe3041be53f4c357427d8060f50199f))
|
||||
* Peuplement de la BDD avec les JSON d'entrée [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([c6d7528](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c6d75281a1fab0d9dc27d4da80f91c6fffb1bc0e))
|
||||
* planning events ([c9b0f0d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9b0f0d77a5ec61a239deb71959738f3b0e82d37))
|
||||
* Pre cablage du dashboard [#] ([1911f79](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1911f79f4578f8bc3b455308182c46d2d59e5580))
|
||||
* Préparation de la gestion des compétences en énumérant les élèves ([1c75927](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1c75927bbab497cfc86fc3a9aea11d436318be69)), closes [#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)
|
||||
* Preparation des modèles Settings pour l'enregistrement SMTP [[#17](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/17)] ([eda6f58](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eda6f587fb21bf041784209228518c8a6f03b1b5))
|
||||
* preparation du dockerfile pour le frontend [[#13](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/13)] ([9716373](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9716373fa2d754177d4e71082b9079b71daab971))
|
||||
* Rattachement d'un dossier de compétences à une période scolaire ([7de839e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7de839ee5c9b09f7874575bdaf57436ec11b293f)), closes [#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)
|
||||
* Refactoring de la fonction de création de profil sur guardian côté ([753a8d6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/753a8d647ec3e45c8aabecba6d38b1a19741e0c0))
|
||||
* Sauvegarde des compétences d'un élève [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([0513603](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05136035ab1d811c904e35d99ddb884c68b7fd74))
|
||||
* Sauvegarde des fichiers migration ([017c029](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/017c0290dd1fab8afa3a05541e57a321733ff5c9))
|
||||
* Signatures électroniques docuseal [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([c8c8941](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c8c8941ec875b541cfb55c3504a0e951f36163ef))
|
||||
* Sortie des calculs des montants totaux de la partie configuration + revue du rendu [[#18](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/18)] ([799e1c6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/799e1c6717fceec4b29edfbdd0af52268b7e8fce))
|
||||
* Suite de la gestion des sessions ([8ea68bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8ea68bbad0646d99209d1821a2b71364630005b3))
|
||||
* Suppression de l'ancienne POPUP de RF ([5927e48](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5927e48e6544e8819a29766562107834d44e7a5d))
|
||||
* Suppression des localStorage ([023b46e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/023b46e16e8da56971a8c55c0930e6ab4fbf53ec))
|
||||
* Suppression des templates docuseal [[#22](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/22)] ([081dc06](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/081dc060014ee15ca6881fc83b779679a271326d))
|
||||
* Upload du SEPA par les parents / Création d'un composant header ([8417d3e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8417d3eb141b116e2e9f8c6038831ce1bbe30e2a))
|
||||
* Utilisation d'une clef API Docuseal par établissement ([23ab7d0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/23ab7d04ef0c940b7008e8bc7d4b43b373d16d40))
|
||||
* Utilisation de l'établissement en variable de session / gestion de ([f2ad1de](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2ad1de5a4215395f3aa7a0e04ac2eb3edc5ec51))
|
||||
* Utilisation des nouvelles alertes dans la page admin de la gestion ([67193a8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/67193a8b3601784f37563094f3fdede943523b53))
|
||||
* Validation du dossier d'inscription en affectant l'élève à une ([0f49236](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/0f49236965f575f6af17837b9860fa4481227785))
|
||||
|
||||
|
||||
### Corrections de bugs
|
||||
|
||||
* correction des redirections vers la login page ([2e0fe86](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2e0fe86c71e9f02e8ee77ccbd80533a63a31ef63))
|
||||
* Ajout d'un champ is_required pour les documents parents facultatifs ([5866427](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5866427544e24c8e79cb773d38bda683f63f4531))
|
||||
* Ajout d'un message de confirmation lors de la suppression d'un ([9248480](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/92484804f6483eab401612315b5513cc78e6a726))
|
||||
* ajout de credential include dans get CSRF ([c161fa7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c161fa7e7568437ba501a565ad53192b9cb3b6f3))
|
||||
* Ajout de l'établissement dans la requête KPI récupérant les ([ada2a44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ada2a44c3ec9ba45462bd7e78984dfa38008e231))
|
||||
* Ajout des niveaux scolaires dans le back [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([05542df](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05542dfc40649fd194ee551f0298f1535753f219))
|
||||
* ajout des urls prod et demo ([043d93d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/043d93dcc476e5eb3962fdbe0f6a81b937122647))
|
||||
* Ajout du % ou € en mode édition de réduction ([f2628bb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f2628bb45a14da42d014e42b1521820ffeedfb33))
|
||||
* Ajout du controle sur le format des dates ([e538ac3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e538ac3d56294d4e647a38d730168ea567c76f04))
|
||||
* Ajout du mode Visu ([e1c6073](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1c607308c12cf75695e9d4593dc27ebe74e6a4f))
|
||||
* ajustement du handlePhoneChange [[#41](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/41)] ([31fdc61](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/31fdc612b10843ce694b55696f67bd2a80d56769))
|
||||
* Application des périodes à un studentCompetency lors de la création ([d65b171](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d65b171da8a310acca15936a39e44239763c88b9))
|
||||
* application des recommandations linter es pour générer un build de prod ([d1aa8b5](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d1aa8b54fb71bb946e95a19105f51f7f29c75fda))
|
||||
* Application du formattage sur les fichiers modifiés ([001a5bc](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/001a5bc83c0bf54061b2b04967da3fc11e2cd8dc))
|
||||
* boucle inifinie dans UseEffect ([f3c4284](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f3c428477879729d36760bb61dac015311c84fec))
|
||||
* Bug lorsqu'on déselectionne un paiementPlan ([d64500f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d64500f4022710423c77d023476065816ecd061d))
|
||||
* build error ([65d5b8c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/65d5b8c424bf0e0a9da1b39500c8252f683725c7))
|
||||
* Calcul du montant total des tarif par RF + affichage des tarifs ([c269b89](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c269b89d3d58cc65f254b75f6d713c4fd15f6320)), closes [#26](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/26)
|
||||
* calcul nombre de pages dans chaque tab ([5440f5c](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5440f5cbdbca8b9435a17914c7e7c4ecc34e6bb3))
|
||||
* Champs requis sur les teachers and classes ([42b4c99](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/42b4c99be86f050ccd76302caf725af5df413d17))
|
||||
* Changement d'icone associé aux documents soumis à validation ([500b6e9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/500b6e9af7ac76dafa35bd830cd0767cece47d27))
|
||||
* code mort ([4fc061f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4fc061fc255b4174f794ac58da1b6849419e9f1a))
|
||||
* Condition de validation d'ajout d'un nouveau document parent / ([9e69790](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9e69790683fd83b0e48a9f70150661cb06a7b556))
|
||||
* conflits + closeModal lors de la création d'un RF ([1617b13](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1617b132c4f67fdbaf261808a0a9596b7a72a4dc))
|
||||
* coquille ([c9c7e77](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c9c7e7715efde8766c3b2ad2c355dc9a9960b19f))
|
||||
* coquille dans les imports ([4ecf25a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4ecf25a6ab90a57da0013f6ed603d6cd5bd4eeeb))
|
||||
* Correction de l'affichage des numéros de téléphone [[#41](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/41)] ([4f774c1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f774c18e47fc57e081022a03ea352638e7211d2))
|
||||
* correction de l'ouverture du dashbord [[#39](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/39)] ([a157d53](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a157d53932d5576fc9768f5c063cf9aafa214d43))
|
||||
* Correction de la désactivation des spécialités lorsqu'on ([afc1632](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/afc1632797c0d35df7da03432eba9ab0f1875f55)), closes [#2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/2)
|
||||
* Correction dépendances circulaires ([fc9a1ed](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fc9a1ed252e1e115e4a2f7c4a3a04ee6757be683))
|
||||
* Correction des Protected Routes avec multi role ([dd0884b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dd0884bbce6b6549f0f3fca991045f7170889710))
|
||||
* correction des refresh des protected routes [[#36](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/36)] ([839a262](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/839a26257b659a86903d3f982548884cc87366b9))
|
||||
* Correction du Establishment context au refresh ([43e301e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/43e301ed641a742323a98c430e30e134babc4aa4))
|
||||
* correction fileGroup lors de l'enregistrement d'un nouveau responsable ([dce2114](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/dce2114a7940310e2c4241c2cdbd7e3fd060fb60))
|
||||
* Correction option fusion ([e61cd51](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e61cd51ce2b0665c18f9497e6d2b1f7b8196723e))
|
||||
* Correction sur le calcul du nombre total de pages [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([5946cbd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5946cbdee661527317dac66f99f0abce021c835a))
|
||||
* correction titre mail reset mdp ([cac1519](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cac1519bf311b660831222d76d4d5165ee4f4d7e))
|
||||
* Correction URL ([170f7c4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/170f7c4fa80e1cd40079ac861e7e633c62f143df))
|
||||
* Corrrection typo dans description des tableaux frais/réduction ([175932f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/175932ffa3fb2747cafd158b8142df9b7010a3d4))
|
||||
* csrf ([59a0d40](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/59a0d401301fe77226fd5f294a3cd7e589d46fad))
|
||||
* Division par 0 ([a42cf34](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a42cf348a0a3cc43c6c6b643b1da158690d67cb8))
|
||||
* double confirmation sur les popup ([677cec1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/677cec1ec2f7a3582327f4747d088c6bccbd2560))
|
||||
* entrypoint access right ([a041ffa](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a041ffaee75b74e0d559fb14bc79fbcfae98da14))
|
||||
* faire plaisir à LSO ([9374b00](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9374b001c9944cb6af1e10451f0e5f236a7890e8))
|
||||
* formulaire sur toute la larguer + initiation à un autre style de bg ([4fd40ac](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4fd40ac5fc90ea1ddda9d73ea290b588074c6e2f))
|
||||
* Fusion documents ([857b8b2](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/857b8b26c3722171007399dc66cd9980b33151c5))
|
||||
* Generation d'une fiche d'élève avec le nouveau modèle PayementMode ([4f40d1f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4f40d1f29d7abd1e0c6bf889b10f811f184ff10d))
|
||||
* Génération uniquement des compétences évaluées dans le PDF ([eca8d7a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/eca8d7a8d59f39313123166859f4c4bf548d150e))
|
||||
* gestion des codes retours ([7f35527](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7f3552764979e098ca2e8c3547354c8ae6feaa23))
|
||||
* Gestion des listes d'inscription "vides" [[#1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/1)] ([edc9724](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/edc97242f2219f48233441b8c7ec97ef9551c60c))
|
||||
* gestion du jour d'échéance ([2576d21](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2576d2173460267664d927bd093580a21c18725b))
|
||||
* import du Loader ([e2a39ff](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e2a39ff74dd9671bb1d00de2b6cec1cd3e4ff614))
|
||||
* inject env var ([fc337b1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fc337b1e0b4605f3490435f4819b01d38f921156))
|
||||
* Limite du nombre de responsables légaux à 2 [[#27](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/27)] ([1ced4a1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/1ced4a10696057b8df114dc95adf9868e8d7aa43))
|
||||
* Link documents with establishments ([2f6d30b](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/2f6d30b85b90508cae49081a82eadea5039f60b2))
|
||||
* load the school image eorrectly ([6bc2405](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bc24055cd5a79d59d2b56c7e767ac1b30d99fff))
|
||||
* Lors de la création d'un clone, on prend le nom de l'élève et pas ([db8e1d8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/db8e1d8ab320222370c64d7b7fde3e43c59921e8))
|
||||
* Messages de retour reset/new password ([4a6b7ce](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/4a6b7ce379747565c77205728e7b0d9c8a7c9585))
|
||||
* Mise à jour correcte du fichier après avoir été signé ([5ea3cbb](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/5ea3cbb0790840d823e799cc64766a99ef5591a9))
|
||||
* Mise à jour des upcomming events ([f93c428](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f93c42825964d91d11af26541eecb9ba5f01e801))
|
||||
* mise à jour settings pour la prod / correction CORS ([25e2799](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/25e2799c0f0e46b1a6d78bcc849cc777e67a01f1))
|
||||
* Mise en page des inscriptions (boutons ajout / barre de recherche) ([cf14431](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cf144310a13fa4cbd01a292002d8a9963acc4598))
|
||||
* Modèle créé 2 fois par erreur ([49907d7](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/49907d7ec8847017115191e937ba9f68350c92bd))
|
||||
* Modification d'un guardian sans changer d'adresse mail (même ([95b449d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/95b449ddfde4160a55237e0c50e6bed604dcdfe5))
|
||||
* Ne pas dissocier de responsable s'il n'y en a pas d'autre rattaché ([ac0672f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ac0672f3349aa2ac62db0e3927658a3f2d66cebf))
|
||||
* Ne pas retourner d'erreur si pas de dossier d'inscription ([be27fe1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/be27fe1232c3808b0a65d5f1b265ef454eb35e74))
|
||||
* Nouvelle amélioration ([8b3f963](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8b3f9637a91fe817c87427015a89ba3e469d525d))
|
||||
* On attend que la session soit mise à jour pour intiialiser le ([ccecd78](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/ccecd78704c3e6db58724401b92dd065a7e733ab))
|
||||
* On commence à la page 1 ([3c62cc9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3c62cc9ad2cfa691ee798d27ee6b377676e50bb7))
|
||||
* On empêche la sauvegarde d'un document à signer tant qu'aucun ([be013f0](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/be013f07864345024320114bd734508a033fd5db))
|
||||
* On ne peut sélectionner que les élèves inscrits [[#16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/16)] ([56c223f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/56c223f3cc0498b4a6619d68f0185c36482c4ec9))
|
||||
* Ordre des guardians lors de leur création / déselection correcte si ([3b667d3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3b667d3b150684a685d7d76cf06d050049ee07cd))
|
||||
* pagination annuaire ([980f169](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/980f169c1d1a46f0d47f4b9ff65fa940ac610023))
|
||||
* PieChart ([fe2d4d4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/fe2d4d45137df3b1ead4d21c29722fec0bd0fbab))
|
||||
* Positionnement de la variable isSepa ([82573f1](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/82573f1b2333a01675cebf575f33ab77e70e138b))
|
||||
* Possibilité d'ajouter un 2ème guardian, même si son mail est ([8cf2290](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8cf22905e533a23ee679107cc0bcae1198badb4a))
|
||||
* Récupération d'un template donné ([9b13d52](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9b13d52e8d8926bcc6f756ff4d2c9d278a0cc387))
|
||||
* Refresh par profil role ([24069b8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/24069b894ef2009d9fe0ad884e7a39c29a5a9504))
|
||||
* refresh token ([053d524](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/053d524a513adfec8bd9b3467fc358c257776a85))
|
||||
* régression CORS_ALLOWED_ORIGINS ([a69498d](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/a69498dd06649b601a16a509c7a80c9f67c7872e))
|
||||
* régression lors de l'uniformisation des modales ([00f7bfd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/00f7bfde4abd10d080dc2035d3607d6c35e7db14))
|
||||
* Remise du message de confirmation supprimé par erreur ([efcc5e6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/efcc5e66722c829dcdf522a6903c616901a14604))
|
||||
* Remise en état du bouton Submit ([e9650c9](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e9650c992e6c8339d7acde4000bf4f3dd8e98bac))
|
||||
* Remise en place de l'API_KEY docuseal dans le back ([6d80594](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6d805940fe5524cae1864c0beebcd136bda84eda))
|
||||
* remove lint error ([aef6c19](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/aef6c193b1eaaedbb0642ce7929b2cfe8f47d682))
|
||||
* Remplacement des enum par des modèles pour les payementModes et les ([7fe5346](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/7fe53465acc2df53351f713ccacd12223d6eff1a))
|
||||
* restore du start.py suite à des tests ([de5f7cd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/de5f7cd41e52b27ee3d8f47cf47fbfdad78216ac))
|
||||
* right ([05f1f16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/05f1f16727c2510385425b23fa6ab98fa62d07be))
|
||||
* Scroll de l'emploi du temps élève ([f38a441](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f38a4414c28ec52981201c15b7eda0dccc1f932f))
|
||||
* searchTerm inscription ([8f0cf16](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/8f0cf16f707ac1cd51f0d619fa1c5ea0ba023f68))
|
||||
* Session storage selectedEstablishmentEvaluationFrequency et selectedEstablishmentTotalCapacity ([e30753f](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e30753f1d6d2911d51bb9dfbf32fbae6f2b62b5d))
|
||||
* Suite du commit précédent ([cd9c10a](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/cd9c10a88af2a05350570c424fb284280c0f65ee))
|
||||
* Suppression d'un profil uniquement s'il ne contient aucun guardian ([330018e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/330018edfdbdc071c15838bc22b8a4e726773204))
|
||||
* Suppression de la top bar admin [[#34](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/34)] ([3990d75](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/3990d75e521bea007a8f479924507498d9586a71))
|
||||
* Suppression de print inutiles ([43874f8](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/43874f8b9e76b3e9f131f240ee895d958cd73fab))
|
||||
* Suppression event planning ([c117f96](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/c117f96e528244ee68ce69b7685880e171976e32))
|
||||
* Unicité des fees + utilisation de l'establishmentID [[#44](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/44)] ([d37e6c3](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/d37e6c384d0ef16053ed9fcc1e979f7f902cc8d8))
|
||||
* Uniformisation des Modales et Popup [[#35](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/issues/35)] ([f252efd](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/f252efdef4fce1f2d19ac7ca1eb9c049706c0d9f))
|
||||
* Utilisation des bonnes colonnes pour les fees et discounts selon si ([9f1f97e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/9f1f97e0c56771208305b28a740504c220287053))
|
||||
* Utilisation du signal "post-migrate" pour créer la spécialité par ([e1202c6](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/e1202c6e6d4061552fa7d530e3e09b11384843c3))
|
||||
* Variables booléennes par défaut ([6bedf71](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/6bedf715ccf9bb9bae4f92d735e3d7b714c96849))
|
||||
* variables csrf ([789816e](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/789816e9868685e2ae08b536b6b6ada1a6a64595))
|
||||
* warning sur ouverture modale de fichiers ([889a3a4](https://git.v0id.ovh:5022/n3wt-innov/n3wt-school/commit/889a3a48c5c2a3f6cb65de8ede0efbe639408011))
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
https://localhost:4443, https://127.0.0.1:4443 {
|
||||
reverse_proxy docuseal:3000
|
||||
}
|
||||
@ -18,6 +18,19 @@ const nextConfig = {
|
||||
protocol: 'https',
|
||||
hostname: 'www.gravatar.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.demo.n3wtschool.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.prod.n3wtschool.com',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '8080',
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n3wt-school-front-end",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import EmailSender from '@/components/Admin/EmailSender';
|
||||
import InstantMessaging from '@/components/Admin/InstantMessaging';
|
||||
import AnnouncementScheduler from '@/components/Admin/AnnouncementScheduler';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function MessageriePage({ csrfToken }) {
|
||||
@ -18,11 +17,6 @@ export default function MessageriePage({ csrfToken }) {
|
||||
label: 'Messagerie Instantanée',
|
||||
content: <InstantMessaging csrfToken={csrfToken} />,
|
||||
},
|
||||
{
|
||||
id: 'announcement',
|
||||
label: 'Planifier une Annonce',
|
||||
content: <AnnouncementScheduler csrfToken={csrfToken} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Users, Clock, CalendarCheck, School, AlertTriangle, CheckCircle2 } from 'lucide-react';
|
||||
import {
|
||||
Users,
|
||||
Clock,
|
||||
CalendarCheck,
|
||||
School,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import Loader from '@/components/Loader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import EventCard from '@/components/EventCard';
|
||||
import logger from '@/utils/logger';
|
||||
import {
|
||||
fetchRegisterForms,
|
||||
@ -15,20 +23,6 @@ import Attendance from '@/components/Grades/Attendance';
|
||||
import LineChart from '@/components/Charts/LineChart';
|
||||
import PieChart from '@/components/Charts/PieChart';
|
||||
|
||||
// Composant EventCard pour afficher les événements
|
||||
const EventCard = ({ title, date, description, type }) => (
|
||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<CalendarCheck className="text-blue-500" size={20} />
|
||||
<div>
|
||||
<h4 className="font-medium">{title}</h4>
|
||||
<p className="text-sm text-gray-500">{date}</p>
|
||||
<p className="text-sm mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const mockCompletionRate = 72; // en pourcentage
|
||||
|
||||
export default function DashboardPage() {
|
||||
@ -39,8 +33,11 @@ export default function DashboardPage() {
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
|
||||
const [absencesToday, setAbsencesToday] = useState([]);
|
||||
const { selectedEstablishmentId, selectedEstablishmentTotalCapacity, apiDocuseal } =
|
||||
useEstablishment();
|
||||
const {
|
||||
selectedEstablishmentId,
|
||||
selectedEstablishmentTotalCapacity,
|
||||
apiDocuseal,
|
||||
} = useEstablishment();
|
||||
|
||||
const [statusDistribution, setStatusDistribution] = useState([
|
||||
{ label: 'Non envoyé', value: 0 },
|
||||
@ -220,8 +217,10 @@ export default function DashboardPage() {
|
||||
|
||||
{/* Événements et KPIs */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Colonne de gauche : Graphique des inscriptions + Présence */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Graphique des inscriptions */}
|
||||
<div className="lg:col-span-1 bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<h2 className="text-lg font-semibold mb-6">
|
||||
{t('inscriptionTrends')}
|
||||
</h2>
|
||||
@ -234,20 +233,20 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Présence et assiduité */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1">
|
||||
<Attendance absences={absencesToday} readOnly={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Événements à venir */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100">
|
||||
{/* Colonne de droite : Événements à venir */}
|
||||
<div className="bg-stone-50 p-6 rounded-lg shadow-sm border border-gray-100 flex-1 h-full">
|
||||
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
|
||||
{upcomingEvents.map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ajout du composant Attendance en dessous, en lecture seule */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Attendance absences={absencesToday} readOnly={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import { useNotification } from '@/context/NotificationContext';
|
||||
import { useSearchParams } from 'next/navigation'; // Ajoute cet import
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [activeTab, setActiveTab] = useState('structure');
|
||||
const [activeTab, setActiveTab] = useState('smtp');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@ -40,8 +40,6 @@ export default function SettingsPage() {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam === 'smtp') {
|
||||
setActiveTab('smtp');
|
||||
} else if (tabParam === 'structure') {
|
||||
setActiveTab('structure');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
@ -79,18 +77,6 @@ export default function SettingsPage() {
|
||||
}
|
||||
}, [activeTab, csrfToken]); // Ajouter csrfToken comme dépendance
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleSmtpServerChange = (e) => {
|
||||
setSmtpServer(e.target.value);
|
||||
};
|
||||
@ -107,26 +93,6 @@ export default function SettingsPage() {
|
||||
setSmtpPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleUseTlsChange = (e) => {
|
||||
setUseTls(e.target.checked);
|
||||
};
|
||||
|
||||
const handleUseSslChange = (e) => {
|
||||
setUseSsl(e.target.checked);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
showNotification(
|
||||
'Les mots de passe ne correspondent pas',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSmtpSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const smtpData = {
|
||||
@ -164,11 +130,6 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex space-x-4 mb-4">
|
||||
<Tab
|
||||
text="Informations de la structure"
|
||||
active={activeTab === 'structure'}
|
||||
onClick={() => handleTabClick('structure')}
|
||||
/>
|
||||
<Tab
|
||||
text="Paramètres SMTP"
|
||||
active={activeTab === 'smtp'}
|
||||
@ -176,28 +137,6 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TabContent isActive={activeTab === 'structure'}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<InputText
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
<InputText
|
||||
label="Confirmer le mot de passe"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
/>
|
||||
<Button type="submit" primary text="Mettre à jour"></Button>
|
||||
</form>
|
||||
</TabContent>
|
||||
<TabContent isActive={activeTab === 'smtp'}>
|
||||
<form onSubmit={handleSmtpSubmit}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
@ -78,7 +78,7 @@ export default function Page() {
|
||||
handleTuitionFees();
|
||||
|
||||
// Fetch data for registration file schoolFileTemplates
|
||||
fetchRegistrationSchoolFileMasters()
|
||||
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setFichiers(data);
|
||||
})
|
||||
|
||||
@ -313,13 +313,13 @@ export default function CreateSubscriptionPage() {
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
fetchRegistrationSchoolFileMasters()
|
||||
fetchRegistrationSchoolFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setSchoolFileMasters(data);
|
||||
})
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
fetchRegistrationParentFileMasters()
|
||||
fetchRegistrationParentFileMasters(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setParentFileMasters(data);
|
||||
})
|
||||
@ -502,7 +502,11 @@ export default function CreateSubscriptionPage() {
|
||||
// Mode édition
|
||||
editRegisterForm(registerFormID, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('Dossier mis à jour avec succès:', response);
|
||||
showNotification(
|
||||
"Dossier d'inscription mis à jour avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -613,6 +617,11 @@ export default function CreateSubscriptionPage() {
|
||||
Promise.all([...clonePromises, ...parentClonePromises])
|
||||
.then(() => {
|
||||
// Redirection après succès
|
||||
showNotification(
|
||||
"Dossier d'inscription créé avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@ -8,9 +8,11 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const { showNotification } = useNotification();
|
||||
const searchParams = useSearchParams();
|
||||
const studentId = searchParams.get('studentId');
|
||||
const enable = searchParams.get('enabled') === 'true';
|
||||
@ -26,11 +28,21 @@ export default function Page() {
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
showNotification(
|
||||
"Dossier d'inscription soumis avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
logger.error('Error:', error.message);
|
||||
showNotification(
|
||||
"Erreur lors de la soumission du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
if (error.details) {
|
||||
logger.error('Form errors:', error.details);
|
||||
setFormErrors(error.details);
|
||||
|
||||
@ -52,6 +52,7 @@ import {
|
||||
HISTORICAL_FILTER,
|
||||
} from '@/utils/constants';
|
||||
import AlertMessage from '@/components/AlertMessage';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page({ params: { locale } }) {
|
||||
const t = useTranslations('subscriptions');
|
||||
@ -91,8 +92,6 @@ export default function Page({ params: { locale } }) {
|
||||
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
||||
const [selectedRegisterForm, setSelectedRegisterForm] = useState([]);
|
||||
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
|
||||
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
|
||||
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
|
||||
@ -103,6 +102,7 @@ export default function Page({ params: { locale } }) {
|
||||
const csrfToken = useCsrfToken();
|
||||
const router = useRouter();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const openSepaUploadModal = (row) => {
|
||||
setSelectedRowForUpload(row);
|
||||
@ -133,9 +133,8 @@ export default function Page({ params: { locale } }) {
|
||||
const registerFormCurrrentYearDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { registerForms, count, page_size } = data;
|
||||
if (registerForms) {
|
||||
setRegistrationFormsDataCurrentYear(registerForms);
|
||||
}
|
||||
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
setTotalCurrentYear(count);
|
||||
@ -153,9 +152,8 @@ export default function Page({ params: { locale } }) {
|
||||
const registerFormNextYearDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { registerForms, count, page_size } = data;
|
||||
if (registerForms) {
|
||||
setRegistrationFormsDataNextYear(registerForms);
|
||||
}
|
||||
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
setTotalNextYear(count);
|
||||
@ -173,9 +171,7 @@ export default function Page({ params: { locale } }) {
|
||||
const registerFormHistoricalDataHandler = (data) => {
|
||||
if (data) {
|
||||
const { registerForms, count, page_size } = data;
|
||||
if (registerForms) {
|
||||
setRegistrationFormsDataHistorical(registerForms);
|
||||
}
|
||||
|
||||
const calculatedTotalPages =
|
||||
count === 0 ? count : Math.ceil(count / page_size);
|
||||
@ -225,12 +221,11 @@ export default function Page({ params: { locale } }) {
|
||||
|
||||
fetchDataAndSetState();
|
||||
}
|
||||
}, [selectedEstablishmentId, reloadFetch, currentSchoolYearPage, searchTerm]);
|
||||
}, [selectedEstablishmentId, reloadFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
const fetchDataAndSetState = () => {
|
||||
setIsLoading(true);
|
||||
fetchRegisterForms(
|
||||
selectedEstablishmentId,
|
||||
CURRENT_YEAR_FILTER,
|
||||
@ -247,7 +242,6 @@ export default function Page({ params: { locale } }) {
|
||||
.then(registerFormHistoricalDataHandler)
|
||||
.catch(requestErrorHandler);
|
||||
|
||||
setIsLoading(false);
|
||||
setReloadFetch(false);
|
||||
};
|
||||
|
||||
@ -256,7 +250,7 @@ export default function Page({ params: { locale } }) {
|
||||
}, 500); // Debounce la recherche
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [searchTerm]);
|
||||
}, [searchTerm, selectedEstablishmentId, currentSchoolYearPage, itemsPerPage]);
|
||||
|
||||
/**
|
||||
* UseEffect to update page count of tab
|
||||
@ -294,15 +288,21 @@ export default function Page({ params: { locale } }) {
|
||||
editRegisterForm(row.student.id, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('Mandat SEPA uploadé avec succès :', response);
|
||||
setPopupMessage('Le mandat SEPA a été uploadé avec succès.');
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
'Le mandat SEPA a été uploadé avec succès.',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setReloadFetch(true);
|
||||
closeSepaUploadModal();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Erreur lors de l'upload du mandat SEPA :", error);
|
||||
setPopupMessage("Erreur lors de l'upload du mandat SEPA.");
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
"Erreur lors de l'upload du mandat SEPA.",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -321,21 +321,22 @@ export default function Page({ params: { locale } }) {
|
||||
archiveRegisterForm(id)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage(
|
||||
"Le dossier d'inscription a été correctement archivé"
|
||||
showNotification(
|
||||
"Le dossier d'inscription a été correctement archivé",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
setRegistrationForms(
|
||||
registrationForms.filter((fiche) => fiche.id !== id)
|
||||
);
|
||||
setReloadFetch(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage(
|
||||
"Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur."
|
||||
showNotification(
|
||||
"Erreur lors de l'archivage du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
@ -350,16 +351,20 @@ export default function Page({ params: { locale } }) {
|
||||
sendRegisterForm(id)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setPopupMessage("Le dossier d'inscription a été envoyé avec succès");
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
"Le dossier d'inscription a été envoyé avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
setReloadFetch(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error archiving data:', error);
|
||||
setPopupMessage(
|
||||
"Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur."
|
||||
showNotification(
|
||||
"Erreur lors de l'envoi du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setPopupVisible(true);
|
||||
});
|
||||
setConfirmPopupVisible(false);
|
||||
});
|
||||
@ -694,7 +699,7 @@ export default function Page({ params: { locale } }) {
|
||||
];
|
||||
|
||||
let emptyMessage;
|
||||
if (activeTab === CURRENT_YEAR_FILTER) {
|
||||
if (activeTab === CURRENT_YEAR_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
@ -702,7 +707,7 @@ export default function Page({ params: { locale } }) {
|
||||
message="Veuillez procéder à la création d'un nouveau dossier d'inscription pour l'année scolaire en cours."
|
||||
/>
|
||||
);
|
||||
} else if (activeTab === NEXT_YEAR_FILTER) {
|
||||
} else if (activeTab === NEXT_YEAR_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="info"
|
||||
@ -710,7 +715,7 @@ export default function Page({ params: { locale } }) {
|
||||
message="Aucun dossier n'a encore été créé pour la prochaine année scolaire."
|
||||
/>
|
||||
);
|
||||
} else if (activeTab === HISTORICAL_FILTER) {
|
||||
} else if (activeTab === HISTORICAL_FILTER && searchTerm === '') {
|
||||
emptyMessage = (
|
||||
<AlertMessage
|
||||
type="info"
|
||||
@ -836,12 +841,6 @@ export default function Page({ params: { locale } }) {
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
isOpen={confirmPopupVisible}
|
||||
message={confirmPopupMessage}
|
||||
|
||||
@ -9,6 +9,7 @@ import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
@ -30,6 +31,7 @@ export default function Page() {
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
const requestErrorHandler = (err) => {
|
||||
logger.error('Error fetching data:', err);
|
||||
@ -63,11 +65,20 @@ export default function Page() {
|
||||
editRegisterForm(studentId, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('RF mis à jour avec succès:', response);
|
||||
showNotification(
|
||||
'Le dossier d\'inscription a été validé avec succès',
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
setIsLoading(false);
|
||||
// Logique supplémentaire après la mise à jour (par exemple, redirection ou notification)
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification(
|
||||
"Erreur lors de la validation du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setIsLoading(false);
|
||||
logger.error('Erreur lors de la mise à jour du RF:', error);
|
||||
});
|
||||
|
||||
@ -7,25 +7,47 @@ import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { FE_PARENTS_HOME_URL } from '@/utils/Url';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const studentId = searchParams.get('studentId');
|
||||
const enable = searchParams.get('enabled') === 'true';
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
try {
|
||||
const result = await editRegisterForm(studentId, data, csrfToken);
|
||||
const handleSubmit = (data) => {
|
||||
setIsLoading(true);
|
||||
editRegisterForm(studentId, data, csrfToken)
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
showNotification(
|
||||
"Dossier d'inscription soumis avec succès",
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(FE_PARENTS_HOME_URL);
|
||||
} catch (error) {
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
showNotification(
|
||||
"Erreur lors de la soumission du dossier d'inscription",
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
logger.error('Error:', error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
}
|
||||
|
||||
return (
|
||||
<InscriptionFormShared
|
||||
studentId={studentId}
|
||||
|
||||
@ -6,8 +6,7 @@ import { useRouter, usePathname } from 'next/navigation';
|
||||
import { MessageSquare, Settings, Home, Menu } from 'lucide-react';
|
||||
import {
|
||||
FE_PARENTS_HOME_URL,
|
||||
FE_PARENTS_MESSAGERIE_URL,
|
||||
FE_PARENTS_SETTINGS_URL,
|
||||
FE_PARENTS_MESSAGERIE_URL
|
||||
} from '@/utils/Url';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import { disconnect } from '@/app/actions/authAction';
|
||||
@ -41,13 +40,7 @@ export default function Layout({ children }) {
|
||||
name: 'Messagerie',
|
||||
url: FE_PARENTS_MESSAGERIE_URL,
|
||||
icon: MessageSquare,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
name: 'Paramètres',
|
||||
url: FE_PARENTS_SETTINGS_URL,
|
||||
icon: Settings,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
// Déterminer la page actuelle pour la sidebar
|
||||
|
||||
@ -2,7 +2,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Table from '@/components/Table';
|
||||
import { Edit3, Users, Download, Eye, Upload } from 'lucide-react';
|
||||
import {
|
||||
Edit3,
|
||||
Users,
|
||||
Download,
|
||||
Eye,
|
||||
Upload,
|
||||
CalendarDays,
|
||||
} from 'lucide-react';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import { FE_PARENTS_EDIT_SUBSCRIPTION_URL } from '@/utils/Url';
|
||||
@ -10,11 +17,16 @@ import {
|
||||
fetchChildren,
|
||||
editRegisterForm,
|
||||
} from '@/app/actions/subscriptionAction';
|
||||
import { fetchUpcomingEvents } from '@/app/actions/planningAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useClasses } from '@/context/ClassesContext';
|
||||
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import ParentPlanningSection from '@/components/ParentPlanningSection';
|
||||
import EventCard from '@/components/EventCard';
|
||||
|
||||
export default function ParentHomePage() {
|
||||
const [children, setChildren] = useState([]);
|
||||
@ -22,6 +34,9 @@ export default function ParentHomePage() {
|
||||
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
|
||||
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
|
||||
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
|
||||
const [showPlanning, setShowPlanning] = useState(false);
|
||||
const [planningClassName, setPlanningClassName] = useState(null);
|
||||
const [upcomingEvents, setUpcomingEvents] = useState([]);
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
const [reloadFetch, setReloadFetch] = useState(false);
|
||||
@ -35,7 +50,20 @@ export default function ParentHomePage() {
|
||||
});
|
||||
setReloadFetch(false);
|
||||
}
|
||||
}, [selectedEstablishmentId, reloadFetch]);
|
||||
}, [selectedEstablishmentId, reloadFetch, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
// Fetch des événements à venir
|
||||
fetchUpcomingEvents(selectedEstablishmentId)
|
||||
.then((data) => {
|
||||
setUpcomingEvents(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching upcoming events:', error);
|
||||
});
|
||||
}
|
||||
}, [selectedEstablishmentId]);
|
||||
|
||||
function handleView(eleveId) {
|
||||
logger.debug(`View dossier for student id: ${eleveId}`);
|
||||
@ -97,6 +125,13 @@ export default function ParentHomePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const showClassPlanning = (student) => {
|
||||
setPlanningClassName(
|
||||
`${student.associated_class_name} - ${getNiveauLabel(student.level)}`
|
||||
);
|
||||
setShowPlanning(true);
|
||||
};
|
||||
|
||||
const childrenColumns = [
|
||||
{
|
||||
name: 'photo',
|
||||
@ -127,6 +162,12 @@ export default function ParentHomePage() {
|
||||
},
|
||||
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
|
||||
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
|
||||
{
|
||||
name: 'Classe',
|
||||
transform: (row) => (
|
||||
<div className="text-center">{row.student.associated_class_name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Niveau',
|
||||
transform: (row) => (
|
||||
@ -192,7 +233,6 @@ export default function ParentHomePage() {
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</a>
|
||||
{/* Nouvelle action Upload */}
|
||||
<button
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
uploadingStudentId === row.student.id && uploadState === 'on'
|
||||
@ -201,7 +241,7 @@ export default function ParentHomePage() {
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleUpload(row.student.id); // Activer ou désactiver l'upload pour cet étudiant
|
||||
toggleUpload(row.student.id);
|
||||
}}
|
||||
aria-label="Uploader un fichier"
|
||||
>
|
||||
@ -211,6 +251,7 @@ export default function ParentHomePage() {
|
||||
)}
|
||||
|
||||
{row.status === 5 && (
|
||||
<>
|
||||
<button
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
@ -221,6 +262,17 @@ export default function ParentHomePage() {
|
||||
>
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-emerald-500 hover:text-emerald-700 ml-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showClassPlanning(row.student);
|
||||
}}
|
||||
aria-label="Voir le planning de la classe"
|
||||
>
|
||||
<CalendarDays className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
@ -228,18 +280,54 @@ export default function ParentHomePage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="px-2 py-4 md:px-4 max-w-full">
|
||||
<div className="w-full h-full">
|
||||
{showPlanning && planningClassName ? (
|
||||
// Affichage grand format mais respectant la sidebar
|
||||
<>
|
||||
<div className="p-4 flex items-center border-b">
|
||||
<button
|
||||
className="text-emerald-600 hover:text-emerald-800 font-semibold flex items-center"
|
||||
onClick={() => setShowPlanning(false)}
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<PlanningProvider
|
||||
establishmentId={selectedEstablishmentId}
|
||||
modeSet={PlanningModes.CLASS_SCHEDULE}
|
||||
readOnly={true}
|
||||
>
|
||||
<ParentPlanningSection planningClassName={planningClassName} />
|
||||
</PlanningProvider>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Affichage classique avec le tableau des enfants
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-3 px-1 flex items-center gap-2">
|
||||
<Users className="h-6 w-6 text-emerald-600" />
|
||||
Enfants
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
data={children}
|
||||
columns={childrenColumns}
|
||||
defaultTheme="bg-gray-50"
|
||||
{/* Section des événements à venir */}
|
||||
{upcomingEvents.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<SectionHeader
|
||||
icon={CalendarDays}
|
||||
title="Événements à venir"
|
||||
description="Prochains événements de l'établissement"
|
||||
/>
|
||||
<div className="bg-stone-50 p-4 rounded-lg shadow-sm border border-gray-100">
|
||||
{upcomingEvents.slice(0, 3).map((event, index) => (
|
||||
<EventCard key={index} {...event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SectionHeader
|
||||
icon={Users}
|
||||
title="Vos enfants"
|
||||
description="Suivez le parcours de vos enfants"
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<Table data={children} columns={childrenColumns} />
|
||||
</div>
|
||||
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
|
||||
{uploadState === 'on' && uploadingStudentId && (
|
||||
@ -262,6 +350,7 @@ export default function ParentHomePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,20 +14,18 @@ import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsr
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [userFieldError, setUserFieldError] = useState('');
|
||||
const [passwordFieldError, setPasswordFieldError] = useState('');
|
||||
const { initializeContextWithSession } = useEstablishment();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken(); // Utilisez le hook useCsrfToken
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
function handleFormLogin(formData) {
|
||||
setIsLoading(true);
|
||||
setErrorMessage('');
|
||||
|
||||
login({
|
||||
email: formData.get('login'),
|
||||
@ -37,7 +35,11 @@ export default function Page() {
|
||||
logger.debug('Sign In Result', result);
|
||||
|
||||
if (result.error) {
|
||||
setErrorMessage(result.error);
|
||||
showNotification(
|
||||
result.error,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
// On initialise le contexte establishement avec la session
|
||||
@ -48,7 +50,11 @@ export default function Page() {
|
||||
if (url) {
|
||||
router.push(url);
|
||||
} else {
|
||||
setErrorMessage('Type de rôle non géré');
|
||||
showNotification(
|
||||
'Type de rôle non géré',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
});
|
||||
setIsLoading(false);
|
||||
@ -59,8 +65,10 @@ export default function Page() {
|
||||
error
|
||||
);
|
||||
setIsLoading(false);
|
||||
setErrorMessage(
|
||||
'Une erreur est survenue lors de la récupération de la session.'
|
||||
showNotification(
|
||||
'Une erreur est survenue lors de la récupération de la session.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -68,7 +76,11 @@ export default function Page() {
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la connexion:', error);
|
||||
setIsLoading(false);
|
||||
setErrorMessage('Une erreur est survenue lors de la connexion.');
|
||||
showNotification(
|
||||
'Une erreur est survenue lors de la connexion.',
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,7 +110,6 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
errorMsg={userFieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -107,11 +118,9 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
errorMsg={passwordFieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<div className="input-group mb-4"></div>
|
||||
<label className="text-red-500">{errorMessage}</label>
|
||||
<label>
|
||||
<a
|
||||
className="float-right mb-4"
|
||||
|
||||
@ -3,68 +3,61 @@
|
||||
import React, { useState } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup'; // Importez le composant Popup
|
||||
import { User } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import { User } from 'lucide-react';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { sendNewPassword } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [userFieldError, setUserFieldError] = useState('');
|
||||
const { showNotification } = useNotification();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const [popupConfirmAction, setPopupConfirmAction] = useState(null);
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
function validate(formData) {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setUserFieldError('');
|
||||
setErrorMessage('');
|
||||
setPopupMessage('Mot de passe réinitialisé avec succès !');
|
||||
setPopupConfirmAction(() => () => setPopupVisible(false));
|
||||
setPopupVisible(true);
|
||||
}, 1000); // Simule un délai de traitement
|
||||
} else {
|
||||
const data = { email: formData.get('email') };
|
||||
setIsLoading(true);
|
||||
sendNewPassword(data, csrfToken)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setUserFieldError('');
|
||||
setErrorMessage('');
|
||||
if (data.errorMessage === '') {
|
||||
setPopupMessage(data.message);
|
||||
setPopupConfirmAction(() => () => setPopupVisible(false));
|
||||
setPopupVisible(true);
|
||||
if (data.message !== '') {
|
||||
showNotification(
|
||||
data.message,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
if (data.errorFields) {
|
||||
setUserFieldError(data.errorFields.email);
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
showNotification(
|
||||
data.errorMessage,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.email,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
setIsLoading(false);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@ -89,10 +82,8 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
errorMsg={userFieldError}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-red-500">{errorMessage}</p>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Réinitialiser"
|
||||
@ -112,12 +103,6 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={popupConfirmAction}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,110 +6,71 @@ import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup';
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import { KeySquare } from 'lucide-react';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { getResetPassword, resetPassword } from '@/app/actions/authAction';
|
||||
import { resetPassword } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const { showNotification } = useNotification();
|
||||
const uuid = searchParams.get('uuid');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [password1FieldError, setPassword1FieldError] = useState('');
|
||||
const [password2FieldError, setPassword2FieldError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
} else {
|
||||
getResetPassword(uuid)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setIsLoading(true);
|
||||
if (data.errorFields) {
|
||||
setPassword1FieldError(data.errorFields.password1);
|
||||
setPassword2FieldError(data.errorFields.password2);
|
||||
}
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function validate(formData) {
|
||||
if (useFakeData) {
|
||||
setTimeout(() => {
|
||||
setPopupMessage('Mot de passe réinitialisé avec succès');
|
||||
setPopupVisible(true);
|
||||
}, 1000);
|
||||
} else {
|
||||
const data = {
|
||||
password1: formData.get('password1'),
|
||||
password2: formData.get('password2'),
|
||||
};
|
||||
setIsLoading(true);
|
||||
resetPassword(uuid, data, csrfToken)
|
||||
.then((data) => {
|
||||
if (data.message !== '') {
|
||||
|
||||
logger.debug('Success:', data);
|
||||
setPassword1FieldError('');
|
||||
setPassword2FieldError('');
|
||||
setErrorMessage('');
|
||||
if (data.errorMessage === '') {
|
||||
setPopupMessage(data.message);
|
||||
setPopupVisible(true);
|
||||
showNotification(
|
||||
data.message,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if (data.errorFields) {
|
||||
setPassword1FieldError(data.errorFields.password1);
|
||||
setPassword2FieldError(data.errorFields.password2);
|
||||
showNotification(
|
||||
data.errorMessage,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.password1 || data.errorFields.password2,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Error fetching data:', error);
|
||||
error = error.errorMessage;
|
||||
logger.debug(error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
setIsOpen={setPopupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => {
|
||||
setPopupVisible(false);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
popupClassName="w-full max-w-xs sm:max-w-md"
|
||||
/>
|
||||
<div className="container max mx-auto p-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Logo className="h-150 w-150" />
|
||||
@ -131,7 +92,6 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
errorMsg={password1FieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -140,10 +100,8 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
errorMsg={password2FieldError}
|
||||
className="w-full"
|
||||
/>
|
||||
<label className="text-red-500">{errorMessage}</label>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
|
||||
@ -1,40 +1,29 @@
|
||||
'use client';
|
||||
// src/app/pages/subscribe.js
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import Logo from '@/components/Logo';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import InputTextIcon from '@/components/InputTextIcon';
|
||||
import Loader from '@/components/Loader'; // Importez le composant Loader
|
||||
import Button from '@/components/Button'; // Importez le composant Button
|
||||
import Popup from '@/components/Popup'; // Importez le composant Popup
|
||||
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
|
||||
import Loader from '@/components/Loader';
|
||||
import Button from '@/components/Button';
|
||||
import { User, KeySquare } from 'lucide-react';
|
||||
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { subscribe } from '@/app/actions/authAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [userFieldError, setUserFieldError] = useState('');
|
||||
const [password1FieldError, setPassword1FieldError] = useState('');
|
||||
const [password2FieldError, setPassword2FieldError] = useState('');
|
||||
const { showNotification } = useNotification();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
const establishment_id = searchParams.get('establishment_id');
|
||||
|
||||
function isOK(data) {
|
||||
return data.errorMessage === '';
|
||||
}
|
||||
|
||||
function subscribeFormSubmit(formData) {
|
||||
const data = {
|
||||
email: formData.get('login'),
|
||||
@ -46,25 +35,29 @@ export default function Page() {
|
||||
subscribe(data, csrfToken)
|
||||
.then((data) => {
|
||||
logger.debug('Success:', data);
|
||||
setUserFieldError('');
|
||||
setPassword1FieldError('');
|
||||
setPassword2FieldError('');
|
||||
setErrorMessage('');
|
||||
if (isOK(data)) {
|
||||
setIsLoading(false);
|
||||
setPopupMessage(data.message);
|
||||
setPopupVisible(true);
|
||||
if (data.message !== '') {
|
||||
showNotification(
|
||||
data.message,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
if (data.errorMessage) {
|
||||
setErrorMessage(data.errorMessage);
|
||||
}
|
||||
if (data.errorFields) {
|
||||
setUserFieldError(data.errorFields.email);
|
||||
setPassword1FieldError(data.errorFields.password1);
|
||||
setPassword2FieldError(data.errorFields.password2);
|
||||
showNotification(
|
||||
data.errorMessage,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
} else if (data.errorFields) {
|
||||
showNotification(
|
||||
data.errorFields.email || data.errorFields.password1 || data.errorFields.password2,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
@ -75,7 +68,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
if (isLoading === true) {
|
||||
return <Loader />; // Affichez le composant Loader
|
||||
return <Loader />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@ -100,7 +93,6 @@ export default function Page() {
|
||||
IconItem={User}
|
||||
label="Identifiant"
|
||||
placeholder="Identifiant"
|
||||
errorMsg={userFieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -109,7 +101,6 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Mot de passe"
|
||||
placeholder="Mot de passe"
|
||||
errorMsg={password1FieldError}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<InputTextIcon
|
||||
@ -118,10 +109,8 @@ export default function Page() {
|
||||
IconItem={KeySquare}
|
||||
label="Confirmation mot de passe"
|
||||
placeholder="Confirmation mot de passe"
|
||||
errorMsg={password2FieldError}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-red-500">{errorMessage}</p>
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
text="Enregistrer"
|
||||
@ -143,18 +132,6 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Popup
|
||||
isOpen={popupVisible}
|
||||
setIsOpen={setPopupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => {
|
||||
setPopupVisible(false);
|
||||
router.push(`${FE_USERS_LOGIN_URL}`);
|
||||
}}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
popupClassName="w-full max-w-xs sm:max-w-md"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -199,14 +199,3 @@ export const resetPassword = (uuid, data, csrfToken) => {
|
||||
});
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const getResetPassword = (uuid) => {
|
||||
const url = `${BE_AUTH_RESET_PASSWORD_URL}/${uuid}`;
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
.catch(errorHandler);
|
||||
};
|
||||
|
||||
@ -47,11 +47,8 @@ export const fetchRegistrationFileFromGroup = async (groupId) => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileMasters = (id = null) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${id}`;
|
||||
}
|
||||
export const fetchRegistrationSchoolFileMasters = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -61,11 +58,8 @@ export const fetchRegistrationSchoolFileMasters = (id = null) => {
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationParentFileMasters = (id = null) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`;
|
||||
}
|
||||
export const fetchRegistrationParentFileMasters = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -75,11 +69,8 @@ export const fetchRegistrationParentFileMasters = (id = null) => {
|
||||
return fetch(request).then(requestResponseHandler).catch(errorHandler);
|
||||
};
|
||||
|
||||
export const fetchRegistrationSchoolFileTemplates = (id = null) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`;
|
||||
if (id) {
|
||||
url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${id}`;
|
||||
}
|
||||
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
|
||||
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
|
||||
const request = new Request(`${url}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import WeekView from '@/components/Calendar/WeekView';
|
||||
import MonthView from '@/components/Calendar/MonthView';
|
||||
import YearView from '@/components/Calendar/YearView';
|
||||
@ -22,7 +22,7 @@ import { fr } from 'date-fns/locale';
|
||||
import { AnimatePresence, motion } from 'framer-motion'; // Ajouter cet import
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
const Calendar = ({ modeSet, onDateClick, onEventClick, planningClassName='' }) => {
|
||||
const {
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
@ -30,6 +30,8 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
setViewType,
|
||||
events,
|
||||
hiddenSchedules,
|
||||
planningMode,
|
||||
parentView
|
||||
} = usePlanning();
|
||||
const [visibleEvents, setVisibleEvents] = useState([]);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
@ -91,6 +93,7 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 bg-white sticky top-0 z-30 border-b shadow-sm h-[64px]">
|
||||
{/* Navigation à gauche */}
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
@ -104,8 +107,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Menu déroulant pour le mois/année */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||
@ -120,8 +121,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
</h2>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Menu de sélection du mois/année */}
|
||||
{showDatePicker && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 w-64">
|
||||
{viewType !== 'year' && (
|
||||
@ -155,7 +154,6 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded-full"
|
||||
@ -163,9 +161,11 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Numéro de semaine au centre */}
|
||||
{viewType === 'week' && (
|
||||
{/* Centre : numéro de semaine ou classe/niveau */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{((planningMode === PlanningModes.PLANNING || planningMode === PlanningModes.CLASS_SCHEDULE) && viewType === 'week' && !parentView) && (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-gray-600">
|
||||
<span>Semaine</span>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md">
|
||||
@ -173,16 +173,27 @@ const Calendar = ({ modeSet, onDateClick, onEventClick }) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{parentView && (
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-md text-base font-semibold">
|
||||
{/* À adapter selon les props disponibles */}
|
||||
{planningClassName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contrôles à droite */}
|
||||
<div className="flex items-center gap-4">
|
||||
{planningMode === PlanningModes.PLANNING && (
|
||||
<ToggleView viewType={viewType} setViewType={setViewType} />
|
||||
)}
|
||||
{(planningMode === PlanningModes.PLANNING || (planningMode === PlanningModes.CLASS_SCHEDULE && !parentView)) && (
|
||||
<button
|
||||
onClick={onDateClick}
|
||||
className="w-10 h-10 flex items-center justify-center bg-emerald-600 text-white rounded-full hover:bg-emerald-700 shadow-md transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
import { usePlanning, PlanningModes } from '@/context/PlanningContext';
|
||||
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { getWeekEvents } from '@/utils/events';
|
||||
import { isToday } from 'date-fns';
|
||||
|
||||
|
||||
const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
const { currentDate } = usePlanning();
|
||||
const { currentDate, planningMode, parentView } = usePlanning();
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const scrollContainerRef = useRef(null); // Ajouter cette référence
|
||||
|
||||
@ -106,10 +107,14 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
key={event.id}
|
||||
className="rounded-sm overflow-hidden cursor-pointer hover:shadow-lg"
|
||||
style={eventStyle}
|
||||
onClick={(e) => {
|
||||
onClick={
|
||||
parentView
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}}
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="p-1">
|
||||
<div
|
||||
@ -198,11 +203,15 @@ const WeekView = ({ onDateClick, onEventClick, events }) => {
|
||||
className={`h-20 relative border-b border-gray-100
|
||||
${isWeekend(day) ? 'bg-gray-50' : 'bg-white'}
|
||||
${isToday(day) ? 'bg-emerald-100/50 border-x border-emerald-600' : ''}`}
|
||||
onClick={() => {
|
||||
onClick={
|
||||
parentView
|
||||
? undefined
|
||||
: () => {
|
||||
const date = new Date(day);
|
||||
date.setHours(hour);
|
||||
onDateClick(date);
|
||||
}}
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="grid gap-1">
|
||||
{dayEvents
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
// Utilisation de couleurs hexadécimales pour le SVG
|
||||
const COLORS = [
|
||||
'fill-blue-400 text-blue-400',
|
||||
'fill-orange-400 text-orange-400',
|
||||
'fill-purple-400 text-purple-400',
|
||||
'fill-emerald-400 text-emerald-400',
|
||||
'#60a5fa', // bleu-400
|
||||
'#fb923c', // orange-400
|
||||
'#a78bfa', // violet-400
|
||||
'#34d399', // émeraude-400
|
||||
];
|
||||
|
||||
export default function PieChart({ data }) {
|
||||
@ -23,7 +24,20 @@ export default function PieChart({ data }) {
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<svg width={100} height={100} viewBox="0 0 32 32">
|
||||
{data.map((slice, idx) => {
|
||||
if (slice.value === 0) return null;
|
||||
const value = (slice.value / total) * 100;
|
||||
if (value === 100) {
|
||||
// Cas 100% : on dessine un cercle plein
|
||||
return (
|
||||
<circle
|
||||
key={idx}
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="16"
|
||||
fill={COLORS[idx % COLORS.length]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const startAngle = (cumulative / 100) * 360;
|
||||
const endAngle = ((cumulative + value) / 100) * 360;
|
||||
const largeArc = value > 50 ? 1 : 0;
|
||||
@ -42,7 +56,7 @@ export default function PieChart({ data }) {
|
||||
<path
|
||||
key={idx}
|
||||
d={pathData}
|
||||
className={COLORS[idx % COLORS.length].split(' ')[0]}
|
||||
fill={COLORS[idx % COLORS.length]}
|
||||
stroke="#fff"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
@ -50,17 +64,21 @@ export default function PieChart({ data }) {
|
||||
})}
|
||||
</svg>
|
||||
<div className="ml-4 flex flex-col space-y-1">
|
||||
{data.map((slice, idx) => (
|
||||
{data.map((slice, idx) =>
|
||||
slice.value > 0 && (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center text-xs font-semibold ${COLORS[idx % COLORS.length].split(' ')[1]}`}
|
||||
className="flex items-center text-xs font-semibold"
|
||||
style={{ color: COLORS[idx % COLORS.length] }}
|
||||
>
|
||||
<span
|
||||
className={`inline-block w-3 h-3 mr-2 rounded ${COLORS[idx % COLORS.length].split(' ')[0]}`}
|
||||
className="inline-block w-3 h-3 mr-2 rounded"
|
||||
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
|
||||
/>
|
||||
{slice.label} : {slice.value}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
131
Front-End/src/components/EventCard.js
Normal file
131
Front-End/src/components/EventCard.js
Normal file
@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { CalendarCheck } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Formate une date en format français avec jour de la semaine
|
||||
* @param {string} startDateString - Date de début au format ISO
|
||||
* @param {string} endDateString - Date de fin au format ISO (optionnel)
|
||||
* @returns {object} Objet contenant la date formatée et le jour
|
||||
*/
|
||||
const formatEventDate = (startDateString, endDateString) => {
|
||||
if (!startDateString)
|
||||
return { formattedDate: '', dayName: '', timeIndicator: '', timeRange: '' };
|
||||
|
||||
try {
|
||||
const startDate = new Date(startDateString);
|
||||
const endDate = endDateString ? new Date(endDateString) : null;
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const eventDate = new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate()
|
||||
);
|
||||
|
||||
// Options pour le formatage de la date
|
||||
const dateOptions = {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
const dayOptions = {
|
||||
weekday: 'long',
|
||||
};
|
||||
|
||||
const timeOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
const formattedDate = startDate.toLocaleDateString('fr-FR', dateOptions);
|
||||
const dayName = startDate.toLocaleDateString('fr-FR', dayOptions);
|
||||
|
||||
// Formatage de l'heure
|
||||
let timeRange = '';
|
||||
if (endDate) {
|
||||
const startTime = startDate.toLocaleTimeString('fr-FR', timeOptions);
|
||||
const endTime = endDate.toLocaleTimeString('fr-FR', timeOptions);
|
||||
timeRange = `${startTime} - ${endTime}`;
|
||||
} else {
|
||||
timeRange = startDate.toLocaleTimeString('fr-FR', timeOptions);
|
||||
}
|
||||
|
||||
// Calcul des jours restants
|
||||
const diffTime = eventDate - today;
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
let timeIndicator = '';
|
||||
if (diffDays === 0) {
|
||||
timeIndicator = "Aujourd'hui";
|
||||
} else if (diffDays === 1) {
|
||||
timeIndicator = 'Demain';
|
||||
} else if (diffDays > 0 && diffDays <= 7) {
|
||||
timeIndicator = `Dans ${diffDays} jours`;
|
||||
}
|
||||
|
||||
return {
|
||||
formattedDate,
|
||||
dayName: dayName.charAt(0).toUpperCase() + dayName.slice(1),
|
||||
timeIndicator,
|
||||
timeRange,
|
||||
};
|
||||
} catch (error) {
|
||||
// logger.error('Erreur lors du formatage de la date:', error);
|
||||
return {
|
||||
formattedDate: startDateString,
|
||||
dayName: '',
|
||||
timeIndicator: '',
|
||||
timeRange: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Composant EventCard pour afficher les événements à venir
|
||||
* @param {string} title - Titre de l'événement
|
||||
* @param {string} start - Date de début de l'événement (format ISO)
|
||||
* @param {string} end - Date de fin de l'événement (format ISO)
|
||||
* @param {string} date - Date de l'événement (pour compatibilité)
|
||||
* @param {string} description - Description de l'événement
|
||||
* @param {string} type - Type d'événement (optionnel)
|
||||
* @returns {JSX.Element} Carte d'événement
|
||||
*/
|
||||
const EventCard = ({ title, start, end, date, description, type }) => {
|
||||
// Utiliser start si disponible, sinon date pour compatibilité
|
||||
const eventDate = start || date;
|
||||
const { formattedDate, dayName, timeIndicator, timeRange } = formatEventDate(
|
||||
eventDate,
|
||||
end
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 mb-3 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<CalendarCheck className="text-emerald-500" size={20} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900 mb-1">{title}</h4>
|
||||
<div className="flex flex-col text-sm text-gray-500">
|
||||
<span className="font-medium text-emerald-600">{dayName}</span>
|
||||
<span>{formattedDate}</span>
|
||||
{timeRange && (
|
||||
<span className="text-xs text-gray-600 mt-1">{timeRange}</span>
|
||||
)}
|
||||
{timeIndicator && (
|
||||
<span className="text-xs text-blue-600 mt-1 font-medium">
|
||||
{timeIndicator}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-sm mt-2 text-gray-700">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventCard;
|
||||
@ -33,12 +33,19 @@ export default function FlashNotification({
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
}, [message, type, errorCode, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type !== 'error') {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false); // Déclenche la disparition
|
||||
setTimeout(onClose, 300); // Appelle onClose après l'animation
|
||||
}, displayPeriod); // Notification visible pendant 3 secondes par défaut
|
||||
}, displayPeriod);
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose, displayPeriod]);
|
||||
}
|
||||
// Pour les erreurs, pas de timeout : la notification reste affichée
|
||||
}, [onClose, displayPeriod, type]);
|
||||
|
||||
if (!message || !isVisible) return null;
|
||||
|
||||
|
||||
@ -230,7 +230,7 @@ export default function FilesToUpload({
|
||||
<div className="mt-4">
|
||||
{actionType === 'view' && selectedFile.fileName ? (
|
||||
<iframe
|
||||
src={`${BASE_URL}/${selectedFile.fileName}`}
|
||||
src={`${BASE_URL}${selectedFile.fileName}`}
|
||||
title="Document Viewer"
|
||||
className="w-full"
|
||||
style={{
|
||||
|
||||
@ -112,8 +112,13 @@ export default function StudentInfoForm({
|
||||
(field === 'birth_place' &&
|
||||
(!formData.birth_place || formData.birth_place.trim() === '')) ||
|
||||
(field === 'birth_postal_code' &&
|
||||
(!formData.birth_postal_code ||
|
||||
String(formData.birth_postal_code).trim() === '')) ||
|
||||
(
|
||||
!formData.birth_postal_code ||
|
||||
String(formData.birth_postal_code).trim() === '' ||
|
||||
isNaN(Number(formData.birth_postal_code)) ||
|
||||
!Number.isInteger(Number(formData.birth_postal_code))
|
||||
)
|
||||
) ||
|
||||
(field === 'address' &&
|
||||
(!formData.address || formData.address.trim() === '')) ||
|
||||
(field === 'attending_physician' &&
|
||||
|
||||
@ -81,14 +81,13 @@ export default function ValidateSubscription({
|
||||
};
|
||||
|
||||
const handleAssignClass = () => {
|
||||
const fusionParam = mergeDocuments ? 'true' : 'false';
|
||||
if (formData.associated_class) {
|
||||
const data = {
|
||||
student: {
|
||||
associated_class: formData.associated_class,
|
||||
},
|
||||
status: 5,
|
||||
fusionParam: fusionParam,
|
||||
fusionParam: mergeDocuments,
|
||||
};
|
||||
|
||||
onAccept(data);
|
||||
@ -147,7 +146,7 @@ export default function ValidateSubscription({
|
||||
{allTemplates[currentTemplateIndex].name || 'Document sans nom'}
|
||||
</h3>
|
||||
<iframe
|
||||
src={`${BASE_URL}/${allTemplates[currentTemplateIndex].file}`}
|
||||
src={`${BASE_URL}${allTemplates[currentTemplateIndex].file}`}
|
||||
title={
|
||||
allTemplates[currentTemplateIndex].type === 'main'
|
||||
? 'Document Principal'
|
||||
|
||||
30
Front-End/src/components/ParentPlanningSection.js
Normal file
30
Front-End/src/components/ParentPlanningSection.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Calendar from '@/components/Calendar/Calendar';
|
||||
import { usePlanning } from '@/context/PlanningContext';
|
||||
|
||||
const ParentPlanningSection = ({ planningClassName }) => {
|
||||
const { setHiddenSchedules, schedules } = usePlanning();
|
||||
|
||||
useEffect(() => {
|
||||
if (schedules && schedules.length > 0 && planningClassName) {
|
||||
const visibleSchedule = schedules.find(s => s.name === planningClassName);
|
||||
const hidden = schedules
|
||||
.filter(s => s.name !== planningClassName)
|
||||
.map(s => s.id);
|
||||
setHiddenSchedules(hidden);
|
||||
|
||||
// Optionnel : si aucun planning ne correspond, tout masquer
|
||||
if (!visibleSchedule) {
|
||||
setHiddenSchedules(schedules.map(s => s.id));
|
||||
}
|
||||
}
|
||||
}, [schedules, planningClassName, setHiddenSchedules]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full h-full overflow-hidden">
|
||||
<Calendar planningClassName={planningClassName} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParentPlanningSection;
|
||||
@ -9,6 +9,9 @@ import { usePopup } from '@/context/PopupContext';
|
||||
import { getRightStr } from '@/utils/rights';
|
||||
import { ChevronDown } from 'lucide-react'; // Import de l'icône
|
||||
import Image from 'next/image'; // Import du composant Image
|
||||
import {
|
||||
BASE_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
||||
const {
|
||||
@ -20,6 +23,9 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
||||
user,
|
||||
setSelectedEstablishmentEvaluationFrequency,
|
||||
setSelectedEstablishmentTotalCapacity,
|
||||
selectedEstablishmentLogo,
|
||||
setSelectedEstablishmentLogo,
|
||||
setApiDocuseal
|
||||
} = useEstablishment();
|
||||
const { isConnected, connectionStatus } = useChatConnection();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
@ -33,12 +39,18 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
||||
user.roles[roleId].establishment__evaluation_frequency;
|
||||
const establishmentTotalCapacity =
|
||||
user.roles[roleId].establishment__total_capacity;
|
||||
const establishmentLogo =
|
||||
user.roles[roleId].establishment__logo;
|
||||
const establishmentApiDocuseal =
|
||||
user.roles[roleId].establishment__api_docuseal;
|
||||
setProfileRole(role);
|
||||
setSelectedEstablishmentId(establishmentId);
|
||||
setSelectedEstablishmentEvaluationFrequency(
|
||||
establishmentEvaluationFrequency
|
||||
);
|
||||
setSelectedEstablishmentTotalCapacity(establishmentTotalCapacity);
|
||||
setSelectedEstablishmentLogo(establishmentLogo);
|
||||
setApiDocuseal(establishmentApiDocuseal);
|
||||
setSelectedRoleId(roleId);
|
||||
if (onRoleChange) {
|
||||
onRoleChange(roleId);
|
||||
@ -99,14 +111,14 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
||||
<div className={`relative ${className}`}>
|
||||
<DropdownMenu
|
||||
buttonContent={
|
||||
<div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white">
|
||||
<div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white h-24">
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={getGravatarUrl(user?.email)}
|
||||
src={selectedEstablishmentLogo ? `${BASE_URL}${selectedEstablishmentLogo}` : getGravatarUrl(user?.email)}
|
||||
alt="Profile"
|
||||
className="w-10 h-10 rounded-full object-cover shadow-md"
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-16 h-16 rounded-full object-cover shadow-md"
|
||||
width={64}
|
||||
height={64}
|
||||
/>
|
||||
{/* Bulle de statut de connexion au chat */}
|
||||
<div
|
||||
@ -116,19 +128,22 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="font-bold text-left truncate max-w-full"
|
||||
className="font-semibold text-base text-gray-900 text-left truncate max-w-full"
|
||||
title={user?.email}
|
||||
>
|
||||
{user?.email}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm text-gray-500 text-left truncate max-w-full"
|
||||
title={`${getRightStr(selectedEstablishment?.role_type) || ''}${selectedEstablishment?.name ? ', ' + selectedEstablishment.name : ''}`}
|
||||
className="font-semibold text-base text-emerald-700 text-left truncate max-w-full"
|
||||
title={selectedEstablishment?.name || ''}
|
||||
>
|
||||
{selectedEstablishment?.name || ''}
|
||||
</div>
|
||||
<div
|
||||
className="italic text-sm text-emerald-600 text-left truncate max-w-full"
|
||||
title={getRightStr(selectedEstablishment?.role_type) || ''}
|
||||
>
|
||||
{getRightStr(selectedEstablishment?.role_type) || ''}
|
||||
{selectedEstablishment?.name
|
||||
? `, ${selectedEstablishment.name}`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
@ -150,12 +165,17 @@ const ProfileSelector = ({ onRoleChange, className = '' }) => {
|
||||
...establishments.map((establishment) => ({
|
||||
type: 'item',
|
||||
label: (
|
||||
<div className="text-left">
|
||||
<div className="font-bold">
|
||||
{getRightStr(establishment.role_type)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{establishment.name}
|
||||
<div className="flex items-center text-left">
|
||||
<Image
|
||||
src={establishment.logo ? `${BASE_URL}${establishment.logo}` : getGravatarUrl(user?.email)}
|
||||
alt="Profile"
|
||||
className="w-8 h-8 rounded-full object-cover shadow-md mr-3"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-bold ext-sm text-gray-500">{establishment.name}</div>
|
||||
<div className="italic text-sm text-gray-500">{getRightStr(establishment.role_type)}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
@ -35,7 +35,7 @@ function Sidebar({ currentPage, items, onCloseMobile }) {
|
||||
return (
|
||||
<div className="w-64 bg-stone-50 border-r h-full border-gray-200">
|
||||
<div className="border-b border-gray-200 ">
|
||||
<ProfileSelector className="border-none" />
|
||||
<ProfileSelector className="border-none h-24" />
|
||||
</div>
|
||||
<nav className="space-y-1 px-4 py-6">
|
||||
{items.map((item) => (
|
||||
|
||||
@ -78,10 +78,10 @@ export default function FilesGroupsManagement({
|
||||
useEffect(() => {
|
||||
if (selectedEstablishmentId) {
|
||||
Promise.all([
|
||||
fetchRegistrationSchoolFileMasters(),
|
||||
fetchRegistrationSchoolFileMasters(selectedEstablishmentId),
|
||||
fetchRegistrationFileGroups(selectedEstablishmentId),
|
||||
fetchRegistrationSchoolFileTemplates(),
|
||||
fetchRegistrationParentFileMasters(),
|
||||
fetchRegistrationSchoolFileTemplates(selectedEstablishmentId),
|
||||
fetchRegistrationParentFileMasters(selectedEstablishmentId),
|
||||
])
|
||||
.then(
|
||||
([
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import logger from '@/utils/logger';
|
||||
import { RecurrenceType } from '@/context/PlanningContext';
|
||||
import Calendar from '@/components/Calendar/Calendar';
|
||||
import ScheduleEventModal from '@/components/Structure/Planning/ScheduleEventModal';
|
||||
|
||||
@ -50,6 +50,10 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
const storedApiDocuseal = sessionStorage.getItem('apiDocuseal');
|
||||
return storedApiDocuseal ? JSON.parse(storedApiDocuseal) : null;
|
||||
});
|
||||
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
|
||||
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
|
||||
return storedLogo ? JSON.parse(storedLogo) : null;
|
||||
});
|
||||
|
||||
// Sauvegarder dans sessionStorage à chaque mise à jour
|
||||
const setSelectedEstablishmentId = (id) => {
|
||||
@ -95,6 +99,11 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
sessionStorage.setItem('apiDocuseal', JSON.stringify(api));
|
||||
};
|
||||
|
||||
const setSelectedEstablishmentLogo = (logo) => {
|
||||
setSelectedEstablishmentLogoState(logo);
|
||||
sessionStorage.setItem('selectedEstablishmentLogo', JSON.stringify(logo));
|
||||
};
|
||||
|
||||
/**
|
||||
* Fonction d'initialisation du contexte avec la session (appelée lors du login)
|
||||
* @param {*} session
|
||||
@ -114,6 +123,7 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
evaluation_frequency: role.establishment__evaluation_frequency,
|
||||
total_capacity: role.establishment__total_capacity,
|
||||
api_docuseal: role.establishment__api_docuseal,
|
||||
logo: role.establishment__logo,
|
||||
role_id: i,
|
||||
role_type: role.role_type,
|
||||
}));
|
||||
@ -136,6 +146,9 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
setApiDocuseal(
|
||||
userEstablishments[roleIndexDefault].api_docuseal
|
||||
);
|
||||
setSelectedEstablishmentLogo(
|
||||
userEstablishments[roleIndexDefault].logo
|
||||
);
|
||||
setProfileRole(userEstablishments[roleIndexDefault].role_type);
|
||||
}
|
||||
if (endInitFunctionHandler) {
|
||||
@ -156,6 +169,7 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
setSelectedEstablishmentEvaluationFrequencyState(null);
|
||||
setSelectedEstablishmentTotalCapacityState(null);
|
||||
setApiDocusealState(null);
|
||||
setSelectedEstablishmentLogoState(null);
|
||||
sessionStorage.clear();
|
||||
};
|
||||
|
||||
@ -172,6 +186,8 @@ export const EstablishmentProvider = ({ children }) => {
|
||||
setSelectedEstablishmentTotalCapacity,
|
||||
apiDocuseal,
|
||||
setApiDocuseal,
|
||||
selectedEstablishmentLogo,
|
||||
setSelectedEstablishmentLogo,
|
||||
selectedRoleId,
|
||||
setSelectedRoleId,
|
||||
profileRole,
|
||||
|
||||
@ -33,12 +33,13 @@ export const RecurrenceType = Object.freeze({
|
||||
|
||||
export const PlanningModes = Object.freeze({
|
||||
CLASS_SCHEDULE: 'classSchedule',
|
||||
PLANNING: 'planning',
|
||||
PLANNING: 'planning'
|
||||
});
|
||||
|
||||
export function PlanningProvider({
|
||||
children,
|
||||
modeSet = PlanningModes.PLANNING,
|
||||
readOnly = false
|
||||
}) {
|
||||
const [events, setEvents] = useState([]);
|
||||
const [schedules, setSchedules] = useState([]);
|
||||
@ -46,6 +47,7 @@ export function PlanningProvider({
|
||||
const [planningMode, setPlanningMode] = useState(modeSet);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
|
||||
const [parentView, setParentView] = useState(readOnly);
|
||||
const [hiddenSchedules, setHiddenSchedules] = useState([]);
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
@ -136,10 +138,12 @@ export function PlanningProvider({
|
||||
viewType,
|
||||
setViewType,
|
||||
hiddenSchedules,
|
||||
setHiddenSchedules,
|
||||
toggleScheduleVisibility,
|
||||
planningMode,
|
||||
reloadEvents,
|
||||
reloadPlanning,
|
||||
parentView
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -129,7 +129,6 @@ export const FE_ADMIN_MESSAGERIE_URL = '/admin/messagerie';
|
||||
// PARENT HOME
|
||||
export const FE_PARENTS_HOME_URL = '/parents';
|
||||
export const FE_PARENTS_MESSAGERIE_URL = '/parents/messagerie';
|
||||
export const FE_PARENTS_SETTINGS_URL = '/parents/settings';
|
||||
export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription';
|
||||
|
||||
// API DOCUSEAL
|
||||
|
||||
@ -6,11 +6,11 @@ export const RIGHTS = {
|
||||
|
||||
export function getRightStr(right) {
|
||||
if (right === RIGHTS.ADMIN) {
|
||||
return 'ADMINISTRATEUR';
|
||||
return 'Administrateur';
|
||||
} else if (right === RIGHTS.TEACHER) {
|
||||
return 'PROFESSEUR';
|
||||
return 'Professeur';
|
||||
} else if (right === RIGHTS.PARENT) {
|
||||
return 'PARENT';
|
||||
return 'Parent';
|
||||
} else {
|
||||
return 'NON DEFINI';
|
||||
}
|
||||
|
||||
22
conf/backend.env.default
Normal file
22
conf/backend.env.default
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
TZ="Europe/Paris"
|
||||
TEST_MODE=true
|
||||
CSRF_COOKIE_SECURE=true
|
||||
CSRF_COOKIE_DOMAIN=".localhost"
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
||||
CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
||||
BASE_URL=http://localhost:3000
|
||||
DEBUG=false
|
||||
EMAIL_HOST="smtp.hostinger.com"
|
||||
EMAIL_PORT="587"
|
||||
EMAIL_HOST_USER=""
|
||||
EMAIL_HOST_PASSWORD=''
|
||||
EMAIL_USE_TLS=true
|
||||
EMAIL_USE_SSL=false
|
||||
DB_NAME="school"
|
||||
DB_USER="postgres"
|
||||
DB_PASSWORD="postgres"
|
||||
DB_HOST="database"
|
||||
DB_PORT="5432"
|
||||
URL_DJANGO="http://localhost:8080"
|
||||
SECRET_KEY="<SIGNINGKEY>"
|
||||
@ -1,15 +1,19 @@
|
||||
services:
|
||||
redis:
|
||||
image: 'redis:latest'
|
||||
image: "redis:latest"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
expose:
|
||||
- 6379
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
|
||||
database:
|
||||
image: 'postgres:latest'
|
||||
image: "postgres:latest"
|
||||
expose:
|
||||
- 5432
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@ -20,17 +24,13 @@ services:
|
||||
image: git.v0id.ovh/n3wt-innov/n3wt-school/backend:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
- TEST_MODE=True
|
||||
env_file: "./conf/backend.env"
|
||||
links:
|
||||
- "database:database"
|
||||
- "redis:redis"
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
volumes:
|
||||
- ./conf/application.json:/Back-End/Subscriptions/Configuration/application.json
|
||||
command: python start.py
|
||||
|
||||
frontend:
|
||||
@ -40,6 +40,8 @@ services:
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
- NODE_ENV=production
|
||||
- NEXT_PUBLIC_API_URL=http://toto:8080
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- ./conf/env:/app/.env
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
|
||||
@ -1,55 +1,24 @@
|
||||
services:
|
||||
redis:
|
||||
image: "redis:latest"
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
expose:
|
||||
- 6379
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
|
||||
database:
|
||||
image: "postgres:latest"
|
||||
ports:
|
||||
- 5432:5432
|
||||
expose:
|
||||
- 5432
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: school
|
||||
TZ: Europe/Paris
|
||||
docuseal_db:
|
||||
image: postgres:latest
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DOCUSEAL_DB_HOST: docuseal_db
|
||||
POSTGRES_DB: docuseal
|
||||
ports:
|
||||
- 5433:5432 # port différent si besoin d'accès direct depuis l'hôte
|
||||
|
||||
docuseal:
|
||||
image: docuseal/docuseal:latest
|
||||
container_name: docuseal_app
|
||||
depends_on:
|
||||
- docuseal_db
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@docuseal_db:5432/docuseal
|
||||
volumes:
|
||||
- ./docuseal:/data/docuseal
|
||||
|
||||
caddy:
|
||||
image: caddy:2
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4000:4443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- docuseal
|
||||
|
||||
backend:
|
||||
build:
|
||||
@ -58,35 +27,15 @@ services:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./Back-End:/Back-End
|
||||
environment:
|
||||
- TZ=Europe/Paris
|
||||
- TEST_MODE=True
|
||||
- CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
||||
- CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
|
||||
env_file: "./conf/backend.env"
|
||||
links:
|
||||
- "database:database"
|
||||
- "redis:redis"
|
||||
depends_on:
|
||||
- redis
|
||||
- database
|
||||
#- docuseal
|
||||
command: python start.py
|
||||
|
||||
# frontend:
|
||||
# build:
|
||||
# context: ./Front-End
|
||||
# args:
|
||||
# - BUILD_MODE=development
|
||||
# ports:
|
||||
# - 3000:3000
|
||||
# volumes:
|
||||
# - ./Front-End:/app
|
||||
# env_file:
|
||||
# - .env
|
||||
# environment:
|
||||
# - TZ=Europe/Paris
|
||||
# depends_on:
|
||||
# - backend
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
|
||||
59
docs/tests_automatiques.md
Normal file
59
docs/tests_automatiques.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Documentation des tests automatiques
|
||||
|
||||
Ce document décrit la structure et l’utilisation du starter de tests automatiques pour valider tous les endpoints exposés par les fichiers `urls.py` des applications Django du Back-End.
|
||||
|
||||
## Objectif
|
||||
|
||||
- Vérifier automatiquement que chaque route définie dans le projet répond bien à une requête HTTP (statut attendu : 200, 401, 403, 404).
|
||||
- Permettre une validation rapide de la couverture des endpoints lors de chaque pipeline CI.
|
||||
|
||||
## Structure
|
||||
|
||||
- Le fichier principal est `Back-End/test_all_endpoints.py`.
|
||||
- Ce test utilise `pytest` et `pytest-django` pour parcourir toutes les routes du projet et effectuer une requête GET anonyme.
|
||||
- Les routes dynamiques (avec paramètres) sont ignorées dans ce starter.
|
||||
|
||||
## Exécution
|
||||
|
||||
1. Installer les dépendances si besoin :
|
||||
```sh
|
||||
pip install pytest pytest-django djangorestframework
|
||||
```
|
||||
2. Lancer les tests :
|
||||
```sh
|
||||
pytest Back-End/test_all_endpoints.py
|
||||
```
|
||||
|
||||
## Intégration CI
|
||||
|
||||
- Ajouter la commande de test dans votre pipeline CI (GitHub Actions, GitLab CI, Jenkins, etc.) :
|
||||
```sh
|
||||
pytest Back-End/test_all_endpoints.py
|
||||
```
|
||||
|
||||
## Personnalisation
|
||||
|
||||
- Pour tester les routes nécessitant une authentification ou des paramètres, compléter le test avec des cas spécifiques.
|
||||
- Pour chaque nouvelle route, le test s’exécutera automatiquement.
|
||||
|
||||
## Ajout
|
||||
|
||||
- Ajout d'un fichier de tests automatiques dédié à Auth (`test_auth_endpoints.py`) pour tester tous les endpoints Auth (GET, dynamiques, login JWT, accès protégé, structure JSON).
|
||||
- Les tests créent un utilisateur et un profilRole de test, réalisent un login, et vérifient les accès aux endpoints principaux, y compris les routes dynamiques et protégées.
|
||||
|
||||
## Utilisation
|
||||
|
||||
- Lancer les tests avec :
|
||||
```bash
|
||||
pytest --json-report --json-report-file=tests_automatiques.json
|
||||
```
|
||||
- Le rapport inclura les résultats détaillés pour chaque endpoint Auth.
|
||||
|
||||
## Extension
|
||||
|
||||
- Étendre sur le même modèle pour les autres applications (School, Subscriptions, etc.)
|
||||
- Ajouter des tests POST/PUT/DELETE et des cas d’erreur/permissions.
|
||||
|
||||
---
|
||||
|
||||
Pour toute question ou évolution, se référer au ticket associé.
|
||||
41
initDocusealUsers.sh
Normal file
41
initDocusealUsers.sh
Normal file
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Attendre que le service docuseal soit prêt
|
||||
until curl -s http://docuseal:3000 > /dev/null; do
|
||||
echo "Waiting for docuseal to be ready..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
encrypt_password() {
|
||||
local password=$1
|
||||
ruby -e "require 'bcrypt'; puts BCrypt::Password.create('$password')"
|
||||
}
|
||||
|
||||
# Mot de passe encrypté
|
||||
ENCRYPTED_PASSWORD=$(encrypt_password "$USER_PASSWORD")
|
||||
|
||||
# Insérer les données dans les tables accounts et users si elles n'existent pas déjà
|
||||
PGPASSWORD=$POSTGRES_PASSWORD psql -v ON_ERROR_STOP=1 --host=database --port=5432 --username="$POSTGRES_USER" --dbname=docuseal <<-EOSQL
|
||||
-- Vérifier l'existence du compte
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM accounts WHERE name = '$USER_COMPANY') THEN
|
||||
-- Insérer dans la table accounts
|
||||
INSERT INTO accounts (name, timezone, locale, created_at, updated_at, uuid) VALUES
|
||||
('$USER_COMPANY', 'UTC', 'en', NOW(), NOW(), gen_random_uuid());
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
|
||||
-- Vérifier l'existence de l'utilisateur
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM users WHERE email = '$USER_EMAIL') THEN
|
||||
-- Insérer dans la table users
|
||||
INSERT INTO users (first_name, last_name, email, encrypted_password, account_id, role, sign_in_count, failed_attempts, created_at, updated_at, uuid, otp_required_for_login) VALUES
|
||||
('$USER_FIRST_NAME', '$USER_LAST_NAME', '$USER_EMAIL', '$ENCRYPTED_PASSWORD', (SELECT id FROM accounts WHERE name = '$USER_COMPANY'), 'admin', 1, 0, NOW(), NOW(), gen_random_uuid(), false);
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
EOSQL
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n3wt-school",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"release": "standard-version",
|
||||
|
||||
Reference in New Issue
Block a user