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

View File

@@ -0,0 +1,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 *

View 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

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

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

View 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

View 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

View 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

View File

@@ -0,0 +1 @@
from .administre import *

View 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

View 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

View 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

View File

@@ -0,0 +1,2 @@
from .administre import *
from .commun import *

View 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

View 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

View 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

View 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

View 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('_')

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