This commit is contained in:
2022-11-08 21:19:51 +01:00
commit 4c456eafc3
160 changed files with 21472 additions and 0 deletions

View 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})