init
This commit is contained in:
12
backend-django/backend/utils/__init__.py
Normal file
12
backend-django/backend/utils/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .alimentation import *
|
||||
from .alimentation_decorators import *
|
||||
from .decisions import *
|
||||
from .decorators import *
|
||||
from .extraction import *
|
||||
from .functions import *
|
||||
from .initial import *
|
||||
from .insertion import *
|
||||
from .logging import *
|
||||
from .permissions import *
|
||||
from .predicates import *
|
||||
from .view_predicates import *
|
||||
259
backend-django/backend/utils/alimentation.py
Normal file
259
backend-django/backend/utils/alimentation.py
Normal file
@@ -0,0 +1,259 @@
|
||||
import datetime
|
||||
from enum import Enum, EnumMeta, unique
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def excel_date_converter(value) -> Union[str, float]:
|
||||
"""
|
||||
convertit la valeur en date (sans information de temps)
|
||||
note : type(np.nan) => <class 'float'>
|
||||
"""
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value.date().isoformat()
|
||||
if isinstance(value, str):
|
||||
val = '-'.join(list(reversed(value.split('/'))))
|
||||
return datetime.datetime.strptime(val, "%Y-%m-%d").date().isoformat() if val else np.nan
|
||||
return str(value) if value else np.nan
|
||||
|
||||
|
||||
def excel_int64_converter(value) -> Union[int, float]:
|
||||
"""
|
||||
convertit la valeur en entier
|
||||
note : type(np.nan) => <class 'float'>
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
val = value.strip() if isinstance(value, str) else value
|
||||
return np.int64(val) if val else np.nan
|
||||
|
||||
|
||||
def excel_uint64_converter(value) -> Union[int, float]:
|
||||
"""
|
||||
convertit la valeur en entier non signé
|
||||
note : type(np.nan) => <class 'float'>
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
val = value.strip() if isinstance(value, str) else value
|
||||
return np.uint64(val) if val else np.nan
|
||||
|
||||
|
||||
class FileCols(str, Enum):
|
||||
""" Classe de base pour les colonnes d'un fichier """
|
||||
|
||||
converter: str
|
||||
|
||||
def __new__(metacls, value: str, converter=None):
|
||||
obj = str.__new__(metacls, value)
|
||||
obj._value_ = value
|
||||
obj.converter = converter
|
||||
return obj
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@classmethod
|
||||
def columns(cls, *args: 'FileCols') -> List[str]:
|
||||
"""
|
||||
Renvoie les noms de colonnes. Deux cas :
|
||||
- il n'y a pas d'argument : tous les noms de colonnes
|
||||
- il y a des arguments : uniquement leurs noms de colonnes
|
||||
|
||||
:param args: membres de l'enum
|
||||
:type args: class:`FileCols` (multiple)
|
||||
|
||||
:return: noms de colonnes
|
||||
:rtype: List[str]
|
||||
"""
|
||||
return [member.value for member in (args or cls)]
|
||||
|
||||
@classmethod
|
||||
def col_mapping(cls, mapping: Dict[Union[str, 'FileCols'], Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Renvoie un mapping de noms pour utiliser avec pandas.DataFrame.rename.
|
||||
Les clés du dictionnaire initial peut être des membres de l'enum, elles seront converties
|
||||
en noms de colonnes.
|
||||
|
||||
:param mapping: correspondances initiales
|
||||
:type mapping: Dict[Union[str, FileCols], Any]
|
||||
|
||||
:return: nouvelles correspondances
|
||||
:rtype: Dict[str, Any]
|
||||
"""
|
||||
return {k.value if isinstance(k, cls) else k: v for k, v in mapping.items()}
|
||||
|
||||
@classmethod
|
||||
def converters(cls, *args: 'FileCols') -> Dict[str, Any]:
|
||||
"""
|
||||
Renvoie un mapping de conversion pour Pandas. Deux cas :
|
||||
- il n'y a pas d'argument : mapping de toutes les colonnes qui ont un convertisseur explicite
|
||||
- il y a des arguments : mapping des colonnes fournies qui ont un convertisseur explicite
|
||||
|
||||
:return: mapping entre noms de colonnes et convertisseur
|
||||
:rtype: Dict[str, Any]
|
||||
"""
|
||||
return {member.value: member.converter for member in (args or cls) if member.converter is not None}
|
||||
|
||||
|
||||
class BOCols(FileCols):
|
||||
""" Colonnes du fichier BO """
|
||||
|
||||
AFFECTATION_1 = ('Affectation -1 L', str) # AV
|
||||
AFFECTATION_2 = ('Affectation -2 L', str) # AX
|
||||
AFFECTATION_3 = ('Affectation -3 L', str) # AZ
|
||||
AFFECTATION_4 = ('Affectation -4 L', str) # BB
|
||||
AFFECTATION_5 = ('Affectation -5 L', str) # BD
|
||||
AFFECTATION_6 = ('Affectation -6 L', str) # BF
|
||||
AFFECTATION_7 = ('Affectation -7 L', str) # BH
|
||||
AFFECTATION_8 = ('Affectation -8 L', str) # BJ
|
||||
AFFECTATION_9 = ('Affectation -9 L', str) # BL
|
||||
AGE_ANNEES = ('Age en années (au 31/12)', excel_uint64_converter) # CG
|
||||
ANNEE_NOTATION = ('Année notation A', excel_uint64_converter) # CL
|
||||
ANNEE_NOTATION_1 = ('Année notation A-1', excel_uint64_converter) # CR
|
||||
ANNEE_NOTATION_2 = ('Année notation A-2', excel_uint64_converter) # CX
|
||||
ANNEE_NOTATION_3 = ('Année notation A-3', excel_uint64_converter) # DD
|
||||
ANNEE_NOTATION_4 = ('Année notation A-4', excel_uint64_converter) # DJ
|
||||
ANNEE_NOTATION_5 = ('Année notation A-5', excel_uint64_converter) # DP
|
||||
APTITUDE_EMPLOI_SUP = ('Apt resp / Emp sup A', str) # CP
|
||||
APTITUDE_EMPLOI_SUP_1 = ('Apt resp / Emp sup A-1', str) # CV
|
||||
APTITUDE_EMPLOI_SUP_2 = ('Apt resp / Emp sup A-2', str) # DB
|
||||
APTITUDE_EMPLOI_SUP_3 = ('Apt resp / Emp sup A-3', str) # DH
|
||||
APTITUDE_EMPLOI_SUP_4 = ('Apt resp / Emp sup A-4', str) # DN
|
||||
APTITUDE_EMPLOI_SUP_5 = ('Apt resp / Emp sup A-5', str) # DT
|
||||
ARME = ('Arme', str) # L
|
||||
CREDO_FE = ('CREDO FE act', str) # X
|
||||
DATE_AFFECTATION_1 = ('Affectation -1 DD', excel_date_converter) # AU
|
||||
DATE_AFFECTATION_2 = ('Affectation -2 DD', excel_date_converter) # AW
|
||||
DATE_AFFECTATION_3 = ('Affectation -3 DD', excel_date_converter) # AY
|
||||
DATE_AFFECTATION_4 = ('Affectation -4 DD', excel_date_converter) # BA
|
||||
DATE_AFFECTATION_5 = ('Affectation -5 DD', excel_date_converter) # BC
|
||||
DATE_AFFECTATION_6 = ('Affectation -6 DD', excel_date_converter) # BE
|
||||
DATE_AFFECTATION_7 = ('Affectation -7 DD', excel_date_converter) # BG
|
||||
DATE_AFFECTATION_8 = ('Affectation -8 DD', excel_date_converter) # BI
|
||||
DATE_AFFECTATION_9 = ('Affectation -9 DD', excel_date_converter) # BK
|
||||
DATE_ARRIVEE_FE = ('Date arrivée FE', excel_date_converter) # AA
|
||||
DATE_DEBUT_GRADE = ('Grade act DD', excel_date_converter) # AH
|
||||
DATE_DERNIER_ACR = ('Dernière mutation ACR D', excel_date_converter) # AC
|
||||
DATE_ENTREE_SERVICE = ('Entrée en Service', excel_date_converter) # N
|
||||
DATE_FONCTION_1 = ('Fonction -1 DD', excel_date_converter) # BM
|
||||
DATE_FONCTION_2 = ('Fonction -2 DD', excel_date_converter) # BO
|
||||
DATE_FONCTION_3 = ('Fonction -3 DD', excel_date_converter) # BQ
|
||||
DATE_FONCTION_4 = ('Fonction -4 DD', excel_date_converter) # BS
|
||||
DATE_FONCTION_5 = ('Fonction -5 DD', excel_date_converter) # BU
|
||||
DATE_FONCTION_6 = ('Fonction -6 DD', excel_date_converter) # BW
|
||||
DATE_FONCTION_7 = ('Fonction -7 DD', excel_date_converter) # BY
|
||||
DATE_FONCTION_8 = ('Fonction -8 DD', excel_date_converter) # CA
|
||||
DATE_FONCTION_9 = ('Fonction -9 DD', excel_date_converter) # CC
|
||||
DATE_FUD = ('Date prise effet FUD départ', excel_date_converter) # DV
|
||||
DATE_LIEN_SERVICE = ('Lien au service DF', excel_date_converter) # EG
|
||||
DATE_NAISSANCE = ('Naissance', excel_date_converter) # R
|
||||
DATE_POSITION_STATUAIRE = ('Date Position statutaire', excel_date_converter) # AF
|
||||
DATE_RDC = ('Date Radiation des contrôles', excel_date_converter) # W
|
||||
DATE_STATUT_CONCERTO = ('Situation administrative act DD', excel_date_converter) # DX
|
||||
DATE_STATUT_CONCERTO_FUTUR = ('Date Position statu future', excel_date_converter) # DZ
|
||||
DATE_MARIAGE = ('Situation familiale (IT0002) DD', excel_date_converter) # CH
|
||||
DERNIER_DIPLOME = ('Dernier diplôme militaire LA', str) # V
|
||||
DIPLOME_PLUS_HAUT_NIVEAU = ('Diplôme militaire de plus haut niveau', str) # U
|
||||
DOMAINE = ('Domaine EIP act LA', str) # I
|
||||
DOMAINE_GESTION = ('Domaine de gestion act LA', str) # H
|
||||
DOMAINE_POSTE = ('Domaine emploi occupé act LA', str) # ED
|
||||
EIP = ('EIP act LA', str) # F
|
||||
EIS = ('EIS act LA', str) # G
|
||||
ENFANTS = ('Enfants', str) # EK
|
||||
FILIERE = ('Filière EIP act LA', str) # J
|
||||
FILIERE_POSTE = ('Nature de filière emploi occupé act LA', str) # EE
|
||||
FONCTION = ('Fonction act L', str) # T
|
||||
FONCTION_1 = ('Fonction -1 L', str) # BN
|
||||
FONCTION_2 = ('Fonction -2 L', str) # BP
|
||||
FONCTION_3 = ('Fonction -3 L', str) # BR
|
||||
FONCTION_4 = ('Fonction -4 L', str) # BT
|
||||
FONCTION_5 = ('Fonction -5 L', str) # BV
|
||||
FONCTION_6 = ('Fonction -6 L', str) # BX
|
||||
FONCTION_7 = ('Fonction -7 L', str) # BZ
|
||||
FONCTION_8 = ('Fonction -8 L', str) # CB
|
||||
FONCTION_9 = ('Fonction -9 L', str) # CD
|
||||
FUD = ('Type de FUD départ') # DW
|
||||
GARNISON = ('Garnison act', str) # Z
|
||||
GRADE = ('GRADE TA', str) # B
|
||||
ID_DEF = ('Identifiant défense', str) # E
|
||||
ID_DEF_CONJOINT = ('Identifiant défense du conjoint militaire', str) # AN
|
||||
ID_SAP = ('Matricule SAP', excel_int64_converter) # A
|
||||
ID_SAP_CONJOINT = ('Identifiant SAP du conjoint militaire', excel_int64_converter) # AM
|
||||
INTERRUPTION_SERVICE = ('Interruption de service', str) # AG
|
||||
MARQUEUR_PN = ('Marquant Personnel Navigant') # EH
|
||||
NF = ('Niveau fonctionnel EIP act', str) # K
|
||||
NF_POSTE = ('Niveau fonctionnel emploi occupé act C', str) # EF
|
||||
NOM = ('NOM', str) # C
|
||||
NOMBRE_ENFANTS = ("Nbr total d'enfants", excel_uint64_converter) # O
|
||||
NR_OU_IRIS = ('IRIS / RAC retenu A', excel_int64_converter) # CN
|
||||
NR_OU_IRIS_1 = ('IRIS / RAC retenu A-1', excel_int64_converter) # CT
|
||||
NR_OU_IRIS_2 = ('IRIS / RAC retenu A-2', excel_int64_converter) # CZ
|
||||
NR_OU_IRIS_3 = ('IRIS / RAC retenu A-3', excel_int64_converter) # DF
|
||||
NR_OU_IRIS_4 = ('IRIS / RAC retenu A-4', excel_int64_converter) # DL
|
||||
NR_OU_IRIS_5 = ('IRIS / RAC retenu A-5', excel_int64_converter) # DR
|
||||
ORIGINE_RECRUTEMENT = ('Origine recrutement LA', str) # Q
|
||||
PLS_GB_MAX = ('PLS GB Max') # EI
|
||||
POSITION_STATUTAIRE = ('Position statutaire act L', str) # AE
|
||||
POTENTIEL_RESPONSABILITE_SUP = ('Potentiel responsabilités catégorie sup A', str) # CQ
|
||||
POTENTIEL_RESPONSABILITE_SUP_1 = ('Potentiel responsabilités catégorie sup A-1', str) # CW
|
||||
POTENTIEL_RESPONSABILITE_SUP_2 = ('Potentiel responsabilités catégorie sup A-2', str) # DC
|
||||
POTENTIEL_RESPONSABILITE_SUP_3 = ('Potentiel responsabilités catégorie sup A-3', str) # DI
|
||||
POTENTIEL_RESPONSABILITE_SUP_4 = ('Potentiel responsabilités catégorie sup A-4', str) # DO
|
||||
POTENTIEL_RESPONSABILITE_SUP_5 = ('Potentiel responsabilités catégorie sup A-5', str) # DU
|
||||
PRENOM = ('Prénom', str) # D
|
||||
PROFESSION_CONJOINT = ('Profession conjoint Act L', str) # AL
|
||||
RAC_OU_IRIS_CUMULE = ('NR/NGC cumulé A', excel_int64_converter) # CM
|
||||
RAC_OU_IRIS_CUMULE_1 = ('NR/NGC cumulé A-1', excel_int64_converter) # CS
|
||||
RAC_OU_IRIS_CUMULE_2 = ('NR/NGC cumulé A-2', excel_int64_converter) # CY
|
||||
RAC_OU_IRIS_CUMULE_3 = ('NR/NGC cumulé A-3', excel_int64_converter) # DE
|
||||
RAC_OU_IRIS_CUMULE_4 = ('NR/NGC cumulé A-4', excel_int64_converter) # DK
|
||||
RAC_OU_IRIS_CUMULE_5 = ('NR/NGC cumulé A-5', excel_int64_converter) # DQ
|
||||
REGROUPEMENT_ORIGINE_RECRUTEMENT = ('Regroupement origine recrutement C', str) # M
|
||||
RF_QSR = ('QSR A', str) # CO
|
||||
RF_QSR_1 = ('QSR A-1', str) # CU
|
||||
RF_QSR_2 = ('QSR A-2', str) # DA
|
||||
RF_QSR_3 = ('QSR A-3', str) # DG
|
||||
RF_QSR_4 = ('QSR A-4', str) # DM
|
||||
RF_QSR_5 = ('QSR A-5', str) # DS
|
||||
SEXE = ('Sexe', str) # CJ
|
||||
SEXE_CONJOINT = ('Sexe du conjoint', str) # AT
|
||||
SITUATION_FAMILIALE = ('Situation familiale (IT0002) L', str) # CI
|
||||
STATUT_CONCERTO = ('Situation admi actuelle', str) # DY
|
||||
STATUT_CONCERTO_FUTUR = ('Position statutaire future', str) # EA
|
||||
|
||||
|
||||
class FmobCols(FileCols):
|
||||
""" Colonnes du fichier FMOB """
|
||||
|
||||
ID_SAP = ('Matricule SAP', excel_int64_converter) # A
|
||||
|
||||
|
||||
class InseeCols(FileCols):
|
||||
""" Colonnes du fichier INSEE """
|
||||
|
||||
CODE_INSEE = ('CODE INSEE', str) # D
|
||||
CODE_POSTAL = ('CODE POSTAL', str) # C
|
||||
CREDO_FE = ('CREDO FE act', str) # B
|
||||
ID_SAP = ('Matricule SAP', excel_int64_converter) # A
|
||||
|
||||
|
||||
class ReoCols(FileCols):
|
||||
""" Colonnes du fichier REO """
|
||||
|
||||
ANNEE_PROJET = ('Année Projet', str) # B
|
||||
CATEGORIE = ('Catégorie /EFF CELL', str) # P
|
||||
CODE_NF = ('NFEO', str) # W
|
||||
CODE_POSTAL = ('Code Postal /OB G', str) # J
|
||||
DOMAINE = ('DEO', str) # U
|
||||
DOMAINE_GESTION = ('Postes NFS', str) # AA
|
||||
EIP = ('MGS gestionnaire / EFF CELL', str) # T
|
||||
FONCTION_ID = ('ETR Code / EFF CELL', str) # Y
|
||||
FONCTION_LIBELLE = ('ETR Libellé / EFF CELL', str) # Z
|
||||
FORMATION_EMPLOI = ('Code CREDO Long OB', str) # F
|
||||
FILIERE = ('FEO', str) # V
|
||||
ID_POSTE = ('N° De Poste', str) # X
|
||||
|
||||
|
||||
17
backend-django/backend/utils/alimentation_decorators.py
Normal file
17
backend-django/backend/utils/alimentation_decorators.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from logging import Logger, LoggerAdapter
|
||||
from typing import Any, List, Set, Tuple, Union
|
||||
|
||||
from .logging import TAG_DATA_FEED, TAG_PERF, get_logger
|
||||
|
||||
|
||||
def get_data_logger(value: Any = None, tags: Union[str, List[str], Set[str], Tuple[str]] = None) -> Union[Logger, LoggerAdapter]:
|
||||
""" variante spécifique de 'get_logger' qui ajoute toujours le tag TAG_DATA_FEED """
|
||||
all_tags = TAG_DATA_FEED
|
||||
if tags:
|
||||
all_tags = [TAG_DATA_FEED, *tags] if isinstance(tags, (list, set, tuple)) else [TAG_DATA_FEED, tags]
|
||||
return get_logger(value, all_tags)
|
||||
|
||||
|
||||
def data_perf_logger_factory(func):
|
||||
""" fonction de création de logger pour la performance et l'alimentation, à utiliser avec 'execution_time' ou 'query_count' """
|
||||
return get_data_logger(func, TAG_PERF)
|
||||
32
backend-django/backend/utils/attributes.py
Normal file
32
backend-django/backend/utils/attributes.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import functools
|
||||
|
||||
|
||||
def rgetattr(obj, attr: str, safe: bool = False, *args, **kwargs):
|
||||
"""
|
||||
Version récursive de getattr pour utiliser une propriété chaînée même avec None au milieu.
|
||||
exemples :
|
||||
- rgetattr(obj, 'a.b.c')
|
||||
- rgetattr(obj, 'a.b.c', safe=True)
|
||||
|
||||
:param obj: cible
|
||||
:type obj: Any
|
||||
|
||||
:param attr: propriété qui peut être chaînée en séparant par '.'
|
||||
:type attr: str
|
||||
|
||||
:param safe: indique qu'il faut tester la présence de l'attribut avec hasattr (utile pour les relations entre modèles Django), defaults to False
|
||||
:type safe: bool
|
||||
|
||||
:return: valeur de l'attribut
|
||||
:rtype: Any
|
||||
"""
|
||||
def _getattr(obj, attr):
|
||||
return getattr(obj, attr, *args) if not safe or hasattr(obj, attr) else None
|
||||
return functools.reduce(_getattr, [obj] + attr.split('.'))
|
||||
|
||||
def safe_rgetattr(obj, attr: str, safe: bool = False, *args, **kwargs):
|
||||
"""
|
||||
version safe de rgetattr
|
||||
exemples : safe_rgetattr(obj, 'a.b.c')
|
||||
"""
|
||||
return rgetattr(obj, attr, safe=True, *args, **kwargs)
|
||||
266
backend-django/backend/utils/decisions.py
Normal file
266
backend-django/backend/utils/decisions.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
|
||||
from ..models import Administre, CustomUser, Decision, DecisionChoices, Administres_Pams
|
||||
from ..models import StatutPamChoices as StatutPam
|
||||
from .logging import get_logger
|
||||
from .permissions import KEY_WRITE, Profiles, get_profiles_by_adm
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# clé d'une valeur de l'arbre de décision : choix disponibles
|
||||
KEY_CHOICES = 'choices'
|
||||
|
||||
# clé d'une valeur de l'arbre de décision : profils habilités à modifier le statut
|
||||
KEY_PROFILES = 'editable_by'
|
||||
|
||||
# clé d'un élément de "get_available_decisions" : pour une création
|
||||
KEY_CREATE = 'creation'
|
||||
|
||||
# clé d'un élément de "get_available_decisions" : pour une mise à jour
|
||||
KEY_UPDATE = 'update'
|
||||
|
||||
class ExtraDecisions(str, Enum):
|
||||
""" décisions en plus """
|
||||
|
||||
EMPTY = ''
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def __init_decisions():
|
||||
D = DecisionChoices
|
||||
EX = ExtraDecisions
|
||||
P = Profiles
|
||||
return {
|
||||
# === commun ===
|
||||
|
||||
None: {
|
||||
KEY_PROFILES: (P.FILIERE, P.BVT),
|
||||
KEY_CHOICES: (D.PROPOSITION_FE, D.HME_PROPOSITION_VIVIER)
|
||||
},
|
||||
D.REMIS_A_DISPOSITION: {
|
||||
KEY_PROFILES: (P.FILIERE,),
|
||||
KEY_CHOICES: (EX.EMPTY,),
|
||||
},
|
||||
|
||||
# === en métropole ===
|
||||
|
||||
D.PROPOSITION_FE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL),
|
||||
KEY_CHOICES: (D.DIALOGUE_EN_COURS, D.FOREMP_EN_COURS)
|
||||
},
|
||||
D.DIALOGUE_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL,),
|
||||
KEY_CHOICES: (D.DIALOGUE_TERMINE, D.DIALOGUE_INFRUCTUEUX)
|
||||
},
|
||||
D.DIALOGUE_TERMINE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL,),
|
||||
KEY_CHOICES: (D.FOREMP_EN_COURS,)
|
||||
},
|
||||
D.DIALOGUE_INFRUCTUEUX: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL,),
|
||||
KEY_CHOICES: (D.FOREMP_EN_COURS, D.REMIS_A_DISPOSITION)
|
||||
},
|
||||
D.FOREMP_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL,),
|
||||
KEY_CHOICES: (D.FOREMP_TERMINE,)
|
||||
},
|
||||
D.FOREMP_TERMINE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL,),
|
||||
KEY_CHOICES: (D.PREPOSITIONNE, D.REMIS_A_DISPOSITION)
|
||||
},
|
||||
D.PREPOSITIONNE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.POSITIONNE, D.REMIS_A_DISPOSITION)
|
||||
},
|
||||
D.POSITIONNE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.OMIP_EN_COURS, D.OMI_EN_COURS, D.REMIS_A_DISPOSITION)
|
||||
},
|
||||
D.OMIP_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.OMIP_TERMINE, D.REMIS_A_DISPOSITION),
|
||||
},
|
||||
D.OMIP_TERMINE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.ATTENTE_AVIONAGE, D.OMI_EN_COURS, D.REMIS_A_DISPOSITION),
|
||||
},
|
||||
D.ATTENTE_AVIONAGE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.OMI_EN_COURS,),
|
||||
},
|
||||
D.OMI_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.OMI_ACTIVE, D.REMIS_A_DISPOSITION),
|
||||
},
|
||||
D.OMI_ACTIVE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.OMI_ANNULE,),
|
||||
},
|
||||
D.OMI_ANNULE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.REMIS_A_DISPOSITION,),
|
||||
},
|
||||
|
||||
# === hors métropole (HME) ===
|
||||
|
||||
D.HME_PROPOSITION_VIVIER: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_DIALOGUE_INITIE,)
|
||||
},
|
||||
D.HME_ETUDE_DESISTEMENT: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_DESISTEMENT,)
|
||||
},
|
||||
D.HME_DESISTEMENT: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.REMIS_A_DISPOSITION,)
|
||||
},
|
||||
D.HME_DIALOGUE_INITIE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_DIALOGUE_EN_COURS,)
|
||||
},
|
||||
D.HME_DIALOGUE_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_DIALOGUE_TERMINE, D.HME_DIALOGUE_INFRUCTUEUX)
|
||||
},
|
||||
D.HME_DIALOGUE_TERMINE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_PREPOSITIONNE,)
|
||||
},
|
||||
D.HME_DIALOGUE_INFRUCTUEUX: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_ETUDE_DESISTEMENT, D.HME_PREPOSITIONNE, D.REMIS_A_DISPOSITION)
|
||||
},
|
||||
D.HME_FOREMP_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_FOREMP_TERMINE, D.HME_OMI_EN_COURS)
|
||||
},
|
||||
D.HME_FOREMP_TERMINE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_OMIP_EN_COURS, D.HME_OMI_EN_COURS)
|
||||
},
|
||||
D.HME_PREPOSITIONNE: {
|
||||
KEY_PROFILES: (P.HME,),
|
||||
KEY_CHOICES: (D.HME_VALIDATION_EXPERT, D.HME_REFUS_EXPERT)
|
||||
},
|
||||
D.HME_VALIDATION_EXPERT: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_POSITIONNE,)
|
||||
},
|
||||
D.HME_POSITIONNE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_OMIP_EN_COURS, D.HME_OMI_EN_COURS, D.HME_FOREMP_EN_COURS)
|
||||
},
|
||||
D.HME_REFUS_EXPERT: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_PREPOSITIONNE, D.REMIS_A_DISPOSITION),
|
||||
},
|
||||
D.HME_OMIP_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_OMIP_TERMINE,),
|
||||
},
|
||||
D.HME_OMIP_TERMINE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_ATTENTE_AVIONAGE, D.HME_OMI_EN_COURS),
|
||||
},
|
||||
D.HME_ATTENTE_AVIONAGE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_OMI_EN_COURS,),
|
||||
},
|
||||
D.HME_OMI_EN_COURS: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_ACTUEL, P.PCP_FUTUR),
|
||||
KEY_CHOICES: (D.HME_OMI_ACTIVE,),
|
||||
},
|
||||
D.HME_OMI_ACTIVE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.HME_OMI_ANNULE,),
|
||||
},
|
||||
D.HME_OMI_ANNULE: {
|
||||
KEY_PROFILES: (P.PCP, P.PCP_FUTUR,),
|
||||
KEY_CHOICES: (D.REMIS_A_DISPOSITION,),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# enchaînement des décisions
|
||||
DECISIONS = __init_decisions()
|
||||
|
||||
|
||||
def get_all_decisions() -> Tuple[DecisionChoices]:
|
||||
"""
|
||||
Renvoie tous les statuts de décisions possibles. Une décision vide correspondrait à une absence de décision et n'est donc pas présente.
|
||||
|
||||
:return: toutes les décisions
|
||||
:rtype: Tuple[DecisionChoices]
|
||||
"""
|
||||
return tuple(DecisionChoices)
|
||||
|
||||
|
||||
def get_available_decisions(
|
||||
administres: Union[List[Administres_Pams], Tuple[Administres_Pams]],
|
||||
user: CustomUser = None,
|
||||
profiles_by_adm: Dict[int, Tuple[Profiles]] = None
|
||||
) -> Dict[int, Dict[str, Tuple[Union[DecisionChoices, ExtraDecisions]]]]:
|
||||
"""
|
||||
Renvoie les décisions disponibles pour l'utilisateur s'il modifie le statut de décision des administrés donnés.
|
||||
|
||||
:param administres: Administres_Pams
|
||||
:type administres: Union[List[Administres_Pams], Tuple[Administres_Pams]]
|
||||
|
||||
:param user: utilisateur
|
||||
:type user: class:`CustomUser`, optional
|
||||
|
||||
:param profiles_by_adm: profils pour chaque administré (voir get_profiles_by_adm)
|
||||
:type profiles_by_adm: Dict[int, Tuple[Profiles]], optional
|
||||
|
||||
:return: dictionnaire de dictionnaires {<ID SAP>: {<KEY_CREATE>: <décisions>, <KEY_UPDATE>: <décisions>} }
|
||||
:rtype: Dict[int, Dict[str, Tuple[Union[DecisionChoices, ExtraDecisions]]]]
|
||||
"""
|
||||
pam_without_decision = tuple(x for x in StatutPam if not x.dec_enabled)
|
||||
pam_with_decision = tuple(x for x in StatutPam if x.dec_enabled)
|
||||
result = {}
|
||||
|
||||
# restrictions : dictionnaire de dictionnaires {<ID SAP>: <même forme qu'une valeur de DECISIONS>}
|
||||
restrictions_create_by_adm = {}
|
||||
restrictions_update_by_adm = {}
|
||||
|
||||
adm_to_process = []
|
||||
for adm in administres:
|
||||
adm_id = adm.pk
|
||||
pam = adm.a_statut_pam_annee
|
||||
if pam in pam_without_decision:
|
||||
# le statut PAM ne permet aucun choix
|
||||
result[adm_id] = {KEY_CREATE: (), KEY_UPDATE: ()}
|
||||
elif pam in pam_with_decision:
|
||||
# le statut PAM active l'arbre de décisions
|
||||
adm_to_process.append(adm)
|
||||
statut_decision = adm.decision.de_decision or None if hasattr(adm, Administres_Pams.Cols.REL_DECISION) else None
|
||||
restrictions_update_by_adm[adm_id] = DECISIONS.get(statut_decision) or {}
|
||||
else:
|
||||
logger.info('statut PAM non géré pour les décisions possibles : %s', pam)
|
||||
result[adm_id] = {KEY_CREATE: (), KEY_UPDATE: ()}
|
||||
|
||||
if adm_to_process:
|
||||
default_restrictions_create = DECISIONS.get(None) or {}
|
||||
|
||||
def get_decisions(profiles, restrictions) -> Tuple[Union[DecisionChoices, ExtraDecisions]]:
|
||||
allowed_profiles = restrictions.get(KEY_PROFILES) or ()
|
||||
choices = restrictions.get(KEY_CHOICES)
|
||||
decisions = ()
|
||||
if choices and any(p in allowed_profiles for p in profiles):
|
||||
decisions = tuple(choices)
|
||||
return decisions
|
||||
|
||||
_profiles_by_adm = profiles_by_adm or get_profiles_by_adm(user, *adm_to_process)
|
||||
for adm in adm_to_process:
|
||||
adm_id = adm.pk
|
||||
profiles = (_profiles_by_adm.get(adm_id) or {}).get(KEY_WRITE) or ()
|
||||
result.setdefault(adm_id, {
|
||||
KEY_CREATE: get_decisions(profiles, restrictions_create_by_adm.get(adm_id) or default_restrictions_create),
|
||||
KEY_UPDATE: get_decisions(profiles, restrictions_update_by_adm.get(adm_id) or {})
|
||||
})
|
||||
return result
|
||||
113
backend-django/backend/utils/decorator.py
Normal file
113
backend-django/backend/utils/decorator.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from django.db import connections
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Set, Tuple, Union
|
||||
import logging
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InfoAppel(Enum):
|
||||
""" infos supplémentaires possibles """
|
||||
|
||||
# ajouté au nom de logger
|
||||
CLASSE = 'classe'
|
||||
# ajouté dans le message
|
||||
PID = 'pid'
|
||||
|
||||
|
||||
def get_nom_module(func) -> str:
|
||||
mod = inspect.getmodule(func)
|
||||
return mod.__name__ if mod else None
|
||||
|
||||
|
||||
def get_nombre_requetes() -> int:
|
||||
""" renvoie le nombre total de requêtes (il peut y avoir plusieurs connexions) """
|
||||
|
||||
return sum(len(c.queries) for c in connections.all())
|
||||
|
||||
|
||||
def get_nom_classe(inclure: Union[ List[InfoAppel], Set[InfoAppel], Tuple[InfoAppel] ] = (), *args) -> str:
|
||||
""" renvoie le nom de classe correspondant à l'appel de fonction """
|
||||
|
||||
nom_classe = None
|
||||
if InfoAppel.CLASSE in inclure and args:
|
||||
classe = getattr(args[0], '__class__', None)
|
||||
nom_classe = getattr(classe, '__name__') if classe else None
|
||||
return nom_classe
|
||||
|
||||
|
||||
def get_logger(nom_module: Optional[str], nom_classe: Optional[str]) -> logging.Logger:
|
||||
""" renvoie le logger correspondant au module et à la classe """
|
||||
|
||||
return logging.getLogger(f'{nom_module}.{nom_classe}' if nom_module and nom_classe else nom_classe or nom_module)
|
||||
|
||||
|
||||
def duree_execution(avertir_apres: int = None, inclure: Union[List[InfoAppel], Set[InfoAppel], Tuple[InfoAppel]] = ()):
|
||||
"""
|
||||
décorateur pour tracer le temps d'exécution d'une fonction
|
||||
|
||||
:param avertir_apres: durée (en ms) au-delà de laquelle on souhaite un log d'avertissement, defaults to None
|
||||
:type avertir_apres: int, optional
|
||||
|
||||
:param inclure: infos supplémentaires, defaults to ()
|
||||
:type inclure: Union[List[InfoAppel], Set[InfoAppel], Tuple[InfoAppel]], optional
|
||||
|
||||
:return: résultat de la fonction d'origine
|
||||
"""
|
||||
def inner(func):
|
||||
nom_module = get_nom_module(func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
temps_debut = time.time()
|
||||
try:
|
||||
resultat = func(*args, **kwargs)
|
||||
finally:
|
||||
try:
|
||||
temps_ecoule = round((time.time() - temps_debut) * 1000)
|
||||
|
||||
func_logger = get_logger(nom_module, get_nom_classe(inclure, *args))
|
||||
log_level = logging.WARNING if isinstance(avertir_apres, int) and temps_ecoule > avertir_apres else logging.DEBUG
|
||||
func_logger.log(log_level, "%s'%s' exécuté en %d ms", '[%s] ' % os.getpid() if InfoAppel.PID in inclure else '', func.__name__, temps_ecoule)
|
||||
except Exception:
|
||||
logger.exception('impossible de tracer la durée')
|
||||
return resultat
|
||||
return wrapper
|
||||
return inner
|
||||
|
||||
def nombre_requetes(avertir_apres: int = None, inclure: Union[List[InfoAppel], Set[InfoAppel], Tuple[InfoAppel]] = ()):
|
||||
"""
|
||||
décorateur pour tracer le nombre de requêtes d'une fonction
|
||||
|
||||
:param avertir_apres: nombre de requêtes au-delà duquel on souhaite un log d'avertissement, defaults to None
|
||||
:type avertir_apres: int, optional
|
||||
|
||||
:param inclure: infos supplémentaires, defaults to ()
|
||||
:type inclure: Union[List[InfoAppel], Set[InfoAppel], Tuple[InfoAppel]], optional
|
||||
|
||||
:return: résultat de la fonction d'origine
|
||||
"""
|
||||
def inner(func):
|
||||
nom_module = get_nom_module(func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
nb_debut = get_nombre_requetes()
|
||||
try:
|
||||
resultat = func(*args, **kwargs)
|
||||
finally:
|
||||
try:
|
||||
nb_requetes = get_nombre_requetes() - nb_debut
|
||||
|
||||
func_logger = get_logger(nom_module, get_nom_classe(inclure, *args))
|
||||
log_level = logging.WARNING if isinstance(avertir_apres, int) and nb_requetes > avertir_apres else logging.DEBUG
|
||||
func_logger.log(log_level, "%s'%s' a exécuté %d requête(s)", '[%s] ' % os.getpid() if InfoAppel.PID in inclure else '', func.__name__, nb_requetes)
|
||||
except Exception:
|
||||
logger.exception('impossible de tracer le nombre de requêtes')
|
||||
return resultat
|
||||
return wrapper
|
||||
return inner
|
||||
294
backend-django/backend/utils/decorators.py
Normal file
294
backend-django/backend/utils/decorators.py
Normal file
@@ -0,0 +1,294 @@
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from enum import Enum, auto
|
||||
from logging import Logger, LoggerAdapter
|
||||
from types import (BuiltinFunctionType, BuiltinMethodType,
|
||||
ClassMethodDescriptorType, FunctionType,
|
||||
MethodDescriptorType, MethodType)
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from django.db import connections
|
||||
|
||||
from .logging import TAG_DATA_FEED, TAG_PERF, get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# attribut de classe : logger
|
||||
CLASS_ATTR_LOGGER = 'logger'
|
||||
|
||||
# clé du dictionnaire contexte : désactivé
|
||||
CTX_KEY_DISABLED = 'disabled'
|
||||
|
||||
# clé du dictionnaire contexte : logger
|
||||
CTX_KEY_LOGGER = 'logger'
|
||||
|
||||
# attribut de décorateur : contexte
|
||||
DECORATOR_ATTR_CTX = 'decorator_ctx'
|
||||
|
||||
# attribut de décorateur : clé
|
||||
DECORATOR_ATTR_KEY = 'decorator_key'
|
||||
|
||||
# attribut de décorateur : cible (classe, fonction)
|
||||
DECORATOR_ATTR_TARGET = 'decorator_target'
|
||||
|
||||
# types de fonctions
|
||||
FUNCTION_TYPES = (
|
||||
BuiltinFunctionType,
|
||||
BuiltinMethodType,
|
||||
ClassMethodDescriptorType,
|
||||
FunctionType,
|
||||
MethodDescriptorType,
|
||||
MethodType,
|
||||
)
|
||||
|
||||
|
||||
def _get_query_count() -> int:
|
||||
""" renvoie le nombre total de requêtes (il peut y avoir plusieurs connexions) """
|
||||
|
||||
return sum(len(c.queries) for c in connections.all())
|
||||
|
||||
|
||||
class OnConflict(Enum):
|
||||
""" choix de gestion de conflit quand le même décorateur est déjà utilisé """
|
||||
|
||||
# laisse l'ancien décorateur
|
||||
SKIP = auto()
|
||||
|
||||
# remplace l'ancien décorateur
|
||||
REPLACE = auto()
|
||||
|
||||
# ajoute à l'ancien décorateur
|
||||
STACK = auto()
|
||||
|
||||
|
||||
# amélioration possible : et s'il y a plusieurs décorateurs de même clé ?
|
||||
def _find_former_decorator(key: str, target: Callable) -> Tuple[Optional[Callable], Optional[Callable]]:
|
||||
"""
|
||||
Trouve le décorateur de même clé si la cible est déjà décorée. Un décorateur désactivé est ignoré.
|
||||
note : ne fonctionne pas correctement s'il existe un décorateur intermédiaire sans attribut DECORATOR_ATTR_TARGET
|
||||
|
||||
:param key: clé de décorateur
|
||||
:type key: str
|
||||
|
||||
:param target: fonction potentiellement décorée
|
||||
:type target: Callable
|
||||
|
||||
:return: ancien décorateur + ancienne cible
|
||||
:rtype: Tuple[Callable, Callable]
|
||||
"""
|
||||
current = target
|
||||
decorator_key = getattr(current, DECORATOR_ATTR_KEY, None)
|
||||
decorator_target = getattr(current, DECORATOR_ATTR_TARGET, None)
|
||||
|
||||
while decorator_key and decorator_key != key and decorator_target:
|
||||
current = decorator_target
|
||||
decorator_key = getattr(current, DECORATOR_ATTR_KEY, None)
|
||||
decorator_target = getattr(current, DECORATOR_ATTR_TARGET, None)
|
||||
|
||||
# les décorateurs désactivés ne comptent pas
|
||||
if decorator_key == key and not getattr(current, DECORATOR_ATTR_CTX, {}).get(CTX_KEY_DISABLED, False):
|
||||
return current, decorator_target
|
||||
return None, None
|
||||
|
||||
|
||||
def _create_decorator_from_action(
|
||||
action: Callable,
|
||||
decorator_key: str,
|
||||
on_conflict: OnConflict = OnConflict.SKIP):
|
||||
"""
|
||||
Fonction d'ordre supérieur qui construit un décorateur de fonction/méthode à partir des paramètres. Ce décorateur exécute l'action fournie.
|
||||
|
||||
Un contexte (dictionnaire) est passé à l'appel. Il contient :
|
||||
- l'état désactivé : clé CTX_KEY_DISABLED optionnelle
|
||||
|
||||
:param action: action à exécuter
|
||||
:type action: Callable[[dict, Callable[P, R], P.args, P.kwargs], R]
|
||||
|
||||
:param decorator_key: clé correspondant au décorateur, permet de gérer les conflits
|
||||
:type decorator_key: str, optional
|
||||
|
||||
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
||||
:type on_conflict: class:`OnConflict`
|
||||
"""
|
||||
def inner(func):
|
||||
if on_conflict is not OnConflict.STACK:
|
||||
former_decorator, former_target = _find_former_decorator(decorator_key, func)
|
||||
if former_decorator:
|
||||
if on_conflict is OnConflict.SKIP:
|
||||
# pas de nouveau décorateur
|
||||
return func
|
||||
|
||||
if on_conflict is OnConflict.REPLACE:
|
||||
if former_decorator is func and former_target:
|
||||
# ancien décorateur déjà fourni : on le remplace vraiment
|
||||
func = former_target
|
||||
else:
|
||||
# ancien décorateur déjà décoré : on le désactive
|
||||
former_ctx = getattr(former_decorator, DECORATOR_ATTR_CTX, None)
|
||||
if former_ctx is None:
|
||||
former_ctx = {}
|
||||
getattr(former_decorator, DECORATOR_ATTR_CTX, former_ctx)
|
||||
former_ctx.update({CTX_KEY_DISABLED: True})
|
||||
ctx = {}
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if ctx.get(CTX_KEY_DISABLED, False):
|
||||
# désactivé : simple appel de la fonction d'origine
|
||||
return func(*args, **kwargs)
|
||||
return action(ctx, func, *args, **kwargs)
|
||||
|
||||
setattr(wrapper, DECORATOR_ATTR_CTX, ctx)
|
||||
setattr(wrapper, DECORATOR_ATTR_KEY, decorator_key)
|
||||
setattr(wrapper, DECORATOR_ATTR_TARGET, func)
|
||||
return wrapper
|
||||
return inner
|
||||
|
||||
|
||||
def execution_time(
|
||||
level: int = logging.DEBUG,
|
||||
logger_name: Any = None,
|
||||
logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]] = None,
|
||||
on_conflict: OnConflict = OnConflict.SKIP,
|
||||
warn_after: int = None):
|
||||
"""
|
||||
Décorateur de fonction pour tracer le temps d'exécution (au niveau <level> avec un logger de performances).
|
||||
Pour une classe, le décorateur s'applique aux fonctions/méthodes qui passent le filtre.
|
||||
|
||||
:param level: niveau de base du logger, defaults to logging.DEBUG
|
||||
:type level: int
|
||||
|
||||
:param logger_name: valeur pour déterminer le nom du logger, ignorée si 'logger_factory' est renseigné, defaults to None
|
||||
:type logger_name: str, optional
|
||||
|
||||
:param logger_factory: fonction de création du logger à partir de la fonction annotée, annule 'logger_name', defaults to None.
|
||||
:type logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]], optional
|
||||
|
||||
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
||||
:type on_conflict: class:`OnConflict`
|
||||
|
||||
:param warn_after: durée (en ms) au-delà de laquelle on souhaite un log d'avertissement, defaults to None
|
||||
:type warn_after: int, optional
|
||||
|
||||
:return: résultat de la fonction d'origine
|
||||
"""
|
||||
|
||||
def action(ctx: Dict, func, *args, **kwargs):
|
||||
if CTX_KEY_LOGGER not in ctx:
|
||||
# initialisation du logger, pas toujours possible avant le premier appel
|
||||
ctx.update({CTX_KEY_LOGGER: logger_factory(func) if logger_factory else get_logger(logger_name or func, TAG_PERF)})
|
||||
|
||||
temps_debut = time.time()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
try:
|
||||
temps_ecoule = round((time.time() - temps_debut) * 1000)
|
||||
log_level = logging.WARNING if isinstance(warn_after, int) and temps_ecoule > warn_after else level
|
||||
ctx[CTX_KEY_LOGGER].log(log_level, "'%s' exécuté en %d ms", func.__name__, temps_ecoule)
|
||||
except Exception:
|
||||
logger.exception('impossible de tracer la durée')
|
||||
|
||||
return _create_decorator_from_action(action, inspect.currentframe().f_code.co_name, on_conflict=on_conflict)
|
||||
|
||||
|
||||
def query_count(
|
||||
level: int = logging.DEBUG,
|
||||
logger_name: Any = None,
|
||||
logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]] = None,
|
||||
on_conflict: OnConflict = OnConflict.SKIP,
|
||||
warn_after: int = None):
|
||||
"""
|
||||
Décorateur de fonction pour tracer le nombre de requêtes (au niveau <level> avec un logger de performances).
|
||||
Pour une classe, le décorateur s'applique aux fonctions/méthodes qui passent le filtre.
|
||||
|
||||
:param level: niveau de base du logger, defaults to logging.DEBUG
|
||||
:type level: int
|
||||
|
||||
:param logger_name: valeur pour déterminer le nom du logger, ignorée si 'logger_factory' est renseigné, defaults to None
|
||||
:type logger_name: str, optional
|
||||
|
||||
:param logger_factory: fonction de création du logger à partir de la fonction annotée, annule 'logger_name', defaults to None.
|
||||
:type logger_factory: Callable[[Any], Union[Logger, LoggerAdapter]], optional
|
||||
|
||||
:param on_conflict: stratégie de gestion de conflit quand le décorateur est déjà utilisé, defaults to OnConflict.SKIP
|
||||
:type on_conflict: class:`OnConflict`
|
||||
|
||||
:param warn_after: nombre de requêtes au-delà duquel on souhaite un log d'avertissement, defaults to None
|
||||
:type warn_after: int, optional
|
||||
|
||||
:return: résultat de la fonction d'origine
|
||||
"""
|
||||
|
||||
def action(ctx: Dict, func, *args, **kwargs):
|
||||
if CTX_KEY_LOGGER not in ctx:
|
||||
# initialisation du logger, pas toujours possible avant le premier appel
|
||||
ctx.update({CTX_KEY_LOGGER: logger_factory(func) if logger_factory else get_logger(logger_name or func, TAG_PERF)})
|
||||
|
||||
nb_debut = _get_query_count()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
try:
|
||||
nb_requetes = _get_query_count() - nb_debut
|
||||
log_level = logging.WARNING if isinstance(warn_after, int) and nb_requetes > warn_after else level
|
||||
ctx[CTX_KEY_LOGGER].log(log_level, "'%s' a exécuté %d requête(s)", func.__name__, nb_requetes)
|
||||
except Exception:
|
||||
logger.exception('impossible de tracer le nombre de requêtes')
|
||||
|
||||
return _create_decorator_from_action(action, inspect.currentframe().f_code.co_name, on_conflict=on_conflict)
|
||||
|
||||
|
||||
def class_logger(cls):
|
||||
"""
|
||||
Décorateur de classe pour stocker un logger standard dans l'attribut 'logger' (aucune gestion de conflit)
|
||||
"""
|
||||
if not inspect.isclass(cls):
|
||||
return cls
|
||||
|
||||
@functools.wraps(cls, updated=())
|
||||
class Wrapper(cls):
|
||||
decorated = 1
|
||||
|
||||
setattr(Wrapper, CLASS_ATTR_LOGGER, get_logger(Wrapper))
|
||||
return Wrapper
|
||||
|
||||
|
||||
def decorate_functions(
|
||||
decorator: Callable,
|
||||
func_filter: Callable[[Callable], bool],
|
||||
factory: bool = False):
|
||||
"""
|
||||
Décorateur de classe qui applique le décorateur donné aux fonctions/méthodes (attributs d'un type de FUNCTION_TYPES) qui passent le filtre.
|
||||
|
||||
:param decorator: décorateur à appliquer
|
||||
:type decorator: Callable
|
||||
|
||||
:param func_filter: filtre permettant de sélectionner les fonctions/méthodes à décorer
|
||||
:type func_filter: Callable[[Callable], bool]
|
||||
|
||||
:param factory: True indique que le décorateur est une méthode "factory" à exécuter avec la classe, defaults to False
|
||||
:type factory: bool
|
||||
"""
|
||||
|
||||
def decorate(cls):
|
||||
if not inspect.isclass(cls):
|
||||
return cls
|
||||
|
||||
@functools.wraps(cls, updated=())
|
||||
class Wrapper(cls):
|
||||
decorated = 1
|
||||
|
||||
_decorator = decorator(Wrapper) if factory else decorator
|
||||
for attr_name in dir(Wrapper):
|
||||
# __new__ et __init__ sont complexes à décorer, à gérer au cas par cas
|
||||
if attr_name != '__new__' and attr_name != '__init__':
|
||||
value = getattr(Wrapper, attr_name)
|
||||
if isinstance(value, FUNCTION_TYPES) and func_filter(value):
|
||||
setattr(Wrapper, attr_name, _decorator(value))
|
||||
return Wrapper
|
||||
return decorate
|
||||
1
backend-django/backend/utils/extraction/__init__.py
Normal file
1
backend-django/backend/utils/extraction/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .administre import *
|
||||
276
backend-django/backend/utils/extraction/administre.py
Normal file
276
backend-django/backend/utils/extraction/administre.py
Normal file
@@ -0,0 +1,276 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from backend.models.administre import Administre
|
||||
from backend.models.administre import StatutPamChoices as StatutPam
|
||||
from backend.models.domaine import Domaine
|
||||
from backend.models.filiere import Filiere
|
||||
from backend.models.fonction import Fonction
|
||||
from backend.models.formation_emploi import FormationEmploi
|
||||
from backend.models.grade import Grade
|
||||
from ..alimentation import BOCols
|
||||
from ..alimentation_decorators import data_perf_logger_factory, get_data_logger
|
||||
from ..decorators import execution_time
|
||||
|
||||
logger = get_data_logger(__name__)
|
||||
|
||||
|
||||
@execution_time(logger_factory=data_perf_logger_factory)
|
||||
def to_table_administres_bo(bo: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Création de la table Administrés à partir du fichier de données BO. Sélection et renommage des champs.
|
||||
|
||||
:param bo: table BO
|
||||
:type bo: class:`pandas.DataFrame`
|
||||
|
||||
:return: data frame contenant les information des administrés
|
||||
:rtype: class:`pandas.DataFrame`
|
||||
"""
|
||||
Cols = Administre.Cols
|
||||
|
||||
# import des tables contenant les clés étrangères
|
||||
fonctions = pd.DataFrame.from_records(Fonction.objects.all().values())
|
||||
domaines = pd.DataFrame.from_records(Domaine.objects.all().values())
|
||||
filieres = pd.DataFrame.from_records(Filiere.objects.all().values())
|
||||
|
||||
grades = pd.DataFrame.from_records(Grade.objects.all().values())
|
||||
fe = pd.DataFrame.from_records(FormationEmploi.objects.all().values())
|
||||
|
||||
# sélection des attributs nécessaires à la table administres
|
||||
col_adm = BOCols.columns(
|
||||
BOCols.ID_SAP,
|
||||
BOCols.CREDO_FE,
|
||||
BOCols.FONCTION,
|
||||
BOCols.GRADE,
|
||||
BOCols.DATE_DEBUT_GRADE,
|
||||
BOCols.NOM,
|
||||
BOCols.PRENOM,
|
||||
BOCols.SEXE,
|
||||
BOCols.ID_DEF,
|
||||
BOCols.EIP,
|
||||
BOCols.EIS,
|
||||
BOCols.DOMAINE,
|
||||
BOCols.FILIERE,
|
||||
BOCols.NF,
|
||||
BOCols.DOMAINE_GESTION,
|
||||
BOCols.DATE_ENTREE_SERVICE,
|
||||
BOCols.ARME,
|
||||
BOCols.REGROUPEMENT_ORIGINE_RECRUTEMENT,
|
||||
BOCols.DATE_NAISSANCE,
|
||||
BOCols.DIPLOME_PLUS_HAUT_NIVEAU,
|
||||
BOCols.DATE_RDC,
|
||||
BOCols.DATE_DERNIER_ACR,
|
||||
BOCols.DERNIER_DIPLOME,
|
||||
BOCols.DATE_ARRIVEE_FE,
|
||||
BOCols.POSITION_STATUTAIRE,
|
||||
BOCols.DATE_POSITION_STATUAIRE,
|
||||
BOCols.INTERRUPTION_SERVICE,
|
||||
BOCols.SITUATION_FAMILIALE,
|
||||
BOCols.DATE_MARIAGE,
|
||||
BOCols.NOMBRE_ENFANTS,
|
||||
BOCols.ENFANTS,
|
||||
BOCols.ID_DEF_CONJOINT,
|
||||
BOCols.FONCTION_1,
|
||||
BOCols.FONCTION_2,
|
||||
BOCols.FONCTION_3,
|
||||
BOCols.FONCTION_4,
|
||||
BOCols.FONCTION_5,
|
||||
BOCols.FONCTION_6,
|
||||
BOCols.FONCTION_7,
|
||||
BOCols.FONCTION_8,
|
||||
BOCols.FONCTION_9,
|
||||
BOCols.DATE_FONCTION_1,
|
||||
BOCols.DATE_FONCTION_2,
|
||||
BOCols.DATE_FONCTION_3,
|
||||
BOCols.DATE_FONCTION_4,
|
||||
BOCols.DATE_FONCTION_5,
|
||||
BOCols.DATE_FONCTION_6,
|
||||
BOCols.DATE_FONCTION_7,
|
||||
BOCols.DATE_FONCTION_8,
|
||||
BOCols.DATE_FONCTION_9,
|
||||
BOCols.NF_POSTE,
|
||||
BOCols.DOMAINE_POSTE,
|
||||
BOCols.FILIERE_POSTE,
|
||||
BOCols.PLS_GB_MAX,
|
||||
BOCols.MARQUEUR_PN,
|
||||
BOCols.PROFESSION_CONJOINT,
|
||||
BOCols.ID_SAP_CONJOINT,
|
||||
BOCols.SEXE_CONJOINT,
|
||||
BOCols.ORIGINE_RECRUTEMENT,
|
||||
BOCols.DATE_LIEN_SERVICE,
|
||||
BOCols.AGE_ANNEES,
|
||||
BOCols.STATUT_CONCERTO,
|
||||
BOCols.DATE_STATUT_CONCERTO,
|
||||
BOCols.STATUT_CONCERTO_FUTUR,
|
||||
BOCols.DATE_STATUT_CONCERTO_FUTUR,
|
||||
BOCols.FUD,
|
||||
BOCols.DATE_FUD
|
||||
)
|
||||
administres = bo[col_adm]
|
||||
|
||||
# jointure avec les tables contenant les clés étrangères
|
||||
# mapping avec les postes
|
||||
administres = administres.merge(fonctions, how='left', left_on=BOCols.FONCTION.value, right_on='fon_libelle')
|
||||
logger.debug("Nombre d'administrés dans le fichier")
|
||||
logger.debug('total administres : %s', administres.shape[0])
|
||||
administres = administres.merge(grades, how='inner', left_on=BOCols.GRADE.value, right_on='gr_code')
|
||||
logger.debug("Filtrage par grade reconnu")
|
||||
logger.debug("Nombre d'administres : %s", administres.shape[0])
|
||||
administres = administres.merge(fe, how='inner', left_on=BOCols.CREDO_FE.value, right_on='fe_code')
|
||||
logger.debug("Filtrage par FE reconnue")
|
||||
logger.debug("Nombre d'administres : %s", administres.shape[0])
|
||||
|
||||
# sélection et renommage des champs (BIEN FAIRE LE MAPPING DES COLONNES QU'ON VEUT GARDER)
|
||||
adm_mapping = BOCols.col_mapping({
|
||||
BOCols.ID_SAP: Cols.PK,
|
||||
'fe_code': f'{Cols.REL_FORMATION_EMPLOI}_id',
|
||||
'fon_id': "a_code_fonction",
|
||||
BOCols.GRADE: f'{Cols.REL_GRADE}_id',
|
||||
BOCols.DATE_DEBUT_GRADE: "a_grade_date_debut",
|
||||
BOCols.FONCTION: "a_fonction",
|
||||
BOCols.NOM: "a_nom",
|
||||
BOCols.PRENOM: "a_prenom",
|
||||
BOCols.SEXE: "a_sexe",
|
||||
BOCols.ID_DEF: "a_id_def",
|
||||
BOCols.EIP: "a_eip",
|
||||
BOCols.EIP: "a_eip_fiche_detaille",
|
||||
BOCols.EIS: "a_eis",
|
||||
BOCols.DOMAINE: Cols.REL_DOMAINE,
|
||||
BOCols.FILIERE: Cols.REL_FILIERE,
|
||||
BOCols.NF: 'a_nf',
|
||||
BOCols.DOMAINE_GESTION: "a_domaine_gestion",
|
||||
BOCols.DATE_ENTREE_SERVICE: "a_date_entree_service",
|
||||
BOCols.ARME: "a_arme",
|
||||
BOCols.REGROUPEMENT_ORIGINE_RECRUTEMENT: "a_rg_origine_recrutement",
|
||||
BOCols.DATE_RDC: 'a_date_rdc',
|
||||
BOCols.DATE_DERNIER_ACR: 'a_date_dernier_acr',
|
||||
BOCols.DATE_NAISSANCE: "a_date_naissance",
|
||||
BOCols.DIPLOME_PLUS_HAUT_NIVEAU: "a_diplome_hl",
|
||||
BOCols.DERNIER_DIPLOME: "a_dernier_diplome",
|
||||
BOCols.CREDO_FE: "a_credo_fe",
|
||||
BOCols.DATE_ARRIVEE_FE: "a_date_arrivee_fe",
|
||||
BOCols.POSITION_STATUTAIRE: "a_pos_statuaire",
|
||||
BOCols.DATE_POSITION_STATUAIRE: "a_date_pos_statuaire",
|
||||
BOCols.INTERRUPTION_SERVICE: "a_interruption_service",
|
||||
BOCols.SITUATION_FAMILIALE: "a_situation_fam",
|
||||
BOCols.DATE_MARIAGE: "a_date_mariage",
|
||||
BOCols.NOMBRE_ENFANTS: "a_nombre_enfants",
|
||||
BOCols.ENFANTS: "a_enfants",
|
||||
BOCols.ID_SAP_CONJOINT: "a_sap_conjoint",
|
||||
BOCols.FONCTION_1: "a_fonction1",
|
||||
BOCols.FONCTION_2: "a_fonction2",
|
||||
BOCols.FONCTION_3: "a_fonction3",
|
||||
BOCols.FONCTION_4: "a_fonction4",
|
||||
BOCols.FONCTION_5: "a_fonction5",
|
||||
BOCols.FONCTION_6: "a_fonction6",
|
||||
BOCols.FONCTION_7: "a_fonction7",
|
||||
BOCols.FONCTION_8: "a_fonction8",
|
||||
BOCols.FONCTION_9: "a_fonction9",
|
||||
BOCols.DATE_FONCTION_1: "a_date_fonction1",
|
||||
BOCols.DATE_FONCTION_2: "a_date_fonction2",
|
||||
BOCols.DATE_FONCTION_3: "a_date_fonction3",
|
||||
BOCols.DATE_FONCTION_4: "a_date_fonction4",
|
||||
BOCols.DATE_FONCTION_5: "a_date_fonction5",
|
||||
BOCols.DATE_FONCTION_6: "a_date_fonction6",
|
||||
BOCols.DATE_FONCTION_7: "a_date_fonction7",
|
||||
BOCols.DATE_FONCTION_8: "a_date_fonction8",
|
||||
BOCols.DATE_FONCTION_9: "a_date_fonction9",
|
||||
BOCols.NF_POSTE: "a_nf_poste",
|
||||
BOCols.DOMAINE_POSTE: "a_domaine_poste",
|
||||
BOCols.FILIERE_POSTE: "a_filiere_poste",
|
||||
BOCols.PLS_GB_MAX: "a_pls_gb_max",
|
||||
BOCols.MARQUEUR_PN: "a_marqueur_pn",
|
||||
BOCols.PROFESSION_CONJOINT: "a_profession_conjoint",
|
||||
BOCols.ID_DEF_CONJOINT: "a_id_def_conjoint",
|
||||
BOCols.SEXE_CONJOINT: "a_sexe_conjoint",
|
||||
BOCols.ORIGINE_RECRUTEMENT: "a_origine_recrutement",
|
||||
BOCols.STATUT_CONCERTO: Cols.STATUT_CONCERTO,
|
||||
BOCols.DATE_STATUT_CONCERTO: Cols.DATE_STATUT_CONCERTO,
|
||||
BOCols.STATUT_CONCERTO_FUTUR: Cols.STATUT_CONCERTO_FUTUR,
|
||||
BOCols.DATE_STATUT_CONCERTO_FUTUR: Cols.DATE_STATUT_CONCERTO_FUTUR,
|
||||
BOCols.FUD: 'a_fud',
|
||||
BOCols.DATE_FUD: 'a_date_fud',
|
||||
BOCols.DATE_LIEN_SERVICE: "a_lien_service",
|
||||
BOCols.AGE_ANNEES: "a_age_en_annees"
|
||||
})
|
||||
administres = administres.rename(columns=adm_mapping, errors='raise')
|
||||
|
||||
# initialisation des colonnes vides
|
||||
adm_col_vides = {
|
||||
'a_liste_id_marques': '',
|
||||
'a_notes_gestionnaire': '',
|
||||
'a_notes_partagees': '',
|
||||
'a_flag_particulier': 0
|
||||
}
|
||||
for k, v in adm_col_vides.items():
|
||||
administres[k] = v
|
||||
|
||||
administres = administres[list(adm_mapping.values()) + list(adm_col_vides.keys())]
|
||||
|
||||
administres_dom = administres.merge(domaines, how='inner', left_on=Cols.REL_DOMAINE, right_on='d_code')
|
||||
df = pd.merge(administres, administres_dom, on=[Cols.PK, Cols.REL_DOMAINE], how="left", indicator=True)
|
||||
df = df[df['_merge'] == 'left_only']
|
||||
domaines_not_found = df[Cols.REL_DOMAINE].drop_duplicates().tolist()
|
||||
administres_excluded_by_dom = df[Cols.PK].tolist()
|
||||
logger.debug("Filtrage par domaine actuel reconnu")
|
||||
logger.debug("Nombre d'administres : %s", administres_dom.shape[0])
|
||||
logger.debug('Domaines non retrouvés : %s', domaines_not_found)
|
||||
logger.debug('****************')
|
||||
logger.debug(administres_excluded_by_dom)
|
||||
logger.debug('****************')
|
||||
administres_fil = administres_dom.merge(filieres, how='inner', left_on=Cols.REL_FILIERE, right_on='f_code')
|
||||
# administres.merge(administres_fil, how='')
|
||||
df = pd.merge(administres_dom, administres_fil, on=[Cols.PK, Cols.REL_FILIERE], how="left", indicator=True)
|
||||
df = df[df['_merge'] == 'left_only']
|
||||
filieres_not_found = df[Cols.REL_FILIERE].drop_duplicates().tolist()
|
||||
administres_excluded_by_fil = df[Cols.PK].tolist()
|
||||
logger.debug("Filtrage par filière reconnue")
|
||||
logger.debug("Nombre d'administres restant : %s", administres_fil.shape[0])
|
||||
logger.debug('Filières non retrouvées : %s', filieres_not_found)
|
||||
logger.debug('****************')
|
||||
logger.debug(administres_excluded_by_fil)
|
||||
logger.debug('****************')
|
||||
administres = administres_fil
|
||||
|
||||
administres = administres.merge(domaines, how='left', left_on='a_domaine_poste', right_on='d_code', indicator=True)
|
||||
administres.loc[administres['_merge'] == 'left_only', 'a_domaine_poste'] = None
|
||||
administres.drop(columns='_merge', inplace=True)
|
||||
administres = administres.merge(filieres, how='left', left_on='a_filiere_poste', right_on='f_code', indicator=True)
|
||||
administres.loc[administres['_merge'] == 'left_only', 'a_filiere_poste'] = None
|
||||
# administres = administres.merge(filieres, how='inner', left_on='a_filiere_poste', right_on='f_code')
|
||||
|
||||
administres['a_categorie'] = administres["a_nf"].replace(
|
||||
{'1A': 'MDR', '1B': 'MDR', '1C': 'MDR', '2.': 'SOFF', '3A': 'SOFF', '3B': 'SOFF', '3B NFS': 'SOFF', '4.': 'OFF',
|
||||
'5A': 'OFF', '5B': 'OFF', '5C': 'OFF', '6A': 'OGX', '6B': 'OGX'}, inplace=False)
|
||||
# administres["a_nom_prenom"].fillna('NOM Prenom', inplace=True)
|
||||
# # administres["a_nom_prenom"].replace('', 'NOM Prenom', inplace=True)
|
||||
# nom_prenom_split = administres['a_nom_prenom'].str.split(r" +", n=1).str
|
||||
# administres["a_nom"] = nom_prenom_split.get(0).fillna('')
|
||||
# administres["a_prenom"] = nom_prenom_split.get(1).fillna('')
|
||||
# administres.drop("a_nom_prenom", inplace=True, axis=1)
|
||||
administres['a_nom'] = administres['a_nom'].fillna('NOM')
|
||||
administres['a_prenom'] = administres['a_prenom'].fillna('Prénom')
|
||||
|
||||
administres[f'{Cols.REL_FORMATION_EMPLOI}_id'] = administres[f'{Cols.REL_FORMATION_EMPLOI}_id'].replace({np.nan: None})
|
||||
administres[f'{Cols.REL_GRADE}_id'] = administres[f'{Cols.REL_GRADE}_id'].replace({np.nan: None})
|
||||
|
||||
administres = administres.drop_duplicates(subset=[Cols.PK])
|
||||
logger.debug('Retrait des doublons')
|
||||
logger.debug("Nombre d'administres restants : %s", administres.shape[0])
|
||||
|
||||
administres = administres.reset_index(drop=True)
|
||||
administres['a_sap_conjoint'] = administres['a_sap_conjoint'].replace({np.nan: None})
|
||||
administres['a_nf_poste'] = (administres['a_nf_poste'].replace({np.nan, None})
|
||||
.apply(lambda x: str(x)[1:])
|
||||
.str.upper())
|
||||
administres['a_nf'] = administres['a_nf'].str.upper()
|
||||
administres['a_eip'] = administres[Cols.REL_DOMAINE] + administres[Cols.REL_FILIERE] + administres['a_nf']
|
||||
administres['a_marqueur_pn'] = administres['a_marqueur_pn'].apply(lambda x: True if x == 'X' else False)
|
||||
|
||||
administres = (administres.fillna(np.nan)
|
||||
.replace([np.nan], [None]))
|
||||
logger.debug("Nombre d'administres extrait : %s", administres.shape[0])
|
||||
return administres
|
||||
|
||||
|
||||
20
backend-django/backend/utils/functions.py
Normal file
20
backend-django/backend/utils/functions.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import inspect
|
||||
import sys
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def find_class(func: Callable):
|
||||
"""
|
||||
basé sur inspect._findclass mais plus robuste et qui prend en compte les fonctions locales
|
||||
note : la fonction doit avoir un module sinon None est renvoyé (ex : str => None)
|
||||
"""
|
||||
|
||||
cls = sys.modules.get(getattr(func, '__module__', None))
|
||||
if cls is None:
|
||||
return None
|
||||
for name in func.__qualname__.split('.')[:-1]:
|
||||
if name != '<locals>':
|
||||
cls = getattr(cls, name, None)
|
||||
if not inspect.isclass(cls):
|
||||
return None
|
||||
return cls
|
||||
187
backend-django/backend/utils/initial.py
Normal file
187
backend-django/backend/utils/initial.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Ce module contient les Utilitaires du backend
|
||||
"""
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
import re
|
||||
from django.forms import model_to_dict
|
||||
from backend import constants
|
||||
from backend.models import Administre, Poste, SousVivier, FormationEmploi, RefSvFil, SousVivierAssociation
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
def nf2categorie(nf: str) -> str:
|
||||
"""
|
||||
Renvoie la catégorie associé au niveau fonctionnel en argument
|
||||
|
||||
:type nf: str
|
||||
:param nf: niveau fonctionnel
|
||||
|
||||
:return: catégorie associée au niveau fonctionnel
|
||||
:rtype: str
|
||||
"""
|
||||
list_mdr = ['1A', '1B', '1C']
|
||||
list_soff = ['2.', '3A', '3B', '3B NFS']
|
||||
list_off = ['4.', '5A', '5B', '5C', '6A', '6B']
|
||||
cat = 'MDR' if nf in list_mdr else 'SOFF' if nf in list_soff else 'OFF' if nf in list_off else None
|
||||
return cat
|
||||
|
||||
|
||||
def generate_sv_id(dom: str, fil: str, cat: str) -> str:
|
||||
"""
|
||||
Génère un id de sous-vivier à partir d'un domaine, d'une filière et d'une catégorie.
|
||||
|
||||
:type dom: str
|
||||
:param dom: domaine du sous-vivier
|
||||
|
||||
:type fil: str
|
||||
:param fil: filiere du sous-vivier
|
||||
|
||||
:type cat: str
|
||||
:param cat: catégorie du sous-vivier
|
||||
|
||||
:return: id du sous-vivier
|
||||
:rtype: str
|
||||
"""
|
||||
return f"{dom}, {fil}, {cat}"
|
||||
|
||||
|
||||
def sous_viviers_du_cellule(cellule_code):
|
||||
# Récupère les sous-viviers associés à la cellule
|
||||
sous_vivier = RefSvFil.objects.filter(ref_sv_fil_code=cellule_code)
|
||||
if sous_vivier.exists():
|
||||
sous_viviers = list(sous_vivier.values_list('sous_vivier_id', flat=True))
|
||||
else :
|
||||
return []
|
||||
return sous_viviers
|
||||
|
||||
|
||||
def without_keys(d, keys):
|
||||
"""
|
||||
Suppression des clés d'un dictionnaire
|
||||
|
||||
:type d: Dictionnaire
|
||||
:param d: Dictionnaire contenant les ids
|
||||
|
||||
:type keys: String
|
||||
:param keys: Les clés que nous devons supprimer du dictionnaire
|
||||
|
||||
:return: - **Dictionnaire** (*Dictionnaire*): Dictionnaire sans les clés données en argument.
|
||||
"""
|
||||
return {x: d[x] for x in d if x not in keys}
|
||||
|
||||
def check_positive(valeur_nb):
|
||||
"""
|
||||
Vérifier si une valeur est positive et retourner 1, si elle est nulle ou négative retourner 0.
|
||||
|
||||
:type poste_nb: DataFrame
|
||||
:param poste_nb: ID du sous-vivier
|
||||
|
||||
:return: - **valeur_nb_modifie** (*dataframe*): Dataframe contenant des valeurs 1 ou 0.
|
||||
"""
|
||||
valeur_nb_modifie = valeur_nb.apply(lambda x: 1 if x > 0 else 0)
|
||||
return valeur_nb_modifie
|
||||
|
||||
def cleanString(string):
|
||||
"""Cette fonction supprimera tous les caractères qui ne sont pas alphanumériques.
|
||||
|
||||
:type string: chaîne de caractères
|
||||
:param string: chaîne qui doit être nettoyée
|
||||
|
||||
:return: - **return** (*chaîne de caractères*): chaîne nettoyée.
|
||||
"""
|
||||
return ''.join([i for i in string if i.isalnum()])
|
||||
|
||||
|
||||
def intOrNone(value):
|
||||
"""Cette fonction renvoie l'entier de la valeur ou renvoie None.
|
||||
|
||||
:type value: float
|
||||
:param value: valeur a con
|
||||
|
||||
:return: - **res** (*int or None*): valeur à convertir.
|
||||
"""
|
||||
try:
|
||||
res = int(float(value))
|
||||
except Exception:
|
||||
res = None
|
||||
|
||||
return res
|
||||
|
||||
def impact_decisions(old_administre, administre, old_avis, avis, eip, fe_code, categorie):
|
||||
"""
|
||||
|
||||
:type administre: objet
|
||||
:param administre: instance du model Administre
|
||||
|
||||
:type old_avis: chaine de caractères
|
||||
:param old_avis: avis de l'administré avant la mise à jour
|
||||
|
||||
:type avis: chaine de caractère
|
||||
:param avis: nouvel avis de l'administré
|
||||
|
||||
:type eip: chaine de caractère
|
||||
:param eip: eip actuel du militaire
|
||||
|
||||
:type categorie: dataframe pandas
|
||||
:param categorie: Dataframe contenant les données pretraités à inserer
|
||||
|
||||
:return: - **list_error** (*liste*): liste des identifiants SAP pour lesquels il y a eu une erreur.
|
||||
"""
|
||||
|
||||
list_error = []
|
||||
if (old_avis == 'NON_ETUDIE' or old_avis == 'A_MAINTENIR' or old_avis == 'A_ETUDIER') and (avis == 'A_MUTER' or avis == 'PARTANT' or avis == 'NON_DISPONIBLE'):
|
||||
poste_qs = Poste.objects.filter(p_eip__iexact=eip, formation_emploi_id=fe_code)
|
||||
poste = poste_qs.first()
|
||||
fe = FormationEmploi.objects.get(fe_code=fe_code)
|
||||
fe.save()
|
||||
|
||||
if poste and poste.p_nb_non_etudie > 0:
|
||||
new_nb_p4, new_nb_non_etudie, new_nb_vacant, new_nb_occupe = poste.p_nb_p4 + 1, poste.p_nb_non_etudie - 1, poste.p_nb_vacant + 1, poste.p_nb_occupe - 1
|
||||
poste_qs.update(p_nb_p4=new_nb_p4, p_nb_non_etudie=new_nb_non_etudie,
|
||||
p_nb_vacant=new_nb_vacant,
|
||||
p_nb_occupe=new_nb_occupe)
|
||||
|
||||
else:
|
||||
list_error.append(administre.a_id_sap)
|
||||
|
||||
if (old_avis == 'A_MUTER' or old_avis == 'PARTANT' or old_avis == 'NON_DISPONIBLE') and (avis == 'NON_ETUDIE' or avis == 'A_MAINTENIR' or avis == 'A_ETUDIER'):
|
||||
poste_qs = Poste.objects.filter(p_eip__iexact=eip, formation_emploi_id=fe_code).exclude(p_nb_occupe=0)
|
||||
poste = poste_qs.first()
|
||||
fe = FormationEmploi.objects.get(fe_code=fe_code)
|
||||
if categorie == constants.CATEGORIE_MDR:
|
||||
fe.fe_nb_poste_vacant_mdr = fe.fe_nb_poste_vacant_mdr - 1
|
||||
fe.fe_nb_poste_occupe_mdr = fe.fe_nb_poste_occupe_mdr + 1
|
||||
elif categorie == constants.CATEGORIE_SOFF:
|
||||
fe.fe_nb_poste_vacant_soff = fe.fe_nb_poste_vacant_soff - 1
|
||||
fe.fe_nb_poste_occupe_soff = fe.fe_nb_poste_occupe_soff + 1
|
||||
elif categorie == constants.CATEGORIE_OFF:
|
||||
fe.fe_nb_poste_vacant_off = fe.fe_nb_poste_vacant_off - 1
|
||||
fe.fe_nb_poste_occupe_off = fe.fe_nb_poste_occupe_off + 1
|
||||
|
||||
fe.save()
|
||||
|
||||
if poste and poste.p_nb_p4 > 0:
|
||||
new_nb_p4, new_nb_non_etudie, new_nb_vacant, new_nb_occupe = poste.p_nb_p4 - 1, poste.p_nb_non_etudie + 1, poste.p_nb_vacant - 1, poste.p_nb_occupe + 1
|
||||
poste_qs.update(p_nb_p4=new_nb_p4, p_nb_non_etudie=new_nb_non_etudie,
|
||||
p_nb_vacant=new_nb_vacant,
|
||||
p_nb_occupe=new_nb_occupe)
|
||||
if poste and poste.p_nb_p4 == 0 and poste.p_nb_p3 > 0:
|
||||
new_nb_p3, new_nb_non_etudie, new_nb_vacant, new_nb_occupe = poste.p_nb_p3 - 1, poste.p_nb_non_etudie + 1, poste.p_nb_vacant - 1, poste.p_nb_occupe + 1
|
||||
poste_qs.update(p_nb_p3=new_nb_p3, p_nb_non_etudie=new_nb_non_etudie,
|
||||
p_nb_vacant=new_nb_vacant,
|
||||
p_nb_occupe=new_nb_occupe)
|
||||
if poste and poste.p_nb_p4 == 0 and poste.p_nb_p3 == 0 and poste.p_nb_p2 > 0:
|
||||
new_nb_p2, new_nb_non_etudie, new_nb_vacant, new_nb_occupe = poste.p_nb_p2 - 1, poste.p_nb_non_etudie + 1, poste.p_nb_vacant - 1, poste.p_nb_occupe + 1
|
||||
poste_qs.update(p_nb_p2=new_nb_p2, p_nb_non_etudie=new_nb_non_etudie,
|
||||
p_nb_vacant=new_nb_vacant,
|
||||
p_nb_occupe=new_nb_occupe)
|
||||
if poste and poste.p_nb_p4 == 0 and poste.p_nb_p3 == 0 and poste.p_nb_p2 == 0 and poste.p_nb_p1 > 0:
|
||||
new_nb_p1, new_nb_non_etudie, new_nb_vacant, new_nb_occupe = poste.p_nb_p1 - 1, poste.p_nb_non_etudie + 1, poste.p_nb_vacant - 1, poste.p_nb_occupe + 1
|
||||
poste_qs.update(p_nb_p1=new_nb_p1, p_nb_non_etudie=new_nb_non_etudie,
|
||||
p_nb_vacant=new_nb_vacant,
|
||||
p_nb_occupe=new_nb_occupe)
|
||||
else:
|
||||
list_error.append(administre.a_id_sap)
|
||||
return list_error
|
||||
2
backend-django/backend/utils/insertion/__init__.py
Normal file
2
backend-django/backend/utils/insertion/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .administre import *
|
||||
from .commun import *
|
||||
448
backend-django/backend/utils/insertion/administre.py
Normal file
448
backend-django/backend/utils/insertion/administre.py
Normal file
@@ -0,0 +1,448 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from backend.models.administre import Administre, Administres_Pams
|
||||
from backend.models.administre import StatutPamChoices as StatutPam
|
||||
from backend.models.pam import PAM
|
||||
from backend.models.domaine import Domaine
|
||||
from backend.models.filiere import Filiere
|
||||
from backend.models.formation_emploi import FormationEmploi
|
||||
from backend.models.grade import Grade
|
||||
from django.db.models import Q
|
||||
|
||||
from ..alimentation_decorators import data_perf_logger_factory, get_data_logger
|
||||
from ..decorators import execution_time
|
||||
from .commun import (InsertionCounters, batch_iterator, is_same_model,
|
||||
is_same_value)
|
||||
|
||||
logger = get_data_logger(__name__)
|
||||
|
||||
|
||||
@execution_time(level=logging.INFO, logger_factory=data_perf_logger_factory)
|
||||
def update_administre_fmob(df) -> None:
|
||||
"""
|
||||
Met à jour le statut PAM des administrés quand un formulaire de mobilité est annulé.
|
||||
"""
|
||||
ModelType = Administres_Pams
|
||||
Cols = ModelType.Cols
|
||||
status = StatutPam.A_MAINTENIR
|
||||
|
||||
annee_pam = str(int(df['fmob_millesime'].iloc[0]))
|
||||
|
||||
|
||||
updated = (ModelType.objects
|
||||
.filter(**{f'{Cols.O2M_FMOB}__fmob_annulation_fmob': True})
|
||||
.filter(~Q(**{Cols.STATUT_PAM: status}))
|
||||
.filter(a_statut_pam_annee=StatutPam.NON_ETUDIE)
|
||||
.filter(pam__pam_id=annee_pam)
|
||||
.update(**{Cols.STATUT_PAM: status}))
|
||||
if updated:
|
||||
logger.info('%s | mis à jour car FMOB annulé : %s', ModelType.__name__, updated)
|
||||
|
||||
logger.info('Début de la mise à jour des FMOB en "A traiter"')
|
||||
|
||||
next_status = StatutPam.A_TRAITER
|
||||
next_updated = (ModelType.objects
|
||||
.filter(**{f'{Cols.O2M_FMOB}__isnull' : False})
|
||||
.filter(~Q(**{Cols.STATUT_PAM: status}))
|
||||
.filter(a_statut_pam_annee=StatutPam.NON_ETUDIE)
|
||||
.filter(pam__pam_id=annee_pam)
|
||||
.update(**{Cols.STATUT_PAM: next_status}))
|
||||
if next_updated:
|
||||
logger.info('%s | mis à jour car FMOB existe: %s administrés qui ont un FORMOB', ModelType.__name__, next_updated)
|
||||
logger.info("%s[pk=%s] administré mis à jour", ModelType.__name__,next_updated)
|
||||
|
||||
return updated, next_updated
|
||||
|
||||
|
||||
|
||||
|
||||
@execution_time(level=logging.INFO, logger_factory=data_perf_logger_factory)
|
||||
def insert_administre_bo(df: pd.DataFrame) -> None:
|
||||
"""
|
||||
Insère ou met à jour des données de la table des administrés.
|
||||
|
||||
:param df: Dataframe contenant les données pretraités à inserer
|
||||
:type df: class:`pandas.DataFrame`
|
||||
"""
|
||||
logger.info('start update_administre')
|
||||
ModelType = Administre
|
||||
ModelType_2 = Administres_Pams
|
||||
Cols = ModelType.Cols
|
||||
Cols_2 = ModelType_2.Cols
|
||||
col_pk = Cols.PK
|
||||
fields_to_update = [
|
||||
'formation_emploi_id',
|
||||
'a_code_fonction',
|
||||
'grade_id',
|
||||
'a_grade_date_debut',
|
||||
'a_fonction',
|
||||
'a_nom',
|
||||
'a_prenom',
|
||||
'a_sexe',
|
||||
'a_id_def',
|
||||
'a_eip',
|
||||
'a_eip_fiche_detaille',
|
||||
'a_eis',
|
||||
Cols.REL_DOMAINE,
|
||||
Cols.REL_FILIERE,
|
||||
'a_nf',
|
||||
'a_domaine_gestion',
|
||||
'a_date_entree_service',
|
||||
'a_arme',
|
||||
'a_rg_origine_recrutement',
|
||||
'a_date_rdc',
|
||||
'a_date_dernier_acr',
|
||||
'a_date_naissance',
|
||||
'a_diplome_hl',
|
||||
'a_dernier_diplome',
|
||||
'a_credo_fe',
|
||||
'a_date_arrivee_fe',
|
||||
'a_date_pos_statuaire',
|
||||
'a_pos_statuaire',
|
||||
'a_interruption_service',
|
||||
'a_situation_fam',
|
||||
'a_date_mariage',
|
||||
'a_nombre_enfants',
|
||||
'a_enfants',
|
||||
'a_sap_conjoint',
|
||||
'a_fonction1',
|
||||
'a_fonction2',
|
||||
'a_fonction3',
|
||||
'a_fonction4',
|
||||
'a_fonction5',
|
||||
'a_fonction6',
|
||||
'a_fonction7',
|
||||
'a_fonction8',
|
||||
'a_fonction9',
|
||||
'a_date_fonction1',
|
||||
'a_date_fonction2',
|
||||
'a_date_fonction3',
|
||||
'a_date_fonction4',
|
||||
'a_date_fonction5',
|
||||
'a_date_fonction6',
|
||||
'a_date_fonction7',
|
||||
'a_date_fonction8',
|
||||
'a_date_fonction9',
|
||||
'a_pls_gb_max',
|
||||
'a_marqueur_pn',
|
||||
'a_profession_conjoint',
|
||||
'a_id_def_conjoint',
|
||||
'a_sexe_conjoint',
|
||||
'a_origine_recrutement',
|
||||
Cols.STATUT_CONCERTO,
|
||||
Cols.DATE_STATUT_CONCERTO,
|
||||
Cols.STATUT_CONCERTO_FUTUR,
|
||||
Cols.DATE_STATUT_CONCERTO_FUTUR,
|
||||
'a_fud',
|
||||
'a_date_fud',
|
||||
'a_lien_service',
|
||||
'a_categorie',
|
||||
]
|
||||
|
||||
|
||||
fields_to_update_2 = [
|
||||
'pam_id',
|
||||
'administre_id',
|
||||
'a_statut_pam_annee',
|
||||
]
|
||||
models_in_db = {m.pk: m for m in ModelType.objects.only(col_pk, *fields_to_update)}
|
||||
fields_db = ['a_id_sap'] + fields_to_update
|
||||
|
||||
# Integer ou Float Colonnes à mettre à jour
|
||||
fields_num = [
|
||||
'a_sap_conjoint',
|
||||
'a_pls_gb_max',
|
||||
'a_nombre_enfants',
|
||||
'a_id_sap'
|
||||
]
|
||||
|
||||
# Dict pour convertir toutes les colonnes non Integer en chaîne de caractères.
|
||||
dict_conv_str = {a: str for a in fields_db if a not in fields_num}
|
||||
|
||||
# Dict pour convertir toutes les colonnes Integer en Float.
|
||||
dict_conv_float = {a: float for a in fields_num }
|
||||
|
||||
# Lire tous les administrés de la base de données
|
||||
adm_in_db_df = pd.DataFrame.from_records(Administre.objects.all().values_list(*tuple(fields_db)), columns = fields_db)
|
||||
df = df.drop(['_merge'],1)
|
||||
|
||||
if not adm_in_db_df.empty :
|
||||
# Il va y avoir de modification de type donc c'est mieux de ne pas toucher df
|
||||
df_comparaison = df.copy()
|
||||
|
||||
# Modification de type de quelque champs
|
||||
logger.debug('Conversion des types pour la fusion')
|
||||
|
||||
|
||||
df_comparaison = df_comparaison.fillna(np.nan)
|
||||
df_comparaison = df_comparaison.replace('None', np.nan)
|
||||
adm_in_db_df = adm_in_db_df.fillna(np.nan)
|
||||
adm_in_db_df = adm_in_db_df.replace('None', np.nan)
|
||||
|
||||
adm_in_db_df = adm_in_db_df.astype(dict_conv_str)
|
||||
adm_in_db_df = adm_in_db_df.astype(dict_conv_float)
|
||||
|
||||
df_comparaison = df_comparaison.astype(dict_conv_str)
|
||||
df_comparaison = df_comparaison.astype(dict_conv_float)
|
||||
|
||||
compare = pd.DataFrame([df_comparaison[fields_db].dtypes,adm_in_db_df[fields_db].dtypes]).T
|
||||
logger.debug('Comparaison des types pour la fusion')
|
||||
# logger.debug(compare[compare[0]!=compare[1]].dropna())
|
||||
logger.debug('------------------------------------')
|
||||
|
||||
# Comparaison pour savoir ce qui doit etre creer, mis a jour ou supprimer
|
||||
|
||||
comparing_adm_sap = pd.merge(df_comparaison, adm_in_db_df, how='outer', on = 'a_id_sap', suffixes=(None, "_x"), indicator=True)
|
||||
same_rows = comparing_adm_sap[comparing_adm_sap['_merge'] == 'both'].drop('_merge', axis=1)
|
||||
new_rows = comparing_adm_sap[comparing_adm_sap['_merge'] == 'left_only'].drop('_merge', axis=1)
|
||||
delete_rows = comparing_adm_sap[comparing_adm_sap['_merge'] == 'right_only'].drop('_merge', axis=1)
|
||||
|
||||
|
||||
# Comparaison pour savoir des ligne a mettre a jour, lequel est deja a jour et lequel doit se mettre a jour
|
||||
|
||||
comparing_adm_both = pd.merge(same_rows, adm_in_db_df, how='left', on=fields_db,
|
||||
suffixes=(None, "_x"), indicator=True)
|
||||
|
||||
not_updated_rows = comparing_adm_both[comparing_adm_both['_merge'] == 'both'].drop(['_merge'], axis=1)
|
||||
updated_rows = comparing_adm_both[comparing_adm_both['_merge'] == 'left_only'].drop(['_merge'], axis=1)
|
||||
|
||||
# Creation du df final avec une colonne db_create_status qui dis si la ligne doit etre creer ou mis a jour
|
||||
|
||||
update = df.loc[df['a_id_sap'].isin(list(updated_rows['a_id_sap']))]
|
||||
update['db_create_status'] = 0
|
||||
create = df.loc[df['a_id_sap'].isin(list(new_rows['a_id_sap']))]
|
||||
create['db_create_status'] = 1
|
||||
|
||||
df = pd.concat([update,create])
|
||||
|
||||
|
||||
else :
|
||||
df['db_create_status'] = 1
|
||||
not_updated_rows = pd.DataFrame([])
|
||||
|
||||
|
||||
# IDs existants pour les clés étrangères
|
||||
|
||||
domaines_in_db = set(Domaine.objects.values_list('pk', flat=True))
|
||||
filieres_in_db = set(Filiere.objects.values_list('pk', flat=True))
|
||||
formations_in_db = set(FormationEmploi.objects.values_list('pk', flat=True))
|
||||
grades_in_db = set(Grade.objects.values_list('pk', flat=True))
|
||||
|
||||
fields_not_validated = [f.name for f in ModelType._meta.get_fields() if f.is_relation]
|
||||
fields_not_validated_2 = [f.name for f in ModelType_2._meta.get_fields() if f.is_relation]
|
||||
|
||||
dict_create = {}
|
||||
dict_update = {}
|
||||
dict_up_to_date = {}
|
||||
dict_create_2 = {}
|
||||
set_dom = set()
|
||||
set_fil = set()
|
||||
counters = InsertionCounters()
|
||||
|
||||
annee_pam = PAM.objects.filter(pam_statut='PAM en cours')[0].pam_id
|
||||
annee_pam_suivant = PAM.objects.filter(pam_statut='PAM A+1')[0].pam_id
|
||||
|
||||
def process_row(row):
|
||||
pk = str(row[col_pk])
|
||||
pk_2 = pk + str(annee_pam)
|
||||
pk_3 = pk + str(annee_pam_suivant)
|
||||
try:
|
||||
domaine = row['a_domaine']
|
||||
if domaine is not None and domaine not in domaines_in_db:
|
||||
# TODO déjà fait dans l'extraction, serait mieux placé ici
|
||||
logger.warning("%s[pk=%s] domaine ignoré car absent du référentiel : %s", ModelType.__name__, pk, domaine)
|
||||
set_dom.add(domaine)
|
||||
counters.ignored += 1
|
||||
return
|
||||
|
||||
filiere = row['a_filiere']
|
||||
if filiere is not None and filiere not in filieres_in_db:
|
||||
# TODO déjà fait dans l'extraction, serait mieux placé ici
|
||||
logger.warning("%s[pk=%s] filière ignoré car absente du référentiel : %s", ModelType.__name__, pk, filiere)
|
||||
set_fil.add(filiere)
|
||||
counters.ignored += 1
|
||||
return
|
||||
|
||||
grade = row['grade_id']
|
||||
if grade is not None and grade not in grades_in_db:
|
||||
# TODO déjà fait dans l'extraction, serait mieux placé ici
|
||||
logger.warning("%s[pk=%s] ignoré car grade inconnu : %s", ModelType.__name__, pk, grade)
|
||||
counters.ignored += 1
|
||||
return
|
||||
|
||||
formation = row['formation_emploi_id']
|
||||
if formation is not None and formation not in formations_in_db:
|
||||
# TODO déjà fait dans l'extraction, serait mieux placé ici
|
||||
logger.warning("%s[pk=%s] ignoré car formation-emploi inconnue : %s", ModelType.__name__, pk, formation)
|
||||
counters.ignored += 1
|
||||
return
|
||||
|
||||
|
||||
model_2 = ModelType_2(**{
|
||||
'id' :pk_2,
|
||||
'pam_id' :annee_pam,
|
||||
'administre_id' :pk,
|
||||
'a_statut_pam_annee' :StatutPam.NON_ETUDIE,
|
||||
})
|
||||
|
||||
model_3 = ModelType_2(**{
|
||||
'id' :pk_3,
|
||||
'pam_id' :annee_pam_suivant,
|
||||
'administre_id' :pk,
|
||||
'a_statut_pam_annee' :StatutPam.NON_ETUDIE,
|
||||
})
|
||||
|
||||
in_db = models_in_db.get(pk)
|
||||
model = ModelType(**{
|
||||
'pk': pk,
|
||||
'formation_emploi_id': formation,
|
||||
'grade_id': grade,
|
||||
'a_grade_date_debut': row['a_grade_date_debut'],
|
||||
'a_liste_id_marques': row['a_liste_id_marques'],
|
||||
'a_nom': row['a_nom'],
|
||||
'a_prenom': row['a_prenom'],
|
||||
'a_sexe': row['a_sexe'],
|
||||
'a_id_def': row['a_id_def'],
|
||||
'a_eip': row['a_eip'],
|
||||
'a_eip_fiche_detaille': row['a_eip_fiche_detaille'],
|
||||
f'{Cols.REL_DOMAINE}_id': domaine,
|
||||
'a_domaine_futur_id': domaine,
|
||||
f'{Cols.REL_FILIERE}_id': filiere,
|
||||
'a_filiere_futur_id': filiere,
|
||||
'a_fonction': row['a_fonction'],
|
||||
'a_code_fonction': row['a_code_fonction'],
|
||||
'a_nf': row['a_nf'],
|
||||
'a_nf_futur': row['a_nf'],
|
||||
'a_categorie': row['a_categorie'],
|
||||
'a_domaine_gestion': row['a_domaine_gestion'],
|
||||
'a_date_entree_service': row['a_date_entree_service'],
|
||||
'a_arme': row['a_arme'],
|
||||
'a_rg_origine_recrutement': row['a_rg_origine_recrutement'],
|
||||
'a_date_naissance': row['a_date_naissance'],
|
||||
'a_diplome_hl': row['a_diplome_hl'],
|
||||
'a_dernier_diplome': row['a_dernier_diplome'],
|
||||
'a_credo_fe': row['a_credo_fe'],
|
||||
'a_date_arrivee_fe': row['a_date_arrivee_fe'],
|
||||
'a_date_pos_statuaire': row['a_date_pos_statuaire'],
|
||||
'a_pos_statuaire': row['a_pos_statuaire'],
|
||||
'a_interruption_service': row['a_interruption_service'],
|
||||
'a_situation_fam': row['a_situation_fam'],
|
||||
'a_date_mariage': row['a_date_mariage'],
|
||||
'a_nombre_enfants': row['a_nombre_enfants'],
|
||||
'a_enfants': row['a_enfants'],
|
||||
'a_date_rdc': row['a_date_rdc'],
|
||||
'a_date_dernier_acr': row['a_date_dernier_acr'],
|
||||
'a_eis': row['a_eis'],
|
||||
'a_sap_conjoint': row['a_sap_conjoint'],
|
||||
'a_flag_particulier': row['a_flag_particulier'],
|
||||
'a_notes_gestionnaire': row['a_notes_gestionnaire'],
|
||||
'a_notes_partagees': row['a_notes_partagees'],
|
||||
'a_fonction1': row['a_fonction1'],
|
||||
'a_fonction2': row['a_fonction2'],
|
||||
'a_fonction3': row['a_fonction3'],
|
||||
'a_fonction4': row['a_fonction4'],
|
||||
'a_fonction5': row['a_fonction5'],
|
||||
'a_fonction6': row['a_fonction6'],
|
||||
'a_fonction7': row['a_fonction7'],
|
||||
'a_fonction8': row['a_fonction8'],
|
||||
'a_fonction9': row['a_fonction9'],
|
||||
'a_date_fonction1': row['a_date_fonction1'],
|
||||
'a_date_fonction2': row['a_date_fonction2'],
|
||||
'a_date_fonction3': row['a_date_fonction3'],
|
||||
'a_date_fonction4': row['a_date_fonction4'],
|
||||
'a_date_fonction5': row['a_date_fonction5'],
|
||||
'a_date_fonction6': row['a_date_fonction6'],
|
||||
'a_date_fonction7': row['a_date_fonction7'],
|
||||
'a_date_fonction8': row['a_date_fonction8'],
|
||||
'a_date_fonction9': row['a_date_fonction9'],
|
||||
'a_pls_gb_max': row['a_pls_gb_max'],
|
||||
'a_marqueur_pn': row['a_marqueur_pn'],
|
||||
'a_profession_conjoint': row['a_profession_conjoint'],
|
||||
'a_id_def_conjoint': row['a_id_def_conjoint'],
|
||||
'a_sexe_conjoint': row['a_sexe_conjoint'],
|
||||
'a_origine_recrutement': row['a_origine_recrutement'],
|
||||
'a_lien_service': row['a_lien_service'],
|
||||
'a_statut_concerto': row[Cols.STATUT_CONCERTO],
|
||||
'a_date_statut_concerto': row[Cols.DATE_STATUT_CONCERTO],
|
||||
'a_statut_concerto_futur': row[Cols.STATUT_CONCERTO_FUTUR],
|
||||
'a_date_statut_concerto_futur': row[Cols.DATE_STATUT_CONCERTO_FUTUR],
|
||||
'a_fud': row['a_fud'],
|
||||
'a_date_fud': row['a_date_fud'],
|
||||
})
|
||||
if row['db_create_status']:
|
||||
model_3.full_clean(exclude=fields_not_validated, validate_unique=False)
|
||||
dict_create_2.setdefault(pk_3, model_3)
|
||||
|
||||
model_2.full_clean(exclude=fields_not_validated, validate_unique=False)
|
||||
dict_create_2.setdefault(pk_2, model_2)
|
||||
|
||||
model.full_clean(exclude=fields_not_validated, validate_unique=False)
|
||||
dict_create.setdefault(pk, model)
|
||||
else:
|
||||
if not ModelType_2.objects.filter(Q(id=pk_2)).exists():
|
||||
model_2.full_clean(exclude=fields_not_validated_2, validate_unique=False)
|
||||
dict_create_2.setdefault(pk_2, model_2)
|
||||
|
||||
if not ModelType_2.objects.filter(Q(id=pk_3)).exists():
|
||||
model_3.full_clean(exclude=fields_not_validated, validate_unique=False)
|
||||
dict_create_2.setdefault(pk_3, model_3)
|
||||
model.full_clean(exclude=fields_not_validated, validate_unique=False)
|
||||
dict_update.setdefault(pk, model)
|
||||
|
||||
except Exception:
|
||||
counters.errors += 1
|
||||
logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, row['index'], pk)
|
||||
|
||||
df['index'] = df.index
|
||||
df.apply(process_row, axis=1)
|
||||
|
||||
if counters.errors:
|
||||
logger.warning("%s | en erreur : %s", ModelType.__name__, counters.errors)
|
||||
|
||||
if counters.ignored:
|
||||
logger.warning('%s | ignoré(s) : %s', ModelType.__name__, counters.ignored)
|
||||
|
||||
if set_dom:
|
||||
logger.warning('%s %s(s) ignoré(s) : %s', len(set_dom), Domaine.__name__, set_dom)
|
||||
|
||||
if set_fil:
|
||||
logger.warning('%s %s(s) ignorée(s) : %s', len(set_fil), Filiere.__name__, set_fil)
|
||||
|
||||
if not not_updated_rows.empty and fields_to_update:
|
||||
logger.info('%s | déjà à jour : %s', ModelType.__name__, len(not_updated_rows))
|
||||
|
||||
batch_size = 50
|
||||
if dict_create:
|
||||
logger.info('%s | à créer : %s', ModelType.__name__, len(dict_create))
|
||||
for idx, data_batch in enumerate(batch_iterator(list(dict_create.values()), batch_size)):
|
||||
ModelType.objects.bulk_create(data_batch)
|
||||
logger.debug('créé(s) : %s (lot %s)', len(data_batch), idx + 1)
|
||||
logger.info('%s | créé(s) : %s', ModelType.__name__, len(dict_create))
|
||||
else :
|
||||
logger.info('%s | rien à créer', ModelType.__name__)
|
||||
|
||||
if dict_create_2:
|
||||
logger.info('%s | à créer dans la vue A+1 : (%s)', ModelType_2.__name__, len(dict_create_2))
|
||||
for idx, data_batch in enumerate(batch_iterator(list(dict_create_2.values()), batch_size)):
|
||||
ModelType_2.objects.bulk_create(data_batch)
|
||||
logger.debug('créé(s) : %s (lot %s)', len(data_batch), idx + 1)
|
||||
logger.info('%s | créé(s) : %s', ModelType_2.__name__, len(dict_create_2))
|
||||
|
||||
if dict_update and fields_to_update:
|
||||
logger.info('%s | à mettre à jour : %s', ModelType.__name__, len(dict_update))
|
||||
for idx, data_batch in enumerate(batch_iterator(list(dict_update.values()), batch_size)):
|
||||
ModelType.objects.bulk_update(data_batch, fields=fields_to_update)
|
||||
logger.debug('mis à jour : %s (lot %s)', len(data_batch), idx + 1)
|
||||
logger.info('%s | mis à jour : %s', ModelType.__name__, len(dict_update))
|
||||
else :
|
||||
logger.info('%s | rien à mettre à jour', ModelType.__name__)
|
||||
|
||||
|
||||
adm_cree = len(dict_create) + len(dict_create_2)
|
||||
adm_modifie = len(dict_update)
|
||||
return adm_cree, adm_modifie, len(not_updated_rows), counters.errors, counters.ignored, set_dom, set_fil
|
||||
|
||||
# pas de suppression ici, il est prévu de faire un upload de fichier spécifique
|
||||
|
||||
61
backend-django/backend/utils/insertion/commun.py
Normal file
61
backend-django/backend/utils/insertion/commun.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
import pandas as pd
|
||||
from django.db.models import Choices, Model
|
||||
|
||||
from ..alimentation_decorators import get_data_logger
|
||||
|
||||
logger = get_data_logger(__name__)
|
||||
|
||||
|
||||
def batch_iterator(iterable: Union[List, Tuple, pd.DataFrame], batch_size: int) -> Union[List, Tuple, pd.DataFrame]:
|
||||
"""
|
||||
(je pense que le nom _batch_generator porterait à confusion)
|
||||
Générateur qui morcelle un itérable en lots de taille donnée.
|
||||
|
||||
:param iterable: itérable (supported un data frame)
|
||||
:type iterable: Union[List,Tuple,pd.DataFrame]
|
||||
|
||||
:param batch_size: taille de lot
|
||||
:type batch_size: int
|
||||
|
||||
:return: lot (valide, utilisateur, nouveau)
|
||||
:rtype: Tuple[bool, Optional[UserType], bool]
|
||||
"""
|
||||
length = len(iterable)
|
||||
for idx in range(0, length, batch_size):
|
||||
if isinstance(iterable, pd.DataFrame):
|
||||
yield iterable.iloc[idx:min(idx + batch_size, length)]
|
||||
else:
|
||||
yield iterable[idx:min(idx + batch_size, length)]
|
||||
|
||||
|
||||
def is_same_value(val1: Any, val2: Any) -> bool:
|
||||
"""
|
||||
Indique si deux valeurs sont équivalentes. Le cas le plus courant est une instance de Choices comparée à sa valeur.
|
||||
"""
|
||||
_1 = val1 if isinstance(val1, Choices) else val1
|
||||
_2 = val2 if isinstance(val2, Choices) else val2
|
||||
return _1 == _2
|
||||
|
||||
|
||||
def is_same_model(fields: Union[List[str], Tuple[str]], m1: Model, m2: Model) -> bool:
|
||||
"""
|
||||
Indique si deux modèles contiennent les mêmes données. Ce test permet de limiter les MAJ.
|
||||
"""
|
||||
|
||||
def _get_value(m: Model, field: str):
|
||||
""" Récupère la valeur d'un champ """
|
||||
attr = getattr(m, field)
|
||||
return attr.value if isinstance(attr, Choices) else attr
|
||||
|
||||
return not all(is_same_value(getattr(m1, f), getattr(m2, f)) for f in fields)
|
||||
|
||||
|
||||
class InsertionCounters:
|
||||
"""
|
||||
conteneur de compteurs pour faciliter l'incrémentation dans des fonctions imbriquées
|
||||
"""
|
||||
|
||||
ignored = 0
|
||||
errors = 0
|
||||
126
backend-django/backend/utils/logging.py
Normal file
126
backend-django/backend/utils/logging.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import inspect
|
||||
import sys
|
||||
import types
|
||||
from logging import Filter, Logger, LoggerAdapter, getLogger
|
||||
from typing import Any, List, Optional, Set, Tuple, Union
|
||||
|
||||
from .functions import find_class
|
||||
|
||||
# nom du module de fonction built-in
|
||||
BUILTIN_MODULE = 'builtins'
|
||||
|
||||
# tag pour l'alimentation
|
||||
TAG_DATA_FEED = 'alimentation'
|
||||
|
||||
# tag pour la performance
|
||||
TAG_PERF = 'performance'
|
||||
|
||||
# clé du dictionnaire 'kwargs' (standard pour 'logging')
|
||||
KEY_EXTRA = 'extra'
|
||||
|
||||
# clé du dictionnaire 'extra' (le type attendu est une liste ou un tuple)
|
||||
KEY_EXTRA_TAGS = 'tags'
|
||||
|
||||
# cache des adaptateurs
|
||||
cache_adapters = {}
|
||||
|
||||
|
||||
def __get_full_name(module_name: Optional[str] = None, cls_name: Optional[str] = None) -> Optional[str]:
|
||||
""" renvoie le nom complet à partir du nom de module et du nom de classe """
|
||||
|
||||
if module_name or cls_name:
|
||||
return f'{module_name}.{cls_name}' if module_name and cls_name else cls_name or module_name
|
||||
return None
|
||||
|
||||
|
||||
def get_logger_name(value: Any = None) -> Optional[str]:
|
||||
"""
|
||||
Renvoie un nom de logger pour la valeur donnée :
|
||||
- None ou chaîne vide : None
|
||||
- chaîne non vide : la valeur initiale
|
||||
- un objet sans module ou de module BUILTIN_MODULE : None (utiliser une chaîne pour forcer le nom)
|
||||
- classe, instance de classe, fonction ou méthode : <module>.<nom qualifié de la classe> si possible, sinon <module>
|
||||
|
||||
:param valeur: valeur initiale
|
||||
:type valeur: Any, optional
|
||||
|
||||
:return: nom de logger
|
||||
:rtype: str
|
||||
"""
|
||||
if value is None or isinstance(value, str):
|
||||
return value or None
|
||||
|
||||
module = inspect.getmodule(value)
|
||||
module_name = module.__name__ if module else None
|
||||
if not module_name or module_name == BUILTIN_MODULE:
|
||||
return None
|
||||
|
||||
if inspect.isclass(value):
|
||||
return __get_full_name(module_name, value.__qualname__)
|
||||
|
||||
if isinstance(value, (
|
||||
types.BuiltinFunctionType,
|
||||
types.BuiltinMethodType,
|
||||
types.ClassMethodDescriptorType,
|
||||
types.FunctionType,
|
||||
types.MethodDescriptorType,
|
||||
types.MethodType,
|
||||
)):
|
||||
cls = find_class(value)
|
||||
return __get_full_name(module_name, cls.__qualname__ if cls else None)
|
||||
|
||||
return __get_full_name(module_name, value.__class__.__qualname__)
|
||||
|
||||
|
||||
def get_logger(value: Any = None, tags: Union[str, List[str], Set[str], Tuple[str]] = None) -> Union[Logger, LoggerAdapter]:
|
||||
"""
|
||||
Renvoie un logger standard avec un nom obtenu par 'get_logger_name'
|
||||
|
||||
:param valeur: valeur initiale
|
||||
:type valeur: Any, optional
|
||||
|
||||
:param tags: tag(s) à ajouter dans le contexte de log
|
||||
:type tags: Union[str, List[str], Set[str], Tuple[str]], optional
|
||||
|
||||
:raises TypeError: type inattendu
|
||||
|
||||
:return: logger
|
||||
:rtype: class:`logging.Logger` ou class:`logging.LoggerAdapter`
|
||||
"""
|
||||
logger_name = get_logger_name(value)
|
||||
if not tags:
|
||||
return getLogger(logger_name)
|
||||
|
||||
adapters = cache_adapters.setdefault(tags if isinstance(tags, str) else frozenset(tags), {})
|
||||
return adapters.get(logger_name) or adapters.setdefault(logger_name, TagLoggerAdapter(getLogger(logger_name), {KEY_EXTRA_TAGS: tags}))
|
||||
|
||||
|
||||
class TagLoggerAdapter(LoggerAdapter):
|
||||
"""
|
||||
Adaptateur qui ajout si nécessaire des tags.
|
||||
"""
|
||||
|
||||
def __init__(self, logger, extra):
|
||||
super().__init__(logger, extra)
|
||||
if not isinstance(self.extra, dict):
|
||||
self.extra = {}
|
||||
tags = self.extra.setdefault(KEY_EXTRA_TAGS, frozenset())
|
||||
if not isinstance(tags, frozenset):
|
||||
self.extra.update({KEY_EXTRA_TAGS: frozenset(tags) if isinstance(tags, (list, set, tuple)) else frozenset([tags])})
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
""" ajoute les tags au contexte si nécessaire """
|
||||
tags = self.extra.get(KEY_EXTRA_TAGS)
|
||||
if tags:
|
||||
kw_extra = kwargs.setdefault(KEY_EXTRA, {})
|
||||
kw_tags = kw_extra.setdefault(KEY_EXTRA_TAGS, set())
|
||||
if not isinstance(kw_tags, (list, tuple, set)):
|
||||
kw_tags = set(kw_tags)
|
||||
kw_extra.update({KEY_EXTRA_TAGS: kw_tags})
|
||||
for tag in tags:
|
||||
if isinstance(kw_tags, set):
|
||||
kw_tags.add(tag)
|
||||
elif tag not in kw_tags:
|
||||
kw_tags = set(*kw_tags, tag)
|
||||
kw_extra.update({KEY_EXTRA_TAGS: kw_tags})
|
||||
return msg, kwargs
|
||||
404
backend-django/backend/utils/permissions.py
Normal file
404
backend-django/backend/utils/permissions.py
Normal file
@@ -0,0 +1,404 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from django.db.models import Exists, OuterRef, Q, QuerySet
|
||||
|
||||
|
||||
from ..models import (Administre, CustomUser, Decision, FormationEmploi, Poste,
|
||||
RefOrg, RefSvFil, Administres_Pams, Postes_Pams)
|
||||
from ..utils.attributes import safe_rgetattr
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# clé d'un élément de "get_profiles_by_adm" : pour la lecture
|
||||
KEY_READ = 'read'
|
||||
|
||||
# clé d'un élément de "get_profiles_by_adm" : pour l'écriture
|
||||
KEY_WRITE = 'write'
|
||||
|
||||
|
||||
class Profiles(str, Enum):
|
||||
""" profils applicatifs """
|
||||
|
||||
# super utilisateur appelé administrateur
|
||||
SUPER = 'SUPER'
|
||||
|
||||
# gestionnaire de filière appelé gestionnaire BGCAT
|
||||
FILIERE = 'FILIERE'
|
||||
|
||||
# gestionnaire BVT
|
||||
BVT = 'BVT'
|
||||
|
||||
# gestionnaire inter-domaine
|
||||
ITD = 'ITD'
|
||||
|
||||
# gestionnaire PCP (sans détail actuel/futur) appelé pameur BMOB
|
||||
PCP = 'PCP'
|
||||
|
||||
# gestionnaire PCP actuel (ou gagnant), disponible quand l'administré change de FE
|
||||
PCP_ACTUEL = 'PCP_ACTUEL'
|
||||
|
||||
# gestionnaire PCP futur (ou perdant), disponible quand l'administré change de FE
|
||||
PCP_FUTUR = 'PCP_FUTUR'
|
||||
|
||||
# expert HME
|
||||
HME = 'HME'
|
||||
|
||||
def __repr__(self):
|
||||
return "%s.%s" % (self.__class__.__name__, self._name_)
|
||||
|
||||
|
||||
class ProfileSummary:
|
||||
"""
|
||||
résumé des profils de l'utilisateur
|
||||
il s'agit d'une vision simplifiée, sans le détail des sous-viviers ou des groupes FE
|
||||
"""
|
||||
|
||||
"""
|
||||
Méthode d'initialisation
|
||||
|
||||
:param org_code: code du référentiel organique lié à l'utilisateur, défaut : None
|
||||
:type org_code: str, optional
|
||||
|
||||
:param profiles: profils de l'utilisateur, dictionnaire {<KEY_READ>: <profils>, <KEY_WRITE>: <profils>} }
|
||||
:type profiles: Dict[str, Tuple[Profiles]], optional
|
||||
"""
|
||||
def __init__(self, org_code: str = None, profiles: Dict[str, Tuple[Profiles]] = {}):
|
||||
self.org_code = org_code
|
||||
self.profiles = profiles
|
||||
|
||||
|
||||
def is_truthy(val: Any) -> bool:
|
||||
"""indique si une valeur des référentiels est considérée comme vraie"""
|
||||
return val and val != 'nan'
|
||||
|
||||
def get_queryset_org_by_user(user: CustomUser) -> QuerySet:
|
||||
"""
|
||||
Crée un QuerySet pour récupérer le référentiel organique lié à l'utilisateur.
|
||||
|
||||
:param user: utilisateur
|
||||
:type user: class:`CustomUser`
|
||||
|
||||
:return: QuerySet pour récupérer le référentiel
|
||||
:rtype: class:`QuerySet`
|
||||
"""
|
||||
return RefOrg.objects.filter(ref_gest__isnull=False, ref_gest__pk=user.id)
|
||||
|
||||
def get_queryset_org_by_any_code(org_code: str) -> QuerySet:
|
||||
"""
|
||||
Crée un QuerySet pour récupérer les référentiel organiques liés au code donné.
|
||||
|
||||
:param org_code: code de référentiel (de niveau 1 à 4)
|
||||
:type org_code: str
|
||||
|
||||
:return: QuerySet pour récupérer les référentiels
|
||||
:rtype: class:`QuerySet`
|
||||
"""
|
||||
return RefOrg.objects.filter(Q(ref_org_code_niv_org1=org_code)
|
||||
| Q(ref_org_code_niv_org2=org_code)
|
||||
| Q(ref_org_code_niv_org3=org_code)
|
||||
| Q(ref_org_code_niv_org4=org_code))
|
||||
|
||||
def get_lvl4_org_codes_by_any_code(org_code: str) -> Tuple[str]:
|
||||
"""
|
||||
Retourne les codes de niveau 4 des référentiels organiques liés au code donné.
|
||||
|
||||
:param org_code: code de référentiel (de niveau 1 à 4)
|
||||
:type org_code: str
|
||||
|
||||
:return: codes de niveau 4
|
||||
:rtype: Tuple[str]
|
||||
"""
|
||||
return tuple(code for code in get_queryset_org_by_any_code(org_code).values_list('ref_org_code_niv_org4', flat=True).distinct() if is_truthy(code))
|
||||
|
||||
def get_adm_filter_by_lvl4_codes_fil(org_codes: Union[List[str], Tuple[str]]) -> Q:
|
||||
"""
|
||||
Crée un filtre pour récupérer les administrés liés à un gestionnaire de filière à partir de codes de référentiels de niveau 4.
|
||||
|
||||
:param org_codes: codes de référentiels de niveau 4
|
||||
:type org_code: str
|
||||
|
||||
:return: filtre pour récupérer les administrés
|
||||
:rtype: class:`Q`
|
||||
"""
|
||||
Cols = Administre.Cols
|
||||
Cols_Pams = Administres_Pams.Cols
|
||||
return Q(Exists(
|
||||
RefSvFil.objects
|
||||
.filter(ref_sv_fil_code__in=org_codes)
|
||||
.filter(ref_sv_fil_dom=OuterRef(f'{Cols_Pams.REL_ADMINISTRE}__{Cols.REL_DOMAINE}'),
|
||||
ref_sv_fil_fil=OuterRef(f'{Cols_Pams.REL_ADMINISTRE}__{Cols.REL_FILIERE}'),
|
||||
ref_sv_fil_cat=OuterRef(f'{Cols_Pams.REL_ADMINISTRE}__{Cols.CATEGORIE}'))
|
||||
))
|
||||
|
||||
def get_adm_filter_by_lvl4_codes_pcp(org_codes: Union[List[str], Tuple[str]]) -> Q:
|
||||
"""
|
||||
Crée un filtre pour récupérer les administrés liés à un gestionnaire PCP à partir de codes de référentiels de niveau 4.
|
||||
|
||||
:param org_codes: codes de référentiels de niveau 4
|
||||
:type org_code: str
|
||||
|
||||
:return: filtre pour récupérer les administrés
|
||||
:rtype: class:`Q`
|
||||
"""
|
||||
Cols = Administre.Cols
|
||||
Cols_Pams = Administres_Pams.Cols
|
||||
cat_filter = Q(**{f'{Cols_Pams.REL_ADMINISTRE}__{Cols.CATEGORIE}': 'MDR'})
|
||||
sub_qs = FormationEmploi.objects.filter(fe_code=OuterRef(f'{Cols_Pams.REL_ADMINISTRE}__{Cols.REL_FORMATION_EMPLOI}'))
|
||||
return (
|
||||
(~cat_filter & Exists(sub_qs.filter(fe_code_niv_org4__in=org_codes)))
|
||||
| (cat_filter & Exists(sub_qs.filter(fe_code_niv_org4_mdr__in=org_codes)))
|
||||
)
|
||||
|
||||
def get_adm_filter_by_lvl4_codes_future_pcp(org_codes: Union[List[str], Tuple[str]]) -> Q:
|
||||
"""
|
||||
Crée un filtre pour récupérer les administrés liés à un futur gestionnaire PCP à partir de codes de référentiels de niveau 4.
|
||||
|
||||
:param org_codes: codes de référentiels de niveau 4
|
||||
:type org_code: str
|
||||
|
||||
:return: filtre pour récupérer les administrés
|
||||
:rtype: class:`Q`
|
||||
"""
|
||||
Cols = Administres_Pams.Cols
|
||||
cat_filter = Q(**{f'{Cols.REL_ADMINISTRE}__{Administre.Cols.CATEGORIE}': 'MDR'})
|
||||
fe_code_path = f'{Cols.REL_DECISION}__{Decision.Cols.REL_POSTE}__{Poste.Cols.REL_FORMATION_EMPLOI}'
|
||||
sub_qs = FormationEmploi.objects.filter(fe_code=OuterRef(fe_code_path))
|
||||
return (Q(**{f'{fe_code_path}__isnull': False}) & (
|
||||
(~cat_filter & Exists(sub_qs.filter(fe_code_niv_org4__in=org_codes)))
|
||||
| (cat_filter & Exists(sub_qs.filter(fe_code_niv_org4_mdr__in=org_codes)))
|
||||
))
|
||||
|
||||
def get_poste_filter_by_lvl4_codes_fil(org_codes: Union[List[str], Tuple[str]]) -> Q:
|
||||
"""
|
||||
Crée un filtre pour récupérer les postes liés à un gestionnaire fil à partir de codes de référentiels de niveau 4.
|
||||
|
||||
:param org_codes: codes de référentiels de niveau 4
|
||||
:type org_code: str
|
||||
|
||||
:return: filtre pour récupérer les postes
|
||||
:rtype: class:`Q`
|
||||
"""
|
||||
Cols = Poste.Cols
|
||||
Cols_Pam = Postes_Pams.Cols
|
||||
return Q(Exists(
|
||||
RefSvFil.objects
|
||||
.filter(ref_sv_fil_code__in=org_codes)
|
||||
.filter(sous_vivier_id=OuterRef(f'{Cols_Pam.REL_POSTE}__{Cols.M2M_SOUS_VIVIERS}'))
|
||||
))
|
||||
|
||||
def get_poste_filter_by_lvl4_codes_pcp(org_codes: Union[List[str], Tuple[str]]) -> Q:
|
||||
"""
|
||||
Crée un filtre pour récupérer les postes liés à un gestionnaire PCP à partir de codes de référentiels de niveau 4.
|
||||
|
||||
:param org_codes: codes de référentiels de niveau 4
|
||||
:type org_code: str
|
||||
|
||||
:return: filtre pour récupérer les postes
|
||||
:rtype: class:`Q`
|
||||
"""
|
||||
Cols = Poste.Cols
|
||||
Cols_Pam = Postes_Pams.Cols
|
||||
cat_filter = Q(**{f'{Cols_Pam.REL_POSTE}__{Cols.CATEGORIE}': 'MDR'})
|
||||
sub_qs = FormationEmploi.objects.filter(fe_code=OuterRef(f'{Cols_Pam.REL_POSTE}__{Cols.REL_FORMATION_EMPLOI}'))
|
||||
return (
|
||||
(~cat_filter & Exists(sub_qs.filter(fe_code_niv_org4__in=org_codes)))
|
||||
| (cat_filter & Exists(sub_qs.filter(fe_code_niv_org4_mdr__in=org_codes)))
|
||||
)
|
||||
|
||||
def get_profile_summary(user: Optional[CustomUser]) -> ProfileSummary:
|
||||
"""
|
||||
Renvoie le résumé des profils de l'utilisateur.
|
||||
|
||||
:param user: utilisateur
|
||||
:type user: class:`CustomUser`
|
||||
|
||||
:return: résumé des profils
|
||||
:rtype: class:`ProfileSummary`
|
||||
"""
|
||||
if not user or not user.is_authenticated:
|
||||
return ProfileSummary()
|
||||
|
||||
org = (get_queryset_org_by_user(user)
|
||||
.only('pk', 'ref_org_droit_lect', 'ref_org_droit_ecr', 'ref_org_ref_fe', 'ref_org_ref_sv_fil', 'ref_org_expert_hme', 'ref_org_bvt', 'ref_org_itd')
|
||||
.first())
|
||||
|
||||
is_super = user.is_superuser
|
||||
is_read = org and is_truthy(org.ref_org_droit_lect)
|
||||
is_write = org and is_truthy(org.ref_org_droit_ecr)
|
||||
is_fil = org and is_truthy(org.ref_org_ref_sv_fil)
|
||||
is_hme = org and is_truthy(org.ref_org_expert_hme)
|
||||
is_bvt = org and is_truthy(org.ref_org_bvt)
|
||||
is_itd = org and is_truthy(org.ref_org_itd)
|
||||
is_pcp = org and is_truthy(org.ref_org_ref_fe)
|
||||
# logger.debug('user %s => superuser: %s, READ: %s, WRITE: %s, FIL: %s, PCP: %s, BVT: %s, ITD: %s, HME: %s', user.pk, is_super, is_read, is_write, is_fil, is_pcp, is_bvt, is_itd, is_hme)
|
||||
|
||||
profiles = []
|
||||
if is_super:
|
||||
profiles.append(Profiles.SUPER)
|
||||
if is_fil:
|
||||
profiles.append(Profiles.FILIERE)
|
||||
if is_pcp:
|
||||
profiles.append(Profiles.PCP)
|
||||
if is_bvt:
|
||||
profiles.append(Profiles.BVT)
|
||||
if is_itd:
|
||||
profiles.append(Profiles.ITD)
|
||||
if is_hme:
|
||||
profiles.append(Profiles.HME)
|
||||
|
||||
|
||||
return ProfileSummary(org_code=org.pk if org else None, profiles={
|
||||
KEY_READ: tuple(profiles) if is_read else (Profiles.SUPER,) if is_super else (),
|
||||
KEY_WRITE: tuple(profiles) if is_write else (Profiles.SUPER,) if is_super else(),
|
||||
})
|
||||
|
||||
def _group_adm_by_profile(org_code: Optional[str], administres: Tuple[Administres_Pams], profiles: Tuple[Profiles]) -> Dict[Profiles, Tuple[int]]:
|
||||
"""
|
||||
Groupe les administrés liés à certains profils, uniquement parmi les administrés en paramètre.
|
||||
Il ne s'agit pas d'un partitionnement car un même administré peut être lié à plusieurs profils.
|
||||
|
||||
:param org_code: code de référentiel (de niveau 1 à 4)
|
||||
:type org_code: str
|
||||
|
||||
:param administres: administrés à grouper
|
||||
:type administres: Tuple[Administres_Pams]
|
||||
|
||||
:param profiles: profils utilisés
|
||||
:type profiles: Tuple[Profiles]
|
||||
|
||||
:return: dictionnaire {<profil>: <ID d'administrés>}
|
||||
:rtype: Dict[Profiles, Tuple[int]]
|
||||
"""
|
||||
A = Administre.Cols
|
||||
AP = Administres_Pams.Cols
|
||||
D = Decision.Cols
|
||||
P = Profiles
|
||||
|
||||
adm_ids_fil = ()
|
||||
adm_ids_bvt = ()
|
||||
adm_ids_pcp = []
|
||||
adm_ids_current_pcp = []
|
||||
adm_ids_future_pcp = ()
|
||||
|
||||
if org_code:
|
||||
is_fil = P.FILIERE in profiles
|
||||
is_bvt = P.BVT in profiles
|
||||
is_pcp = P.PCP in profiles
|
||||
if is_fil or is_bvt or is_pcp:
|
||||
adm_by_id = {adm.pk: adm for adm in administres}
|
||||
qs = Administres_Pams.objects
|
||||
if is_bvt:
|
||||
adm_ids_bvt = tuple(
|
||||
qs.filter(pk__in=adm_by_id.keys())
|
||||
.filter(Q(**{f'{AP.REL_ADMINISTRE}__{A.REL_SOUS_VIVIER}': 'BVT'})).values_list('pk', flat=True))
|
||||
if is_fil or is_pcp:
|
||||
codes_lvl4 = get_lvl4_org_codes_by_any_code(org_code)
|
||||
if codes_lvl4:
|
||||
if is_fil:
|
||||
adm_ids_fil = tuple(
|
||||
qs.filter(pk__in=adm_by_id.keys())
|
||||
.filter(get_adm_filter_by_lvl4_codes_fil(codes_lvl4)).values_list('pk', flat=True))
|
||||
if is_pcp:
|
||||
raw_adm_ids_current_pcp = set(
|
||||
qs.filter(pk__in=adm_by_id.keys())
|
||||
.filter(get_adm_filter_by_lvl4_codes_pcp(codes_lvl4)).values_list('pk', flat=True))
|
||||
for adm_id in raw_adm_ids_current_pcp:
|
||||
adm = adm_by_id.get(adm_id)
|
||||
fe_avant = safe_rgetattr(adm, f'{AP.REL_ADMINISTRE}.{A.REL_FORMATION_EMPLOI}_id')
|
||||
fe_apres = safe_rgetattr(adm, f'{AP.REL_DECISION}.{D.REL_POSTE}.{Poste.Cols.REL_FORMATION_EMPLOI}_id')
|
||||
if fe_apres is None or fe_avant == fe_apres:
|
||||
# pas de décision ou même FE : juste PCP
|
||||
adm_ids_pcp.append(adm_id)
|
||||
else:
|
||||
# sinon PCP_ACTUEL
|
||||
adm_ids_current_pcp.append(adm_id)
|
||||
adm_ids_future_pcp = tuple(
|
||||
qs.filter(pk__in=[adm_id for adm_id in adm_by_id.keys() if adm_id not in adm_ids_pcp])
|
||||
.filter(get_adm_filter_by_lvl4_codes_future_pcp(codes_lvl4)).values_list('pk', flat=True))
|
||||
return {
|
||||
P.FILIERE: adm_ids_fil,
|
||||
P.BVT: adm_ids_bvt,
|
||||
P.PCP: tuple(adm_ids_pcp),
|
||||
P.PCP_ACTUEL: tuple(adm_ids_current_pcp),
|
||||
P.PCP_FUTUR: tuple(adm_ids_future_pcp)
|
||||
}
|
||||
|
||||
def _get_profiles_for_adm(adm_id: int, global_profiles: Tuple[Profiles], adm_ids_by_profile: Dict[Profiles, Tuple[int]]) -> Tuple[Profiles]:
|
||||
"""
|
||||
Renvoie les profils qui concernent l'administré. Ils sont différents des profils globaux car un utilisateur qui a le profil PCP
|
||||
n'est pas forcément PCP pour tous les administrés, il peut être PCP_ACTUEL et/ou PCP_FUTUR en fonction de l'administré.
|
||||
note : Profiles.SUPER est ignoré car il n'a pas de sens dans ce contexte
|
||||
|
||||
:param adm_id: ID de l'administré
|
||||
:type adm_id: int
|
||||
|
||||
:param global_profiles: profils globaux, ceux qu'on trouve dans un class:`ProfileSummary`.
|
||||
:type global_profiles: Tuple[Administre]
|
||||
|
||||
:param adm_ids_by_profile: IDs d'administrés groupés par profil, dictionnaire {<profil>: <ID d'administrés>}
|
||||
:type adm_ids_by_profile: Dict[Profiles, Tuple[int]]
|
||||
|
||||
:return: profils qui concernent l'administré
|
||||
:rtype: Tuple[Profiles]
|
||||
"""
|
||||
P = Profiles
|
||||
profiles = set()
|
||||
if P.HME in global_profiles:
|
||||
profiles.add(P.HME)
|
||||
if P.FILIERE in global_profiles and adm_id in adm_ids_by_profile.get(P.FILIERE):
|
||||
profiles.add(P.FILIERE)
|
||||
if P.BVT in global_profiles and adm_id in adm_ids_by_profile.get(P.BVT):
|
||||
profiles.add(P.BVT)
|
||||
if P.PCP in global_profiles:
|
||||
if adm_id in adm_ids_by_profile.get(P.PCP):
|
||||
profiles.add(P.PCP)
|
||||
if adm_id in adm_ids_by_profile.get(P.PCP_ACTUEL):
|
||||
profiles.add(P.PCP_ACTUEL)
|
||||
if adm_id in adm_ids_by_profile.get(P.PCP_FUTUR):
|
||||
profiles.add(P.PCP_FUTUR)
|
||||
return tuple(profiles)
|
||||
|
||||
def get_profiles_by_adm(user: Optional[CustomUser], administre: Administres_Pams, *args: Administres_Pams) -> Dict[int, Dict[str, Tuple[Profiles]]]:
|
||||
"""
|
||||
Renvoie un dictionnaire dont les clés sont les ID SAP des administrés donnés et les valeurs sont les profils de l'utilisateur.
|
||||
|
||||
:param user: utilisateur
|
||||
:type user: class:`CustomUser`
|
||||
|
||||
:param administre: administré
|
||||
:type administre: class:`Administres_Pams`
|
||||
|
||||
:param args: administrés supplémentaires
|
||||
:type args: class:`Administres_Pams` (multiple)
|
||||
|
||||
:return: dictionnaire de dictionnaires {<ID SAP>: {<KEY_READ>: <profils>, <KEY_WRITE>: <profils>} }
|
||||
:rtype: Dict[int, Dict[str, Tuple[Profiles]]]
|
||||
"""
|
||||
values = (administre, *args)
|
||||
if not values:
|
||||
return {}
|
||||
|
||||
summary = get_profile_summary(user)
|
||||
r_profiles = summary.profiles.get(KEY_READ, ())
|
||||
w_profiles = summary.profiles.get(KEY_WRITE, ())
|
||||
|
||||
if not r_profiles and not w_profiles:
|
||||
return {adm.administre.pk: {KEY_READ: (), KEY_WRITE: ()} for adm in values}
|
||||
|
||||
same_profiles = sorted(set(r_profiles)) == sorted(set(w_profiles))
|
||||
org_code = summary.org_code
|
||||
r_adm_ids_by_profile = _group_adm_by_profile(org_code, values, r_profiles)
|
||||
w_adm_ids_by_profile = _group_adm_by_profile(org_code, values, w_profiles) if not same_profiles else r_adm_ids_by_profile
|
||||
|
||||
result = {}
|
||||
for adm in values:
|
||||
adm_id = adm.pk
|
||||
r_profiles_adm = _get_profiles_for_adm(adm_id, r_profiles, r_adm_ids_by_profile)
|
||||
w_profiles_adm = _get_profiles_for_adm(adm_id, w_profiles, w_adm_ids_by_profile) if not same_profiles else r_profiles_adm
|
||||
result.setdefault(adm_id, {
|
||||
KEY_READ: r_profiles_adm,
|
||||
KEY_WRITE: w_profiles_adm
|
||||
})
|
||||
return result
|
||||
96
backend-django/backend/utils/predicates.py
Normal file
96
backend-django/backend/utils/predicates.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# from typing import Callable
|
||||
from .functions import find_class
|
||||
from typing import Tuple, Callable # Any, Optional, Union,
|
||||
|
||||
|
||||
def func_class_name_is_in(cls_name: str, *args: str) -> Callable[[Callable], bool]:
|
||||
"""
|
||||
Crée un prédicat qui renvoie True si le nom de la classe de la fonction/méthode existe et fait partie des valeurs données.
|
||||
|
||||
:param cls_name: nom de classe
|
||||
:type cls_name: str
|
||||
|
||||
:param args: noms supplémentaires
|
||||
:type args: str (multiple)
|
||||
|
||||
:return: filtre
|
||||
:rtype: Callable[[Callable], bool]
|
||||
"""
|
||||
values = (cls_name, *args)
|
||||
|
||||
def func_class_name_is_in(func: Callable) -> bool:
|
||||
cls = find_class(func)
|
||||
return bool(cls) and cls.__name__ in values
|
||||
return func_class_name_is_in
|
||||
|
||||
|
||||
def func_class_name_is_not_in(cls_name: str, *args: str) -> Callable[[Callable], bool]:
|
||||
"""
|
||||
Crée un prédicat qui renvoie True si le nom de la classe de la fonction/méthode existe et ne fait pas partie des valeurs données.
|
||||
|
||||
:param cls_name: nom de classe
|
||||
:type cls_name: str
|
||||
|
||||
:param args: noms supplémentaires
|
||||
:type args: str (multiple)
|
||||
|
||||
:return: filtre
|
||||
:rtype: Callable[[Callable], bool]
|
||||
"""
|
||||
values = (cls_name, *args)
|
||||
|
||||
def func_class_name_is_not_in(func: Callable) -> bool:
|
||||
cls = find_class(func)
|
||||
return bool(cls) and cls.__name__ not in values
|
||||
return func_class_name_is_not_in
|
||||
|
||||
|
||||
def func_name_is_in(name: str, *args: str) -> Callable[[Callable], bool]:
|
||||
"""
|
||||
Crée un prédicat qui renvoie True si le nom de la fonction/méthode fait partie des valeurs données.
|
||||
|
||||
:param name: nom de fonction/méthode
|
||||
:type name: str
|
||||
|
||||
:param args: noms supplémentaires
|
||||
:type args: str (multiple)
|
||||
|
||||
:return: filtre
|
||||
:rtype: Callable[[Callable], bool]
|
||||
"""
|
||||
values = (name, *args)
|
||||
|
||||
def func_name_is_in(func: Callable) -> bool:
|
||||
return func.__name__ in values
|
||||
return func_name_is_in
|
||||
|
||||
|
||||
def func_name_is_not_in(name: str, *args: str) -> Callable[[Callable], bool]:
|
||||
"""
|
||||
Crée un prédicat qui renvoie True si le nom de la fonction/méthode ne fait pas partie des valeurs données.
|
||||
|
||||
:param name: nom de fonction/méthode
|
||||
:type name: str
|
||||
|
||||
:param args: noms supplémentaires
|
||||
:type args: str (multiple)
|
||||
|
||||
:return: filtre
|
||||
:rtype: Callable[[Callable], bool]
|
||||
"""
|
||||
values = (name, *args)
|
||||
|
||||
def func_name_is_not_in(func: Callable) -> bool:
|
||||
return func.__name__ not in values
|
||||
return func_name_is_not_in
|
||||
|
||||
|
||||
def func_is_public(func: Callable) -> bool:
|
||||
"""
|
||||
Prédicat qui renvoie True si la fonction/méthode est "publique".
|
||||
(les notions de public, protected, private ne sont que des conventions de nommage en Python)
|
||||
|
||||
:return: filtre
|
||||
:rtype: Callable[[Callable], bool]
|
||||
"""
|
||||
return not func.__name__.startswith('_')
|
||||
14
backend-django/backend/utils/view_predicates.py
Normal file
14
backend-django/backend/utils/view_predicates.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .predicates import func_name_is_in
|
||||
|
||||
|
||||
# fonctions exposées par les vues Django
|
||||
VIEW_FUNCTIONS = ['get', 'post', 'put', 'patch', 'delete']
|
||||
|
||||
# fonctions exposées par les viewsets Django
|
||||
VIEWSET_FUNCTIONS = [*VIEW_FUNCTIONS, 'create', 'list', 'retrieve', 'update', 'partial_update', 'destroy']
|
||||
|
||||
# prédicat qui renvoie True si le nom de la fonction/méthode fait partie de VIEW_FUNCTIONS.
|
||||
view_functions = func_name_is_in(*VIEW_FUNCTIONS)
|
||||
|
||||
# prédicat qui renvoie True si le nom de la fonction/méthode fait partie de VIEWSET_FUNCTIONS.
|
||||
viewset_functions = func_name_is_in(*VIEWSET_FUNCTIONS)
|
||||
Reference in New Issue
Block a user