init
This commit is contained in:
426
backend-django/backend/views/scoring.py
Normal file
426
backend-django/backend/views/scoring.py
Normal file
@@ -0,0 +1,426 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import pandas as pd
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.db.transaction import atomic
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..models import Administre, StatutPamChoices
|
||||
from ..models import AvisPosteChoices as AvisPoste
|
||||
from ..models import Poste, SousVivier
|
||||
from ..models.calcul import Calcul
|
||||
from ..models.calcul import StatutCalculChoices as StatutCalcul
|
||||
from ..serializers import ScoringSelectifValidator, ScoringValidator
|
||||
from ..utils.decorators import class_logger
|
||||
from ..utils_calcul import lancer_calculs, lancer_calculSelectif
|
||||
from .commun import (GestionnairePermission, api_exception,
|
||||
execution_time_viewset, query_count_viewset)
|
||||
import _json
|
||||
|
||||
|
||||
# @execution_time_viewset
|
||||
# @query_count_viewset
|
||||
@class_logger
|
||||
class ScoringView(APIView):
|
||||
"""
|
||||
Cette classe est dédiée au vue de lancement pour le scoring et au calcul des scores des administrés à muter avec les postes à pourvoir.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated, GestionnairePermission]
|
||||
|
||||
def __recuperer_chemin_requete(self, request: Request) -> str:
|
||||
""" récupère et reconstruit le chemin de la requête (mais pas forcément tel que le client la connaît)
|
||||
|
||||
:type request: rest_framework.request.Request
|
||||
:param request: Request contenant le port et l'adrresse ip du serveur
|
||||
|
||||
:return: - **return** (*string*): une chaine de caractère contenant le statut modifié du calcul.
|
||||
|
||||
"""
|
||||
|
||||
port = request.get_port()
|
||||
return f"{request.scheme}://localhost{':' + port if port else ''}{request.path}"
|
||||
|
||||
|
||||
def __appeler_api_depiler_calculs(self, request: Request) -> None:
|
||||
""" appelle l'API pour lancer le prochain calcul en attente, ce qui permet d'attribuer l'opération à un worker indéfini
|
||||
:type request: rest_framework.request.Request
|
||||
:param request: Request contenant le chemin de la requète
|
||||
"""
|
||||
url = self.__recuperer_chemin_requete(request)
|
||||
try:
|
||||
requests.patch(url, headers=request.headers, cookies=request.COOKIES, timeout=0.001)
|
||||
except requests.exceptions.ReadTimeout:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.logger.debug("échec de l'appel à %s: %s", url, e)
|
||||
|
||||
@atomic
|
||||
def __creer_calcul(self, pam_id: str, sv_id: str) -> Calcul:
|
||||
""" crée un calcul en base sauf s'il existe déjà un calcul en cours ou en attente pour le même sous-vivier
|
||||
Raises
|
||||
------
|
||||
ApiException:
|
||||
un calcul est déjà en cours ou en attente pour le même sous-vivier
|
||||
IntegrityError:
|
||||
la création a échoué
|
||||
|
||||
:type request: rest_framework.request.Request
|
||||
:type sv_id: str
|
||||
:param sv_id: clé primaire du sous vivier
|
||||
:return: - **Object** (*Object*): retourne l'objet calcul crée
|
||||
"""
|
||||
|
||||
Statut = StatutCalcul
|
||||
COL_ID = 'id'
|
||||
COL_SV = 'sous_vivier_id'
|
||||
COL_PAM = 'pam_id'
|
||||
COL_STATUT = 'ca_statut'
|
||||
COL_DEBUT = 'ca_date_debut'
|
||||
|
||||
colonnes = (COL_ID, COL_SV, COL_PAM, COL_DEBUT, COL_STATUT)
|
||||
df = pd.DataFrame.from_records(Calcul.objects.values_list(*colonnes), columns=colonnes)
|
||||
|
||||
en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values
|
||||
if sv_id in en_cours:
|
||||
raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-vivier')
|
||||
|
||||
en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_SV].values
|
||||
if sv_id in en_attente:
|
||||
raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en attente pour ce sous-vivier')
|
||||
|
||||
if en_attente.size:
|
||||
calcul_autorise = False
|
||||
else:
|
||||
nb_max = getattr(settings, 'MAX_CALCULS', None)
|
||||
calcul_autorise = not isinstance(nb_max, int) or len(en_cours) < nb_max
|
||||
|
||||
date_debut = timezone.now()
|
||||
Calcul.objects.filter(Q(id=str(sv_id)+str(pam_id)) & Q(pam_id=pam_id) & Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete()
|
||||
if calcul_autorise:
|
||||
return Calcul.objects.create(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_COURS)
|
||||
return Calcul.objects.create(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_ATTENTE)
|
||||
|
||||
@atomic
|
||||
def __creer_calculSelectif(self, sv_id: str, pam_id: str, l_a_id: list, l_p_id: list) -> Calcul:
|
||||
""" crée un calcul en base sauf s'il existe déjà un calcul en cours ou en attente pour le même sous-vivier
|
||||
Raises
|
||||
------
|
||||
ApiException:
|
||||
un calcul est déjà en cours ou en attente pour le même sous-vivier
|
||||
IntegrityError:
|
||||
la création a échoué
|
||||
|
||||
:type request: rest_framework.request.Request
|
||||
:type sv_id: str
|
||||
:param sv_id: clé primaire du sous vivier
|
||||
:return: - **Object** (*Object*): retourne l'objet calcul crée
|
||||
"""
|
||||
|
||||
Statut = StatutCalcul
|
||||
COL_ID = 'id'
|
||||
COL_SV = 'sous_vivier_id'
|
||||
COL_STATUT = 'ca_statut'
|
||||
COL_DEBUT = 'ca_date_debut'
|
||||
COL_PAM = 'pam_id'
|
||||
|
||||
colonnes = (COL_ID, COL_SV, COL_DEBUT, COL_STATUT, COL_PAM)
|
||||
df = pd.DataFrame.from_records(Calcul.objects.values_list(*colonnes), columns=colonnes)
|
||||
|
||||
|
||||
en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values
|
||||
|
||||
if l_a_id and l_p_id:
|
||||
if sv_id in en_cours:
|
||||
raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-ensemble')
|
||||
|
||||
en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_SV].values
|
||||
if sv_id in en_attente:
|
||||
raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en attente pour ce sous-ensemble')
|
||||
|
||||
if en_attente.size:
|
||||
calcul_autorise = False
|
||||
else:
|
||||
nb_max = getattr(settings, 'MAX_CALCULS', None)
|
||||
calcul_autorise = not isinstance(nb_max, int) or len(en_cours) < nb_max
|
||||
|
||||
date_debut = timezone.now()
|
||||
Calcul.objects.filter(Q(id=str(sv_id)+str(pam_id)+'selectif') & Q(pam_id=pam_id) & Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete()
|
||||
if calcul_autorise:
|
||||
return Calcul.objects.create(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_COURS)
|
||||
return Calcul.objects.create(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_ATTENTE)
|
||||
|
||||
@atomic
|
||||
def __modifier_statut_premier_calcul_en_attente(self) -> Optional[Tuple[str, datetime]]:
|
||||
""" lance le calcul en attente le plus ancien (si possible). Il ne s'agit que d'une modification en base.
|
||||
(ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé
|
||||
:type request: rest_framework.request.Request
|
||||
:type sv_id: str
|
||||
:param sv_id: clé primaire du sous vivier
|
||||
:type ca_date_debut : date
|
||||
:param ca_date_debut: date de début calcul en attente
|
||||
:return: - **Object** (*Object*): retourne l'objet calcul crée
|
||||
Raises
|
||||
------
|
||||
ApiException:
|
||||
un calcul est déjà en cours ou en attente pour le même sous-vivier
|
||||
IntegrityError:
|
||||
la création a échoué
|
||||
"""
|
||||
|
||||
Statut = StatutCalcul
|
||||
COL_ID = 'id'
|
||||
COL_SV = 'sous_vivier_id'
|
||||
COL_STATUT = 'ca_statut'
|
||||
COL_DEBUT = 'ca_date_debut'
|
||||
COL_PAM = 'pam_id'
|
||||
|
||||
|
||||
colonnes = (COL_ID, COL_SV, COL_PAM, COL_DEBUT, COL_STATUT)
|
||||
df = pd.DataFrame.from_records(Calcul.objects.order_by(COL_DEBUT).values_list(*colonnes), columns=colonnes)
|
||||
|
||||
en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE]
|
||||
if en_attente.empty:
|
||||
return None
|
||||
|
||||
en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values
|
||||
nb_max = getattr(settings, 'MAX_CALCULS', None)
|
||||
calcul_autorise = not isinstance(nb_max, int) or en_cours.size < nb_max
|
||||
if not calcul_autorise:
|
||||
return None
|
||||
|
||||
a_lancer = en_attente.iloc[0]
|
||||
sv_id = a_lancer[COL_ID]
|
||||
date_attente = a_lancer[COL_DEBUT]
|
||||
date_debut = timezone.now()
|
||||
|
||||
# la date de début est mise à jour aussi pour éviter de compter le temps d'attente
|
||||
mis_a_jour = (
|
||||
Calcul.objects
|
||||
.filter(sous_vivier_id=sv_id, ca_date_debut=date_attente, ca_statut=Statut.EN_ATTENTE)
|
||||
.update(ca_date_debut=date_debut, ca_statut=Statut.EN_COURS)
|
||||
)
|
||||
return (sv_id, date_debut) if mis_a_jour else None
|
||||
|
||||
|
||||
def __executer_calcul(self, pam_id: str,sv_id: str, date_debut: datetime) -> Calcul:
|
||||
""" exécute un calcul (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé
|
||||
:type request: rest_framework.request.Request
|
||||
:type sv_id: str
|
||||
:param sv_id: clé primaire du sous vivier
|
||||
:type ca_date_debut : date
|
||||
:param ca_date_debut: date de début calcul en attente
|
||||
:return: - **Object** (*Object*): retourne l'objet calcul ainsi que son statut
|
||||
"""
|
||||
|
||||
Statut = StatutCalcul
|
||||
qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut)
|
||||
qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS)
|
||||
try:
|
||||
lancer_calculs(pam_id,sv_id)
|
||||
except SystemExit:
|
||||
self.logger.info("OGURE M'A TUER")
|
||||
qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE)
|
||||
raise
|
||||
except Exception as e:
|
||||
qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut)
|
||||
if str(e) == "Arret du calcul":
|
||||
qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE)
|
||||
raise
|
||||
else:
|
||||
qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ERREUR)
|
||||
raise
|
||||
qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE)
|
||||
return qs_calcul.first()
|
||||
|
||||
def __executer_calculSelectif(self, sv_id: str, pam_id: str, l_a_id: list, l_p_id: list, date_debut: datetime) -> Calcul:
|
||||
""" exécute un calcul (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé
|
||||
:type request: rest_framework.request.Request
|
||||
:type sv_id: str
|
||||
:param sv_id: clé primaire du sous vivier
|
||||
:type ca_date_debut : date
|
||||
:param ca_date_debut: date de début calcul en attente
|
||||
:return: - **Object** (*Object*): retourne l'objet calcul ainsi que son statut
|
||||
"""
|
||||
|
||||
Statut = StatutCalcul
|
||||
qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id)+'selectif',sous_vivier_id=sv_id, pam_id=pam_id, ca_date_debut=date_debut)
|
||||
qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS)
|
||||
try:
|
||||
lancer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id)
|
||||
|
||||
except SystemExit:
|
||||
qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE)
|
||||
raise
|
||||
except Exception as e:
|
||||
qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id)+'selectif',sous_vivier_id=sv_id, pam_id=pam_id,ca_date_debut=date_debut)
|
||||
if str(e) == "Arret du calcul":
|
||||
qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE)
|
||||
raise
|
||||
else:
|
||||
qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ERREUR)
|
||||
raise
|
||||
qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE)
|
||||
return qs_calcul.first()
|
||||
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
"""La fonction get vérifie s'il y a un calcul en cours et renvoie des informations sur le calcul.
|
||||
:type request: rest_framework.request.Request
|
||||
:param request: Request contenant l'identifiant du sous vivier
|
||||
:return: - réponse contenant les informations sur le calcul comme statut, date_debut, date_fin, administres et postes.
|
||||
"""
|
||||
debut = None
|
||||
fin = None
|
||||
administres = 0
|
||||
postes = 0
|
||||
try:
|
||||
sv_id = request.query_params['sous_vivier_id']
|
||||
pam_id = request.query_params['pam_id']
|
||||
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
|
||||
debut = calcul.ca_date_debut
|
||||
fin = calcul.ca_date_fin
|
||||
administres_statuts = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER]
|
||||
administres = Administre.objects.filter(pam__pam_id=pam_id,sous_vivier_id=sv_id,a_statut_pam__in=administres_statuts).count()
|
||||
postes = Poste.objects.values_list('p_avis').filter(Q(p_pam__pam_id=pam_id) & Q(sous_viviers=sv_id) & Q(p_avis__in=[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4])).count()
|
||||
statut = calcul.ca_statut
|
||||
statut_pourcentage = calcul.ca_statut_pourcentage
|
||||
|
||||
except:
|
||||
statut = StatutCalcul.AUCUN
|
||||
statut_pourcentage = 0
|
||||
|
||||
return Response(
|
||||
{"statut": statut, "statut_pourcentage": statut_pourcentage, "date_debut": debut, "date_fin": fin, "administres": administres, "postes": postes})
|
||||
|
||||
def patch(self, request: Request, id=None) -> Response:
|
||||
"""La fonction patch est une méthode privée qui execute des méthodes privées qui exécute le calcul et le dépile.
|
||||
:type request: rest_framework.request.Request
|
||||
:param request: Request contenant l'identifiant du sous vivier et la date de dbut du calcul
|
||||
"""
|
||||
Statut = StatutCalcul
|
||||
try:
|
||||
id_et_debut = self.__modifier_statut_premier_calcul_en_attente()
|
||||
if id_et_debut is None:
|
||||
return Response({'message': "aucun calcul n'a été lancé"})
|
||||
|
||||
try:
|
||||
calcul = self.__executer_calcul(id_et_debut[0], id_et_debut[1])
|
||||
return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN})
|
||||
finally:
|
||||
self.__appeler_api_depiler_calculs(request)
|
||||
except (Http404, APIException):
|
||||
raise
|
||||
except SystemExit:
|
||||
raise APIException('lancement du prochain calcul arrêté')
|
||||
except:
|
||||
message = 'impossible de lancer le prochain calcul en attente'
|
||||
self.logger.exception(message)
|
||||
raise APIException(message)
|
||||
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
""" lance le calcul sur un sous vivier
|
||||
:type request: rest_framework.request.Request
|
||||
:param request: Request contenant l'identifiant du sous vivier
|
||||
:type sv_id: str
|
||||
:param sv_id: clé primaire du sous vivier
|
||||
:return: - **return** (*Response*): retourne le statut des postes
|
||||
"""
|
||||
try:
|
||||
# Gestion des 2 cas, calculs sélectifs ou calculs sur l'ensemble du sous-vivier
|
||||
if 'administre_id' in request.data.keys() and 'poste_id' in request.data.keys():
|
||||
Statut = StatutCalcul
|
||||
validator = ScoringSelectifValidator(data=request.data)
|
||||
validator.is_valid(raise_exception=True)
|
||||
sv_id = validator.validated_data.get('sous_vivier_id')
|
||||
pam_id = validator.validated_data.get('pam_id')
|
||||
get_object_or_404(SousVivier.objects, sv_id=sv_id)
|
||||
l_a_id = validator.validated_data.get('administre_id')
|
||||
l_p_id = validator.validated_data.get('poste_id')
|
||||
# calcul
|
||||
calcul = self.__creer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id)
|
||||
if calcul.ca_statut == Statut.EN_COURS:
|
||||
try:
|
||||
calcul = self.__executer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id, calcul.ca_date_debut)
|
||||
except Exception as e:
|
||||
Statut= Statut.ERREUR
|
||||
calcul.ca_statut = Statut
|
||||
calcul.save()
|
||||
error = str(e).split(',')
|
||||
return JsonResponse({'info': error[0]})
|
||||
finally:
|
||||
self.__appeler_api_depiler_calculs(request)
|
||||
|
||||
else:
|
||||
Statut = StatutCalcul
|
||||
validator = ScoringValidator(data=request.data)
|
||||
# validation
|
||||
validator.is_valid(raise_exception=True)
|
||||
sv_id = validator.validated_data.get('sous_vivier_id')
|
||||
pam_id = validator.validated_data.get('pam_id')
|
||||
get_object_or_404(SousVivier.objects, sv_id=sv_id)
|
||||
# calcul
|
||||
calcul = self.__creer_calcul(pam_id,sv_id)
|
||||
if calcul.ca_statut == Statut.EN_COURS:
|
||||
try:
|
||||
calcul = self.__executer_calcul(pam_id,sv_id, calcul.ca_date_debut)
|
||||
except Exception as e:
|
||||
|
||||
Statut= Statut.ERREUR
|
||||
calcul.ca_statut = Statut
|
||||
calcul.save()
|
||||
error = str(e).split(',')
|
||||
return JsonResponse({'info': error[0]})
|
||||
|
||||
finally:
|
||||
self.__appeler_api_depiler_calculs(request)
|
||||
return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN})
|
||||
except (Http404, APIException):
|
||||
raise
|
||||
except SystemExit:
|
||||
raise APIException('calcul arrêté')
|
||||
except:
|
||||
message = 'impossible de lancer le calcul'
|
||||
self.logger.exception(message)
|
||||
raise APIException(message)
|
||||
|
||||
|
||||
@class_logger
|
||||
@execution_time_viewset
|
||||
@query_count_viewset
|
||||
class ArretCalcul(APIView):
|
||||
"""
|
||||
Cette classe est dédiée a l'arrêt du calcul.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated, GestionnairePermission]
|
||||
|
||||
def post(self, request):
|
||||
"""La fonction post change le statut d'un calcul à EN_ATTENTE_ARRET
|
||||
|
||||
:type request: rest_framework.request.Request
|
||||
:param request: Request contenant l'identifiant du sous vivier
|
||||
|
||||
:return: - **return** (*json*): json contenant le statut modifié du calcul.
|
||||
"""
|
||||
Statut = StatutCalcul
|
||||
sv_id = request.data['sous_vivier_id']
|
||||
pam_id = request.data['pam_id']
|
||||
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
|
||||
if Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id).exists() and calcul.ca_statut == Statut.EN_COURS:
|
||||
calcul.ca_statut = Statut.EN_ATTENTE_ARRET
|
||||
calcul.save()
|
||||
elif Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id).exists() and calcul.ca_statut == Statut.EN_ATTENTE:
|
||||
calcul.ca_statut = Statut.TERMINE
|
||||
calcul.save()
|
||||
return Response({"statut": calcul.ca_statut})
|
||||
Reference in New Issue
Block a user