feat: Configuration des compétences par cycle [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-18 00:45:49 +02:00
parent 2888f8dcce
commit 4e5aab6db7
29 changed files with 1001 additions and 82 deletions

View File

@ -6,7 +6,9 @@ from rest_framework.views import APIView
from rest_framework import status
from .models import Establishment
from .serializers import EstablishmentSerializer
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
from N3wtSchool.bdd import delete_object, getAllObjects
from School.models import Competency, EstablishmentCompetency
from django.db.models import Q
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
@ -20,7 +22,17 @@ class EstablishmentListCreateView(APIView):
establishment_data = JSONParser().parse(request)
establishment_serializer = EstablishmentSerializer(data=establishment_data)
if establishment_serializer.is_valid():
establishment_serializer.save()
establishment = establishment_serializer.save()
# Création des EstablishmentCompetency pour chaque compétence existante
competencies = Competency.objects.filter(
Q(end_of_cycle=True) | ~Q(level=None)
)
for competency in competencies:
EstablishmentCompetency.objects.get_or_create(
establishment=establishment,
competency=competency,
defaults={'is_required': True}
)
return JsonResponse(establishment_serializer.data, safe=False, status=status.HTTP_201_CREATED)
return JsonResponse(establishment_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)

View File

@ -1,5 +1,6 @@
from django.db import models
from Auth.models import Profile
from django.utils import timezone
class Messagerie(models.Model):
id = models.AutoField(primary_key=True)

View File

@ -165,31 +165,24 @@ class Competency(models.Model):
class EstablishmentCompetency(models.Model):
"""
Relation entre un établissement et une compétence.
Permet de définir quelles compétences sont sélectionnées par un établissement.
Peut référencer une compétence de référence OU une compétence custom propre à l'établissement.
"""
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE)
competency = models.ForeignKey(Competency, on_delete=models.CASCADE)
is_selected = models.BooleanField(default=False)
competency = models.ForeignKey(
Competency, on_delete=models.CASCADE, null=True, blank=True,
help_text="Compétence de référence (optionnelle si custom)"
)
custom_name = models.TextField(null=True, blank=True, help_text="Nom de la compétence custom")
custom_category = models.ForeignKey(
Category, on_delete=models.CASCADE, null=True, blank=True,
help_text="Catégorie de la compétence custom"
)
is_required = models.BooleanField(default=True)
class Meta:
unique_together = ('establishment', 'competency')
unique_together = ('establishment', 'competency', 'custom_name', 'custom_category')
def __str__(self):
return f"{self.establishment.name} - {self.competency.name}"
# class StudentCompetency(models.Model):
# """
# Relation entre un élève et une compétence.
# Permet d'attribuer une note à un élève pour une compétence.
# """
# student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores')
# competency = models.ForeignKey(Competency, on_delete=models.CASCADE, related_name='student_scores')
# score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) # Note attribuée
# comment = models.TextField(blank=True, null=True) # Commentaire facultatif
# class Meta:
# unique_together = ('student', 'competency')
# def __str__(self):
# return f"{self.student} - {self.competency.name} - Score: {self.score}"
if self.competency:
return f"{self.establishment.name} - {self.competency.name}"
return f"{self.establishment.name} - {self.custom_name} (custom)"

View File

@ -1,13 +1,47 @@
from rest_framework import serializers
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode
from .models import (
Teacher,
Speciality,
SchoolClass,
Planning,
LEVEL_CHOICES,
Discount,
Fee,
PaymentPlan,
PaymentMode,
Domain,
Category,
Competency,
EstablishmentCompetency
)
from Auth.models import Profile, ProfileRole
from Subscriptions.models import Student
from Establishment.models import Establishment
from Auth.serializers import ProfileRoleSerializer
from N3wtSchool import settings, bdd
from N3wtSchool import settings
from django.utils import timezone
import pytz
class DomainSerializer(serializers.ModelSerializer):
class Meta:
model = Domain
fields = '__all__'
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = '__all__'
class CompetencySerializer(serializers.ModelSerializer):
class Meta:
model = Competency
fields = '__all__'
class EstablishmentCompetencySerializer(serializers.ModelSerializer):
class Meta:
model = EstablishmentCompetency
fields = '__all__'
class SpecialitySerializer(serializers.ModelSerializer):
updated_date_formatted = serializers.SerializerMethodField()

View File

@ -2,7 +2,8 @@ import json
import os
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from School.models import Domain, Category, Competency
from Establishment.models import Establishment
from School.models import Domain, Category, Competency, EstablishmentCompetency
@receiver(post_migrate)
def load_json_data(sender, **kwargs):
@ -32,15 +33,15 @@ def load_json_data(sender, **kwargs):
for domain_data in data['domaines']:
# Vérifiez si le domaine existe déjà
domain, created = Domain.objects.get_or_create(name=domain_data['nom'], cycle=cycle)
domain, _ = Domain.objects.get_or_create(name=domain_data['nom'], cycle=cycle)
for category_data in domain_data['categories']:
# Vérifiez si la catégorie existe déjà
category, created = Category.objects.get_or_create(name=category_data['nom'], domain=domain)
category, _ = Category.objects.get_or_create(name=category_data['nom'], domain=domain)
for competency_data in category_data['competences']:
# Vérifiez si la compétence existe déjà
Competency.objects.get_or_create(
competency, _ = Competency.objects.get_or_create(
name=competency_data['nom'],
end_of_cycle=competency_data.get('fin_cycle', False),
level=competency_data.get('niveau'),

View File

@ -1,22 +1,18 @@
from django.urls import path, re_path
from .views import (
TeacherListCreateView,
TeacherDetailView,
SpecialityListCreateView,
SpecialityDetailView,
SchoolClassListCreateView,
SchoolClassDetailView,
PlanningListCreateView,
PlanningDetailView,
FeeListCreateView,
FeeDetailView,
DiscountListCreateView,
DiscountDetailView,
PaymentPlanListCreateView,
PaymentPlanDetailView,
PaymentModeListCreateView,
PaymentModeDetailView,
TeacherListCreateView, TeacherDetailView,
SpecialityListCreateView, SpecialityDetailView,
SchoolClassListCreateView, SchoolClassDetailView,
PlanningListCreateView, PlanningDetailView,
FeeListCreateView, FeeDetailView,
DiscountListCreateView, DiscountDetailView,
PaymentPlanListCreateView, PaymentPlanDetailView,
PaymentModeListCreateView, PaymentModeDetailView,
DomainListCreateView, DomainDetailView,
CategoryListCreateView, CategoryDetailView,
CompetencyListCreateView, CompetencyDetailView,
EstablishmentCompetencyListCreateView, EstablishmentCompetencyDetailView,
)
urlpatterns = [
@ -43,4 +39,16 @@ urlpatterns = [
re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"),
re_path(r'^paymentModes/(?P<id>[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"),
re_path(r'^domains$', DomainListCreateView.as_view(), name="domain_list_create"),
re_path(r'^domains/(?P<id>[0-9]+)$', DomainDetailView.as_view(), name="domain_detail"),
re_path(r'^categories$', CategoryListCreateView.as_view(), name="category_list_create"),
re_path(r'^categories/(?P<id>[0-9]+)$', CategoryDetailView.as_view(), name="category_detail"),
re_path(r'^competencies$', CompetencyListCreateView.as_view(), name="competency_list_create"),
re_path(r'^competencies/(?P<id>[0-9]+)$', CompetencyDetailView.as_view(), name="competency_detail"),
re_path(r'^establishmentCompetencies$', EstablishmentCompetencyListCreateView.as_view(), name="establishment_competency_list_create"),
re_path(r'^establishmentCompetencies/(?P<id>[0-9]+)$', EstablishmentCompetencyDetailView.as_view(), name="establishment_competency_detail"),
]

View File

@ -12,7 +12,11 @@ from .models import (
Discount,
Fee,
PaymentPlan,
PaymentMode
PaymentMode,
Domain,
Category,
Competency,
EstablishmentCompetency
)
from .serializers import (
TeacherSerializer,
@ -22,9 +26,15 @@ from .serializers import (
DiscountSerializer,
FeeSerializer,
PaymentPlanSerializer,
PaymentModeSerializer
PaymentModeSerializer,
DomainSerializer,
CategorySerializer,
CompetencySerializer,
EstablishmentCompetencySerializer
)
from N3wtSchool.bdd import delete_object, getAllObjects, getObject
from django.db.models import Q
from collections import defaultdict
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
@ -411,3 +421,349 @@ class PaymentModeDetailView(APIView):
def delete(self, request, id):
return delete_object(PaymentMode, id)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class DomainListCreateView(APIView):
def get(self, request):
domains = Domain.objects.all()
serializer = DomainSerializer(domains, many=True)
return JsonResponse(serializer.data, safe=False)
def post(self, request):
data = JSONParser().parse(request)
serializer = DomainSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class DomainDetailView(APIView):
def get(self, request, id):
try:
domain = Domain.objects.get(id=id)
serializer = DomainSerializer(domain)
return JsonResponse(serializer.data, safe=False)
except Domain.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
try:
domain = Domain.objects.get(id=id)
except Domain.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
data = JSONParser().parse(request)
serializer = DomainSerializer(domain, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
try:
domain = Domain.objects.get(id=id)
domain.delete()
return JsonResponse({'message': 'Deleted'}, safe=False)
except Domain.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
# Répète la même logique pour Category, Competency, EstablishmentCompetency
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CategoryListCreateView(APIView):
def get(self, request):
categories = Category.objects.all()
serializer = CategorySerializer(categories, many=True)
return JsonResponse(serializer.data, safe=False)
def post(self, request):
data = JSONParser().parse(request)
serializer = CategorySerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CategoryDetailView(APIView):
def get(self, request, id):
try:
category = Category.objects.get(id=id)
serializer = CategorySerializer(category)
return JsonResponse(serializer.data, safe=False)
except Category.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
try:
category = Category.objects.get(id=id)
except Category.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
data = JSONParser().parse(request)
serializer = CategorySerializer(category, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
try:
category = Category.objects.get(id=id)
category.delete()
return JsonResponse({'message': 'Deleted'}, safe=False)
except Category.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CompetencyListCreateView(APIView):
def get(self, request):
cycle = request.GET.get('cycle')
if cycle is None:
return JsonResponse({'error': 'cycle est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
competencies_list = getAllObjects(Competency)
competencies_list = competencies_list.filter(
category__domain__cycle=cycle
).distinct()
serializer = CompetencySerializer(competencies_list, many=True)
print(f'len : {competencies_list.count()}')
return JsonResponse(serializer.data, safe=False)
def post(self, request):
data = JSONParser().parse(request)
serializer = CompetencySerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CompetencyDetailView(APIView):
def get(self, request, id):
try:
competency = Competency.objects.get(id=id)
serializer = CompetencySerializer(competency)
return JsonResponse(serializer.data, safe=False)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
try:
competency = Competency.objects.get(id=id)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
data = JSONParser().parse(request)
serializer = CompetencySerializer(competency, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
try:
competency = Competency.objects.get(id=id)
competency.delete()
return JsonResponse({'message': 'Deleted'}, safe=False)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class EstablishmentCompetencyListCreateView(APIView):
def get(self, request):
establishment_id = request.GET.get('establishment_id')
cycle = request.GET.get('cycle')
if not establishment_id or not cycle:
return JsonResponse({'error': 'establishment_id et cycle sont requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
# Toutes les compétences du cycle
competencies = Competency.objects.filter(category__domain__cycle=cycle).select_related('category', 'category__domain')
# Récupérer les EstablishmentCompetency pour l'établissement et le cycle
ec_qs = EstablishmentCompetency.objects.filter(
Q(competency__in=competencies) | Q(competency__isnull=True),
establishment_id=establishment_id
).select_related('competency', 'custom_category')
# Required = compétences de référence requises
required_ids = set(ec_qs.filter(is_required=True, competency__isnull=False).values_list('competency_id', flat=True))
# Custom = compétences custom (pas de lien vers Competency)
custom_ecs = ec_qs.filter(is_required=False, competency__isnull=True)
# Organisation par domaine > catégorie
result = []
selected_count = 0
domaines = Domain.objects.filter(cycle=cycle)
for domaine in domaines:
domaine_dict = {
"domaine_id": domaine.id,
"domaine_nom": domaine.name,
"categories": []
}
categories = domaine.categories.all()
for categorie in categories:
categorie_dict = {
"categorie_id": categorie.id,
"categorie_nom": categorie.name,
"competences": []
}
# Liste des noms de compétences custom pour cette catégorie
custom_for_cat = custom_ecs.filter(custom_category=categorie)
custom_names = set(ec.custom_name.strip().lower() for ec in custom_for_cat if ec.custom_name)
# Compétences de référence (on saute celles qui sont déjà en custom)
competences = categorie.competencies.all()
for comp in competences:
if comp.name.strip().lower() in custom_names:
continue # On n'affiche pas la compétence de référence si une custom du même nom existe
if comp.id in required_ids:
state = "required"
selected_count += 1
else:
state = "none"
categorie_dict["competences"].append({
"competence_id": comp.id,
"nom": comp.name,
"state": state
})
# Ajout des compétences custom
for ec in custom_for_cat:
categorie_dict["competences"].append({
"competence_id": ec.id,
"nom": ec.custom_name,
"state": "custom"
})
selected_count += 1
domaine_dict["categories"].append(categorie_dict)
result.append(domaine_dict)
return JsonResponse({
"selected_count": selected_count,
"data": result
}, safe=False)
def post(self, request):
"""
Crée une ou plusieurs compétences custom pour un établissement (is_required=False)
Attendu dans le body :
[
{ "establishment_id": ..., "category_id": ..., "nom": ... },
...
]
"""
data = JSONParser().parse(request)
# Si data est un dict (un seul objet), on le met dans une liste
if isinstance(data, dict):
data = [data]
created = []
errors = []
for item in data:
establishment_id = item.get("establishment_id")
category_id = item.get("category_id")
nom = item.get("nom")
if not establishment_id or not category_id or not nom:
errors.append({"error": "establishment_id, category_id et nom sont requis", "item": item})
continue
try:
category = Category.objects.get(id=category_id)
# Vérifier si une compétence custom du même nom existe déjà pour cet établissement et cette catégorie
ec_exists = EstablishmentCompetency.objects.filter(
establishment_id=establishment_id,
competency__isnull=True,
custom_name=nom,
).exists()
if ec_exists:
errors.append({"error": "Une compétence custom de ce nom existe déjà pour cet établissement", "item": item})
continue
ec = EstablishmentCompetency.objects.create(
establishment_id=establishment_id,
competency=None,
custom_name=nom,
custom_category=category,
is_required=False
)
created.append({
"competence_id": ec.id,
"nom": ec.custom_name,
"state": "custom"
})
except Exception as e:
errors.append({"error": str(e), "item": item})
status_code = status.HTTP_201_CREATED if created else status.HTTP_400_BAD_REQUEST
return JsonResponse({
"created": created,
"errors": errors
}, status=status_code, safe=False)
def delete(self, request):
"""
Supprime une ou plusieurs compétences custom (EstablishmentCompetency) à partir d'une liste d'IDs.
Attendu dans le body :
{
"ids": [1, 2, 3, ...]
}
"""
data = JSONParser().parse(request)
ids = data.get("ids", [])
deleted = []
errors = []
for ec_id in ids:
try:
ec = EstablishmentCompetency.objects.get(id=ec_id)
ec.delete()
deleted.append(ec_id)
except EstablishmentCompetency.DoesNotExist:
errors.append({"id": ec_id, "error": "No object found"})
return JsonResponse({
"deleted": deleted,
"errors": errors
}, safe=False)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class EstablishmentCompetencyDetailView(APIView):
def get(self, request, id):
try:
ec = EstablishmentCompetency.objects.get(id=id)
serializer = EstablishmentCompetencySerializer(ec)
return JsonResponse(serializer.data, safe=False)
except EstablishmentCompetency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
try:
ec = EstablishmentCompetency.objects.get(id=id)
except EstablishmentCompetency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
data = JSONParser().parse(request)
serializer = EstablishmentCompetencySerializer(ec, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
try:
ec = EstablishmentCompetency.objects.get(id=id)
ec.delete()
return JsonResponse({'message': 'Deleted'}, safe=False)
except EstablishmentCompetency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)