chore: Initial Commit

feat: Gestion des inscriptions [#1]
feat(frontend): Création des vues pour le paramétrage de l'école [#2]
feat: Gestion du login [#6]
fix: Correction lors de la migration des modèle [#8]
feat: Révision du menu principal [#9]
feat: Ajout d'un footer [#10]
feat: Création des dockers compose pour les environnements de
développement et de production [#12]
doc(ci): Mise en place de Husky et d'un suivi de version automatique [#14]
This commit is contained in:
Luc SORIGNET
2024-11-18 10:02:58 +01:00
committed by N3WT DE COMPET
commit af0cd1c840
228 changed files with 22694 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
Back-End/*/Configuration/application.json
.venv/
__pycache__/
node_modules/
Back-End/*/migrations/*
Back-End/documents
Back-End/*.dmp
Back-End/staticfiles
hardcoded-strings-report.md

1
.husky/commit-msg Normal file
View File

@ -0,0 +1 @@
npx --no -- commitlint --edit $1

0
.husky/pre-commit Normal file
View File

View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
node scripts/prepare-commit-msg.js "$1" "$2"

27
.versionrc Normal file
View File

@ -0,0 +1,27 @@
{
"header": "# Changelog\n\nToutes les modifications notables apportées à ce projet seront documentées dans ce fichier.\n",
"tagPrefix": "",
"bumpFiles": [
{
"filename": "package.json",
"type": "json"
},
{
"filename": "Front-End/package.json",
"updater": "scripts/update-version.js"
},
{
"filename": "Back-End/__version__.py",
"updater": "scripts/update-version.js"
}
],
"types": [
{ "type": "feat", "section": "Nouvelles fonctionnalités", "hidden": false },
{ "type": "fix", "section": "Corrections de bugs", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Mises en forme", "hidden": true },
{ "type": "refactor", "section": "Refactorisations", "hidden": false },
{ "type": "test", "section": "Tests", "hidden": true },
{ "type": "chore", "section": "Tâches diverses", "hidden": true }
]
}

20
Back-End/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# Dockerfile
# The first instruction is what image we want to base our container on
# We Use an official Python runtime as a parent image
FROM python:3.12.7
# Allows docker to cache installed dependencies between builds
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
# Mounts the application code to the image
COPY . .
WORKDIR /Back-End
EXPOSE 8080
ENV DJANGO_SETTINGS_MODULE N3wtSchool.settings
ENV DJANGO_SUPERUSER_PASSWORD=admin
ENV DJANGO_SUPERUSER_USERNAME=admin
ENV DJANGO_SUPERUSER_EMAIL=admin@n3wtschool.com

View File

@ -0,0 +1 @@
default_app_config = 'GestionEnseignants.apps.GestionenseignantsConfig'

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GestionenseignantsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'GestionEnseignants'

View File

@ -0,0 +1,32 @@
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

View File

@ -0,0 +1,83 @@
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']

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,17 @@
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"),
]

View File

@ -0,0 +1,180 @@
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)

View File

@ -0,0 +1,4 @@
{
"mailFrom":"",
"password":""
}

View File

@ -0,0 +1,63 @@
{
"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"
}
]
}

View File

@ -0,0 +1,18 @@
{
"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"
]
}

View File

@ -0,0 +1 @@
default_app_config = 'GestionInscriptions.apps.GestionInscriptionsConfig'

View File

@ -0,0 +1,11 @@
from django.contrib import admin
from .models import *
admin.site.register(Eleve)
admin.site.register(Responsable)
class EleveAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.user = request.user
super().save_model(request, obj, form, change)

View File

@ -0,0 +1,10 @@
from django.apps import AppConfig
from django.conf import settings
class GestioninscriptionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'GestionInscriptions'
def ready(self):
from GestionInscriptions.signals import clear_cache
clear_cache()

View File

@ -0,0 +1,45 @@
# 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

View File

@ -0,0 +1,74 @@
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)

View File

@ -0,0 +1,123 @@
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

View File

@ -0,0 +1,20 @@
from rest_framework.pagination import PageNumberPagination
from N3wtSchool import settings
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = settings.NB_MAX_PAGE
page_size = settings.NB_RESULT_PER_PAGE
def get_paginated_response(self, data):
return ({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'page_size': self.page_size,
'max_page_size' : self.max_page_size,
'fichesInscriptions': data }
)

View File

@ -0,0 +1,176 @@
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__'

View File

@ -0,0 +1,43 @@
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()

View File

@ -0,0 +1,44 @@
# 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])

View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load rest_framework %}
{% block content %}
<h1>Création d'une nouvelle fiche d'inscription</h1>
<br>
<form action='{% url 'GestionInscriptions:nouvelEleve' %}' method="post">
{% csrf_token %}
<ul>
<li style="margin-bottom: 15px"><strong>ELEVE</strong></li>
<div class="input-group">
<label for="nomEleve">Nom</label>
<div class="input-wrapper">
<input type="text" id="nomEleve" name="nomEleve">
</div>
</div>
<div class="input-group">
<label for="prenomEleve">Prénom</label>
<div class="input-wrapper">
<input type="text" id="prenomEleve" name="prenomEleve">
</div>
</div>
<li style="margin-bottom: 15px"><strong>LISTE DES CONTACTS</strong></li>
<div class="input-group">
<label for="mail">Adresse e-mail </label>
<div class="input-wrapper">
<input type="text" id="mail" name="mailResponsable">
</div>
</div>
<div class="input-group">
<label for="telephone">Numéro de téléphone</label>
<div class="input-wrapper">
<input type="text" id="telephone" name="telephoneResponsable">
</div>
</div>
<div class="input-group">
<label for="nomContact">Nom</label>
<div class="input-wrapper">
<input type="text" id="nomContact" name="nomResponsable">
</div>
</div>
<div class="input-group">
<label for="prenomContact">Prénom</label>
<div class="input-wrapper">
<input type="text" id="prenomContact" name="prenomResponsable">
</div>
</div>
</ul>
<input class="btn primary" type="submit" value="Créer" name="valider">
<br>
<input class="btn" type="button" value="Annuler" name="cancel">
</form>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<h1>Configuration des dossiers d'inscriptions</h1>
<br>
<form action='{% url 'GestionInscriptions:index' %}' method="post">
{% csrf_token %}
<ul>
<div style="margin-bottom: 15px">
<label>Relance automatique :</label>
<input type="text" name="delaiRelance" value="{{ delaiRelance }}">
<label>secondes</label>
</div>
</ul>
<input class="btn" type="submit" value="Configurer" name="valider">
</form>
{% endblock %}

View File

@ -0,0 +1,162 @@
{% extends "base.html" %}
{% block content %}
<h1>Création du dossier d'inscription</h1>
<br>
<form action='{% url 'GestionInscriptions:validate' eleve.id %}' method="post" enctype="multipart/form-data">
{% csrf_token %}
{% with responsable=eleve.getResponsablePrincipal %}
<ul>
<li style="margin-bottom: 15px"><strong>ELEVE</strong></li>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomEleve" value="{{ eleve.nom }}">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomEleve" value="{{ eleve.prenom }}">
</div>
<div style="margin-bottom: 15px">
<label>Genre :<br></label>
{% for genre in genres %}
<label>{{ genre }}</label>
<input type="radio" id="{{ genre }}" name="genre" value="{{ genre }}">
{% endfor %}
</div>
<div style="margin-bottom: 15px">
<label>Adresse :</label>
<input type="text" name="adresseEleve">
</div>
<div style="margin-bottom: 15px">
<label>Date de naissance :</label>
<input type="text" name="dateNaissanceEleve">
</div>
<div style="margin-bottom: 15px">
<label>Lieu de naissance :</label>
<input type="text" name="lieuNaissanceEleve">
</div>
<div style="margin-bottom: 15px">
<label>Code postal de naissance :</label>
<input type="text" name="codePostalNaissanceEleve">
</div>
<div style="margin-bottom: 15px">
<label>Nationalité :</label>
<input type="text" name="nationaliteEleve">
</div>
<div style="margin-bottom: 15px">
<label>Langue parlée :</label>
<input type="text" name="langueEleve">
</div>
<div style="margin-bottom: 15px">
<label>Ambiance :<br></label>
{% for ambiance in ambiances %}
<label>{{ ambiance }}</label>
<input type="radio" id="{{ ambiance }}" name="ambiance" value="{{ ambiance }}">
{% endfor %}
</div>
<div style="margin-bottom: 15px">
<label>Médecin traitant :</label>
<input type="text" name="medecinTraitantEleve">
</div>
<li style="margin-bottom: 15px"><strong>RESPONSABLES</strong></li>
<ul>
<li style="margin-bottom: 15px"><strong>RESPONSABLE 1</strong></li>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomResponsable1" value="{{ responsable.nom }}">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomResponsable1" value="{{ responsable.prenom }}">
</div>
<div style="margin-bottom: 15px">
<label>Adresse :</label>
<input type="text" name="adresseResponsable1" value="{{ responsable.adresse }}">
</div>
<div style="margin-bottom: 15px">
<label>Date de naissance :</label>
<input type="text" name="dateNaissanceResponsable1" value="{{ responsable.dateNaissance }}">
</div>
<div style="margin-bottom: 15px">
<label>Mail :</label>
<input type="text" name="mailResponsable1" value="{{ responsable.mail }}">
</div>
<div style="margin-bottom: 15px">
<label>Téléphone :</label>
<input type="text" name="telephoneResponsable1" value="{{ responsable.telephone }}">
</div>
<div style="margin-bottom: 15px">
<label>Profession :</label>
<input type="text" name="professionResponsable1" value="{{ responsable.profession }}">
</div>
<li style="margin-bottom: 15px"><strong>RESPONSABLE 2</strong></li>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomResponsable2">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomResponsable2">
</div>
<div style="margin-bottom: 15px">
<label>Adresse :</label>
<input type="text" name="adresseResponsable2">
</div>
<div style="margin-bottom: 15px">
<label>Date de naissance :</label>
<input type="text" name="dateNaissanceResponsable2">
</div>
<div style="margin-bottom: 15px">
<label>Mail :</label>
<input type="text" name="mailResponsable2">
</div>
<div style="margin-bottom: 15px">
<label>Téléphone :</label>
<input type="text" name="telephoneResponsable2">
</div>
<div style="margin-bottom: 15px">
<label>Profession :</label>
<input type="text" name="professionResponsable2">
</div>
</ul>
<li style="margin-bottom: 15px"><strong>FRATRIE</strong></li>
<ul>
<li style="margin-bottom: 15px"><strong>FRERE - SOEUR 1 :</strong></li>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomFrere1">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomFrere1">
</div>
<div style="margin-bottom: 15px">
<label>Date de naissance :</label>
<input type="text" name="dateNaissanceFrere1">
</div>
<li style="margin-bottom: 15px"><strong>FRERE - SOEUR 2 :</strong></li>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomFrere2">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomFrere2">
</div>
<div style="margin-bottom: 15px">
<label>Date de naissance :</label>
<input type="text" name="dateNaissanceFrere2">
</div>
</ul>
<li style="margin-bottom: 15px"><strong>PAIEMENT</strong></li>
<div style="margin-bottom: 15px">
<label>Mode Paiement :<br></label>
{% for modePaiement in modesPaiement %}
<label>{{ modePaiement }}</label>
<input type="radio" id="{{ modePaiement }}" name="modePaiement" value="{{ modePaiement }}">
{% endfor %}
</div>
</ul>
<input class="btn" type="submit" value="Valider" name="valider">
{% endwith %}
</form>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block content %}
<h1>Edition d'une fiche d'inscription</h1>
<br>
<form action='{% url 'GestionInscriptions:index' %}' method="post">
{% csrf_token %}
{% with responsable=eleve.getResponsablePrincipal %}
<ul>
<div style="margin-bottom: 15px">
<input type="hidden" name="fiche_id" value="{{ eleve.id }}">
</div>
<li style="margin-bottom: 15px"><strong>ELEVE</strong></li>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomEleve" value="{{ eleve.nom }}">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomEleve" value="{{ eleve.prenom }}">
</div>
<li style="margin-bottom: 15px"><strong>LISTE DES CONTACTS</strong></li>
<div style="margin-bottom: 15px">
<label>Adresse e-mail :</label>
<input type="text" name="mail" value="{{ responsable.mail }}">
</div>
<div style="margin-bottom: 15px">
<label>Numéro de téléphone :</label>
<input type="text" name="telephone" value="{{ responsable.telephone }}">
</div>
<div style="margin-bottom: 15px">
<label>Nom :</label>
<input type="text" name="nomContact" value="{{ responsable.nom }}">
</div>
<div style="margin-bottom: 15px">
<label>Prénom :</label>
<input type="text" name="prenomContact" value="{{ responsable.prenom }}">
</div>
</ul>
<input class="btn" type="submit" value="Modifier" name="valider">
{% endwith %}
</form>
{% endblock %}

View File

@ -0,0 +1,126 @@
{% extends "base.html" %}
{% load myTemplateTag %}
{% block content %}
<h1>Inscriptions 2024/2025</h1>
<br>
<div>
<br>
<br>
</div>
<section class="heading-section">
<!-- Search bar -->
<div class="input-group max-80">
<div class="input-wrapper max">
<span class="icon-ctn">
<i class="icon user-search"></i>
</span>
<input type="text" id="username" placeholder="Rechercher">
</div>
</div>
<a class="btn primary" href="nouvelEleve">
Ajouter <i class="icon profile-add"></i>
</a>
</section>
<section class="heading-section">
<div class="alphabet-filter">
{% for letter in "*ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
<a class="item" href="?letter={{ letter }}">{{ letter }}</a>
{% endfor %}
</div>
</section>
<table class="table">
<thead>
<tr>
<th>Nom</th>
<th>Prenom</th>
<th>Mail</th>
<th>Téléphone</th>
<th>MàJ Le</th>
<th>Statut</th>
<th>Fichiers</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for ficheInscription in ficheInscriptions_list %}
{% with eleve=ficheInscription.eleve %}
{% with responsable=eleve.getResponsablePrincipal %}
{% with fichiers=ficheInscription|recupereFichiersDossierInscription %}
<tr>
<td>{{ eleve.nom }}</td>
<td>{{ eleve.prenom }}</td>
<td>{{ responsable.mail }}</td>
<td>{{ responsable.telephone }}</td>
<td>{{ ficheInscription.dateMAJ }}</td>
<td>
{% if ficheInscription.etat == 0 %}
<span class="tag blue"> Créé</span>
{% elif ficheInscription.etat == 1 %}
<span class="tag orange"> Envoyé</span>
{% elif ficheInscription.etat == 2 %}
<span class="tag purple"> En Validation</span>
{% else %}
<span class="tag green"> Validé</span>
{% endif %}
</td>
<td>
{% for fichier in fichiers %}
<a href="{{ fichier.url }}">{{ fichier.nom }}</a>
{% endfor %}
</td>
<td class="actions">
<button class="icon-btn" onclick="location.href='{% url 'GestionInscriptions:send' eleve.id %}'" type="submit"> <i class="icon directbox-send"></i></button>
<button class="icon-btn"> <i class="icon edit"></i></button>
<button class="icon-btn red"> <i class="icon user-minus"></i></button>
</td>
</tr>
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
</tbody>
<tfoot>
<tr>
<td></td>
<td colspan="6">
<div class="pagination">
{% if ficheInscriptions_list.has_previous %}
{% if ficheInscriptions_list.previous_page_number == 1 %}
<a class="item" href="?page={{ ficheInscriptions_list.previous_page_number }}">&lt;</a>
<a class="item" href="?page=1">1</a>
{% else %}
<a class="item" href="?page={{ ficheInscriptions_list.previous_page_number }}">&lt;</a>
<a class="item" href="?page=1">1</a>
<a class="item" >...</a>
{% endif %}
{% endif %}
{% if ficheInscriptions_list %}
<a class="item active">{{ ficheInscriptions_list.number }}</a>
{% else %}
<a class="item">{{ ficheInscriptions_list.number }}</a>
{% endif %}
{% if ficheInscriptions_list.has_next %}
{% if ficheInscriptions_list.next_page_number == ficheInscriptions_list.paginator.num_pages %}
<a class="item" href="?page={{ ficheInscriptions_list.next_page_number }}">{{ ficheInscriptions_list.next_page_number }}</a>
<a class="item" href="?page={{ ficheInscriptions_list.next_page_number }}">&gt;</a>
{% else %}
<a class="item" >...</a>
<a class="item" href="?page={{ ficheInscriptions_list.paginator.num_pages }}">{{ ficheInscriptions_list.paginator.num_pages }}</a>
<a class="item" href="?page={{ ficheInscriptions_list.next_page_number }}">&gt;</a>
{% endif %}
{% endif %}
</div>
</td>
<td></td>
</tr>
</tfoot>
</table>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% 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 class="itemlogo">
<div class="circle"></div>
</div>
<a class="item active">
<i class="icon receipt-edit"></i> Administration
</a>
<a class="item">
<i class="icon user-line"></i> Statistiques
</a>
<a class="item">
<i class="icon book"></i> Paramétrage
</a>
</div>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,97 @@
<!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>

View File

@ -0,0 +1,23 @@
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

View File

@ -0,0 +1,31 @@
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"),
]

View File

@ -0,0 +1,181 @@
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

View File

@ -0,0 +1,289 @@
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)

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
class GestionloginConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'GestionLogin'

View File

@ -0,0 +1,20 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from GestionLogin.models import Profil
from N3wtSchool import bdd
class EmailBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(Profil.USERNAME_FIELD)
try:
user = Profil.objects.get(email=username)
# Vérifie le mot de passe de l'utilisateur
if user.check_password(password):
return user
except Profil.DoesNotExist:
return None

View File

@ -0,0 +1,25 @@
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)

View File

@ -0,0 +1,28 @@
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

View File

@ -0,0 +1,61 @@
{% 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>

View File

@ -0,0 +1,64 @@
{% 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>

View File

@ -0,0 +1,37 @@
{% 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>

View File

@ -0,0 +1,64 @@
{% 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>

View File

@ -0,0 +1,22 @@
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"),
]

View File

@ -0,0 +1,120 @@
from django.core.exceptions import ValidationError
from abc import ABC, abstractmethod
from N3wtSchool import bdd, error
class Validator(ABC):
formName=""
data = {}
errorFields={}
@abstractmethod
def validate(self):
pass
def getErrorFields(self):
return self.errorFields
class ValidatorAuthentication(Validator):
def __init__(self, **kwargs):
self.data = kwargs['data']
self.errorFields = {'email':'','password':''}
def __str__ (self):
return "VALIDATOR_AUTHENTICATION : " + self.data
def validate(self):
email = self.data.get("email")
password = self.data.get("password")
if email == None or not email:
self.errorFields['email'] = error.returnMessage[error.INCOMPLETE]
if password == None or not password:
self.errorFields['password'] = error.returnMessage[error.INCOMPLETE]
return (not self.errorFields['email'] and not self.errorFields['password']), self.errorFields
def getErrorFields(self):
return super().getErrorFields()
class ValidatorSubscription(Validator):
def __init__(self, **kwargs):
self.data = kwargs['data']
self.errorFields = {'email':'','password1':'', 'password2':''}
def __str__ (self):
return "VALIDATOR_SUBSCRIPTION : " + self.data
def validate(self):
email = self.data.get("email")
password1 = self.data.get("password1")
password2 = self.data.get("password2")
if password1 != password2:
self.errorFields['password1'] = error.returnMessage[error.DIFFERENT_PASWWORD]
self.errorFields['password2'] = error.returnMessage[error.DIFFERENT_PASWWORD]
else:
if email == None or not email:
self.errorFields['email'] = error.returnMessage[error.INCOMPLETE]
if password1 == None or not password1:
self.errorFields['password1'] = error.returnMessage[error.INCOMPLETE]
if password2 == None or not password2:
self.errorFields['password2'] = error.returnMessage[error.INCOMPLETE]
return (not self.errorFields['email'] and not self.errorFields['password1'] and not self.errorFields['password2']), self.errorFields
def getErrorFields(self):
return super().getErrorFields()
class ValidatorNewPassword(Validator):
def __init__(self, **kwargs):
self.data = kwargs['data']
self.errorFields = {'email':''}
def __str__ (self):
return "VALIDATOR_NEW_PASSWORD : " + self.data
def validate(self):
email = self.data.get("email")
if email == None or not email:
self.errorFields['email'] = error.returnMessage[error.INCOMPLETE]
return not self.errorFields['email'], self.errorFields
def getErrorFields(self):
return super().getErrorFields()
class ValidatorResetPassword(Validator):
def __init__(self, **kwargs):
self.data = kwargs['data']
self.errorFields = {'password1':'', 'password2':''}
def __str__ (self):
return "VALIDATOR_RESET_PASSWORD : " + self.data
def validate(self):
password1 = self.data.get("password1")
password2 = self.data.get("password2")
if password1 != password2:
self.errorFields['password1'] = error.returnMessage[error.DIFFERENT_PASWWORD]
self.errorFields['password2'] = error.returnMessage[error.DIFFERENT_PASWWORD]
else:
if password1 == None or not password1:
self.errorFields['password1'] = error.returnMessage[error.INCOMPLETE]
if password2 == None or not password2:
self.errorFields['password2'] = error.returnMessage[error.INCOMPLETE]
return (not self.errorFields['password1'] and not self.errorFields['password2']), self.errorFields
def getErrorFields(self):
return super().getErrorFields()

View File

@ -0,0 +1,264 @@
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)

View File

@ -0,0 +1 @@
default_app_config = 'GestionMessagerie.apps.GestionMessagerieConfig'

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class GestionMessagerieConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'GestionMessagerie'

View File

@ -0,0 +1,15 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from GestionLogin.models import Profil
class Messagerie(models.Model):
id = models.AutoField(primary_key=True)
objet = models.CharField(max_length=200, default="", blank=True)
emetteur = models.ForeignKey(Profil, on_delete=models.PROTECT, related_name='messages_envoyes')
destinataire = models.ForeignKey(Profil, on_delete=models.PROTECT, related_name='messages_recus')
corpus = models.CharField(max_length=200, default="", blank=True)
def __str__(self):
return 'Messagerie_'+self.id

View File

@ -0,0 +1,16 @@
from rest_framework import serializers
from GestionLogin.models import Profil
from GestionMessagerie.models import Messagerie
class MessageSerializer(serializers.ModelSerializer):
destinataire_profil = serializers.SerializerMethodField()
emetteur_profil = serializers.SerializerMethodField()
class Meta:
model = Messagerie
fields = '__all__'
def get_destinataire_profil(self, obj):
return obj.destinataire.email
def get_emetteur_profil(self, obj):
return obj.emetteur.email

View File

@ -0,0 +1,9 @@
from django.urls import path, re_path
from GestionMessagerie.views import MessagerieView, MessageView
urlpatterns = [
re_path(r'^messagerie/([0-9]+)$', MessagerieView.as_view(), name="messagerie"),
re_path(r'^message$', MessageView.as_view(), name="message"),
re_path(r'^message/([0-9]+)$', MessageView.as_view(), name="message"),
]

View File

@ -0,0 +1,32 @@
from django.http.response import JsonResponse
from rest_framework.views import APIView
from rest_framework.parsers import JSONParser
from .models import *
from GestionMessagerie.serializers import MessageSerializer
from N3wtSchool import bdd
class MessagerieView(APIView):
def get(self, request, _idProfile):
messagesList = bdd.getObjects(_objectName=Messagerie, _columnName='destinataire__id', _value=_idProfile)
messages_serializer = MessageSerializer(messagesList, many=True)
return JsonResponse(messages_serializer.data, safe=False)
class MessageView(APIView):
def get(self, request, _id):
message=bdd.getObject(Messagerie, "id", _id)
message_serializer=MessageSerializer(message)
return JsonResponse(message_serializer.data, safe=False)
def post(self, request):
message_data=JSONParser().parse(request)
message_serializer = MessageSerializer(data=message_data)
if message_serializer.is_valid():
message_serializer.save()
return JsonResponse('Nouveau Message ajouté', safe=False)
return JsonResponse(message_serializer.errors, safe=False)

View File

@ -0,0 +1 @@
default_app_config = 'GestionNotification.apps.GestionNotificationConfig'

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class GestionNotificationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'GestionNotification'
def ready(self):
import GestionNotification.signals

View File

@ -0,0 +1,28 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from GestionLogin.models import Profil
class TypeNotif(models.IntegerChoices):
NOTIF_NONE = 0, _('Aucune notification')
NOTIF_MESSAGE = 1, _('Un message a été reçu')
NOTIF_DI = 2, _('Le dossier d\'inscription a été mis à jour')
class Notification(models.Model):
user = models.ForeignKey(Profil, on_delete=models.PROTECT)
message = models.CharField(max_length=255)
is_read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
typeNotification = models.IntegerField(choices=TypeNotif, default=0)
@property
def typeNotification_label(self):
return self.get_typeNotification_display()
def get_typeNotification_display(self):
return TypeNotif(self.typeNotification).label
def __str__(self):
return f'Notification for {self.user.username}'

View File

@ -0,0 +1,23 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Notification, TypeNotif
from GestionMessagerie.models import Messagerie
from GestionInscriptions.models import FicheInscription
@receiver(post_save, sender=Messagerie)
def notification_MESSAGE(sender, instance, created, **kwargs):
if created:
Notification.objects.create(
user=instance.destinataire,
message=(TypeNotif.NOTIF_MESSAGE).label,
typeNotification=TypeNotif.NOTIF_MESSAGE
)
@receiver(post_save, sender=FicheInscription)
def notification_DI(sender, instance, created, **kwargs):
for responsable in instance.eleve.responsables.all():
Notification.objects.create(
user=responsable.profilAssocie,
message=(TypeNotif.NOTIF_DI).label,
typeNotification=TypeNotif.NOTIF_DI
)

View File

@ -0,0 +1,7 @@
from django.urls import path, re_path
from GestionNotification.views import NotificationView
urlpatterns = [
re_path(r'^notification$', NotificationView.as_view(), name="notification"),
]

View File

@ -0,0 +1,15 @@
from django.http.response import JsonResponse
from rest_framework.views import APIView
from .models import *
from GestionInscriptions.serializers import NotificationSerializer
from N3wtSchool import bdd
class NotificationView(APIView):
def get(self, request):
notifsList=bdd.getAllObjects(Notification)
notifs_serializer=NotificationSerializer(notifsList, many=True)
return JsonResponse(notifs_serializer.data, safe=False)

View File

@ -0,0 +1,5 @@
from __future__ import absolute_import, unicode_literals
from .celery import app as celery_app
__all__ = ('celery_app',)
default_app_config = 'N3wtSchool.apps.N3wtSchoolConfig' # Assurer l'utilisation de la configuration d'application

View File

@ -0,0 +1,8 @@
# n3wtschool/apps.py
from django.apps import AppConfig
class N3wtSchoolConfig(AppConfig):
name = 'N3wtSchool'
def ready(self):
import N3wtSchool.signals

View File

@ -0,0 +1,16 @@
"""
ASGI config for N3wtSchool project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'N3wtSchool.settings')
application = get_asgi_application()

View File

@ -0,0 +1,86 @@
import logging
from django.db.models import Q
from GestionInscriptions.models import FicheInscription, Profil, Eleve
def getAllObjects(_objectName):
result = _objectName.objects.all()
if not result:
logging.warning("Aucun résultat n'a été trouvé - " + _objectName.__name__)
return result
def getObject(_objectName, _columnName, _value):
result=None
try :
result = _objectName.objects.get(**{_columnName: _value})
except _objectName.DoesNotExist:
logging.error("Aucun résultat n'a été trouvé - " + _objectName.__name__ + " (" + _columnName + "=" + str(_value) + ")")
return result
def getObjects(_objectName, _columnName, _value, _reverseCondition=False):
results=None
try :
results = _objectName.objects.filter(**{_columnName: _value}) if _reverseCondition == False else _objectName.objects.filter(~Q(**{_columnName: _value}))
except _objectName.DoesNotExist:
logging.error("Aucun résultat n'a été trouvé - " + _objectName.__name__ + " (" + _columnName + "=" + str(_value) + ")")
return results
def existsProfilInList(objectList, valueToCheck):
result = False
for objectInstance in objectList:
if objectInstance.email == valueToCheck:
result = True
return result
def getProfile(objectList, valueToCheck):
result = None
for objectInstance in objectList:
if objectInstance.email == valueToCheck:
result = objectInstance
return result
def getEleveByCodeFI(_codeFI):
eleve = None
ficheInscriptions_List=getAllObjects(FicheInscription)
for fi in ficheInscriptions_List:
if fi.codeLienInscription == _codeFI:
eleve = fi.eleve
return eleve
def getLastId(_object):
result = 1
try:
result = _object.objects.latest('id').id
except:
logging.warning("Aucun résultat n'a été trouvé - ")
return result
def searchObjects(_objectName, _searchTerm, _excludeState=None):
"""
Recherche générique sur les objets avec possibilité d'exclure certains états
_objectName: Classe du modèle
_searchTerm: Terme de recherche
_excludeState: État à exclure de la recherche (optionnel)
"""
try:
query = _objectName.objects.all()
# Si on a un état à exclure
if _excludeState is not None:
query = query.filter(etat__lt=_excludeState)
# Si on a un terme de recherche
if _searchTerm and _searchTerm.strip():
terms = _searchTerm.lower().strip().split()
for term in terms:
query = query.filter(
Q(eleve__nom__icontains=term) |
Q(eleve__prenom__icontains=term)
)
return query.order_by('eleve__nom', 'eleve__prenom')
except _objectName.DoesNotExist:
logging.error(f"Aucun résultat n'a été trouvé - {_objectName.__name__} (recherche: {_searchTerm})")
return None

View File

@ -0,0 +1,20 @@
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from django.apps import apps
import logging
# Définir le module de réglages de Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'N3wtSchool.settings')
app = Celery('N3wtSchool')
# Lire les configurations de Celery depuis les réglages de Django
app.config_from_object('django.conf:settings', namespace='CELERY')
# Découverte automatique des tâches des apps Django
app.autodiscover_tasks(lambda: [n.name for n in apps.get_app_configs()])
# Configurer le logger global pour Celery
logger = logging.getLogger('celery')
logger.setLevel(logging.WARNING)

View File

@ -0,0 +1,31 @@
from typing import Final
WRONG_ID: Final = 1
INCOMPLETE: Final = 2
BAD_URL: Final = 3
ALREADY_EXISTS: Final = 4
DIFFERENT_PASWWORD: Final = 5
PROFIL_NOT_EXISTS: Final = 6
MESSAGE_REINIT_PASSWORD: Final = 7
EXPIRED_URL: Final = 8
PASSWORD_CHANGED: Final = 8
WRONG_MAIL_FORMAT: Final = 9
PROFIL_INACTIVE: Final = 10
MESSAGE_ACTIVATION_PROFILE: Final = 11
PROFIL_ACTIVE: Final = 12
returnMessage = {
WRONG_ID:'Identifiants invalides',
INCOMPLETE:'Renseignez les champs obligatoires',
BAD_URL:'Lien invalide : veuillez contacter l\'administrateur',
ALREADY_EXISTS: 'Profil déjà existant',
DIFFERENT_PASWWORD: 'Les mots de passe ne correspondent pas',
PROFIL_NOT_EXISTS: 'Aucun profil associé à cet utilisateur',
MESSAGE_REINIT_PASSWORD: 'Un mail a été envoyé à l\'adresse \'%s\'',
EXPIRED_URL:'L\'URL a expiré. Effectuer à nouveau la demande de réinitialisation de mot de passe : http://localhost:3000/password/reset?uuid=%s',
PASSWORD_CHANGED: 'Le mot de passe a été réinitialisé',
WRONG_MAIL_FORMAT: 'L\'adresse mail est mal formatée',
PROFIL_INACTIVE: 'Le profil n\'est pas actif',
MESSAGE_ACTIVATION_PROFILE: 'Votre profil a été activé avec succès',
PROFIL_ACTIVE: 'Le profil est déjà actif',
}

View File

@ -0,0 +1,10 @@
# redis_client.py
import redis
from django.conf import settings
# Configurer le client Redis
redis_client = redis.StrictRedis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
)

View File

@ -0,0 +1,14 @@
from io import BytesIO
from django.http import HttpResponse
from django.template.loader import get_template
from xhtml2pdf import pisa
def render_to_pdf(template_src, context_dict={}):
template = get_template(template_src)
html = template.render(context_dict)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
return HttpResponse(result.getvalue(), content_type='application/pdf')

View File

@ -0,0 +1,247 @@
"""
Django settings for N3wtSchool project.
Generated by 'django-admin startproject' using Django 5.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
import json
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
LOGIN_REDIRECT_URL = '/GestionInscriptions/fichesInscriptions'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-afjm6kvigncxzx6jjjf(qb0n(*qvi#je79r=gqflcn007d_ve9'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'GestionInscriptions.apps.GestioninscriptionsConfig',
'GestionLogin.apps.GestionloginConfig',
'GestionMessagerie.apps.GestionMessagerieConfig',
'GestionNotification.apps.GestionNotificationConfig',
'GestionEnseignants.apps.GestionenseignantsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'django_celery_beat',
'N3wtSchool',
'drf_yasg',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', # Déplacez ici, avant CorsMiddleware
'corsheaders.middleware.CorsMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'N3wtSchool.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates", BASE_DIR / "static/templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://redis:6379',
}
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
WSGI_APPLICATION = 'N3wtSchool.wsgi.application'
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 6,
}
},
#{
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
#},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
DEBUG = True
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
########################################################################
#################### Application Settings ##############################
########################################################################
with open('GestionInscriptions/Configuration/application.json', 'r') as f:
jsonObject = json.load(f)
DJANGO_SUPERUSER_PASSWORD='admin'
DJANGO_SUPERUSER_USERNAME='admin'
DJANGO_SUPERUSER_EMAIL='admin@n3wtschool.com'
EMAIL_HOST='smtp.gmail.com'
EMAIL_PORT=587
EMAIL_HOST_USER=jsonObject['mailFrom']
EMAIL_HOST_PASSWORD=jsonObject['password']
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
EMAIL_INSCRIPTION_SUBJECT = '[N3WT-SCHOOL] Dossier Inscription'
EMAIL_INSCRIPTION_CORPUS = """Bonjour,
Afin de procéder à l'inscription de votre petit bout, vous trouverez ci-joint le lien vers la page d'authentification : http://localhost:3000/users/login
S'il s'agit de votre première connexion, veuillez procéder à l'activation de votre compte : http://localhost:3000/users/subscribe
identifiant = %s
Cordialement,
"""
EMAIL_RELANCE_SUBJECT = '[N3WT-SCHOOL] Relance - Dossier Inscription'
EMAIL_RELANCE_CORPUS = 'Bonjour,\nN\'ayant pas eu de retour de votre part, nous vous renvoyons le lien vers le formulaire d\'inscription : http://localhost:3000/users/login\nCordialement'
EMAIL_REINIT_SUBJECT = 'Réinitialisation du mot de passe'
EMAIL_REINIT_CORPUS = 'Bonjour,\nVous trouverez ci-joint le lien pour réinitialiser votre mot de passe : http://localhost:3000/users/password/reset?uuid=%s\nCordialement'
DOCUMENT_DIR = 'documents'
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_ALL_HEADERS = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000"
]
CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000", # Front Next.js
"http://localhost:8080" # Insomnia
]
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SECURE = False
CSRF_COOKIE_NAME = 'csrftoken'
USE_TZ = True
TZ_APPLI = 'Europe/Paris'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
"NAME": "school",
"USER": "postgres",
"PASSWORD": "postgres",
"HOST": "database",
"PORT": "5432",
}
}
AUTH_USER_MODEL = 'GestionLogin.Profil'
AUTHENTICATION_BACKENDS = ('GestionLogin.backends.EmailBackend', )
SILENCED_SYSTEM_CHECKS = ["auth.W004"]
EXPIRATION_URL_NB_DAYS = 7
EXPIRATION_DI_NB_DAYS = 20
DATE_FORMAT = '%d-%m-%Y %H:%M'
EXPIRATION_SESSION_NB_SEC = 10
NB_RESULT_PER_PAGE = 8
NB_MAX_PAGE = 100
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'GestionInscriptions.pagination.CustomPagination',
'PAGE_SIZE': NB_RESULT_PER_PAGE
}
CELERY_BROKER_URL = 'redis://redis:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis:6379/0'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Paris'
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
URL_DJANGO = 'http://localhost:8080/'
REDIS_HOST = 'redis'
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PASSWORD = None
SECRET_KEY = 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3'

View File

@ -0,0 +1,21 @@
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from django_celery_beat.models import IntervalSchedule, PeriodicTask
import json
@receiver(post_migrate)
def setup_periodic_tasks(sender, **kwargs):
schedule, created = IntervalSchedule.objects.get_or_create(
every=5,
period=IntervalSchedule.SECONDS,
)
# Déclarer la tâche périodique
PeriodicTask.objects.get_or_create(
interval=schedule, # Utiliser l'intervalle défini ci-dessus
name='Tâche périodique toutes les 5 secondes',
task='GestionInscriptions.tasks.check_for_signature_deadlines', # Remplacer par le nom de ta tâche
kwargs=json.dumps({}) # Si nécessaire, ajoute
)

View File

@ -0,0 +1,49 @@
"""
URL configuration for N3wtSchool project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path, re_path
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="N3wtSchool API",
default_version='v1',
description="Documentation de l'API de N3wtSchool",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@example.com"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('admin/', admin.site.urls),
path("GestionInscriptions/", include(("GestionInscriptions.urls", 'GestionInscriptions'), namespace='GestionInscriptions')),
path("GestionLogin/", include(("GestionLogin.urls", 'GestionLogin'), namespace='GestionLogin')),
path("GestionMessagerie/", include(("GestionMessagerie.urls", 'GestionMessagerie'), namespace='GestionMessagerie')),
path("GestionNotification/", include(("GestionNotification.urls", 'GestionNotification'), namespace='GestionNotification')),
path("GestionEnseignants/", include(("GestionEnseignants.urls", 'GestionEnseignants'), namespace='GestionEnseignants')),
# Documentation Api
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

View File

@ -0,0 +1,16 @@
"""
WSGI config for N3wtSchool project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'N3wtSchool.settings')
application = get_wsgi_application()

1
Back-End/__version__.py Normal file
View File

@ -0,0 +1 @@
__version__ = "0.0.1"

BIN
Back-End/db.sqlite3 Normal file

Binary file not shown.

22
Back-End/manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'N3wtSchool.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

BIN
Back-End/requirements.txt Normal file

Binary file not shown.

15
Back-End/saveDB.py Normal file
View File

@ -0,0 +1,15 @@
import subprocess
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("action", type=str, help="dump | restore")
parser.add_argument("fileName", type=str, help="nom du fichier dump")
args = parser.parse_args()
if args.action == "dump":
process=subprocess.Popen(["pg_dump", "-h", "database", "-d", "school", "-U", "postgres", "-p", "5432", "-Fc", "-f", args.fileName + ".dmp"])
process.wait()
elif args.action == "restore":
process=subprocess.Popen(["pg_restore", "--clean", "-h", "database", "-d", "school", "-U", "postgres", args.fileName + ".dmp"])
process.wait()

37
Back-End/start.py Normal file
View File

@ -0,0 +1,37 @@
import subprocess
import os
def run_command(command):
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate(input=b"y\n")
if process.returncode != 0:
print(f"Error running command: {' '.join(command)}")
print(f"stdout: {stdout.decode()}")
print(f"stderr: {stderr.decode()}")
return process.returncode
commands = [
["python", "manage.py", "collectstatic", "--noinput"],
["python", "manage.py", "flush", "--noinput"],
["python", "manage.py", "makemigrations", "GestionInscriptions"],
["python", "manage.py", "makemigrations", "GestionNotification"],
["python", "manage.py", "makemigrations", "GestionMessagerie"],
["python", "manage.py", "makemigrations", "GestionLogin"],
["python", "manage.py", "makemigrations", "GestionEnseignants"],
["python", "manage.py", "migrate"]
]
for command in commands:
if run_command(command) != 0:
exit(1)
# Lancer les processus en parallèle
processes = [
subprocess.Popen(["python", "manage.py", "runserver", "0.0.0.0:8080"]),
subprocess.Popen(["celery", "-A", "N3wtSchool", "worker", "--loglevel=info"]),
subprocess.Popen(["celery", "-A", "N3wtSchool", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"])
]
# Attendre la fin des processus
for process in processes:
process.wait()

View File

@ -0,0 +1,22 @@
h1,h2,h3,h4,h5,h6{
font-family: var(--font-text);
text-transform: uppercase;
}
h1,h2,h3,h4,h5,h6{
color: var(--dark-teal);
}
h1.negative,h2.negative,h3.negative,h4.negative,h5.negative,h6.negative{
color: #FFF;
}
.heading-title{
font-size: 2em;
font-weight: bold;
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,80 @@
/**
* ICONS
**/
.icon{
display: block;
height: 1.5rem;
width: 1.5rem;
background-color: #8F8F8F;
background: no-repeat;
-webkit-mask-size: cover;
mask-size: cover;
}
.icon.user-add{
mask: url(../img/icons/user-add.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/user-add.svg) no-repeat 50% 50%;
}
.icon.directbox-send{
mask: url(../img/icons/directbox-send.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/directbox-send.svg) no-repeat 50% 50%;
}
.icon.arrow-square-up{
mask: url(../img/icons/arrow-square-up.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/arrow-square-up.svg) no-repeat 50% 50% ;
}
.icon.arrow-square-down{
transform: rotate(180deg);
mask: url(../img/icons/arrow-square-up.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/arrow-square-up.svg) no-repeat 50% 50% ;
}
.icon.book{
mask: url(../img/icons/book.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/book.svg) no-repeat 50% 50%;
}
.icon.briefcase{
mask: url(../img/icons/briefcase.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/briefcase.svg) no-repeat 50% 50%;
}
.icon.calculator{
mask: url(../img/icons/calculator.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/calculator.svg) no-repeat 50% 50%;
}
.icon.key{
mask: url(../img/icons/key.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/key.svg) no-repeat 50% 50%;
}
.icon.receipt-edit{
mask: url(../img/icons/receipt-edit.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/receipt-edit.svg) no-repeat 50% 50%;
}
.icon.teacher{
mask: url(../img/icons/teacher.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/teacher.svg) no-repeat 50% 50%;
}
.icon.user-line{
mask: url(../img/icons/user-line.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/user-line.svg) no-repeat 50% 50%;
}
.icon.user{
mask: url(../img/icons/user.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/user.svg) no-repeat 50% 50%;
}
.icon.user-minus{
mask: url(../img/icons/user-minus.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/user-minus.svg) no-repeat 50% 50%;
}
.icon.profile-add{
mask: url(../img/icons/profile-add.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/profile-add.svg) no-repeat 50% 50%;
}
.icon.edit{
mask: url(../img/icons/edit.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/edit.svg) no-repeat 50% 50%;
}
.icon.user-search {
mask: url(../img/icons/user-search.svg) no-repeat 50% 50%;
-webkit-mask: url(../img/icons/user-search.svg) no-repeat 50% 50%;
}

View File

@ -0,0 +1,506 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
@import url('./icons.css');
@import url('./headings.css');
:root{
/*Colors*/
--deep-blue:#3C87F4;
--artic-blue:#DFE9F5;
--light_grey:#8F8F8F;
--darker-blue:#3572CA;
--dark-teal:#011922;
--darker-teal:#000E14;
--clear-blue:#63A5BF;
--lighter-grey:#F5F5F5;
/*component settings*/
--primary-color:var(--dark-teal);
--background-color:var(#FFF);
--font-color:var(--light_grey);
--font-text:"Roboto", sans-serif;
--topbar-bg-color:#FFF;
--sidebar-item-bg:var(--darker-teal);
--sidebar-item-font-color:#FFF;
--topbar-item-font-color:#000;
--topbar-item-bg-color:#FFF;
}
body{
color:var(--font-color);
background:var(--background-color);
font-family: var(--font-text);
font-size: 16px;
margin:0;
padding:0;
}
a{
text-decoration: none;
color:var(--dark-teal)
}
a.negative{
color: var(--clear-blue);
}
.right{
text-align: right;
float: right;
}
.sidebar{
display: block;
width: 300px;
background-color: var(--primary-color);
border-radius: 0px 1em 1em 0px ;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
}
.sidebar>.itemlogo{
height: 300px;
width: 300px;
}
.sidebar>.item{
display: block;
height: 75px;
line-height: 75px;
background-color: var(--sidebar-item-bg);
color: var(--sidebar-item-font-color);
text-transform: uppercase;
text-align: center;
}
.sidebar>.item:hover{
background-color: var(--clear-blue);
color: var(--dark-teal);
}
.sidebar>.item.active{
background-color: #FFF;
color: var(--dark-teal);
}
.sidebar>.item>.icon{
background-color: var(--sidebar-item-font-color);
line-height: 75px;
display: block;
float: left;
height: 32px;
margin-top: 20px;
margin-left: 40px;
}
.sidebar>.item:hover>.icon{
background-color: var(--dark-teal);
}
.sidebar>.item.active>.icon{
background-color: var(--dark-teal);
}
.circle{
width: 200px;
height: 200px;
background-color: #FFF;
border-radius: 100px;
margin: auto;
margin-top: 75px;
}
.container{
display: block;
padding: 20px;
position: absolute;
width: calc(100% - 340px);
top: 0px;
bottom:0px;
right: 0px;
overflow-y: scroll;
}
.negative{
background-color: var(--dark-teal);
color:#FFF
}
.container.full-size{
left: 0px;
}
.input-wrapper{
width: 300px;
height: 35px;
border-radius: 5px;
background-color: #FFF;
border-color: #C3C3C3;
border-style: solid;
border-width: 1px;
}
.input-group>label{
font-size: 0.6em;
display: block;
font-weight: bold;
margin-bottom: 10px;
color: #011922;
text-transform: uppercase;
}
.negative>.input-group>label{
color: #FFF;
}
.input-wrapper>span.icon-ctn{
height: 35px;
width: 35px;
float: left;
background-color: #f3f3f3;
vertical-align: middle;
border-radius: 8px 35px 35px 8px;
}
.input-wrapper>span.icon-ctn>.icon{
height: 35px;
width: 35px;
background-size: 20px;
background-position: center;
background-repeat: no-repeat;
display: block;
margin: auto;
background-color: #8F8F8F;
}
.input-wrapper>input{
line-height: 35px;
width: calc(100% - 50px);
height: 35px;
border: none;
font-size: 14px;
margin-left: 5px;
outline: none;
background-color: transparent;
}
.max{
width: 100%;
}
.max-90{
width: 90%;
}
.max-80{
width: 80%;
}
/*
* Bouton
*/
.btn{
display: flex;
font-family: var(--font-text);
background-color: #8F8F8F;
color: #FFF;
height: 35px;
min-width: 75px;
padding-left: 5px;
padding-right: 5px;
font-size: 1em;
outline: none;
border: none;
border-radius: 5px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
line-height: 35px;
font-size: 0.6em;
font-weight: bold;
letter-spacing: 15%;
vertical-align: middle;
justify-content: space-between;
}
.btn>*{
display: flexbox;
}
.negative>.btn{
background-color: #FFF;
color: var(--dark-teal);
}
.btn>.icon{
display: inline-block;
background-color: #FFF;
margin-top: auto;
margin-bottom: auto;
vertical-align: middle;
}
.btn:hover{
background-color:#606060;
}
.btn.primary{
background-color: var(--dark-teal);
}
.negative>.btn.primary{
background-color: var(--clear-blue);
}
.btn.primary:hover{
background-color: var(--darker-teal);
}
.negative>.btn.primary:hover{
background-color: #3b687a;
}
.logo-circular{
margin-top: 50px;
text-align: center;
width: 125px;
height: 125px;
border-radius: 125px;
background-color: #FFF;
}
.login-heading{
margin-top: 50px;
text-align: center;
}
.login-heading.negative{
color: #FFF;;
}
.login-form{
margin-top: 50px;
width: 300px;
}
.form-group-submit{
margin-top: 50px;
}
.centered{
display: block;
margin-left: auto;
margin-right: auto;
}
.centered>*{
margin-left: auto;
margin-right: auto;
}
.table-action{
width: 100%;
}
.splited-row-table{
margin-left: auto;
margin-right: auto;
width: 90%;
border-collapse: collapse;
}
.splited-row-table>thead>tr>th{
padding: 10px;
text-align: left;
color: #000;
}
.splited-row-table>tbody>tr{
background-color: #f4f4f4;
color:#8F8F8F;
line-height:50px;
}
.table-actions .icon{
background-color: #8F8F8F;
}
.table{
margin-left: auto;
margin-right: auto;
width: 90%;
border-collapse: separate;
border-spacing: 0;
}
.table>thead>tr>th{
padding: 15px;
text-align: left;
color: #48586B;
font-weight: bold;
text-transform: Capitalise;
background-color: #F5F6F8;
border-bottom: 1px solid #EBECF0;
border-top: 1px solid #EBECF0;
}
.table>thead>tr>th:first-child{
border-radius: 15px 0 0 0;
border-left: 1px solid #EBECF0;
}
.table>thead>tr>th:last-child{
border-radius: 0 15px 0 0 ;
border-right: 1px solid #EBECF0;
}
.table>tbody>tr>td>.avatar{
height: 42px;
width: 42px;
border-radius: 42px;
background-color: #8F8F8F;
display: block;
margin: auto;
float: left;
margin-right: 20px;
}
.table>tbody>tr>td{
padding: 15px;
color: #48586B;
border-bottom: 1px solid #EBECF0;
vertical-align: middle;
}
.table>tbody>tr:nth-child(even)>td{
padding: 15px;
background-color: #F5F6F8;
border-bottom: 1px solid #EBECF0;
vertical-align: middle;
}
.table>tbody>tr>td:first-child{
border-left: 1px solid #EBECF0;
}
.table>tbody>tr>td:last-child{
border-right: 1px solid #EBECF0;
}
.table>tfoot>tr>td{
padding: 35px;
color: #48586B;
border-bottom: 1px solid #EBECF0;
}
.table>tfoot>tr>td:first-child{
border-radius: 0 0 0 15px;
border-left: 1px solid #EBECF0;
}
.table>tfoot>tr>td:last-child{
border-radius: 0 0 15px 0 ;
border-right: 1px solid #EBECF0;
}
.pagination{
text-align: center;
}
.pagination>a.item{
display: inline-block;
width: 35px;
height: 35px;
line-height: 35px;
background-color: #F5F6F8;
color: #48586B;
margin: 5px;
border-radius: 5px;
}
.pagination>a.item.active{
background-color: var(--dark-teal);
color: #FFF;
}
.alphabet-filter{
text-align: center;
}
.alphabet-filter>a.item{
display: inline-block;
width: 35px;
height: 35px;
line-height: 35px;
background-color: #F5F6F8;
color: #48586B;
margin: 5px;
border-radius: 5px;
}
.alphabet-filter>a.item.active{
background-color: var(--dark-teal);
color: #FFF;
}
.tag{
padding: 5px;
display: block;
background-color: #F5F6F8;
text-align: center;
border-radius: 2px;
color: #48586B;
border: 1px solid #48586B;
font-weight: bold
}
.tag.green{
background-color: #E0F5EE;
border: 1px solid #4BC097;
color: #4BC097;
}
.tag.red{
background-color: #ffe4e4;
border: 1px solid #ff6a6a;
color: #ff6a6a;
}
.tag.orange{
background-color: #FFF0E4;
border: 1px solid #FFB06A;
color: #FFB06A;
}
.tag.purple{
background-color: #fae4ff;
border: 1px solid #e66aff;
color: #e66aff;
}
.tag.blue{
background-color: #E7E9FD;
border: 1px solid #7E89F1;
color: #7E89F1;
}
.tag.teal{
background-color: #b3f5f9;
border: 1px solid #359ab3;
color: #359ab3;
}
.icon-btn {
display: block;
width: 35px;
height: 35px;
text-align: center;
line-height: 35px;
background-color: #F5F6F8;
color: #48586B;
border: none;
border-radius: 0.5rem;
outline: none;
}
.icon-btn:hover{
background-color: #dfe0e1;
color: #FFF;
}
.icon-btn>.icon{
display: block;
height: 100%;
width: 100%;
background-size: 20px;
background-position: center;
background-repeat: no-repeat;
margin: auto;
background-color: #8F8F8F;
}
.icon-btn.primary{
background-color: var(--dark-teal);
}
.icon-btn.primary>.icon{
background-color: #FFF;
}
.icon-btn.primary:hover{
background-color: var(--darker-teal);
}
.icon-btn.red{
background-color: rgb(202, 34, 34);
}
.icon-btn.red>.icon{
background-color: #FFF;
}
.icon-btn.red:hover{
background-color: rgb(148, 25, 25);
}
.actions > .icon-btn{
margin-top: 10px;
}
.heading-section{
display: flex;
justify-content: space-between;
width:90%;
margin-left: auto;
margin-right: auto;
text-align: center;
margin-bottom: 15px;
}

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.47021 13.4599L12.0002 9.93994L15.5302 13.4599" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 16.7402V4.67019C22 3.47019 21.02 2.58019 19.83 2.68019H19.77C17.67 2.86019 14.48 3.93019 12.7 5.05019L12.53 5.16019C12.24 5.34019 11.76 5.34019 11.47 5.16019L11.22 5.01019C9.44 3.90019 6.26 2.84019 4.16 2.67019C2.97 2.57019 2 3.47019 2 4.66019V16.7402C2 17.7002 2.78 18.6002 3.74 18.7202L4.03 18.7602C6.2 19.0502 9.55 20.1502 11.47 21.2002L11.51 21.2202C11.78 21.3702 12.21 21.3702 12.47 21.2202C14.39 20.1602 17.75 19.0502 19.93 18.7602L20.26 18.7202C21.22 18.6002 22 17.7002 22 16.7402Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 5.49023V20.4902" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.75 8.49023H5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 11.4902H5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99983 22H15.9998C20.0198 22 20.7398 20.39 20.9498 18.43L21.6998 10.43C21.9698 7.99 21.2698 6 16.9998 6H6.99983C2.72983 6 2.02983 7.99 2.29983 10.43L3.04983 18.43C3.25983 20.39 3.97983 22 7.99983 22Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 6V5.2C8 3.43 8 2 11.2 2H12.8C16 2 16 3.43 16 5.2V6" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 13V14C14 14.01 14 14.01 14 14.02C14 15.11 13.99 16 12 16C10.02 16 10 15.12 10 14.03V13C10 12 10 12 11 12H13C14 12 14 12 14 13Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.65 11C19.34 12.68 16.7 13.68 14 14.02" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.62012 11.27C4.87012 12.81 7.41012 13.74 10.0001 14.03" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 22.75H10C4.57 22.75 2.25 20.43 2.25 15V9C2.25 3.57 4.57 1.25 10 1.25H14C19.43 1.25 21.75 3.57 21.75 9V15C21.75 20.43 19.43 22.75 14 22.75ZM10 2.75C5.39 2.75 3.75 4.39 3.75 9V15C3.75 19.61 5.39 21.25 10 21.25H14C18.61 21.25 20.25 19.61 20.25 15V9C20.25 4.39 18.61 2.75 14 2.75H10Z" fill="currentColor"/>
<path d="M15 10.8301H9C7.76 10.8301 6.75 9.82008 6.75 8.58008V7.58008C6.75 6.34008 7.76 5.33008 9 5.33008H15C16.24 5.33008 17.25 6.34008 17.25 7.58008V8.58008C17.25 9.82008 16.24 10.8301 15 10.8301ZM9 6.83008C8.59 6.83008 8.25 7.17008 8.25 7.58008V8.58008C8.25 8.99008 8.59 9.33008 9 9.33008H15C15.41 9.33008 15.75 8.99008 15.75 8.58008V7.58008C15.75 7.17008 15.41 6.83008 15 6.83008H9Z" fill="currentColor"/>
<path d="M8.15016 14.9198C8.02016 14.9198 7.89016 14.8898 7.77016 14.8398C7.65016 14.7898 7.54016 14.7198 7.45016 14.6298C7.26016 14.4398 7.16016 14.1898 7.16016 13.9198C7.16016 13.7898 7.18016 13.6598 7.23016 13.5398C7.28016 13.4098 7.35016 13.3098 7.45016 13.2098C7.49016 13.1698 7.54016 13.1198 7.60016 13.0898C7.65016 13.0498 7.71016 13.0198 7.77016 12.9998C7.83016 12.9698 7.90016 12.9498 7.96016 12.9398C8.28016 12.8698 8.63016 12.9798 8.86016 13.2098C8.95016 13.2998 9.03016 13.4098 9.08016 13.5398C9.13016 13.6598 9.16016 13.7898 9.16016 13.9198C9.16016 14.1898 9.05016 14.4398 8.86016 14.6298C8.67016 14.8198 8.42016 14.9198 8.15016 14.9198Z" fill="currentColor"/>
<path d="M12.1602 14.9199C11.8902 14.9199 11.6402 14.8199 11.4502 14.6299C11.2602 14.4399 11.1602 14.1899 11.1602 13.9199C11.1602 13.7899 11.1802 13.6599 11.2302 13.5399C11.2802 13.4099 11.3502 13.3099 11.4502 13.2099C11.4902 13.1699 11.5402 13.1199 11.6002 13.0899C11.6502 13.0499 11.7102 13.0199 11.7702 12.9999C11.8302 12.9699 11.9002 12.9499 11.9602 12.9399C12.1502 12.8999 12.3502 12.9199 12.5402 12.9999C12.6602 13.0499 12.7702 13.1199 12.8602 13.2099C12.9502 13.3099 13.0302 13.4099 13.0802 13.5399C13.1302 13.6599 13.1602 13.7899 13.1602 13.9199C13.1602 14.1899 13.0502 14.4399 12.8602 14.6299C12.6702 14.8199 12.4202 14.9199 12.1602 14.9199Z" fill="currentColor"/>
<path d="M16.1502 14.9201C16.0202 14.9201 15.8902 14.8901 15.7702 14.8401C15.6502 14.7901 15.5402 14.7201 15.4502 14.6301C15.3502 14.5301 15.2802 14.4201 15.2302 14.3001C15.1802 14.1801 15.1602 14.0501 15.1602 13.9201C15.1602 13.7901 15.1802 13.6601 15.2302 13.5401C15.2802 13.4101 15.3502 13.3101 15.4502 13.2101C15.8202 12.8401 16.4902 12.8401 16.8602 13.2101C17.0502 13.4001 17.1602 13.6601 17.1602 13.9201C17.1602 14.1901 17.0502 14.4401 16.8602 14.6301C16.6802 14.8101 16.4302 14.9201 16.1502 14.9201Z" fill="currentColor"/>
<path d="M8.1599 18.9199C7.8899 18.9199 7.6399 18.8199 7.4499 18.6299C7.2599 18.4399 7.1499 18.1899 7.1499 17.9199C7.1499 17.6599 7.2599 17.3999 7.4499 17.2099C7.4899 17.1699 7.5499 17.1199 7.5999 17.0899C7.6499 17.0499 7.7099 17.0199 7.7699 16.9999C7.8299 16.9699 7.8999 16.9499 7.9599 16.9399C8.2899 16.8799 8.6299 16.9799 8.8599 17.2099C8.9099 17.2599 8.9499 17.3099 8.9899 17.3599C9.0199 17.4199 9.0499 17.4799 9.0799 17.5399C9.0999 17.5999 9.1199 17.6599 9.1399 17.7199C9.1499 17.7899 9.1599 17.8499 9.1599 17.9199C9.1599 18.1899 9.0499 18.4399 8.8599 18.6299C8.6699 18.8199 8.4199 18.9199 8.1599 18.9199Z" fill="currentColor"/>
<path d="M12.1599 18.9199C11.8899 18.9199 11.6399 18.8199 11.4499 18.6299C11.2599 18.4399 11.1499 18.1899 11.1499 17.9199C11.1499 17.6599 11.2599 17.3999 11.4499 17.2099C11.6799 16.9799 12.0299 16.8799 12.3499 16.9399C12.4099 16.9499 12.4799 16.9699 12.5399 16.9999C12.5999 17.0199 12.6599 17.0499 12.7099 17.0899C12.7599 17.1199 12.8099 17.1699 12.8599 17.2099C13.0499 17.3999 13.1499 17.6599 13.1499 17.9199C13.1499 18.1899 13.0499 18.4399 12.8599 18.6299C12.6699 18.8199 12.4199 18.9199 12.1599 18.9199Z" fill="currentColor"/>
<path d="M16.1502 18.9202C15.8902 18.9202 15.6402 18.8202 15.4502 18.6302C15.2602 18.4402 15.1602 18.1902 15.1602 17.9202C15.1602 17.6602 15.2602 17.4002 15.4502 17.2102C15.5402 17.1202 15.6502 17.0502 15.7702 17.0002C16.1402 16.8502 16.5902 16.9302 16.8602 17.2102C17.0502 17.4002 17.1502 17.6602 17.1502 17.9202C17.1502 18.1902 17.0502 18.4402 16.8602 18.6302C16.7702 18.7202 16.6602 18.7902 16.5402 18.8402C16.4202 18.8902 16.2902 18.9202 16.1502 18.9202Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8V2L10 4" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 2L14 4" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 12C3 12 3 13.79 3 16V17C3 19.76 3 22 8 22H16C20 22 21 19.76 21 17V16C21 13.79 21 12 17 12C16 12 15.72 12.21 15.2 12.6L14.18 13.68C13 14.94 11 14.94 9.81 13.68L8.8 12.6C8.28 12.21 8 12 7 12Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 12V10C5 7.99004 5 6.33004 8 6.04004" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 12V10C19 7.99004 19 6.33004 16 6.04004" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 946 B

View File

@ -0,0 +1,5 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7601 3.59997L5.5501 12.29C5.2401 12.62 4.9401 13.27 4.8801 13.72L4.5101 16.96C4.3801 18.13 5.2201 18.93 6.3801 18.73L9.6001 18.18C10.0501 18.1 10.6801 17.77 10.9901 17.43L19.2001 8.73997C20.6201 7.23997 21.2601 5.52997 19.0501 3.43997C16.8501 1.36997 15.1801 2.09997 13.7601 3.59997Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.3899 5.05005C12.8199 7.81005 15.0599 9.92005 17.8399 10.2" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.5 22H21.5" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 820 B

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17.0625 8.06542C17.8909 8.06542 18.5625 7.39385 18.5625 6.56542C18.5625 5.737 17.8909 5.06543 17.0625 5.06543C16.2341 5.06543 15.5625 5.737 15.5625 6.56542C15.5625 7.39385 16.2341 8.06542 17.0625 8.06542Z" fill="#8F8F8F"/>
<path d="M23.7411 7.77903C23.2404 6.50432 22.3224 4.66509 20.7919 3.14185C19.2669 1.62385 17.4471 0.732108 16.1891 0.251266C14.808 -0.276591 13.2386 0.0561262 12.1909 1.09886L8.50947 4.76306C7.41817 5.8492 7.10364 7.52047 7.72675 8.92179C7.91997 9.35642 8.13011 9.78153 8.35464 10.1922L0.274593 18.2722C0.0987652 18.4481 0 18.6865 0 18.9352V23.0602C0 23.578 0.419717 23.9977 0.937496 23.9977H5.06248C5.58026 23.9977 5.99998 23.578 5.99998 23.0602V21.3727L7.68747 21.3726C8.20525 21.3726 8.62497 20.9529 8.62497 20.4351V18.7477H10.3125C10.8302 18.7477 11.25 18.328 11.25 17.8102C11.25 17.2924 10.8302 16.8727 10.3125 16.8727H7.68747C7.16969 16.8727 6.74997 17.2924 6.74997 17.8102V19.4976L5.06248 19.4977C4.5447 19.4977 4.12498 19.9174 4.12498 20.4352V22.1227H1.87499V19.3235L10.1885 11.0101C10.4939 10.7046 10.5511 10.2301 10.3271 9.86084C10.0022 9.32534 9.70379 8.75309 9.44012 8.15998C9.13079 7.46432 9.28838 6.63327 9.83227 6.09192L13.5137 2.42776C14.0393 1.90468 14.8267 1.73785 15.5198 2.0027C16.6084 2.41876 18.1772 3.1846 19.4693 4.47075C20.7694 5.76468 21.5609 7.35669 21.9959 8.46434C22.2673 9.15565 22.1024 9.94104 21.5756 10.4653L17.8497 14.1737C17.3072 14.7137 16.4974 14.8716 15.7865 14.5759C15.2002 14.3321 14.6324 14.0424 14.099 13.715C13.6578 13.4441 13.0805 13.5823 12.8096 14.0236C12.5388 14.4649 12.6769 15.0422 13.1182 15.313C13.7347 15.6914 14.3902 16.0259 15.0664 16.3071C16.4793 16.8949 18.091 16.579 19.1724 15.5027L22.8983 11.7943C23.9551 10.7425 24.2859 9.16639 23.7411 7.77903Z" fill="#8F8F8F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 19.5H14.5" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 21.5V17.5" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.16 10.87C12.06 10.86 11.94 10.86 11.83 10.87C9.44997 10.79 7.55997 8.84 7.55997 6.44C7.54997 3.99 9.53997 2 11.99 2C14.44 2 16.43 3.99 16.43 6.44C16.43 8.84 14.53 10.79 12.16 10.87Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.99 21.81C10.17 21.81 8.36004 21.35 6.98004 20.43C4.56004 18.81 4.56004 16.17 6.98004 14.56C9.73004 12.72 14.24 12.72 16.99 14.56" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 834 B

View File

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5 11.3V7.04001C20.5 3.01001 19.56 2 15.78 2H8.22C4.44 2 3.5 3.01001 3.5 7.04001V18.3C3.5 20.96 4.96001 21.59 6.73001 19.69L6.73999 19.68C7.55999 18.81 8.80999 18.88 9.51999 19.83L10.53 21.18" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 7H16" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 11H15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.211 14.7703L14.671 18.3103C14.531 18.4503 14.401 18.7103 14.371 18.9003L14.181 20.2503C14.111 20.7403 14.451 21.0803 14.941 21.0103L16.291 20.8203C16.481 20.7903 16.751 20.6603 16.881 20.5203L20.421 16.9803C21.031 16.3703 21.321 15.6603 20.421 14.7603C19.531 13.8703 18.821 14.1603 18.211 14.7703Z" stroke="black" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.6992 15.2803C17.9992 16.3603 18.8392 17.2003 19.9192 17.5003" stroke="black" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0098 16.9998C11.1598 16.9998 10.2998 16.7798 9.62982 16.3498L3.60982 12.4198C2.48982 11.6898 1.81982 10.4598 1.81982 9.11979C1.81982 7.77979 2.48982 6.54979 3.60982 5.81979L9.63982 1.89979C10.9798 1.02979 13.0698 1.02979 14.3998 1.90979L20.3898 5.83979C21.4998 6.56979 22.1698 7.79979 22.1698 9.12979C22.1698 10.4598 21.4998 11.6898 20.3898 12.4198L14.3998 16.3498C13.7298 16.7898 12.8698 16.9998 12.0098 16.9998ZM12.0098 2.74979C11.4398 2.74979 10.8698 2.87979 10.4598 3.15979L4.43982 7.07979C3.73982 7.53979 3.32982 8.27979 3.32982 9.11979C3.32982 9.95979 3.72982 10.6998 4.43982 11.1598L10.4598 15.0898C11.2898 15.6298 12.7498 15.6298 13.5798 15.0898L19.5698 11.1598C20.2698 10.6998 20.6698 9.95979 20.6698 9.11979C20.6698 8.27979 20.2698 7.53979 19.5698 7.07979L13.5798 3.14979C13.1598 2.88979 12.5898 2.74979 12.0098 2.74979Z" fill="currentColor"/>
<path d="M12.0002 22.7501C11.5602 22.7501 11.1102 22.6901 10.7502 22.5701L7.56018 21.5101C6.05018 21.0101 4.86018 19.3601 4.87018 17.7701L4.88018 13.0801C4.88018 12.6701 5.22018 12.3301 5.63018 12.3301C6.04018 12.3301 6.38018 12.6701 6.38018 13.0801L6.37018 17.7701C6.37018 18.7101 7.15018 19.7901 8.04018 20.0901L11.2302 21.1501C11.6302 21.2801 12.3702 21.2801 12.7702 21.1501L15.9602 20.0901C16.8502 19.7901 17.6302 18.7101 17.6302 17.7801V13.1401C17.6302 12.7301 17.9702 12.3901 18.3802 12.3901C18.7902 12.3901 19.1302 12.7301 19.1302 13.1401V17.7801C19.1302 19.3701 17.9502 21.0101 16.4402 21.5201L13.2502 22.5801C12.8902 22.6901 12.4402 22.7501 12.0002 22.7501Z" fill="currentColor"/>
<path d="M21.3999 15.75C20.9899 15.75 20.6499 15.41 20.6499 15V9C20.6499 8.59 20.9899 8.25 21.3999 8.25C21.8099 8.25 22.1499 8.59 22.1499 9V15C22.1499 15.41 21.8099 15.75 21.3999 15.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.40991 22C3.40991 18.13 7.25991 15 11.9999 15C12.9599 15 13.8899 15.13 14.7599 15.37" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 18C22 18.32 21.96 18.63 21.88 18.93C21.79 19.33 21.63 19.72 21.42 20.06C20.73 21.22 19.46 22 18 22C16.97 22 16.04 21.61 15.34 20.97C15.04 20.71 14.78 20.4 14.58 20.06C14.21 19.46 14 18.75 14 18C14 16.92 14.43 15.93 15.13 15.21C15.86 14.46 16.88 14 18 14C19.18 14 20.25 14.51 20.97 15.33C21.61 16.04 22 16.98 22 18Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.49 17.98H16.51" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 16.52V19.51" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.5901 22C20.5901 18.13 16.7402 15 12.0002 15C7.26015 15 3.41016 18.13 3.41016 22" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.40991 22C3.40991 18.13 7.25994 15 11.9999 15C12.9599 15 13.8899 15.13 14.7599 15.37" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 18C22 18.32 21.96 18.63 21.88 18.93C21.79 19.33 21.63 19.72 21.42 20.06C20.73 21.22 19.46 22 18 22C16.97 22 16.04 21.61 15.34 20.97C15.04 20.71 14.78 20.4 14.58 20.06C14.21 19.46 14 18.75 14 18C14 16.92 14.43 15.93 15.13 15.21C15.86 14.46 16.88 14 18 14C19.18 14 20.25 14.51 20.97 15.33C21.61 16.04 22 16.98 22 18Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.49 17.98H16.51" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.40991 22C3.40991 18.13 7.25994 15 11.9999 15" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.2 21.4C19.9673 21.4 21.4 19.9673 21.4 18.2C21.4 16.4327 19.9673 15 18.2 15C16.4327 15 15 16.4327 15 18.2C15 19.9673 16.4327 21.4 18.2 21.4Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 22L21 21" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 800 B

Some files were not shown because too many files have changed in this diff Show More