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