mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
Compare commits
313 Commits
0.0.1
...
e9a30b7bde
| Author | SHA1 | Date | |
|---|---|---|---|
| e9a30b7bde | |||
| ff1d113698 | |||
| 12a6ad1d61 | |||
| 856443d4ed | |||
| ace4dcbf07 | |||
| 61f63f9dc9 | |||
| d9e998d2ff | |||
| abb4b525b2 | |||
| b4f70e6bad | |||
| 8549699dec | |||
| a034149eae | |||
| 12f5fc7aa9 | |||
| 2dc0dfa268 | |||
| dd00cba385 | |||
| 7486f6c5ce | |||
| 1e5bc6ccba | |||
| 0fb668b212 | |||
| 5e62ee5100 | |||
| e89d2fc4c3 | |||
| 9481a0132d | |||
| 482e8c1357 | |||
| 0e0141d155 | |||
| 7f002e2e6a | |||
| 0064b8d35a | |||
| ec2c1daebc | |||
| 67cea2f1c6 | |||
| 5785bfae46 | |||
| a17078709b | |||
| d58155da06 | |||
| 043d93dcc4 | |||
| 6bc24055cd | |||
| 2f6d30b85b | |||
| c161fa7e75 | |||
| 789816e986 | |||
| 6bedf715cc | |||
| 59a0d40130 | |||
| 25e2799c0f | |||
| 017c0290dd | |||
| fe2d4d4513 | |||
| f93c428259 | |||
| e61cd51ce2 | |||
| 6a0b90e98f | |||
| 8a71fa1830 | |||
| f265540da2 | |||
| 5be5f9f70d | |||
| 68a6a63c4f | |||
| af30ae33b5 | |||
| e509625811 | |||
| 3a2455f918 | |||
| e74f9c98a2 | |||
| 8f0cf16f70 | |||
| 78d96f82f9 | |||
| c117f96e52 | |||
| e4668ef1e5 | |||
| ec2630a6e4 | |||
| d65b171da8 | |||
| 4a6b7ce379 | |||
| 170f7c4fa8 | |||
| ce83e02f7b | |||
| a69498dd06 | |||
| 23ab7d04ef | |||
| 8cf22905e5 | |||
| 314c31fab1 | |||
| be27fe1232 | |||
| 8b54cedcab | |||
| d37145b73e | |||
| e2df29d851 | |||
| eb48523f7d | |||
| e30753f1d6 | |||
| a42cf348a0 | |||
| 55cb20bf8c | |||
| 677cec1ec2 | |||
| 82573f1b23 | |||
| c5248d9fd3 | |||
| 49907d7ec8 | |||
| 87701cb154 | |||
| d877c956b7 | |||
| 98763dc90a | |||
| fd6348fd6b | |||
| 3e5cebef44 | |||
| 87b8cf6c05 | |||
| 3b667d3b15 | |||
| eca8d7a8d5 | |||
| de5f7cd41e | |||
| 7de839ee5c | |||
| 0fe6c76189 | |||
| eb7805e54e | |||
| 56c223f3cc | |||
| 4f40d1f29d | |||
| 95b449ddfd | |||
| 05136035ab | |||
| c9c7e7715e | |||
| 5760c89105 | |||
| e65e31014d | |||
| 7fe53465ac | |||
| 00f7bfde4a | |||
| d7fca9e942 | |||
| 4e5aab6db7 | |||
| 2888f8dcce | |||
| 3990d75e52 | |||
| f252efdef4 | |||
| c6bc0d0b51 | |||
| fc9a1ed252 | |||
| 52bba46cbb | |||
| 425e6d73e5 | |||
| 23a593dbc7 | |||
| c6d75281a1 | |||
| 43874f8b9e | |||
| 500b6e9af7 | |||
| 67193a8b36 | |||
| 3c0806e26c | |||
| 175932ffa3 | |||
| 5866427544 | |||
| 6d805940fe | |||
| 69405c577e | |||
| eda6f587fb | |||
| 99a882a64a | |||
| f38a4414c2 | |||
| 980f169c1d | |||
| 9e69790683 | |||
| f887ae1886 | |||
| d64500f402 | |||
| 760ee0009e | |||
| 4fd40ac5fc | |||
| dfd707d7a0 | |||
| e2a39ff74d | |||
| 8fc9478786 | |||
| 4fc061fc25 | |||
| 5440f5cbdb | |||
| 5927e48e65 | |||
| 76f9a7dd14 | |||
| 2a6b3bdf63 | |||
| e9650c992e | |||
| dc402df58b | |||
| e1c607308c | |||
| 0b5ebb9b32 | |||
| 4ecf25a6ab | |||
| 030d19d411 | |||
| 1bccc85951 | |||
| 58144ba0d0 | |||
| cb4fe74a9e | |||
| 1c75927bba | |||
| 0f49236965 | |||
| 256f995698 | |||
| e538ac3d56 | |||
| 889a3a48c5 | |||
| f3c4284778 | |||
| 330018edfd | |||
| ac0672f334 | |||
| 2ab1684791 | |||
| 9374b001c9 | |||
| 053d524a51 | |||
| 4a382d523c | |||
| 1ced4a1069 | |||
| 05542dfc40 | |||
| af60675dc5 | |||
| 266f509d65 | |||
| 857b8b26c3 | |||
| d37aed5f64 | |||
| 5851341235 | |||
| d37e6c384d | |||
| 24069b894e | |||
| 43e301ed64 | |||
| 31fdc612b1 | |||
| 5ea3cbb079 | |||
| db8e1d8ab3 | |||
| 92484804f6 | |||
| be013f0786 | |||
| b23264c0d4 | |||
| ada2a44c3e | |||
| 7f442b9cae | |||
| 545349c7db | |||
| 3c62cc9ad2 | |||
| 001a5bc83c | |||
| 905b95f3a3 | |||
| daad12cf40 | |||
| 1617b132c4 | |||
| 10f66c69dd | |||
| 9f1f97e0c5 | |||
| 8417d3eb14 | |||
| 59aee80c2e | |||
| 2e0fe86c71 | |||
| 7564865d8f | |||
| f7666c894b | |||
| a65bd47905 | |||
| dd0884bbce | |||
| 89b01b79db | |||
| 4c2e2f8756 | |||
| 4f774c18e4 | |||
| a157d53932 | |||
| 839a26257b | |||
| 5a7661db93 | |||
| 9559db59eb | |||
| d6edf250bb | |||
| e0bfd3e115 | |||
| c9350a796b | |||
| 43ed495a9a | |||
| 3bcc620ee1 | |||
| e38604efd6 | |||
| fb73f9e9a8 | |||
| 173ac47fb2 | |||
| a3182c0ba7 | |||
| 91976157e4 | |||
| 6bd5704983 | |||
| cd9c10a88a | |||
| ccecd78704 | |||
| 7d1b9c5657 | |||
| 753a8d647e | |||
| 023b46e16e | |||
| 16178296ec | |||
| 95c154a4a2 | |||
| f2ad1de5a4 | |||
| c03fa0ba42 | |||
| c9b0f0d77a | |||
| e3879f516b | |||
| 2ac4832985 | |||
| eb81bbba92 | |||
| b52b265835 | |||
| cac1519bf3 | |||
| f3490a4e95 | |||
| d1a0067f7b | |||
| b97cf6e02b | |||
| 9b13d52e8d | |||
| 085c086ec7 | |||
| 081dc06001 | |||
| 4b8f85e68d | |||
| d62be6b309 | |||
| dce2114a79 | |||
| c8c8941ec8 | |||
| 8897d523dc | |||
| 445cf35382 | |||
| 1911f79f45 | |||
| c7723eceee | |||
| 508847940c | |||
| c861239d48 | |||
| eb89a324ab | |||
| 214fb186fa | |||
| de86e923cf | |||
| c2bba1abbf | |||
| 0ef6a2b119 | |||
| 8ea68bbad0 | |||
| 65d5b8c424 | |||
| fc337b1e0b | |||
| 44c28d0aa0 | |||
| ef1b036dcc | |||
| 05f1f16727 | |||
| a041ffaee7 | |||
| aae5d27d8c | |||
| cccb5efa2c | |||
| f9e870e11f | |||
| 2d128aaf30 | |||
| 32a77c780a | |||
| aef6c193b1 | |||
| d9655772b4 | |||
| a571d80482 | |||
| d1aa8b54fb | |||
| 9716373fa2 | |||
| ee7fef01ce | |||
| 4c95b6a83f | |||
| 11fc446b90 | |||
| cce78355a3 | |||
| 9bf9c5f62d | |||
| 8b3f9637a9 | |||
| efcc5e6672 | |||
| 2576d21734 | |||
| d3f1ae3d11 | |||
| 0c5e3aa098 | |||
| 23203c0397 | |||
| ffc6ce8de8 | |||
| fb7fbaf839 | |||
| 274db249aa | |||
| c269b89d3d | |||
| f2628bb45a | |||
| 7f35527649 | |||
| 42b4c99be8 | |||
| 1a8ef26f58 | |||
| 72dd7699d6 | |||
| a248898203 | |||
| 6f1631a75b | |||
| cb3f909fa4 | |||
| 665625e028 | |||
| 1c4d96d4c3 | |||
| 0c2e0b92f4 | |||
| ece23deb19 | |||
| b8ef34a04b | |||
| 799e1c6717 | |||
| 5462306a60 | |||
| 8d1a41e269 | |||
| 5a0e65bb75 | |||
| 147a70135d | |||
| c4d45426b5 | |||
| 24352efad3 | |||
| 41aa9d55d3 | |||
| 427b6c7588 | |||
| 58fe509734 | |||
| 83f4d67a6f | |||
| 2b414b8391 | |||
| 830d9a48c0 | |||
| 3c27133cdb | |||
| fb5d485ce1 | |||
| 7acae479da | |||
| cf144310a1 | |||
| 85d4c007cb | |||
| e1202c6e6d | |||
| d51778ba54 | |||
| 5946cbdee6 | |||
| a77dd8ec64 | |||
| afc1632797 | |||
| 692e8454bf | |||
| 56e27628f8 | |||
| edc97242f2 | |||
| 81d1dfa9a7 | |||
| b8511f94b6 |
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é.
|
||||||
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,9 +1,5 @@
|
|||||||
Back-End/*/Configuration/application.json
|
|
||||||
.venv/
|
.venv/
|
||||||
__pycache__/
|
.env
|
||||||
node_modules/
|
node_modules/
|
||||||
Back-End/*/migrations/*
|
|
||||||
Back-End/documents
|
|
||||||
Back-End/*.dmp
|
|
||||||
Back-End/staticfiles
|
|
||||||
hardcoded-strings-report.md
|
hardcoded-strings-report.md
|
||||||
|
backend.env
|
||||||
@ -0,0 +1 @@
|
|||||||
|
cd $(dirname "$0")/../Front-End/ && npm run lint-light
|
||||||
@ -1,4 +1 @@
|
|||||||
#!/bin/sh
|
#node scripts/prepare-commit-msg.js "$1" "$2"
|
||||||
. "$(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
|
||||||
0
Back-End/Auth/__init__.py
Normal file
0
Back-End/Auth/__init__.py
Normal file
@ -3,5 +3,5 @@ from django.db.models.signals import post_migrate
|
|||||||
|
|
||||||
class GestionloginConfig(AppConfig):
|
class GestionloginConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'GestionLogin'
|
name = 'Auth'
|
||||||
|
|
||||||
@ -1,20 +1,20 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from GestionLogin.models import Profil
|
from Auth.models import Profile
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
|
||||||
class EmailBackend(ModelBackend):
|
class EmailBackend(ModelBackend):
|
||||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
|
||||||
if username is None:
|
if username is None:
|
||||||
username = kwargs.get(Profil.USERNAME_FIELD)
|
username = kwargs.get(Profile.USERNAME_FIELD)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = Profil.objects.get(email=username)
|
user = Profile.objects.get(email=username)
|
||||||
|
|
||||||
# Vérifie le mot de passe de l'utilisateur
|
# Vérifie le mot de passe de l'utilisateur
|
||||||
if user.check_password(password):
|
if user.check_password(password):
|
||||||
return user
|
return user
|
||||||
except Profil.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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-11-30 11:02
|
||||||
|
|
||||||
|
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
40
Back-End/Auth/models.py
Normal file
40
Back-End/Auth/models.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.validators import EmailValidator
|
||||||
|
|
||||||
|
class Profile(AbstractUser):
|
||||||
|
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)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
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})"
|
||||||
@ -2,10 +2,10 @@ from rest_framework.pagination import PageNumberPagination
|
|||||||
|
|
||||||
from N3wtSchool import settings
|
from N3wtSchool import settings
|
||||||
|
|
||||||
class CustomPagination(PageNumberPagination):
|
class CustomProfilesPagination(PageNumberPagination):
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
max_page_size = settings.NB_MAX_PAGE
|
max_page_size = settings.NB_MAX_PAGE
|
||||||
page_size = settings.NB_RESULT_PER_PAGE
|
page_size = settings.NB_RESULT_PROFILES_PER_PAGE
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
return ({
|
return ({
|
||||||
@ -16,5 +16,5 @@ class CustomPagination(PageNumberPagination):
|
|||||||
'count': self.page.paginator.count,
|
'count': self.page.paginator.count,
|
||||||
'page_size': self.page_size,
|
'page_size': self.page_size,
|
||||||
'max_page_size' : self.max_page_size,
|
'max_page_size' : self.max_page_size,
|
||||||
'fichesInscriptions': data }
|
'profilesRoles': data }
|
||||||
)
|
)
|
||||||
160
Back-End/Auth/serializers.py
Normal file
160
Back-End/Auth/serializers.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
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', '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'],
|
||||||
|
code=validated_data.get('code', ''),
|
||||||
|
datePeremption=validated_data.get('datePeremption', '')
|
||||||
|
)
|
||||||
|
user.set_password(validated_data['password'])
|
||||||
|
user.full_clean()
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
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.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")
|
||||||
22
Back-End/Auth/urls.py
Normal file
22
Back-End/Auth/urls.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from django.urls import path, re_path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
import Auth.views
|
||||||
|
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/(?P<code>[a-zA-Z]+)$', ResetPasswordView.as_view(), name='resetPassword'),
|
||||||
|
re_path(r'^infoSession$', SessionView.as_view(), name='infoSession'),
|
||||||
|
|
||||||
|
re_path(r'^profiles$', ProfileView.as_view(), name="profile"),
|
||||||
|
re_path(r'^profiles/(?P<id>[0-9]+)$', ProfileSimpleView.as_view(), name="profile"),
|
||||||
|
|
||||||
|
re_path(r'^profileRoles$', ProfileRoleView.as_view(), name="profileRoles"),
|
||||||
|
re_path(r'^profileRoles/(?P<id>[0-9]+)$', ProfileRoleSimpleView.as_view(), name="profileRoles"),
|
||||||
|
]
|
||||||
668
Back-End/Auth/views.py
Normal file
668
Back-End/Auth/views.py
Normal file
@ -0,0 +1,668 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import login, authenticate, get_user_model
|
||||||
|
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.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 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, ProfileRole
|
||||||
|
from rest_framework.decorators import action, api_view
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
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 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):
|
||||||
|
@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'])
|
||||||
|
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,
|
||||||
|
'roleIndexLoginDefault': user.roleIndexLoginDefault,
|
||||||
|
'roles': list(roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JsonResponse(response_data, status=status.HTTP_200_OK)
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return JsonResponse({"error": "Token has expired"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
profil_serializer = ProfileSerializer(data=profil_data)
|
||||||
|
|
||||||
|
if profil_serializer.is_valid():
|
||||||
|
profil = profil_serializer.save()
|
||||||
|
return JsonResponse(profil_serializer.data, 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):
|
||||||
|
@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)
|
||||||
|
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:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
else:
|
||||||
|
retour = error.returnMessage[error.WRONG_ID]
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'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__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):
|
||||||
|
@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 = ''
|
||||||
|
retour = ''
|
||||||
|
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 is None:
|
||||||
|
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
|
||||||
|
else:
|
||||||
|
# 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.full_clean()
|
||||||
|
profil.save()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
@method_decorator(csrf_protect, name='dispatch')
|
||||||
|
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||||
|
class NewPasswordView(APIView):
|
||||||
|
@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 = ''
|
||||||
|
retour = ''
|
||||||
|
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 is None:
|
||||||
|
retourErreur = error.returnMessage[error.PROFIL_NOT_EXISTS]
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
|
||||||
|
@method_decorator(csrf_protect, name='dispatch')
|
||||||
|
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||||
|
class ResetPasswordView(APIView):
|
||||||
|
@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)
|
||||||
|
|
||||||
|
validatorResetPassword = validator.ValidatorResetPassword(data=newProfilConnection)
|
||||||
|
validationOk, errorFields = validatorResetPassword.validate()
|
||||||
|
|
||||||
|
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]
|
||||||
|
elif validationOk:
|
||||||
|
retour = error.returnMessage[error.PASSWORD_CHANGED]
|
||||||
|
|
||||||
|
profil.set_password(newProfilConnection.get('password1'))
|
||||||
|
profil.code = ''
|
||||||
|
profil.datePeremption = ''
|
||||||
|
profil.save()
|
||||||
|
retourErreur = ''
|
||||||
|
|
||||||
|
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
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-11-30 11:02
|
||||||
|
|
||||||
|
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"]]}
|
||||||
|
)
|
||||||
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
|
# The first instruction is what image we want to base our container on
|
||||||
# We Use an official Python runtime as a parent image
|
# We Use an official Python runtime as a parent image
|
||||||
FROM python:3.12.7
|
FROM python:3.12.7
|
||||||
|
WORKDIR /Back-End
|
||||||
|
|
||||||
# Allows docker to cache installed dependencies between builds
|
# Allows docker to cache installed dependencies between builds
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
@ -10,11 +11,5 @@ RUN pip install -r requirements.txt
|
|||||||
|
|
||||||
# Mounts the application code to the image
|
# Mounts the application code to the image
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR /Back-End
|
|
||||||
|
|
||||||
EXPOSE 8080
|
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/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.
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class EstablishmentConfig(AppConfig):
|
||||||
class GestionenseignantsConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'GestionEnseignants'
|
name = 'Establishment'
|
||||||
|
|
||||||
|
|
||||||
31
Back-End/Establishment/migrations/0001_initial.py
Normal file
31
Back-End/Establishment/migrations/0001_initial.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
||||||
|
|
||||||
|
import Establishment.models
|
||||||
|
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)),
|
||||||
|
('logo', 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
37
Back-End/Establishment/models.py
Normal file
37
Back-End/Establishment/models.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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)
|
||||||
|
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)
|
||||||
@ -1 +0,0 @@
|
|||||||
default_app_config = 'GestionEnseignants.apps.GestionenseignantsConfig'
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
class Specialite(models.Model):
|
|
||||||
nom = models.CharField(max_length=100)
|
|
||||||
dateCreation = models.DateTimeField(auto_now=True)
|
|
||||||
codeCouleur = models.CharField(max_length=7, default='#FFFFFF')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.nom
|
|
||||||
|
|
||||||
class Enseignant(models.Model):
|
|
||||||
nom = models.CharField(max_length=100)
|
|
||||||
prenom = models.CharField(max_length=100)
|
|
||||||
mail = models.EmailField(unique=True)
|
|
||||||
specialite = models.ForeignKey(Specialite, on_delete=models.SET_NULL, null=True, blank=True, related_name='enseignants')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.nom} {self.prenom}"
|
|
||||||
|
|
||||||
class Classe(models.Model):
|
|
||||||
nom_ambiance = models.CharField(max_length=255)
|
|
||||||
tranche_age = models.JSONField()
|
|
||||||
nombre_eleves = models.PositiveIntegerField()
|
|
||||||
langue_enseignement = models.CharField(max_length=255)
|
|
||||||
annee_scolaire = models.CharField(max_length=9)
|
|
||||||
dateCreation = models.DateTimeField(auto_now_add=True)
|
|
||||||
specialites = models.ManyToManyField(Specialite, related_name='classes')
|
|
||||||
enseignant_principal = models.ForeignKey(Enseignant, on_delete=models.SET_NULL, null=True, blank=True, related_name='classes_principal')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.nom_ambiance
|
|
||||||
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
from .models import Enseignant, Specialite, Classe
|
|
||||||
from N3wtSchool import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
class SpecialiteSerializer(serializers.ModelSerializer):
|
|
||||||
dateCreation_formattee = serializers.SerializerMethodField()
|
|
||||||
class Meta:
|
|
||||||
model = Specialite
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
def get_dateCreation_formattee(self, obj):
|
|
||||||
utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale
|
|
||||||
local_tz = pytz.timezone(settings.TZ_APPLI)
|
|
||||||
local_time = utc_time.astimezone(local_tz)
|
|
||||||
|
|
||||||
return local_time.strftime("%d-%m-%Y %H:%M")
|
|
||||||
|
|
||||||
class ClasseSerializer(serializers.ModelSerializer):
|
|
||||||
specialites = SpecialiteSerializer(many=True, read_only=True)
|
|
||||||
specialites_ids = serializers.PrimaryKeyRelatedField(queryset=Specialite.objects.all(), many=True, source='specialites')
|
|
||||||
dateCreation_formattee = serializers.SerializerMethodField()
|
|
||||||
enseignant_principal = serializers.SerializerMethodField()
|
|
||||||
enseignant_principal_id = serializers.PrimaryKeyRelatedField(queryset=Enseignant.objects.all(), source='enseignant_principal', write_only=False, read_only=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Classe
|
|
||||||
fields = ['id', 'nom_ambiance', 'tranche_age', 'nombre_eleves', 'langue_enseignement', 'specialites', 'specialites_ids', 'enseignant_principal', 'enseignant_principal_id', 'annee_scolaire', 'dateCreation', 'dateCreation_formattee']
|
|
||||||
|
|
||||||
def get_enseignant_principal(self, obj):
|
|
||||||
from .serializers import EnseignantDetailSerializer
|
|
||||||
if obj.enseignant_principal:
|
|
||||||
return EnseignantDetailSerializer(obj.enseignant_principal).data
|
|
||||||
return None
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
specialites_data = validated_data.pop('specialites', [])
|
|
||||||
classe = Classe.objects.create(**validated_data)
|
|
||||||
classe.specialites.set(specialites_data)
|
|
||||||
return classe
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
specialites_data = validated_data.pop('specialites', [])
|
|
||||||
instance.nom_ambiance = validated_data.get('nom_ambiance', instance.nom_ambiance)
|
|
||||||
instance.tranche_age = validated_data.get('tranche_age', instance.tranche_age)
|
|
||||||
instance.nombre_eleves = validated_data.get('nombre_eleves', instance.nombre_eleves)
|
|
||||||
instance.langue_enseignement = validated_data.get('langue_enseignement', instance.langue_enseignement)
|
|
||||||
instance.annee_scolaire = validated_data.get('annee_scolaire', instance.annee_scolaire)
|
|
||||||
instance.enseignant_principal = validated_data.get('enseignant_principal', instance.enseignant_principal)
|
|
||||||
instance.save()
|
|
||||||
instance.specialites.set(specialites_data)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def get_dateCreation_formattee(self, obj):
|
|
||||||
utc_time = timezone.localtime(obj.dateCreation) # Convertir en heure locale
|
|
||||||
local_tz = pytz.timezone(settings.TZ_APPLI)
|
|
||||||
local_time = utc_time.astimezone(local_tz)
|
|
||||||
|
|
||||||
return local_time.strftime("%d-%m-%Y %H:%M")
|
|
||||||
|
|
||||||
class EnseignantSerializer(serializers.ModelSerializer):
|
|
||||||
specialite = SpecialiteSerializer(read_only=True)
|
|
||||||
specialite_id = serializers.PrimaryKeyRelatedField(queryset=Specialite.objects.all(), source='specialite', write_only=False, read_only=False)
|
|
||||||
classes_principal = ClasseSerializer(many=True, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Enseignant
|
|
||||||
fields = ['id', 'nom', 'prenom', 'mail', 'specialite', 'specialite_id', 'classes_principal']
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
specialite = validated_data.pop('specialite', None)
|
|
||||||
enseignant = Enseignant.objects.create(**validated_data)
|
|
||||||
enseignant.specialite = specialite
|
|
||||||
enseignant.save()
|
|
||||||
return enseignant
|
|
||||||
|
|
||||||
class EnseignantDetailSerializer(serializers.ModelSerializer):
|
|
||||||
specialite = SpecialiteSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Enseignant
|
|
||||||
fields = ['id', 'nom', 'prenom', 'mail', 'specialite']
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
from django.urls import path, re_path
|
|
||||||
|
|
||||||
from GestionEnseignants.views import EnseignantsView, EnseignantView, SpecialitesView, SpecialiteView, ClassesView, ClasseView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
re_path(r'^enseignants$', EnseignantsView.as_view(), name="enseignants"),
|
|
||||||
re_path(r'^enseignant$', EnseignantView.as_view(), name="enseignant"),
|
|
||||||
re_path(r'^enseignant/([0-9]+)$', EnseignantView.as_view(), name="enseignant"),
|
|
||||||
|
|
||||||
re_path(r'^specialites$', SpecialitesView.as_view(), name="specialites"),
|
|
||||||
re_path(r'^specialite$', SpecialiteView.as_view(), name="specialite"),
|
|
||||||
re_path(r'^specialite/([0-9]+)$', SpecialiteView.as_view(), name="specialite"),
|
|
||||||
|
|
||||||
re_path(r'^classes$', ClassesView.as_view(), name="classes"),
|
|
||||||
re_path(r'^classe$', ClasseView.as_view(), name="classe"),
|
|
||||||
re_path(r'^classe/([0-9]+)$', ClasseView.as_view(), name="classe"),
|
|
||||||
]
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
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 django.core.cache import cache
|
|
||||||
from .models import Enseignant, Specialite, Classe
|
|
||||||
from .serializers import EnseignantSerializer, SpecialiteSerializer, ClasseSerializer
|
|
||||||
from N3wtSchool import bdd
|
|
||||||
|
|
||||||
class EnseignantsView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
enseignantsList=bdd.getAllObjects(Enseignant)
|
|
||||||
enseignants_serializer=EnseignantSerializer(enseignantsList, many=True)
|
|
||||||
|
|
||||||
return JsonResponse(enseignants_serializer.data, safe=False)
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class EnseignantView(APIView):
|
|
||||||
def get (self, request, _id):
|
|
||||||
enseignant = bdd.getObject(_objectName=Enseignant, _columnName='id', _value=_id)
|
|
||||||
enseignant_serializer=EnseignantSerializer(enseignant)
|
|
||||||
|
|
||||||
return JsonResponse(enseignant_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
enseignant_data=JSONParser().parse(request)
|
|
||||||
enseignant_serializer = EnseignantSerializer(data=enseignant_data)
|
|
||||||
|
|
||||||
if enseignant_serializer.is_valid():
|
|
||||||
enseignant_serializer.save()
|
|
||||||
|
|
||||||
return JsonResponse(enseignant_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(enseignant_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def put(self, request, _id):
|
|
||||||
enseignant_data=JSONParser().parse(request)
|
|
||||||
enseignant = bdd.getObject(_objectName=Enseignant, _columnName='id', _value=_id)
|
|
||||||
enseignant_serializer = EnseignantSerializer(enseignant, data=enseignant_data)
|
|
||||||
if enseignant_serializer.is_valid():
|
|
||||||
enseignant_serializer.save()
|
|
||||||
return JsonResponse(enseignant_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(enseignant_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def delete(self, request, _id):
|
|
||||||
enseignant = bdd.getObject(_objectName=Enseignant, _columnName='id', _value=_id)
|
|
||||||
if enseignant != None:
|
|
||||||
enseignant.delete()
|
|
||||||
|
|
||||||
return JsonResponse("La suppression de la spécialité a été effectuée avec succès", safe=False)
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class SpecialitesView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
specialitesList=bdd.getAllObjects(Specialite)
|
|
||||||
specialites_serializer=SpecialiteSerializer(specialitesList, many=True)
|
|
||||||
|
|
||||||
return JsonResponse(specialites_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
specialites_data=JSONParser().parse(request)
|
|
||||||
all_valid = True
|
|
||||||
for specialite_data in specialites_data:
|
|
||||||
specialite_serializer = SpecialiteSerializer(data=specialite_data)
|
|
||||||
|
|
||||||
if specialite_serializer.is_valid():
|
|
||||||
specialite_serializer.save()
|
|
||||||
else:
|
|
||||||
all_valid = False
|
|
||||||
break
|
|
||||||
if all_valid:
|
|
||||||
specialitesList = bdd.getAllObjects(Specialite)
|
|
||||||
specialites_serializer = SpecialiteSerializer(specialitesList, many=True)
|
|
||||||
|
|
||||||
return JsonResponse(specialite_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class SpecialiteView(APIView):
|
|
||||||
def get (self, request, _id):
|
|
||||||
specialite = bdd.getObject(_objectName=Specialite, _columnName='id', _value=_id)
|
|
||||||
specialite_serializer=SpecialiteSerializer(specialite)
|
|
||||||
|
|
||||||
return JsonResponse(specialite_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
specialite_data=JSONParser().parse(request)
|
|
||||||
specialite_serializer = SpecialiteSerializer(data=specialite_data)
|
|
||||||
|
|
||||||
if specialite_serializer.is_valid():
|
|
||||||
specialite_serializer.save()
|
|
||||||
return JsonResponse(specialite_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(specialite_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def put(self, request, _id):
|
|
||||||
specialite_data=JSONParser().parse(request)
|
|
||||||
specialite = bdd.getObject(_objectName=Specialite, _columnName='id', _value=_id)
|
|
||||||
specialite_serializer = SpecialiteSerializer(specialite, data=specialite_data)
|
|
||||||
if specialite_serializer.is_valid():
|
|
||||||
specialite_serializer.save()
|
|
||||||
return JsonResponse(specialite_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(specialite_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def delete(self, request, _id):
|
|
||||||
specialite = bdd.getObject(_objectName=Specialite, _columnName='id', _value=_id)
|
|
||||||
if specialite != None:
|
|
||||||
specialite.delete()
|
|
||||||
|
|
||||||
return JsonResponse("La suppression de la spécialité a été effectuée avec succès", safe=False)
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class ClassesView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
classesList=bdd.getAllObjects(Classe)
|
|
||||||
classes_serializer=ClasseSerializer(classesList, many=True)
|
|
||||||
return JsonResponse(classes_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
all_valid = True
|
|
||||||
classes_data=JSONParser().parse(request)
|
|
||||||
for classe_data in classes_data:
|
|
||||||
classe_serializer = ClasseSerializer(data=classe_data)
|
|
||||||
|
|
||||||
if classe_serializer.is_valid():
|
|
||||||
classe_serializer.save()
|
|
||||||
else:
|
|
||||||
all_valid = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if all_valid:
|
|
||||||
classesList = bdd.getAllObjects(Classe)
|
|
||||||
classes_serializer = ClasseSerializer(classesList, many=True)
|
|
||||||
|
|
||||||
return JsonResponse(classes_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(classe_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class ClasseView(APIView):
|
|
||||||
def get (self, request, _id):
|
|
||||||
classe = bdd.getObject(_objectName=Classe, _columnName='id', _value=_id)
|
|
||||||
classe_serializer=ClasseSerializer(classe)
|
|
||||||
|
|
||||||
return JsonResponse(classe_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
classe_data=JSONParser().parse(request)
|
|
||||||
classe_serializer = ClasseSerializer(data=classe_data)
|
|
||||||
|
|
||||||
if classe_serializer.is_valid():
|
|
||||||
classe_serializer.save()
|
|
||||||
return JsonResponse(classe_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(classe_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def put(self, request, _id):
|
|
||||||
classe_data=JSONParser().parse(request)
|
|
||||||
classe = bdd.getObject(_objectName=Classe, _columnName='id', _value=_id)
|
|
||||||
classe_serializer = ClasseSerializer(classe, data=classe_data)
|
|
||||||
if classe_serializer.is_valid():
|
|
||||||
classe_serializer.save()
|
|
||||||
return JsonResponse(classe_serializer.data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(classe_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def delete(self, request, _id):
|
|
||||||
classe = bdd.getObject(_objectName=Classe, _columnName='id', _value=_id)
|
|
||||||
if classe != None:
|
|
||||||
classe.delete()
|
|
||||||
|
|
||||||
return JsonResponse("La suppression de la classe a été effectuée avec succès", safe=False)
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"mailFrom":"",
|
|
||||||
"password":""
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"states": [
|
|
||||||
"ABSENT",
|
|
||||||
"CREE",
|
|
||||||
"ENVOYE",
|
|
||||||
"EN_VALIDATION",
|
|
||||||
"A_RELANCER",
|
|
||||||
"VALIDE",
|
|
||||||
"ARCHIVE"
|
|
||||||
],
|
|
||||||
"transitions": [
|
|
||||||
{
|
|
||||||
"name": "creationDI",
|
|
||||||
"from": "ABSENT",
|
|
||||||
"to": "CREE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "envoiDI",
|
|
||||||
"from": "CREE",
|
|
||||||
"to": "ENVOYE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "archiveDI",
|
|
||||||
"from": "CREE",
|
|
||||||
"to": "ARCHIVE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "saisiDI",
|
|
||||||
"from": "ENVOYE",
|
|
||||||
"to": "EN_VALIDATION"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "relanceDI",
|
|
||||||
"from": "ENVOYE",
|
|
||||||
"to": "A_RELANCER"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "archiveDI",
|
|
||||||
"from": "A_RELANCER",
|
|
||||||
"to": "ARCHIVE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "archiveDI",
|
|
||||||
"from": "ENVOYE",
|
|
||||||
"to": "ARCHIVE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "valideDI",
|
|
||||||
"from": "EN_VALIDATION",
|
|
||||||
"to": "VALIDE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "archiveDI",
|
|
||||||
"from": "EN_VALIDATION",
|
|
||||||
"to": "ARCHIVE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "archiveDI",
|
|
||||||
"from": "VALIDE",
|
|
||||||
"to": "ARCHIVE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"activationMailRelance": "Oui",
|
|
||||||
"delaiRelance": "30",
|
|
||||||
"ambiances": [
|
|
||||||
"2-3 ans",
|
|
||||||
"3-6 ans",
|
|
||||||
"6-12 ans"
|
|
||||||
],
|
|
||||||
"genres": [
|
|
||||||
"Fille",
|
|
||||||
"Garçon"
|
|
||||||
],
|
|
||||||
"modesPaiement": [
|
|
||||||
"Chèque",
|
|
||||||
"Virement",
|
|
||||||
"Prélèvement SEPA"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
default_app_config = 'GestionInscriptions.apps.GestionInscriptionsConfig'
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
# state_machine.py
|
|
||||||
import json
|
|
||||||
from GestionInscriptions.models import FicheInscription
|
|
||||||
from GestionInscriptions.signals import clear_cache
|
|
||||||
|
|
||||||
state_mapping = {
|
|
||||||
"ABSENT": FicheInscription.EtatDossierInscription.DI_ABSENT,
|
|
||||||
"CREE": FicheInscription.EtatDossierInscription.DI_CREE,
|
|
||||||
"ENVOYE": FicheInscription.EtatDossierInscription.DI_ENVOYE,
|
|
||||||
"EN_VALIDATION": FicheInscription.EtatDossierInscription.DI_EN_VALIDATION,
|
|
||||||
"A_RELANCER": FicheInscription.EtatDossierInscription.DI_A_RELANCER,
|
|
||||||
"VALIDE": FicheInscription.EtatDossierInscription.DI_VALIDE,
|
|
||||||
"ARCHIVE": FicheInscription.EtatDossierInscription.DI_ARCHIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_config(config_file):
|
|
||||||
with open(config_file, 'r') as file:
|
|
||||||
config = json.load(file)
|
|
||||||
return config
|
|
||||||
|
|
||||||
def getStateMachineObject(etat) :
|
|
||||||
return Automate_DI_Inscription(etat)
|
|
||||||
|
|
||||||
def getStateMachineObjectState(etat):
|
|
||||||
return Automate_DI_Inscription(etat).state
|
|
||||||
|
|
||||||
def updateStateMachine(di, transition) :
|
|
||||||
automateModel = load_config('GestionInscriptions/Configuration/automate.json')
|
|
||||||
state_machine = getStateMachineObject(di.etat)
|
|
||||||
print(f'etat DI : {state_machine.state}')
|
|
||||||
if state_machine.trigger(transition, automateModel):
|
|
||||||
di.etat = state_machine.state
|
|
||||||
di.save()
|
|
||||||
clear_cache()
|
|
||||||
|
|
||||||
class Automate_DI_Inscription:
|
|
||||||
def __init__(self, initial_state):
|
|
||||||
self.state = initial_state
|
|
||||||
|
|
||||||
def trigger(self, transition_name, config):
|
|
||||||
for transition in config["transitions"]:
|
|
||||||
if transition["name"] == transition_name and self.state == state_mapping[transition["from"]]:
|
|
||||||
self.state = state_mapping[transition["to"]]
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
from django.core.mail import send_mail
|
|
||||||
import re
|
|
||||||
from N3wtSchool import settings
|
|
||||||
|
|
||||||
def envoieReinitMotDePasse(recipients, code):
|
|
||||||
send_mail(
|
|
||||||
settings.EMAIL_REINIT_SUBJECT,
|
|
||||||
settings.EMAIL_REINIT_CORPUS%(str(code)),
|
|
||||||
settings.EMAIL_HOST_USER,
|
|
||||||
[recipients],
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def envoieDossierInscription(recipients):
|
|
||||||
errorMessage = ''
|
|
||||||
try:
|
|
||||||
print(f'{settings.EMAIL_HOST_USER}')
|
|
||||||
send_mail(
|
|
||||||
settings.EMAIL_INSCRIPTION_SUBJECT,
|
|
||||||
settings.EMAIL_INSCRIPTION_CORPUS%[recipients],
|
|
||||||
settings.EMAIL_HOST_USER,
|
|
||||||
[recipients],
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
errorMessage = str(e)
|
|
||||||
|
|
||||||
return errorMessage
|
|
||||||
|
|
||||||
def envoieRelanceDossierInscription(recipients, code):
|
|
||||||
errorMessage = ''
|
|
||||||
try:
|
|
||||||
send_mail(
|
|
||||||
settings.EMAIL_RELANCE_SUBJECT,
|
|
||||||
settings.EMAIL_RELANCE_CORPUS%str(code),
|
|
||||||
settings.EMAIL_HOST_USER,
|
|
||||||
[recipients],
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
errorMessage = str(e)
|
|
||||||
|
|
||||||
return errorMessage
|
|
||||||
|
|
||||||
|
|
||||||
def envoieSEPA(recipients, ref):
|
|
||||||
send_mail(
|
|
||||||
settings.EMAIL_SEPA_SUBJECT%str(ref),
|
|
||||||
settings.EMAIL_SEPA_CORPUS,
|
|
||||||
settings.EMAIL_HOST_USER,
|
|
||||||
[recipients],
|
|
||||||
fail_silently=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def isValid(message, fiche_inscription):
|
|
||||||
# Est-ce que la référence du dossier est VALIDE
|
|
||||||
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.getResponsablePrincipal()
|
|
||||||
mailReponsableAVerifier = responsable.mail
|
|
||||||
|
|
||||||
return responsableMail == mailReponsableAVerifier and str(idMail) == str(fiche_inscription.eleve.id)
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from GestionLogin.models import Profil
|
|
||||||
|
|
||||||
class Langue(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
libelle = models.CharField(max_length=200, default="")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "LANGUE"
|
|
||||||
|
|
||||||
class Responsable(models.Model):
|
|
||||||
nom = models.CharField(max_length=200, default="")
|
|
||||||
prenom = models.CharField(max_length=200, default="")
|
|
||||||
dateNaissance = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
adresse = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
mail = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
telephone = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
profession = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
profilAssocie = models.ForeignKey(Profil, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.nom + "_" + self.prenom
|
|
||||||
|
|
||||||
class Frere(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
nom = models.CharField(max_length=200, default="")
|
|
||||||
prenom = models.CharField(max_length=200, default="")
|
|
||||||
dateNaissance = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "FRERE"
|
|
||||||
|
|
||||||
class Eleve(models.Model):
|
|
||||||
|
|
||||||
class GenreEleve(models.IntegerChoices):
|
|
||||||
NONE = 0, _('Sélection du genre')
|
|
||||||
MALE = 1, _('Garçon')
|
|
||||||
FEMALE = 2, _('Fille')
|
|
||||||
|
|
||||||
class NiveauEleve(models.IntegerChoices):
|
|
||||||
NONE = 0, _('Sélection du niveau')
|
|
||||||
TPS = 1, _('TPS - Très Petite Section')
|
|
||||||
PS = 2, _('PS - Petite Section')
|
|
||||||
MS = 3, _('MS - Moyenne Section')
|
|
||||||
GS = 4, _('GS - Grande Section')
|
|
||||||
|
|
||||||
class ModePaiement(models.IntegerChoices):
|
|
||||||
NONE = 0, _('Sélection du mode de paiement')
|
|
||||||
PRELEVEMENT_SEPA = 1, _('Prélèvement SEPA')
|
|
||||||
CHEQUE = 2, _('Chèque')
|
|
||||||
|
|
||||||
nom = models.CharField(max_length=200, default="")
|
|
||||||
prenom = models.CharField(max_length=200, default="")
|
|
||||||
genre = models.IntegerField(choices=GenreEleve, default=GenreEleve.NONE, blank=True)
|
|
||||||
niveau = models.IntegerField(choices=NiveauEleve, default=NiveauEleve.NONE, blank=True)
|
|
||||||
nationalite = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
adresse = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
dateNaissance = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
lieuNaissance = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
codePostalNaissance = models.IntegerField(default=0, blank=True)
|
|
||||||
medecinTraitant = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
modePaiement = models.IntegerField(choices=ModePaiement, default=ModePaiement.NONE, blank=True)
|
|
||||||
|
|
||||||
# Relation N-N
|
|
||||||
profils = models.ManyToManyField(Profil, blank=True)
|
|
||||||
|
|
||||||
# Relation N-N
|
|
||||||
responsables = models.ManyToManyField(Responsable, blank=True)
|
|
||||||
|
|
||||||
# Relation N-N
|
|
||||||
freres = models.ManyToManyField(Frere, blank=True)
|
|
||||||
|
|
||||||
# Relation N-N
|
|
||||||
languesParlees = models.ManyToManyField(Langue, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.nom + "_" + self.prenom
|
|
||||||
|
|
||||||
def getLanguesParlees(self):
|
|
||||||
return self.languesParlees.all()
|
|
||||||
|
|
||||||
def getResponsablePrincipal(self):
|
|
||||||
return self.responsables.all()[0]
|
|
||||||
|
|
||||||
def getResponsables(self):
|
|
||||||
return self.responsables.all()
|
|
||||||
|
|
||||||
def getProfils(self):
|
|
||||||
return self.profils.all()
|
|
||||||
|
|
||||||
def getFreres(self):
|
|
||||||
return self.freres.all()
|
|
||||||
|
|
||||||
def getNbFreres(self):
|
|
||||||
return self.freres.count()
|
|
||||||
|
|
||||||
class FicheInscription(models.Model):
|
|
||||||
|
|
||||||
class EtatDossierInscription(models.IntegerChoices):
|
|
||||||
DI_ABSENT = 0, _('Pas de dossier d\'inscription')
|
|
||||||
DI_CREE = 1, _('Dossier d\'inscription créé')
|
|
||||||
DI_ENVOYE = 2, _('Dossier d\'inscription envoyé')
|
|
||||||
DI_EN_VALIDATION = 3, _('Dossier d\'inscription en cours de validation')
|
|
||||||
DI_A_RELANCER = 4, _('Dossier d\'inscription à relancer')
|
|
||||||
DI_VALIDE = 5, _('Dossier d\'inscription validé')
|
|
||||||
DI_ARCHIVE = 6, _('Dossier d\'inscription archivé')
|
|
||||||
|
|
||||||
# Relation 1-1
|
|
||||||
eleve = models.OneToOneField(Eleve, on_delete=models.CASCADE, primary_key=True)
|
|
||||||
etat = models.IntegerField(choices=EtatDossierInscription, default=EtatDossierInscription.DI_ABSENT)
|
|
||||||
dateMAJ = models.DateTimeField(auto_now=True)
|
|
||||||
notes = models.CharField(max_length=200, blank=True)
|
|
||||||
codeLienInscription = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
fichierInscription = models.FileField(upload_to=settings.DOCUMENT_DIR, default="", blank=True)
|
|
||||||
di_associe = models.CharField(max_length=200, default="", blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "FI_" + self.eleve.nom + "_" + self.eleve.prenom
|
|
||||||
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
from GestionInscriptions.models import FicheInscription, Eleve, Responsable, Frere, Langue
|
|
||||||
from GestionLogin.models import Profil
|
|
||||||
from GestionLogin.serializers import ProfilSerializer
|
|
||||||
from GestionMessagerie.models import Messagerie
|
|
||||||
from GestionNotification.models import Notification
|
|
||||||
from N3wtSchool import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
class LanguesSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
class Meta:
|
|
||||||
model = Langue
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
class FrereSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
class Meta:
|
|
||||||
model = Frere
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
class ResponsableSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
profil_associe = serializers.SerializerMethodField()
|
|
||||||
class Meta:
|
|
||||||
model = Responsable
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
def get_profil_associe(self, obj):
|
|
||||||
return obj.profilAssocie.email
|
|
||||||
|
|
||||||
class EleveSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
responsables = ResponsableSerializer(many=True, required=False)
|
|
||||||
freres = FrereSerializer(many=True, required=False)
|
|
||||||
langues = LanguesSerializer(many=True, required=False)
|
|
||||||
class Meta:
|
|
||||||
model = Eleve
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
def get_or_create_packages(self, responsables_data):
|
|
||||||
responsables_ids = []
|
|
||||||
for responsable_data in responsables_data:
|
|
||||||
responsable_instance, created = Responsable.objects.get_or_create( id=responsable_data.get('id'),
|
|
||||||
defaults=responsable_data)
|
|
||||||
responsables_ids.append(responsable_instance.id)
|
|
||||||
return responsables_ids
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
responsables_data = validated_data.pop('responsables', [])
|
|
||||||
freres_data = validated_data.pop('freres', [])
|
|
||||||
langues_data = validated_data.pop('languesParlees', [])
|
|
||||||
eleve = Eleve.objects.create(**validated_data)
|
|
||||||
eleve.responsables.set(self.get_or_create_packages(responsables_data))
|
|
||||||
eleve.freres.set(self.get_or_create_packages(freres_data))
|
|
||||||
eleve.languesParlees.set(self.get_or_create_packages(langues_data))
|
|
||||||
|
|
||||||
return eleve
|
|
||||||
|
|
||||||
def create_or_update_packages(self, responsables_data):
|
|
||||||
responsables_ids = []
|
|
||||||
|
|
||||||
|
|
||||||
for responsable_data in responsables_data:
|
|
||||||
responsable_instance, created = Responsable.objects.update_or_create( id=responsable_data.get('id'),
|
|
||||||
defaults=responsable_data)
|
|
||||||
|
|
||||||
responsables_ids.append(responsable_instance.id)
|
|
||||||
return responsables_ids
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
responsables_data = validated_data.pop('responsables', [])
|
|
||||||
freres_data = validated_data.pop('freres', [])
|
|
||||||
langues_data = validated_data.pop('languesParlees', [])
|
|
||||||
if responsables_data:
|
|
||||||
instance.responsables.set(self.create_or_update_packages(responsables_data))
|
|
||||||
if freres_data:
|
|
||||||
instance.freres.set(self.create_or_update_packages(freres_data))
|
|
||||||
if langues_data:
|
|
||||||
instance.freres.set(self.create_or_update_packages(langues_data))
|
|
||||||
|
|
||||||
for field in self.fields:
|
|
||||||
try:
|
|
||||||
setattr(instance, field, validated_data[field])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
class FicheInscriptionSerializer(serializers.ModelSerializer):
|
|
||||||
eleve = EleveSerializer(many=False, required=True)
|
|
||||||
fichierInscription = serializers.FileField(required=False)
|
|
||||||
etat_label = serializers.SerializerMethodField()
|
|
||||||
dateMAJ_formattee = serializers.SerializerMethodField()
|
|
||||||
class Meta:
|
|
||||||
model = FicheInscription
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
eleve_data = validated_data.pop('eleve')
|
|
||||||
eleve = EleveSerializer.create(EleveSerializer(), eleve_data)
|
|
||||||
ficheEleve = FicheInscription.objects.create(eleve=eleve, **validated_data)
|
|
||||||
return ficheEleve
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
eleve_data = validated_data.pop('eleve')
|
|
||||||
eleve = instance.eleve
|
|
||||||
eleve_serializer = EleveSerializer.update(EleveSerializer(), eleve, eleve_data)
|
|
||||||
|
|
||||||
for field in self.fields:
|
|
||||||
try:
|
|
||||||
setattr(instance, field, validated_data[field])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def get_etat_label(self, obj):
|
|
||||||
return obj.get_etat_display()
|
|
||||||
|
|
||||||
def get_dateMAJ_formattee(self, obj):
|
|
||||||
utc_time = timezone.localtime(obj.dateMAJ) # Convertir en heure locale
|
|
||||||
local_tz = pytz.timezone(settings.TZ_APPLI)
|
|
||||||
local_time = utc_time.astimezone(local_tz)
|
|
||||||
|
|
||||||
return local_time.strftime("%d-%m-%Y %H:%M")
|
|
||||||
|
|
||||||
class EleveByParentSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
class Meta:
|
|
||||||
model = Eleve
|
|
||||||
fields = ['id', 'nom', 'prenom']
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(EleveByParentSerializer , self).__init__(*args, **kwargs)
|
|
||||||
for field in self.fields:
|
|
||||||
self.fields[field].required = False
|
|
||||||
|
|
||||||
class FicheInscriptionByParentSerializer(serializers.ModelSerializer):
|
|
||||||
eleve = EleveByParentSerializer(many=False, required=True)
|
|
||||||
class Meta:
|
|
||||||
model = FicheInscription
|
|
||||||
fields = ['eleve', 'etat']
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(FicheInscriptionByParentSerializer, self).__init__(*args, **kwargs)
|
|
||||||
for field in self.fields:
|
|
||||||
self.fields[field].required = False
|
|
||||||
|
|
||||||
class ResponsableByDICreationSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
class Meta:
|
|
||||||
model = Responsable
|
|
||||||
fields = ['id', 'nom', 'prenom', 'mail', 'profilAssocie']
|
|
||||||
|
|
||||||
class EleveByDICreationSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
responsables = ResponsableByDICreationSerializer(many=True, required=False)
|
|
||||||
class Meta:
|
|
||||||
model = Eleve
|
|
||||||
fields = ['id', 'nom', 'prenom', 'responsables']
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(EleveByDICreationSerializer , self).__init__(*args, **kwargs)
|
|
||||||
for field in self.fields:
|
|
||||||
self.fields[field].required = False
|
|
||||||
|
|
||||||
class NotificationSerializer(serializers.ModelSerializer):
|
|
||||||
typeNotification_label = serializers.ReadOnlyField()
|
|
||||||
class Meta:
|
|
||||||
model = Notification
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from GestionInscriptions.models import FicheInscription, Eleve, Responsable
|
|
||||||
from GestionLogin.models import Profil
|
|
||||||
from N3wtSchool import settings
|
|
||||||
from N3wtSchool.redis_client import redis_client
|
|
||||||
|
|
||||||
def clear_cache():
|
|
||||||
# Préfixes des clés à supprimer
|
|
||||||
prefixes = ['N3WT_']
|
|
||||||
|
|
||||||
for prefix in prefixes:
|
|
||||||
# Utiliser le motif pour obtenir les clés correspondant au préfixe
|
|
||||||
pattern = f'*{prefix}*'
|
|
||||||
print(f'pattern : {pattern}')
|
|
||||||
for key in redis_client.scan_iter(pattern):
|
|
||||||
redis_client.delete(key)
|
|
||||||
print(f'deleting : {key}')
|
|
||||||
|
|
||||||
@receiver(post_save, sender=FicheInscription)
|
|
||||||
@receiver(post_delete, sender=FicheInscription)
|
|
||||||
def clear_cache_after_change(sender, instance, **kwargs):
|
|
||||||
clear_cache()
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=Eleve.responsables.through)
|
|
||||||
def check_orphan_reponsables(sender, **kwargs):
|
|
||||||
action = kwargs.pop('action', None)
|
|
||||||
instance = kwargs.pop('instance', None)
|
|
||||||
# pre_clear : lors de la suppression d'une FI (on fait un "clear" sur chaque relation)
|
|
||||||
if action in ('post_remove', 'post_clear'):
|
|
||||||
if instance.responsables.all():
|
|
||||||
Responsable.objects.filter(eleve=None).delete()
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=Eleve.profils.through)
|
|
||||||
def check_orphan_profils(sender, **kwargs):
|
|
||||||
action = kwargs.pop('action', None)
|
|
||||||
instance = kwargs.pop('instance', None)
|
|
||||||
# pre_clear : lors de la suppression d'une FI (on fait un "clear" sur chaque relation)
|
|
||||||
if action in ('post_remove', 'post_clear'):
|
|
||||||
if instance.profils.all():
|
|
||||||
Profil.objects.filter(eleve=None).delete()
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
# tasks.py
|
|
||||||
from celery import shared_task
|
|
||||||
from django.utils import timezone
|
|
||||||
from GestionInscriptions.automate import Automate_DI_Inscription, updateStateMachine
|
|
||||||
from .models import FicheInscription
|
|
||||||
from GestionMessagerie.models import Messagerie
|
|
||||||
from N3wtSchool import settings, bdd
|
|
||||||
import requests
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def check_for_signature_deadlines():
|
|
||||||
now = timezone.now()
|
|
||||||
deadline = now - timezone.timedelta(days=settings.EXPIRATION_DI_NB_DAYS)
|
|
||||||
# deadline = now - timezone.timedelta(seconds=settings.EXPIRATION_DI_NB_DAYS)
|
|
||||||
|
|
||||||
dossiers_en_attente = FicheInscription.objects.filter(etat=FicheInscription.EtatDossierInscription.DI_ENVOYE, dateMAJ__lt=deadline)
|
|
||||||
|
|
||||||
for dossier in dossiers_en_attente:
|
|
||||||
send_notification(dossier)
|
|
||||||
|
|
||||||
def send_notification(dossier):
|
|
||||||
print(f'Dossier en attente.... {dossier} - Positionnement à l\'état A_RELANCER')
|
|
||||||
|
|
||||||
# Changer l'état de l'automate
|
|
||||||
updateStateMachine(dossier, 'relanceDI')
|
|
||||||
|
|
||||||
url = settings.URL_DJANGO + 'GestionMessagerie/message'
|
|
||||||
|
|
||||||
destinataires = dossier.eleve.profils.all()
|
|
||||||
for destinataire in destinataires:
|
|
||||||
message = {
|
|
||||||
"objet": "[RELANCE]",
|
|
||||||
"destinataire" : destinataire.id,
|
|
||||||
"corpus": "RELANCE pour le dossier d'inscription"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(url, json=message)
|
|
||||||
|
|
||||||
# subject = f"Dossier d'inscription non signé - {dossier.objet}"
|
|
||||||
# message = f"Le dossier d'inscription avec l'objet '{dossier.objet}' n'a pas été signé depuis {dossier.created_at}."
|
|
||||||
# send_mail(subject, message, settings.EMAIL_HOST_USER, [dossier.destinataire.email])
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>{{ pdf_title }}</title>
|
|
||||||
<style type="text/css">
|
|
||||||
body {
|
|
||||||
font-weight: 200;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 100;
|
|
||||||
text-align: center;
|
|
||||||
color: #007cae;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 100;
|
|
||||||
/* text-align: right;*/
|
|
||||||
padding: 10px 20px 0px 20px;
|
|
||||||
}
|
|
||||||
.title span {
|
|
||||||
color: #007cae;
|
|
||||||
}
|
|
||||||
.details {
|
|
||||||
padding: 10px 20px 0px 20px;
|
|
||||||
text-align: left !important;
|
|
||||||
/*margin-left: 40%;*/
|
|
||||||
}
|
|
||||||
.hrItem {
|
|
||||||
border: none;
|
|
||||||
height: 1px;
|
|
||||||
/* Set the hr color */
|
|
||||||
color: #333; /* old IE */
|
|
||||||
background-color: #fff; /* Modern Browsers */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% load myTemplateTag %}
|
|
||||||
<div class='wrapper'>
|
|
||||||
<div class='header'>
|
|
||||||
<p class='title'>{{ pdf_title }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class='details'>
|
|
||||||
Signé le : <b>{{ dateSignature }}</b> <br/>
|
|
||||||
A : <b>{{ heureSignature }}</b>
|
|
||||||
<hr class='hrItem' />
|
|
||||||
<h1>ELEVE</h1>
|
|
||||||
{% with niveau=eleve|recupereNiveauEleve %}
|
|
||||||
{% with genre=eleve|recupereGenreEleve %}
|
|
||||||
NOM : <b>{{ eleve.nom }}</b> <br/>
|
|
||||||
PRENOM : <b>{{ eleve.prenom }}</b> <br/>
|
|
||||||
ADRESSE : <b>{{ eleve.adresse }}</b> <br/>
|
|
||||||
GENRE : <b>{{ genre }}</b> <br/>
|
|
||||||
NE(E) LE : <b>{{ eleve.dateNaissance }}</b> <br/>
|
|
||||||
A : <b>{{ eleve.lieuNaissance }} ({{ eleve.codePostalNaissance }})</b> <br/>
|
|
||||||
NATIONALITE : <b>{{ eleve.nationalite }}</b> <br/>
|
|
||||||
NIVEAU : <b>{{ niveau }}</b> <br/>
|
|
||||||
MEDECIN TRAITANT : <b>{{ eleve.medecinTraitant }}</b> <br/>
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
<hr class='hrItem' />
|
|
||||||
<h1>RESPONSABLES</h1>
|
|
||||||
{% with responsables_List=eleve.getResponsables %}
|
|
||||||
{% with freres_List=eleve.getFreres %}
|
|
||||||
{% for responsable in responsables_List%}
|
|
||||||
<h2>Responsable {{ forloop.counter }}</h2>
|
|
||||||
NOM : <b>{{ responsable.nom }}</b> <br/>
|
|
||||||
PRENOM : <b>{{ responsable.prenom }}</b> <br/>
|
|
||||||
ADRESSE : <b>{{ responsable.adresse }}</b> <br/>
|
|
||||||
NE(E) LE : <b>{{ responsable.dateNaissance }}</b> <br/>
|
|
||||||
MAIL : <b>{{ responsable.mail }}</b> <br/>
|
|
||||||
TEL : <b>{{ responsable.telephone }}</b> <br/>
|
|
||||||
PROFESSION : <b>{{ responsable.profession }}</b> <br/>
|
|
||||||
{% endfor %}
|
|
||||||
<hr class='hrItem' />
|
|
||||||
<h1>FRATRIE</h1>
|
|
||||||
{% for frere in freres_List%}
|
|
||||||
<h2>Frère - Soeur {{ forloop.counter }}</h2>
|
|
||||||
NOM : <b>{{ frere.nom }}</b> <br/>
|
|
||||||
PRENOM : <b>{{ frere.prenom }}</b> <br/>
|
|
||||||
NE(E) LE : <b>{{ frere.dateNaissance }}</b> <br/>
|
|
||||||
{% endfor %}
|
|
||||||
<hr class='hrItem' />
|
|
||||||
<h1>MODALITES DE PAIEMENT</h1>
|
|
||||||
{% with modePaiement=eleve|recupereModePaiement %}
|
|
||||||
<b>{{ modePaiement }}</b> <br/>
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
from GestionInscriptions.models import FicheInscription, Eleve
|
|
||||||
from django import template
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
# @register.filter
|
|
||||||
# def recupereFichiersDossierInscription(pk):
|
|
||||||
# fichiers_list = FicheInscription.objects.filter(fiche_inscription=pk)
|
|
||||||
# return fichiers_list
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def recupereModePaiement(pk):
|
|
||||||
ficheInscription = FicheInscription.objects.get(eleve=pk)
|
|
||||||
return Eleve.ModePaiement(int(ficheInscription.eleve.modePaiement)).label
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def recupereNiveauEleve(pk):
|
|
||||||
ficheInscription = FicheInscription.objects.get(eleve=pk)
|
|
||||||
return Eleve.NiveauEleve(int(ficheInscription.eleve.niveau)).label
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def recupereGenreEleve(pk):
|
|
||||||
ficheInscription = FicheInscription.objects.get(eleve=pk)
|
|
||||||
return Eleve.GenreEleve(int(ficheInscription.eleve.genre)).label
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
from django.urls import path, re_path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
from GestionInscriptions.views import ListFichesInscriptionView, FicheInscriptionView, EleveView, ResponsableView, ListeEnfantsView, ListeElevesView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
re_path(r'^fichesInscription/([a-zA-z]+)$', ListFichesInscriptionView.as_view(), name="listefichesInscriptions"),
|
|
||||||
re_path(r'^ficheInscription$', FicheInscriptionView.as_view(), name="fichesInscriptions"),
|
|
||||||
re_path(r'^ficheInscription/([0-9]+)$', FicheInscriptionView.as_view(), name="fichesInscriptions"),
|
|
||||||
|
|
||||||
# Page de formulaire d'inscription - ELEVE
|
|
||||||
re_path(r'^eleve/([0-9]+)$', EleveView.as_view(), name="eleves"),
|
|
||||||
|
|
||||||
# Page de formulaire d'inscription - RESPONSABLE
|
|
||||||
re_path(r'^recupereDernierResponsable$', ResponsableView.as_view(), name="recupereDernierResponsable"),
|
|
||||||
|
|
||||||
# Envoi d'un dossier d'inscription
|
|
||||||
re_path(r'^send/([0-9]+)$', views.send, name="send"),
|
|
||||||
|
|
||||||
# Archivage d'un dossier d'inscription
|
|
||||||
re_path(r'^archive/([0-9]+)$', views.archive, name="archive"),
|
|
||||||
|
|
||||||
# Envoi d'une relance de dossier d'inscription
|
|
||||||
re_path(r'^sendRelance/([0-9]+)$', views.relance, name="relance"),
|
|
||||||
|
|
||||||
# Page PARENT - Liste des enfants
|
|
||||||
re_path(r'^enfants/([0-9]+)$', ListeEnfantsView.as_view(), name="enfants"),
|
|
||||||
|
|
||||||
# Page INSCRIPTION - Liste des élèves
|
|
||||||
re_path(r'^eleves$', ListeElevesView.as_view(), name="enfants"),
|
|
||||||
]
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
from django.shortcuts import render,get_object_or_404,get_list_or_404
|
|
||||||
from .models import FicheInscription, Eleve, Responsable, Frere
|
|
||||||
import time
|
|
||||||
from datetime import date, datetime, timedelta
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
from django.conf import settings
|
|
||||||
from N3wtSchool import renderers
|
|
||||||
from N3wtSchool import bdd
|
|
||||||
|
|
||||||
from io import BytesIO
|
|
||||||
from django.core.files import File
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
from rest_framework.parsers import JSONParser
|
|
||||||
|
|
||||||
def recupereListeFichesInscription():
|
|
||||||
context = {
|
|
||||||
"ficheInscriptions_list": bdd.getAllObjects(FicheInscription),
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
|
|
||||||
def recupereListeFichesInscriptionEnAttenteSEPA():
|
|
||||||
|
|
||||||
ficheInscriptionsSEPA_list = FicheInscription.objects.filter(modePaiement="Prélèvement SEPA").filter(etat=FicheInscription.EtatDossierInscription['SEPA_ENVOYE'])
|
|
||||||
return ficheInscriptionsSEPA_list
|
|
||||||
|
|
||||||
def updateEleve(eleve, inputs, erase=False):
|
|
||||||
eleve.nom = inputs["nomEleve"]
|
|
||||||
eleve.prenom = inputs["prenomEleve"]
|
|
||||||
eleve.ambiance = inputs["ambiance"]
|
|
||||||
eleve.genre = inputs["genre"]
|
|
||||||
eleve.adresse = inputs["adresseEleve"]
|
|
||||||
eleve.dateNaissance = inputs["dateNaissanceEleve"]
|
|
||||||
eleve.lieuNaissance = inputs["lieuNaissanceEleve"]
|
|
||||||
eleve.codePostalNaissance = inputs["codePostalNaissanceEleve"]
|
|
||||||
eleve.nationalite = inputs["nationaliteEleve"]
|
|
||||||
eleve.medecinTraitant = inputs["medecinTraitantEleve"]
|
|
||||||
|
|
||||||
|
|
||||||
responsable=eleve.getResponsablePrincipal()
|
|
||||||
responsable.adresse = inputs["adresseResponsable1"]
|
|
||||||
responsable.dateNaissance = inputs["dateNaissanceResponsable1"]
|
|
||||||
responsable.profession = inputs["professionResponsable1"]
|
|
||||||
responsable.save()
|
|
||||||
|
|
||||||
# Création du 2ème responsable
|
|
||||||
if inputs["nomResponsable2"] != "" and inputs["prenomResponsable2"] != "":
|
|
||||||
responsable2 = Responsable.objects.create(nom=inputs["nomResponsable2"],
|
|
||||||
prenom=inputs["prenomResponsable2"],
|
|
||||||
dateNaissance=inputs["dateNaissanceResponsable2"],
|
|
||||||
adresse=inputs["adresseResponsable2"],
|
|
||||||
mail=inputs["mailResponsable2"],
|
|
||||||
telephone=inputs["telephoneResponsable2"],
|
|
||||||
profession=inputs["professionResponsable2"])
|
|
||||||
responsable2.save()
|
|
||||||
eleve.responsables.add(responsable2)
|
|
||||||
|
|
||||||
# Création du 1er frère
|
|
||||||
if inputs["nomFrere1"] != "" and inputs["prenomFrere1"] != "":
|
|
||||||
frere1 = Frere.objects.create(nom=inputs["nomFrere1"],
|
|
||||||
prenom=inputs["prenomFrere1"],
|
|
||||||
dateNaissance=inputs["dateNaissanceFrere1"])
|
|
||||||
frere1.save()
|
|
||||||
eleve.freres.add(frere1)
|
|
||||||
|
|
||||||
# Création du 2ème frère
|
|
||||||
if inputs["nomFrere2"] != "" and inputs["prenomFrere2"] != "":
|
|
||||||
frere2 = Frere.objects.create(nom=inputs["nomFrere2"],
|
|
||||||
prenom=inputs["prenomFrere2"],
|
|
||||||
dateNaissance=inputs["dateNaissanceFrere2"])
|
|
||||||
frere2.save()
|
|
||||||
eleve.freres.add(frere2)
|
|
||||||
|
|
||||||
eleve.save()
|
|
||||||
|
|
||||||
def _now():
|
|
||||||
return datetime.now(ZoneInfo(settings.TZ_APPLI))
|
|
||||||
|
|
||||||
def convertToStr(dateValue, dateFormat):
|
|
||||||
return dateValue.strftime(dateFormat)
|
|
||||||
|
|
||||||
def convertToDate(date_time):
|
|
||||||
format = '%d-%m-%Y %H:%M'
|
|
||||||
datetime_str = datetime.strptime(date_time, format)
|
|
||||||
|
|
||||||
return datetime_str
|
|
||||||
|
|
||||||
def convertTelephone(telephoneValue, separator='-'):
|
|
||||||
return f"{telephoneValue[:2]}{separator}{telephoneValue[2:4]}{separator}{telephoneValue[4:6]}{separator}{telephoneValue[6:8]}{separator}{telephoneValue[8:10]}"
|
|
||||||
|
|
||||||
def generePDF(ficheEleve):
|
|
||||||
data = {
|
|
||||||
'pdf_title': "Dossier d'inscription de %s"%ficheEleve.eleve.prenom,
|
|
||||||
'dateSignature': convertToStr(_now(), '%d-%m-%Y'),
|
|
||||||
'heureSignature': convertToStr(_now(), '%H:%M'),
|
|
||||||
'eleve':ficheEleve.eleve,
|
|
||||||
}
|
|
||||||
|
|
||||||
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
|
|
||||||
|
|
||||||
nomFichierPDF = "Dossier_Inscription_%s_%s.pdf"%(ficheEleve.eleve.nom, ficheEleve.eleve.prenom)
|
|
||||||
pathFichier = Path(settings.DOCUMENT_DIR + "/" + nomFichierPDF)
|
|
||||||
if os.path.exists(str(pathFichier)):
|
|
||||||
os.remove(str(pathFichier))
|
|
||||||
|
|
||||||
receipt_file = BytesIO(pdf.content)
|
|
||||||
# fichier = Fichier.objects.create(fiche_inscription=ficheEleve)
|
|
||||||
# fichier.document = File(receipt_file, nomFichierPDF)
|
|
||||||
# fichier.save()
|
|
||||||
|
|
||||||
def genereRandomCode(length):
|
|
||||||
return ''.join(random.choice(string.ascii_letters) for i in range(length))
|
|
||||||
|
|
||||||
def calculeDatePeremption(_start, nbDays):
|
|
||||||
return convertToStr(_start + timedelta(days=nbDays), settings.DATE_FORMAT)
|
|
||||||
|
|
||||||
# Fonction permettant de retourner la valeur du QueryDict
|
|
||||||
# QueryDict [ index ] -> Dernière valeur d'une liste
|
|
||||||
# dict (QueryDict [ index ]) -> Toutes les valeurs de la liste
|
|
||||||
def _(liste):
|
|
||||||
return liste[0]
|
|
||||||
|
|
||||||
def toNewEleveJSONRequest(jsonOrigin):
|
|
||||||
etat=FicheInscription.EtatDossierInscription.DI_CREE
|
|
||||||
telephone = convertTelephone(_(jsonOrigin['telephoneResponsable']))
|
|
||||||
finalJSON = {
|
|
||||||
"eleve":
|
|
||||||
{
|
|
||||||
"nom" : _(jsonOrigin['nomEleve']),
|
|
||||||
"prenom" : _(jsonOrigin['prenomEleve']),
|
|
||||||
"responsables" : [
|
|
||||||
{
|
|
||||||
"nom" : _(jsonOrigin['nomResponsable']),
|
|
||||||
"prenom" : _(jsonOrigin['prenomResponsable']),
|
|
||||||
"mail" : _(jsonOrigin['mailResponsable']),
|
|
||||||
"telephone" : telephone
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"profils" : [
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"etat": str(etat),
|
|
||||||
"dateMAJ": str(convertToStr(_now(), '%d-%m-%Y %H:%M')),
|
|
||||||
}
|
|
||||||
print(finalJSON)
|
|
||||||
return finalJSON
|
|
||||||
|
|
||||||
def toEditEleveJSONRequest(jsonOrigin):
|
|
||||||
telephone = convertTelephone(_(jsonOrigin['telephoneResponsable']), '.')
|
|
||||||
finalJSON = {
|
|
||||||
"eleve":
|
|
||||||
{
|
|
||||||
"id" : _(jsonOrigin['fiche_id']),
|
|
||||||
"nom" : _(jsonOrigin['nomEleve']),
|
|
||||||
"prenom" : _(jsonOrigin['prenomEleve']),
|
|
||||||
"responsables" : [
|
|
||||||
{
|
|
||||||
"id" : _(jsonOrigin['responsable_id']),
|
|
||||||
"nom" : _(jsonOrigin['nomResponsable']),
|
|
||||||
"prenom" : _(jsonOrigin['prenomResponsable']),
|
|
||||||
"mail" : _(jsonOrigin['mailResponsable']),
|
|
||||||
"telephone" : telephone
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"profils" : [
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"dateMAJ": str(convertToStr(_now(), '%d-%m-%Y %H:%M')),
|
|
||||||
}
|
|
||||||
print(finalJSON)
|
|
||||||
return finalJSON
|
|
||||||
|
|
||||||
def getArgFromRequest(_argument, _request):
|
|
||||||
resultat = None
|
|
||||||
data=JSONParser().parse(_request)
|
|
||||||
resultat = data[_argument]
|
|
||||||
return resultat
|
|
||||||
@ -1,289 +0,0 @@
|
|||||||
from django.http.response import JsonResponse
|
|
||||||
from django.contrib.auth import login, authenticate, get_user_model
|
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.core.paginator import Paginator
|
|
||||||
from django.core.files import File
|
|
||||||
from django.db.models import Q # Ajout de cet import
|
|
||||||
from rest_framework.parsers import JSONParser
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import GestionInscriptions.mailManager as mailer
|
|
||||||
import GestionInscriptions.util as util
|
|
||||||
from GestionInscriptions.serializers import FicheInscriptionSerializer, EleveSerializer, FicheInscriptionByParentSerializer, EleveByDICreationSerializer
|
|
||||||
from GestionInscriptions.pagination import CustomPagination
|
|
||||||
from GestionInscriptions.signals import clear_cache
|
|
||||||
from .models import Eleve, Responsable, FicheInscription
|
|
||||||
from GestionInscriptions.automate import Automate_DI_Inscription, load_config, getStateMachineObjectState, updateStateMachine
|
|
||||||
|
|
||||||
from GestionLogin.models import Profil
|
|
||||||
|
|
||||||
from N3wtSchool import settings, renderers, bdd
|
|
||||||
|
|
||||||
class ListFichesInscriptionView(APIView):
|
|
||||||
pagination_class = CustomPagination
|
|
||||||
|
|
||||||
def get(self, request, _filter):
|
|
||||||
if _filter == 'all':
|
|
||||||
# Récupération des paramètres
|
|
||||||
search = request.GET.get('search', '').strip()
|
|
||||||
page_size = request.GET.get('page_size', None)
|
|
||||||
|
|
||||||
# Gestion du page_size
|
|
||||||
if page_size is not None:
|
|
||||||
try:
|
|
||||||
page_size = int(page_size)
|
|
||||||
except ValueError:
|
|
||||||
page_size = settings.NB_RESULT_PER_PAGE
|
|
||||||
|
|
||||||
cached_page_size = cache.get('N3WT_page_size')
|
|
||||||
if cached_page_size != page_size:
|
|
||||||
clear_cache()
|
|
||||||
cache.set('N3WT_page_size', page_size)
|
|
||||||
|
|
||||||
# Gestion du cache
|
|
||||||
page_number = request.GET.get('page', 1)
|
|
||||||
cache_key = f'N3WT_ficheInscriptions_page_{page_number}_search_{search}'
|
|
||||||
cached_page = cache.get(cache_key)
|
|
||||||
if cached_page:
|
|
||||||
return JsonResponse(cached_page, safe=False)
|
|
||||||
|
|
||||||
# Filtrage des résultats
|
|
||||||
if search:
|
|
||||||
# Utiliser la nouvelle fonction de recherche
|
|
||||||
ficheInscriptions_List = bdd.searchObjects(
|
|
||||||
FicheInscription,
|
|
||||||
search,
|
|
||||||
_excludeState=6 # Exclure les fiches archivées
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Récupère toutes les fiches non archivées
|
|
||||||
ficheInscriptions_List = bdd.getObjects(FicheInscription, 'etat', 6, _reverseCondition=True)
|
|
||||||
|
|
||||||
# Pagination
|
|
||||||
paginator = self.pagination_class()
|
|
||||||
page = paginator.paginate_queryset(ficheInscriptions_List, request)
|
|
||||||
if page is not None:
|
|
||||||
ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True)
|
|
||||||
response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data)
|
|
||||||
cache.set(cache_key, response_data, timeout=60*15)
|
|
||||||
return JsonResponse(response_data, safe=False)
|
|
||||||
|
|
||||||
elif _filter == 'archived' :
|
|
||||||
page_size = request.GET.get('page_size', None)
|
|
||||||
if page_size is not None:
|
|
||||||
try:
|
|
||||||
page_size = int(page_size)
|
|
||||||
except ValueError:
|
|
||||||
page_size = settings.NB_RESULT_PER_PAGE
|
|
||||||
|
|
||||||
cached_page_size = cache.get('N3WT_archived_page_size')
|
|
||||||
|
|
||||||
# Comparer avec le nouveau page_size
|
|
||||||
if cached_page_size != page_size:
|
|
||||||
# Appeler cached_page() et mettre à jour le cache
|
|
||||||
clear_cache()
|
|
||||||
cache.set('N3WT_archived_page_size',page_size)
|
|
||||||
|
|
||||||
page_number = request.GET.get('page', 1)
|
|
||||||
cache_key_page = f'N3WT_ficheInscriptions_archives_page_{page_number}'
|
|
||||||
cached_page = cache.get(cache_key_page)
|
|
||||||
if cached_page:
|
|
||||||
return JsonResponse(cached_page, safe=False)
|
|
||||||
|
|
||||||
ficheInscriptions_List=bdd.getObjects(FicheInscription, 'etat', 6)
|
|
||||||
paginator = self.pagination_class()
|
|
||||||
page = paginator.paginate_queryset(ficheInscriptions_List, request)
|
|
||||||
if page is not None:
|
|
||||||
ficheInscriptions_serializer = FicheInscriptionSerializer(page, many=True)
|
|
||||||
response_data = paginator.get_paginated_response(ficheInscriptions_serializer.data)
|
|
||||||
cache.set(cache_key_page, response_data, timeout=60*15)
|
|
||||||
|
|
||||||
return JsonResponse(response_data, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
fichesEleve_data=JSONParser().parse(request)
|
|
||||||
for ficheEleve_data in fichesEleve_data:
|
|
||||||
# Ajout de la date de mise à jour
|
|
||||||
ficheEleve_data["dateMAJ"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
|
|
||||||
json.dumps(ficheEleve_data)
|
|
||||||
# Ajout du code d'inscription
|
|
||||||
code = util.genereRandomCode(12)
|
|
||||||
ficheEleve_data["codeLienInscription"] = code
|
|
||||||
ficheEleve_serializer = FicheInscriptionSerializer(data=ficheEleve_data)
|
|
||||||
|
|
||||||
if ficheEleve_serializer.is_valid():
|
|
||||||
ficheEleve_serializer.save()
|
|
||||||
|
|
||||||
return JsonResponse(ficheEleve_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class FicheInscriptionView(APIView):
|
|
||||||
pagination_class = CustomPagination
|
|
||||||
|
|
||||||
def get(self, request, _id):
|
|
||||||
ficheInscription=bdd.getObject(FicheInscription, "eleve__id", _id)
|
|
||||||
fiche_serializer=FicheInscriptionSerializer(ficheInscription)
|
|
||||||
return JsonResponse(fiche_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
ficheEleve_data=JSONParser().parse(request)
|
|
||||||
# Ajout de la date de mise à jour
|
|
||||||
ficheEleve_data["dateMAJ"] = util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
|
|
||||||
json.dumps(ficheEleve_data)
|
|
||||||
# Ajout du code d'inscription
|
|
||||||
code = util.genereRandomCode(12)
|
|
||||||
ficheEleve_data["codeLienInscription"] = code
|
|
||||||
|
|
||||||
responsablesId = ficheEleve_data.pop('idResponsables', [])
|
|
||||||
ficheEleve_serializer = FicheInscriptionSerializer(data=ficheEleve_data)
|
|
||||||
|
|
||||||
if ficheEleve_serializer.is_valid():
|
|
||||||
di = ficheEleve_serializer.save()
|
|
||||||
|
|
||||||
# Mise à jour de l'automate
|
|
||||||
updateStateMachine(di, 'creationDI')
|
|
||||||
|
|
||||||
# Récupération du reponsable associé
|
|
||||||
for responsableId in responsablesId:
|
|
||||||
responsable = Responsable.objects.get(id=responsableId)
|
|
||||||
di.eleve.responsables.add(responsable)
|
|
||||||
di.save()
|
|
||||||
|
|
||||||
ficheInscriptions_List=bdd.getAllObjects(FicheInscription)
|
|
||||||
return JsonResponse({'totalInscrits':len(ficheInscriptions_List)}, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(ficheEleve_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def put(self, request, id):
|
|
||||||
ficheEleve_data=JSONParser().parse(request)
|
|
||||||
admin = ficheEleve_data.pop('admin', 1)
|
|
||||||
ficheEleve_data["dateMAJ"] = str(util.convertToStr(util._now(), '%d-%m-%Y %H:%M'))
|
|
||||||
ficheEleve = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id)
|
|
||||||
currentState = getStateMachineObjectState(ficheEleve.etat)
|
|
||||||
if admin == 0 and currentState == FicheInscription.EtatDossierInscription.DI_ENVOYE:
|
|
||||||
json.dumps(ficheEleve_data)
|
|
||||||
|
|
||||||
# Ajout du fichier d'inscriptions
|
|
||||||
data = {
|
|
||||||
'pdf_title': "Dossier d'inscription de %s"%ficheEleve.eleve.prenom,
|
|
||||||
'dateSignature': util.convertToStr(util._now(), '%d-%m-%Y'),
|
|
||||||
'heureSignature': util.convertToStr(util._now(), '%H:%M'),
|
|
||||||
'eleve':ficheEleve.eleve,
|
|
||||||
}
|
|
||||||
|
|
||||||
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
|
|
||||||
|
|
||||||
nomFichierPDF = "Dossier_Inscription_%s_%s.pdf"%(ficheEleve.eleve.nom, ficheEleve.eleve.prenom)
|
|
||||||
pathFichier = Path(settings.DOCUMENT_DIR + "/" + nomFichierPDF)
|
|
||||||
if os.path.exists(str(pathFichier)):
|
|
||||||
print(f'File exists : {str(pathFichier)}')
|
|
||||||
os.remove(str(pathFichier))
|
|
||||||
|
|
||||||
receipt_file = BytesIO(pdf.content)
|
|
||||||
ficheEleve.fichierInscription = File(receipt_file, nomFichierPDF)
|
|
||||||
|
|
||||||
# Mise à jour de l'automate
|
|
||||||
updateStateMachine(di, 'saisiDI')
|
|
||||||
|
|
||||||
ficheEleve_serializer = FicheInscriptionSerializer(ficheEleve, data=ficheEleve_data)
|
|
||||||
if ficheEleve_serializer.is_valid():
|
|
||||||
di = ficheEleve_serializer.save()
|
|
||||||
return JsonResponse("Updated Successfully", safe=False)
|
|
||||||
|
|
||||||
return JsonResponse(ficheEleve_serializer.errors, safe=False)
|
|
||||||
|
|
||||||
def delete(self, request, id):
|
|
||||||
fiche_inscription = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id)
|
|
||||||
if fiche_inscription != None:
|
|
||||||
eleve = fiche_inscription.eleve
|
|
||||||
eleve.responsables.clear()
|
|
||||||
eleve.profils.clear()
|
|
||||||
eleve.delete()
|
|
||||||
clear_cache()
|
|
||||||
|
|
||||||
return JsonResponse("La suppression du dossier a été effectuée avec succès", safe=False)
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False)
|
|
||||||
|
|
||||||
class EleveView(APIView):
|
|
||||||
def get(self, request, _id):
|
|
||||||
eleve = bdd.getObject(_objectName=Eleve, _columnName='id', _value=_id)
|
|
||||||
eleve_serializer = EleveSerializer(eleve)
|
|
||||||
return JsonResponse(eleve_serializer.data, safe=False)
|
|
||||||
|
|
||||||
class ResponsableView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
lastResponsable = bdd.getLastId(Responsable)
|
|
||||||
return JsonResponse({"lastid":lastResponsable}, safe=False)
|
|
||||||
|
|
||||||
def send(request, id):
|
|
||||||
fiche_inscription = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id)
|
|
||||||
if fiche_inscription != None:
|
|
||||||
eleve = fiche_inscription.eleve
|
|
||||||
responsable = eleve.getResponsablePrincipal()
|
|
||||||
mail = responsable.mail
|
|
||||||
errorMessage = mailer.envoieDossierInscription(mail)
|
|
||||||
if errorMessage == '':
|
|
||||||
fiche_inscription.dateMAJ=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
|
|
||||||
# Mise à jour de l'automate
|
|
||||||
updateStateMachine(fiche_inscription, 'envoiDI')
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":errorMessage}, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False)
|
|
||||||
|
|
||||||
def archive(request, id):
|
|
||||||
fiche_inscription = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id)
|
|
||||||
if fiche_inscription != None:
|
|
||||||
fiche_inscription.dateMAJ=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
|
|
||||||
# Mise à jour de l'automate
|
|
||||||
updateStateMachine(fiche_inscription, 'archiveDI')
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":''}, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False)
|
|
||||||
|
|
||||||
def relance(request, id):
|
|
||||||
fiche_inscription = bdd.getObject(_objectName=FicheInscription, _columnName='eleve__id', _value=id)
|
|
||||||
if fiche_inscription != None:
|
|
||||||
eleve = fiche_inscription.eleve
|
|
||||||
responsable = eleve.getResponsablePrincipal()
|
|
||||||
mail = responsable.mail
|
|
||||||
errorMessage = mailer.envoieRelanceDossierInscription(mail, fiche_inscription.codeLienInscription)
|
|
||||||
if errorMessage == '':
|
|
||||||
fiche_inscription.etat=FicheInscription.EtatDossierInscription.DI_ENVOYE
|
|
||||||
fiche_inscription.dateMAJ=util.convertToStr(util._now(), '%d-%m-%Y %H:%M')
|
|
||||||
fiche_inscription.save()
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":errorMessage}, safe=False)
|
|
||||||
|
|
||||||
return JsonResponse({"errorMessage":'Aucun dossier d\'inscription rattaché à l\'élève'}, safe=False)
|
|
||||||
|
|
||||||
# API utilisée pour la vue parent
|
|
||||||
class ListeEnfantsView(APIView):
|
|
||||||
# Récupération des élèves d'un parent
|
|
||||||
# idProfile : identifiant du profil connecté rattaché aux fiches d'élèves
|
|
||||||
def get(self, request, _idProfile):
|
|
||||||
students = bdd.getObjects(_objectName=FicheInscription, _columnName='eleve__responsables__profilAssocie__id', _value=_idProfile)
|
|
||||||
students_serializer = FicheInscriptionByParentSerializer(students, many=True)
|
|
||||||
return JsonResponse(students_serializer.data, safe=False)
|
|
||||||
|
|
||||||
# API utilisée pour la vue de création d'un DI
|
|
||||||
class ListeElevesView(APIView):
|
|
||||||
# Récupération de la liste des élèves inscrits ou en cours d'inscriptions
|
|
||||||
def get(self, request):
|
|
||||||
students = bdd.getAllObjects(_objectName=Eleve)
|
|
||||||
students_serializer = EleveByDICreationSerializer(students, many=True)
|
|
||||||
return JsonResponse(students_serializer.data, safe=False)
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.core.validators import EmailValidator
|
|
||||||
|
|
||||||
class Profil(AbstractUser):
|
|
||||||
class Droits(models.IntegerChoices):
|
|
||||||
PROFIL_UNDEFINED = -1, _('Profil non défini')
|
|
||||||
PROFIL_ECOLE = 0, _('Profil école')
|
|
||||||
PROFIL_PARENT = 1, _('Profil parent')
|
|
||||||
PROFIL_ADMIN = 2, _('Profil administrateur')
|
|
||||||
|
|
||||||
email = models.EmailField(max_length=255, unique=True, default="", validators=[EmailValidator()])
|
|
||||||
|
|
||||||
USERNAME_FIELD = 'email'
|
|
||||||
|
|
||||||
REQUIRED_FIELDS = ('password', )
|
|
||||||
|
|
||||||
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)
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
from GestionLogin.models import Profil
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
class ProfilSerializer(serializers.ModelSerializer):
|
|
||||||
id = serializers.IntegerField(required=False)
|
|
||||||
password = serializers.CharField(write_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Profil
|
|
||||||
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'estConnecte', 'droit', 'username', 'is_active']
|
|
||||||
extra_kwargs = {'password': {'write_only': True}}
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
user = Profil(
|
|
||||||
username=validated_data['username'],
|
|
||||||
email=validated_data['email'],
|
|
||||||
is_active=validated_data['is_active'],
|
|
||||||
droit=validated_data['droit']
|
|
||||||
)
|
|
||||||
user.set_password(validated_data['password'])
|
|
||||||
user.save()
|
|
||||||
return user
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
ret = super().to_representation(instance)
|
|
||||||
ret['password'] = '********'
|
|
||||||
return ret
|
|
||||||
@ -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>
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
from django.urls import path, re_path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
import GestionLogin.views
|
|
||||||
from GestionLogin.views import ProfilView, ListProfilView, SessionView, LoginView, SubscribeView, NewPasswordView, ResetPasswordView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
re_path(r'^csrf$', GestionLogin.views.csrf, name='csrf'),
|
|
||||||
|
|
||||||
re_path(r'^login$', LoginView.as_view(), name="login"),
|
|
||||||
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$', GestionLogin.views.infoSession, name='infoSession'),
|
|
||||||
|
|
||||||
re_path(r'^profils$', ListProfilView.as_view(), name="profil"),
|
|
||||||
re_path(r'^profil$', ProfilView.as_view(), name="profil"),
|
|
||||||
re_path(r'^profil/([0-9]+)$', ProfilView.as_view(), name="profil"),
|
|
||||||
|
|
||||||
# Test SESSION VIEW
|
|
||||||
re_path(r'^session$', SessionView.as_view(), name="session"),
|
|
||||||
]
|
|
||||||
@ -1,264 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import login, authenticate, get_user_model
|
|
||||||
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 datetime import datetime
|
|
||||||
import jwt
|
|
||||||
import json
|
|
||||||
|
|
||||||
from . import validator
|
|
||||||
from .models import Profil
|
|
||||||
|
|
||||||
from GestionInscriptions.models import FicheInscription
|
|
||||||
from GestionInscriptions.serializers import ProfilSerializer
|
|
||||||
from GestionInscriptions.signals import clear_cache
|
|
||||||
import GestionInscriptions.mailManager as mailer
|
|
||||||
import GestionInscriptions.util as util
|
|
||||||
|
|
||||||
from N3wtSchool import bdd, error
|
|
||||||
|
|
||||||
def csrf(request):
|
|
||||||
token = get_token(request)
|
|
||||||
return JsonResponse({'csrfToken': token})
|
|
||||||
|
|
||||||
class SessionView(APIView):
|
|
||||||
|
|
||||||
def post(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 = Profil.objects.get(id=user_id)
|
|
||||||
|
|
||||||
response_data = {
|
|
||||||
'user': {
|
|
||||||
'id': user.id,
|
|
||||||
'email': user.email,
|
|
||||||
'role': user.droit, # Assure-toi que le champ 'droit' existe et contient le rôle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JsonResponse(response_data, status=status.HTTP_200_OK)
|
|
||||||
except jwt.ExpiredSignatureError:
|
|
||||||
return JsonResponse({"error": "Token has expired"}, status=status.HTTP_401_UNAUTHORIZED)
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
|
|
||||||
|
|
||||||
class ListProfilView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
profilsList = bdd.getAllObjects(_objectName=Profil)
|
|
||||||
profils_serializer = ProfilSerializer(profilsList, many=True)
|
|
||||||
return JsonResponse(profils_serializer.data, safe=False)
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class ProfilView(APIView):
|
|
||||||
def get(self, request, _id):
|
|
||||||
profil=bdd.getObject(Profil, "id", _id)
|
|
||||||
profil_serializer=ProfilSerializer(profil)
|
|
||||||
return JsonResponse(profil_serializer.data, safe=False)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
profil_data=JSONParser().parse(request)
|
|
||||||
print(f'{profil_data}')
|
|
||||||
profil_serializer = ProfilSerializer(data=profil_data)
|
|
||||||
|
|
||||||
if profil_serializer.is_valid():
|
|
||||||
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 = Profil.objects.get(id=_id)
|
|
||||||
profil_serializer = ProfilSerializer(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 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":Profil.Droits.PROFIL_UNDEFINED, "username":""}, safe=False)
|
|
||||||
|
|
||||||
@method_decorator(csrf_protect, name='dispatch')
|
|
||||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
|
||||||
class LoginView(APIView):
|
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
return JsonResponse({
|
|
||||||
'errorFields':'',
|
|
||||||
'errorMessage':'',
|
|
||||||
'profil':0,
|
|
||||||
}, safe=False)
|
|
||||||
|
|
||||||
def post(self, 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]
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
#'jwtToken':jwt_token if profil != -1 else ''
|
|
||||||
}, safe=False)
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
retourErreur = error.returnMessage[error.BAD_URL]
|
|
||||||
retour = ''
|
|
||||||
newProfilConnection=JSONParser().parse(request)
|
|
||||||
|
|
||||||
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(Profil.objects.all(), newProfilConnection.get('email'))
|
|
||||||
if profil == 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)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
profil.set_password(newProfilConnection.get('password1'))
|
|
||||||
profil.is_active = True
|
|
||||||
profil.full_clean()
|
|
||||||
profil.save()
|
|
||||||
clear_cache()
|
|
||||||
retour = error.returnMessage[error.MESSAGE_ACTIVATION_PROFILE]
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
retourErreur = error.returnMessage[error.BAD_URL]
|
|
||||||
retour = ''
|
|
||||||
newProfilConnection=JSONParser().parse(request)
|
|
||||||
|
|
||||||
validatorNewPassword = validator.ValidatorNewPassword(data=newProfilConnection)
|
|
||||||
validationOk, errorFields = validatorNewPassword.validate()
|
|
||||||
if validationOk:
|
|
||||||
|
|
||||||
profil = bdd.getProfile(Profil.objects.all(), newProfilConnection.get('email'))
|
|
||||||
if profil == 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)
|
|
||||||
|
|
||||||
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]
|
|
||||||
retour = ''
|
|
||||||
newProfilConnection=JSONParser().parse(request)
|
|
||||||
|
|
||||||
validatorResetPassword = validator.ValidatorResetPassword(data=newProfilConnection)
|
|
||||||
validationOk, errorFields = validatorResetPassword.validate()
|
|
||||||
|
|
||||||
profil = bdd.getObject(Profil, "code", _uuid)
|
|
||||||
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)
|
|
||||||
elif validationOk:
|
|
||||||
retour = error.returnMessage[error.PASSWORD_CHANGED]
|
|
||||||
|
|
||||||
profil.set_password(newProfilConnection.get('password1'))
|
|
||||||
profil.code = ''
|
|
||||||
profil.datePeremption = ''
|
|
||||||
profil.save()
|
|
||||||
clear_cache()
|
|
||||||
retourErreur=''
|
|
||||||
|
|
||||||
return JsonResponse({'message':retour, "errorMessage":retourErreur, "errorFields":errorFields}, 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)
|
||||||
101
Back-End/GestionMessagerie/migrations/0001_initial.py
Normal file
101
Back-End/GestionMessagerie/migrations/0001_initial.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-11-30 11:02
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
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='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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from Auth.models import Profile
|
||||||
from django.conf import settings
|
from django.utils import timezone
|
||||||
from GestionLogin.models import Profil
|
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):
|
class Messagerie(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
objet = models.CharField(max_length=200, default="", blank=True)
|
objet = models.CharField(max_length=200, default="", blank=True)
|
||||||
emetteur = models.ForeignKey(Profil, on_delete=models.PROTECT, related_name='messages_envoyes')
|
emetteur = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_envoyes')
|
||||||
destinataire = models.ForeignKey(Profil, on_delete=models.PROTECT, related_name='messages_recus')
|
destinataire = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='messages_recus')
|
||||||
corpus = models.CharField(max_length=200, default="", blank=True)
|
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):
|
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,13 +1,265 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from GestionLogin.models import Profil
|
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):
|
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()
|
destinataire_profil = serializers.SerializerMethodField()
|
||||||
emetteur_profil = serializers.SerializerMethodField()
|
emetteur_profil = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Messagerie
|
model = Messagerie
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
read_only_fields = ['date_envoi']
|
||||||
|
|
||||||
def get_destinataire_profil(self, obj):
|
def get_destinataire_profil(self, obj):
|
||||||
return obj.destinataire.email
|
return obj.destinataire.email
|
||||||
|
|||||||
@ -1,9 +1,22 @@
|
|||||||
from django.urls import path, re_path
|
from django.urls import path
|
||||||
|
from .views import (
|
||||||
from GestionMessagerie.views import MessagerieView, MessageView
|
InstantConversationListView, InstantConversationCreateView, InstantConversationDeleteView,
|
||||||
|
InstantMessageListView, InstantMessageCreateView,
|
||||||
|
InstantMarkAsReadView, FileUploadView,
|
||||||
|
InstantRecipientSearchView
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^messagerie/([0-9]+)$', MessagerieView.as_view(), name="messagerie"),
|
# URLs pour messagerie instantanée
|
||||||
re_path(r'^message$', MessageView.as_view(), name="message"),
|
path('conversations/', InstantConversationListView.as_view(), name='conversations'),
|
||||||
re_path(r'^message/([0-9]+)$', MessageView.as_view(), name="message"),
|
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.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):
|
conversations = Conversation.objects.filter(
|
||||||
def get(self, request, _idProfile):
|
participants__participant=user,
|
||||||
messagesList = bdd.getObjects(_objectName=Messagerie, _columnName='destinataire__id', _value=_idProfile)
|
participants__is_active=True,
|
||||||
messages_serializer = MessageSerializer(messagesList, many=True)
|
is_active=True
|
||||||
return JsonResponse(messages_serializer.data, safe=False)
|
).distinct().order_by('-last_activity')
|
||||||
|
|
||||||
class MessageView(APIView):
|
serializer = ConversationSerializer(conversations, many=True, context={'user': user})
|
||||||
def get(self, request, _id):
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
message=bdd.getObject(Messagerie, "id", _id)
|
except Profile.DoesNotExist:
|
||||||
message_serializer=MessageSerializer(message)
|
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
return JsonResponse(message_serializer.data, safe=False)
|
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):
|
def post(self, request):
|
||||||
message_data=JSONParser().parse(request)
|
serializer = ConversationCreateSerializer(data=request.data)
|
||||||
message_serializer = MessageSerializer(data=message_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():
|
class InstantMessageListView(APIView):
|
||||||
message_serializer.save()
|
"""
|
||||||
|
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')
|
||||||
|
|
||||||
return JsonResponse('Nouveau Message ajouté', safe=False)
|
# 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-11-30 11:02
|
||||||
|
|
||||||
|
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
@ -2,7 +2,7 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from GestionLogin.models import Profil
|
from Auth.models import Profile
|
||||||
|
|
||||||
class TypeNotif(models.IntegerChoices):
|
class TypeNotif(models.IntegerChoices):
|
||||||
NOTIF_NONE = 0, _('Aucune notification')
|
NOTIF_NONE = 0, _('Aucune notification')
|
||||||
@ -10,7 +10,7 @@ class TypeNotif(models.IntegerChoices):
|
|||||||
NOTIF_DI = 2, _('Le dossier d\'inscription a été mis à jour')
|
NOTIF_DI = 2, _('Le dossier d\'inscription a été mis à jour')
|
||||||
|
|
||||||
class Notification(models.Model):
|
class Notification(models.Model):
|
||||||
user = models.ForeignKey(Profil, on_delete=models.PROTECT)
|
user = models.ForeignKey(Profile, on_delete=models.PROTECT)
|
||||||
message = models.CharField(max_length=255)
|
message = models.CharField(max_length=255)
|
||||||
is_read = models.BooleanField(default=False)
|
is_read = models.BooleanField(default=False)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@ -2,22 +2,22 @@ from django.db.models.signals import post_save
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from .models import Notification, TypeNotif
|
from .models import Notification, TypeNotif
|
||||||
from GestionMessagerie.models import Messagerie
|
from GestionMessagerie.models import Messagerie
|
||||||
from GestionInscriptions.models import FicheInscription
|
from Subscriptions.models import RegistrationForm
|
||||||
|
|
||||||
@receiver(post_save, sender=Messagerie)
|
# @receiver(post_save, sender=Messagerie)
|
||||||
def notification_MESSAGE(sender, instance, created, **kwargs):
|
# def notification_MESSAGE(sender, instance, created, **kwargs):
|
||||||
if created:
|
# if created:
|
||||||
Notification.objects.create(
|
# Notification.objects.create(
|
||||||
user=instance.destinataire,
|
# user=instance.destinataire,
|
||||||
message=(TypeNotif.NOTIF_MESSAGE).label,
|
# message=(TypeNotif.NOTIF_MESSAGE).label,
|
||||||
typeNotification=TypeNotif.NOTIF_MESSAGE
|
# typeNotification=TypeNotif.NOTIF_MESSAGE
|
||||||
)
|
# )
|
||||||
|
|
||||||
@receiver(post_save, sender=FicheInscription)
|
# @receiver(post_save, sender=RegistrationForm)
|
||||||
def notification_DI(sender, instance, created, **kwargs):
|
# def notification_DI(sender, instance, created, **kwargs):
|
||||||
for responsable in instance.eleve.responsables.all():
|
# for responsable in instance.student.guardians.all():
|
||||||
Notification.objects.create(
|
# Notification.objects.create(
|
||||||
user=responsable.profilAssocie,
|
# user=responsable.associated_profile,
|
||||||
message=(TypeNotif.NOTIF_DI).label,
|
# message=(TypeNotif.NOTIF_DI).label,
|
||||||
typeNotification=TypeNotif.NOTIF_DI
|
# typeNotification=TypeNotif.NOTIF_DI
|
||||||
)
|
# )
|
||||||
|
|||||||
@ -3,5 +3,5 @@ from django.urls import path, re_path
|
|||||||
from GestionNotification.views import NotificationView
|
from GestionNotification.views import NotificationView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^notification$', NotificationView.as_view(), name="notification"),
|
re_path(r'^notifications$', NotificationView.as_view(), name="notifications"),
|
||||||
]
|
]
|
||||||
@ -3,7 +3,7 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
from GestionInscriptions.serializers import NotificationSerializer
|
from Subscriptions.serializers import NotificationSerializer
|
||||||
|
|
||||||
from N3wtSchool import bdd
|
from N3wtSchool import bdd
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,40 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
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')
|
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)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from GestionInscriptions.models import FicheInscription, Profil, Eleve
|
from django.http import JsonResponse
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from Subscriptions.models import RegistrationForm, Student
|
||||||
|
from Auth.models import Profile
|
||||||
|
|
||||||
|
logger = logging.getLogger('N3wtSchool')
|
||||||
|
|
||||||
def getAllObjects(_objectName):
|
def getAllObjects(_objectName):
|
||||||
result = _objectName.objects.all()
|
result = _objectName.objects.all()
|
||||||
if not result:
|
if not result:
|
||||||
logging.warning("Aucun résultat n'a été trouvé - " + _objectName.__name__)
|
logger.warning("Aucun résultat n'a été trouvé - " + _objectName.__name__)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def getObject(_objectName, _columnName, _value):
|
def getObject(_objectName, _columnName, _value):
|
||||||
@ -13,7 +18,7 @@ def getObject(_objectName, _columnName, _value):
|
|||||||
try :
|
try :
|
||||||
result = _objectName.objects.get(**{_columnName: _value})
|
result = _objectName.objects.get(**{_columnName: _value})
|
||||||
except _objectName.DoesNotExist:
|
except _objectName.DoesNotExist:
|
||||||
logging.error("Aucun résultat n'a été trouvé - " + _objectName.__name__ + " (" + _columnName + "=" + str(_value) + ")")
|
logger.error("Aucun résultat n'a été trouvé - " + _objectName.__name__ + " (" + _columnName + "=" + str(_value) + ")")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -22,7 +27,7 @@ def getObjects(_objectName, _columnName, _value, _reverseCondition=False):
|
|||||||
try :
|
try :
|
||||||
results = _objectName.objects.filter(**{_columnName: _value}) if _reverseCondition == False else _objectName.objects.filter(~Q(**{_columnName: _value}))
|
results = _objectName.objects.filter(**{_columnName: _value}) if _reverseCondition == False else _objectName.objects.filter(~Q(**{_columnName: _value}))
|
||||||
except _objectName.DoesNotExist:
|
except _objectName.DoesNotExist:
|
||||||
logging.error("Aucun résultat n'a été trouvé - " + _objectName.__name__ + " (" + _columnName + "=" + str(_value) + ")")
|
logger.error("Aucun résultat n'a été trouvé - " + _objectName.__name__ + " (" + _columnName + "=" + str(_value) + ")")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@ -41,46 +46,62 @@ def getProfile(objectList, valueToCheck):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def getEleveByCodeFI(_codeFI):
|
def getEleveByCodeFI(_codeFI):
|
||||||
eleve = None
|
student = None
|
||||||
ficheInscriptions_List=getAllObjects(FicheInscription)
|
ficheInscriptions_List=getAllObjects(RegistrationForm)
|
||||||
for fi in ficheInscriptions_List:
|
for rf in ficheInscriptions_List:
|
||||||
if fi.codeLienInscription == _codeFI:
|
if rf.codeLienInscription == _codeFI:
|
||||||
eleve = fi.eleve
|
student = rf.student
|
||||||
return eleve
|
return student
|
||||||
|
|
||||||
def getLastId(_object):
|
def getLastId(_object):
|
||||||
result = 1
|
result = 1
|
||||||
try:
|
try:
|
||||||
result = _object.objects.latest('id').id
|
result = _object.objects.latest('id').id
|
||||||
except:
|
except:
|
||||||
logging.warning("Aucun résultat n'a été trouvé - ")
|
logger.warning("Aucun résultat n'a été trouvé - ")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def searchObjects(_objectName, _searchTerm, _excludeState=None):
|
def searchObjects(_objectName, _searchTerm=None, _excludeStates=None):
|
||||||
"""
|
"""
|
||||||
Recherche générique sur les objets avec possibilité d'exclure certains états
|
Recherche générique sur les objets avec possibilité d'exclure certains états
|
||||||
_objectName: Classe du modèle
|
_objectName: SchoolClass du modèle
|
||||||
_searchTerm: Terme de recherche
|
_searchTerm: Terme de recherche
|
||||||
_excludeState: État à exclure de la recherche (optionnel)
|
_excludeStates: Liste d'état à exclure de la recherche (optionnel)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
query = _objectName.objects.all()
|
query = _objectName.objects.all()
|
||||||
|
|
||||||
# Si on a un état à exclure
|
# Si on a un état à exclure
|
||||||
if _excludeState is not None:
|
if _excludeStates is not None:
|
||||||
query = query.filter(etat__lt=_excludeState)
|
query = query.exclude(status__in=_excludeStates)
|
||||||
|
|
||||||
# Si on a un terme de recherche
|
# Si on a un terme de recherche
|
||||||
if _searchTerm and _searchTerm.strip():
|
if _searchTerm and _searchTerm.strip():
|
||||||
terms = _searchTerm.lower().strip().split()
|
terms = _searchTerm.lower().strip().split()
|
||||||
for term in terms:
|
for term in terms:
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
Q(eleve__nom__icontains=term) |
|
Q(student__last_name__icontains=term) |
|
||||||
Q(eleve__prenom__icontains=term)
|
Q(student__first_name__icontains=term)
|
||||||
)
|
)
|
||||||
|
|
||||||
return query.order_by('eleve__nom', 'eleve__prenom')
|
return query.order_by('student__last_name', 'student__first_name')
|
||||||
|
|
||||||
except _objectName.DoesNotExist:
|
except _objectName.DoesNotExist:
|
||||||
logging.error(f"Aucun résultat n'a été trouvé - {_objectName.__name__} (recherche: {_searchTerm})")
|
logging.error(f"Aucun résultat n'a été trouvé - {_objectName.__name__} (recherche: {_searchTerm})")
|
||||||
return None
|
return 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:
|
||||||
|
related_obj.delete()
|
||||||
|
obj_name = str(obj) # Utiliser la méthode __str__
|
||||||
|
obj.delete()
|
||||||
|
return JsonResponse({'message': f'La suppression de l\'objet {obj_name} a été effectuée avec succès'}, safe=False)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
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 typing import Final
|
||||||
|
from N3wtSchool import settings
|
||||||
|
|
||||||
WRONG_ID: Final = 1
|
WRONG_ID: Final = 1
|
||||||
INCOMPLETE: Final = 2
|
INCOMPLETE: Final = 2
|
||||||
@ -8,11 +9,14 @@ DIFFERENT_PASWWORD: Final = 5
|
|||||||
PROFIL_NOT_EXISTS: Final = 6
|
PROFIL_NOT_EXISTS: Final = 6
|
||||||
MESSAGE_REINIT_PASSWORD: Final = 7
|
MESSAGE_REINIT_PASSWORD: Final = 7
|
||||||
EXPIRED_URL: Final = 8
|
EXPIRED_URL: Final = 8
|
||||||
PASSWORD_CHANGED: Final = 8
|
PASSWORD_CHANGED: Final = 9
|
||||||
WRONG_MAIL_FORMAT: Final = 9
|
WRONG_MAIL_FORMAT: Final = 10
|
||||||
PROFIL_INACTIVE: Final = 10
|
PROFIL_INACTIVE: Final = 11
|
||||||
MESSAGE_ACTIVATION_PROFILE: Final = 11
|
MESSAGE_ACTIVATION_PROFILE: Final = 12
|
||||||
PROFIL_ACTIVE: 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 = {
|
returnMessage = {
|
||||||
WRONG_ID:'Identifiants invalides',
|
WRONG_ID:'Identifiants invalides',
|
||||||
@ -22,7 +26,7 @@ returnMessage = {
|
|||||||
DIFFERENT_PASWWORD: 'Les mots de passe ne correspondent pas',
|
DIFFERENT_PASWWORD: 'Les mots de passe ne correspondent pas',
|
||||||
PROFIL_NOT_EXISTS: 'Aucun profil associé à cet utilisateur',
|
PROFIL_NOT_EXISTS: 'Aucun profil associé à cet utilisateur',
|
||||||
MESSAGE_REINIT_PASSWORD: 'Un mail a été envoyé à l\'adresse \'%s\'',
|
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é',
|
PASSWORD_CHANGED: 'Le mot de passe a été réinitialisé',
|
||||||
WRONG_MAIL_FORMAT: 'L\'adresse mail est mal formatée',
|
WRONG_MAIL_FORMAT: 'L\'adresse mail est mal formatée',
|
||||||
PROFIL_INACTIVE: 'Le profil n\'est pas actif',
|
PROFIL_INACTIVE: 'Le profil n\'est pas actif',
|
||||||
|
|||||||
210
Back-End/N3wtSchool/mailManager.py
Normal file
210
Back-End/N3wtSchool/mailManager.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
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)
|
||||||
|
logger.info(f"Establishment trouvé: {establishment.name} (ID: {id_establishement})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Récupérer les paramètres SMTP associés à l'établissement
|
||||||
|
smtp_settings = SMTPSettings.objects.get(establishment=establishment)
|
||||||
|
logger.info(f"Paramètres SMTP trouvés pour {establishment.name}: {smtp_settings.smtp_server}:{smtp_settings.smtp_port}")
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
logger.warning(f"Aucun paramètre SMTP spécifique trouvé pour l'établissement {establishment.name} (ID: {id_establishement})")
|
||||||
|
# Aucun paramètre SMTP spécifique, retournera None
|
||||||
|
return None
|
||||||
|
except Establishment.DoesNotExist:
|
||||||
|
logger.error(f"Aucun établissement trouvé avec l'ID {id_establishement}")
|
||||||
|
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
|
||||||
|
logger.info(f"Utilisation de la connexion SMTP spécifique: {username}")
|
||||||
|
else:
|
||||||
|
from_email = settings.EMAIL_HOST_USER
|
||||||
|
logger.info(f"Utilisation de la configuration SMTP par défaut: {from_email}")
|
||||||
|
|
||||||
|
logger.info(f"From email: {from_email}")
|
||||||
|
logger.info(f"Configuration par défaut - Host: {settings.EMAIL_HOST}, Port: {settings.EMAIL_PORT}, Use TLS: {settings.EMAIL_USE_TLS}")
|
||||||
|
|
||||||
|
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"Settings : {connection}")
|
||||||
|
logger.error(f"Settings : {connection}")
|
||||||
|
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
|
from xhtml2pdf import pisa
|
||||||
|
|
||||||
|
class PDFResult:
|
||||||
|
def __init__(self, content):
|
||||||
|
self.content = content
|
||||||
|
|
||||||
def render_to_pdf(template_src, context_dict={}):
|
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)
|
template = get_template(template_src)
|
||||||
html = template.render(context_dict)
|
html = template.render(context_dict)
|
||||||
result = BytesIO()
|
result = BytesIO()
|
||||||
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
|
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
|
||||||
|
|
||||||
if pdf.err:
|
if pdf.err:
|
||||||
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
|
# Lever une exception ou retourner None en cas d'erreur
|
||||||
return HttpResponse(result.getvalue(), content_type='application/pdf')
|
raise ValueError("Erreur lors de la génération du PDF.")
|
||||||
|
|
||||||
|
# Retourner le contenu du PDF en mémoire
|
||||||
|
return PDFResult(result.getvalue())
|
||||||
@ -13,31 +13,43 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
import os
|
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'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
MEDIA_URL = '/data/'
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'data')
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/GestionInscriptions/fichesInscriptions'
|
BASE_URL = os.getenv('BASE_URL', 'http://localhost:3000')
|
||||||
|
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = '/Subscriptions/registerForms'
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = os.getenv('DJANGO_DEBUG', True)
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'GestionInscriptions.apps.GestioninscriptionsConfig',
|
'Common.apps.CommonConfig',
|
||||||
'GestionLogin.apps.GestionloginConfig',
|
'Subscriptions.apps.GestioninscriptionsConfig',
|
||||||
|
'Auth.apps.GestionloginConfig',
|
||||||
'GestionMessagerie.apps.GestionMessagerieConfig',
|
'GestionMessagerie.apps.GestionMessagerieConfig',
|
||||||
|
'GestionEmail.apps.GestionEmailConfig',
|
||||||
'GestionNotification.apps.GestionNotificationConfig',
|
'GestionNotification.apps.GestionNotificationConfig',
|
||||||
'GestionEnseignants.apps.GestionenseignantsConfig',
|
'School.apps.SchoolConfig',
|
||||||
|
'Planning.apps.PlanningConfig',
|
||||||
|
'Establishment.apps.EstablishmentConfig',
|
||||||
|
'Settings.apps.SettingsConfig',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@ -49,17 +61,20 @@ INSTALLED_APPS = [
|
|||||||
'django_celery_beat',
|
'django_celery_beat',
|
||||||
'N3wtSchool',
|
'N3wtSchool',
|
||||||
'drf_yasg',
|
'drf_yasg',
|
||||||
|
'rest_framework_simplejwt',
|
||||||
|
'channels',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware', # Déplacez ici, avant CorsMiddleware
|
'django.middleware.common.CommonMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'N3wtSchool.middleware.ContentSecurityPolicyMiddleware'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -92,6 +107,73 @@ SESSION_CACHE_ALIAS = 'default'
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'N3wtSchool.wsgi.application'
|
WSGI_APPLICATION = 'N3wtSchool.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
#LOGGING
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": { # Ajout des formateurs
|
||||||
|
"verbose": {
|
||||||
|
"format": "{asctime} [{levelname}] [{name}] {module}.{funcName} - {message}",
|
||||||
|
"style": "{",
|
||||||
|
"datefmt": "%Y-%m-%d %H:%M:%S"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "verbose", # Utilisation du formateur
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("ROOT_LOG_LEVEL", "INFO"),
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
|
||||||
|
"celery": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("CELERY_LOG_LEVEL", "INFO"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"N3wtSchool": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("N3WTSCHOOL_LOG_LEVEL", "INFO"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"GestionNotification": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("GESTION_NOTIFICATION_LOG_LEVEL", "INFO"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"Auth": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("GESTION_LOGIN_LOG_LEVEL", "INFO"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"Subscriptions": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": os.getenv("GESTION_INSCRIPTIONS_LOG_LEVEL", "DEBUG"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"GestionMessagerie": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"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"),
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@ -127,8 +209,6 @@ USE_I18N = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
@ -147,72 +227,76 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
#################### Application Settings ##############################
|
#################### Application Settings ##############################
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
with open('GestionInscriptions/Configuration/application.json', 'r') as f:
|
|
||||||
jsonObject = json.load(f)
|
|
||||||
|
|
||||||
DJANGO_SUPERUSER_PASSWORD='admin'
|
# Configuration de l'email de l'application
|
||||||
DJANGO_SUPERUSER_USERNAME='admin'
|
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.com')
|
||||||
DJANGO_SUPERUSER_EMAIL='admin@n3wtschool.com'
|
EMAIL_PORT = os.getenv('EMAIL_PORT', 587)
|
||||||
|
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
|
||||||
EMAIL_HOST='smtp.gmail.com'
|
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
|
||||||
EMAIL_PORT=587
|
|
||||||
EMAIL_HOST_USER=jsonObject['mailFrom']
|
|
||||||
EMAIL_HOST_PASSWORD=jsonObject['password']
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'true').lower() == 'true'
|
||||||
EMAIL_USE_SSL = False
|
EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', 'false').lower() == 'true'
|
||||||
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'
|
|
||||||
|
|
||||||
DOCUMENT_DIR = 'documents'
|
DOCUMENT_DIR = 'documents'
|
||||||
|
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
# Configuration CORS temporaire pour debug
|
||||||
CORS_ALLOW_ALL_HEADERS = True
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
# Configuration CORS spécifique pour la production
|
||||||
"http://localhost:3000"
|
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 = [
|
# Méthodes HTTP autorisées
|
||||||
"http://localhost:3000", # Front Next.js
|
CORS_ALLOWED_METHODS = [
|
||||||
"http://localhost:8080" # Insomnia
|
'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_HTTPONLY = False
|
||||||
CSRF_COOKIE_SECURE = False
|
CSRF_COOKIE_SECURE = os.getenv('CSRF_COOKIE_SECURE', 'false').lower() == 'true'
|
||||||
CSRF_COOKIE_NAME = 'csrftoken'
|
CSRF_COOKIE_NAME = 'csrftoken'
|
||||||
|
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN', '')
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
TZ_APPLI = 'Europe/Paris'
|
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 = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
"NAME": "school",
|
"NAME": DB_NAME,
|
||||||
"USER": "postgres",
|
"USER": DB_USER,
|
||||||
"PASSWORD": "postgres",
|
"PASSWORD": DB_PASSWORD,
|
||||||
"HOST": "database",
|
"HOST": DB_HOST,
|
||||||
"PORT": "5432",
|
"PORT": DB_PORT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'GestionLogin.Profil'
|
AUTH_USER_MODEL = 'Auth.Profile'
|
||||||
AUTHENTICATION_BACKENDS = ('GestionLogin.backends.EmailBackend', )
|
AUTHENTICATION_BACKENDS = ('Auth.backends.EmailBackend', )
|
||||||
SILENCED_SYSTEM_CHECKS = ["auth.W004"]
|
SILENCED_SYSTEM_CHECKS = ["auth.W004"]
|
||||||
|
|
||||||
EXPIRATION_URL_NB_DAYS = 7
|
EXPIRATION_URL_NB_DAYS = 7
|
||||||
@ -221,12 +305,17 @@ DATE_FORMAT = '%d-%m-%Y %H:%M'
|
|||||||
|
|
||||||
EXPIRATION_SESSION_NB_SEC = 10
|
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
|
NB_MAX_PAGE = 100
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_PAGINATION_CLASS': 'GestionInscriptions.pagination.CustomPagination',
|
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
|
||||||
'PAGE_SIZE': NB_RESULT_PER_PAGE
|
'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'
|
CELERY_BROKER_URL = 'redis://redis:6379/0'
|
||||||
@ -237,11 +326,37 @@ CELERY_RESULT_SERIALIZER = 'json'
|
|||||||
CELERY_TIMEZONE = 'Europe/Paris'
|
CELERY_TIMEZONE = 'Europe/Paris'
|
||||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
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_HOST = 'redis'
|
||||||
REDIS_PORT = 6379
|
REDIS_PORT = 6379
|
||||||
REDIS_DB = 0
|
REDIS_DB = 0
|
||||||
REDIS_PASSWORD = None
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Django Channels Configuration
|
||||||
|
ASGI_APPLICATION = 'N3wtSchool.asgi.application'
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||||
|
'CONFIG': {
|
||||||
|
"hosts": [('redis', 6379)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -16,6 +16,6 @@ def setup_periodic_tasks(sender, **kwargs):
|
|||||||
PeriodicTask.objects.get_or_create(
|
PeriodicTask.objects.get_or_create(
|
||||||
interval=schedule, # Utiliser l'intervalle défini ci-dessus
|
interval=schedule, # Utiliser l'intervalle défini ci-dessus
|
||||||
name='Tâche périodique toutes les 5 secondes',
|
name='Tâche périodique toutes les 5 secondes',
|
||||||
task='GestionInscriptions.tasks.check_for_signature_deadlines', # Remplacer par le nom de ta tâche
|
task='Subscriptions.tasks.check_for_signature_deadlines', # Remplacer par le nom de ta tâche
|
||||||
kwargs=json.dumps({}) # Si nécessaire, ajoute
|
kwargs=json.dumps({}) # Si nécessaire, ajoute
|
||||||
)
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user