import numpy as np from asgiref.sync import sync_to_async from django.db.models import ManyToOneRel, Window, F, Q, Sum, Case, When, IntegerField, Prefetch from django.db.models.functions import Rank from django.forms import model_to_dict from django.http import JsonResponse from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics, viewsets, serializers, status from rest_framework import viewsets, status from rest_framework.views import APIView from rest_framework.response import Response import pandas as pd from . import constants from django.db.models import Q from .filters import AdministreFilter, RelatedOrderingFilter, PosteFilter from .paginations import HeavyDataPagination from .serializers import AdministreSerializer, FileCompetenceSerializer, FmobSerializer, FormationEmploiSerializer, \ PcpFeGroupeSerializer, \ PosteSerializer, \ NotationSerializer, MarqueSerializer, MarquesGroupeSerializer, FileSerializer, \ FiliereSerializer, DomaineSerializer, FileSVSerializer, DecisionSerializer, \ SousVivierAssociationSerializer, ScoringValidator, AlimentationReferentielSerializer from .models import Administre, Competence, FMOB, FormationEmploi, PcpFeGroupe, Poste, Notation, \ PreferencesListe, Marque, MarquesGroupe, \ Filiere, Domaine, SousVivier, Grade, CustomUser, Decision, Calcul, SousVivierAssociation, \ StatutPamChoices as StatutPam from .utils import cleanString, impact_decisions from .utils_extraction import to_table_administres, to_table_domaines, to_table_fe, to_table_filieres, \ to_table_fmob, to_table_fonctions, to_table_garnisons, to_table_grades, \ to_table_fmob, to_table_fonctions, to_table_garnisons, to_table_grades, \ to_table_liste_preference, \ to_table_groupesMarques, to_table_marques, to_table_postes, process_files, open_excel from .utils_insertion import insert_Administre, insert_Domaine, insert_FMOB, insert_Filiere, insert_Fonction, \ insert_FormationEmploi, insert_Garnison, insert_Grade, insert_MarquesGroupe, insert_Notation, \ insert_Marque, insert_Poste, insert_liste_preference, insert_matching from .utils_matching import matching_parfait, preprocess_matching from .utils_calcul import lancer_calculs, notations from .reporting import poste_vacant_vivier, reporting_taux_armement_pcp, reporting_taux_armement_gestionnaire, \ reporting_suivi_pam_admin, \ reporting_suivi_pam_poste import json from django.shortcuts import render from django.contrib.auth.decorators import login_required from datetime import datetime, date from django.conf import settings from django.db.transaction import atomic from django.http import Http404 from rest_framework import serializers, status from rest_framework.exceptions import APIException from typing import Optional, Tuple import logging import requests import time logger = logging.getLogger(__name__) def api_exception(status_code: int, message: Optional[str] = None) -> APIException: return type('MyException', (APIException,), {'status_code': status_code, 'default_detail': message}) # Vue de lancement pour le scoring # Calcul les scores des administrés à muter pour les postes à pourvoir et stocke les résultats dans la table Calcul 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. """ def __recuperer_chemin_requete(self, 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): """ 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) headers = request.headers cookies = request.COOKIES try: requests.patch(url, headers=request.headers, cookies=request.COOKIES, timeout=0.001) except requests.exceptions.ReadTimeout: pass except Exception as e: logger.debug("échec de l'appel à %s: %s", url, e) @atomic def __creer_calcul(self, 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 = Calcul.Statut COL_ID = 'sous_vivier_id' COL_STATUT = 'ca_statut' COL_DEBUT = 'ca_date_debut' colonnes = (COL_ID, 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_ID].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_ID].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(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete() if calcul_autorise: return Calcul.objects.create(sous_vivier_id=sv_id, ca_date_debut=date_debut,ca_statut_pourcentage =0 , ca_statut=Statut.EN_COURS) return Calcul.objects.create(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 = Calcul.Statut COL_ID = 'sous_vivier_id' COL_STATUT = 'ca_statut' COL_DEBUT = 'ca_date_debut' colonnes = (COL_ID, 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_ID].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, 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 = Calcul.Statut COL_ID = 'sous_vivier_id' COL_STATUT = 'ca_statut' COL_DEBUT = 'ca_date_debut' qs_calcul = Calcul.objects.filter(sous_vivier_id=sv_id, ca_date_debut=date_debut) qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS) try: lancer_calculs(sv_id) except SystemExit: 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(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.ARRETER) 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): """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: - **return** (*JsonResponse*): json 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'] calcul = Calcul.objects.get(sous_vivier_id=sv_id) debut = calcul.ca_date_debut fin = calcul.ca_date_fin administres_statuts = ['A_MUTER'] administres = Administre.objects.filter(sous_vivier_id=sv_id, a_statut_pam__in=administres_statuts).count() postes = Poste.objects.filter(Q(sous_vivier_id=sv_id) & ( Q(p_nb_p1__gt=0) | Q(p_nb_p2__gt=0) | Q(p_nb_p3__gt=0) | Q(p_nb_p4__gt=0) | Q( p_nb_vacant__gt=0))).count() statut = calcul.ca_statut statut_pourcentage = calcul.ca_statut_pourcentage except: statut = "AUCUN" statut_pourcentage = 0 return JsonResponse( {"statut": statut, "statut_pourcentage": statut_pourcentage, "date_debut": debut, "date_fin": fin, "administres": administres, "postes": postes}) def patch(self, request, id=None): """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 = Calcul.Statut 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 api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, 'lancement du prochain calcul arrêté') except: message = 'impossible de lancer le prochain calcul en attente' logger.exception(message) raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message) def post(self, request): """ 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 """ Statut = Calcul.Statut try: # validation validator = ScoringValidator(data=request.data) validator.is_valid(raise_exception=True) sv_id = validator.validated_data.get('sous_vivier_id') generics.get_object_or_404(SousVivier.objects, sv_id=sv_id) # calcul calcul = self.__creer_calcul(sv_id) if calcul.ca_statut == Statut.EN_COURS: try: calcul = self.__executer_calcul(sv_id, calcul.ca_date_debut) 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 api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, 'calcul arrêté') except: message = 'impossible de lancer le calcul' logger.exception(message) raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message) class ArretCalul(APIView): """ Cette classe est dédiée a l'arret du calcul. """ 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. """ sv_id = request.data['sous_vivier_id'] calcul = Calcul.objects.get(sous_vivier_id=sv_id) if Calcul.objects.filter(sous_vivier_id=sv_id).exists() and calcul.ca_statut == 'EN_COURS': calcul.ca_statut = 'EN_ATTENTE_ARRET' calcul.save() elif Calcul.objects.filter(sous_vivier_id=sv_id).exists() and calcul.ca_statut == 'EN_ATTENTE': calcul.ca_statut = 'ARRETER' calcul.save() return JsonResponse( {"statut": calcul.ca_statut}) # Vue pour la fiche détaillée des administrés class FicheDetailleeView(APIView): """ Cette classe est dédiée au vue de la fiche détaillée des administrés """ def get(self, request): """La fonction get recupére les infos administrés :type request: rest_framework.request.Request :param request: Request contenant l'identifiant de l'administré :return: - **Response** (*objet*): informations concernant l'administré, informations sur le formulaire de mobilité. """ res = {"result": "error"} if 'administre_id' in request.query_params: administre_id = request.query_params['administre_id'] try: administre = Administre.objects.get(a_id_sap=administre_id) res = model_to_dict(administre) fmobs = FMOB.objects.filter(administre_id=administre_id).order_by('-fmob_millesime') res["fmob"] = None if fmobs.exists(): fmob_dict = model_to_dict(fmobs[0]) res["fmob"] = fmob_dict res["fe"] = None fe = list(FormationEmploi.objects.filter(fe_code=administre.formation_emploi_id).values( *('fe_code', 'garnison__gar_code_postal', 'garnison__gar_lieu', 'fe_libelle'))) # if fe.exists(): # fe = fe[0] # garnison_dict = model_to_dict() # res["garnison"] = garnison_dict if len(fe): res["fe"] = fe[0] res["conjoint"] = None conjoint = Administre.objects.filter(a_id_sap=administre.a_sap_conjoint) if conjoint.exists(): print('has conjoint') conjoint_dict = model_to_dict(conjoint[0]) conjoint_dict["fe"] = None fe_conjoint = list(FormationEmploi.objects.filter(fe_code=conjoint[0].formation_emploi_id).values( *('fe_code', 'garnison__gar_code_postal', 'garnison__gar_lieu', 'fe_libelle'))) if len(fe): conjoint_dict["fe"] = fe_conjoint[0] res["conjoint"] = conjoint_dict except Exception as e: print(e) print(e.args) print(res) # TODO: return error return Response(res) # Vue de calcul des indicateurs # Renvoie les indicateurs pour les vues : # - taux armement FE pour les PCP # - taux armement FE pour les gestionnaires # - suivi pam des gestionnaires pour les administrés # - suivi pam des gestionnaires pour les postes class ReportingView(APIView): """ Cette classe est dédiée au vue du calcul des indicateurs - taux armement FE pour les PCP - taux armement FE pour les gestionnaires - suivi pam des gestionnaires pour les administrés - suivi pam des gestionnaires pour les postes """ def get(self, request): """La fonction get renvoie les indicateurs :type request: rest_framework.request.Request :param request: Request contenant le type, sous_vivier id, niveau fonctinonel et l'id de la formation emploi. :return: - **return** (*json*): Si le type est SUIVI_PAM_GESTIONNAIRE le json contiendra l'information sur le suivi pam des gestionnaires pour les administrés et les postes. Si le type est TAUX_ARMEMENT_FE le json contiendra les indicateurs sur les taux d'armement. """ type = request.query_params["type"] sv_id = [] if 'sv_id' in request.query_params: sv_id = request.query_params["sv_id"] if 'vue' in request.query_params: vue = request.query_params["vue"] if "nf" in request.query_params: nf = request.query_params.getlist("nf") else: nf = ['1A', '1B', '1C', '2.', '3A', '3B', '3B NFS', '4.', '5A', '5B', '5C', '6A', '6B'] if 'fe_id' in request.query_params: # a voir fe_id = request.query_params.getlist('fe_id') else: fe_id = list(FormationEmploi.objects.values_list('fe_code', flat=True)) # Vues pour les indicateurs sur le suivi du pam if (type == "SUIVI_PAM_GESTIONNAIRE"): if "d_code" in request.query_params: d_id = request.query_params.getlist("d_code") else: filieres = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id', flat=True).distinct()) d_id = list(Filiere.objects.filter(f_code__in=filieres).values_list('domaine_id', flat=True).distinct()) if "f_code" in request.query_params: f_id = request.query_params.getlist("f_code") else: f_id = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id', flat=True).distinct()) if "categorie" in request.query_params: categorie = request.query_params.getlist("categorie") else: categorie = list( SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('sva_categorie', flat=True).distinct()) # résultats pour la vue suivi pam des gestionnaires pour les administrés nb_a_etudier, nb_a_muter, nb_a_maintenir, nb_non_etudie_administres, nb_a_partant, nb_a_non_dispo, nb_prepos_administres, nb_pos_administres, nb_omi_active_administres, nb_omi_en_cours_administres, reste_a_realiser_administres, reste_a_realiser_a_etudier, reste_a_realiser_a_muter = reporting_suivi_pam_admin( sv_id, f_id, d_id, nf, categorie) # résultats pour la vue suivi pam des gestionnaires pour les postes nb_p1, nb_p2, nb_p3, nb_p4, nb_gele, nb_non_etudie_postes, reste_a_realiser_postes, reste_a_realiser_p1, reste_a_realiser_p2, reste_a_realiser_p3, reste_a_realiser_p4, nb_prepos_postes, nb_pos_postes, nb_omi_active_postes, nb_omi_en_cours_postes = reporting_suivi_pam_poste( sv_id, f_id, d_id, nf, categorie) return JsonResponse( {"nb_a_etudier": nb_a_etudier, "nb_a_muter": nb_a_muter, "nb_a_maintenir": nb_a_maintenir, "nb_a_partant": nb_a_partant, "nb_a_non_dispo": nb_a_non_dispo, "nb_non_etudie_administres": nb_non_etudie_administres, "nb_prepos_administres": nb_prepos_administres, "nb_pos_administres": nb_pos_administres, "nb_omi_en_cours_administres": nb_omi_en_cours_administres, "nb_omi_active_administres": nb_omi_active_administres, "reste_a_realiser_administres": reste_a_realiser_administres, "reste_a_realiser_a_etudier": reste_a_realiser_a_etudier, "reste_a_realiser_a_muter": reste_a_realiser_a_muter, "nb_p1": nb_p1, "nb_p2": nb_p2, "nb_p3": nb_p3, "nb_p4": nb_p4, "nb_gele": nb_gele, "nb_non_etudie_postes": nb_non_etudie_postes, "reste_a_realiser_postes": reste_a_realiser_postes, "reste_a_realiser_p1": reste_a_realiser_p1, "reste_a_realiser_p2": reste_a_realiser_p2, "reste_a_realiser_p3": reste_a_realiser_p3, "reste_a_realiser_p4": reste_a_realiser_p4, "nb_prepos_postes": nb_prepos_postes, "nb_pos_postes": nb_pos_postes, "nb_omi_en_cours_postes": nb_omi_en_cours_postes, "nb_omi_active_postes": nb_omi_active_postes}, safe=False) # Vues pour les indicateurs sur les taux d'armement elif (type == 'TAUX_ARMEMENT_FE'): if "d_code" in request.query_params: d_id = request.query_params.getlist("d_code") else: d_id = list(Domaine.objects.all().values_list('d_code', flat=True)) if "f_code" in request.query_params: f_id = request.query_params.getlist("f_code") else: f_id = list(Filiere.objects.all().values_list('f_code', flat=True)) if "categorie" in request.query_params: categorie = request.query_params.getlist("categorie") else: categorie = ['MDR', 'SOFF', 'OFF', 'OGX'] if len(sv_id) == 0: # résultats pour la vue taux armement FE pour les PCP nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_pcp( fe_id, f_id, d_id, nf, categorie) else: # résultats pour la vue taux armement FE pour les gestionnaires nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_gestionnaire( fe_id, f_id, d_id, nf, categorie, sv_id) return JsonResponse({'nb_militaires_actuel': nb_militaires_actuel, "nb_postes_actuel": nb_postes_actuel, 'ecart_actuel': ecart_actuel, 'taux_armement_actuel': taux_armement_actuel, 'nb_militaires_entrants': nb_militaires_entrants, 'nb_militaires_sortants': nb_militaires_sortants, 'nb_militaires_projete': nb_militaires_projete, 'taux_armement_projete': taux_armement_projete, 'taux_armement_cible': taux_armement_cible}, safe=False) else: return JsonResponse({"autre vue": "autres indicateurs demandes"}) # Vue pour afficher le nombre de postes vacants par fe et sous-vivier class PosteVacantView(APIView): """ Cette classe est dédiée au vue pour afficher le nombre de postes vacants par fe et sous-vivier """ def get(self, request): """La fonction get renvoie les postes vaccants :type request: rest_framework.request.Request :param request: Requset contenant l'identifiant de la formation emploi et l'identifiant du sous-vivier. :return: - **return** (*json*): json contenant le nombre de poste vaccant du sous vivier. """ fe_id = request.query_params["fe_id"] sv_id = request.query_params["sv_id"] nb_poste_vacant_vivier = poste_vacant_vivier(sv_id, fe_id) return JsonResponse({'nb_poste_vacant_vivier': nb_poste_vacant_vivier}, safe=False) # Vue de chargement des sous-viviers : # - Charge et traite le fichier de définition des sous-viviers # - Attribue les sous-viviers présents aux administrés et postes correspondants class ChargementSVView(APIView): """ Cette classe est dédiée au vue de chargement des sous-viviers. Charge et traite le fichier de définition des sous-viviers et attribue les sous-viviers présents aux administrés et postes correspondants """ serializer_class = FileSVSerializer def get(self, request): """La fonction get renvoie une reponse contenant ok :type request: rest_framework.request.Request :param request: Requset :return: - **return** (*json*): json contenant "ok". """ return Response({"get": "ok"}) def post(self, request): """La fonction post charge les sous-viviers :type request: rest_framework.request.Request :param request: Request contenant le fichier SV :return: - **Response** (*Response*): Reponse contient la liste des sous-viviers créés, ignorés et où il y a eu une erreur. """ serializer = FileSVSerializer(data=request.data) if not serializer.is_valid(): return Response( data=serializer.errors, status=status.HTTP_400_BAD_REQUEST ) sv_file = request.data['SV'] sv_df = open_excel(sv_file, sheetname="GESTIONNAIRES", engine='openpyxl') sv = pd.DataFrame(columns=['gestionnaire_id_sap', 'f_code', 'asso_sv_categorie', 'arme']) for i in range(len(sv_df)): sv.loc[i] = [sv_df.iloc[i, 0], sv_df.iloc[i, 3], sv_df.iloc[i, 4], sv_df.iloc[i, 5]] sv.dropna(subset=['f_code'], inplace=True) sv['arme'] = sv['arme'].replace({np.nan: None}) sv.reset_index(drop=True, inplace=True) sv['asso_sv_categorie'].replace({"SOUS-OFFICIER": "SOFF", "OFFICIER": "OFF", "MILITAIRE DU RANG": "MDR"}, inplace=True) sv['sv_id'] = sv.f_code + '_' + sv.asso_sv_categorie sv.f_code = sv.f_code.apply(lambda x: x.split(',')) sv = sv.explode(column='f_code') sv.reset_index(drop=True, inplace=True) errors = [] created = [] ignored = [] sv.fillna(np.nan, inplace=True) sv.replace([np.nan], [None], inplace=True) print(sv.head()) for i in range(len(sv)): try: filiere = Filiere.objects.get(f_code=sv.at[i, "f_code"]) categorie = sv.at[i, "asso_sv_categorie"] asso_sv = SousVivierAssociation.objects.filter(sva_categorie=categorie, filiere__f_code=sv.at[i, "f_code"]) # Vérifier que filiere et catégorie n'ont pas déjà de sous vivier if asso_sv.count() == 0 and categorie is not None: # Créer le sous-vivier correspondant dans la table SousVivier sous_vivier = SousVivier(sv_id=sv.at[i, "sv_id"], sv_libelle=str(sv.at[i, "sv_id"])) print(model_to_dict(sous_vivier)) sous_vivier.save() print(sv.at[i, "sv_id"]) # Insertion du nouveau sous-vivier dans la table d'association (sans le droit arme dans un premier temps) sva = SousVivierAssociation(sva_id=i, sous_vivier_id=sous_vivier.sv_id, filiere_id=filiere.f_code, sva_categorie=categorie, sva_arme=sv.at[i, "arme"]) sva.save() # Mise à jour du sous_vivier_id pour les postes et administres ayant la filiere et la categorie en cours Administre.objects.filter(a_categorie=categorie, a_filiere_id=filiere.f_code).update( sous_vivier_id=sous_vivier.sv_id) Poste.objects.filter(p_categorie=categorie, p_filiere_id=filiere.f_code).update( sous_vivier_id=sous_vivier.sv_id) created.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"])) else: ignored.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"])) # Si l'association appartient déjà à un sous_vivier, on ne fait rien except Filiere.DoesNotExist: errors.append("Filiere " + sv.at[i, 'f_code'] + "non retrouvée") return Response({"created": created, "ignored": ignored, "errors": errors}) # Vue de chargement des competences : # - Charge et traite les fichiers de compétences # - Attribue les compétences présentes aux administrés et postes correspondants class ChargementCompetenceView(APIView): """ Cette classe est dédiée au vue de chargement des competences. Charge et traite les fichiers de compétences. Attribue les compétences présentes aux administrés et postes correspondants """ serializer_class = FileCompetenceSerializer def get(self, request): """La fonction get renvoie une reponse contenant ok :type request: rest_framework.request.Request :param request: Requset :return: - **return** (*json*): json contenant "ok". """ return Response({"get": "ok"}) def post(self, request): """ La fonction post charge les competences. :type request: rest_framework.request.Request :param request: Request contenant le fichier de competence :return: - **Response** (*Response*): Reponse contient un message de la réussite pour charger la compétence . """ serializer = FileCompetenceSerializer(data=request.data) if not serializer.is_valid(): return Response( data=serializer.errors, status=status.HTTP_400_BAD_REQUEST ) competence_file_v1 = request.data['competence_v1'] competence_file_v2 = request.data['competence_v2'] competence_poste_v1 = open_excel(competence_file_v1, sheetname='Feuil1', engine='openpyxl') competence_poste_v1.drop_duplicates(subset=['Macro compétence (0 à n par officier)'], inplace=True) competence_mili = open_excel(competence_file_v2, sheetname="MILITAIRE", engine='openpyxl') competence_mili.reset_index(drop=True, inplace=True) competence_poste = open_excel(competence_file_v2, sheetname='REO', engine='openpyxl') competence_poste.dropna(subset=['COMPETENCE 1'], inplace=True) competence_poste.reset_index(drop=True, inplace=True) print("Preprocessing OK") # Ajout des compétences du fichier version 1, dans la table Compétence for i in range(len(competence_poste_v1)): comp = Competence(comp_id=cleanString(competence_poste_v1.at[i, 'Macro compétence (0 à n par officier)']), comp_libelle=competence_poste_v1.at[i, 'Macro compétence (0 à n par officier)']) comp.save() print("Ajout des compétences du fichier 1 dans la table Competence --> OK") # Ajout des compétences du fichier version 2, dans la table Compétence competence_mili_1 = competence_mili[['COMPETENCE 1']].drop_duplicates().reset_index(drop=True) competence_mili_2 = competence_mili[['COMPETENCE 2']].drop_duplicates().reset_index(drop=True).rename( columns={'COMPETENCE 2': "COMPETENCE 1"}) competence_mili_3 = competence_mili[['COMPETENCE 2.1']].drop_duplicates().reset_index(drop=True).rename( columns={'COMPETENCE 2.1': "COMPETENCE 1"}) competence_poste_1 = competence_poste[['COMPETENCE 1']].drop_duplicates(subset=["COMPETENCE 1"]).reset_index( drop=True) total_competences = pd.concat([competence_mili_1, competence_mili_2, competence_mili_3, competence_poste_1]).drop_duplicates().reset_index(drop=True) total_competences.dropna(inplace=True) total_competences.reset_index(drop=True, inplace=True) for i in range(len(total_competences)): competence = Competence(comp_id=cleanString(total_competences.at[i, "COMPETENCE 1"]), comp_libelle=total_competences.at[i, 'COMPETENCE 1']) competence.save() print('Ajout des compétences du fichier 2 dans la table Competence --> OK') # Ajout des compétences du fichier 2 dans la table Poste print(competence_poste) for i in range(len(competence_poste)): postes = Poste.objects.filter(p_code_fonction=competence_poste.at[i, 'CODE FONCTION'], formation_emploi_id=competence_poste.at[i, 'CODE FE'], p_domaine=competence_poste.at[i, 'DOM EIP'], p_filiere=competence_poste.at[i, "FIL EIP"], p_nf=competence_poste.at[i, 'NR EIP']) postes.update(competence_id=cleanString(competence_poste.at[i, "COMPETENCE 1"])) print('Ajout des compétences du fichier 2 dans la table Poste --> OK') # Ajout des compétences du fichier 2 dans la table d'association Administrés et Compétences liste_admin = [] for i in range(len(competence_mili)): sap = competence_mili.at[i, 'N SAP'] admin_competences = "" if not pd.isna(competence_mili.at[i, 'COMPETENCE 1']): ca1_id = cleanString(competence_mili.at[i, 'COMPETENCE 1']) admin_competences = admin_competences + ca1_id + "," if not pd.isna(competence_mili.at[i, 'COMPETENCE 2']): ca2_id = cleanString(competence_mili.at[i, 'COMPETENCE 2']) admin_competences = admin_competences + ca2_id + "," if not pd.isna(competence_mili.at[i, 'COMPETENCE 2.1']): ca3_id = cleanString(competence_mili.at[i, 'COMPETENCE 2.1']) admin_competences = admin_competences + ca3_id + "," liste_admin.append(Administre(a_id_sap=sap, a_liste_id_competences=admin_competences)) Administre.objects.bulk_update(liste_admin, fields=['a_liste_id_competences']) print('Ajout des compétences du fichier 2 dans la table CompetenceAdministre --> OK') return JsonResponse({"msg": "Chargement competence ok"}) class CurrentUserView(APIView): """ Cette classe est dédiée au vue de l'utilisateur courant: """ def get(self, request): """La fonction get verifie si l'utilisateur est authentifié :type request: rest_framework.request.Request :param request: Request contenant l'identifiant de l'utilisateur :return: - **Response** (*json*): json contenant l'identité et le role de l'utilisateur. En plus de la formation emploi group et le sous-vivier specifique à cet utilisateur. """ if request.user.is_authenticated: current_user = CustomUser.objects.get(id=request.user.id) try: administre = model_to_dict(current_user.administre) except: administre = None current_user = model_to_dict(current_user, fields=[field.name for field in CustomUser._meta.get_fields() if field.name not in ["password", "logentry", "groups", "user_permissions"]]) current_user['sous_viviers'] = list(SousVivier.objects.filter(gestionnaire_id=request.user.id).values()) current_user['identite'] = administre try: pcpFeGroupe = model_to_dict(PcpFeGroupe.objects.get(gestionnaire_id=request.user.id)) except: pcpFeGroupe = None current_user['fe_groupe'] = pcpFeGroupe current_user['role'] = "ADMINISTRATEUR" if request.user.is_superuser else "GESTIONNAIRE" return Response(current_user) return Response({"msg": "Authentification requise"}) class ReferencesView(APIView): """ Cette classe est dédiée au vue de la reference. """ def get(self, request): """ La fonction get envoie les references :return: - **JsonReponse** (*json*): json contenant les groupes marques, les marques, les competences, les domaines et les filieres. """ groupesMarques = [obj.as_dict() for obj in MarquesGroupe.objects.all().order_by('gm_code')] marques = [obj.as_dict() for obj in Marque.objects.all().order_by('mar_code')] competences = [obj.as_dict() for obj in Competence.objects.all().order_by('comp_id')] domaines = [obj.as_dict() for obj in Domaine.objects.all().order_by('d_code')] filieres = [obj.as_dict() for obj in Filiere.objects.all().order_by('domaine__d_code', 'f_code')] # TODO: Vérifier la présence de l'ordre sur les postes' grades = [obj.as_dict() for obj in Grade.objects.all()] FEs = [obj.as_dict() for obj in FormationEmploi.objects.all().order_by('fe_code')] return JsonResponse( {"groupesMarques": groupesMarques, "marques": marques, "domaines": domaines, "filieres": filieres, "grades": grades, "competences": competences, "FEs": FEs}, safe=False) # Vue pour l'alimentation et l'update en BDD : # - Charge et traite les fichiers de données # - Remplit la base de données pour les modèles Domaines, Filieres, Grade, Fonction, Garnison, FormationEmploi, Marque, MarquesGroupe, Administre, Poste, FMOB, class AlimentationView(APIView): """ Cette classe est dédiée au vue de l'alimentation et l'update en BDD. Charge et traite les fichiers de données et remplit la base de données pour les modèles Domaines, Filieres, Grade, Fonction, Garnison, FormationEmploi, Marque, MarquesGroupe, Administre, Poste, FMOB. """ serializer_class = FileSerializer def get(self, request): """La fonction get renvoie une reponse contenant Formulaire d'alimentation d'OGURE NG :type request: rest_framework.request.Request :param request: Request :return: - **return** (*json*): json contenant "Formulaire d'alimentation d'OGURE NG". """ return Response("Formulaire d'alimentation d'OGURE NG") def post(self, request): """La fonction post charge les fichiers. :type request: rest_framework.request.Request :param request: Request contenant les fichiers :return: - **Response** (*Response*): Reponse contient les erreurs de chargement des données . """ # TODO: préparer une variable qui renvoie les erreurs sous 3 axes : données référentielles, administrés, postes serializer = FileSerializer(data=request.data) if not serializer.is_valid(): return Response( data=serializer.errors, status=status.HTTP_400_BAD_REQUEST ) logger.info('---------------------Upload begining---------------------------') start_time = time.time() file_donnees_bo = request.data['Donnees_BO_ADT'] file_zpropaf = request.data['ZPROPAF'] file_reo = request.data['REO'] file_fmob = request.data['FMOB'] file_filiere_domaine = request.data['domaine_filiere'] file_insee = request.data['insee_maping'] file_diplomes = request.data['diplomes'] # file_suivi_pam = request.data['suivi_pam'] logger.info('---------------------Upload ending-----------------------------') logger.debug("--------------upload time -- %d seconds ------------------------", time.time() - start_time) # ETL from xlsx source : Ordre : domaines, filieres, garnisons, fonctions, grades, fe, groupesMarques, marques, gestionnaires, administres, flag_pam_admnistres, postes, fmob, listePreferences filiere_domaine_df, donnees_bo_df, zpropaf_df, reo_df, fmob_df, femp_df, insee_df, diplomes_df = process_files( file_filiere_domaine, file_donnees_bo, file_zpropaf, file_reo, file_fmob, file_insee, file_diplomes) logger.info('---------------------Insert beginning---------------------------') start_time_insert = time.time() insert_Domaine(to_table_domaines(filiere_domaine_df)) logger.debug('1. Domaine -----------> Success') insert_Garnison(to_table_garnisons(insee_df, donnees_bo_df)) logger.debug('2. Garnison ----------> Success') insert_Fonction(to_table_fonctions(reo_df)) logger.debug('3. Fonction ----------> Success') insert_Grade() logger.debug('4. Grade -------------> Success') insert_Filiere(to_table_filieres(filiere_domaine_df)) logger.debug('5. Filières -----------> Success') insert_MarquesGroupe(to_table_groupesMarques()) logger.debug('6. MarquesGroupe -----> Success') insert_Marque(to_table_marques()) logger.debug('7. Marque ------------> Success') insert_FormationEmploi(to_table_fe(reo_df, donnees_bo_df)) logger.debug('8. FormationEmplois ---> Success') administres_list, administres_error = insert_Administre( to_table_administres(donnees_bo_df, zpropaf_df, insee_df, diplomes_df)) logger.debug('9. Administre --------> Success') postes_list = insert_Poste(to_table_postes(reo_df)) logger.debug('10. Poste --------------> Success') logger.debug('time: %d s', time.time() - start_time_insert) fmob_error = insert_FMOB(to_table_fmob(fmob_df, femp_df, zpropaf_df)) logger.debug('11. Fmob ----------------> Success') logger.info('---------------------Insert ending -----------------------------') logger.info('-------------------- Début des mises à jour --------------------') # administre : le statut PAM par défaut est A_TRAITER s'il y a un FMOB (Administre.objects .filter(a_statut_pam=StatutPam.A_ETUDIER, fmobs__isnull=False) .update(a_statut_pam=StatutPam.A_TRAITER)) logger.info('--------------------- Fin des mises à jour ---------------------') logger.debug("------------------insert time -- %d seconds --------------------", time.time() - start_time_insert) # # result = {"administres_error": administres_error, "fmob_error": fmob_error} # result = {"administres_error": "", "fmob_error": fmob_error} return Response({"administres_error": ""}) class AlimentationReferentielView(APIView): """ Vue pour alimenter la base à partir de référentiels """ serializer_class = AlimentationReferentielSerializer def get(self, request): return Response("Formulaire d'alimentation d'OGURE NG (référentiel") def post(self, request): """ Charge le(s) fichier(s) et met à jour la base. :param request: requête, contient les fichiers :type request: class:`rest_framework.request.Request` :raises: class:`rest_framework.exceptions.APIException` :return: réponse :rtype: class:`rest_framework.response.Response` """ try: validator = AlimentationReferentielSerializer(data=request.data) validator.is_valid(raise_exception=True) # récupération des fichiers referentiel_fe = request.data.get('referentiel_fe', None) if referentiel_fe: def process_referentiel_fe(referentiel) -> pd.DataFrame: """ Fonction de lecture du référentiel de FE. :param referentiel: Fichier du référentiel FE :type referentiel: XLSX :return: DataFrame :rtype: class:`pandas.DataFrame` """ return open_excel(referentiel, sheetname=0, engine='openpyxl') def convertir_fe(df: pd.DataFrame) -> pd.DataFrame: """ Fonction de conversion du DataFrame de FE. :param df: DataFrame du référentiel FE :type df: class:`pandas.DataFrame` :return: DataFrame :rtype: class:`pandas.DataFrame` """ col_pk_avant = 'FE CREDO' col_mere_avant = 'FE mère CREDO' col_zone_def_avant = 'Zone de Défense' Cols = FormationEmploi.Cols col_pk_apres = Cols.PK col_mere_apres = Cols.REL_MERE col_zone_def_apres = Cols.ZONE_DEFENSE return (df[[col_pk_avant, col_mere_avant, col_zone_def_avant]] .drop_duplicates(subset=col_pk_avant, keep='first') .rename(columns={ col_pk_avant: col_pk_apres, col_mere_avant: col_mere_apres, col_zone_def_avant: col_zone_def_apres })) def mettre_a_jour_fe(df: pd.DataFrame) -> None: """ Met à jour les FE base à partir du DataFrame de FE. :param df: DataFrame du référentiel FE :type df: class:`pandas.DataFrame` :return: DataFrame :rtype: class:`pandas.DataFrame` """ TypeModele = FormationEmploi Cols = TypeModele.Cols champs_maj = (Cols.REL_MERE, Cols.ZONE_DEFENSE) modeles_en_base = { m.pk: m for m in TypeModele.objects.select_related(Cols.REL_MERE).only('pk', *champs_maj) } taille_batch = 100 dict_create = {} dict_update = {} for rec in df.to_dict('records'): pk = str(rec.get(Cols.PK, None)) id_mere = rec.get(Cols.REL_MERE, None) mere = modeles_en_base.get(str(id_mere), None) if id_mere else None zone_defense = rec.get(Cols.ZONE_DEFENSE, None) en_base = modeles_en_base.get(pk, None) if not en_base or (mere != getattr(en_base, Cols.REL_MERE, None) or zone_defense != getattr(en_base, Cols.ZONE_DEFENSE, None)): try: modele = TypeModele(pk=pk, mere=mere, zone_defense=zone_defense) except Exception as e: raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" a échoué') from e (dict_create if not en_base else dict_update).setdefault(pk, modele) # TODO pas de création pour l'instant (ni de suppression) # if dict_create: # FormationEmploi.objects.bulk_create(dict_create.values(), batch_size=taille_batch) if dict_update and champs_maj: FormationEmploi.objects.bulk_update(dict_update.values(), batch_size=taille_batch, fields=champs_maj) logger.debug(f"MAJ: %s", len(dict_update)) df_referentiel_fe = process_referentiel_fe(referentiel_fe) df_referentiel_fe = convertir_fe(df_referentiel_fe) mettre_a_jour_fe(df_referentiel_fe) return Response({'message':'OK'}) except (Http404, APIException): raise except: message = "impossible d'alimenter le référentiel" logger.exception(message) raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message) # Vue API des administrés # Pagination OK # Ordering OK # Search NOK # Filtering NOK class AdministreView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue de l'administre. """ serializer_class = AdministreSerializer filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] # filterset_fields = ['category', 'in_stock'] filterset_class = AdministreFilter ordering_fields = 'a_id_sap' ordering = ['a_id_sap'] pagination_class = HeavyDataPagination # important : mettre à jour quand le serializer change def get_queryset(self): Cols = Administre.Cols return Administre.objects.select_related( Cols.REL_FONCTION, Cols.REL_GRADE, ).prefetch_related( Cols.M2M_COMPETENCES, Cols.O2M_FMOB, Prefetch(Cols.REL_DECISION, queryset=Decision.objects.select_related('poste__formation_emploi__garnison')), Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_GARNISON, FormationEmploi.Cols.REL_MERE)), Prefetch(Cols.REL_SOUS_VIVIER, queryset=SousVivier.objects.select_related(SousVivier.Cols.REL_GESTIONNAIRE)), ) @atomic def put(self, request): """La fonction put met à jour une liste d'administres. :type request: rest_framework.request.Request :param request: Request contenant la liste d'administres. :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des administres . """ try: req_data = request.data is_list = isinstance(req_data, list) validator = self.serializer_class(data=req_data, many=is_list, partial=True) validator.is_valid(raise_exception=True) today = date.today() Cols = Administre.Cols def copy_data_item(input, pk): """Copie et complète si besoin le dictionnaire en entrée""" result = {**input} if pk and Cols.PK not in result: result[Cols.PK] = pk return result if is_list: # Update multiple elements data = [copy_data_item(item, req_data[idx][Cols.PK]) for idx, item in enumerate(validator.validated_data)] fields = [key for key in data[0].keys() if key != Cols.PK] objs = [Administre(**data_item) for data_item in data] if Cols.STATUT_PAM in fields: list_error = [] for i in range(len(objs)): old_administre = Administre.objects.get(a_id_sap=objs[i].a_id_sap) administre = objs[i] eip = administre.a_eip fe_code = old_administre.formation_emploi_id avis = administre.a_statut_pam old_avis = old_administre.a_statut_pam try: categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] except: erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' print(erreur) raise Exception(erreur) liste_error = impact_decisions(old_administre, administre, old_avis, avis, eip, fe_code, categorie) Administre.objects.bulk_update(objs, fields=fields) else: # Update one element data = copy_data_item(validator.validated_data, validator.validated_data[Cols.PK]) # c'est un bug la récupération de a_id_sap en-dessous, non ? a_id_sap = Administre.objects.update(data) db_instance = Administre.objects.filter(a_id_sap=a_id_sap).first() db_instance.tag.clear() return Response({'msg': 'updated'}) except APIException: raise except Exception: message = 'échec de la mise à jour de N administrés' logger.exception(message) raise APIException(message) def partial_update(self, request, pk=None): """ La fonction put met à jour un administre. :type request: rest_framework.request.Request :param request: Request contenant l'administre. :type pk: integer :param pk: Primary Key de l'administre. :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour de l'administre . """ try: logger.debug("Mise à jour administré") req_data = request.data # TODO valider les données (il faut d'abord corriger la MAJ des compétences) # validator = self.serializer_class(data=req_data, partial=True) # validator.is_valid(raise_exception=True) Cols = Administre.Cols # Copie et complète si besoin le dictionnaire en entrée # administre = {**validator.validated_data} administre = {**req_data} administre[Cols.PK] = pk fields = [key for key in administre.keys() if key != Cols.PK] # and key != 'a_liste_id_competences'] if 'a_domaine_futur' in fields: administre['a_domaine_futur_id'] = administre['a_domaine_futur'] del administre['a_domaine_futur'] if 'a_filiere_futur' in fields: administre['a_filiere_futur_id'] = administre['a_filiere_futur'] del administre['a_filiere_futur'] if Cols.STATUT_PAM in fields: old_administre = Administre.objects.get(a_id_sap=pk) eip = old_administre.a_eip fe_code = old_administre.formation_emploi_id avis = administre[Cols.STATUT_PAM] old_avis = old_administre.a_statut_pam try: categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] except: erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' print(erreur) raise APIException(erreur) liste_error = impact_decisions(old_administre, Administre(**administre), old_avis, avis, eip, fe_code, categorie) if 'a_liste_id_competences' in fields: adm=Administre(pk=pk) #adm=request.data['a_liste_id_competences'] if administre['a_liste_id_competences']: adm.a_liste_id_competences.set(administre['a_liste_id_competences'].split(',')) else: adm.a_liste_id_competences.set("") else: copy = { k: v for k, v in administre.items() if k != 'a_liste_id_competences'} adm = Administre(**copy) if fields: Administre.objects.bulk_update([adm], fields=fields) return Response({'msg': 'updated'}) except (Http404, APIException): raise except Exception: message = "échec de la mise à jour de l'administré" logger.exception(message) raise APIException(message) # GET /api/administres => 0 administrés # POST /api/administres => Crée 1 nouvel administres => id=1 # GET /api/administres/1 => 1 administre précédemment créé # POST /api/administres/1 => 1 administre précédemment créé class PosteView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des postes. """ lookup_value_regex = r"[\w.]+" serializer_class = PosteSerializer pagination_class = HeavyDataPagination filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] # filterset_fields = ['category', 'in_stock'] filterset_class = PosteFilter ordering_fields = '__all_related__' ordering = ['p_id'] # important : mettre à jour quand le serializer change def get_queryset(self): """Cette fonction permet d'ajouter plus de logique à l'attribut queryset.""" Cols = Poste.Cols return Poste.objects.select_related( Cols.REL_FONCTION ).prefetch_related( Cols.M2M_COMPETENCES, Cols.O2M_DECISION, Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_GARNISON, FormationEmploi.Cols.REL_MERE)), Prefetch(Cols.REL_SOUS_VIVIER, queryset=SousVivier.objects.select_related(SousVivier.Cols.REL_GESTIONNAIRE)), ).annotate( p_nb_prepositionne=Sum( Case(When(decisions__de_decision=constants.DECISION_PREPOSITIONNE, then=1), default=0, output_field=IntegerField())), p_nb_positionne=Sum(Case(When(decisions__de_decision=constants.DECISION_POSITIONNE, then=1), default=0, output_field=IntegerField())), p_nb_omi_en_cours=Sum(Case(When(decisions__de_decision=constants.DECISION_OMI_EN_COURS, then=1), default=0, output_field=IntegerField())), p_nb_omi_active=Sum(Case(When(decisions__de_decision=constants.DECISION_OMI_ACTIVE, then=1), default=0, output_field=IntegerField())), ) def put(self, request): """La fonction put met à jour une liste des postes. :type request: rest_framework.request.Request :param request: Request contenant la liste des postes. :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des postes . """ data = request.data serialized = self.serializer_class(data=data, many=isinstance(data, list), partial=True) serialized.is_valid(raise_exception=True) if isinstance(data, list): # Update multiple elements try: fields = [key for key in data[0].keys() if key != 'p_id' ] objs = [Poste(**data[i]) for i in range(len(data))] except: raise Exception("bulk update error") Poste.objects.bulk_update(objs, fields=fields) else: # Update one element a_id_sap = Poste.objects.update(serialized.validated_data) db_instance = Poste.objects.filter( a_id_sap=a_id_sap).first() db_instance.tag.clear() return Response({'msg': 'updated'}) def partial_update(self, request, pk=None): """ La fonction put met à jour un poste. :type request: rest_framework.request.Request :param request: Request contenant le poste. :type pk: integer :param pk: Primary Key du poste. :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour du poste . """ poste = request.data fields = [key for key in poste.keys() if key != 'p_id']# and key != 'competence'] poste['p_id'] = pk if 'competence' in fields: pos=Poste(pk=pk) if poste['competence']: pos.competences.set(poste['competence'].split(',')) else: pos.competences.set("") else: copy = { k: v for k, v in poste.items() if k != 'competence'} pos = Poste(**copy) if fields: Poste.objects.bulk_update([pos], fields=fields) # copy = { k: v for k, v in poste.items() if k != 'competence'} # pos = Poste(**copy) # if fields: # Poste.objects.bulk_update([copy], fields=fields) # if 'competence' in poste.items(): # pos.competence.set(poste['competence'].split(',')) # pos.save() return Response({'msg': 'updated'}) class NotationView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des notations. """ serializer_class = NotationSerializer queryset = Notation.objects.all() pagination_class = HeavyDataPagination def list(self, request, pk=None): """La fonction list envoie le classement des postes pour un administré particulier et inversement. :type request: rest_framework.request.Request :param request: Request contenant l'administre ou le poste. :return: - **JsonResponse** (*JsonResponse*): Json contenant le classement. """ notations_list = [] q = 'poste' if 'administre_id' in request.query_params: administre_id = request.query_params['administre_id'] administres_keys = ( 'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere', 'poste__p_eip', 'poste__formation_emploi__fe_code', 'poste__competences', 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', 'poste__p_code_fonction', 'poste__p_avis', 'poste__p_nb_p1', 'poste__p_nb_p2', 'poste__p_nb_p3', 'poste__p_nb_p4', 'poste__p_dep', 'poste__formation_emploi__fe_libelle', 'poste__competences', 'poste__p_fonction', 'poste__formation_emploi__garnison', 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', 'poste__formation_emploi__garnison__gar_lieu', 'no_score_administre', 'no_flag_cple_ideal') counter = 10 notation_qs = Notation.objects.filter(administre_id=administre_id).order_by('-no_score_administre')[ :counter].select_related('poste') notation_qs_matching_parfait = Notation.objects.filter(administre_id=administre_id, no_flag_cple_ideal=True).select_related('poste') notation_qs.union(notation_qs_matching_parfait) notation_qs = notation_qs | notation_qs_matching_parfait notations_list = list(notation_qs.values(*administres_keys)) for notation in notations_list: poste_id = notation['poste__p_id'] topCounter = 0 allNotationsInvolved = Notation.objects.filter(poste_id=poste_id) for note in allNotationsInvolved: topList = list( Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre')[:10].values( 'poste_id')) topPostes = [poste['poste_id'] for poste in topList] if poste_id in topPostes: topCounter += 1 notation['poste__nb_top'] = topCounter if 'poste_id' in request.query_params: q = "administre" poste_id = request.query_params['poste_id'] poste_unit = Poste.objects.get(p_id=poste_id) try: counter = poste_unit.p_nb_p1 + poste_unit.p_nb_p2 + poste_unit.p_nb_p3 + poste_unit.p_nb_p4 if counter < 10: counter = 10 except: counter = 10 postes_keys = ( 'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__a_statut_pam', 'administre__grade_id', 'administre__a_liste_id_marques', 'administre__decision__de_decision', 'administre__decision__de_date_decision', 'no_score_administre', 'no_flag_cple_ideal', 'administre__a_notes_gestionnaire', 'administre__a_fonction', 'administre__a_code_fonction', 'administre__a_liste_id_marques', 'administre__a_liste_id_competences', 'administre__decision__poste_id') notation_qs = Notation.objects.filter(poste_id=poste_id).order_by('-no_score_administre')[ :counter].select_related( 'administre') notation_qs_matching_parfait = Notation.objects.filter(poste_id=poste_id, no_flag_cple_ideal=True).select_related('administre') notation_qs.union(notation_qs_matching_parfait) notations_list = list( notation_qs.values(*postes_keys)) for notation in notations_list: administre_id = notation['administre__a_id_sap'] topCounter = 0 allNotationsInvolved = Notation.objects.filter(administre_id=administre_id) for note in allNotationsInvolved: topList = list( Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre')[:10].values( 'administre_id')) topAdministres = [administre['administre_id'] for administre in topList] if administre_id in topAdministres: topCounter += 1 notation['administre__nb_top'] = topCounter result = [] for notation in notations_list: res_notation = {q: {}} for key in notation: if (q + "__") in key: res_notation[q][key.replace(q + '__', '')] = notation[key] else: res_notation[key] = notation[key] if q == "poste": res_notation[q]['p_nb_prepositionne'] = Decision.objects.filter( de_decision=constants.DECISION_PREPOSITIONNE).count() res_notation[q]['p_nb_positionne'] = Decision.objects.filter( de_decision=constants.DECISION_POSITIONNE).count() res_notation[q]['p_nb_omi_active'] = Decision.objects.filter( de_decision=constants.DECISION_OMI_ACTIVE).count() res_notation[q]['p_nb_omi_en_cours'] = Decision.objects.filter( de_decision=constants.DECISION_OMI_EN_COURS).count() result.append(res_notation) return Response(result) class DecisionView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des decisions. """ serializer_class = DecisionSerializer queryset = Decision.objects.all() def list(self, request, pk=None): """Cette fonction envoie les informations du poste lié à l'administre dans une décision avec le score de l'administre et inversement. :type request: rest_framework.request.Request :param request: Request contenant l'administre ou le poste. :return: - **res_decision** (*JsonResponse*): Json contenant le classement. """ decision = [] q = 'poste' administre_id = None poste_id = None if 'administre_id' in request.query_params: administre_id = request.query_params['administre_id'] administres_keys = ( 'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere', 'poste__p_eip','poste__p_avis', 'poste__formation_emploi__fe_code', 'poste__competences', 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', 'poste__formation_emploi__fe_libelle', 'poste__formation_emploi__garnison__gar_lieu', 'poste__p_dep', 'poste__p_code_fonction', 'poste__competences', 'poste__p_fonction', # 'poste__decision__de_decision', 'poste__p_nb_p1', 'poste__p_nb_p2', 'poste__p_nb_p3', 'poste__p_nb_p4', 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', 'de_decision', 'de_date_decision', 'de_notes_gestionnaire') decision = Decision.objects.filter(administre_id=administre_id).select_related('poste') decision = list(decision.values(*administres_keys)) if 'poste_id' in request.query_params: q = "administre" poste_id = request.query_params['poste_id'] postes_keys = ( 'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__a_statut_pam', 'administre__a_fonction', 'administre__a_code_fonction', 'administre__a_liste_id_competences', 'administre__grade_id', 'administre__a_liste_id_marques', 'administre__decision__de_decision', 'administre__decision__de_date_decision', 'de_decision', 'de_date_decision', 'de_notes_gestionnaire', 'administre__a_notes_gestionnaire', 'administre__a_liste_id_competences', 'administre__decision__poste_id') decision = Decision.objects.filter(poste_id=poste_id).select_related('administre') decision = list(decision.values(*postes_keys)) res_decision = [] if len(decision) > 0: for k in range(len(decision)): decision_unit = decision[k] try: administre_id = decision_unit.administre_id except: administre_id = None try: poste_id = decision_unit.poste_id except: poste_id = None try: res_decision_unit = {q: {}, 'no_score_administre': Notation.objects.get( administre_id=administre_id, poste_id=poste_id).no_score_administre} except: res_decision_unit = {q: {}, 'no_score_administre': None} for key in decision_unit: if (q + "__") in key: res_decision_unit[q][key.replace(q + '__', '')] = decision_unit[key] else: res_decision_unit[key] = decision_unit[key] # Ajout du relevé des décisions sur le poste (cas q = "poste") if q == "poste": res_decision_unit[q]['p_nb_prepositionne'] = Decision.objects.filter( de_decision=constants.DECISION_PREPOSITIONNE).count() res_decision_unit[q]['p_nb_positionne'] = Decision.objects.filter( de_decision=constants.DECISION_POSITIONNE).count() res_decision_unit[q]['p_nb_omi_active'] = Decision.objects.filter( de_decision=constants.DECISION_OMI_ACTIVE).count() res_decision_unit[q]['p_nb_omi_en_cours'] = Decision.objects.filter( de_decision=constants.DECISION_OMI_EN_COURS).count() res_decision.append(res_decision_unit) return Response(res_decision) def create(self, request): # équivalent post """Cette fonction crée une decision. :type request: rest_framework.request.Request :param request: Request contenant l'administre, le poste et la decision. :return: - **response** (*JsonResponse*): Json contenant le message "Décision changée". """ result = {} # TODO : try except administre_id = request.data['administre_id'] poste_id = request.data['poste_id'] de_decision = request.data['de_decision'] decision = Decision(administre_id=administre_id, poste_id=poste_id) administre_decision = Decision.objects.filter(administre_id=administre_id) # Lorsqu'on crée une décision on supprime toute décision précédente sur l'administré # poste_decision = Decision.objects.filter(poste_id=poste_id) if administre_decision.exists(): administre_decision.delete() decision.de_decision = de_decision decision.de_date_decision = timezone.now() decision.save() poste = Poste.objects.get(p_id=poste_id) poste.p_nb_vacant = poste.p_nb_vacant - 1 poste.save() response = JsonResponse({'status': 'success', 'message': 'Décision changée'}) return response class FormationEmploiView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des FormationEmplois. """ serializer_class = FormationEmploiSerializer # important : mettre à jour quand le serializer change def get_queryset(self): Cols = FormationEmploi.Cols return FormationEmploi.objects.select_related(Cols.REL_GARNISON, Cols.REL_MERE) class ListesPreferencesView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des ListesPreferences. """ serializer_class = PreferencesListe queryset = PreferencesListe.objects.all() class MarqueView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des marques. """ serializer_class = MarqueSerializer queryset = Marque.objects.all() class MarquesGroupeView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des marquegroupes. """ serializer_class = MarquesGroupeSerializer queryset = MarquesGroupe.objects.all() class FmobView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des Fmob. """ serializer_class = FmobSerializer queryset = FMOB.objects.all() class FiliereView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des filieres. """ serializer_class = FiliereSerializer queryset = Filiere.objects.all() class DomaineView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue des domaines. """ serializer_class = DomaineSerializer queryset = Domaine.objects.all() class SousVivierAssociationView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue du SousVivierAssociation. """ serializer_class = SousVivierAssociationSerializer queryset = SousVivierAssociation.objects.all() class PcpFeGroupeView(viewsets.ModelViewSet): """ Cette classe est dédiée au vue du PcpFeGroupe. """ serializer_class = PcpFeGroupeSerializer queryset = PcpFeGroupe.objects.all()