mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 15:33:22 +00:00
Merge remote-tracking branch 'origin/develop'
This commit is contained in:
58
.github/copilot-instructions.md
vendored
Normal file
58
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
# Instructions Copilot - Projet N3WT-SCHOOL
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger ou améliorer le projet N3WT-SCHOOL de manière minimaliste et fonctionnelle, sans dépendances inutiles.
|
||||
|
||||
## Architecture du projet
|
||||
|
||||
### Structure
|
||||
|
||||
- **Backend** : Python Django (dossier `Back-End/`)
|
||||
- **Frontend** : NextJS (dossier `Front-End/`)
|
||||
- **Tests frontend** : `Front-End/src/test/`
|
||||
- **Code frontend** : `Front-End/src/`
|
||||
|
||||
## Gestion des tickets
|
||||
|
||||
### Règles générales
|
||||
|
||||
- Chaque **nouvelle fonctionnalité** ou **correction** nécessite un ticket Gitea
|
||||
- **Exemptions** : modifications documentaires, refactoring, chore, style
|
||||
|
||||
### Cycle de vie d'un ticket
|
||||
|
||||
1. **Création** → label `etat/En Pause`
|
||||
2. **Affectation** → label `etat/En Cours`
|
||||
3. **Développement terminé** → label `etat/Codé`
|
||||
4. **Tests validés** → label `etat/Testé`
|
||||
|
||||
### Gestion des branches
|
||||
|
||||
- **Base** : branche `develop`
|
||||
- **Nomenclature** : `<type>-<nom_ticket>-<numero>` (ex: `feat-ma_super_feat-1234`)
|
||||
- **Types** : feat, fix, docs, style, refactor, test, chore
|
||||
|
||||
## Exigences qualité
|
||||
|
||||
Pour le front-end, les exigences de qualité sont les suivantes :
|
||||
|
||||
- **Linting** : Utiliser ESLint pour le code JavaScript/TypeScript
|
||||
- **Formatage** : Utiliser Prettier pour le formatage du code
|
||||
- **Tests** : Utiliser Jest pour les tests unitaires et d'intégration
|
||||
- Référence : [frontend guideline](./instructions/frontend.instruction.md)
|
||||
|
||||
### Tests
|
||||
|
||||
- Tests unitaires obligatoires pour chaque nouvelle fonctionnalité
|
||||
- Localisation : `Front-End/src/test/`
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation en français pour les nouvelles fonctionnalités (si applicable)
|
||||
- Référence : [documentation guidelines](./instructions/documentation.instruction.md)
|
||||
|
||||
## Références
|
||||
|
||||
- **Tickets** : [issues guidelines](./instructions/issues.instruction.md)
|
||||
- **Commits** : [commit guidelines](./instructions/general-commit.instruction.md)
|
||||
6
.github/instructions/documentation.instruction.md
vendored
Normal file
6
.github/instructions/documentation.instruction.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
La documentation doit être en français et claire pour les utilisateurs francophones.
|
||||
Toutes la documentation doit être dans le dossier docs/
|
||||
Seul les fichiers README.md, CHANGELOG.md doivent être à la racine.
|
||||
La documentation doit être conscise et pertinente, sans répétitions inutiles entre les documents.
|
||||
Tout ce qui concerne la gestion de projet, roadmap ne doit pas apparaître dans la documentation.
|
||||
Tout ce qui concerne les modifications de type chore n'a pas besoin d'être documenté.
|
||||
0
.github/instructions/frontend.instruction.md
vendored
Normal file
0
.github/instructions/frontend.instruction.md
vendored
Normal file
28
.github/instructions/general-commit.instruction.md
vendored
Normal file
28
.github/instructions/general-commit.instruction.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
### **Commit Guidelines (Conventionnel)**
|
||||
|
||||
Les messages de commits se basent sur **Conventional Commits** (https://www.conventionalcommits.org/en/v1.0.0/) , pour une meilleure gestion des versions et une génération automatique du changelog.
|
||||
|
||||
#### **Format standard** :
|
||||
|
||||
```
|
||||
<type>(<scope>): <description> [#<ticket-id>]
|
||||
```
|
||||
|
||||
- **Types autorisés** :
|
||||
|
||||
- `feat` : Nouvelle fonctionnalité.
|
||||
- `fix` : Correction d’un bug.
|
||||
- `docs` : Modifications liées à la documentation.
|
||||
- `style` : Mise en forme du code (pas de changements fonctionnels).
|
||||
- `refactor` : Refactorisation sans ajout de fonctionnalités ni correction de bugs.
|
||||
- `test` : Ajout ou modification de tests.
|
||||
- `chore` : Maintenance ou tâches diverses (ex. mise à jour des dépendances).
|
||||
|
||||
- **Scope (optionnel)** : Précisez une partie spécifique du projet (`backend`, `frontend`, `API`, etc.).
|
||||
|
||||
- **Exemples** :
|
||||
```
|
||||
feat(frontend): ajout de la gestion des utilisateurs dans le dashboard [#1]
|
||||
fix(backend): correction du bug lié à l'authentification JWT [#1]
|
||||
docs: mise à jour du README avec les nouvelles instructions d’installation [#2]
|
||||
```
|
||||
22
.github/instructions/issues.instruction.md
vendored
Normal file
22
.github/instructions/issues.instruction.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
- Chaque nouveau ticket doit faire l'objet d'une analyse pour définir les modifications à effectuer.
|
||||
- l'analyse doit être présente dans la description du ticket au format:
|
||||
|
||||
# Description de la modification
|
||||
|
||||
Définir la modification
|
||||
|
||||
# Solution envisagée
|
||||
|
||||
Définir l'implementation avec l'impact dans le code
|
||||
|
||||
# Modification documentaire
|
||||
|
||||
Définir les documents à modifier si il existe il peut ne pas y en avoir.
|
||||
|
||||
# Tests
|
||||
|
||||
Définir ici comment va être tester la fonctionnalité et les cas de test.
|
||||
|
||||
- Une fois créer son label doit passer à `etat/En Pause`
|
||||
- Les labels 'subject' sont ensuites ajouté en fonction du sujet de modification.
|
||||
- le label 'workload' est défini en fonction de si la modification de code est longue (gros impact) à faire ou rapide (faible impact)
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,10 +1,4 @@
|
||||
Back-End/*/Configuration/application.json
|
||||
.venv/
|
||||
__pycache__/
|
||||
.env
|
||||
node_modules/
|
||||
Back-End/*/migrations/*
|
||||
Back-End/documents
|
||||
Back-End/data
|
||||
Back-End/*.dmp
|
||||
Back-End/staticfiles
|
||||
hardcoded-strings-report.md
|
||||
@ -0,0 +1 @@
|
||||
cd $(dirname "$0")/../Front-End/ && npm run lint-light
|
||||
@ -1,4 +1 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
node scripts/prepare-commit-msg.js "$1" "$2"
|
||||
13
.vscode/launch.json
vendored
Normal file
13
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"webRoot": "${workspaceFolder}/Front-End/",
|
||||
"url": "http://localhost:3000",
|
||||
},
|
||||
]
|
||||
}
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"eslint.workingDirectories": ["./Front-End"],
|
||||
"giteaCopilotTools.owner": "n3wt-innov",
|
||||
"giteaCopilotTools.repo": "n3wt-school",
|
||||
"giteaCopilotTools.autoRefreshInterval": 15
|
||||
}
|
||||
13
.vscode/tasks.json
vendored
Normal file
13
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start Frontend Dev Server",
|
||||
"type": "shell",
|
||||
"command": "npm run dev",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
7
Back-End/.gitignore
vendored
Normal file
7
Back-End/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
__pycache__
|
||||
/*/migrations/*
|
||||
documents
|
||||
data
|
||||
*.dmp
|
||||
staticfiles
|
||||
/*/Configuration/application*.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
@ -4,22 +4,37 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.core.validators import EmailValidator
|
||||
|
||||
class Profile(AbstractUser):
|
||||
class Droits(models.IntegerChoices):
|
||||
PROFIL_UNDEFINED = -1, _('NON DEFINI')
|
||||
PROFIL_ECOLE = 0, _('ECOLE')
|
||||
PROFIL_ADMIN = 1, _('ADMIN')
|
||||
PROFIL_PARENT = 2, _('PARENT')
|
||||
|
||||
email = models.EmailField(max_length=255, unique=True, default="", validators=[EmailValidator()])
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
|
||||
REQUIRED_FIELDS = ('password', )
|
||||
|
||||
roleIndexLoginDefault = models.IntegerField(default=0)
|
||||
code = models.CharField(max_length=200, default="", blank=True)
|
||||
datePeremption = models.CharField(max_length=200, default="", blank=True)
|
||||
droit = models.IntegerField(choices=Droits, default=Droits.PROFIL_UNDEFINED)
|
||||
estConnecte = models.BooleanField(default=False, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.email + " - " + str(self.droit)
|
||||
return self.email
|
||||
|
||||
class ProfileRole(models.Model):
|
||||
class RoleType(models.IntegerChoices):
|
||||
PROFIL_UNDEFINED = -1, _('NON DEFINI')
|
||||
PROFIL_ECOLE = 0, _('ECOLE')
|
||||
PROFIL_ADMIN = 1, _('ADMIN')
|
||||
PROFIL_PARENT = 2, _('PARENT')
|
||||
|
||||
profile = models.ForeignKey('Profile', on_delete=models.CASCADE, related_name='roles')
|
||||
role_type = models.IntegerField(choices=RoleType.choices, default=RoleType.PROFIL_UNDEFINED)
|
||||
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='profile_roles')
|
||||
is_active = models.BooleanField(default=False)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.profile.email} - {self.get_role_type_display()}"
|
||||
|
||||
class Directeur(models.Model):
|
||||
profile_role = models.OneToOneField("ProfileRole", on_delete=models.CASCADE, related_name='directeur_profile')
|
||||
last_name = models.CharField(max_length=100)
|
||||
first_name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name} ({self.profile_role.profile.email})"
|
||||
20
Back-End/Auth/pagination.py
Normal file
20
Back-End/Auth/pagination.py
Normal file
@ -0,0 +1,20 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
from N3wtSchool import settings
|
||||
|
||||
class CustomProfilesPagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = settings.NB_MAX_PAGE
|
||||
page_size = settings.NB_RESULT_PROFILES_PER_PAGE
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return ({
|
||||
'links': {
|
||||
'next': self.get_next_link(),
|
||||
'previous': self.get_previous_link()
|
||||
},
|
||||
'count': self.page.paginator.count,
|
||||
'page_size': self.page_size,
|
||||
'max_page_size' : self.max_page_size,
|
||||
'profilesRoles': data }
|
||||
)
|
||||
@ -1,51 +1,160 @@
|
||||
from rest_framework import serializers
|
||||
from Auth.models import Profile
|
||||
from django.core.exceptions import ValidationError
|
||||
from Auth.models import Profile, ProfileRole
|
||||
from Establishment.models import Establishment
|
||||
from Subscriptions.models import Guardian, RegistrationForm
|
||||
from School.models import Teacher
|
||||
from N3wtSchool import settings
|
||||
from django.utils import timezone
|
||||
import pytz
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
password = serializers.CharField(write_only=True)
|
||||
roles = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active']
|
||||
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles', 'roleIndexLoginDefault']
|
||||
extra_kwargs = {'password': {'write_only': True}}
|
||||
|
||||
def get_roles(self, obj):
|
||||
roles = ProfileRole.objects.filter(profile=obj)
|
||||
roles_data = []
|
||||
for role in roles:
|
||||
# Récupérer l'ID de l'associated_person en fonction du type de rôle
|
||||
if role.role_type == ProfileRole.RoleType.PROFIL_PARENT:
|
||||
guardian = Guardian.objects.filter(profile_role=role).first()
|
||||
id_associated_person = guardian.id if guardian else None
|
||||
else:
|
||||
teacher = Teacher.objects.filter(profile_role=role).first()
|
||||
id_associated_person = teacher.id if teacher else None
|
||||
|
||||
roles_data.append({
|
||||
'id_associated_person': id_associated_person,
|
||||
'role_type': role.role_type,
|
||||
'establishment': role.establishment.id,
|
||||
'establishment_name': role.establishment.name,
|
||||
'is_active': role.is_active,
|
||||
})
|
||||
return roles_data
|
||||
|
||||
def create(self, validated_data):
|
||||
user = Profile(
|
||||
username=validated_data['username'],
|
||||
email=validated_data['email'],
|
||||
is_active=validated_data['is_active'],
|
||||
droit=validated_data['droit']
|
||||
code=validated_data.get('code', ''),
|
||||
datePeremption=validated_data.get('datePeremption', '')
|
||||
)
|
||||
user.set_password(validated_data['password'])
|
||||
user.full_clean()
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def to_representation(self, instance):
|
||||
ret = super().to_representation(instance)
|
||||
ret['password'] = '********'
|
||||
return ret
|
||||
|
||||
class ProfilUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active']
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': True, 'required': False}
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
password = validated_data.pop('password', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
|
||||
if password:
|
||||
instance.set_password(password)
|
||||
instance.save()
|
||||
|
||||
instance.full_clean()
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance):
|
||||
ret = super().to_representation(instance)
|
||||
ret['password'] = '********'
|
||||
ret['roles'] = self.get_roles(instance)
|
||||
return ret
|
||||
|
||||
class ProfileRoleSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
profile = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), required=False)
|
||||
profile_data = ProfileSerializer(write_only=True, required=False)
|
||||
associated_profile_email = serializers.SerializerMethodField()
|
||||
associated_person = serializers.SerializerMethodField()
|
||||
updated_date_formatted = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ProfileRole
|
||||
fields = ['id', 'role_type', 'establishment', 'is_active', 'profile', 'profile_data', 'associated_profile_email', 'associated_person', 'updated_date_formatted']
|
||||
|
||||
def create(self, validated_data):
|
||||
profile_data = validated_data.pop('profile_data', None)
|
||||
profile = validated_data.pop('profile', None)
|
||||
|
||||
if profile_data:
|
||||
profile_serializer = ProfileSerializer(data=profile_data)
|
||||
profile_serializer.is_valid(raise_exception=True)
|
||||
profile = profile_serializer.save()
|
||||
elif profile:
|
||||
profile = Profile.objects.get(id=profile.id)
|
||||
|
||||
profile_role = ProfileRole.objects.create(profile=profile, **validated_data)
|
||||
return profile_role
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
profile_data = validated_data.pop('profile_data', None)
|
||||
profile = validated_data.pop('profile', None)
|
||||
|
||||
if profile_data:
|
||||
profile_serializer = ProfileSerializer(instance.profile, data=profile_data)
|
||||
profile_serializer.is_valid(raise_exception=True)
|
||||
profile = profile_serializer.save()
|
||||
elif profile:
|
||||
profile = Profile.objects.get(id=profile.id)
|
||||
|
||||
if profile:
|
||||
instance.profile = profile
|
||||
|
||||
instance.role_type = validated_data.get('role_type', instance.role_type)
|
||||
instance.establishment_id = validated_data.get('establishment', instance.establishment.id)
|
||||
instance.is_active = validated_data.get('is_active', instance.is_active)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def get_associated_profile_email(self, obj):
|
||||
if obj.profile:
|
||||
return obj.profile.email
|
||||
return None
|
||||
|
||||
def get_associated_person(self, obj):
|
||||
if obj.role_type == ProfileRole.RoleType.PROFIL_PARENT:
|
||||
guardian = Guardian.objects.filter(profile_role=obj).first()
|
||||
if guardian:
|
||||
students = guardian.student_set.all()
|
||||
students_list = []
|
||||
for student in students:
|
||||
registration_form = RegistrationForm.objects.filter(student=student).first()
|
||||
registration_status = registration_form.status if registration_form else None
|
||||
students_list.append({
|
||||
"id": f"{student.id}",
|
||||
"student_name": f"{student.last_name} {student.first_name}",
|
||||
"registration_status": registration_status
|
||||
})
|
||||
return {
|
||||
"id": guardian.id,
|
||||
"guardian_name": f"{guardian.last_name} {guardian.first_name}",
|
||||
"students": students_list
|
||||
}
|
||||
else:
|
||||
teacher = Teacher.objects.filter(profile_role=obj).first()
|
||||
if teacher:
|
||||
classes = teacher.schoolclass_set.all()
|
||||
classes_list = [{"id": classe.id, "name": classe.atmosphere_name} for classe in classes]
|
||||
specialities = teacher.specialities.all()
|
||||
specialities_list = [{"name": speciality.name, "color_code": speciality.color_code} for speciality in specialities]
|
||||
return {
|
||||
"id": teacher.id,
|
||||
"teacher_name": f"{teacher.last_name} {teacher.first_name}",
|
||||
"classes": classes_list,
|
||||
"specialities": specialities_list
|
||||
}
|
||||
return None
|
||||
|
||||
def get_updated_date_formatted(self, obj):
|
||||
utc_time = timezone.localtime(obj.updated_date)
|
||||
local_tz = pytz.timezone(settings.TZ_APPLI)
|
||||
local_time = utc_time.astimezone(local_tz)
|
||||
|
||||
return local_time.strftime("%d-%m-%Y %H:%M")
|
||||
@ -1,61 +0,0 @@
|
||||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<title>Monteschool</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="logo-circular centered">
|
||||
<img src="{% static 'img/logo_min.svg' %}" alt="">
|
||||
</div>
|
||||
<h1 class="login-heading">Authentification</h1>
|
||||
<form class="centered login-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<label for="userInput">{{ form.email.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon user"></i>
|
||||
</span>
|
||||
<input type="text" id="userInput" placeholder='Identifiant' name="email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="userInput">{{ form.password.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon key"></i>
|
||||
</span>
|
||||
<input type="password" id="userInput" placeholder="Mot de passe" name="password">
|
||||
</div>
|
||||
<p style="color:#FF0000">{{ message }}</p>
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<p style="color:#FF0000">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p style="color:#FF0000">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<label><a class="right" href='/reset/{{code}}'>Mot de passe oublié ?</a></label>
|
||||
</div>
|
||||
<div class="form-group-submit">
|
||||
<button href="" class="btn primary" type="submit" name="connect">Se Connecter</button>
|
||||
<br>
|
||||
<h2>Pas de compte ?</h2>
|
||||
<br>
|
||||
<button href="" class="btn " name="register">S'inscrire</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,64 +0,0 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<title>Monteschool</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container negative full-size">
|
||||
<div class="logo-circular centered">
|
||||
<img src="{% static 'img/logo_min.svg' %}" alt="">
|
||||
</div>
|
||||
<h1 class="login-heading">Nouveau Mot de Passe</h1>
|
||||
<form class="negative centered login-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group" hidden>
|
||||
<label for="userInput">Identifiant</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon user"></i>
|
||||
</span>
|
||||
<input type="text" id="userInput" placeholder='Identifiant' value='{{ identifiant }}' name="email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="password">{{ form.password1.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon key"></i>
|
||||
</span>
|
||||
<input type="password" id="password" placeholder="{{ form.password1.label }}" name="password1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="confirmPassword">{{ form.password2.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon key"></i>
|
||||
</span>
|
||||
<input type="password" id="confirmPassword" placeholder="{{ form.password2.label }}" name="password2">
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#FF0000">{{ message }}</p>
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<p style="color:#FF0000">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p style="color:#FF0000">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="form-group-submit negative">
|
||||
<button href="" class="btn primary" type="submit" name="save">Enregistrer</button>
|
||||
<br>
|
||||
<button href="" class="btn" type="submit" name="cancel">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,37 +0,0 @@
|
||||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<title>Monteschool</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container negative full-size">
|
||||
<div class="logo-circular centered">
|
||||
<img src="{% static 'img/logo_min.svg' %}" alt="">
|
||||
</div>
|
||||
<h1 class="login-heading"> Réinitialiser Mot de Passe</h1>
|
||||
<form class="negative centered login-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<label for="username">Identifiant</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon user"></i>
|
||||
</span>
|
||||
<input type="text" id="username" placeholder="Identifiant" name="email">
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#FF0000">{{ message }}</p>
|
||||
<div class="form-group-submit negative">
|
||||
<button href="" class="btn primary" type="submit" name="reinit">Réinitialiser</button>
|
||||
<br>
|
||||
<button href="" class="btn" type="submit" name="cancel">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,64 +0,0 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<title>Monteschool</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container negative full-size">
|
||||
<div class="logo-circular centered">
|
||||
<img src="{% static 'img/logo_min.svg' %}" alt="">
|
||||
</div>
|
||||
<h1 class="login-heading">S'inscrire</h1>
|
||||
<form class="negative centered login-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<label for="username">{{ form.email.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon user"></i>
|
||||
</span>
|
||||
<input type="text" id="username" placeholder="Identifiant" name="email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="password">{{ form.password1.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon key"></i>
|
||||
</span>
|
||||
<input type="password" id="password" placeholder="{{ form.password1.label }}" name="password1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="confirmPassword">{{ form.password2.label }}</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="icon-ctn">
|
||||
<i class="icon key"></i>
|
||||
</span>
|
||||
<input type="password" id="confirmPassword" placeholder="{{ form.password2.label }}" name="password2">
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:#FF0000">{{ message }}</p>
|
||||
{% if form.errors %}
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<p style="color:#FF0000">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p style="color:#FF0000">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="form-group-submit negative">
|
||||
<button href="" class="btn primary" type="submit" name="validate">Enregistrer</button>
|
||||
<br>
|
||||
<button href="" class="btn" name="cancel">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -2,21 +2,21 @@ from django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
import Auth.views
|
||||
from Auth.views import ProfileView, ProfileListView, SessionView, LoginView, SubscribeView, NewPasswordView, ResetPasswordView
|
||||
from Auth.views import ProfileRoleView, ProfileRoleSimpleView, ProfileSimpleView, ProfileView, SessionView, LoginView, RefreshJWTView, SubscribeView, NewPasswordView, ResetPasswordView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^csrf$', Auth.views.csrf, name='csrf'),
|
||||
|
||||
re_path(r'^login$', LoginView.as_view(), name="login"),
|
||||
re_path(r'^refreshJWT$', RefreshJWTView.as_view(), name="refresh_jwt"),
|
||||
re_path(r'^subscribe$', SubscribeView.as_view(), name='subscribe'),
|
||||
re_path(r'^newPassword$', NewPasswordView.as_view(), name='newPassword'),
|
||||
re_path(r'^resetPassword/([a-zA-Z]+)$', ResetPasswordView.as_view(), name='resetPassword'),
|
||||
re_path(r'^infoSession$', Auth.views.infoSession, name='infoSession'),
|
||||
re_path(r'^resetPassword/(?P<code>[a-zA-Z]+)$', ResetPasswordView.as_view(), name='resetPassword'),
|
||||
re_path(r'^infoSession$', SessionView.as_view(), name='infoSession'),
|
||||
|
||||
re_path(r'^profiles$', ProfileListView.as_view(), name="profile"),
|
||||
re_path(r'^profile$', ProfileView.as_view(), name="profile"),
|
||||
re_path(r'^profile/([0-9]+)$', ProfileView.as_view(), name="profile"),
|
||||
re_path(r'^profiles$', ProfileView.as_view(), name="profile"),
|
||||
re_path(r'^profiles/(?P<id>[0-9]+)$', ProfileSimpleView.as_view(), name="profile"),
|
||||
|
||||
# Test SESSION VIEW
|
||||
re_path(r'^session$', SessionView.as_view(), name="session"),
|
||||
re_path(r'^profileRoles$', ProfileRoleView.as_view(), name="profileRoles"),
|
||||
re_path(r'^profileRoles/(?P<id>[0-9]+)$', ProfileRoleSimpleView.as_view(), name="profileRoles"),
|
||||
]
|
||||
@ -4,47 +4,81 @@ from django.http.response import JsonResponse
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt, csrf_protect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.cache import cache
|
||||
from django.middleware.csrf import get_token
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework import status
|
||||
from Auth.pagination import CustomProfilesPagination
|
||||
|
||||
from datetime import datetime
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
import json
|
||||
|
||||
from . import validator
|
||||
from .models import Profile
|
||||
from .models import Profile, ProfileRole
|
||||
from rest_framework.decorators import action, api_view
|
||||
from django.db.models import Q
|
||||
|
||||
from Auth.serializers import ProfileSerializer, ProfilUpdateSerializer
|
||||
from Subscriptions.models import RegistrationForm
|
||||
from Subscriptions.signals import clear_cache
|
||||
import Subscriptions.mailManager as mailer
|
||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||
from Subscriptions.models import RegistrationForm, Guardian
|
||||
import N3wtSchool.mailManager as mailer
|
||||
import Subscriptions.util as util
|
||||
import logging
|
||||
from N3wtSchool import bdd, error, settings
|
||||
|
||||
from N3wtSchool import bdd, error
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
logger = logging.getLogger("AuthViews")
|
||||
|
||||
|
||||
@swagger_auto_schema(
|
||||
method='get',
|
||||
operation_description="Obtenir un token CSRF",
|
||||
responses={200: openapi.Response('Token CSRF', schema=openapi.Schema(type=openapi.TYPE_OBJECT, properties={
|
||||
'csrfToken': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}))}
|
||||
)
|
||||
@api_view(['GET'])
|
||||
def csrf(request):
|
||||
token = get_token(request)
|
||||
return JsonResponse({'csrfToken': token})
|
||||
|
||||
class SessionView(APIView):
|
||||
|
||||
def post(self, request):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Vérifier une session utilisateur",
|
||||
manual_parameters=[openapi.Parameter('Authorization', openapi.IN_HEADER, type=openapi.TYPE_STRING, description='Bearer token')],
|
||||
responses={
|
||||
200: openapi.Response('Session valide', schema=openapi.Schema(type=openapi.TYPE_OBJECT, properties={
|
||||
'user': openapi.Schema(type=openapi.TYPE_OBJECT, properties={
|
||||
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'roleIndexLoginDefault': openapi.Schema(type=openapi.TYPE_INTEGER),
|
||||
'roles': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_OBJECT, properties={
|
||||
'role_type': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'establishment': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}))
|
||||
})
|
||||
})),
|
||||
401: openapi.Response('Session invalide')
|
||||
}
|
||||
)
|
||||
def get(self, request):
|
||||
token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1]
|
||||
|
||||
try:
|
||||
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
||||
print(f'decode : {decoded_token}')
|
||||
user_id = decoded_token.get('id')
|
||||
user = Profile.objects.get(id=user_id)
|
||||
|
||||
userid = decoded_token.get('user_id')
|
||||
user = Profile.objects.get(id=userid)
|
||||
roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name')
|
||||
response_data = {
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'email': user.email,
|
||||
'role': user.droit, # Assure-toi que le champ 'droit' existe et contient le rôle
|
||||
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
||||
'roles': list(roles)
|
||||
}
|
||||
}
|
||||
return JsonResponse(response_data, status=status.HTTP_200_OK)
|
||||
@ -53,208 +87,433 @@ class SessionView(APIView):
|
||||
except jwt.InvalidTokenError:
|
||||
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
class ProfileListView(APIView):
|
||||
class ProfileView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir la liste des profils",
|
||||
responses={200: ProfileSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
profilsList = bdd.getAllObjects(_objectName=Profile)
|
||||
profils_serializer = ProfileSerializer(profilsList, many=True)
|
||||
return JsonResponse(profils_serializer.data, safe=False)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ProfileView(APIView):
|
||||
def get(self, request, _id):
|
||||
profil=bdd.getObject(Profile, "id", _id)
|
||||
profil_serializer=ProfileSerializer(profil)
|
||||
return JsonResponse(profil_serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Créer un nouveau profil",
|
||||
request_body=ProfileSerializer,
|
||||
responses={
|
||||
200: ProfileSerializer,
|
||||
400: 'Données invalides'
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
profil_data=JSONParser().parse(request)
|
||||
print(f'{profil_data}')
|
||||
profil_data = JSONParser().parse(request)
|
||||
profil_serializer = ProfileSerializer(data=profil_data)
|
||||
|
||||
if profil_serializer.is_valid():
|
||||
profil_serializer.save()
|
||||
|
||||
profil = profil_serializer.save()
|
||||
return JsonResponse(profil_serializer.data, safe=False)
|
||||
|
||||
|
||||
return JsonResponse(profil_serializer.errors, safe=False)
|
||||
|
||||
def put(self, request, _id):
|
||||
data=JSONParser().parse(request)
|
||||
profil = Profile.objects.get(id=_id)
|
||||
profil_serializer = ProfilUpdateSerializer(profil, data=data)
|
||||
if profil_serializer.is_valid():
|
||||
profil_serializer.save()
|
||||
return JsonResponse("Updated Successfully", safe=False)
|
||||
|
||||
return JsonResponse(profil_serializer.errors, safe=False)
|
||||
|
||||
def delete(self, request, _id):
|
||||
return bdd.delete_object(Profile, _id)
|
||||
|
||||
def infoSession(request):
|
||||
profilCache = cache.get('session_cache')
|
||||
if profilCache:
|
||||
return JsonResponse({"cacheSession":True,"typeProfil":profilCache.droit, "username":profilCache.email}, safe=False)
|
||||
else:
|
||||
return JsonResponse({"cacheSession":False,"typeProfil":Profile.Droits.PROFIL_UNDEFINED, "username":""}, safe=False)
|
||||
return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ProfileSimpleView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir un profil par son ID",
|
||||
responses={200: ProfileSerializer}
|
||||
)
|
||||
def get(self, request, id):
|
||||
profil = bdd.getObject(Profile, "id", id)
|
||||
profil_serializer = ProfileSerializer(profil)
|
||||
return JsonResponse(profil_serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Mettre à jour un profil",
|
||||
request_body=ProfileSerializer,
|
||||
responses={
|
||||
200: 'Mise à jour réussie',
|
||||
400: 'Données invalides'
|
||||
}
|
||||
)
|
||||
def put(self, request, id):
|
||||
data = JSONParser().parse(request)
|
||||
profil = Profile.objects.get(id=id)
|
||||
profil_serializer = ProfileSerializer(profil, data=data)
|
||||
if profil_serializer.is_valid():
|
||||
profil_serializer.save()
|
||||
return JsonResponse(profil_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(profil_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprimer un profil",
|
||||
responses={200: 'Suppression réussie'}
|
||||
)
|
||||
def delete(self, request, id):
|
||||
return bdd.delete_object(Profile, id)
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LoginView(APIView):
|
||||
|
||||
def get(self, request):
|
||||
return JsonResponse({
|
||||
'errorFields':'',
|
||||
'errorMessage':'',
|
||||
'profil':0,
|
||||
}, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Connexion utilisateur",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email', 'password'],
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
200: openapi.Response('Connexion réussie', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'token': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'refresh': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
)),
|
||||
400: openapi.Response('Connexion échouée', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'errorFields': openapi.Schema(type=openapi.TYPE_OBJECT),
|
||||
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
))
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
data=JSONParser().parse(request)
|
||||
data = JSONParser().parse(request)
|
||||
validatorAuthentication = validator.ValidatorAuthentication(data=data)
|
||||
retour = error.returnMessage[error.WRONG_ID]
|
||||
validationOk, errorFields = validatorAuthentication.validate()
|
||||
user = None
|
||||
|
||||
if validationOk:
|
||||
user = authenticate(
|
||||
email=data.get('email'),
|
||||
password=data.get('password'),
|
||||
)
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
user.estConnecte = True
|
||||
user.save()
|
||||
clear_cache()
|
||||
retour = ''
|
||||
else:
|
||||
retour = error.returnMessage[error.PROFIL_INACTIVE]
|
||||
# Vérifier si l'utilisateur a un role actif
|
||||
has_active_role = ProfileRole.objects.filter(profile=user, is_active=True).first()
|
||||
if not has_active_role:
|
||||
return JsonResponse({"errorMessage": "Profil inactif"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
login(request, user)
|
||||
user.save()
|
||||
retour = ''
|
||||
access_token, refresh_token = makeToken(user)
|
||||
|
||||
return JsonResponse({
|
||||
'token': access_token,
|
||||
'refresh': refresh_token
|
||||
}, safe=False)
|
||||
|
||||
# Génération du token JWT
|
||||
# jwt_token = jwt.encode({
|
||||
# 'id': user.id,
|
||||
# 'email': user.email,
|
||||
# 'role': "admin"
|
||||
# }, settings.SECRET_KEY, algorithm='HS256')
|
||||
else:
|
||||
retour = error.returnMessage[error.WRONG_ID]
|
||||
|
||||
|
||||
return JsonResponse({
|
||||
'errorFields':errorFields,
|
||||
'errorMessage':retour,
|
||||
'profil':user.id if user else -1,
|
||||
'droit':user.droit if user else -1,
|
||||
#'jwtToken':jwt_token if profil != -1 else ''
|
||||
}, safe=False)
|
||||
'errorFields': errorFields,
|
||||
'errorMessage': retour,
|
||||
}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def makeToken(user):
|
||||
"""
|
||||
Fonction pour créer un token JWT pour l'utilisateur donné.
|
||||
"""
|
||||
try:
|
||||
# Récupérer tous les rôles de l'utilisateur actifs
|
||||
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': roles,
|
||||
'type': 'access',
|
||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
|
||||
'iat': datetime.utcnow(),
|
||||
}
|
||||
|
||||
access_token = jwt.encode(access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
|
||||
# Générer le Refresh Token (exp: 7 jours)
|
||||
refresh_payload = {
|
||||
'user_id': user.id,
|
||||
'type': 'refresh',
|
||||
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
|
||||
'iat': datetime.utcnow(),
|
||||
}
|
||||
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
|
||||
return access_token, refresh_token
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la création du token: {str(e)}")
|
||||
return None
|
||||
|
||||
class RefreshJWTView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Rafraîchir le token d'accès",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['refresh'],
|
||||
properties={
|
||||
'refresh': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
200: openapi.Response('Token rafraîchi avec succès', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'token': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'refresh': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
}
|
||||
)),
|
||||
400: openapi.Response('Échec du rafraîchissement', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
))
|
||||
}
|
||||
)
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
def post(self, request):
|
||||
data = JSONParser().parse(request)
|
||||
refresh_token = data.get("refresh")
|
||||
logger.info(f"Token reçu: {refresh_token[:20]}...") # Ne pas logger le token complet pour la sécurité
|
||||
|
||||
if not refresh_token:
|
||||
return JsonResponse({'errorMessage': 'Refresh token manquant'}, status=400)
|
||||
|
||||
try:
|
||||
# Décoder le Refresh Token
|
||||
logger.info("Tentative de décodage du token")
|
||||
logger.info(f"Algorithme utilisé: {settings.SIMPLE_JWT['ALGORITHM']}")
|
||||
|
||||
# Vérifier le format du token avant décodage
|
||||
token_parts = refresh_token.split('.')
|
||||
if len(token_parts) != 3:
|
||||
logger.error("Format de token invalide - pas 3 parties")
|
||||
return JsonResponse({'errorMessage': 'Format de token invalide'}, status=400)
|
||||
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings.SIMPLE_JWT['SIGNING_KEY'],
|
||||
algorithms=[settings.SIMPLE_JWT['ALGORITHM']] # Noter le passage en liste
|
||||
)
|
||||
|
||||
logger.info(f"Token décodé avec succès. Type: {payload.get('type')}")
|
||||
# Vérifier s'il s'agit bien d'un Refresh Token
|
||||
if payload.get('type') != 'refresh':
|
||||
return JsonResponse({'errorMessage': 'Token invalide'}, status=400)
|
||||
|
||||
# Récupérer les informations utilisateur
|
||||
user = Profile.objects.get(id=payload['user_id'])
|
||||
if not user:
|
||||
return JsonResponse({'errorMessage': 'Utilisateur non trouvé'}, status=404)
|
||||
|
||||
new_access_token, new_refresh_token = makeToken(user)
|
||||
|
||||
return JsonResponse({'token': new_access_token, 'refresh': new_refresh_token}, status=200)
|
||||
|
||||
except ExpiredSignatureError as e:
|
||||
logger.error(f"Token expiré: {str(e)}")
|
||||
return JsonResponse({'errorMessage': 'Refresh token expiré'}, status=400)
|
||||
except InvalidTokenError as e:
|
||||
logger.error(f"Token invalide: {str(e)}")
|
||||
return JsonResponse({'errorMessage': f'Token invalide: {str(e)}'}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur inattendue: {str(e)}")
|
||||
return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class SubscribeView(APIView):
|
||||
|
||||
def get(self, request):
|
||||
return JsonResponse({
|
||||
'message':'',
|
||||
'errorFields':'',
|
||||
'errorMessage':''
|
||||
}, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Inscription utilisateur",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'establishment_id', openapi.IN_QUERY,
|
||||
description="ID de l'établissement",
|
||||
type=openapi.TYPE_INTEGER,
|
||||
required=True
|
||||
)
|
||||
],
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email', 'password1', 'password2'],
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'password1': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'password2': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
200: openapi.Response('Inscription réussie', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'message': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'errorFields': openapi.Schema(type=openapi.TYPE_OBJECT),
|
||||
'id': openapi.Schema(type=openapi.TYPE_INTEGER)
|
||||
}
|
||||
))
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection=JSONParser().parse(request)
|
||||
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
establishment_id = newProfilConnection['establishment_id']
|
||||
|
||||
validatorSubscription = validator.ValidatorSubscription(data=newProfilConnection)
|
||||
validationOk, errorFields = validatorSubscription.validate()
|
||||
|
||||
|
||||
if validationOk:
|
||||
|
||||
# On vérifie que l'email existe : si ce n'est pas le cas, on retourne une erreur
|
||||
profil = bdd.getProfile(Profile.objects.all(), newProfilConnection.get('email'))
|
||||
if profil == None:
|
||||
if profil is None:
|
||||
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
|
||||
else:
|
||||
if profil.is_active:
|
||||
retourErreur=error.returnMessage[error.PROFIL_ACTIVE]
|
||||
return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields, "id":profil.id}, safe=False)
|
||||
# Vérifier si le profil a déjà un rôle actif pour l'établissement donné
|
||||
active_roles = ProfileRole.objects.filter(profile=profil, establishment=establishment_id, is_active=True)
|
||||
if active_roles.exists():
|
||||
retourErreur = error.returnMessage[error.PROFIL_ACTIVE]
|
||||
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False)
|
||||
else:
|
||||
try:
|
||||
profil.set_password(newProfilConnection.get('password1'))
|
||||
profil.is_active = True
|
||||
profil.full_clean()
|
||||
profil.save()
|
||||
clear_cache()
|
||||
|
||||
# Récupérer le ProfileRole existant pour l'établissement et le profil
|
||||
profile_role = ProfileRole.objects.filter(profile=profil, establishment=establishment_id).first()
|
||||
if profile_role:
|
||||
profile_role.is_active = True
|
||||
profile_role.save()
|
||||
else:
|
||||
# Si aucun ProfileRole n'existe, en créer un nouveau
|
||||
role_data = {
|
||||
'profile': profil.id,
|
||||
'establishment': establishment_id,
|
||||
'is_active': True
|
||||
}
|
||||
role_serializer = ProfileRoleSerializer(data=role_data)
|
||||
if role_serializer.is_valid():
|
||||
role_serializer.save()
|
||||
else:
|
||||
return JsonResponse(role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
retour = error.returnMessage[error.MESSAGE_ACTIVATION_PROFILE]
|
||||
retourErreur=''
|
||||
return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields, "id":profil.id}, safe=False)
|
||||
retourErreur = ''
|
||||
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": profil.id}, safe=False)
|
||||
except ValidationError as e:
|
||||
retourErreur = error.returnMessage[error.WRONG_MAIL_FORMAT]
|
||||
return JsonResponse({'message':retour,'errorMessage':retourErreur, "errorFields":errorFields}, safe=False)
|
||||
|
||||
return JsonResponse({'message':retour, 'errorMessage':retourErreur, "errorFields":errorFields, "id":-1}, safe=False)
|
||||
|
||||
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False)
|
||||
|
||||
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields, "id": -1}, safe=False)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class NewPasswordView(APIView):
|
||||
def get(self, request):
|
||||
return JsonResponse({
|
||||
'message':'',
|
||||
'errorFields':'',
|
||||
'errorMessage':''
|
||||
}, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Demande de nouveau mot de passe",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email'],
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
200: openapi.Response('Demande réussie', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'message': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'errorFields': openapi.Schema(type=openapi.TYPE_OBJECT)
|
||||
}
|
||||
))
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection=JSONParser().parse(request)
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
|
||||
validatorNewPassword = validator.ValidatorNewPassword(data=newProfilConnection)
|
||||
validationOk, errorFields = validatorNewPassword.validate()
|
||||
if validationOk:
|
||||
|
||||
profil = bdd.getProfile(Profile.objects.all(), newProfilConnection.get('email'))
|
||||
if profil == None:
|
||||
if profil is None:
|
||||
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
|
||||
else:
|
||||
# Génération d'une URL provisoire pour modifier le mot de passe
|
||||
profil.code = util.genereRandomCode(12)
|
||||
profil.datePeremption = util.calculeDatePeremption(util._now(), settings.EXPIRATION_URL_NB_DAYS)
|
||||
profil.save()
|
||||
clear_cache()
|
||||
retourErreur = ''
|
||||
retour = error.returnMessage[error.MESSAGE_REINIT_PASSWORD]%(newProfilConnection.get('email'))
|
||||
mailer.envoieReinitMotDePasse(newProfilConnection.get('email'), profil.code)
|
||||
try:
|
||||
# Génération d'une URL provisoire pour modifier le mot de passe
|
||||
profil.code = util.genereRandomCode(12)
|
||||
profil.datePeremption = util.calculeDatePeremption(util._now(), settings.EXPIRATION_URL_NB_DAYS)
|
||||
profil.save()
|
||||
retourErreur = ''
|
||||
retour = error.returnMessage[error.MESSAGE_REINIT_PASSWORD] % (newProfilConnection.get('email'))
|
||||
mailer.envoieReinitMotDePasse(newProfilConnection.get('email'), profil.code)
|
||||
except ValidationError as e:
|
||||
retourErreur = error.returnMessage[error.WRONG_MAIL_FORMAT]
|
||||
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False)
|
||||
|
||||
return JsonResponse({'message':retour, 'errorMessage':retourErreur, "errorFields":errorFields}, safe=False)
|
||||
return JsonResponse({'message': retour, 'errorMessage': retourErreur, "errorFields": errorFields}, safe=False)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ResetPasswordView(APIView):
|
||||
def get(self, request, _uuid):
|
||||
return JsonResponse({
|
||||
'message':'',
|
||||
'errorFields':'',
|
||||
'errorMessage':''
|
||||
}, safe=False)
|
||||
|
||||
def post(self, request, _uuid):
|
||||
retourErreur = error.returnMessage[error.BAD_URL]
|
||||
@swagger_auto_schema(
|
||||
operation_description="Réinitialisation du mot de passe",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['password1', 'password2'],
|
||||
properties={
|
||||
'password1': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'password2': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
),
|
||||
responses={
|
||||
200: openapi.Response('Réinitialisation réussie', schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'message': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'errorMessage': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'errorFields': openapi.Schema(type=openapi.TYPE_OBJECT)
|
||||
}
|
||||
))
|
||||
}
|
||||
)
|
||||
def post(self, request, code):
|
||||
retourErreur = ''
|
||||
retour = ''
|
||||
newProfilConnection=JSONParser().parse(request)
|
||||
newProfilConnection = JSONParser().parse(request)
|
||||
|
||||
validatorResetPassword = validator.ValidatorResetPassword(data=newProfilConnection)
|
||||
validationOk, errorFields = validatorResetPassword.validate()
|
||||
|
||||
profil = bdd.getObject(Profile, "code", _uuid)
|
||||
|
||||
profil = bdd.getObject(Profile, "code", code)
|
||||
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]
|
||||
|
||||
@ -262,7 +521,149 @@ class ResetPasswordView(APIView):
|
||||
profil.code = ''
|
||||
profil.datePeremption = ''
|
||||
profil.save()
|
||||
clear_cache()
|
||||
retourErreur=''
|
||||
retourErreur = ''
|
||||
|
||||
return JsonResponse({'message':retour, "errorMessage":retourErreur, "errorFields":errorFields}, safe=False)
|
||||
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
|
||||
|
||||
class ProfileRoleView(APIView):
|
||||
pagination_class = CustomProfilesPagination
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir la liste des profile_roles",
|
||||
responses={200: ProfileRoleSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
filter = request.GET.get('filter', '').strip()
|
||||
page_size = request.GET.get('page_size', None)
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
|
||||
# Gestion du page_size
|
||||
if page_size is not None:
|
||||
try:
|
||||
page_size = int(page_size)
|
||||
except ValueError:
|
||||
page_size = settings.NB_RESULT_PROFILES_PER_PAGE
|
||||
|
||||
if establishment_id is None:
|
||||
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Récupérer les ProfileRole en fonction du filtre
|
||||
profiles_roles_List = ProfileRole.objects.filter(establishment_id=establishment_id)
|
||||
|
||||
if filter == 'parents':
|
||||
profiles_roles_List = profiles_roles_List.filter(role_type=ProfileRole.RoleType.PROFIL_PARENT)
|
||||
elif filter == 'school':
|
||||
profiles_roles_List = profiles_roles_List.filter(
|
||||
Q(role_type=ProfileRole.RoleType.PROFIL_ECOLE) |
|
||||
Q(role_type=ProfileRole.RoleType.PROFIL_ADMIN)
|
||||
)
|
||||
else:
|
||||
return JsonResponse({'error': 'Filtre invalide'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Trier les résultats par date de mise à jour
|
||||
profiles_roles_List = profiles_roles_List.distinct().order_by('-updated_date')
|
||||
|
||||
if not profiles_roles_List:
|
||||
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)
|
||||
|
||||
# Pagination
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(profiles_roles_List, request)
|
||||
|
||||
if page is not None:
|
||||
profile_roles_serializer = ProfileRoleSerializer(page, many=True)
|
||||
response_data = paginator.get_paginated_response(profile_roles_serializer.data)
|
||||
return JsonResponse(response_data, safe=False)
|
||||
|
||||
return JsonResponse({'error': 'aucune donnée trouvée', 'count': 0}, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Créer un nouveau profile_role",
|
||||
request_body=ProfileRoleSerializer,
|
||||
responses={
|
||||
200: ProfileRoleSerializer,
|
||||
400: 'Données invalides'
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
profile_role_data = JSONParser().parse(request)
|
||||
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
||||
|
||||
if profile_role_serializer.is_valid():
|
||||
profile_role = profile_role_serializer.save()
|
||||
return JsonResponse(profile_role_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(profile_role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class ProfileRoleSimpleView(APIView):
|
||||
@swagger_auto_schema(
|
||||
operation_description="Obtenir un profile_role par son ID",
|
||||
responses={200: ProfileRoleSerializer}
|
||||
)
|
||||
def get(self, request, id):
|
||||
profile_role = bdd.getObject(ProfileRole, "id", id)
|
||||
profile_role_serializer = ProfileRoleSerializer(profile_role)
|
||||
return JsonResponse(profile_role_serializer.data, safe=False)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Mettre à jour un profile_role",
|
||||
request_body=ProfileRoleSerializer,
|
||||
responses={
|
||||
200: 'Mise à jour réussie',
|
||||
400: 'Données invalides'
|
||||
}
|
||||
)
|
||||
def put(self, request, id):
|
||||
data = JSONParser().parse(request)
|
||||
profile_role = ProfileRole.objects.get(id=id)
|
||||
profile_role_serializer = ProfileRoleSerializer(profile_role, data=data)
|
||||
if profile_role_serializer.is_valid():
|
||||
profile_role_serializer.save()
|
||||
return JsonResponse(profile_role_serializer.data, safe=False)
|
||||
|
||||
return JsonResponse(profile_role_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprimer un profile_role",
|
||||
responses={200: 'Suppression réussie'}
|
||||
)
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
# Récupérer le ProfileRole
|
||||
profile_role = ProfileRole.objects.get(id=id)
|
||||
profile = profile_role.profile
|
||||
|
||||
# Vérifier si le ProfileRole est de type PARENT
|
||||
if profile_role.role_type == ProfileRole.RoleType.PROFIL_PARENT:
|
||||
guardian = Guardian.objects.filter(profile_role=profile_role).first()
|
||||
if guardian:
|
||||
# Vérifier si ce Guardian est rattaché à des élèves
|
||||
for student in guardian.student_set.all():
|
||||
# Vérifier si l'élève n'a pas d'autres Guardians
|
||||
other_guardians = student.guardians.exclude(id=guardian.id)
|
||||
if not other_guardians.exists():
|
||||
return JsonResponse(
|
||||
{"error": f"Impossible de supprimer ce profil car l'élève {student.first_name} {student.last_name} n'aura plus de responsable légal."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Supprimer le ProfileRole
|
||||
profile_role.delete()
|
||||
|
||||
# Vérifier si le profil n'a plus de rôles associés
|
||||
if not ProfileRole.objects.filter(profile=profile).exists():
|
||||
profile.delete()
|
||||
|
||||
return JsonResponse({'message': 'Suppression réussie'}, safe=False)
|
||||
|
||||
except ProfileRole.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"error": "ProfileRole non trouvé."},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"error": f"Une erreur est survenue : {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
0
Back-End/Common/__init__.py
Normal file
0
Back-End/Common/__init__.py
Normal file
3
Back-End/Common/admin.py
Normal file
3
Back-End/Common/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
9
Back-End/Common/apps.py
Normal file
9
Back-End/Common/apps.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'Common'
|
||||
|
||||
def ready(self):
|
||||
import Common.signals
|
||||
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
43
Back-End/Common/models.py
Normal file
43
Back-End/Common/models.py
Normal file
@ -0,0 +1,43 @@
|
||||
from django.db import models
|
||||
|
||||
class Domain(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
cycle = models.IntegerField(choices=[(1, 'Cycle 1'), (2, 'Cycle 2'), (3, 'Cycle 3'), (4, 'Cycle 4')])
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} (Cycle {self.cycle})"
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name='categories')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class PaymentPlanType(models.Model):
|
||||
code = models.CharField(max_length=50, unique=True)
|
||||
label = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
class PaymentModeType(models.Model):
|
||||
code = models.CharField(max_length=50, unique=True)
|
||||
label = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
class Cycle(models.Model):
|
||||
number = models.IntegerField(unique=True)
|
||||
label = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return f"Cycle {self.number} - {self.label}"
|
||||
|
||||
class Level(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
cycle = models.ForeignKey(Cycle, on_delete=models.CASCADE, related_name='levels')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
15
Back-End/Common/serializers.py
Normal file
15
Back-End/Common/serializers.py
Normal file
@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
from Common.models import (
|
||||
Domain,
|
||||
Category
|
||||
)
|
||||
|
||||
class DomainSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = '__all__'
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = '__all__'
|
||||
98
Back-End/Common/signals.py
Normal file
98
Back-End/Common/signals.py
Normal file
@ -0,0 +1,98 @@
|
||||
import json
|
||||
import os
|
||||
from django.db.models.signals import post_migrate
|
||||
from django.dispatch import receiver
|
||||
from Common.models import Domain, Category, PaymentModeType, PaymentPlanType, Cycle, Level
|
||||
from School.models import Competency
|
||||
|
||||
@receiver(post_migrate)
|
||||
def common_post_migrate(sender, **kwargs):
|
||||
if sender.name != "School":
|
||||
return
|
||||
|
||||
# Chemin absolu vers le répertoire Back-End
|
||||
backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Chemins vers les fichiers JSON
|
||||
json_files = [
|
||||
("Cycle1.json", 1),
|
||||
("Cycle2.json", 2),
|
||||
("Cycle3.json", 3),
|
||||
("Cycle4.json", 4),
|
||||
]
|
||||
|
||||
for file_name, cycle in json_files:
|
||||
json_file_path = os.path.join(backend_dir, "competences", file_name)
|
||||
|
||||
if not os.path.exists(json_file_path):
|
||||
print(f"Fichier JSON introuvable : {json_file_path}")
|
||||
continue
|
||||
|
||||
with open(json_file_path, 'r', encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
|
||||
for domain_data in data['domaines']:
|
||||
# Vérifiez si le domaine existe déjà
|
||||
domain, _ = Domain.objects.get_or_create(name=domain_data['nom'], cycle=cycle)
|
||||
|
||||
for category_data in domain_data['categories']:
|
||||
# Vérifiez si la catégorie existe déjà
|
||||
category, _ = Category.objects.get_or_create(name=category_data['nom'], domain=domain)
|
||||
|
||||
for competency_data in category_data['competences']:
|
||||
# Vérifiez si la compétence existe déjà
|
||||
competency, _ = Competency.objects.get_or_create(
|
||||
name=competency_data['nom'],
|
||||
end_of_cycle=competency_data.get('fin_cycle', False),
|
||||
level=competency_data.get('niveau'),
|
||||
category=category
|
||||
)
|
||||
print(f"Données importées depuis : {json_file_path}")
|
||||
|
||||
payment_mode_types = [
|
||||
{"code": "SEPA", "label": "Prélèvement SEPA"},
|
||||
{"code": "TRANSFER", "label": "Virement"},
|
||||
{"code": "CHECK", "label": "Chèque"},
|
||||
{"code": "CASH", "label": "Espèce"},
|
||||
]
|
||||
for mode in payment_mode_types:
|
||||
PaymentModeType.objects.get_or_create(code=mode["code"], defaults={"label": mode["label"]})
|
||||
|
||||
payment_plan_types = [
|
||||
{"code": "ONE_TIME", "label": "1 fois"},
|
||||
{"code": "THREE_TIMES", "label": "3 fois"},
|
||||
{"code": "TEN_TIMES", "label": "10 fois"},
|
||||
{"code": "TWELVE_TIMES", "label": "12 fois"},
|
||||
]
|
||||
for plan in payment_plan_types:
|
||||
PaymentPlanType.objects.get_or_create(code=plan["code"], defaults={"label": plan["label"]})
|
||||
|
||||
# Création des cycles
|
||||
cycles_data = [
|
||||
{"number": 1, "label": "Cycle 1"},
|
||||
{"number": 2, "label": "Cycle 2"},
|
||||
{"number": 3, "label": "Cycle 3"},
|
||||
{"number": 4, "label": "Cycle 4"},
|
||||
]
|
||||
cycle_objs = {}
|
||||
for cycle in cycles_data:
|
||||
obj, _ = Cycle.objects.get_or_create(number=cycle["number"], defaults={"label": cycle["label"]})
|
||||
cycle_objs[cycle["number"]] = obj
|
||||
|
||||
# Création des niveaux et association au cycle
|
||||
levels_data = [
|
||||
{"name": "TPS", "cycle": 1},
|
||||
{"name": "PS", "cycle": 1},
|
||||
{"name": "MS", "cycle": 1},
|
||||
{"name": "GS", "cycle": 1},
|
||||
{"name": "CP", "cycle": 2},
|
||||
{"name": "CE1", "cycle": 2},
|
||||
{"name": "CE2", "cycle": 2},
|
||||
{"name": "CM1", "cycle": 3},
|
||||
{"name": "CM2", "cycle": 3},
|
||||
]
|
||||
for level in levels_data:
|
||||
Level.objects.get_or_create(
|
||||
name=level["name"],
|
||||
defaults={"cycle": cycle_objs[level["cycle"]]}
|
||||
)
|
||||
3
Back-End/Common/tests.py
Normal file
3
Back-End/Common/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
Back-End/Common/urls.py
Normal file
14
Back-End/Common/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from .views import (
|
||||
DomainListCreateView, DomainDetailView,
|
||||
CategoryListCreateView, CategoryDetailView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^domains$', DomainListCreateView.as_view(), name="domain_list_create"),
|
||||
re_path(r'^domains/(?P<id>[0-9]+)$', DomainDetailView.as_view(), name="domain_detail"),
|
||||
|
||||
re_path(r'^categories$', CategoryListCreateView.as_view(), name="category_list_create"),
|
||||
re_path(r'^categories/(?P<id>[0-9]+)$', CategoryDetailView.as_view(), name="category_detail"),
|
||||
]
|
||||
110
Back-End/Common/views.py
Normal file
110
Back-End/Common/views.py
Normal file
@ -0,0 +1,110 @@
|
||||
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.views import APIView
|
||||
from rest_framework import status
|
||||
from .models import (
|
||||
Domain,
|
||||
Category
|
||||
)
|
||||
from .serializers import (
|
||||
DomainSerializer,
|
||||
CategorySerializer
|
||||
)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class DomainListCreateView(APIView):
|
||||
def get(self, request):
|
||||
domains = Domain.objects.all()
|
||||
serializer = DomainSerializer(domains, many=True)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
|
||||
def post(self, request):
|
||||
data = JSONParser().parse(request)
|
||||
serializer = DomainSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
|
||||
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class DomainDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
try:
|
||||
domain = Domain.objects.get(id=id)
|
||||
serializer = DomainSerializer(domain)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
except Domain.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def put(self, request, id):
|
||||
try:
|
||||
domain = Domain.objects.get(id=id)
|
||||
except Domain.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
data = JSONParser().parse(request)
|
||||
serializer = DomainSerializer(domain, data=data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
domain = Domain.objects.get(id=id)
|
||||
domain.delete()
|
||||
return JsonResponse({'message': 'Deleted'}, safe=False)
|
||||
except Domain.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Répète la même logique pour Category, Competency, EstablishmentCompetency
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class CategoryListCreateView(APIView):
|
||||
def get(self, request):
|
||||
categories = Category.objects.all()
|
||||
serializer = CategorySerializer(categories, many=True)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
|
||||
def post(self, request):
|
||||
data = JSONParser().parse(request)
|
||||
serializer = CategorySerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
|
||||
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class CategoryDetailView(APIView):
|
||||
def get(self, request, id):
|
||||
try:
|
||||
category = Category.objects.get(id=id)
|
||||
serializer = CategorySerializer(category)
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
except Category.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def put(self, request, id):
|
||||
try:
|
||||
category = Category.objects.get(id=id)
|
||||
except Category.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
data = JSONParser().parse(request)
|
||||
serializer = CategorySerializer(category, data=data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return JsonResponse(serializer.data, safe=False)
|
||||
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
category = Category.objects.get(id=id)
|
||||
category.delete()
|
||||
return JsonResponse({'message': 'Deleted'}, safe=False)
|
||||
except Category.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
@ -3,6 +3,7 @@
|
||||
# The first instruction is what image we want to base our container on
|
||||
# We Use an official Python runtime as a parent image
|
||||
FROM python:3.12.7
|
||||
WORKDIR /Back-End
|
||||
|
||||
# Allows docker to cache installed dependencies between builds
|
||||
COPY requirements.txt requirements.txt
|
||||
@ -10,11 +11,5 @@ RUN pip install -r requirements.txt
|
||||
|
||||
# Mounts the application code to the image
|
||||
COPY . .
|
||||
WORKDIR /Back-End
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV DJANGO_SETTINGS_MODULE N3wtSchool.settings
|
||||
ENV DJANGO_SUPERUSER_PASSWORD=admin
|
||||
ENV DJANGO_SUPERUSER_USERNAME=admin
|
||||
ENV DJANGO_SUPERUSER_EMAIL=admin@n3wtschool.com
|
||||
|
||||
1
Back-End/DocuSeal/__init__.py
Normal file
1
Back-End/DocuSeal/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file is intentionally left blank to make this directory a Python package.
|
||||
9
Back-End/DocuSeal/urls.py
Normal file
9
Back-End/DocuSeal/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path, re_path
|
||||
from .views import generate_jwt_token, clone_template, remove_template, download_template
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'generateToken$', generate_jwt_token, name='generate_jwt_token'),
|
||||
re_path(r'cloneTemplate$', clone_template, name='clone_template'),
|
||||
re_path(r'removeTemplate/(?P<id>[0-9]+)$', remove_template, name='remove_template'),
|
||||
re_path(r'downloadTemplate/(?P<slug>[\w-]+)$', download_template, name='download_template')
|
||||
]
|
||||
200
Back-End/DocuSeal/views.py
Normal file
200
Back-End/DocuSeal/views.py
Normal file
@ -0,0 +1,200 @@
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
import jwt
|
||||
import datetime
|
||||
import requests
|
||||
from Establishment.models import Establishment
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
def generate_jwt_token(request):
|
||||
# Récupérer l'établissement concerné (par ID ou autre info transmise)
|
||||
establishment_id = request.data.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Récupérer les données de la requête
|
||||
user_email = request.data.get('user_email')
|
||||
documents_urls = request.data.get('documents_urls', [])
|
||||
template_id = request.data.get('id')
|
||||
|
||||
if not user_email:
|
||||
return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Utiliser la clé API de l'établissement comme secret JWT
|
||||
jwt_secret = establishment.api_docuseal
|
||||
jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM']
|
||||
expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA']
|
||||
|
||||
payload = {
|
||||
'user_email': user_email,
|
||||
'documents_urls': documents_urls,
|
||||
'template_id': template_id,
|
||||
'exp': datetime.datetime.utcnow() + expiration_delta
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm)
|
||||
return Response({'token': token}, status=status.HTTP_200_OK)
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
def clone_template(request):
|
||||
# Récupérer l'établissement concerné
|
||||
establishment_id = request.data.get('establishment_id')
|
||||
print(f"establishment_id : {establishment_id}")
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Récupérer les données de la requête
|
||||
document_id = request.data.get('templateId')
|
||||
email = request.data.get('email')
|
||||
is_required = request.data.get('is_required')
|
||||
|
||||
# Vérifier les données requises
|
||||
if not document_id:
|
||||
return Response({'error': 'template ID is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# URL de l'API de DocuSeal pour cloner le template
|
||||
clone_url = f'https://docuseal.com/api/templates/{document_id}/clone'
|
||||
|
||||
# Faire la requête pour cloner le template
|
||||
try:
|
||||
response = requests.post(clone_url, headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to clone template'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
|
||||
if is_required:
|
||||
# URL de l'API de DocuSeal pour créer une submission
|
||||
submission_url = f'https://docuseal.com/api/submissions'
|
||||
|
||||
try:
|
||||
clone_id = data['id']
|
||||
response = requests.post(submission_url, json={
|
||||
'template_id': clone_id,
|
||||
'send_email': False,
|
||||
'submitters': [{'email': email}]
|
||||
}, headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to create submission'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
data[0]['id'] = clone_id
|
||||
return Response(data[0], status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
else:
|
||||
print(f'NOT REQUIRED -> on ne crée pas de submission')
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['DELETE'])
|
||||
def remove_template(request, id):
|
||||
# Récupérer l'établissement concerné
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# URL de l'API de DocuSeal pour supprimer le template
|
||||
|
||||
clone_url = f'https://docuseal.com/api/templates/{id}'
|
||||
|
||||
try:
|
||||
response = requests.delete(clone_url, headers={
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to remove template'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(['GET'])
|
||||
def download_template(request, slug):
|
||||
# Récupérer l'établissement concerné
|
||||
establishment_id = request.GET.get('establishment_id')
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
establishment = Establishment.objects.get(id=establishment_id)
|
||||
except Establishment.DoesNotExist:
|
||||
return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Vérifier la clé API reçue dans le header
|
||||
api_key = request.headers.get('X-Auth-Token')
|
||||
if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
|
||||
return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# Vérifier les données requises
|
||||
if not slug:
|
||||
return Response({'error': 'slug is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# URL de l'API de DocuSeal pour télécharger le template
|
||||
download_url = f'https://docuseal.com/submitters/{slug}/download'
|
||||
|
||||
try:
|
||||
response = requests.get(download_url, headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': establishment.api_docuseal
|
||||
})
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return Response({'error': 'Failed to download template'}, status=response.status_code)
|
||||
|
||||
data = response.json()
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.RequestException as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
1
Back-End/Establishment/__init__.py
Normal file
1
Back-End/Establishment/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
default_app_config = 'Establishment.apps.EstablishmentConfig'
|
||||
3
Back-End/Establishment/admin.py
Normal file
3
Back-End/Establishment/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
7
Back-End/Establishment/apps.py
Normal file
7
Back-End/Establishment/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class EstablishmentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'Establishment'
|
||||
|
||||
|
||||
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
38
Back-End/Establishment/models.py
Normal file
38
Back-End/Establishment/models.py
Normal file
@ -0,0 +1,38 @@
|
||||
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')
|
||||
SECONDAIRE = 3, _('Secondaire')
|
||||
|
||||
class EvaluationFrequency(models.IntegerChoices):
|
||||
TRIMESTER = 1, _("Trimestre")
|
||||
SEMESTER = 2, _("Semestre")
|
||||
YEAR = 3, _("Année")
|
||||
|
||||
class Establishment(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
address = models.CharField(max_length=255)
|
||||
total_capacity = models.IntegerField()
|
||||
establishment_type = ArrayField(models.IntegerField(choices=StructureType.choices))
|
||||
evaluation_frequency = models.IntegerField(choices=EvaluationFrequency.choices, default=EvaluationFrequency.TRIMESTER)
|
||||
licence_code = models.CharField(max_length=100, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
api_docuseal = models.CharField(max_length=255, blank=True, null=True)
|
||||
logo = models.FileField(
|
||||
upload_to=registration_logo_upload_to,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
91
Back-End/Establishment/serializers.py
Normal file
91
Back-End/Establishment/serializers.py
Normal file
@ -0,0 +1,91 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Establishment
|
||||
from School.models import SchoolClass, Teacher, Speciality, Fee, Discount, PaymentMode, PaymentPlan
|
||||
from Subscriptions.models import RegistrationForm, RegistrationFileGroup
|
||||
from Auth.models import Profile
|
||||
|
||||
class EstablishmentSerializer(serializers.ModelSerializer):
|
||||
profile_count = serializers.SerializerMethodField()
|
||||
profiles = serializers.SerializerMethodField()
|
||||
school_class_count = serializers.SerializerMethodField()
|
||||
school_classes = serializers.SerializerMethodField()
|
||||
teacher_count = serializers.SerializerMethodField()
|
||||
teachers = serializers.SerializerMethodField()
|
||||
speciality_count = serializers.SerializerMethodField()
|
||||
specialities = serializers.SerializerMethodField()
|
||||
fee_count = serializers.SerializerMethodField()
|
||||
fees = serializers.SerializerMethodField()
|
||||
discount_count = serializers.SerializerMethodField()
|
||||
discounts = serializers.SerializerMethodField()
|
||||
active_payment_mode_count = serializers.SerializerMethodField()
|
||||
active_payment_modes = serializers.SerializerMethodField()
|
||||
active_payment_plan_count = serializers.SerializerMethodField()
|
||||
active_payment_plans = serializers.SerializerMethodField()
|
||||
file_group_count = serializers.SerializerMethodField()
|
||||
file_groups = serializers.SerializerMethodField()
|
||||
registration_form_count = serializers.SerializerMethodField()
|
||||
registration_forms = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Establishment
|
||||
fields = '__all__'
|
||||
|
||||
def get_profile_count(self, obj):
|
||||
return Profile.objects.filter(roles__establishment=obj).distinct().count()
|
||||
|
||||
def get_profiles(self, obj):
|
||||
return list(Profile.objects.filter(roles__establishment=obj).distinct().values_list('email', flat=True))
|
||||
|
||||
def get_school_class_count(self, obj):
|
||||
return SchoolClass.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_school_classes(self, obj):
|
||||
return list(SchoolClass.objects.filter(establishment=obj).distinct().values_list('atmosphere_name', flat=True))
|
||||
|
||||
def get_teacher_count(self, obj):
|
||||
return Teacher.objects.filter(profile_role__establishment=obj).distinct().count()
|
||||
|
||||
def get_teachers(self, obj):
|
||||
return list(Teacher.objects.filter(profile_role__establishment=obj).distinct().values_list('last_name', 'first_name'))
|
||||
|
||||
def get_speciality_count(self, obj):
|
||||
return Speciality.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_specialities(self, obj):
|
||||
return list(Speciality.objects.filter(establishment=obj).distinct().values_list('name', flat=True))
|
||||
|
||||
def get_fee_count(self, obj):
|
||||
return Fee.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_fees(self, obj):
|
||||
return list(Fee.objects.filter(establishment=obj).distinct().values_list('name', flat=True))
|
||||
|
||||
def get_discount_count(self, obj):
|
||||
return Discount.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_discounts(self, obj):
|
||||
return list(Discount.objects.filter(establishment=obj).distinct().values_list('name', flat=True))
|
||||
|
||||
def get_active_payment_mode_count(self, obj):
|
||||
return PaymentMode.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_active_payment_modes(self, obj):
|
||||
return list(PaymentMode.objects.filter(establishment=obj).distinct().values_list('mode', flat=True))
|
||||
|
||||
def get_active_payment_plan_count(self, obj):
|
||||
return PaymentPlan.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_active_payment_plans(self, obj):
|
||||
return list(PaymentPlan.objects.filter(establishment=obj).distinct().values_list('plan_type', flat=True))
|
||||
|
||||
def get_file_group_count(self, obj):
|
||||
return RegistrationFileGroup.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_file_groups(self, obj):
|
||||
return list(RegistrationFileGroup.objects.filter(establishment=obj).distinct().values_list('name', flat=True))
|
||||
|
||||
def get_registration_form_count(self, obj):
|
||||
return RegistrationForm.objects.filter(establishment=obj).distinct().count()
|
||||
|
||||
def get_registration_forms(self, obj):
|
||||
return list(RegistrationForm.objects.filter(establishment=obj).distinct().values_list('student__last_name', 'student__first_name'))
|
||||
7
Back-End/Establishment/urls.py
Normal file
7
Back-End/Establishment/urls.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.urls import path, re_path
|
||||
from .views import EstablishmentListCreateView, EstablishmentDetailView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^establishments$', EstablishmentListCreateView.as_view(), name='establishment_list_create'),
|
||||
re_path(r'^establishments/(?P<id>[0-9]+)$', EstablishmentDetailView.as_view(), name="establishment_detail"),
|
||||
]
|
||||
132
Back-End/Establishment/views.py
Normal file
132
Back-End/Establishment/views.py
Normal file
@ -0,0 +1,132 @@
|
||||
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, 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, 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')
|
||||
class EstablishmentListCreateView(APIView):
|
||||
def get(self, request):
|
||||
establishments = getAllObjects(Establishment)
|
||||
establishments_serializer = EstablishmentSerializer(establishments, many=True)
|
||||
return JsonResponse(establishments_serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request):
|
||||
establishment_data = JSONParser().parse(request)
|
||||
try:
|
||||
establishment, data = create_establishment_with_directeur(establishment_data)
|
||||
# Création des EstablishmentCompetency pour chaque compétence existante
|
||||
competencies = Competency.objects.filter(
|
||||
Q(end_of_cycle=True) | ~Q(level=None)
|
||||
)
|
||||
for competency in competencies:
|
||||
EstablishmentCompetency.objects.get_or_create(
|
||||
establishment=establishment,
|
||||
competency=competency,
|
||||
defaults={'is_required': True}
|
||||
)
|
||||
return JsonResponse(data, safe=False, status=status.HTTP_201_CREATED)
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@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)
|
||||
establishment_serializer = EstablishmentSerializer(establishment)
|
||||
return JsonResponse(establishment_serializer.data, safe=False)
|
||||
except Establishment.DoesNotExist:
|
||||
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def put(self, request, id):
|
||||
"""
|
||||
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)
|
||||
|
||||
# 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, status=status.HTTP_200_OK)
|
||||
return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, id):
|
||||
return delete_object(Establishment, id)
|
||||
|
||||
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", {})
|
||||
|
||||
# Vérification de la présence du directeur
|
||||
if not directeur_data or not directeur_data.get("email"):
|
||||
raise ValueError("Le champ 'directeur.email' est obligatoire.")
|
||||
|
||||
directeur_email = directeur_data.get("email")
|
||||
last_name = directeur_data.get("last_name", "")
|
||||
first_name = directeur_data.get("first_name", "")
|
||||
password = directeur_data.get("password", "Provisoire01!")
|
||||
|
||||
# Création ou récupération du profil utilisateur
|
||||
profile, created = Profile.objects.get_or_create(
|
||||
email=directeur_email,
|
||||
defaults={"username": directeur_email}
|
||||
)
|
||||
if created or not profile.has_usable_password():
|
||||
profile.set_password(password)
|
||||
profile.save()
|
||||
|
||||
# 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
|
||||
profile_role, _ = ProfileRole.objects.get_or_create(
|
||||
profile=profile,
|
||||
establishment=establishment,
|
||||
role_type=ProfileRole.RoleType.PROFIL_ADMIN,
|
||||
defaults={"is_active": False}
|
||||
)
|
||||
|
||||
# Création ou mise à jour du Directeur lié à ce ProfileRole
|
||||
Directeur.objects.update_or_create(
|
||||
profile_role=profile_role,
|
||||
defaults={
|
||||
"last_name": last_name,
|
||||
"first_name": first_name
|
||||
}
|
||||
)
|
||||
|
||||
# Création du SMTPSettings rattaché à l'établissement si des données sont fournies
|
||||
if smtp_settings_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
|
||||
1
Back-End/GestionEmail/__init__.py
Normal file
1
Back-End/GestionEmail/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
default_app_config = 'GestionEmail.apps.GestionEmailConfig'
|
||||
5
Back-End/GestionEmail/apps.py
Normal file
5
Back-End/GestionEmail/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class GestionEmailConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'GestionEmail'
|
||||
9
Back-End/GestionEmail/urls.py
Normal file
9
Back-End/GestionEmail/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
SendEmailView, search_recipients
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('send-email/', SendEmailView.as_view(), name='send_email'),
|
||||
path('search-recipients/', search_recipients, name='search_recipients'),
|
||||
]
|
||||
119
Back-End/GestionEmail/views.py
Normal file
119
Back-End/GestionEmail/views.py
Normal file
@ -0,0 +1,119 @@
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.db.models import Q
|
||||
from Auth.models import Profile, ProfileRole
|
||||
|
||||
import N3wtSchool.mailManager as mailer
|
||||
from N3wtSchool import bdd
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from rest_framework.exceptions import NotFound
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
# Ajouter un logger pour debug
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SendEmailView(APIView):
|
||||
"""
|
||||
API pour envoyer des emails aux parents et professeurs.
|
||||
"""
|
||||
def post(self, request):
|
||||
# Ajouter du debug
|
||||
logger.info(f"Request data received: {request.data}")
|
||||
logger.info(f"Request content type: {request.content_type}")
|
||||
|
||||
data = request.data
|
||||
recipients = data.get('recipients', [])
|
||||
cc = data.get('cc', [])
|
||||
bcc = data.get('bcc', [])
|
||||
subject = data.get('subject', 'Notification')
|
||||
message = data.get('message', '')
|
||||
establishment_id = data.get('establishment_id', '')
|
||||
|
||||
# Debug des données reçues
|
||||
logger.info(f"Recipients: {recipients} (type: {type(recipients)})")
|
||||
logger.info(f"CC: {cc} (type: {type(cc)})")
|
||||
logger.info(f"BCC: {bcc} (type: {type(bcc)})")
|
||||
logger.info(f"Subject: {subject}")
|
||||
logger.info(f"Message length: {len(message) if message else 0}")
|
||||
logger.info(f"Establishment ID: {establishment_id}")
|
||||
|
||||
if not recipients or not message:
|
||||
logger.error("Recipients or message missing")
|
||||
logger.error(f"Recipients empty: {not recipients}, Message empty: {not message}")
|
||||
logger.error(f"Recipients value: '{recipients}', Message value: '{message}'")
|
||||
return Response({'error': 'Les destinataires et le message sont requis.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
# Récupérer la connexion SMTP
|
||||
logger.info("Tentative de récupération de la connexion SMTP...")
|
||||
connection = mailer.getConnection(establishment_id)
|
||||
logger.info(f"Connexion SMTP récupérée: {connection}")
|
||||
|
||||
# Envoyer l'email
|
||||
logger.info("Tentative d'envoi de l'email...")
|
||||
result = mailer.sendMail(
|
||||
subject=subject,
|
||||
message=message,
|
||||
recipients=recipients,
|
||||
cc=cc,
|
||||
bcc=bcc,
|
||||
attachments=[],
|
||||
connection=connection
|
||||
)
|
||||
logger.info(f"Email envoyé avec succès: {result}")
|
||||
return result
|
||||
except NotFound as e:
|
||||
logger.error(f"NotFound error: {str(e)}")
|
||||
return Response({'error': str(e)}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception during email sending: {str(e)}")
|
||||
logger.error(f"Exception type: {type(e)}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def search_recipients(request):
|
||||
"""
|
||||
API pour rechercher des destinataires en fonction d'un terme de recherche et d'un établissement.
|
||||
"""
|
||||
query = request.GET.get('q', '').strip() # Récupérer le terme de recherche depuis les paramètres GET
|
||||
establishment_id = request.GET.get('establishment_id', None) # Récupérer l'ID de l'établissement
|
||||
|
||||
if not query:
|
||||
return JsonResponse([], safe=False) # Retourner une liste vide si aucun terme n'est fourni
|
||||
|
||||
if not establishment_id:
|
||||
return JsonResponse({'error': 'establishment_id est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Rechercher dans les champs pertinents (nom, prénom, email) et filtrer par establishment_id
|
||||
profiles = Profile.objects.filter(
|
||||
Q(first_name__icontains=query) |
|
||||
Q(last_name__icontains=query) |
|
||||
Q(email__icontains=query),
|
||||
roles__establishment_id=establishment_id, # Utiliser 'roles' au lieu de 'profilerole'
|
||||
roles__is_active=True # Filtrer uniquement les ProfileRole actifs
|
||||
).distinct()
|
||||
|
||||
# Construire la réponse avec les rôles associés
|
||||
results = []
|
||||
for profile in profiles:
|
||||
profile_roles = ProfileRole.objects.filter(
|
||||
profile=profile,
|
||||
establishment_id=establishment_id,
|
||||
is_active=True # Inclure uniquement les ProfileRole actifs
|
||||
).values(
|
||||
'id', 'role_type', 'establishment__name', 'is_active'
|
||||
)
|
||||
results.append({
|
||||
'id': profile.id,
|
||||
'first_name': profile.first_name,
|
||||
'last_name': profile.last_name,
|
||||
'email': profile.email,
|
||||
'roles': list(profile_roles) # Inclure tous les rôles actifs associés pour cet établissement
|
||||
})
|
||||
|
||||
return JsonResponse(results, safe=False)
|
||||
627
Back-End/GestionMessagerie/consumers.py
Normal file
627
Back-End/GestionMessagerie/consumers.py
Normal file
@ -0,0 +1,627 @@
|
||||
import json
|
||||
import logging
|
||||
from uuid import UUID
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
from django.utils import timezone
|
||||
from .models import Conversation, ConversationParticipant, Message, UserPresence, MessageRead
|
||||
from .serializers import MessageSerializer, ConversationSerializer
|
||||
from Auth.models import Profile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def serialize_for_websocket(data):
|
||||
"""
|
||||
Convertit récursivement les objets non-sérialisables en JSON en types sérialisables
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return {key: serialize_for_websocket(value) for key, value in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [serialize_for_websocket(item) for item in data]
|
||||
elif isinstance(data, UUID):
|
||||
return str(data)
|
||||
elif isinstance(data, Decimal):
|
||||
return float(data)
|
||||
elif isinstance(data, datetime):
|
||||
return data.isoformat()
|
||||
else:
|
||||
return data
|
||||
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
"""Consumer WebSocket pour la messagerie instantanée"""
|
||||
|
||||
async def connect(self):
|
||||
self.user_id = self.scope['url_route']['kwargs']['user_id']
|
||||
self.user_group_name = f'user_{self.user_id}'
|
||||
|
||||
# Vérifier si l'utilisateur est authentifié
|
||||
user = self.scope.get('user')
|
||||
if not user or user.is_anonymous:
|
||||
logger.warning(f"Tentative de connexion WebSocket non authentifiée pour user_id: {self.user_id}")
|
||||
await self.close()
|
||||
return
|
||||
|
||||
# Vérifier que l'utilisateur connecté correspond à l'user_id de l'URL
|
||||
if str(user.id) != str(self.user_id):
|
||||
logger.warning(f"Tentative d'accès WebSocket avec user_id incorrect: {self.user_id} vs {user.id}")
|
||||
await self.close()
|
||||
return
|
||||
|
||||
self.user = user
|
||||
|
||||
# Rejoindre le groupe utilisateur
|
||||
await self.channel_layer.group_add(
|
||||
self.user_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Rejoindre les groupes des conversations de l'utilisateur
|
||||
conversations = await self.get_user_conversations(self.user_id)
|
||||
for conversation in conversations:
|
||||
await self.channel_layer.group_add(
|
||||
f'conversation_{conversation.id}',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Mettre à jour le statut de présence
|
||||
presence = await self.update_user_presence(self.user_id, 'online')
|
||||
|
||||
# Notifier les autres utilisateurs du changement de statut
|
||||
if presence:
|
||||
await self.broadcast_presence_update(self.user_id, 'online')
|
||||
|
||||
# Envoyer les statuts de présence existants des autres utilisateurs connectés
|
||||
await self.send_existing_user_presences()
|
||||
|
||||
await self.accept()
|
||||
|
||||
logger.info(f"User {self.user_id} connected to chat")
|
||||
|
||||
async def send_existing_user_presences(self):
|
||||
"""Envoyer les statuts de présence existants des autres utilisateurs connectés"""
|
||||
try:
|
||||
# Obtenir toutes les conversations de cet utilisateur
|
||||
conversations = await self.get_user_conversations(self.user_id)
|
||||
|
||||
# Créer un set pour éviter les doublons d'utilisateurs
|
||||
other_users = set()
|
||||
|
||||
# Pour chaque conversation, récupérer les participants
|
||||
for conversation in conversations:
|
||||
participants = await self.get_conversation_participants(conversation.id)
|
||||
for participant in participants:
|
||||
if participant.id != self.user_id:
|
||||
other_users.add(participant.id)
|
||||
|
||||
# Envoyer le statut de présence pour chaque utilisateur
|
||||
for user_id in other_users:
|
||||
presence = await self.get_user_presence(user_id)
|
||||
if presence:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'presence_update',
|
||||
'user_id': str(user_id),
|
||||
'status': presence.status
|
||||
}))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending existing user presences: {str(e)}")
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Quitter tous les groupes
|
||||
await self.channel_layer.group_discard(
|
||||
self.user_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
if hasattr(self, 'user'):
|
||||
conversations = await self.get_user_conversations(self.user_id)
|
||||
for conversation in conversations:
|
||||
await self.channel_layer.group_discard(
|
||||
f'conversation_{conversation.id}',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Mettre à jour le statut de présence
|
||||
presence = await self.update_user_presence(self.user_id, 'offline')
|
||||
|
||||
# Notifier les autres utilisateurs du changement de statut
|
||||
if presence:
|
||||
await self.broadcast_presence_update(self.user_id, 'offline')
|
||||
|
||||
logger.info(f"User {self.user_id} disconnected from chat")
|
||||
|
||||
async def receive(self, text_data):
|
||||
"""Recevoir et traiter les messages du client"""
|
||||
try:
|
||||
text_data_json = json.loads(text_data)
|
||||
message_type = text_data_json.get('type')
|
||||
|
||||
if message_type == 'send_message':
|
||||
await self.handle_send_message(text_data_json)
|
||||
elif message_type == 'typing_start':
|
||||
await self.handle_typing_start(text_data_json)
|
||||
elif message_type == 'typing_stop':
|
||||
await self.handle_typing_stop(text_data_json)
|
||||
elif message_type == 'mark_as_read':
|
||||
await self.handle_mark_as_read(text_data_json)
|
||||
elif message_type == 'join_conversation':
|
||||
await self.handle_join_conversation(text_data_json)
|
||||
elif message_type == 'leave_conversation':
|
||||
await self.handle_leave_conversation(text_data_json)
|
||||
elif message_type == 'presence_update':
|
||||
await self.handle_presence_update(text_data_json)
|
||||
else:
|
||||
logger.warning(f"Unknown message type: {message_type}")
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': f'Unknown message type: {message_type}'
|
||||
}))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Invalid JSON format'
|
||||
}))
|
||||
except Exception as e:
|
||||
logger.error(f"Error in receive: {str(e)}")
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Internal server error'
|
||||
}))
|
||||
|
||||
async def handle_send_message(self, data):
|
||||
"""Gérer l'envoi d'un nouveau message"""
|
||||
conversation_id = data.get('conversation_id')
|
||||
content = data.get('content', '').strip()
|
||||
message_type = data.get('message_type', 'text')
|
||||
attachment = data.get('attachment')
|
||||
|
||||
# Vérifier qu'on a soit du contenu, soit un fichier
|
||||
if not conversation_id or (not content and not attachment):
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Conversation ID and content or attachment are required'
|
||||
}))
|
||||
return
|
||||
|
||||
# Vérifier que l'utilisateur peut envoyer dans cette conversation
|
||||
can_send = await self.can_user_send_message(self.user_id, conversation_id)
|
||||
if not can_send:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'You cannot send messages to this conversation'
|
||||
}))
|
||||
return
|
||||
|
||||
# Créer le message avec ou sans fichier
|
||||
message = await self.create_message(conversation_id, self.user_id, content, message_type, attachment)
|
||||
if not message:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': 'Failed to create message'
|
||||
}))
|
||||
return
|
||||
|
||||
# Sérialiser le message
|
||||
message_data = await self.serialize_message(message)
|
||||
|
||||
# Auto-marquer comme lu pour les utilisateurs connectés (présents dans la conversation)
|
||||
await self.auto_mark_read_for_online_users(message, conversation_id)
|
||||
|
||||
# Envoyer le message à tous les participants de la conversation
|
||||
await self.channel_layer.group_send(
|
||||
f'conversation_{conversation_id}',
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'message': message_data
|
||||
}
|
||||
)
|
||||
|
||||
async def handle_typing_start(self, data):
|
||||
"""Gérer le début de frappe"""
|
||||
conversation_id = data.get('conversation_id')
|
||||
if conversation_id:
|
||||
await self.update_typing_status(self.user_id, conversation_id, True)
|
||||
|
||||
# Récupérer le nom de l'utilisateur
|
||||
user_name = await self.get_user_display_name(self.user_id)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
f'conversation_{conversation_id}',
|
||||
{
|
||||
'type': 'typing_status',
|
||||
'user_id': str(self.user_id),
|
||||
'user_name': user_name,
|
||||
'is_typing': True,
|
||||
'conversation_id': str(conversation_id)
|
||||
}
|
||||
)
|
||||
|
||||
async def handle_typing_stop(self, data):
|
||||
"""Gérer l'arrêt de frappe"""
|
||||
conversation_id = data.get('conversation_id')
|
||||
if conversation_id:
|
||||
await self.update_typing_status(self.user_id, conversation_id, False)
|
||||
|
||||
# Récupérer le nom de l'utilisateur
|
||||
user_name = await self.get_user_display_name(self.user_id)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
f'conversation_{conversation_id}',
|
||||
{
|
||||
'type': 'typing_status',
|
||||
'user_id': str(self.user_id),
|
||||
'user_name': user_name,
|
||||
'is_typing': False,
|
||||
'conversation_id': str(conversation_id)
|
||||
}
|
||||
)
|
||||
|
||||
async def handle_mark_as_read(self, data):
|
||||
"""Marquer les messages comme lus"""
|
||||
conversation_id = data.get('conversation_id')
|
||||
if conversation_id:
|
||||
await self.mark_conversation_as_read(self.user_id, conversation_id)
|
||||
await self.channel_layer.group_send(
|
||||
f'conversation_{conversation_id}',
|
||||
{
|
||||
'type': 'messages_read',
|
||||
'user_id': str(self.user_id),
|
||||
'conversation_id': str(conversation_id)
|
||||
}
|
||||
)
|
||||
|
||||
async def handle_join_conversation(self, data):
|
||||
"""Rejoindre une conversation"""
|
||||
conversation_id = data.get('conversation_id')
|
||||
if conversation_id:
|
||||
await self.channel_layer.group_add(
|
||||
f'conversation_{conversation_id}',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def handle_leave_conversation(self, data):
|
||||
"""Quitter une conversation"""
|
||||
conversation_id = data.get('conversation_id')
|
||||
if conversation_id:
|
||||
await self.channel_layer.group_discard(
|
||||
f'conversation_{conversation_id}',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def handle_presence_update(self, data):
|
||||
"""Gérer les mises à jour de présence"""
|
||||
status = data.get('status', 'online')
|
||||
if status in ['online', 'offline', 'away']:
|
||||
await self.update_user_presence(self.user_id, status)
|
||||
await self.broadcast_presence_update(self.user_id, status)
|
||||
|
||||
# Méthodes pour recevoir les messages des groupes
|
||||
async def chat_message(self, event):
|
||||
"""Envoyer un message de chat au WebSocket"""
|
||||
message_data = serialize_for_websocket(event['message'])
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'new_message',
|
||||
'message': message_data
|
||||
}))
|
||||
|
||||
async def typing_status(self, event):
|
||||
"""Envoyer le statut de frappe"""
|
||||
# Ne pas envoyer à l'expéditeur
|
||||
if str(event['user_id']) != str(self.user_id):
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'typing_status',
|
||||
'user_id': str(event['user_id']),
|
||||
'user_name': event.get('user_name', ''),
|
||||
'is_typing': event['is_typing'],
|
||||
'conversation_id': str(event['conversation_id'])
|
||||
}))
|
||||
|
||||
async def messages_read(self, event):
|
||||
"""Notifier que des messages ont été lus"""
|
||||
if str(event['user_id']) != str(self.user_id):
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'messages_read',
|
||||
'user_id': str(event['user_id']),
|
||||
'conversation_id': str(event['conversation_id'])
|
||||
}))
|
||||
|
||||
async def user_presence_update(self, event):
|
||||
"""Notifier d'un changement de présence"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'presence_update',
|
||||
'user_id': str(event['user_id']),
|
||||
'status': event['status']
|
||||
}))
|
||||
|
||||
async def new_conversation_notification(self, event):
|
||||
"""Notifier d'une nouvelle conversation"""
|
||||
conversation = serialize_for_websocket(event['conversation'])
|
||||
conversation_id = conversation['id']
|
||||
|
||||
# Rejoindre automatiquement le groupe de la nouvelle conversation
|
||||
await self.channel_layer.group_add(
|
||||
f'conversation_{conversation_id}',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Envoyer la notification au client
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'new_conversation',
|
||||
'conversation': conversation
|
||||
}))
|
||||
|
||||
# Diffuser les présences des participants de cette nouvelle conversation
|
||||
try:
|
||||
participants = await self.get_conversation_participants(conversation_id)
|
||||
for participant in participants:
|
||||
# Ne pas diffuser sa propre présence à soi-même
|
||||
if participant.id != self.user_id:
|
||||
presence = await self.get_user_presence(participant.id)
|
||||
if presence:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'presence_update',
|
||||
'user_id': str(participant.id),
|
||||
'status': presence.status
|
||||
}))
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending presence updates for new conversation: {str(e)}")
|
||||
|
||||
async def broadcast_presence_update(self, user_id, status):
|
||||
"""Diffuser un changement de statut de présence à tous les utilisateurs connectés"""
|
||||
try:
|
||||
# Obtenir tous les utilisateurs qui ont des conversations avec cet utilisateur
|
||||
user_conversations = await self.get_user_conversations(user_id)
|
||||
|
||||
# Créer un set pour éviter les doublons d'utilisateurs
|
||||
notified_users = set()
|
||||
|
||||
# Pour chaque conversation, notifier tous les participants
|
||||
for conversation in user_conversations:
|
||||
participants = await self.get_conversation_participants(conversation.id)
|
||||
for participant in participants:
|
||||
if participant.id != user_id and participant.id not in notified_users:
|
||||
notified_users.add(participant.id)
|
||||
# Envoyer la notification au groupe utilisateur
|
||||
await self.channel_layer.group_send(
|
||||
f'user_{participant.id}',
|
||||
{
|
||||
'type': 'user_presence_update',
|
||||
'user_id': user_id,
|
||||
'status': status
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Broadcasted presence update for user {user_id} ({status}) to {len(notified_users)} users")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error broadcasting presence update: {str(e)}")
|
||||
|
||||
# Méthodes d'accès aux données (database_sync_to_async)
|
||||
@database_sync_to_async
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return Profile.objects.get(id=user_id)
|
||||
except Profile.DoesNotExist:
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_display_name(self, user_id):
|
||||
"""Obtenir le nom d'affichage d'un utilisateur"""
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
if user.first_name and user.last_name:
|
||||
return f"{user.first_name} {user.last_name}"
|
||||
elif user.first_name:
|
||||
return user.first_name
|
||||
elif user.last_name:
|
||||
return user.last_name
|
||||
else:
|
||||
return user.email or f"Utilisateur {user_id}"
|
||||
except Profile.DoesNotExist:
|
||||
return f"Utilisateur {user_id}"
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_conversations(self, user_id):
|
||||
return list(Conversation.objects.filter(
|
||||
participants__participant_id=user_id,
|
||||
participants__is_active=True,
|
||||
is_active=True
|
||||
).distinct())
|
||||
|
||||
@database_sync_to_async
|
||||
def get_conversation_participants(self, conversation_id):
|
||||
"""Obtenir tous les participants d'une conversation"""
|
||||
return list(Profile.objects.filter(
|
||||
conversation_participants__conversation_id=conversation_id,
|
||||
conversation_participants__is_active=True
|
||||
))
|
||||
|
||||
@database_sync_to_async
|
||||
def get_conversations_data(self, user_id):
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
conversations = Conversation.objects.filter(
|
||||
participants__participant=user,
|
||||
participants__is_active=True,
|
||||
is_active=True
|
||||
).distinct()
|
||||
|
||||
serializer = ConversationSerializer(conversations, many=True, context={'user': user})
|
||||
return serializer.data
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting conversations data: {str(e)}")
|
||||
return []
|
||||
|
||||
@database_sync_to_async
|
||||
def can_user_send_message(self, user_id, conversation_id):
|
||||
return ConversationParticipant.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
participant_id=user_id,
|
||||
is_active=True
|
||||
).exists()
|
||||
|
||||
@database_sync_to_async
|
||||
def create_message(self, conversation_id, sender_id, content, message_type, attachment=None):
|
||||
try:
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
sender = Profile.objects.get(id=sender_id)
|
||||
|
||||
message_data = {
|
||||
'conversation': conversation,
|
||||
'sender': sender,
|
||||
'content': content,
|
||||
'message_type': message_type
|
||||
}
|
||||
|
||||
# Ajouter les informations du fichier si présent
|
||||
if attachment:
|
||||
message_data.update({
|
||||
'file_url': attachment.get('fileUrl'),
|
||||
'file_name': attachment.get('fileName'),
|
||||
'file_size': attachment.get('fileSize'),
|
||||
'file_type': attachment.get('fileType'),
|
||||
})
|
||||
# Si c'est un fichier, s'assurer que le type de message est correct
|
||||
if attachment.get('fileType', '').startswith('image/'):
|
||||
message_data['message_type'] = 'image'
|
||||
else:
|
||||
message_data['message_type'] = 'file'
|
||||
|
||||
message = Message.objects.create(**message_data)
|
||||
|
||||
# Mettre à jour l'activité de la conversation
|
||||
conversation.last_activity = message.created_at
|
||||
conversation.save(update_fields=['last_activity'])
|
||||
|
||||
return message
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating message: {str(e)}")
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def serialize_message(self, message):
|
||||
serializer = MessageSerializer(message)
|
||||
return serialize_for_websocket(serializer.data)
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user_presence(self, user_id):
|
||||
"""Récupérer la présence d'un utilisateur"""
|
||||
try:
|
||||
return UserPresence.objects.get(user_id=user_id)
|
||||
except UserPresence.DoesNotExist:
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def update_user_presence(self, user_id, status):
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
presence, created = UserPresence.objects.get_or_create(user=user)
|
||||
old_status = presence.status
|
||||
presence.status = status
|
||||
presence.save()
|
||||
|
||||
# Si le statut a changé, notifier les autres utilisateurs
|
||||
if old_status != status or created:
|
||||
logger.info(f"User {user_id} presence changed from {old_status} to {status}")
|
||||
|
||||
return presence
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user presence: {str(e)}")
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def update_typing_status(self, user_id, conversation_id, is_typing):
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
presence, created = UserPresence.objects.get_or_create(user=user)
|
||||
if is_typing:
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
presence.is_typing_in = conversation
|
||||
else:
|
||||
presence.is_typing_in = None
|
||||
presence.save()
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating typing status: {str(e)}")
|
||||
|
||||
@database_sync_to_async
|
||||
def mark_conversation_as_read(self, user_id, conversation_id):
|
||||
"""Marquer tous les messages non lus d'une conversation comme lus"""
|
||||
try:
|
||||
# Mettre à jour le last_read_at du participant
|
||||
participant = ConversationParticipant.objects.get(
|
||||
conversation_id=conversation_id,
|
||||
participant_id=user_id
|
||||
)
|
||||
current_time = timezone.now()
|
||||
participant.last_read_at = current_time
|
||||
participant.save(update_fields=['last_read_at'])
|
||||
|
||||
# Créer des enregistrements MessageRead pour tous les messages non lus
|
||||
# que l'utilisateur n'a pas encore explicitement lus
|
||||
unread_messages = Message.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
created_at__lte=current_time,
|
||||
is_deleted=False
|
||||
).exclude(
|
||||
sender_id=user_id # Exclure ses propres messages
|
||||
).exclude(
|
||||
read_by__participant_id=user_id # Exclure les messages déjà marqués comme lus
|
||||
)
|
||||
|
||||
# Créer les enregistrements MessageRead en batch
|
||||
message_reads = [
|
||||
MessageRead(message=message, participant_id=user_id, read_at=current_time)
|
||||
for message in unread_messages
|
||||
]
|
||||
|
||||
if message_reads:
|
||||
MessageRead.objects.bulk_create(message_reads, ignore_conflicts=True)
|
||||
logger.info(f"Marked {len(message_reads)} messages as read for user {user_id} in conversation {conversation_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking conversation as read: {str(e)}")
|
||||
|
||||
@database_sync_to_async
|
||||
def auto_mark_read_for_online_users(self, message, conversation_id):
|
||||
"""Auto-marquer comme lu pour les utilisateurs en ligne dans la conversation"""
|
||||
try:
|
||||
# Obtenir tous les participants de la conversation (synchrone)
|
||||
participants = ConversationParticipant.objects.filter(
|
||||
conversation_id=conversation_id,
|
||||
is_active=True
|
||||
).exclude(participant_id=message.sender.id)
|
||||
|
||||
# Obtenir l'heure de création du message
|
||||
message_time = message.created_at
|
||||
|
||||
# Préparer les enregistrements MessageRead à créer
|
||||
message_reads = []
|
||||
|
||||
for participant_obj in participants:
|
||||
participant = participant_obj.participant
|
||||
|
||||
# Vérifier si l'utilisateur est en ligne (synchrone)
|
||||
try:
|
||||
presence = UserPresence.objects.filter(user=participant).first()
|
||||
if presence and presence.status == 'online':
|
||||
# Vérifier qu'il n'existe pas déjà un enregistrement MessageRead
|
||||
if not MessageRead.objects.filter(message=message, participant=participant).exists():
|
||||
message_reads.append(MessageRead(
|
||||
message=message,
|
||||
participant=participant,
|
||||
read_at=message_time
|
||||
))
|
||||
except:
|
||||
# En cas d'erreur de présence, ne pas marquer comme lu
|
||||
continue
|
||||
|
||||
# Créer les enregistrements MessageRead en batch
|
||||
if message_reads:
|
||||
MessageRead.objects.bulk_create(message_reads, ignore_conflicts=True)
|
||||
logger.info(f"Auto-marked {len(message_reads)} messages as read for online users in conversation {conversation_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto_mark_read_for_online_users: {str(e)}")
|
||||
108
Back-End/GestionMessagerie/middleware.py
Normal file
108
Back-End/GestionMessagerie/middleware.py
Normal file
@ -0,0 +1,108 @@
|
||||
import jwt
|
||||
import logging
|
||||
from urllib.parse import parse_qs
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from channels.middleware import BaseMiddleware
|
||||
from channels.db import database_sync_to_async
|
||||
from Auth.models import Profile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user(user_id):
|
||||
"""Récupérer l'utilisateur de manière asynchrone"""
|
||||
try:
|
||||
return Profile.objects.get(id=user_id)
|
||||
except Profile.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
class JWTAuthMiddleware(BaseMiddleware):
|
||||
"""Middleware pour l'authentification JWT dans les WebSockets"""
|
||||
|
||||
def __init__(self, inner):
|
||||
super().__init__(inner)
|
||||
|
||||
def _check_cors_origin(self, scope):
|
||||
"""Vérifier si l'origine est autorisée pour les WebSockets"""
|
||||
origin = None
|
||||
|
||||
# Récupérer l'origine depuis les headers
|
||||
for name, value in scope.get('headers', []):
|
||||
if name == b'origin':
|
||||
origin = value.decode('latin1')
|
||||
break
|
||||
|
||||
if not origin:
|
||||
logger.warning("Aucune origine trouvée dans les headers WebSocket")
|
||||
return False
|
||||
|
||||
# Récupérer les origines autorisées depuis la configuration CORS
|
||||
allowed_origins = getattr(settings, 'CORS_ALLOWED_ORIGINS', [])
|
||||
|
||||
# Si CORS_ORIGIN_ALLOW_ALL est True, autoriser toutes les origines
|
||||
if getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False):
|
||||
logger.info(f"Origine WebSocket autorisée (CORS_ORIGIN_ALLOW_ALL): {origin}")
|
||||
return True
|
||||
|
||||
# Vérifier si l'origine est dans la liste des origines autorisées
|
||||
if origin in allowed_origins:
|
||||
logger.info(f"Origine WebSocket autorisée: {origin}")
|
||||
return True
|
||||
|
||||
logger.warning(f"Origine WebSocket non autorisée: {origin}. Origines autorisées: {allowed_origins}")
|
||||
return False
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
# Vérifier les CORS pour les WebSockets
|
||||
if not self._check_cors_origin(scope):
|
||||
logger.error("Connexion WebSocket refusée: origine non autorisée")
|
||||
# Fermer la connexion WebSocket avec un code d'erreur
|
||||
await send({
|
||||
'type': 'websocket.close',
|
||||
'code': 1008 # Policy Violation
|
||||
})
|
||||
return
|
||||
|
||||
# Extraire le token de l'URL
|
||||
query_string = parse_qs(scope['query_string'].decode())
|
||||
token = query_string.get('token')
|
||||
|
||||
if token:
|
||||
token = token[0]
|
||||
try:
|
||||
# Décoder le token JWT
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.SIMPLE_JWT['SIGNING_KEY'],
|
||||
algorithms=[settings.SIMPLE_JWT['ALGORITHM']]
|
||||
)
|
||||
|
||||
# Vérifier que c'est un token d'accès
|
||||
if payload.get('type') != 'access':
|
||||
logger.warning(f"Token type invalide: {payload.get('type')}")
|
||||
scope['user'] = AnonymousUser()
|
||||
else:
|
||||
# Récupérer l'utilisateur
|
||||
user_id = payload.get('user_id')
|
||||
user = await get_user(user_id)
|
||||
scope['user'] = user
|
||||
logger.info(f"Utilisateur authentifié via JWT: {user.email if hasattr(user, 'email') else 'Unknown'}")
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("Token JWT expiré")
|
||||
scope['user'] = AnonymousUser()
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning(f"Token JWT invalide: {str(e)}")
|
||||
scope['user'] = AnonymousUser()
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'authentification JWT: {str(e)}")
|
||||
scope['user'] = AnonymousUser()
|
||||
else:
|
||||
scope['user'] = AnonymousUser()
|
||||
|
||||
return await super().__call__(scope, receive, send)
|
||||
|
||||
def JWTAuthMiddlewareStack(inner):
|
||||
"""Stack middleware pour l'authentification JWT"""
|
||||
return JWTAuthMiddleware(inner)
|
||||
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
@ -1,15 +1,113 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from Auth.models import Profile
|
||||
|
||||
from django.utils import timezone
|
||||
import uuid
|
||||
|
||||
class Conversation(models.Model):
|
||||
"""Modèle pour gérer les conversations entre utilisateurs"""
|
||||
CONVERSATION_TYPES = [
|
||||
('private', 'Privée'),
|
||||
('group', 'Groupe'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=255, blank=True, null=True) # Nom pour les groupes
|
||||
conversation_type = models.CharField(max_length=10, choices=CONVERSATION_TYPES, default='private')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_activity = models.DateTimeField(default=timezone.now)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return f'Conversation: {self.name}'
|
||||
return f'Conversation {self.id}'
|
||||
|
||||
def get_participants(self):
|
||||
return Profile.objects.filter(conversation_participants__conversation=self)
|
||||
|
||||
class ConversationParticipant(models.Model):
|
||||
"""Modèle pour gérer les participants d'une conversation"""
|
||||
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='participants')
|
||||
participant = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='conversation_participants')
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
last_read_at = models.DateTimeField(default=timezone.now)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('conversation', 'participant')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.participant.email} in {self.conversation.id}'
|
||||
|
||||
class Message(models.Model):
|
||||
"""Modèle pour les messages instantanés"""
|
||||
MESSAGE_TYPES = [
|
||||
('text', 'Texte'),
|
||||
('file', 'Fichier'),
|
||||
('image', 'Image'),
|
||||
('system', 'Système'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages')
|
||||
sender = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='sent_messages')
|
||||
content = models.TextField()
|
||||
message_type = models.CharField(max_length=10, choices=MESSAGE_TYPES, default='text')
|
||||
file_url = models.URLField(blank=True, null=True) # Pour les fichiers/images
|
||||
file_name = models.CharField(max_length=255, blank=True, null=True) # Nom original du fichier
|
||||
file_size = models.BigIntegerField(blank=True, null=True) # Taille en bytes
|
||||
file_type = models.CharField(max_length=100, blank=True, null=True) # MIME type
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
ordering = ['created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'Message from {self.sender.email} at {self.created_at}'
|
||||
|
||||
class MessageRead(models.Model):
|
||||
"""Modèle pour tracker les messages lus par chaque participant"""
|
||||
message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='read_by')
|
||||
participant = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name='read_messages')
|
||||
read_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('message', 'participant')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.participant.email} read {self.message.id}'
|
||||
|
||||
class UserPresence(models.Model):
|
||||
"""Modèle pour gérer la présence des utilisateurs"""
|
||||
PRESENCE_STATUS = [
|
||||
('online', 'En ligne'),
|
||||
('away', 'Absent'),
|
||||
('busy', 'Occupé'),
|
||||
('offline', 'Hors ligne'),
|
||||
]
|
||||
|
||||
user = models.OneToOneField(Profile, on_delete=models.CASCADE, related_name='presence')
|
||||
status = models.CharField(max_length=10, choices=PRESENCE_STATUS, default='offline')
|
||||
last_seen = models.DateTimeField(default=timezone.now)
|
||||
is_typing_in = models.ForeignKey(Conversation, on_delete=models.SET_NULL, null=True, blank=True, related_name='typing_users')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user.email} - {self.status}'
|
||||
|
||||
# Ancien modèle conservé pour compatibilité
|
||||
class Messagerie(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
objet = models.CharField(max_length=200, default="", blank=True)
|
||||
emetteur = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_envoyes')
|
||||
destinataire = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_recus')
|
||||
corpus = models.CharField(max_length=200, default="", blank=True)
|
||||
date_envoi = models.DateTimeField(auto_now_add=True) # Date d'envoi du message
|
||||
is_read = models.BooleanField(default=False) # Statut lu/non lu
|
||||
conversation_id = models.CharField(max_length=100, blank=True, default="") # Pour regrouper les messages par conversation
|
||||
|
||||
def __str__(self):
|
||||
return 'Messagerie_'+self.id
|
||||
return f'Messagerie_{self.id}'
|
||||
7
Back-End/GestionMessagerie/routing.py
Normal file
7
Back-End/GestionMessagerie/routing.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<user_id>\w+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
re_path(r'ws/chat/conversation/(?P<conversation_id>[\w-]+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
]
|
||||
@ -1,14 +1,266 @@
|
||||
from rest_framework import serializers
|
||||
from Auth.models import Profile
|
||||
from GestionMessagerie.models import Messagerie
|
||||
from GestionMessagerie.models import Messagerie, Conversation, ConversationParticipant, Message, MessageRead, UserPresence
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
class ProfileSimpleSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur simple pour les profils utilisateur"""
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['id', 'first_name', 'last_name', 'email']
|
||||
|
||||
class UserPresenceSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur pour la présence utilisateur"""
|
||||
user = ProfileSimpleSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = UserPresence
|
||||
fields = ['user', 'status', 'last_seen', 'is_typing_in']
|
||||
|
||||
class MessageReadSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur pour les messages lus"""
|
||||
participant = ProfileSimpleSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MessageRead
|
||||
fields = ['participant', 'read_at']
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur pour les messages instantanés"""
|
||||
sender = ProfileSimpleSerializer(read_only=True)
|
||||
read_by = MessageReadSerializer(many=True, read_only=True)
|
||||
attachment = serializers.SerializerMethodField()
|
||||
is_read = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ['id', 'conversation', 'sender', 'content', 'message_type', 'file_url',
|
||||
'file_name', 'file_size', 'file_type', 'attachment',
|
||||
'created_at', 'updated_at', 'is_edited', 'is_deleted', 'read_by', 'is_read']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
def get_attachment(self, obj):
|
||||
"""Retourne les informations du fichier attaché sous forme d'objet"""
|
||||
if obj.file_url:
|
||||
return {
|
||||
'fileName': obj.file_name,
|
||||
'fileSize': obj.file_size,
|
||||
'fileType': obj.file_type,
|
||||
'fileUrl': obj.file_url,
|
||||
}
|
||||
return None
|
||||
|
||||
def get_is_read(self, obj):
|
||||
"""Détermine si le message est lu par l'utilisateur actuel"""
|
||||
user = self.context.get('user')
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Si c'est le message de l'utilisateur lui-même, vérifier si quelqu'un d'autre l'a lu
|
||||
if obj.sender == user:
|
||||
# Pour les messages envoyés par l'utilisateur, vérifier si au moins un autre participant l'a explicitement lu
|
||||
# Utiliser le modèle MessageRead pour une vérification précise
|
||||
from .models import MessageRead
|
||||
other_participants = obj.conversation.participants.exclude(participant=user).filter(is_active=True)
|
||||
|
||||
for participant in other_participants:
|
||||
# Vérifier si ce participant a explicitement lu ce message
|
||||
if MessageRead.objects.filter(message=obj, participant=participant.participant).exists():
|
||||
return True
|
||||
|
||||
# Fallback: vérifier last_read_at seulement si l'utilisateur était en ligne récemment
|
||||
# ou si last_read_at est postérieur à created_at (lecture explicite après réception)
|
||||
if (participant.last_read_at and
|
||||
participant.last_read_at > obj.created_at):
|
||||
|
||||
# Vérifier la présence de l'utilisateur pour s'assurer qu'il était en ligne
|
||||
try:
|
||||
from .models import UserPresence
|
||||
user_presence = UserPresence.objects.filter(user=participant.participant).first()
|
||||
|
||||
# Si l'utilisateur était en ligne récemment (dans les 5 minutes suivant le message)
|
||||
# ou si last_read_at est bien après created_at (lecture délibérée)
|
||||
time_diff = participant.last_read_at - obj.created_at
|
||||
if (user_presence and user_presence.last_seen and
|
||||
user_presence.last_seen >= obj.created_at) or time_diff.total_seconds() > 10:
|
||||
return True
|
||||
except:
|
||||
# En cas d'erreur, continuer avec la logique conservative
|
||||
pass
|
||||
|
||||
return False
|
||||
else:
|
||||
# Pour les messages reçus, vérifier si l'utilisateur actuel l'a lu
|
||||
# D'abord vérifier dans MessageRead pour une lecture explicite
|
||||
from .models import MessageRead
|
||||
if MessageRead.objects.filter(message=obj, participant=user).exists():
|
||||
return True
|
||||
|
||||
# Fallback: vérifier last_read_at du participant
|
||||
participant = obj.conversation.participants.filter(
|
||||
participant=user,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if participant and participant.last_read_at:
|
||||
# Seulement considérer comme lu si last_read_at est postérieur à created_at
|
||||
return participant.last_read_at > obj.created_at
|
||||
|
||||
return False
|
||||
|
||||
class ConversationParticipantSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur pour les participants d'une conversation"""
|
||||
participant = ProfileSimpleSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ConversationParticipant
|
||||
fields = ['participant', 'joined_at', 'last_read_at', 'is_active']
|
||||
|
||||
class ConversationSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur pour les conversations"""
|
||||
participants = ConversationParticipantSerializer(many=True, read_only=True)
|
||||
last_message = serializers.SerializerMethodField()
|
||||
unread_count = serializers.SerializerMethodField()
|
||||
interlocuteur = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = ['id', 'name', 'conversation_type', 'created_at', 'updated_at',
|
||||
'last_activity', 'is_active', 'participants', 'last_message', 'unread_count', 'interlocuteur']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
def get_last_message(self, obj):
|
||||
last_message = obj.messages.filter(is_deleted=False).last()
|
||||
if last_message:
|
||||
return MessageSerializer(last_message).data
|
||||
return None
|
||||
|
||||
def get_unread_count(self, obj):
|
||||
user = self.context.get('user')
|
||||
if not user or not user.is_authenticated:
|
||||
return 0
|
||||
|
||||
participant = obj.participants.filter(participant=user).first()
|
||||
if not participant:
|
||||
return 0
|
||||
|
||||
# Nouvelle logique : compter les messages qui ne sont PAS dans MessageRead
|
||||
# et qui ont été créés après last_read_at (ou tous si last_read_at est None)
|
||||
|
||||
# Base query : messages de la conversation, excluant les propres messages et les supprimés
|
||||
# ET ne comptant que les messages textuels
|
||||
base_query = obj.messages.filter(
|
||||
is_deleted=False,
|
||||
message_type='text' # Ne compter que les messages textuels
|
||||
).exclude(sender=user)
|
||||
|
||||
# Si l'utilisateur n'a pas de last_read_at, tous les messages sont non lus
|
||||
if not participant.last_read_at:
|
||||
unread_from_timestamp = base_query
|
||||
else:
|
||||
# Messages créés après le dernier moment de lecture
|
||||
unread_from_timestamp = base_query.filter(
|
||||
created_at__gt=participant.last_read_at
|
||||
)
|
||||
|
||||
# Soustraire les messages explicitement marqués comme lus dans MessageRead
|
||||
from .models import MessageRead
|
||||
read_message_ids = MessageRead.objects.filter(
|
||||
participant=user,
|
||||
message__conversation=obj
|
||||
).values_list('message_id', flat=True)
|
||||
|
||||
# Compter les messages non lus = messages après last_read_at MOINS ceux explicitement lus
|
||||
unread_count = unread_from_timestamp.exclude(
|
||||
id__in=read_message_ids
|
||||
).count()
|
||||
|
||||
return unread_count
|
||||
|
||||
def get_interlocuteur(self, obj):
|
||||
"""Pour les conversations privées, retourne l'autre participant"""
|
||||
user = self.context.get('user')
|
||||
if not user or not user.is_authenticated or obj.conversation_type != 'private':
|
||||
return None
|
||||
|
||||
# Trouver l'autre participant (pas l'utilisateur actuel)
|
||||
other_participant = obj.participants.filter(is_active=True).exclude(participant=user).first()
|
||||
if other_participant:
|
||||
return ProfileSimpleSerializer(other_participant.participant).data
|
||||
return None
|
||||
|
||||
class ConversationCreateSerializer(serializers.ModelSerializer):
|
||||
"""Sérialiseur pour créer une conversation"""
|
||||
participant_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
write_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = ['name', 'conversation_type', 'participant_ids']
|
||||
|
||||
def create(self, validated_data):
|
||||
participant_ids = validated_data.pop('participant_ids')
|
||||
conversation_type = validated_data.get('conversation_type', 'private')
|
||||
|
||||
# Pour les conversations privées, ne pas utiliser de nom spécifique
|
||||
# Le nom sera géré côté frontend en affichant le nom de l'interlocuteur
|
||||
if conversation_type == 'private':
|
||||
validated_data['name'] = None
|
||||
|
||||
conversation = super().create(validated_data)
|
||||
|
||||
# Ajouter les participants
|
||||
participants = []
|
||||
for participant_id in participant_ids:
|
||||
try:
|
||||
participant = Profile.objects.get(id=participant_id)
|
||||
ConversationParticipant.objects.create(
|
||||
conversation=conversation,
|
||||
participant=participant
|
||||
)
|
||||
participants.append(participant)
|
||||
except Profile.DoesNotExist:
|
||||
continue
|
||||
|
||||
# Notifier les participants via WebSocket de la nouvelle conversation
|
||||
try:
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
if channel_layer:
|
||||
# Envoyer à chaque participant avec le bon contexte
|
||||
for participant in participants:
|
||||
# Sérialiser la conversation avec le contexte de ce participant
|
||||
conversation_data = ConversationSerializer(conversation, context={'user': participant}).data
|
||||
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f'user_{participant.id}',
|
||||
{
|
||||
'type': 'new_conversation_notification',
|
||||
'conversation': conversation_data
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
# Log l'erreur mais ne pas interrompre la création de la conversation
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Erreur lors de la notification WebSocket de nouvelle conversation: {str(e)}")
|
||||
|
||||
return conversation
|
||||
|
||||
# Ancien sérialiseur conservé pour compatibilité
|
||||
class MessageLegacySerializer(serializers.ModelSerializer):
|
||||
destinataire_profil = serializers.SerializerMethodField()
|
||||
emetteur_profil = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = Messagerie
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = ['date_envoi']
|
||||
|
||||
def get_destinataire_profil(self, obj):
|
||||
return obj.destinataire.email
|
||||
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from GestionMessagerie.views import MessagerieView, MessageView
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
InstantConversationListView, InstantConversationCreateView, InstantConversationDeleteView,
|
||||
InstantMessageListView, InstantMessageCreateView,
|
||||
InstantMarkAsReadView, FileUploadView,
|
||||
InstantRecipientSearchView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^messagerie/([0-9]+)$', MessagerieView.as_view(), name="messagerie"),
|
||||
re_path(r'^message$', MessageView.as_view(), name="message"),
|
||||
re_path(r'^message/([0-9]+)$', MessageView.as_view(), name="message"),
|
||||
# URLs pour messagerie instantanée
|
||||
path('conversations/', InstantConversationListView.as_view(), name='conversations'),
|
||||
path('create-conversation/', InstantConversationCreateView.as_view(), name='create_conversation'),
|
||||
path('send-message/', InstantMessageCreateView.as_view(), name='send_message'),
|
||||
path('conversations/mark-as-read/', InstantMarkAsReadView.as_view(), name='mark_as_read'),
|
||||
path('search-recipients/', InstantRecipientSearchView.as_view(), name='search_recipients'),
|
||||
path('upload-file/', FileUploadView.as_view(), name='upload_file'),
|
||||
|
||||
# URLs avec paramètres - doivent être après les URLs statiques
|
||||
path('conversations/user/<int:user_id>/', InstantConversationListView.as_view(), name='conversations_by_user'),
|
||||
path('conversations/<uuid:conversation_id>/', InstantConversationDeleteView.as_view(), name='delete_conversation'),
|
||||
path('conversations/<uuid:conversation_id>/messages/', InstantMessageListView.as_view(), name='conversation_messages'),
|
||||
]
|
||||
@ -1,32 +1,455 @@
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from django.db import models
|
||||
from .models import Conversation, ConversationParticipant, Message, UserPresence
|
||||
from Auth.models import Profile, ProfileRole
|
||||
from GestionMessagerie.serializers import (
|
||||
ConversationSerializer, MessageSerializer,
|
||||
ConversationCreateSerializer, UserPresenceSerializer,
|
||||
ProfileSimpleSerializer
|
||||
)
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from django.utils import timezone
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import *
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from GestionMessagerie.serializers import MessageSerializer
|
||||
# ====================== MESSAGERIE INSTANTANÉE ======================
|
||||
|
||||
from N3wtSchool import bdd
|
||||
class InstantConversationListView(APIView):
|
||||
"""
|
||||
API pour lister les conversations instantanées d'un utilisateur
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Liste les conversations instantanées d'un utilisateur",
|
||||
responses={200: ConversationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, user_id=None):
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
|
||||
class MessagerieView(APIView):
|
||||
def get(self, request, _idProfile):
|
||||
messagesList = bdd.getObjects(_objectName=Messagerie, _columnName='destinataire__id', _value=_idProfile)
|
||||
messages_serializer = MessageSerializer(messagesList, many=True)
|
||||
return JsonResponse(messages_serializer.data, safe=False)
|
||||
conversations = Conversation.objects.filter(
|
||||
participants__participant=user,
|
||||
participants__is_active=True,
|
||||
is_active=True
|
||||
).distinct().order_by('-last_activity')
|
||||
|
||||
class MessageView(APIView):
|
||||
def get(self, request, _id):
|
||||
message=bdd.getObject(Messagerie, "id", _id)
|
||||
message_serializer=MessageSerializer(message)
|
||||
return JsonResponse(message_serializer.data, safe=False)
|
||||
|
||||
serializer = ConversationSerializer(conversations, many=True, context={'user': user})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantConversationCreateView(APIView):
|
||||
"""
|
||||
API pour créer une nouvelle conversation instantanée
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Crée une nouvelle conversation instantanée",
|
||||
request_body=ConversationCreateSerializer,
|
||||
responses={201: ConversationSerializer}
|
||||
)
|
||||
def post(self, request):
|
||||
message_data=JSONParser().parse(request)
|
||||
message_serializer = MessageSerializer(data=message_data)
|
||||
serializer = ConversationCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
conversation = serializer.save()
|
||||
response_serializer = ConversationSerializer(conversation, context={'user': request.user})
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if message_serializer.is_valid():
|
||||
message_serializer.save()
|
||||
|
||||
return JsonResponse('Nouveau Message ajouté', safe=False)
|
||||
class InstantMessageListView(APIView):
|
||||
"""
|
||||
API pour lister les messages d'une conversation
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Liste les messages d'une conversation",
|
||||
responses={200: MessageSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, conversation_id):
|
||||
try:
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
messages = conversation.messages.filter(is_deleted=False).order_by('created_at')
|
||||
|
||||
# Récupérer l'utilisateur actuel depuis les paramètres de requête
|
||||
user_id = request.GET.get('user_id')
|
||||
user = None
|
||||
if user_id:
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
except Profile.DoesNotExist:
|
||||
pass
|
||||
|
||||
serializer = MessageSerializer(messages, many=True, context={'user': user})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantMessageCreateView(APIView):
|
||||
"""
|
||||
API pour envoyer un nouveau message instantané
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Envoie un nouveau message instantané",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'conversation_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la conversation'),
|
||||
'sender_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID de l\'expéditeur'),
|
||||
'content': openapi.Schema(type=openapi.TYPE_STRING, description='Contenu du message'),
|
||||
'message_type': openapi.Schema(type=openapi.TYPE_STRING, description='Type de message', default='text')
|
||||
},
|
||||
required=['conversation_id', 'sender_id', 'content']
|
||||
),
|
||||
responses={201: MessageSerializer}
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
conversation_id = request.data.get('conversation_id')
|
||||
sender_id = request.data.get('sender_id')
|
||||
content = request.data.get('content', '').strip()
|
||||
message_type = request.data.get('message_type', 'text')
|
||||
|
||||
if not all([conversation_id, sender_id, content]):
|
||||
return Response(
|
||||
{'error': 'conversation_id, sender_id, and content are required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Vérifier que la conversation existe
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
|
||||
# Vérifier que l'expéditeur existe et peut envoyer dans cette conversation
|
||||
sender = Profile.objects.get(id=sender_id)
|
||||
participant = ConversationParticipant.objects.filter(
|
||||
conversation=conversation,
|
||||
participant=sender,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if not participant:
|
||||
return Response(
|
||||
{'error': 'You are not a participant in this conversation'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
# Récupérer les données de fichier si disponibles
|
||||
file_url = request.data.get('file_url')
|
||||
file_name = request.data.get('file_name')
|
||||
file_type = request.data.get('file_type')
|
||||
file_size = request.data.get('file_size')
|
||||
|
||||
# Créer le message
|
||||
message = Message.objects.create(
|
||||
conversation=conversation,
|
||||
sender=sender,
|
||||
content=content,
|
||||
message_type=message_type,
|
||||
file_url=file_url,
|
||||
file_name=file_name,
|
||||
file_type=file_type,
|
||||
file_size=file_size
|
||||
)
|
||||
|
||||
# Mettre à jour l'activité de la conversation
|
||||
conversation.last_activity = message.created_at
|
||||
conversation.save(update_fields=['last_activity'])
|
||||
|
||||
serializer = MessageSerializer(message)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'Sender not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantMarkAsReadView(APIView):
|
||||
"""
|
||||
API pour marquer une conversation comme lue
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Marque une conversation comme lue pour un utilisateur",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'user_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID de l\'utilisateur')
|
||||
},
|
||||
required=['user_id']
|
||||
),
|
||||
responses={200: openapi.Response('Success')}
|
||||
)
|
||||
def post(self, request, conversation_id):
|
||||
try:
|
||||
user_id = request.data.get('user_id')
|
||||
if not user_id:
|
||||
return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
participant = ConversationParticipant.objects.get(
|
||||
conversation_id=conversation_id,
|
||||
participant_id=user_id,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
participant.last_read_at = timezone.now()
|
||||
participant.save(update_fields=['last_read_at'])
|
||||
|
||||
return Response({'status': 'success'}, status=status.HTTP_200_OK)
|
||||
|
||||
except ConversationParticipant.DoesNotExist:
|
||||
return Response({'error': 'Participant not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class UserPresenceView(APIView):
|
||||
"""
|
||||
API pour gérer la présence des utilisateurs
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Met à jour le statut de présence d'un utilisateur",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'status': openapi.Schema(type=openapi.TYPE_STRING, description='Statut de présence')
|
||||
},
|
||||
required=['status']
|
||||
),
|
||||
responses={200: UserPresenceSerializer}
|
||||
)
|
||||
def post(self, request, user_id):
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
status_value = request.data.get('status')
|
||||
|
||||
if status_value not in ['online', 'away', 'busy', 'offline']:
|
||||
return Response({'error': 'Invalid status'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
presence, created = UserPresence.objects.get_or_create(user=user)
|
||||
presence.status = status_value
|
||||
presence.last_seen = timezone.now()
|
||||
presence.save()
|
||||
|
||||
serializer = UserPresenceSerializer(presence)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Récupère le statut de présence d'un utilisateur",
|
||||
responses={200: UserPresenceSerializer}
|
||||
)
|
||||
def get(self, request, user_id):
|
||||
try:
|
||||
user = Profile.objects.get(id=user_id)
|
||||
presence, created = UserPresence.objects.get_or_create(user=user)
|
||||
|
||||
if created:
|
||||
presence.status = 'offline'
|
||||
presence.save()
|
||||
|
||||
serializer = UserPresenceSerializer(presence)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except Profile.DoesNotExist:
|
||||
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class FileUploadView(APIView):
|
||||
"""
|
||||
API pour l'upload de fichiers dans la messagerie instantanée
|
||||
"""
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Upload un fichier pour la messagerie",
|
||||
manual_parameters=[
|
||||
openapi.Parameter('file', openapi.IN_FORM, description="Fichier à uploader", type=openapi.TYPE_FILE, required=True),
|
||||
openapi.Parameter('conversation_id', openapi.IN_FORM, description="ID de la conversation", type=openapi.TYPE_INTEGER, required=True),
|
||||
openapi.Parameter('sender_id', openapi.IN_FORM, description="ID de l'expéditeur", type=openapi.TYPE_INTEGER, required=True),
|
||||
],
|
||||
responses={
|
||||
200: openapi.Response('Success', openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'fileUrl': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'fileName': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'fileSize': openapi.Schema(type=openapi.TYPE_INTEGER),
|
||||
'fileType': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
}
|
||||
)),
|
||||
400: 'Bad Request',
|
||||
413: 'File too large',
|
||||
415: 'Unsupported file type'
|
||||
}
|
||||
)
|
||||
def post(self, request):
|
||||
try:
|
||||
file = request.FILES.get('file')
|
||||
conversation_id = request.data.get('conversation_id')
|
||||
sender_id = request.data.get('sender_id')
|
||||
|
||||
if not file:
|
||||
return Response({'error': 'Aucun fichier fourni'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not conversation_id or not sender_id:
|
||||
return Response({'error': 'conversation_id et sender_id requis'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Vérifier que la conversation existe et que l'utilisateur y participe
|
||||
try:
|
||||
conversation = Conversation.objects.get(id=conversation_id)
|
||||
sender = Profile.objects.get(id=sender_id)
|
||||
|
||||
# Vérifier que l'expéditeur participe à la conversation
|
||||
if not ConversationParticipant.objects.filter(
|
||||
conversation=conversation,
|
||||
participant=sender,
|
||||
is_active=True
|
||||
).exists():
|
||||
return Response({'error': 'Accès non autorisé à cette conversation'}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
except (Conversation.DoesNotExist, Profile.DoesNotExist):
|
||||
return Response({'error': 'Conversation ou utilisateur introuvable'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Valider le type de fichier
|
||||
allowed_types = [
|
||||
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain'
|
||||
]
|
||||
|
||||
if file.content_type not in allowed_types:
|
||||
return Response({'error': 'Type de fichier non autorisé'}, status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
|
||||
|
||||
# Valider la taille du fichier (10MB max)
|
||||
max_size = 10 * 1024 * 1024 # 10MB
|
||||
if file.size > max_size:
|
||||
return Response({'error': 'Fichier trop volumineux (max 10MB)'}, status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
|
||||
|
||||
# Générer un nom de fichier unique
|
||||
file_extension = os.path.splitext(file.name)[1]
|
||||
unique_filename = f"{uuid.uuid4()}{file_extension}"
|
||||
|
||||
# Chemin de stockage : messagerie/conversation_id/
|
||||
storage_path = f"messagerie/{conversation_id}/{unique_filename}"
|
||||
|
||||
# Sauvegarder le fichier
|
||||
file_path = default_storage.save(storage_path, ContentFile(file.read()))
|
||||
|
||||
# Générer l'URL du fichier
|
||||
file_url = default_storage.url(file_path)
|
||||
if not file_url.startswith('http'):
|
||||
# Construire l'URL complète si nécessaire
|
||||
file_url = request.build_absolute_uri(file_url)
|
||||
|
||||
return Response({
|
||||
'fileUrl': file_url,
|
||||
'fileName': file.name,
|
||||
'fileSize': file.size,
|
||||
'fileType': file.content_type,
|
||||
'filePath': file_path
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
return Response({'error': f'Erreur lors de l\'upload: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantRecipientSearchView(APIView):
|
||||
"""
|
||||
API pour rechercher des destinataires pour la messagerie instantanée
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Recherche des destinataires pour la messagerie instantanée",
|
||||
manual_parameters=[
|
||||
openapi.Parameter('establishment_id', openapi.IN_QUERY, description="ID de l'établissement", type=openapi.TYPE_INTEGER, required=True),
|
||||
openapi.Parameter('q', openapi.IN_QUERY, description="Terme de recherche", type=openapi.TYPE_STRING, required=True)
|
||||
],
|
||||
responses={200: ProfileSimpleSerializer(many=True)}
|
||||
)
|
||||
def get(self, request):
|
||||
try:
|
||||
establishment_id = request.query_params.get('establishment_id')
|
||||
search_query = request.query_params.get('q', '').strip()
|
||||
|
||||
if not establishment_id:
|
||||
return Response({'error': 'establishment_id is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Récupérer les IDs des profils actifs dans l'établissement
|
||||
profile_roles = ProfileRole.objects.filter(
|
||||
establishment_id=establishment_id,
|
||||
is_active=True
|
||||
).values_list('profile_id', flat=True)
|
||||
|
||||
# Rechercher les profils correspondants
|
||||
users = Profile.objects.filter(id__in=profile_roles)
|
||||
|
||||
# Appliquer le filtre de recherche si un terme est fourni
|
||||
if search_query:
|
||||
users = users.filter(
|
||||
Q(first_name__icontains=search_query) |
|
||||
Q(last_name__icontains=search_query) |
|
||||
Q(email__icontains=search_query)
|
||||
)
|
||||
|
||||
# Exclure l'utilisateur actuel des résultats
|
||||
if request.user.is_authenticated:
|
||||
users = users.exclude(id=request.user.id)
|
||||
|
||||
serializer = ProfileSimpleSerializer(users[:10], many=True) # Limiter à 10 résultats
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
class InstantConversationDeleteView(APIView):
|
||||
"""
|
||||
API pour supprimer (désactiver) une conversation instantanée
|
||||
"""
|
||||
@swagger_auto_schema(
|
||||
operation_description="Supprime une conversation instantanée (désactivation soft)",
|
||||
responses={200: openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'success': openapi.Schema(type=openapi.TYPE_BOOLEAN),
|
||||
'message': openapi.Schema(type=openapi.TYPE_STRING)
|
||||
}
|
||||
)}
|
||||
)
|
||||
def delete(self, request, conversation_id):
|
||||
try:
|
||||
# Récupérer la conversation par son ID UUID
|
||||
conversation = Conversation.objects.filter(id=conversation_id).first()
|
||||
|
||||
if not conversation:
|
||||
return Response({'error': 'Conversation not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Suppression simple : désactiver la conversation
|
||||
conversation.is_active = False
|
||||
conversation.save()
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Conversation deleted successfully'
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting conversation: {str(e)}")
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return JsonResponse(message_serializer.errors, safe=False)
|
||||
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
@ -4,20 +4,20 @@ from .models import Notification, TypeNotif
|
||||
from GestionMessagerie.models import Messagerie
|
||||
from Subscriptions.models import RegistrationForm
|
||||
|
||||
@receiver(post_save, sender=Messagerie)
|
||||
def notification_MESSAGE(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
Notification.objects.create(
|
||||
user=instance.destinataire,
|
||||
message=(TypeNotif.NOTIF_MESSAGE).label,
|
||||
typeNotification=TypeNotif.NOTIF_MESSAGE
|
||||
)
|
||||
# @receiver(post_save, sender=Messagerie)
|
||||
# def notification_MESSAGE(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# Notification.objects.create(
|
||||
# user=instance.destinataire,
|
||||
# message=(TypeNotif.NOTIF_MESSAGE).label,
|
||||
# typeNotification=TypeNotif.NOTIF_MESSAGE
|
||||
# )
|
||||
|
||||
@receiver(post_save, sender=RegistrationForm)
|
||||
def notification_DI(sender, instance, created, **kwargs):
|
||||
for responsable in instance.student.guardians.all():
|
||||
Notification.objects.create(
|
||||
user=responsable.associated_profile,
|
||||
message=(TypeNotif.NOTIF_DI).label,
|
||||
typeNotification=TypeNotif.NOTIF_DI
|
||||
)
|
||||
# @receiver(post_save, sender=RegistrationForm)
|
||||
# def notification_DI(sender, instance, created, **kwargs):
|
||||
# for responsable in instance.student.guardians.all():
|
||||
# Notification.objects.create(
|
||||
# user=responsable.associated_profile,
|
||||
# message=(TypeNotif.NOTIF_DI).label,
|
||||
# typeNotification=TypeNotif.NOTIF_DI
|
||||
# )
|
||||
|
||||
@ -3,5 +3,5 @@ from django.urls import path, re_path
|
||||
from GestionNotification.views import NotificationView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^notification$', NotificationView.as_view(), name="notification"),
|
||||
re_path(r'^notifications$', NotificationView.as_view(), name="notifications"),
|
||||
]
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"hostSMTP": "",
|
||||
"portSMTP": 25,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"useSSL": false,
|
||||
"useTLS": false
|
||||
}
|
||||
@ -8,9 +8,40 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from django.urls import re_path
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'N3wtSchool.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
# is populated before importing code that may import ORM models.
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
# Import consumers after Django is initialized
|
||||
from GestionMessagerie.consumers import ChatConsumer
|
||||
from GestionMessagerie.middleware import JWTAuthMiddlewareStack
|
||||
|
||||
# WebSocket URL patterns
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<user_id>\w+)/$', ChatConsumer.as_asgi()),
|
||||
]
|
||||
|
||||
# Créer l'application ASGI avec gestion des fichiers statiques
|
||||
if settings.DEBUG:
|
||||
# En mode DEBUG, utiliser ASGIStaticFilesHandler pour servir les fichiers statiques
|
||||
http_application = ASGIStaticFilesHandler(django_asgi_app)
|
||||
else:
|
||||
http_application = django_asgi_app
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": http_application,
|
||||
"websocket": AllowedHostsOriginValidator(
|
||||
JWTAuthMiddlewareStack(
|
||||
URLRouter(websocket_urlpatterns)
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
@ -2,7 +2,8 @@ import logging
|
||||
from django.db.models import Q
|
||||
from django.http import JsonResponse
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from Subscriptions.models import RegistrationForm, Profile, Student
|
||||
from Subscriptions.models import RegistrationForm, Student
|
||||
from Auth.models import Profile
|
||||
|
||||
logger = logging.getLogger('N3wtSchool')
|
||||
|
||||
@ -92,6 +93,7 @@ def searchObjects(_objectName, _searchTerm=None, _excludeStates=None):
|
||||
def delete_object(model_class, object_id, related_field=None):
|
||||
try:
|
||||
obj = model_class.objects.get(id=object_id)
|
||||
|
||||
if related_field and hasattr(obj, related_field):
|
||||
related_obj = getattr(obj, related_field)
|
||||
if related_obj:
|
||||
@ -103,5 +105,3 @@ def delete_object(model_class, object_id, related_field=None):
|
||||
return JsonResponse({'error': f'L\'objet {model_class.__name__} n\'existe pas avec cet ID'}, status=404, safe=False)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': f'Une erreur est survenue : {str(e)}'}, status=500, safe=False)
|
||||
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
201
Back-End/N3wtSchool/mailManager.py
Normal file
201
Back-End/N3wtSchool/mailManager.py
Normal file
@ -0,0 +1,201 @@
|
||||
from django.core.mail import send_mail, get_connection, EmailMultiAlternatives, EmailMessage
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
import re
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import NotFound
|
||||
from Settings.models import SMTPSettings
|
||||
from Establishment.models import Establishment # Importer le modèle Establishment
|
||||
import logging
|
||||
|
||||
# Ajouter un logger pour debug
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def getConnection(id_establishement):
|
||||
try:
|
||||
# Récupérer l'instance de l'établissement
|
||||
establishment = Establishment.objects.get(id=id_establishement)
|
||||
try:
|
||||
# Récupérer les paramètres SMTP associés à l'établissement
|
||||
smtp_settings = SMTPSettings.objects.get(establishment=establishment)
|
||||
|
||||
# Créer une connexion SMTP avec les paramètres récupérés
|
||||
connection = get_connection(
|
||||
host=smtp_settings.smtp_server,
|
||||
port=smtp_settings.smtp_port,
|
||||
username=smtp_settings.smtp_user,
|
||||
password=smtp_settings.smtp_password,
|
||||
use_tls=smtp_settings.use_tls,
|
||||
use_ssl=smtp_settings.use_ssl
|
||||
)
|
||||
return connection
|
||||
except SMTPSettings.DoesNotExist:
|
||||
# Aucun paramètre SMTP spécifique, retournera None
|
||||
return None
|
||||
except Establishment.DoesNotExist:
|
||||
raise NotFound(f"Aucun établissement trouvé avec l'ID {id_establishement}")
|
||||
|
||||
def sendMail(subject, message, recipients, cc=[], bcc=[], attachments=[], connection=None):
|
||||
try:
|
||||
# S'assurer que recipients, cc, bcc sont des listes
|
||||
if isinstance(recipients, str):
|
||||
recipients = [recipients]
|
||||
if isinstance(cc, str):
|
||||
cc = [cc]
|
||||
if isinstance(bcc, str):
|
||||
bcc = [bcc]
|
||||
|
||||
# Récupération robuste du username
|
||||
username = getattr(connection, 'username', None)
|
||||
|
||||
plain_message = strip_tags(message)
|
||||
if connection is not None:
|
||||
from_email = username
|
||||
else:
|
||||
from_email = settings.EMAIL_HOST_USER
|
||||
|
||||
|
||||
logger.info(f"From email: {from_email}")
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=plain_message,
|
||||
from_email=from_email,
|
||||
to=recipients,
|
||||
cc=cc,
|
||||
bcc=bcc,
|
||||
connection=connection
|
||||
)
|
||||
email.attach_alternative(message, "text/html")
|
||||
|
||||
for attachment in attachments:
|
||||
email.attach(*attachment)
|
||||
|
||||
logger.info("Tentative d'envoi de l'email...")
|
||||
email.send(fail_silently=False)
|
||||
logger.info("Email envoyé avec succès !")
|
||||
return Response({'message': 'Email envoyé avec succès.'}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'envoi de l'email: {str(e)}")
|
||||
logger.error(f"Type d'erreur: {type(e)}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def envoieReinitMotDePasse(recipients, code):
|
||||
errorMessage = ''
|
||||
try:
|
||||
EMAIL_REINIT_SUBJECT = 'Réinitialisation du mot de passe'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'code': str(code)
|
||||
}
|
||||
subject = EMAIL_REINIT_SUBJECT
|
||||
html_message = render_to_string('emails/resetPassword.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients)
|
||||
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
|
||||
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 = ''
|
||||
try:
|
||||
# Préparation du contexte pour le template
|
||||
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Dossier Inscription'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'email': recipients,
|
||||
'establishment': establishment_id
|
||||
}
|
||||
# Récupérer la connexion SMTP
|
||||
connection = getConnection(establishment_id)
|
||||
subject = EMAIL_INSCRIPTION_SUBJECT
|
||||
html_message = render_to_string('emails/inscription.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
|
||||
return errorMessage
|
||||
|
||||
def sendMandatSEPA(recipients, establishment_id):
|
||||
errorMessage = ''
|
||||
try:
|
||||
# Préparation du contexte pour le template
|
||||
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Mandat de prélèvement SEPA'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'email': recipients,
|
||||
'establishment': establishment_id
|
||||
}
|
||||
|
||||
# Récupérer la connexion SMTP
|
||||
connection = getConnection(establishment_id)
|
||||
subject = EMAIL_INSCRIPTION_SUBJECT
|
||||
html_message = render_to_string('emails/sepa.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection)
|
||||
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
|
||||
return errorMessage
|
||||
|
||||
def envoieRelanceDossierInscription(recipients, code):
|
||||
EMAIL_RELANCE_SUBJECT = '[N3WT-SCHOOL] Relance - Dossier Inscription'
|
||||
EMAIL_RELANCE_CORPUS = 'Bonjour,\nN\'ayant pas eu de retour de votre part, nous vous renvoyons le lien vers le formulaire d\'inscription : ' + BASE_URL + '/users/login\nCordialement'
|
||||
errorMessage = ''
|
||||
try:
|
||||
sendMail(EMAIL_RELANCE_SUBJECT, EMAIL_RELANCE_CORPUS%str(code), recipients)
|
||||
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
|
||||
return errorMessage
|
||||
|
||||
def isValid(message, fiche_inscription):
|
||||
# Est-ce que la référence du dossier est VALIDATED
|
||||
subject = message.subject
|
||||
print ("++++ " + subject)
|
||||
responsableMail = message.from_header
|
||||
result = re.search('<(.*)>', responsableMail)
|
||||
|
||||
if result:
|
||||
responsableMail = result.group(1)
|
||||
|
||||
result = re.search(r'.*\[Ref(.*)\].*', subject)
|
||||
idMail = -1
|
||||
if result:
|
||||
idMail = result.group(1).strip()
|
||||
|
||||
eleve = fiche_inscription.eleve
|
||||
responsable = eleve.getMainGuardian()
|
||||
mailReponsableAVerifier = responsable.mail
|
||||
|
||||
return responsableMail == mailReponsableAVerifier and str(idMail) == str(fiche_inscription.eleve.id)
|
||||
11
Back-End/N3wtSchool/middleware.py
Normal file
11
Back-End/N3wtSchool/middleware.py
Normal file
@ -0,0 +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'] = f"frame-ancestors 'self' {settings.BASE_URL}"
|
||||
return response
|
||||
@ -4,11 +4,22 @@ from django.template.loader import get_template
|
||||
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
class PDFResult:
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
|
||||
def render_to_pdf(template_src, context_dict={}):
|
||||
"""
|
||||
Génère un PDF à partir d'un template HTML et retourne le contenu en mémoire.
|
||||
"""
|
||||
template = get_template(template_src)
|
||||
html = template.render(context_dict)
|
||||
html = template.render(context_dict)
|
||||
result = BytesIO()
|
||||
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
|
||||
|
||||
if pdf.err:
|
||||
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
|
||||
return HttpResponse(result.getvalue(), content_type='application/pdf')
|
||||
# Lever une exception ou retourner None en cas d'erreur
|
||||
raise ValueError("Erreur lors de la génération du PDF.")
|
||||
|
||||
# Retourner le contenu du PDF en mémoire
|
||||
return PDFResult(result.getvalue())
|
||||
@ -13,33 +13,43 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
# Configuration du logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
MEDIA_URL = '/data/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'data')
|
||||
|
||||
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 = ['*']
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'Common.apps.CommonConfig',
|
||||
'Subscriptions.apps.GestioninscriptionsConfig',
|
||||
'Auth.apps.GestionloginConfig',
|
||||
'GestionMessagerie.apps.GestionMessagerieConfig',
|
||||
'GestionEmail.apps.GestionEmailConfig',
|
||||
'GestionNotification.apps.GestionNotificationConfig',
|
||||
'School.apps.SchoolConfig',
|
||||
'Planning.apps.PlanningConfig',
|
||||
'Establishment.apps.EstablishmentConfig',
|
||||
'Settings.apps.SettingsConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@ -51,17 +61,20 @@ INSTALLED_APPS = [
|
||||
'django_celery_beat',
|
||||
'N3wtSchool',
|
||||
'drf_yasg',
|
||||
'rest_framework_simplejwt',
|
||||
'channels',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware', # Déplacez ici, avant CorsMiddleware
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'N3wtSchool.middleware.ContentSecurityPolicyMiddleware'
|
||||
]
|
||||
|
||||
|
||||
@ -148,6 +161,11 @@ LOGGING = {
|
||||
"level": os.getenv("GESTION_MESSAGERIE_LOG_LEVEL", "INFO"),
|
||||
"propagate": False,
|
||||
},
|
||||
"GestionEmail": {
|
||||
"handlers": ["console"],
|
||||
"level": os.getenv("GESTION_EMAIL_LOG_LEVEL", "INFO"),
|
||||
"propagate": False,
|
||||
},
|
||||
"School": {
|
||||
"handlers": ["console"],
|
||||
"level": os.getenv("GESTION_ENSEIGNANTS_LOG_LEVEL", "INFO"),
|
||||
@ -191,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 = [
|
||||
@ -211,67 +227,71 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
#################### Application Settings ##############################
|
||||
########################################################################
|
||||
|
||||
with open('Subscriptions/Configuration/application.json', 'r') as f:
|
||||
jsonObject = json.load(f)
|
||||
|
||||
DJANGO_SUPERUSER_PASSWORD='admin'
|
||||
DJANGO_SUPERUSER_USERNAME='admin'
|
||||
DJANGO_SUPERUSER_EMAIL='admin@n3wtschool.com'
|
||||
|
||||
EMAIL_HOST='smtp.gmail.com'
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=jsonObject['mailFrom']
|
||||
EMAIL_HOST_PASSWORD=jsonObject['password']
|
||||
# Configuration de l'email de l'application
|
||||
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 = True
|
||||
EMAIL_USE_SSL = False
|
||||
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Dossier Inscription'
|
||||
EMAIL_INSCRIPTION_CORPUS = """Bonjour,
|
||||
|
||||
Afin de procéder à l'inscription de votre petit bout, vous trouverez ci-joint le lien vers la page d'authentification : http://localhost:3000/users/login
|
||||
|
||||
S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte : http://localhost:3000/users/subscribe
|
||||
identifiant = %s
|
||||
|
||||
Cordialement,
|
||||
"""
|
||||
|
||||
EMAIL_RELANCE_SUBJECT = '[N3WT-SCHOOL] Relance - Dossier Inscription'
|
||||
EMAIL_RELANCE_CORPUS = 'Bonjour,\nN\'ayant pas eu de retour de votre part, nous vous renvoyons le lien vers le formulaire d\'inscription : http://localhost:3000/users/login\nCordialement'
|
||||
EMAIL_REINIT_SUBJECT = 'Réinitialisation du mot de passe'
|
||||
EMAIL_REINIT_CORPUS = 'Bonjour,\nVous trouverez ci-joint le lien pour réinitialiser votre mot de passe : http://localhost:3000/users/password/reset?uuid=%s\nCordialement'
|
||||
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'
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_ALL_HEADERS = True
|
||||
# Configuration CORS temporaire pour debug
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000"
|
||||
# Configuration CORS spécifique pour la production
|
||||
CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000,http://127.0.0.1:8080').split(',')
|
||||
|
||||
|
||||
CORS_ALLOW_HEADERS = [
|
||||
'accept',
|
||||
'accept-encoding',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'dnt',
|
||||
'origin',
|
||||
'user-agent',
|
||||
'x-csrftoken',
|
||||
'x-requested-with',
|
||||
'X-Auth-Token',
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
"http://localhost:3000", # Front Next.js
|
||||
"http://localhost:8080" # Insomnia
|
||||
# Méthodes HTTP autorisées
|
||||
CORS_ALLOWED_METHODS = [
|
||||
'DELETE',
|
||||
'GET',
|
||||
'OPTIONS',
|
||||
'PATCH',
|
||||
'POST',
|
||||
'PUT',
|
||||
]
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,12 +305,17 @@ DATE_FORMAT = '%d-%m-%Y %H:%M'
|
||||
|
||||
EXPIRATION_SESSION_NB_SEC = 10
|
||||
|
||||
NB_RESULT_PER_PAGE = 8
|
||||
NB_RESULT_SUBSCRIPTIONS_PER_PAGE = 8
|
||||
NB_RESULT_PROFILES_PER_PAGE = 15
|
||||
NB_MAX_PAGE = 100
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomPagination',
|
||||
'PAGE_SIZE': NB_RESULT_PER_PAGE
|
||||
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
|
||||
'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE,
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
),
|
||||
}
|
||||
|
||||
CELERY_BROKER_URL = 'redis://redis:6379/0'
|
||||
@ -301,11 +326,44 @@ 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),
|
||||
'ROTATE_REFRESH_TOKENS': False,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'VERIFYING_KEY': None,
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
'USER_ID_FIELD': 'id',
|
||||
'USER_ID_CLAIM': 'user_id',
|
||||
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
||||
}
|
||||
|
||||
# Configuration for DocuSeal JWT
|
||||
DOCUSEAL_JWT = {
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'EXPIRATION_DELTA': timedelta(hours=1)
|
||||
}
|
||||
|
||||
# Django Channels Configuration
|
||||
ASGI_APPLICATION = 'N3wtSchool.asgi.application'
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [('redis', 6379)],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -39,11 +39,17 @@ schema_view = get_schema_view(
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("Common/", include(("Common.urls", 'Common'), namespace='Common')),
|
||||
path("Subscriptions/", include(("Subscriptions.urls", 'Subscriptions'), namespace='Subscriptions')),
|
||||
path("Auth/", include(("Auth.urls", 'Auth'), namespace='Auth')),
|
||||
path("GestionMessagerie/", include(("GestionMessagerie.urls", 'GestionMessagerie'), namespace='GestionMessagerie')),
|
||||
path("GestionEmail/", include(("GestionEmail.urls", 'GestionEmail'), namespace='GestionEmail')),
|
||||
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
|
||||
path("School/", include(("School.urls", 'School'), namespace='School')),
|
||||
path("DocuSeal/", include(("DocuSeal.urls", 'DocuSeal'), namespace='DocuSeal')),
|
||||
path("Planning/", include(("Planning.urls", 'Planning'), namespace='Planning')),
|
||||
path("Establishment/", include(("Establishment.urls", 'Establishment'), namespace='Establishment')),
|
||||
path("Settings/", include(("Settings.urls", 'Settings'), namespace='Settings')),
|
||||
# Documentation Api
|
||||
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
|
||||
1
Back-End/Planning/__init__.py
Normal file
1
Back-End/Planning/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
default_app_config = 'Planning.apps.PlanningConfig'
|
||||
3
Back-End/Planning/admin.py
Normal file
3
Back-End/Planning/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
7
Back-End/Planning/apps.py
Normal file
7
Back-End/Planning/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class PlanningConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'Planning'
|
||||
|
||||
|
||||
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
47
Back-End/Planning/models.py
Normal file
47
Back-End/Planning/models.py
Normal file
@ -0,0 +1,47 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from School.models import SchoolClass
|
||||
|
||||
from Establishment.models import Establishment
|
||||
|
||||
class RecursionType(models.IntegerChoices):
|
||||
RECURSION_NONE = 0, _('Aucune')
|
||||
RECURSION_DAILY = 1, _('Quotidienne')
|
||||
RECURSION_WEEKLY = 2, _('Hebdomadaire')
|
||||
RECURSION_MONTHLY = 3, _('Mensuel')
|
||||
RECURSION_CUSTOM = 4, _('Personnalisé')
|
||||
|
||||
class Planning(models.Model):
|
||||
establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE)
|
||||
school_class = models.ForeignKey(
|
||||
SchoolClass,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="planning",
|
||||
null=True, # Permet des valeurs nulles
|
||||
blank=True # Rend le champ facultatif dans les formulaires
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(default="", blank=True, null=True)
|
||||
color= models.CharField(max_length=255, default="#000000")
|
||||
|
||||
def __str__(self):
|
||||
return f'Planning {self.name}'
|
||||
|
||||
|
||||
class Events(models.Model):
|
||||
planning = models.ForeignKey(Planning, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=255)
|
||||
description = models.TextField(default="", blank=True, null=True)
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField()
|
||||
recursionType = models.IntegerField(choices=RecursionType, default=0)
|
||||
recursionEnd = models.DateTimeField(default=None, blank=True, null=True)
|
||||
color= models.CharField(max_length=255)
|
||||
location = models.CharField(max_length=255, default="", blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f'Event {self.title}'
|
||||
13
Back-End/Planning/serializers.py
Normal file
13
Back-End/Planning/serializers.py
Normal file
@ -0,0 +1,13 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Planning, Events
|
||||
|
||||
|
||||
class PlanningSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Planning
|
||||
fields = '__all__'
|
||||
class EventsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Events
|
||||
fields = '__all__'
|
||||
11
Back-End/Planning/urls.py
Normal file
11
Back-End/Planning/urls.py
Normal file
@ -0,0 +1,11 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from Planning.views import PlanningView,PlanningWithIdView,EventsView,EventsWithIdView,UpcomingEventsView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^plannings$', PlanningView.as_view(), name="planning"),
|
||||
re_path(r'^plannings/(?P<id>[0-9]+)$', PlanningWithIdView.as_view(), name="planning"),
|
||||
re_path(r'^events$', EventsView.as_view(), name="events"),
|
||||
re_path(r'^events/(?P<id>[0-9]+)$', EventsWithIdView.as_view(), name="events"),
|
||||
re_path(r'^events/upcoming', UpcomingEventsView.as_view(), name="events"),
|
||||
]
|
||||
169
Back-End/Planning/views.py
Normal file
169
Back-End/Planning/views.py
Normal file
@ -0,0 +1,169 @@
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from django.utils import timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from .models import Planning, Events, RecursionType
|
||||
|
||||
from .serializers import PlanningSerializer, EventsSerializer
|
||||
|
||||
from N3wtSchool import bdd
|
||||
|
||||
|
||||
class PlanningView(APIView):
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
planning_mode = request.GET.get('planning_mode', None)
|
||||
|
||||
plannings = bdd.getAllObjects(Planning)
|
||||
|
||||
if establishment_id is not None:
|
||||
plannings = plannings.filter(establishment=establishment_id)
|
||||
|
||||
# Filtrer en fonction du planning_mode
|
||||
if planning_mode == "classSchedule":
|
||||
plannings = plannings.filter(school_class__isnull=False)
|
||||
elif planning_mode == "planning":
|
||||
plannings = plannings.filter(school_class__isnull=True)
|
||||
|
||||
planning_serializer = PlanningSerializer(plannings.distinct(), many=True)
|
||||
return JsonResponse(planning_serializer.data, safe=False)
|
||||
|
||||
def post(self, request):
|
||||
planning_serializer = PlanningSerializer(data=request.data)
|
||||
if planning_serializer.is_valid():
|
||||
planning_serializer.save()
|
||||
return JsonResponse(planning_serializer.data, status=201)
|
||||
return JsonResponse(planning_serializer.errors, status=400)
|
||||
|
||||
|
||||
|
||||
class PlanningWithIdView(APIView):
|
||||
def get(self, request,id):
|
||||
planning = Planning.objects.get(pk=id)
|
||||
if planning is None:
|
||||
return JsonResponse({"errorMessage":'Le dossier d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
planning_serializer=PlanningSerializer(planning)
|
||||
|
||||
return JsonResponse(planning_serializer.data, safe=False)
|
||||
|
||||
def put(self, request, id):
|
||||
try:
|
||||
planning = Planning.objects.get(pk=id)
|
||||
except Planning.DoesNotExist:
|
||||
return JsonResponse({'error': 'Planning not found'}, status=404)
|
||||
|
||||
planning_serializer = PlanningSerializer(planning, data=request.data)
|
||||
if planning_serializer.is_valid():
|
||||
planning_serializer.save()
|
||||
return JsonResponse(planning_serializer.data)
|
||||
return JsonResponse(planning_serializer.errors, status=400)
|
||||
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
planning = Planning.objects.get(pk=id)
|
||||
except Planning.DoesNotExist:
|
||||
return JsonResponse({'error': 'Planning not found'}, status=404)
|
||||
|
||||
planning.delete()
|
||||
return JsonResponse({'message': 'Planning deleted'}, status=204)
|
||||
|
||||
class EventsView(APIView):
|
||||
def get(self, request):
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
planning_mode = request.GET.get('planning_mode', None)
|
||||
filterParams = {}
|
||||
plannings=[]
|
||||
events = Events.objects.all()
|
||||
if establishment_id is not None :
|
||||
filterParams['establishment'] = establishment_id
|
||||
if planning_mode is not None:
|
||||
filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
|
||||
if filterParams:
|
||||
plannings = Planning.objects.filter(**filterParams)
|
||||
events = Events.objects.filter(planning__in=plannings)
|
||||
events_serializer = EventsSerializer(events, many=True)
|
||||
return JsonResponse(events_serializer.data, safe=False)
|
||||
|
||||
def post(self, request):
|
||||
events_serializer = EventsSerializer(data=request.data)
|
||||
if events_serializer.is_valid():
|
||||
event = events_serializer.save()
|
||||
|
||||
# Gérer les événements récurrents
|
||||
if event.recursionType != RecursionType.RECURSION_NONE:
|
||||
self.create_recurring_events(event)
|
||||
|
||||
return JsonResponse(events_serializer.data, status=201)
|
||||
return JsonResponse(events_serializer.errors, status=400)
|
||||
|
||||
def create_recurring_events(self, event):
|
||||
current_start = event.start
|
||||
current_end = event.end
|
||||
|
||||
while current_start < event.recursionEnd:
|
||||
if event.recursionType == RecursionType.RECURSION_DAILY:
|
||||
current_start += relativedelta(days=1)
|
||||
current_end += relativedelta(days=1)
|
||||
elif event.recursionType == RecursionType.RECURSION_WEEKLY:
|
||||
current_start += relativedelta(weeks=1)
|
||||
current_end += relativedelta(weeks=1)
|
||||
elif event.recursionType == RecursionType.RECURSION_MONTHLY:
|
||||
current_start += relativedelta(months=1)
|
||||
current_end += relativedelta(months=1)
|
||||
else:
|
||||
break # Pour d'autres types de récurrence non gérés
|
||||
|
||||
# Créer une nouvelle occurrence
|
||||
Events.objects.create(
|
||||
planning=event.planning,
|
||||
title=event.title,
|
||||
description=event.description,
|
||||
start=current_start,
|
||||
end=current_end,
|
||||
recursionEnd=event.recursionEnd,
|
||||
recursionType=event.recursionType, # Les occurrences ne sont pas récurrentes
|
||||
color=event.color,
|
||||
location=event.location,
|
||||
)
|
||||
|
||||
class EventsWithIdView(APIView):
|
||||
def put(self, request, id):
|
||||
try:
|
||||
event = Events.objects.get(pk=id)
|
||||
except Events.DoesNotExist:
|
||||
return JsonResponse({'error': 'Event not found'}, status=404)
|
||||
|
||||
events_serializer = EventsSerializer(event, data=request.data)
|
||||
if events_serializer.is_valid():
|
||||
events_serializer.save()
|
||||
return JsonResponse(events_serializer.data)
|
||||
return JsonResponse(events_serializer.errors, status=400)
|
||||
|
||||
def delete(self, request, id):
|
||||
try:
|
||||
event = Events.objects.get(pk=id)
|
||||
except Events.DoesNotExist:
|
||||
return JsonResponse({'error': 'Event not found'}, status=404)
|
||||
|
||||
event.delete()
|
||||
return JsonResponse({'message': 'Event deleted'}, status=200)
|
||||
|
||||
class UpcomingEventsView(APIView):
|
||||
def get(self, request):
|
||||
current_date = timezone.now()
|
||||
establishment_id = request.GET.get('establishment_id', None)
|
||||
|
||||
if establishment_id is not None:
|
||||
# Filtrer les plannings par establishment_id et sans school_class
|
||||
plannings = Planning.objects.filter(establishment=establishment_id, school_class__isnull=True)
|
||||
# Filtrer les événements associés à ces plannings et qui sont à venir
|
||||
upcoming_events = Events.objects.filter(planning__in=plannings, start__gte=current_date)
|
||||
else:
|
||||
# Récupérer tous les événements à venir si aucun establishment_id n'est fourni
|
||||
# et les plannings ne doivent pas être rattachés à une school_class
|
||||
plannings = Planning.objects.filter(school_class__isnull=True)
|
||||
upcoming_events = Events.objects.filter(planning__in=plannings, start__gte=current_date)
|
||||
|
||||
events_serializer = EventsSerializer(upcoming_events, many=True)
|
||||
return JsonResponse(events_serializer.data, safe=False)
|
||||
@ -1,14 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
def create_speciality(sender, **kwargs):
|
||||
from .models import Speciality
|
||||
if not Speciality.objects.filter(name='GROUPE').exists():
|
||||
Speciality.objects.create(name='GROUPE', color_code='#FF0000')
|
||||
|
||||
class SchoolConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'School'
|
||||
|
||||
def ready(self):
|
||||
post_migrate.connect(create_speciality, sender=self)
|
||||
name = 'School'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
427
Back-End/School/management/commands/init_mock_datas.py
Normal file
427
Back-End/School/management/commands/init_mock_datas.py
Normal file
@ -0,0 +1,427 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from Subscriptions.models import (
|
||||
RegistrationForm,
|
||||
RegistrationFileGroup
|
||||
)
|
||||
from Auth.models import Profile, ProfileRole
|
||||
from Common.models import (
|
||||
PaymentModeType,
|
||||
PaymentPlanType,
|
||||
Level
|
||||
)
|
||||
from School.models import (
|
||||
FeeType,
|
||||
Speciality,
|
||||
Teacher,
|
||||
DiscountType,
|
||||
Fee,
|
||||
Discount,
|
||||
)
|
||||
from django.utils import timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import os
|
||||
from django.conf import settings
|
||||
from faker import Faker
|
||||
import random
|
||||
import json
|
||||
|
||||
from School.serializers import (
|
||||
FeeSerializer,
|
||||
DiscountSerializer,
|
||||
PaymentModeSerializer,
|
||||
PaymentPlanSerializer,
|
||||
SpecialitySerializer,
|
||||
TeacherSerializer,
|
||||
SchoolClassSerializer
|
||||
)
|
||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||
from Establishment.serializers import EstablishmentSerializer
|
||||
from Subscriptions.serializers import StudentSerializer
|
||||
|
||||
from Subscriptions.util import getCurrentSchoolYear, getNextSchoolYear # Import des fonctions nécessaires
|
||||
|
||||
# Définir le chemin vers le dossier mock_datas
|
||||
MOCK_DATAS_PATH = os.path.join(settings.BASE_DIR, 'School', 'management', 'mock_datas')
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Initialise toutes les données mock'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.init_establishments()
|
||||
self.init_profiles()
|
||||
self.init_fees()
|
||||
self.init_discounts()
|
||||
self.init_payment_modes()
|
||||
self.init_payment_plans()
|
||||
self.init_specialities()
|
||||
self.init_teachers()
|
||||
self.init_school_classes()
|
||||
self.init_file_group()
|
||||
self.init_register_form()
|
||||
|
||||
def load_data(self, filename):
|
||||
with open(os.path.join(MOCK_DATAS_PATH, filename), 'r') as file:
|
||||
return json.load(file)
|
||||
|
||||
def init_establishments(self):
|
||||
establishments_data = self.load_data('establishments.json')
|
||||
|
||||
self.establishments = []
|
||||
for establishment_data in establishments_data:
|
||||
serializer = EstablishmentSerializer(data=establishment_data)
|
||||
if serializer.is_valid():
|
||||
establishment = serializer.save()
|
||||
self.establishments.append(establishment)
|
||||
self.stdout.write(self.style.SUCCESS(f'Establishment {establishment.name} created or updated successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for establishment: {serializer.errors}'))
|
||||
|
||||
def init_profiles(self):
|
||||
fake = Faker()
|
||||
|
||||
for _ in range(50):
|
||||
# Générer des données fictives pour le profil
|
||||
profile_data = {
|
||||
"username": fake.user_name(),
|
||||
"email": fake.email(),
|
||||
"password": "Provisoire01!",
|
||||
"code": "",
|
||||
"datePeremption": ""
|
||||
}
|
||||
|
||||
# Créer le profil
|
||||
profile_serializer = ProfileSerializer(data=profile_data)
|
||||
if profile_serializer.is_valid():
|
||||
profile = profile_serializer.save()
|
||||
profile.set_password(profile_data["password"])
|
||||
profile.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Profile {profile.email} created successfully'))
|
||||
|
||||
# Créer entre 1 et 3 ProfileRole pour chaque profil
|
||||
num_roles = random.randint(1, 3)
|
||||
created_roles = set()
|
||||
for _ in range(num_roles):
|
||||
establishment = random.choice(self.establishments)
|
||||
role_type = random.choice([ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN, ProfileRole.RoleType.PROFIL_PARENT])
|
||||
|
||||
# Vérifier si le rôle existe déjà pour cet établissement
|
||||
if (establishment.id, role_type) in created_roles:
|
||||
continue
|
||||
|
||||
profile_role_data = {
|
||||
"profile": profile.id,
|
||||
"establishment": establishment.id,
|
||||
"role_type": role_type,
|
||||
"is_active": random.choice([True, False])
|
||||
}
|
||||
|
||||
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
|
||||
if profile_role_serializer.is_valid():
|
||||
profile_role_serializer.save()
|
||||
created_roles.add((establishment.id, role_type))
|
||||
self.stdout.write(self.style.SUCCESS(f'ProfileRole for {profile.email} created successfully with role type {role_type}'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for profile role: {profile_role_serializer.errors}'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for profile: {profile_serializer.errors}'))
|
||||
|
||||
def init_fees(self):
|
||||
fees_data = self.load_data('fees.json')
|
||||
|
||||
for fee_data in fees_data:
|
||||
establishment = random.choice(self.establishments)
|
||||
print(f'establishment : {establishment}')
|
||||
fee_data["name"] = fee_data['name']
|
||||
fee_data["establishment"] = establishment.id
|
||||
fee_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE])
|
||||
|
||||
serializer = FeeSerializer(data=fee_data)
|
||||
if serializer.is_valid():
|
||||
fee = serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Fee {fee.name} created successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for fee: {serializer.errors}'))
|
||||
|
||||
def init_discounts(self):
|
||||
discounts_data = self.load_data('discounts.json')
|
||||
|
||||
for discount_data in discounts_data:
|
||||
establishment = random.choice(self.establishments)
|
||||
discount_data["name"] = discount_data['name']
|
||||
discount_data["establishment"] = establishment.id
|
||||
discount_data["type"] = random.choice([FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE])
|
||||
discount_data["discount_type"] = random.choice([DiscountType.CURRENCY, DiscountType.PERCENT])
|
||||
|
||||
serializer = DiscountSerializer(data=discount_data)
|
||||
if serializer.is_valid():
|
||||
discount = serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Discount {discount.name} created successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for discount: {serializer.errors}'))
|
||||
|
||||
def init_payment_modes(self):
|
||||
modes = list(PaymentModeType.objects.filter(code__in=["SEPA", "TRANSFER", "CHECK", "CASH"]))
|
||||
types = [FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE]
|
||||
|
||||
for establishment in self.establishments:
|
||||
for mode in modes:
|
||||
for type in types:
|
||||
payment_mode_data = {
|
||||
"mode": mode.pk,
|
||||
"type": type,
|
||||
"establishment": establishment.id
|
||||
}
|
||||
|
||||
serializer = PaymentModeSerializer(data=payment_mode_data)
|
||||
if serializer.is_valid():
|
||||
payment_mode = serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Payment Mode {payment_mode} created successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for payment mode: {serializer.errors}'))
|
||||
|
||||
def init_payment_plans(self):
|
||||
frequencies = list(PaymentPlanType.objects.filter(code__in=["ONE_TIME", "THREE_TIMES", "TEN_TIMES", "TWELVE_TIMES"]))
|
||||
types = [FeeType.REGISTRATION_FEE, FeeType.TUITION_FEE]
|
||||
current_date = timezone.now().date()
|
||||
|
||||
for establishment in self.establishments:
|
||||
for frequency in frequencies:
|
||||
for type in types:
|
||||
payment_plan_data = {
|
||||
"frequency": frequency.pk,
|
||||
"type": type,
|
||||
"establishment": establishment.id,
|
||||
"due_dates": self.generate_due_dates(frequency, current_date)
|
||||
}
|
||||
|
||||
serializer = PaymentPlanSerializer(data=payment_plan_data)
|
||||
if serializer.is_valid():
|
||||
payment_plan = serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Payment Plan {payment_plan} created successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for payment plan: {serializer.errors}'))
|
||||
|
||||
def generate_due_dates(self, frequency, start_date):
|
||||
if frequency.code == "ONE_TIME":
|
||||
return [start_date + relativedelta(months=1)]
|
||||
elif frequency.code == "THREE_TIMES":
|
||||
return [start_date + relativedelta(months=1+4*i) for i in range(3)]
|
||||
elif frequency.code == "TEN_TIMES":
|
||||
return [start_date + relativedelta(months=1+i) for i in range(10)]
|
||||
elif frequency.code == "TWELVE_TIMES":
|
||||
return [start_date + relativedelta(months=1+i) for i in range(12)]
|
||||
|
||||
def init_specialities(self):
|
||||
specialities_data = self.load_data('specialities.json')
|
||||
|
||||
for speciality_data in specialities_data:
|
||||
establishment = random.choice(self.establishments)
|
||||
speciality_data["name"] = speciality_data['name']
|
||||
speciality_data["establishment"] = establishment.id
|
||||
|
||||
serializer = SpecialitySerializer(data=speciality_data)
|
||||
if serializer.is_valid():
|
||||
speciality = serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Speciality {speciality.name} created successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for speciality: {serializer.errors}'))
|
||||
|
||||
def init_teachers(self):
|
||||
fake = Faker()
|
||||
|
||||
# Récupérer tous les profils existants avec un rôle ECOLE ou ADMIN
|
||||
profiles_with_roles = Profile.objects.filter(roles__role_type__in=[ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN]).distinct()
|
||||
|
||||
if not profiles_with_roles.exists():
|
||||
self.stdout.write(self.style.ERROR('No profiles with role_type ECOLE or ADMIN found'))
|
||||
return
|
||||
|
||||
used_profiles = set()
|
||||
|
||||
for _ in range(15):
|
||||
# Récupérer un profil aléatoire qui n'a pas encore été utilisé
|
||||
available_profiles = profiles_with_roles.exclude(id__in=used_profiles)
|
||||
if not available_profiles.exists():
|
||||
self.stdout.write(self.style.ERROR('Not enough profiles with role_type ECOLE or ADMIN available'))
|
||||
break
|
||||
|
||||
profile = random.choice(available_profiles)
|
||||
used_profiles.add(profile.id)
|
||||
|
||||
# Récupérer les ProfileRole associés au profil avec les rôles ECOLE ou ADMIN
|
||||
profile_roles = ProfileRole.objects.filter(profile=profile, role_type__in=[ProfileRole.RoleType.PROFIL_ECOLE, ProfileRole.RoleType.PROFIL_ADMIN])
|
||||
|
||||
if not profile_roles.exists():
|
||||
self.stdout.write(self.style.ERROR(f'No ProfileRole with role_type ECOLE or ADMIN found for profile {profile.email}'))
|
||||
continue
|
||||
|
||||
profile_role = random.choice(profile_roles)
|
||||
|
||||
# Générer des données fictives pour l'enseignant
|
||||
teacher_data = {
|
||||
"last_name": fake.last_name(),
|
||||
"first_name": fake.first_name(),
|
||||
"profile_role": profile_role.id
|
||||
}
|
||||
|
||||
establishment_specialities = list(Speciality.objects.filter(establishment=profile_role.establishment))
|
||||
num_specialities = min(random.randint(1, 3), len(establishment_specialities))
|
||||
selected_specialities = random.sample(establishment_specialities, num_specialities)
|
||||
|
||||
# Créer l'enseignant si il n'existe pas
|
||||
teacher_serializer = TeacherSerializer(data=teacher_data)
|
||||
if teacher_serializer.is_valid():
|
||||
teacher = teacher_serializer.save()
|
||||
# Associer les spécialités
|
||||
teacher.specialities.set(selected_specialities)
|
||||
teacher.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Teacher {teacher.last_name} created successfully for establishment {profile_role.establishment.name}'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for teacher: {teacher_serializer.errors}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Teachers initialized or updated successfully'))
|
||||
|
||||
def init_school_classes(self):
|
||||
school_classes_data = self.load_data('school_classes.json')
|
||||
levels = list(Level.objects.all())
|
||||
|
||||
for index, class_data in enumerate(school_classes_data, start=1):
|
||||
# Randomize establishment
|
||||
establishment = random.choice(self.establishments)
|
||||
class_data["atmosphere_name"] = f"Classe {index}"
|
||||
class_data["establishment"] = establishment.id
|
||||
|
||||
# Randomize levels
|
||||
class_data["levels"] = [level.id for level in random.sample(levels, random.randint(1, min(5, len(levels))))]
|
||||
|
||||
# Randomize teachers
|
||||
establishment_teachers = list(Teacher.objects.filter(profile_role__establishment=establishment))
|
||||
if len(establishment_teachers) > 0:
|
||||
num_teachers = min(2, len(establishment_teachers))
|
||||
selected_teachers = random.sample(establishment_teachers, num_teachers)
|
||||
teachers_ids = [teacher.id for teacher in selected_teachers]
|
||||
else:
|
||||
teachers_ids = []
|
||||
|
||||
# Use the serializer to create or update the school class
|
||||
class_data["teachers"] = teachers_ids
|
||||
serializer = SchoolClassSerializer(data=class_data)
|
||||
if serializer.is_valid():
|
||||
school_class = serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'SchoolClass {school_class.atmosphere_name} created or updated successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for school class: {serializer.errors}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('SchoolClasses initialized or updated successfully'))
|
||||
|
||||
def init_file_group(self):
|
||||
fake = Faker()
|
||||
|
||||
for establishment in self.establishments:
|
||||
for i in range(1, 4): # Créer 3 groupes de fichiers par établissement
|
||||
name = f"Fichiers d'inscription - {fake.word()}"
|
||||
description = fake.sentence()
|
||||
group_data = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"establishment": establishment
|
||||
}
|
||||
RegistrationFileGroup.objects.update_or_create(name=name, defaults=group_data)
|
||||
self.stdout.write(self.style.SUCCESS(f'RegistrationFileGroup {name} initialized or updated successfully'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('All RegistrationFileGroups initialized or updated successfully'))
|
||||
|
||||
def init_register_form(self):
|
||||
fake = Faker('fr_FR') # Utiliser le locale français pour Faker
|
||||
|
||||
file_group_count = RegistrationFileGroup.objects.count()
|
||||
levels = list(Level.objects.all())
|
||||
|
||||
# Récupérer tous les profils existants avec un ProfileRole Parent
|
||||
profiles_with_parent_role = Profile.objects.filter(roles__role_type=ProfileRole.RoleType.PROFIL_PARENT).distinct()
|
||||
|
||||
if not profiles_with_parent_role.exists():
|
||||
self.stdout.write(self.style.ERROR('No profiles with ProfileRole Parent found'))
|
||||
return
|
||||
|
||||
used_profiles = set()
|
||||
|
||||
for _ in range(50):
|
||||
# Récupérer un profil aléatoire qui n'a pas encore été utilisé
|
||||
available_profiles = profiles_with_parent_role.exclude(id__in=used_profiles)
|
||||
if not available_profiles.exists():
|
||||
self.stdout.write(self.style.ERROR('Not enough profiles with ProfileRole Parent available'))
|
||||
break
|
||||
|
||||
profile = random.choice(available_profiles)
|
||||
used_profiles.add(profile.id)
|
||||
|
||||
# Récupérer le ProfileRole Parent associé au profil
|
||||
profile_roles = ProfileRole.objects.filter(profile=profile, role_type=ProfileRole.RoleType.PROFIL_PARENT)
|
||||
profile_role = random.choice(profile_roles)
|
||||
|
||||
# Générer des données fictives pour le guardian
|
||||
guardian_data = {
|
||||
"profile_role": profile_role.id,
|
||||
"last_name": fake.last_name(),
|
||||
"first_name": fake.first_name(),
|
||||
"birth_date": fake.date_of_birth().strftime('%Y-%m-%d'),
|
||||
"address": fake.address(),
|
||||
"phone": "+33122334455",
|
||||
"profession": fake.job()
|
||||
}
|
||||
|
||||
# Générer des données fictives pour l'étudiant
|
||||
student_data = {
|
||||
"last_name": fake.last_name(),
|
||||
"first_name": fake.first_name(),
|
||||
"address": fake.address(),
|
||||
"birth_date": fake.date_of_birth(),
|
||||
"birth_place": fake.city(),
|
||||
"birth_postal_code": fake.postcode(),
|
||||
"nationality": fake.country(),
|
||||
"attending_physician": fake.name(),
|
||||
"level": random.choice(levels).id,
|
||||
"guardians": [guardian_data],
|
||||
"sibling": []
|
||||
}
|
||||
|
||||
# Créer ou mettre à jour l'étudiant
|
||||
student_serializer = StudentSerializer(data=student_data)
|
||||
if student_serializer.is_valid():
|
||||
student = student_serializer.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Student {student.last_name} created successfully'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'Error in data for student: {student_serializer.errors}'))
|
||||
continue
|
||||
|
||||
# Récupérer les frais et les réductions
|
||||
fees = Fee.objects.filter(id__in=[1, 2, 3, 4])
|
||||
discounts = Discount.objects.filter(id__in=[1])
|
||||
|
||||
# Déterminer l'année scolaire (soit l'année en cours, soit l'année prochaine)
|
||||
school_year = random.choice([getCurrentSchoolYear(), getNextSchoolYear()])
|
||||
|
||||
# Créer les données du formulaire d'inscription
|
||||
register_form_data = {
|
||||
"fileGroup": RegistrationFileGroup.objects.get(id=fake.random_int(min=1, max=file_group_count)),
|
||||
"establishment": profile_role.establishment,
|
||||
"status": fake.random_int(min=1, max=3),
|
||||
"school_year": school_year # Ajouter l'année scolaire
|
||||
}
|
||||
|
||||
# Créer ou mettre à jour le formulaire d'inscription
|
||||
register_form, created = RegistrationForm.objects.get_or_create(
|
||||
student=student,
|
||||
establishment=profile_role.establishment,
|
||||
defaults=register_form_data
|
||||
)
|
||||
|
||||
if created:
|
||||
register_form.fees.set(fees)
|
||||
register_form.discounts.set(discounts)
|
||||
self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} created successfully with school year {school_year}'))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'RegistrationForm for student {student.last_name} already exists'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('50 RegistrationForms initialized or updated successfully'))
|
||||
42
Back-End/School/management/mock_datas/discounts.json
Normal file
42
Back-End/School/management/mock_datas/discounts.json
Normal file
@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"name": "Parrainage",
|
||||
"amount": "10.00",
|
||||
"description": "Réduction pour parrainage"
|
||||
},
|
||||
{
|
||||
"name": "Réinscription",
|
||||
"amount": "100.00",
|
||||
"description": "Réduction pour Réinscription"
|
||||
},
|
||||
{
|
||||
"name": "Famille nombreuse",
|
||||
"amount": "50.00",
|
||||
"description": "Réduction pour les familles nombreuses"
|
||||
},
|
||||
{
|
||||
"name": "Excellence académique",
|
||||
"amount": "200.00",
|
||||
"description": "Réduction pour les élèves ayant des résultats académiques exceptionnels"
|
||||
},
|
||||
{
|
||||
"name": "Sportif de haut niveau",
|
||||
"amount": "150.00",
|
||||
"description": "Réduction pour les élèves pratiquant un sport de haut niveau"
|
||||
},
|
||||
{
|
||||
"name": "Artiste talentueux",
|
||||
"amount": "100.00",
|
||||
"description": "Réduction pour les élèves ayant des talents artistiques"
|
||||
},
|
||||
{
|
||||
"name": "Bourse d'études",
|
||||
"amount": "300.00",
|
||||
"description": "Réduction pour les élèves bénéficiant d'une bourse d'études"
|
||||
},
|
||||
{
|
||||
"name": "Réduction spéciale",
|
||||
"amount": "75.00",
|
||||
"description": "Réduction spéciale pour des occasions particulières"
|
||||
}
|
||||
]
|
||||
23
Back-End/School/management/mock_datas/establishments.json
Normal file
23
Back-End/School/management/mock_datas/establishments.json
Normal file
@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"name": "Ecole A",
|
||||
"address": "Adresse de l'Ecole A",
|
||||
"total_capacity": 69,
|
||||
"establishment_type": [1, 2],
|
||||
"licence_code": ""
|
||||
},
|
||||
{
|
||||
"name": "Ecole B",
|
||||
"address": "Adresse de l'Ecole B",
|
||||
"total_capacity": 100,
|
||||
"establishment_type": [2, 3],
|
||||
"licence_code": ""
|
||||
},
|
||||
{
|
||||
"name": "Ecole C",
|
||||
"address": "Adresse de l'Ecole C",
|
||||
"total_capacity": 50,
|
||||
"establishment_type": [1],
|
||||
"licence_code": ""
|
||||
}
|
||||
]
|
||||
32
Back-End/School/management/mock_datas/fees.json
Normal file
32
Back-End/School/management/mock_datas/fees.json
Normal file
@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"name": "Frais d'inscription",
|
||||
"base_amount": "150.00",
|
||||
"description": "Montant de base",
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"name": "Matériel",
|
||||
"base_amount": "85.00",
|
||||
"description": "Livres / jouets",
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"name": "Sorties périscolaires",
|
||||
"base_amount": "120.00",
|
||||
"description": "Sorties",
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"name": "Les colibris",
|
||||
"base_amount": "4500.00",
|
||||
"description": "TPS / PS / MS / GS",
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"name": "Les butterflies",
|
||||
"base_amount": "5000.00",
|
||||
"description": "CP / CE1 / CE2 / CM1 / CM2",
|
||||
"is_active": true
|
||||
}
|
||||
]
|
||||
52
Back-End/School/management/mock_datas/school_classes.json
Normal file
52
Back-End/School/management/mock_datas/school_classes.json
Normal file
@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"age_range": "3-6",
|
||||
"number_of_students": 14,
|
||||
"teaching_language": "",
|
||||
"school_year": "2024-2025",
|
||||
"levels": [2, 3, 4],
|
||||
"type": 1,
|
||||
"time_range": ["08:30", "17:30"],
|
||||
"opening_days": [1, 2, 4, 5]
|
||||
},
|
||||
{
|
||||
"age_range": "2-3",
|
||||
"number_of_students": 5,
|
||||
"teaching_language": "",
|
||||
"school_year": "2024-2025",
|
||||
"levels": [1],
|
||||
"type": 1,
|
||||
"time_range": ["08:30", "17:30"],
|
||||
"opening_days": [1, 2, 4, 5]
|
||||
},
|
||||
{
|
||||
"age_range": "6-12",
|
||||
"number_of_students": 21,
|
||||
"teaching_language": "",
|
||||
"school_year": "2024-2025",
|
||||
"levels": [5, 6, 7, 8, 9],
|
||||
"type": 1,
|
||||
"time_range": ["08:30", "17:30"],
|
||||
"opening_days": [1, 2, 4, 5]
|
||||
},
|
||||
{
|
||||
"age_range": "4-6",
|
||||
"number_of_students": 18,
|
||||
"teaching_language": "",
|
||||
"school_year": "2024-2025",
|
||||
"levels": [4, 5],
|
||||
"type": 1,
|
||||
"time_range": ["08:30", "17:30"],
|
||||
"opening_days": [1, 2, 4, 5]
|
||||
},
|
||||
{
|
||||
"age_range": "7-9",
|
||||
"number_of_students": 20,
|
||||
"teaching_language": "",
|
||||
"school_year": "2024-2025",
|
||||
"levels": [6, 7],
|
||||
"type": 1,
|
||||
"time_range": ["08:30", "17:30"],
|
||||
"opening_days": [1, 2, 4, 5]
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user