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

View File

@@ -0,0 +1,380 @@
"""
La classe Admin est la représentation dun modèle dans linterface dadministration
"""
import numpy as np
from django.contrib import admin, messages
from django.contrib.admin import helpers
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.db.models import (ManyToManyField, ManyToManyRel, ManyToOneRel,
OneToOneRel)
from django.http import HttpResponseRedirect
from django.shortcuts import render
from backend.form import GroupeFeForm, PcpFeGroupeForm, TauxArmementForm
from .models import (FMOB, Administre, Calcul, Competence, CustomUser, Decision,
Filiere, FormationEmploi, GroupeFe, Marque, MarquesGroupe,
Notation, PcpFeGroupe, Poste, PreferencesListe,
SousVivier, SousVivierAssociation, ZoneGeographique,
RefGest, RefOrg, RefSvFil, FichiersExporte)
def is_not_many_to(field):
""" Vérifie qu'il ne s'agit ni d'un champ many-to-one ni d'un champ many-to-many """
return not isinstance(field, ManyToManyField) and not isinstance(field, ManyToManyRel) and (not isinstance(field, ManyToOneRel) or isinstance(field, OneToOneRel))
def default_list_display(modelType):
""" Retourne une liste de champs pour l'attribut list_display """
pk = modelType._meta.pk.name
fields = [field.name for field in modelType._meta.get_fields() if field.name != pk and is_not_many_to(field)]
fields.insert(0, pk)
return fields
class AdministreAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des administrés dans la page d'admin du projet.
list_display va spécifier tous les champs des administrés qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Administre)
class CalculAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des administrés dans la page d'admin du projet.
list_display va spécifier tous les champs des administrés qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Calcul)
class DecisionAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des décisions dans la page d'admin du projet."""
list_display = default_list_display(Decision)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.list_display_links = None
def has_add_permission(self, request):
return False
class NotationAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section notation dans la page d'admin du projet.
list_display va spécifier tous les champs de notation qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Notation)
class PosteAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des postes dans la page d'admin du projet.
list_display va spécifier tous les champs des postes qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Poste)
class ZoneGeographiqueAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des postes dans la page d'admin du projet.
list_display va spécifier tous les champs des postes qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(ZoneGeographique)
class PreferencesListeAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de preferences liste dans la page d'admin du projet.
list_display va spécifier tous les champs de preferences liste qui doivent être affichés dans cette section dans la page admin
"""
list_display = default_list_display(PreferencesListe)
class FMOBAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de FMOB dans la page d'admin du projet.
list_display va spécifier tous les champs de FMOB qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(FMOB)
class MarqueAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de Marque dans la page d'admin du projet.
list_display va spécifier tous les champs de la talbe Marque qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Marque)
class FiliereAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de Filiere dans la page d'admin du projet.
list_display va spécifier les champs f_code et domaine de la talbe Filiere qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Filiere)
class SousVivierAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de sous vivier dans la page d'admin du projet.
list_display va spécifier les champs sv_id, gestionnaire et sv_libelle de la talbe sous vivier qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(SousVivier)
class RefGestAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de référentiels gestionnaires dans la page d'admin du projet.
list_display va spécifier les champs de la talbe référentiel gestionnaire qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(RefGest)
class RefOrgAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de référentiels organiques dans la page d'admin du projet.
list_display va spécifier les champs de la talbe referentiel organique qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(RefOrg)
class RefSvFilAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de référentiels sous-vivier filiere dans la page d'admin du projet.
list_display va spécifier les champs de la talbe referentiel sous-vivier filiere qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(RefSvFil)
class FichiersExporteAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des fichiers exportés dans la page d'admin du projet.
list_display va spécifier les champs id, nom_fichier et date_export de la talbe des fichiers exportés qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(FichiersExporte)
# class SousVivierAssociationAdmin(admin.ModelAdmin):
# """Cette classe est dédiée à la section de SousVivierAssociation dans la page d'admin du projet.
# list_display va spécifier tous les champs de la talbe SousVivierAssociation qui doivent être affichés dans cette section dans la page admin.
# """
# list_display = default_list_display(SousVivierAssociation)
class MarquesGroupeAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section de MarquesGroupe dans la page d'admin du projet.
list_display va spécifier tous les champs de la talbe MarquesGroupe qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(MarquesGroupe)
class CustomUserCreationForm(UserCreationForm):
"""Cette classe est dédiée à la section de creation d'un utilisateur dans la page d'admin du projet.
"""
class Meta(UserCreationForm.Meta):
model = CustomUser
fields = UserCreationForm.Meta.fields + ('id', 'grade')
class CustomUserChangeForm(UserChangeForm):
"""Cette classe est dédiée à la section de changement d'un utilisateur dans la page d'admin du projet.
"""
class Meta(UserChangeForm.Meta):
model = CustomUser
fields = UserCreationForm.Meta.fields + ('id', 'grade')
class CustomUserAdmin(BaseUserAdmin):
"""Cette classe est dédiée à la section des utilisateurs dans la page d'admin du projet.
list_display va spécifier tous les champs de la talbe CustomUser qui doivent être affichés dans cette section dans la page admin.
"""
model = CustomUser
form = CustomUserChangeForm
add_form = CustomUserCreationForm
list_display = BaseUserAdmin.list_display + ('id', 'grade')
fieldsets = BaseUserAdmin.fieldsets + (
('Gestionnaire', {
'fields': ('id', 'grade'),
}),
)
add_fieldsets = BaseUserAdmin.add_fieldsets + (
('Gestionnaire', {
'fields': ('id', 'grade'),
}),
)
# list_display = [field.name for field in CustomUser._meta.get_fields() if not isinstance(field, ManyToManyRel)
# and not field.name in ['logentry', 'sousvivier']]
class CompetenceAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section des competences dans la page d'admin du projet.
list_display va spécifier tous les champs de la talbe Competence qui doivent être affichés dans cette section dans la page admin.
"""
list_display = default_list_display(Competence)
class FormationEmploiAdmin(admin.ModelAdmin):
"""Cette classe est dédiée à la section Formation Emploi dans la page d'admin du projet.
list_display va spécifier tous les champs de la talbe FE qui doivent être affichés dans cette section dans la page admin.
"""
list_display = ['fe_code', 'fe_libelle', 'fe_garnison_lieu', 'fe_code_postal', 'groupe_fe', 'fe_taux_armement_cible_mdr',
'fe_taux_armement_cible_off', 'fe_taux_armement_cible_soff', 'get_mere', 'zone_defense']
list_filter = ('groupe_fe__groupe_fe_nom', 'fe_garnison_lieu')
actions = ['modification_taux_armement_mdr', 'modification_taux_armement_off', 'modification_taux_armement_soff', 'attribution_groupe']
search_fields = ['fe_code']
@admin.display(description='FE mère')
def get_mere(self, obj):
return obj.mere.pk if obj.mere else None
# Fonctions d'actions supplémentaires
@admin.action(description="Changer le taux d'armement pour la catégorie MDR")
def modification_taux_armement_mdr(self, request, queryset):
"""Fonction dédiée à la modification du taux d'armement mdr dans la section Formation Emploi de la page d'administration.
"""
if 'apply' in request.POST:
taux = int(request.POST['taux_armement_cible'])
for fe_id in queryset.values_list('fe_code'):
fe = FormationEmploi.objects.get(fe_code=fe_id[0])
fe.fe_taux_armement_cible_mdr = taux
new_nb_reevalue_mdr = np.ceil(0.01 * taux * fe.fe_nb_poste_reo_mdr)
new_nb_vacant_mdr = new_nb_reevalue_mdr - fe.fe_nb_poste_occupe_mdr
fe.fe_nb_poste_reevalue_mdr = new_nb_reevalue_mdr
fe.fe_nb_poste_vacant_mdr = new_nb_vacant_mdr
fe.save(update_fields=['fe_taux_armement_cible_mdr', 'fe_nb_poste_reevalue_mdr', 'fe_nb_poste_vacant_mdr'])
messages.success(request, '{0} FEs ont été mises à jour'.format(queryset.count()))
return HttpResponseRedirect(request.get_full_path())
form = TauxArmementForm(initial={'_selected_action': queryset.values_list('fe_code', flat=True)})
return render(request,
'admin/taux_cible_mdr.html',
{'form': form, 'formations_emplois': queryset, 'do_action': 'modification_taux_armement_mdr', 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME})
modification_taux_armement_mdr.short_description = "Modification du taux d'armement cible pour la categorie MDR"
@admin.action(description="Changer le taux d'armement pour la catégorie OFF")
def modification_taux_armement_off(self, request, queryset):
"""Fonction dédiée à la modification du taux d'armement off dans la section Formation Emploi de la page d'administration.
"""
if 'apply' in request.POST:
taux = int(request.POST['taux_armement_cible'])
for fe_id in queryset.values_list('fe_code'):
fe = FormationEmploi.objects.get(fe_code=fe_id[0])
fe.fe_taux_armement_cible_off = taux
new_nb_reevalue_off = np.ceil(0.01 * taux * fe.fe_nb_poste_reo_off)
new_nb_vacant_off = new_nb_reevalue_off - fe.fe_nb_poste_occupe_off
fe.fe_nb_poste_reevalue_off = new_nb_reevalue_off
fe.fe_nb_poste_vacant_off = new_nb_vacant_off
fe.save(update_fields=['fe_taux_armement_cible_off', 'fe_nb_poste_reevalue_off', 'fe_nb_poste_vacant_off'])
messages.success(request, '{0} FEs ont été mises à jour'.format(queryset.count()))
return HttpResponseRedirect(request.get_full_path())
form = TauxArmementForm(initial={'_selected_action': queryset.values_list('fe_code', flat=True)})
return render(request,
'admin/taux_cible_off.html',
{'form': form, 'formations_emplois': queryset, 'do_action': 'modification_taux_armement_off', 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME})
modification_taux_armement_off.short_description = "Modification du taux d'armement cible pour la categorie OFF"
@admin.action(description="Changer le taux d'armement pour la catégorie SOFF")
def modification_taux_armement_soff(self, request, queryset):
"""Fonction dédiée à la modification du taux d'armement soff dans la section Formation Emploi de la page d'administration.
"""
if 'apply' in request.POST:
taux = int(request.POST['taux_armement_cible'])
for fe_id in queryset.values_list('fe_code'):
fe = FormationEmploi.objects.get(fe_code=fe_id[0])
fe.fe_taux_armement_cible_soff = taux
new_nb_reevalue_soff = np.ceil(0.01 * taux * fe.fe_nb_poste_reo_soff)
new_nb_vacant_soff = new_nb_reevalue_soff - fe.fe_nb_poste_occupe_soff
fe.fe_nb_poste_reevalue_soff = new_nb_reevalue_soff
fe.fe_nb_poste_vacant_soff = new_nb_vacant_soff
fe.save(update_fields=['fe_taux_armement_cible_soff', 'fe_nb_poste_reevalue_soff', 'fe_nb_poste_vacant_soff'])
messages.success(request, '{0} FEs ont été mises à jour'.format(queryset.count()))
return HttpResponseRedirect(request.get_full_path())
form = TauxArmementForm(initial={'_selected_action': queryset.values_list('fe_code', flat=True)})
return render(request,
'admin/taux_cible_soff.html',
{'form': form, 'formations_emplois': queryset, 'do_action': 'modification_taux_armement_soff', 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME})
modification_taux_armement_soff.short_description = "Modification du taux d'armement cible pour la categorie SOFF"
@admin.action(description='Attribuer les FE à un groupe')
def attribution_groupe(modeladmin, request, queryset):
"""Fonction dédiée à l'attribution du group pour la formation emploi dans la section Formation Emploi de la page d'administration.
"""
if 'apply' in request.POST:
nom = request.POST['nom_groupe_fe']
try:
groupe_fe = GroupeFe.objects.get(groupe_fe_nom=nom)
except:
groupe_fe = GroupeFe(groupe_fe_nom=nom)
groupe_fe.save()
fe = FormationEmploi.objects.filter(fe_code__in=queryset.values_list('fe_code'))
fe.update(groupe_fe_id=groupe_fe.pk)
messages.success(request, '{0} FEs ont été mises à jour'.format(queryset.count()))
return HttpResponseRedirect(request.get_full_path())
form = GroupeFeForm(initial={'_selected_action': queryset.values_list('fe_code', flat=True)})
return render(request,
'admin/groupe_fe.html',
{'form': form, 'formations_emplois': queryset, 'do_action': 'attribution_groupe', 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME})
attribution_groupe.short_description = "Attribution des FE à un groupe"
# class PcpFeGroupeAdmin(admin.ModelAdmin):
# """Cette classe est dédiée à l'association des groupes de FE aux gestionnaires PCP dans la page d'admin du projet.
# """
# list_display = default_list_display(PcpFeGroupe)
# form = PcpFeGroupeForm
# Register your models here.
admin.site.register(Administre, AdministreAdmin)
admin.site.register(Calcul, CalculAdmin)
admin.site.register(Decision, DecisionAdmin)
admin.site.register(FichiersExporte, FichiersExporteAdmin)
admin.site.register(Poste, PosteAdmin)
admin.site.register(SousVivier, SousVivierAdmin)
admin.site.register(RefGest, RefGestAdmin)
admin.site.register(RefOrg, RefOrgAdmin)
admin.site.register(RefSvFil, RefSvFilAdmin)
admin.site.register(Notation, NotationAdmin)
admin.site.register(Filiere, FiliereAdmin)
admin.site.register(FormationEmploi, FormationEmploiAdmin)
# admin.site.register(SousVivierAssociation, SousVivierAssociationAdmin)
admin.site.register(FMOB, FMOBAdmin)
admin.site.register(Marque, MarqueAdmin)
admin.site.register(MarquesGroupe, MarquesGroupeAdmin)
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Competence, CompetenceAdmin)
# admin.site.register(PcpFeGroupe, PcpFeGroupeAdmin)
admin.site.register(ZoneGeographique, ZoneGeographiqueAdmin)

View File

@@ -0,0 +1,13 @@
"""
Ce fichier est créé pour inclure toute configuration de l'application.
"""
from django.apps import AppConfig
class BackendConfig(AppConfig):
"""Cette classe permet de configurer les variables du backend.
"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'backend'

View File

@@ -0,0 +1,7 @@
"""Ce fichier contient les constantes utilisées dans le backend.
"""
CATEGORIE_MDR = "MDR"
CATEGORIE_SOFF = "SOFF"
CATEGORIE_OFF = "OFF"
CATEGORIE_BY_NF = {'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'}

View File

@@ -0,0 +1,414 @@
"""
Ce fichier contient des fonctions qui permet aux utilisateurs de filtrer un ensemble de questions sur la base des champs d'un modèle.
"""
from django.db.models import Q
from django_filters import rest_framework as filters
from requests import request
from rest_framework.filters import OrderingFilter
from .models import Administre
from .models import AvisPosteChoices as AvisPoste
from .models import StatutPamChoices as StatutPam
from .models import (Decision, FormationEmploi, Poste, RefGest, RefOrg,
RefSvFil, SpecifiqueChoices, Administres_Pams,
Postes_Pams)
from .utils.decorators import class_logger
from .utils.logging import get_logger
from .utils.permissions import (KEY_READ, KEY_WRITE, Profiles,
get_adm_filter_by_lvl4_codes_fil,
get_adm_filter_by_lvl4_codes_future_pcp,
get_adm_filter_by_lvl4_codes_pcp,
get_lvl4_org_codes_by_any_code,
get_poste_filter_by_lvl4_codes_fil,
get_poste_filter_by_lvl4_codes_pcp,
get_profile_summary, get_queryset_org_by_user,
is_truthy)
# paramètre de requête pour restreindre par formation(s) d'emploi(s)
REQ_PARAM_FE = 'formation_emploi'
# paramètre de requête pour restreindre à un seul profil
REQ_PARAM_ONLY_PROFILE = 'profile'
# [poste] paramètre de requête pour restreindre par p_specifique
REQ_PARAM_POSTE_SPECIFIQUE = 'p_specifique'
# paramètre de requête pour restreindre par sous-vivier
REQ_PARAM_SOUS_VIVIER = 'sous_vivier'
@class_logger
class AdministreFilter(filters.FilterSet):
"""
Cette classe contient tous les filtres qui peuvent être effectués sur la table des administrés.
"""
aide_a_la_decision = filters.CharFilter(method='aide_a_la_decision_filter')
formation_emploi__in = filters.CharFilter(method='formation_emploi_filter')
sous_vivier__in = filters.CharFilter(method='sous_vivier_filter')
pam__in = filters.CharFilter(method='pam_filter')
class Meta:
model = Administre
fields = {
Administre.Cols.REL_SOUS_VIVIER: ['exact'],
}
def aide_a_la_decision_filter(self, queryset, name, value):
statut = [StatutPam.A_ETUDIER, StatutPam.A_MUTER]
return queryset.filter(Q(**{f'{Administre.Cols.REL_PAM_INTER}__a_statut_pam_annee__in' : statut})
& Q(**{f'{Administre.Cols.REL_PAM_INTER}__{Administres_Pams.Cols.REL_DECISION}__de_decision__isnull' :True})
& Q(**{f'{Administre.Cols.REL_PAM_INTER}__pam_id__exact' : value}))
def formation_emploi_filter(self, queryset, name, value):
list_values = value.split(',')
return queryset.filter(Q(**{f'{Administre.Cols.REL_FORMATION_EMPLOI}__fe_code__in': list_values}) \
| Q(**{f'{Administre.Cols.REL_PAM_INTER}__{Administres_Pams.Cols.REL_DECISION}__{Decision.Cols.REL_POSTE}__{Poste.Cols.REL_FORMATION_EMPLOI}__fe_code__in': list_values}))
def pam_filter(self, queryset, name, value):
return queryset.filter(Q(**{f'{Administre.Cols.M2M_PAM}__pam_id__icontains': value}))
@property
def qs(self):
qs = super().qs
summary = get_profile_summary(self.request.user)
profiles = summary.profiles.get(KEY_READ, ())
if not profiles:
return qs.none()
only_profile = self.request.query_params.get(REQ_PARAM_ONLY_PROFILE)
org_code = summary.org_code
is_fil = Profiles.FILIERE in profiles and (not only_profile or only_profile == Profiles.FILIERE)
is_pcp = Profiles.PCP in profiles and (not only_profile or only_profile == Profiles.PCP)
is_bvt = Profiles.BVT in profiles and (not only_profile or only_profile == Profiles.BVT)
if not org_code or (not is_fil and not is_pcp and not is_bvt):
return qs.none()
codes_lvl4 = get_lvl4_org_codes_by_any_code(org_code)
adm_filter = None
if is_fil:
adm_filter = get_adm_filter_by_lvl4_codes_fil(codes_lvl4)
if is_pcp:
pcp_filter = get_adm_filter_by_lvl4_codes_pcp(codes_lvl4) \
| get_adm_filter_by_lvl4_codes_future_pcp(codes_lvl4)
adm_filter = adm_filter | pcp_filter if adm_filter else pcp_filter
if is_bvt:
bvt_filter = Q(**{f'{Administre.Cols.REL_SOUS_VIVIER}': 'BVT'})
adm_filter = adm_filter | bvt_filter if adm_filter else bvt_filter
return qs.filter(adm_filter)
@class_logger
class AdministrePAMFilter(filters.FilterSet):
"""
Cette classe contient tous les filtres qui peuvent être effectués sur la table des administrés.
"""
aide_a_la_decision = filters.CharFilter(method='aide_a_la_decision_filter')
formation_emploi__in = filters.CharFilter(method='formation_emploi_filter')
pam__in = filters.CharFilter(method='pam_filter')
sous_vivier = filters.CharFilter(method='sous_vivier_filter')
adm__in = filters.CharFilter(method='administre_filter')
class Meta:
model = Administres_Pams
fields = {
}
def aide_a_la_decision_filter(self, queryset, name, value):
statut = [StatutPam.A_ETUDIER, StatutPam.A_MUTER]
return queryset.filter(Q(a_statut_pam_annee__in = statut)
& Q(**{f'{Administres_Pams.Cols.REL_DECISION}__de_decision__isnull' :True}))
def administre_filter(self,queryset, name, value):
values_list = value.split(',')
if values_list:
return queryset.filter(id__in=values_list)
def formation_emploi_filter(self, queryset, name, value):
list_values = value.split(',')
return queryset.filter(Q(**{f'{Administres_Pams.Cols.REL_ADMINISTRE}__{Administre.Cols.REL_FORMATION_EMPLOI}__fe_mere_credo__in': list_values}) \
| Q(**{f'{Administres_Pams.Cols.REL_DECISION}__{Decision.Cols.REL_POSTE}__{Poste.Cols.REL_FORMATION_EMPLOI}__fe_mere_credo__in': list_values}))
def pam_filter(self, queryset, name, value):
return queryset.filter(pam_id = value)
def sous_vivier_filter(self, queryset, name, value):
list_values = value.split('-')
return queryset.filter(administre__sous_vivier__in = list_values)
@property
def qs(self):
qs = super().qs
summary = get_profile_summary(self.request.user)
profiles = summary.profiles.get(KEY_READ, ())
if not profiles:
return qs.none()
only_profile = self.request.query_params.get(REQ_PARAM_ONLY_PROFILE)
org_code = summary.org_code
is_fil = Profiles.FILIERE in profiles and (not only_profile or only_profile == Profiles.FILIERE)
is_pcp = Profiles.PCP in profiles and (not only_profile or only_profile == Profiles.PCP)
is_bvt = Profiles.BVT in profiles and (not only_profile or only_profile == Profiles.BVT)
if not org_code or (not is_fil and not is_pcp and not is_bvt):
return qs.none()
codes_lvl4 = get_lvl4_org_codes_by_any_code(org_code)
adm_filter = None
if is_fil:
adm_filter = get_adm_filter_by_lvl4_codes_fil(codes_lvl4)
if is_pcp:
pcp_filter = get_adm_filter_by_lvl4_codes_pcp(codes_lvl4) \
| get_adm_filter_by_lvl4_codes_future_pcp(codes_lvl4)
adm_filter = adm_filter | pcp_filter if adm_filter else pcp_filter
if is_bvt:
bvt_filter = Q(**{f'{Administres_Pams.Cols.REL_ADMINISTRE}__{Administre.Cols.REL_SOUS_VIVIER}': 'BVT'})
adm_filter = adm_filter | bvt_filter if adm_filter else bvt_filter
return qs.filter(adm_filter)
class DecisionFilter(filters.FilterSet):
"""
Cette classe contient tous les filtres qui peuvent être effectués sur la table des decisions.
"""
class Meta:
model = Decision
fields = ['administre_id', 'poste_id']
@class_logger
class PosteFilter(filters.FilterSet):
"""
Cette classe contient tous les filtres qui peuvent être effectués sur la table des postes.
"""
aide_a_la_decision = filters.CharFilter(method='aide_a_la_decision_filter')
pam__in = filters.CharFilter(method='pam_filter')
class Meta:
model = Poste
fields = [
'p_specifique',
'p_itd_affecte',
Poste.Cols.REL_FORMATION_EMPLOI
]
def aide_a_la_decision_filter(self, queryset, name, value):
statut = [AvisPoste.P1, AvisPoste.P2, AvisPoste.P3, AvisPoste.P4]
return queryset.filter(Q(p_avis__in=statut) & Q(poste__decisions__isnull=True) & Q(poste__p_pam_id__exact = value))
def pam_filter(self, queryset, name, value):
values_list = value.split(',')
return queryset.filter(Q(**{f'{Poste.Cols.M2M_PAM}__pam_id__in': values_list}))
@property
def qs(self):
qs = super().qs
summary = get_profile_summary(self.request.user)
profiles = summary.profiles.get(KEY_READ, ())
if not profiles:
return qs.none()
only_profile = self.request.query_params.get(REQ_PARAM_ONLY_PROFILE)
org_code = summary.org_code
is_fil = Profiles.FILIERE in profiles and (not only_profile or only_profile == Profiles.FILIERE)
is_pcp = Profiles.PCP in profiles and (not only_profile or only_profile == Profiles.PCP)
is_bvt = Profiles.BVT in profiles and (not only_profile or only_profile == Profiles.BVT)
is_itd = Profiles.ITD in profiles and (not only_profile or only_profile == Profiles.ITD)
if not org_code or (not is_fil and not is_pcp and not is_bvt and not is_itd):
return qs.none()
codes_lvl4 = get_lvl4_org_codes_by_any_code(org_code)
poste_filter = None
if is_fil:
poste_filter = get_poste_filter_by_lvl4_codes_fil(codes_lvl4) \
| Q(p_specifique=SpecifiqueChoices.ITD)
if is_pcp:
pcp_filter = get_poste_filter_by_lvl4_codes_pcp(codes_lvl4) \
| Q(p_specifique=SpecifiqueChoices.ITD)
poste_filter = poste_filter | pcp_filter if poste_filter else pcp_filter
if is_itd:
itd_filter = Q(p_specifique=SpecifiqueChoices.ITD)
poste_filter = poste_filter | itd_filter if poste_filter else itd_filter
if is_bvt:
bvt_filter = Q(**{f'{Poste.Cols.M2M_SOUS_VIVIERS}': 'BVT'})
poste_filter = poste_filter | bvt_filter if poste_filter else bvt_filter
return qs.filter(poste_filter)
@classmethod
def get_fields(cls):
"""
Cette fonction renvoie toutes les valeurs de recherche dans
les champs qui peuvent être filtrés. Les champs qui peuvent être filtrés sont spécifiés dans la classe Meta variable fields.
"""
fields = super().get_fields()
for field_name in fields.copy():
lookup_list = cls.Meta.model._meta.get_field(field_name).get_lookups().keys()
fields[field_name] = lookup_list
return fields
@class_logger
class PostePAMFilter(filters.FilterSet):
"""
Cette classe contient tous les filtres qui peuvent être effectués sur la table des postes.
"""
aide_a_la_decision = filters.CharFilter(method='aide_a_la_decision_filter')
pam__in = filters.CharFilter(method='pam_filter')
p_specifique = filters.CharFilter(method='p_specifique_filter')
p_itd_affecte = filters.CharFilter(method='p_itd_affecte_filter')
formation_emploi__in = filters.CharFilter(method='formation_emploi_filter')
# sous_vivier = filters.CharFilter(method='sous_vivier_filter')
poste__in = filters.CharFilter(method='poste_filter')
class Meta:
model = Postes_Pams
fields = [
]
def aide_a_la_decision_filter(self, queryset, name, value):
statut = [AvisPoste.P1, AvisPoste.P2, AvisPoste.P3, AvisPoste.P4]
return queryset.filter(Q(p_avis_pam__in=statut) & Q(**{f'{Postes_Pams.Cols.O2M_DECISION}__de_decision__isnull' :True}) & Q(p_pam = value))
def poste_filter(self,queryset, name, value):
values_list = value.split(',')
if values_list:
return queryset.filter(id__in=values_list)
def pam_filter(self, queryset, name, value):
values_list = value.split(',')
return queryset.filter(Q(p_pam_id__in = values_list)).distinct('poste_id')
def p_specifique_filter(self, queryset, name, value):
return queryset.filter(Q(**{f'{Postes_Pams.Cols.REL_POSTE}__p_specifique': value}))
def p_itd_affecte_filter(self, queryset, name, value):
return queryset.filter(Q(**{f'{Postes_Pams.Cols.REL_POSTE}__p_itd_affecte': value}))
def formation_emploi_filter(self, queryset, name, value):
list_values = value.split(',')
return queryset.filter(Q(**{f'{Postes_Pams.Cols.REL_POSTE}__formation_emploi__fe_mere_credo__in': list_values}))
@property
def qs(self):
qs = super().qs
summary = get_profile_summary(self.request.user)
profiles = summary.profiles.get(KEY_READ, ())
if not profiles:
return qs.none()
only_profile = self.request.query_params.get(REQ_PARAM_ONLY_PROFILE)
org_code = summary.org_code
is_fil = Profiles.FILIERE in profiles and (not only_profile or only_profile == Profiles.FILIERE)
is_pcp = Profiles.PCP in profiles and (not only_profile or only_profile == Profiles.PCP)
is_bvt = Profiles.BVT in profiles and (not only_profile or only_profile == Profiles.BVT)
is_itd = Profiles.ITD in profiles and (not only_profile or only_profile == Profiles.ITD)
if not org_code or (not is_fil and not is_pcp and not is_bvt and not is_itd):
return qs.none()
codes_lvl4 = get_lvl4_org_codes_by_any_code(org_code)
itd_filter_add = Q(**{f'{Postes_Pams.Cols.REL_POSTE}__p_specifique': SpecifiqueChoices.ITD})
poste_filter = None
if is_fil:
poste_filter = get_poste_filter_by_lvl4_codes_fil(codes_lvl4) \
| itd_filter_add
if is_pcp:
list_a_pcp_ok = Administres_Pams.objects.filter(
get_adm_filter_by_lvl4_codes_pcp(codes_lvl4)
| get_adm_filter_by_lvl4_codes_future_pcp(codes_lvl4)
).values_list('administre_id', flat=True)
pcp_filter = get_poste_filter_by_lvl4_codes_pcp(codes_lvl4) \
| itd_filter_add \
| Q(**{f'{Postes_Pams.Cols.O2M_DECISION}__{Decision.Cols.REL_ADMINISTRE}__in': list_a_pcp_ok})
poste_filter = poste_filter | pcp_filter if poste_filter else pcp_filter
if is_itd:
itd_filter = itd_filter_add
poste_filter = poste_filter | itd_filter if poste_filter else itd_filter
if is_bvt:
bvt_filter = Q(**{f'{Postes_Pams.Cols.REL_POSTE}__{Poste.Cols.M2M_SOUS_VIVIERS}': 'BVT'})
poste_filter = poste_filter | bvt_filter if poste_filter else bvt_filter
return qs.filter(poste_filter).distinct()
@classmethod
def get_fields(cls):
"""
Cette fonction renvoie toutes les valeurs de recherche dans
les champs qui peuvent être filtrés. Les champs qui peuvent être filtrés sont spécifiés dans la classe Meta variable fields.
"""
fields = super().get_fields()
for field_name in fields.copy():
lookup_list = cls.Meta.model._meta.get_field(field_name).get_lookups().keys()
fields[field_name] = lookup_list
return fields
class RelatedOrderingFilter(OrderingFilter):
_max_related_depth = 3
@staticmethod
def _get_verbose_name(field, non_verbose_name):
return field.verbose_name if hasattr(field, 'verbose_name') else non_verbose_name.replace('_', ' ')
def _retrieve_all_related_fields(self, fields, model, depth=0):
valid_fields = []
if depth > self._max_related_depth:
return valid_fields
for field in fields:
if field.related_model and field.related_model != model:
rel_fields = self._retrieve_all_related_fields(
field.related_model._meta.get_fields(),
field.related_model,
depth + 1)
for rel_field in rel_fields:
valid_fields.append((
f'{field.name}__{rel_field[0]}',
self._get_verbose_name(field, rel_field[1])
))
else:
valid_fields.append((
field.name,
self._get_verbose_name(field, field.name),
))
return valid_fields
def get_valid_fields(self, queryset, view, context=None):
valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
if not valid_fields == '__all_related__':
if not context:
context = {}
valid_fields = super().get_valid_fields(queryset, view, context)
else:
valid_fields = [
*self._retrieve_all_related_fields(queryset.model._meta.get_fields(), queryset.model),
*[(key, key.title().split('__')) for key in queryset.query.annotations]
]
return valid_fields

View File

@@ -0,0 +1,20 @@
from backend.models import PcpFeGroupe
from django import forms
from django.contrib.admin.helpers import ActionForm
class TauxArmementForm(ActionForm):
"""Cette classe définit le formulaire du taux d'armement
"""
taux_armement_cible = forms.IntegerField(widget=forms.NumberInput, help_text="Taux d'armement cible pour la catégorie")
class GroupeFeForm(ActionForm):
"""Cette classe définit le formulaire du GroupeFe
"""
nom_groupe_fe = forms.CharField(widget=forms.TextInput, help_text="Nom du groupe de formations d'emploi")
class PcpFeGroupeForm(forms.ModelForm):
"""Cette classe définit le formulaire du PcpFeGroupe
"""
class Meta:
model = PcpFeGroupe
fields = ['pcp_fe_groupe', 'pcp_fe_categorie', 'gestionnaire']

View File

@@ -0,0 +1,660 @@
"""
Ce dossier contient tous les modèles de la base de données d'OGURE
"""
# Import
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import ugettext_lazy as _
from bulk_update_or_create import BulkUpdateOrCreateQuerySet
"""
Script de création des diffferentes tables de la base
OGURE NG utilisation de l'ORM de Django pour réaliser cette étape
"""
# Modéle des Sous viviers de militaires et poste
class SousVivier(models.Model):
"""Modèle des Sous viviers de militaires et poste
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'sv_id'
LIBELLE = 'sv_libelle'
REL_GESTIONNAIRE = 'gestionnaire'
sv_id = models.CharField(max_length=100, primary_key=True)
gestionnaire = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
sv_libelle = models.CharField(max_length=100)
# Modèle des Domaines
class Domaine(models.Model):
"""Modèle des Domaines
"""
d_code = models.CharField(primary_key=True, max_length=100)
d_libelle = models.CharField(max_length=100, null=True)
def as_dict(self):
return {
"id": self.d_code,
"code": self.d_code,
"libelle": self.d_code,
}
# Modèle des Filières
class Filiere(models.Model):
"""Modèle des Filières
"""
f_code = models.CharField(primary_key=True, max_length=100)
domaine = models.ForeignKey(Domaine, on_delete=models.SET_NULL, null=True)
f_libelle_court = models.CharField(max_length=100, null=True)
f_libelle_long = models.CharField(max_length=100, null=True)
def as_dict(self):
return {
"id": self.f_code,
"domaineId": self.domaine_id,
"code": self.f_code,
"libelle": self.f_code,
"libelleCourt": self.f_code,
}
# Modèle de l'association des sous-viviers à la catégorie et la filière
class SousVivierAssociation(models.Model):
"""Modèle de l'association des sous-viviers à la catégorie et la filière
"""
CATEGORIE_CHOICES = [('MDR', 'MDR'), ('SOFF', 'SOFF'), ('OFF', 'OFF'), ('OGX', 'OGX')]
sva_id = models.IntegerField(primary_key=True)
sous_vivier = models.ForeignKey(SousVivier, on_delete=models.SET_NULL, null=True)
filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True)
sva_categorie = models.CharField(max_length=5, choices=CATEGORIE_CHOICES)
sva_arme = models.CharField(max_length=20, null=True)
# Modèle des Grades
# FIXME le code grade et libellé sont inversés
class Grade(models.Model):
"""Modèle des Grades
"""
gr_code = models.CharField(max_length=100, primary_key=True)
gr_categorie = models.CharField(max_length=100, null=True)
gr_ordre = models.IntegerField(null=True)
def as_dict(self):
return {
"id": self.gr_code,
"code": self.gr_code,
# TODO corriger ci-dessous : catégorie (ex : "Sous-officiers"), libellé long (ex : "SERGENT CHEF") + ordre
"categorie": self.gr_categorie,
"ordre": self.gr_ordre
}
# Modèle des Garnison
class Garnison(models.Model):
"""Modèle des Garnison
"""
gar_id = models.CharField(max_length=100, primary_key=True)
gar_lieu = models.CharField(max_length=100, verbose_name="Garnison")
gar_code_postal = models.CharField(max_length=100, null=True)
def __str__(self):
return self.gar_lieu
# Modèle de groupe FE
class GroupeFe(models.Model):
"""Modèle de groupe FE
"""
groupe_fe_nom = models.CharField(primary_key=True, max_length=100, verbose_name='Groupe de FE')
def __str__(self):
return self.groupe_fe_nom
# Modèle des Formation d'emplois
class FormationEmploi(models.Model):
"""Modèle des Formation d'emplois
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'fe_code'
ZONE_DEFENSE = 'zone_defense'
REL_GARNISON = 'garnison'
REL_MERE = 'mere'
fe_code = models.CharField(primary_key=True, max_length=100)
groupe_fe = models.ForeignKey(GroupeFe, blank=True, on_delete=models.SET_NULL, null=True)
garnison = models.ForeignKey(Garnison, on_delete=models.CASCADE)
fe_libelle = models.CharField(max_length=100)
fe_taux_armement_cible_off = models.FloatField(null=True, verbose_name="TA cible OFF")
fe_nb_poste_reo_off = models.IntegerField(null=True)
fe_nb_poste_reevalue_off = models.IntegerField(null=True)
fe_nb_poste_vacant_off = models.IntegerField(null=True)
fe_nb_poste_occupe_off = models.IntegerField(null=True)
fe_taux_armement_cible_soff = models.FloatField(null=True, verbose_name="TA cible SOFF")
fe_nb_poste_reo_soff = models.IntegerField(null=True)
fe_nb_poste_reevalue_soff = models.IntegerField(null=True)
fe_nb_poste_vacant_soff = models.IntegerField(null=True)
fe_nb_poste_occupe_soff = models.IntegerField(null=True)
fe_taux_armement_cible_mdr = models.FloatField(null=True, verbose_name="TA cible MDR")
fe_nb_poste_reo_mdr = models.IntegerField(null=True)
fe_nb_poste_reevalue_mdr = models.IntegerField(null=True)
fe_nb_poste_vacant_mdr = models.IntegerField(null=True)
fe_nb_poste_occupe_mdr = models.IntegerField(null=True)
zone_defense = models.CharField('zone de défense', db_column='fe_zone_defense', max_length=64, blank=True, null=True)
mere = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='FE mère', db_column='fe_mere_id')
def as_dict(self):
return {
"id": self.fe_code,
"code": self.fe_code,
"libelle": self.fe_libelle,
"groupe_fe": self.groupe_fe_id
}
# Modèle des Groupes de marques
class MarquesGroupe(models.Model):
"""Modèle des Groupes de marques
"""
gm_id = models.IntegerField(primary_key=True)
gm_type = models.CharField(max_length=100)
gm_code = models.CharField(max_length=100)
gm_libelle = models.CharField(max_length=100)
gm_ordre = models.IntegerField(null=True)
gm_selection_multiple = models.BooleanField(null=True)
def as_dict(self):
return {
"id": self.gm_id,
"type": self.gm_type,
"code": self.gm_code,
"libelle": self.gm_libelle,
"selectionMultiple": self.gm_selection_multiple,
"ordre": self.gm_ordre
}
# Modèle des Marques
class Marque(models.Model):
"""Modèle des Marques
"""
mar_id = models.TextField(primary_key=True)
groupe_marques = models.ForeignKey(MarquesGroupe, on_delete=models.CASCADE)
mar_code = models.CharField(max_length=100)
mar_libelle = models.CharField(max_length=100)
mar_ordre = models.IntegerField(null=True)
def as_dict(self):
return {
"id": self.mar_id,
"groupeMarquesId": self.groupe_marques_id,
"code": self.mar_code,
"libelle": self.mar_libelle,
"ordre": self.mar_ordre
}
# Modèle des fonctions
class Fonction(models.Model):
"""Modèle des fonctions
"""
fon_id = models.CharField(max_length=100, primary_key=True)
fon_libelle = models.CharField(max_length=100)
# Modèle Compétences
class Competence(models.Model):
"""Modèle Compétences
"""
comp_id = models.CharField(primary_key=True, max_length=100)
comp_libelle = models.CharField(null=True, max_length=100)
def as_dict(self):
return {
"id": self.comp_id,
"libelle": self.comp_libelle,
}
class SpecifiqueChoices(models.TextChoices):
"""Choix pour les propositions de CIAT d'un poste"""
SHM = 'SHM', 'SHM'
ITD = 'ITD', 'ITD'
PPE = 'PPE', 'PPE'
class PropositionsArmementChoices(models.TextChoices):
"""Choix pour les propositions d'armement d'un poste"""
PROPOSE = 'PROPOSE', 'Propositions'
VALIDE = 'VALIDE', 'Propositions validées'
# Modèle des Poste
class Poste(models.Model):
""" Modèle des Poste """
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'p_id'
# relations many-to-many
M2M_COMPETENCES = 'competences'
# relations one-to-many
O2M_DECISION = 'decisions'
# relations one-to-one ou many-to-one
REL_FONCTION = 'fonction'
REL_FORMATION_EMPLOI = 'formation_emploi'
REL_SOUS_VIVIER = 'sous_vivier'
objects = BulkUpdateOrCreateQuerySet.as_manager()
CHOICES_NF = [('1A', '1A'), ('1B', '1B'), ('1C', '1C'), ('2.', '2.'), ('3A', '3A'), ('3B', '3B'),
('3B NFS', '3B NFS'), ('4.', '4.'), ('5A', '5A'), ('5B', '5B'), ('5C', '5C'), ('6A', '6A'),
('6B', '6B')]
AVIS = [('P1', 'P1'), ('P2', 'P2'), ('P3', 'P3'), ('P4', 'P4'), ('P4', 'P4'),('P4', 'P4'),('Gele','Gele'),('Non etudie','Non etudie')]
p_id = models.CharField(max_length=100, primary_key=True)
fonction = models.ForeignKey(Fonction, on_delete=models.SET_NULL, null=True)
sous_vivier = models.ForeignKey(SousVivier, on_delete=models.SET_NULL, null=True)
formation_emploi = models.ForeignKey(FormationEmploi, on_delete=models.SET_NULL, null=True)
competences = models.ManyToManyField(Competence)
p_domaine = models.ForeignKey(Domaine, on_delete=models.SET_NULL, null=True)
p_filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True)
p_fonction = models.CharField(max_length=100, null=True)
p_code_fonction = models.CharField(max_length=100, null=True)
p_nf = models.CharField(max_length=100, null=True, choices=CHOICES_NF)
p_categorie = models.CharField(max_length=100, null=True)
p_dep = models.IntegerField(null=True)
p_liste_id_marques = models.CharField(max_length=100, null=True, blank=True)
p_eip = models.CharField(max_length=100)
p_avis = models.CharField(max_length=100, default='Non etudie', choices=AVIS)
p_notes_gestionnaire = models.TextField(null=True)
p_notes_partagees = models.TextField(null=True)
p_poids_competences = models.FloatField(null=True)
p_poids_filiere = models.FloatField(null=True)
p_poids_nf = models.FloatField(null=True)
p_nb_p1 = models.IntegerField(null=True)
p_nb_p2 = models.IntegerField(null=True)
p_nb_p3 = models.IntegerField(null=True)
p_nb_p4 = models.IntegerField(null=True)
p_nb_non_etudie = models.IntegerField(null=True)
p_nb_gele = models.IntegerField(null=True)
p_nb_reo = models.IntegerField(null=True)
p_nb_reevalue = models.IntegerField(null=True)
p_nb_occupe = models.IntegerField(null=True)
p_nb_vacant = models.IntegerField(null=True)
p_nb_affectable = models.IntegerField(null=True)
p_ciat = models.BooleanField('CIAT', default=False, null=True)
p_specifique = models.CharField('Poste spécifique', max_length=250, choices=SpecifiqueChoices.choices, null=True, blank=True)
propositions_armement = models.CharField("propositions d'armement", db_column='p_propositions_armement', max_length=10, choices=PropositionsArmementChoices.choices, null=True, blank=True)
p_priorisation_pcp = models.TextField('Priorisation PCP', null=True, blank=True)
class Meta:
constraints = [
models.CheckConstraint(
name='%(app_label)s_%(class)s_p_specifique_valid',
check=models.Q(p_specifique__in=SpecifiqueChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_propositions_armement_valid',
check=models.Q(propositions_armement__in=PropositionsArmementChoices.values)
)
]
class StatutPamChoices(models.TextChoices):
"""[Administre] choix pour le statut PAM"""
A_ETUDIER = 'A_ETUDIER', 'A étudier'
A_ETUDIER_REC = 'A_ETUDIER_REC', 'A étudier REC'
A_MAINTENIR = 'A_MAINTENIR', 'A maintenir'
A_MUTER = 'A_MUTER', 'A muter'
A_TRAITER = 'A_TRAITER', 'A traiter'
NON_DISPONIBLE = 'NON_DISPONIBLE', 'Non disponible'
NON_ETUDIE = 'NON_ETUDIE', 'Non étudié'
PARTANT = 'PARTANT', 'Partant'
class StatutFuturChoices(models.TextChoices):
"""[Administre] choix pour le statut futur"""
CLDM = 'CLDM', 'CLDM'
CP = 'CP', 'CP'
DRJI = 'DRJI', 'DRJI'
NRCT = 'NRCT', 'NRCT'
DET = 'DET', 'DET'
RECONV = 'RECONV', 'RECONV'
# Modèle des administrés
class Administre(models.Model):
"""Modèle des administrés"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'a_id_sap'
STATUT_CONCERTO = 'a_statut_concerto'
STATUT_PAM = 'a_statut_pam'
DATE_STATUT_CONCERTO = 'a_date_statut_concerto'
# relations many-to-many
M2M_COMPETENCES = 'a_liste_id_competences'
# relations one-to-many
O2M_FMOB = 'fmobs'
# relations one-to-one ou many-to-one
REL_DECISION = 'decision'
REL_FONCTION = 'fonction'
REL_FORMATION_EMPLOI = 'formation_emploi'
REL_GRADE = 'grade'
REL_SOUS_VIVIER = 'sous_vivier'
objects = BulkUpdateOrCreateQuerySet.as_manager()
CHOICES_NF = [('1A', '1A'), ('1B', '1B'), ('1C', '1C'), ('2.', '2.'), ('3A', '3A'), ('3B', '3B'),
('3B NFS', '3B NFS'), ('4.', '4.'), ('5A', '5A'), ('5B', '5B'), ('5C', '5C'), ('6A', '6A'),
('6B', '6B')]
a_id_sap = models.IntegerField(primary_key=True)
formation_emploi = models.ForeignKey(FormationEmploi, related_name="formation_emploi", on_delete=models.SET_NULL,
null=True)
fonction = models.ForeignKey(Fonction, on_delete=models.SET_NULL, null=True)
sous_vivier = models.ForeignKey(SousVivier, on_delete=models.SET_NULL, null=True, blank=True)
grade = models.ForeignKey(Grade, on_delete=models.SET_NULL, null=True)
a_grade_date_debut = models.CharField(max_length=100, null=True)
a_liste_id_marques = models.CharField(max_length=100, null=True)
a_liste_id_competences = models.ManyToManyField(Competence)
a_nom = models.CharField(max_length=100)
a_prenom = models.CharField(max_length=100)
a_sexe = models.CharField(max_length=100)
a_id_def = models.CharField(max_length=100, null=True)
a_eip = models.CharField(max_length=100)
a_fonction = models.CharField(max_length=100, null=True)
a_code_fonction = models.CharField(max_length=100, null=True)
a_domaine = models.ForeignKey(Domaine, related_name="domaine", on_delete=models.SET_NULL, null=True)
a_filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True)
a_nf = models.CharField(max_length=100, null=True, choices=CHOICES_NF)
a_domaine_poste = models.ForeignKey(Domaine, related_name="domaine_poste", on_delete=models.SET_NULL, null=True)
a_filiere_poste = models.ForeignKey(Filiere, related_name="filiere_poste", on_delete=models.SET_NULL, null=True)
a_nf_poste = models.CharField(max_length=100, null=True, choices=CHOICES_NF)
a_categorie = models.CharField(max_length=5, null=True)
a_domaine_futur = models.ForeignKey(Domaine, related_name="domaine_futur", on_delete=models.SET_NULL, null=True)
a_filiere_futur = models.ForeignKey(Filiere, related_name="filiere_futur", on_delete=models.SET_NULL, null=True)
a_nf_futur = models.CharField(max_length=100, null=True, choices=CHOICES_NF)
a_bureau_gestion = models.CharField(max_length=100, null=True)
a_date_entree_service = models.CharField(max_length=100, null=True)
a_arme = models.CharField(max_length=100, null=True)
a_rg_origine_recrutement = models.CharField(max_length=100, null=True)
a_date_naissance = models.CharField(max_length=100, null=True)
a_diplome_hl = models.CharField(max_length=100, null=True)
a_dernier_diplome = models.CharField(max_length=100, null=True)
a_credo_fe = models.CharField(max_length=100, null=True)
a_date_arrivee_fe = models.CharField(max_length=100, null=True)
a_pos_statuaire = models.CharField(max_length=100, null=True)
a_date_pos_statuaire = models.CharField(max_length=100, null=True)
a_interruption_service = models.CharField(max_length=100, null=True)
a_situation_fam = models.CharField(max_length=100, null=True)
a_nombre_enfants = models.IntegerField(null=True)
a_date_rdc = models.DateField(null=True)
a_date_dernier_acr = models.DateField(null=True)
a_eis = models.CharField(max_length=100, null=True)
a_sap_conjoint = models.IntegerField(null=True)
a_flag_particulier = models.IntegerField(null=True)
a_flag_pam = models.IntegerField(null=True)
a_statut_pam = models.CharField('statut PAM', max_length=100, choices=StatutPamChoices.choices, null=True, blank=True)
a_notes_gestionnaire = models.TextField(null=True)
a_notes_partagees = models.TextField(null=True)
a_eip_futur = models.CharField(max_length=100, null=True)
a_affectation1 = models.CharField(max_length=100, null=True)
a_affectation2 = models.CharField(max_length=100, null=True)
a_affectation3 = models.CharField(max_length=100, null=True)
a_liste_depts_souhaites = models.CharField(max_length=100, null=True)
a_pls_gb_max = models.IntegerField(null=True)
a_marqueur_pn = models.BooleanField(default=False)
a_affectation4 = models.CharField(max_length=100, null=True)
a_affectation5 = models.CharField(max_length=100, null=True)
a_affectation6 = models.CharField(max_length=100, null=True)
a_affectation7 = models.CharField(max_length=100, null=True)
a_affectation8 = models.CharField(max_length=100, null=True)
a_affectation9 = models.CharField(max_length=100, null=True)
a_fonction1 = models.CharField(max_length=100, null=True)
a_fonction2 = models.CharField(max_length=100, null=True)
a_fonction3 = models.CharField(max_length=100, null=True)
a_fonction4 = models.CharField(max_length=100, null=True)
a_fonction5 = models.CharField(max_length=100, null=True)
a_fonction6 = models.CharField(max_length=100, null=True)
a_fonction7 = models.CharField(max_length=100, null=True)
a_fonction8 = models.CharField(max_length=100, null=True)
a_fonction9 = models.CharField(max_length=100, null=True)
a_profession_conjoint = models.CharField(max_length=100, null=True)
a_id_def_conjoint = models.CharField(max_length=100, null=True)
a_diplome_1 = models.CharField(max_length=100, null=True)
a_diplome_1_date = models.DateField(max_length=100, null=True)
a_diplome_1_note = models.FloatField(null=True)
a_diplome_2 = models.CharField(max_length=100, null=True)
a_diplome_2_date = models.DateField(max_length=100, null=True)
a_diplome_2_note = models.FloatField(null=True)
a_diplome_3 = models.CharField(max_length=100, null=True)
a_diplome_3_date = models.DateField(max_length=100, null=True)
a_diplome_3_note = models.FloatField(null=True)
a_diplome_4 = models.CharField(max_length=100, null=True)
a_diplome_4_date = models.DateField(max_length=100, null=True)
a_diplome_4_note = models.FloatField(null=True)
a_diplome_5 = models.CharField(max_length=100, null=True)
a_diplome_5_date = models.DateField(max_length=100, null=True)
a_diplome_5_note = models.FloatField(null=True)
a_diplome_6 = models.CharField(max_length=100, null=True)
a_diplome_6_date = models.DateField(max_length=100, null=True)
a_diplome_6_note = models.FloatField(null=True)
a_diplome_7 = models.CharField(max_length=100, null=True)
a_diplome_7_date = models.DateField(max_length=100, null=True)
a_diplome_7_note = models.FloatField(null=True)
a_diplome_8 = models.CharField(max_length=100, null=True)
a_diplome_8_date = models.DateField(max_length=100, null=True)
a_diplome_8_note = models.FloatField(null=True)
a_diplome_9 = models.CharField(max_length=100, null=True)
a_diplome_9_date = models.DateField(max_length=100, null=True)
a_diplome_9_note = models.FloatField(null=True)
a_diplome_10 = models.CharField(max_length=100, null=True)
a_diplome_10_date = models.DateField(max_length=100, null=True)
a_diplome_10_note = models.FloatField(null=True)
a_origine_recrutement = models.CharField(max_length=100, null=True)
statut_futur = models.CharField('statut futur', db_column='a_statut_futur', max_length=8, choices=StatutFuturChoices.choices, null=True, blank=True)
date_statut_futur = models.DateField('date du statut futur', db_column='a_date_statut_futur', null=True, blank=True)
a_statut_concerto = models.CharField('statut CONCERTO', max_length=32, null=True, blank=True)
a_date_statut_concerto = models.CharField('date du statut CONCERTO', max_length=32, null=True, blank=True)
suivi_previsionnel_situation = models.CharField('suivi prévisionnel de la situation du militaire', db_column='a_suivi_previsionnel_situation', max_length=100, null=True, blank=True)
date_suivi_previsionnel_situation = models.DateField('date du suivi prévisionnel de la situation du militaire', db_column='a_date_suivi_previsionnel_situation', null=True, blank=True)
a_annee_previsible_mutation = models.PositiveIntegerField('année prévisible de mutation', null=True)
a_fud = models.CharField('FUD de départ', max_length=32, null=True, blank=True)
a_date_fud = models.CharField('date du FUD', max_length=32, null=True, blank=True)
class Meta:
verbose_name = "Administré"
verbose_name_plural = "Administrés"
constraints = [
models.CheckConstraint(
name='%(app_label)s_%(class)s_statut_pam_valid',
check=models.Q(a_statut_pam__in=StatutPamChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_statut_futur_valid',
check=models.Q(statut_futur__in=StatutFuturChoices.values)
),
]
# class CompetencesAdministre(models.Model):
# """Modèle de table intermédiaire entre Administrés et Compétences
# """
# competence = models.ForeignKey(Competence, on_delete=models.CASCADE)
# administre = models.ForeignKey(Administre, on_delete=models.CASCADE)
# def __unicode__(self):
# return self.competence.comp_id + self.administre.a_id_sap
class FMOB(models.Model):
"""Modèle de FMOB
"""
objects = BulkUpdateOrCreateQuerySet.as_manager()
fmob_id = models.CharField(max_length=100, primary_key=True)
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, null=True, related_name="fmobs",
db_constraint=False)
fmob_millesime = models.IntegerField(null=True)
fmob_annulation_fmob = models.BooleanField(default=False)
fmob_annulation_femp = models.BooleanField(default=False)
fmob_sans_suite_militaire_fmob = models.BooleanField(default=False)
fmob_sans_suite_militaire_femp = models.BooleanField(default=False)
fmob_date_visa_militaire = models.DateField(max_length=100, null=True)
fmob_depart_institution_soff = models.BooleanField(default=False)
fmob_mobilite_bassin_externe = models.BooleanField(default=False)
fmob_mobilite_bassin_interne = models.BooleanField(default=False)
fmob_mobilite_centre_interet_adt = models.BooleanField(default=False)
fmob_mobilite_dans_specialite = models.BooleanField(default=False)
fmob_mobilite_hors_metropole = models.BooleanField(default=False)
fmob_mobilite_recrutement_particulier_administre = models.BooleanField(default=False)
fmob_motif_edition_la = models.CharField(max_length=100, null=True)
fmob_motif_edition_ll = models.CharField(max_length=100, null=True)
fmob_reception_drhat_fmob = models.BooleanField(default=False)
fmob_reconnaissance_parcours_pro_administre = models.BooleanField(default=False)
fmob_proposition_affectation_verrouille = models.CharField(max_length=100, null=True)
fmob_reception_drhat_femp = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_interne = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_externe = models.BooleanField(default=False)
fmob_avis_cdc_mutation_administre = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_centre_interet = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_specialite = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_hors_metropole = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_recrutement_particulier_admin = models.BooleanField(default=False)
fmob_date_deb_fmob = models.CharField(max_length=100, null=True)
fmob_date_fin_fmob = models.CharField(max_length=100, null=True)
fmob_date_signature_admin_fmob = models.DateField(max_length=100, null=True)
fmob_date_signature_admin_femp = models.DateField(max_length=100, null=True)
fmob_date_signature_chef_de_corps = models.DateField(max_length=100, null=True)
fmob_remarques_eventuelles_administres = models.TextField(null=True)
fmob_avis_commandant_formation = models.TextField(null=True)
fmob_fonction_1 = models.CharField(max_length=100, null=True)
fmob_fonction_2 = models.CharField(max_length=100, null=True)
fmob_fonction_3 = models.CharField(max_length=100, null=True)
fmob_fonction_4 = models.CharField(max_length=100, null=True)
fmob_fonction_5 = models.CharField(max_length=100, null=True)
fmob_commune_1 = models.CharField(max_length=100, null=True)
fmob_commune_2 = models.CharField(max_length=100, null=True)
fmob_commune_3 = models.CharField(max_length=100, null=True)
fmob_commune_4 = models.CharField(max_length=100, null=True)
fmob_commune_5 = models.CharField(max_length=100, null=True)
fmob_prio_1 = models.BooleanField(default=False)
fmob_prio_2 = models.BooleanField(default=False)
fmob_prio_3 = models.BooleanField(default=False)
fmob_prio_4 = models.BooleanField(default=False)
fmob_prio_5 = models.BooleanField(default=False)
fmob_avis_mutabilite = models.CharField(max_length=100, null=True)
fmob_obs = models.CharField(max_length=100, null=True)
fmob_fe_future = models.CharField(max_length=100, null=True)
# Modèle Liste de Préference
class PreferencesListe(models.Model):
"""Modèle Liste de Préference
"""
lp_id = models.IntegerField(primary_key=True)
administre = models.ForeignKey(Administre, on_delete=models.CASCADE)
poste = models.ForeignKey(Poste, on_delete=models.CASCADE)
lp_rang_poste = models.IntegerField()
# Modèle pour le suivi des calculs
class Calcul(models.Model):
"""Modèle pour le suivi des calculs
On y intègre également le statut du calcul
"""
class Statut(models.TextChoices):
AUCUN = 'AUCUN', 'aucun'
EN_ATTENTE = 'EN_ATTENTE', 'en attente'
EN_COURS = 'EN_COURS', 'en cours'
TERMINE = 'TERMINE', 'terminé'
ERREUR = 'ERREUR', 'terminé en erreur'
TERMINE_DE_FORCE = 'TERMINE_DE_FORCE', 'terminé de force'
ARRETER = 'ARRETER'
sous_vivier = models.OneToOneField(SousVivier, primary_key=True, on_delete=models.CASCADE)
ca_date_debut = models.DateTimeField(null=True, blank=True)
ca_date_fin = models.DateTimeField(null=True, blank=True)
ca_statut = models.CharField(max_length=100)
ca_statut_pourcentage = models.FloatField(default='100')
# Modèle pour Le PAM
class PAM(models.Model):
"""Modèle pour Le PAM
"""
pam_id = models.IntegerField(primary_key=True)
pam_date = models.CharField(max_length=100)
pam_libelle = models.CharField(max_length=100)
pam_statut = models.CharField(max_length=100)
# Modèle pour les notations
class Notation(models.Model):
"""Modèle pour les notations
"""
no_id = models.AutoField(primary_key=True)
administre = models.ForeignKey(Administre, on_delete=models.CASCADE)
poste = models.ForeignKey(Poste, on_delete=models.CASCADE)
pam = models.ForeignKey(PAM, on_delete=models.SET_NULL, null=True, blank=True)
no_rang_administre = models.IntegerField(null=True)
no_rang_poste = models.IntegerField(null=True)
no_date_execution = models.DateTimeField(auto_now=True)
no_score_administre = models.FloatField(null=True)
no_score_poste = models.FloatField(null=True)
no_flag_cple_ideal = models.BooleanField(default=False)
# Modèle pour les décisions
class Decision(models.Model):
"""Modèle pour les décisions
"""
administre = models.OneToOneField(Administre, on_delete=models.CASCADE, primary_key=True)
poste = models.ForeignKey(Poste, on_delete=models.CASCADE, related_name="decisions")
pam = models.ForeignKey(PAM, on_delete=models.SET_NULL, null=True, blank=True)
de_decision = models.CharField(max_length=100)
de_date_decision = models.DateTimeField(auto_now=True)
de_notes_gestionnaire = models.TextField(null=True, blank=True)
de_notes_partagees = models.TextField(null=True, blank=True)
notation = models.ForeignKey(Notation, on_delete=models.SET_NULL, null=True, blank=True)
# Modèle Utilisateur de l'application
class CustomUser(AbstractUser):
"""Modèle Utilisateur de l'application
"""
administre = models.ForeignKey(Administre, blank=True, on_delete=models.SET_NULL, null=True)
grade = models.CharField(blank=True, null=True, max_length=100)
# Modèle d'association des groupes de FE aux gestionnaires PCP
class PcpFeGroupe(models.Model):
"""Modèle d'association des groupes de FE aux gestionnaires PCP
"""
CATEGORIE_CHOICES = [('MDR', 'MDR'), ('OFF', 'OFF'), ('SOFF', 'SOFF'), ('OGX', 'OGX')]
pcp_fe_id = models.AutoField(primary_key=True)
pcp_fe_groupe = models.ForeignKey(GroupeFe, null=True, on_delete=models.CASCADE,
verbose_name='Nom du groupe')
pcp_fe_categorie = models.CharField(max_length=5, choices=CATEGORIE_CHOICES, null=True, verbose_name='Catégorie')
gestionnaire = models.ForeignKey(CustomUser, on_delete=models.CASCADE, null=True, db_constraint=False)
def __str__(self):
return str(self.gestionnaire.first_name) + ' ' + str(self.gestionnaire.last_name.upper()) + ' / ' + self.pcp_fe_groupe.groupe_fe_nom + ' / ' + self.pcp_fe_categorie
class Meta:
verbose_name = 'Lien gest PCP / groupe FE'
verbose_name_plural = 'Liens gest PCP / groupe FE'

View File

@@ -0,0 +1,18 @@
from .administre import *
from .calcul import *
from .commun import *
from .competence import *
from .decision import *
from .domaine import *
from .fichier_exporte import *
from .filiere import *
from .fmob import *
from .fonction import *
from .formation_emploi import *
from .garnison import *
from .grade import *
from .initial import *
from .poste import *
from .sous_vivier import *
from .pam import *
from .user import *

View File

@@ -0,0 +1,282 @@
from bulk_update_or_create import BulkUpdateOrCreateQuerySet
from django.db import models
from .commun import NiveauFonctionnelChoices, SpecifiqueChoices
from .competence import Competence
from .domaine import Domaine
from .filiere import Filiere
from .fonction import Fonction
from .formation_emploi import FormationEmploi
from .grade import Grade
from .sous_vivier import SousVivier
from .pam import PAM
class SuiviPrevisionnelMilitaire(models.TextChoices):
"""
[Administre] choix pour le gestionnaire PCP
"""
MOB_EXT = 'MOB_EXT', 'Mobilité externe'
MOB_INT = 'MOB_INT', 'Mobilité interne',
NRCT = 'NRCT', 'NRCT',
RDC = 'RDC', 'RDC',
LMT = 'LMT', 'Limite de contrat',
REC = 'REC', 'Reconversion',
REC_OE = 'REC_OE', 'Recrutement OE',
DFP = 'DFP', 'Détachement FP',
class StatutFuturChoices(models.TextChoices):
"""
[Administre] choix pour le statut futur
"""
CLDM = 'CLDM', 'CLDM'
CP = 'CP', 'CP'
DRJI = 'DRJI', 'DRJI'
NRCT = 'NRCT', 'NRCT'
DET = 'DET', 'DET'
RECONV = 'RECONV', 'RECONV'
class StatutPamChoices(models.TextChoices):
"""
[Administre] choix pour le statut PAM
attributs supplémentaires :
- calc_enabled : est-ce que ce statut permet le calcul ?
- dec_enabled : est-ce que ce statut permet de créer une décision ?
"""
# (valeur, calc_enabled, dec_enabled), libellé
A_ETUDIER = ('A_ETUDIER', True, True), 'A étudier'
A_ETUDIER_REC = ('A_ETUDIER_REC', True, True), 'A étudier REC'
A_MAINTENIR = ('A_MAINTENIR', False, True), 'A maintenir'
A_MUTER = ('A_MUTER', True, True), 'A muter'
A_TRAITER = ('A_TRAITER', False, False), 'A traiter'
NON_DISPONIBLE = ('NON_DISPONIBLE', False, False), 'Non disponible'
NON_ETUDIE = ('NON_ETUDIE', False, False), 'Non étudié'
PARTANT = ('PARTANT', False, False), 'Partant'
def __new__(cls, value):
obj = str.__new__(cls, value[0])
obj._value_ = value[0]
obj.calc_enabled = value[1]
obj.dec_enabled = value[2]
return obj
class Administre(models.Model):
"""
Modèle des administrés
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'a_id_sap'
CATEGORIE = 'a_categorie'
STATUT_CONCERTO = 'a_statut_concerto'
STATUT_CONCERTO_FUTUR = 'a_statut_concerto_futur'
DATE_STATUT_CONCERTO = 'a_date_statut_concerto'
DATE_STATUT_CONCERTO_FUTUR = 'a_date_statut_concerto_futur'
# relations many-to-many
M2M_COMPETENCES = 'a_liste_id_competences'
M2M_PAM = 'pam'
# relations one-to-many
O2M_FMOB = 'fmobs'
# relations one-to-one ou many-to-one
REL_DOMAINE = 'a_domaine'
REL_FILIERE = 'a_filiere'
REL_DECISION = 'decision'
REL_FONCTION = 'fonction'
REL_FORMATION_EMPLOI = 'formation_emploi'
REL_GRADE = 'grade'
REL_SOUS_VIVIER = 'sous_vivier'
STATUT_PAM = 'a_statut_pam'
REL_PAM_INTER = 'administre'
objects = BulkUpdateOrCreateQuerySet.as_manager()
a_id_sap = models.IntegerField(primary_key=True)
formation_emploi = models.ForeignKey(FormationEmploi, related_name="formation_emploi", on_delete=models.SET_NULL,
null=True, blank=True)
pam = models.ManyToManyField(PAM, through='Administres_Pams')
fonction = models.ForeignKey(Fonction, on_delete=models.SET_NULL, null=True, blank=True)
sous_vivier = models.ForeignKey(SousVivier, on_delete=models.SET_NULL, null=True, blank=True)
grade = models.ForeignKey(Grade, on_delete=models.SET_NULL, null=True, blank=True)
a_grade_date_debut = models.CharField(max_length=100, null=True, blank=True)
a_liste_id_marques = models.CharField(max_length=100, null=True, blank=True)
a_liste_id_competences = models.ManyToManyField(Competence, blank=True)
a_nom = models.CharField(max_length=100)
a_prenom = models.CharField(max_length=100)
a_sexe = models.CharField(max_length=100, null=True, blank=True)
a_id_def = models.CharField(max_length=100, null=True, blank=True)
a_eip = models.CharField(max_length=100, null=True, blank=True)
a_fonction = models.CharField(max_length=100, null=True, blank=True)
a_code_fonction = models.CharField(max_length=100, null=True, blank=True)
a_domaine = models.ForeignKey(Domaine, on_delete=models.SET_NULL, null=True, blank=True, db_constraint=False)
a_filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True, blank=True, db_constraint=False)
a_nf = models.CharField(max_length=100, null=True, blank=True, choices=NiveauFonctionnelChoices.choices)
a_domaine_poste = models.ForeignKey(Domaine, related_name="domaine_poste", on_delete=models.SET_NULL, null=True, blank=True)
a_filiere_poste = models.ForeignKey(Filiere, related_name="filiere_poste", on_delete=models.SET_NULL, null=True, blank=True)
a_nf_poste = models.CharField(max_length=100, null=True, blank=True, choices=NiveauFonctionnelChoices.choices)
a_categorie = models.CharField(max_length=5, null=True, blank=True)
a_domaine_futur = models.ForeignKey(Domaine, related_name="domaine_futur", on_delete=models.SET_NULL, null=True, blank=True)
a_filiere_futur = models.ForeignKey(Filiere, related_name="filiere_futur", on_delete=models.SET_NULL, null=True, blank=True)
a_nf_futur = models.CharField(max_length=100, null=True, blank=True, choices=NiveauFonctionnelChoices.choices)
a_domaine_gestion = models.CharField('Domaine de gestion (BVT)', max_length=100, null=True, blank=True)
a_date_entree_service = models.CharField(max_length=100, null=True, blank=True)
a_arme = models.CharField(max_length=100, null=True, blank=True)
a_rg_origine_recrutement = models.CharField(max_length=100, null=True, blank=True)
a_date_naissance = models.CharField(max_length=100, null=True, blank=True)
a_diplome_hl = models.CharField(max_length=100, null=True, blank=True)
a_dernier_diplome = models.CharField(max_length=100, null=True, blank=True)
a_credo_fe = models.CharField(max_length=100, null=True, blank=True)
a_date_arrivee_fe = models.CharField(max_length=100, null=True, blank=True)
a_pos_statuaire = models.CharField(max_length=100, null=True, blank=True)
a_date_pos_statuaire = models.CharField(max_length=100, null=True, blank=True)
a_interruption_service = models.CharField(max_length=100, null=True, blank=True)
a_situation_fam = models.CharField(max_length=100, null=True, blank=True)
a_date_mariage = models.CharField(max_length=100, null=True, blank=True)
a_nombre_enfants = models.IntegerField(null=True, blank=True)
a_enfants = models.CharField(max_length=100, null=True, blank=True)
a_date_rdc = models.DateField(null=True, blank=True)
a_date_dernier_acr = models.DateField(null=True, blank=True)
a_eis = models.CharField(max_length=100, null=True, blank=True)
a_sap_conjoint = models.IntegerField(null=True, blank=True)
a_flag_particulier = models.IntegerField(null=True, blank=True)
a_notes_partagees = models.TextField(null=True, blank=True)
a_eip_fiche_detaille = models.CharField(max_length=100, null=True, blank=True)
a_eip_futur = models.CharField(max_length=100, null=True, blank=True)
a_liste_depts_souhaites = models.CharField(max_length=100, null=True, blank=True)
a_liste_zones_geographiques_shm = models.CharField(max_length=100, null=True, blank=True)
a_pls_gb_max = models.IntegerField(null=True, blank=True)
a_marqueur_pn = models.BooleanField(default=False, null=True)
a_fonction1 = models.CharField(max_length=100, null=True, blank=True)
a_fonction2 = models.CharField(max_length=100, null=True, blank=True)
a_fonction3 = models.CharField(max_length=100, null=True, blank=True)
a_fonction4 = models.CharField(max_length=100, null=True, blank=True)
a_fonction5 = models.CharField(max_length=100, null=True, blank=True)
a_fonction6 = models.CharField(max_length=100, null=True, blank=True)
a_fonction7 = models.CharField(max_length=100, null=True, blank=True)
a_fonction8 = models.CharField(max_length=100, null=True, blank=True)
a_fonction9 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction1 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction2 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction3 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction4 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction5 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction6 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction7 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction8 = models.CharField(max_length=100, null=True, blank=True)
a_date_fonction9 = models.CharField(max_length=100, null=True, blank=True)
a_profession_conjoint = models.CharField(max_length=100, null=True, blank=True)
a_id_def_conjoint = models.CharField(max_length=100, null=True, blank=True)
a_notes_gestionnaire = models.TextField(null=True, blank=True)
a_statut_pam = models.CharField('statut PAM', max_length=100,choices=StatutPamChoices.choices, null=True, blank=True)
a_sexe_conjoint = models.CharField(max_length=100, null=True, blank=True)
a_lien_service = models.CharField(max_length=100, null=True, blank=True)
a_age_en_annees = models.CharField(max_length=100, null=True, blank=True)
a_origine_recrutement = models.CharField(max_length=100, null=True, blank=True)
statut_futur = models.CharField('statut futur', db_column='a_statut_futur', max_length=8, choices=StatutFuturChoices.choices, null=True, blank=True)
date_statut_futur = models.DateField('date du statut futur', db_column='a_date_statut_futur', null=True, blank=True)
a_statut_concerto = models.CharField('statut CONCERTO', max_length=1000, null=True, blank=True)
a_date_statut_concerto = models.CharField('date du statut CONCERTO', max_length=32, null=True, blank=True)
a_statut_concerto_futur = models.CharField('statut CONCERTO futur', max_length=100, null=True, blank=True)
a_date_statut_concerto_futur = models.CharField('date du statut CONCERTO futur', max_length=32, null=True, blank=True)
suivi_previsionnel_situation = models.CharField('suivi prévisionnel de la situation du militaire', max_length=100, choices=SuiviPrevisionnelMilitaire.choices, null=True, blank=True)
date_suivi_previsionnel_situation = models.DateField('date du suivi prévisionnel de la situation du militaire', db_column='a_date_suivi_previsionnel_situation', null=True, blank=True)
a_annee_previsible_mutation = models.PositiveIntegerField('année prévisible de mutation', null=True, blank=True)
a_fud = models.CharField('FUD de départ', max_length=32, null=True, blank=True)
a_date_fud = models.CharField('date du FUD', max_length=32, null=True, blank=True)
a_ciat = models.BooleanField('CIAT', default=False, null=True)
a_specifique = models.CharField('PPE / SHM / ITD', max_length=250, choices=SpecifiqueChoices.choices, null=True, blank=True)
a_date_affectation = models.DateField("Date d'affectation", null=True, blank=True)
class Meta:
verbose_name = "Administré"
verbose_name_plural = "Administrés"
constraints = [
models.CheckConstraint(
name='%(app_label)s_%(class)s_a_nf_valid',
check=models.Q(a_nf__in=NiveauFonctionnelChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_a_nf_poste_valid',
check=models.Q(a_nf_poste__in=NiveauFonctionnelChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_a_nf_futur_valid',
check=models.Q(a_nf_futur__in=NiveauFonctionnelChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_statut_pam_valid',
check=models.Q(a_statut_pam__in=StatutPamChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_statut_futur_valid',
check=models.Q(statut_futur__in=StatutFuturChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_a_specifique',
check=models.Q(a_specifique__in=SpecifiqueChoices.values)
)
]
class Administres_Pams(models.Model):
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'id'
REL_PAM = 'pam'
REL_ADMINISTRE = 'administre'
O2M_FMOB = 'fmobs'
REL_DECISION = 'decision'
STATUT_PAM = 'a_statut_pam_annee'
objects = BulkUpdateOrCreateQuerySet.as_manager()
id = models.CharField(primary_key=True, max_length=100)
pam = models.ForeignKey(PAM, on_delete = models.CASCADE, related_name='pam')
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, related_name=Administre.Cols.REL_PAM_INTER)
a_statut_pam_annee = models.CharField('statut PAM', max_length=100,choices=StatutPamChoices.choices, null=True, blank=True)
notes_pam = models.TextField(null=True, blank=True)
a_ciat_pam = models.BooleanField('CIAT', default=False, null=True)
a_specifique_pam = models.CharField('PPE / SHM / ITD', max_length=250, choices=SpecifiqueChoices.choices, null=True, blank=True)
a_liste_depts_souhaites_pam = models.CharField(max_length=100, null=True, blank=True)
a_liste_zones_geographiques_shm_pam = models.CharField(max_length=100, null=True, blank=True)
a_situationfuture_notes_fe = models.TextField(null=True, blank=True)
class Affectation(models.Model):
"""
Modèle pour les affectations
"""
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, null=True, blank=True, related_name='adm_affec', db_constraint=False)
affect_libelle = models.CharField(max_length=100, null=True, blank=True)
affect_date = models.CharField(max_length=100, null=True, blank=True)
class Diplome(models.Model):
"""
Modèle pour les diplomes
"""
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, null=True, blank=True, related_name='adm_dip', db_constraint=False)
diplome_libelle = models.CharField(max_length=100, null=True, blank=True)
diplome_date = models.CharField(max_length=100, null=True, blank=True)
diplome_note = models.CharField(max_length=100, null=True, blank=True)
diplome_niveau = models.CharField(max_length=100, null=True, blank=True)
class FUD(models.Model):
"""
Modèle pour les formulaires unitaires de demandes
"""
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, null=True, blank=True, related_name='adm_fud', db_constraint=False)
fud_libelle = models.CharField(max_length=100, null=True, blank=True)
fud_date_debut = models.CharField(max_length=100, null=True, blank=True)
fud_date_fin = models.CharField(max_length=100, null=True, blank=True)

View File

@@ -0,0 +1,44 @@
from django.db import models
from django.db.models import Q
from .pam import PAM
from .sous_vivier import SousVivier
class StatutCalculChoices(models.TextChoices):
AUCUN = 'AUCUN', 'aucun'
EN_ATTENTE = 'EN_ATTENTE', 'en attente'
EN_ATTENTE_ARRET = 'EN_ATTENTE_ARRET', "en attente d'arrêt"
EN_COURS = 'EN_COURS', 'en cours'
TERMINE = 'TERMINE', 'terminé'
ERREUR_ADMINISTRE = 'ERREUR_ADMINISTRE','terminé en erreur administré',
ERREUR_POSTE = 'ERREUR_POSTE', 'terminé en erreur poste',
ERREUR = 'ERREUR', 'terminé en erreur'
TERMINE_DE_FORCE = 'TERMINE_DE_FORCE', 'terminé de force'
class Calcul(models.Model):
"""
Modèle pour le suivi des calculs
On y intègre également le statut du calcul
"""
id = models.CharField(max_length=100, primary_key=True, default="")
sous_vivier = models.ForeignKey(SousVivier, on_delete=models.CASCADE, verbose_name="Sous-vivier")
pam = models.ForeignKey(PAM, on_delete=models.CASCADE, null=True, blank=True)
ca_date_debut = models.DateTimeField('Date de début', null=True, blank=True)
ca_date_fin = models.DateTimeField('Date de fin', null=True, blank=True)
ca_statut = models.CharField('Statut', max_length=40, choices=StatutCalculChoices.choices)
ca_statut_pourcentage = models.FloatField('Avancement (%)',default=0)
class Meta:
constraints = [
models.CheckConstraint(
name='%(app_label)s_%(class)s_ca_statut_valid',
check=models.Q(ca_statut__in=StatutCalculChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_ca_statut_pourcentage_valid',
check=Q(ca_statut_pourcentage__gte=0.0) & Q(ca_statut_pourcentage__lte=100.0)
),
]

View File

@@ -0,0 +1,31 @@
from django.db import models
class NiveauFonctionnelChoices(models.TextChoices):
"""
Choix pour les niveaux fonctionnels
"""
N1A = '1A', '1A'
N1B = '1B', '1B'
N1C = '1C', '1C'
N2 = '2.', '2.'
N3A = '3A', '3A'
N3B = '3B', '3B'
N3B_NFS = '3B NFS', '3B NFS'
N4 = '4.', '4.'
N5A = '5A', '5A'
N5B = '5B', '5B'
N5C = '5C', '5C'
N6A = '6A', '6A'
N6B = '6B', '6B'
class SpecifiqueChoices(models.TextChoices):
"""
Choix pour les propositions de CIAT
"""
SHM = 'SHM', 'SHM'
ITD = 'ITD', 'ITD'
PPE = 'PPE', 'PPE'

View File

@@ -0,0 +1,31 @@
from django.db import models
from .domaine import Domaine
from .filiere import Filiere
class Competence(models.Model):
"""Modèle Compétences"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'comp_id'
CATEGORIE = 'comp_categorie'
LIBELLE = 'comp_libelle'
# relations one-to-one ou many-to-one
REL_DOMAINE = 'comp_domaine'
REL_FILIERE = 'comp_filiere'
comp_id = models.CharField('ID', primary_key=True, max_length=100)
comp_libelle = models.CharField('libellé', null=True, blank=True, max_length=100)
comp_domaine = models.ForeignKey(Domaine, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='domaine')
comp_filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='filière')
comp_categorie = models.CharField('catégorie', max_length=100, null=True, blank=True)
def as_dict(self):
return {
"id": self.comp_id,
"libelle": self.comp_libelle,
}

View File

@@ -0,0 +1,113 @@
from enum import Enum, auto
from django.db import models
from .administre import Administre, Administres_Pams
from .initial import PAM, Notation
from .poste import Poste, Postes_Pams
class DecisionTree(Enum):
""" enum pour les types d'arbres de décisions """
# métropole
ME = auto()
# hors métropole
HME = auto()
def __repr__(self):
return self.__str__()
class DecisionChoices(models.TextChoices):
"""
Choix pour les statuts de décisions
attributs supplémentaires :
- trees : Tuple[DecisionTree], permet de savoir à quel(s) arbre(s) appartient le statut
"""
# arbre de décision en métropole
PROPOSITION_FE = ('PROPOSITION_FE', DecisionTree.ME), 'Proposition de FE'
DIALOGUE_EN_COURS = ('DIALOGUE_EN_COURS', DecisionTree.ME), 'Dialogue en cours'
DIALOGUE_TERMINE = ('DIALOGUE_TERMINE', DecisionTree.ME), 'Dialogue terminé'
DIALOGUE_INFRUCTUEUX = ('DIALOGUE_INFRUCTUEUX', DecisionTree.ME), 'Dialogue infructueux'
FOREMP_EN_COURS = ('FOREMP_EN_COURS', DecisionTree.ME), 'FOREMP en cours'
FOREMP_TERMINE = ('FOREMP_TERMINE', DecisionTree.ME), 'FOREMP terminé'
PREPOSITIONNE = ('PREPOSITIONNE', DecisionTree.ME), 'Prépositionné'
POSITIONNE = ('POSITIONNE', DecisionTree.ME), 'Positionné'
OMIP_EN_COURS = ('OMIP_EN_COURS', DecisionTree.ME), 'OMIP en cours'
OMIP_TERMINE = ('OMIP_TERMINE', DecisionTree.ME), 'OMIP terminé'
ATTENTE_AVIONAGE = ('ATTENTE_AVIONAGE', DecisionTree.ME), "En attente d'avionage"
OMI_EN_COURS = ('OMI_EN_COURS', DecisionTree.ME), 'OMI en cours'
OMI_ACTIVE = ('OMI_ACTIVE', DecisionTree.ME), 'OMI terminé'
OMI_ANNULE = ('OMI_ANNULE', DecisionTree.ME), 'OMI annulation'
# arbre de décision hors métropole
HME_DIALOGUE_INITIE = ('HME_DIALOGUE_INITIE', DecisionTree.HME), 'Dialogue initié (HME)'
HME_DIALOGUE_EN_COURS = ('HME_DIALOGUE_EN_COURS', DecisionTree.HME), 'Dialogue en cours (HME)'
HME_DIALOGUE_INFRUCTUEUX = ('HME_DIALOGUE_INFRUCTUEUX', DecisionTree.HME), 'Dialogue infructueux (HME)'
HME_DIALOGUE_TERMINE = ('HME_DIALOGUE_TERMINE', DecisionTree.HME), 'Dialogue terminé (HME)'
HME_PROPOSITION_VIVIER = ('HME_PROPOSITION_VIVIER', DecisionTree.HME), 'Proposition vivier HME'
HME_ETUDE_DESISTEMENT = ('HME_ETUDE_DESISTEMENT', DecisionTree.HME), 'Etude désistement (HME)'
HME_DESISTEMENT = ('HME_DESISTEMENT', DecisionTree.HME), 'Désistement activé (HME)'
HME_PREPOSITIONNE = ('HME_PREPOSITIONNE', DecisionTree.HME), 'Prépositionné (HME)'
HME_VALIDATION_EXPERT = ('HME_VALIDATION_EXPERT', DecisionTree.HME), 'Validation HME (HME)'
HME_REFUS_EXPERT = ('HME_REFUS_EXPERT', DecisionTree.HME), 'Refus HME (HME)'
HME_POSITIONNE = ('HME_POSITIONNE', DecisionTree.HME), 'Positionné (HME)'
HME_FOREMP_EN_COURS = ('HME_FOREMP_EN_COURS', DecisionTree.HME), 'FOREMP en cours (HME)'
HME_FOREMP_TERMINE = ('HME_FOREMP_TERMINE', DecisionTree.HME), 'FOREMP terminé (HME)'
HME_OMIP_EN_COURS = ('HME_OMIP_EN_COURS', DecisionTree.HME), 'OMIP en cours (HME)'
HME_OMIP_TERMINE = ('HME_OMIP_TERMINE', DecisionTree.HME), 'OMIP terminé (HME)'
HME_ATTENTE_AVIONAGE = ('HME_ATTENTE_AVIONAGE', DecisionTree.HME), "En attente d'avionage (HME)"
HME_OMI_EN_COURS = ('HME_OMI_EN_COURS', DecisionTree.HME), 'OMI en cours (HME)'
HME_OMI_ACTIVE = ('HME_OMI_ACTIVE', DecisionTree.HME), 'OMI terminé (HME)'
HME_OMI_ANNULE = ('HME_OMI_ANNULE', DecisionTree.ME), 'OMI annulation'
REMIS_A_DISPOSITION = ('REMIS_A_DISPOSITION', (DecisionTree.ME, DecisionTree.HME)), 'Remis à disposition'
def __new__(cls, value):
obj = str.__new__(cls, value[0])
obj._value_ = value[0]
_trees = value[1]
obj.trees = _trees if isinstance(_trees, tuple) else (_trees,) if _trees else ()
return obj
def __repr__(self):
return "%s.%s" % (self.__class__.__name__, self._name_)
# Modèle pour les décisions
class Decision(models.Model):
"""
Modèle pour les décisions
"""
class Cols():
""" Constantes pour les noms de colonnes """
STATUT = 'de_decision'
DATE = 'de_date_decision'
# relations one-to-one ou many-to-one
REL_ADMINISTRE = 'administre'
REL_POSTE = 'poste'
administre_pam = models.OneToOneField(Administres_Pams, on_delete=models.CASCADE, related_name=Administres_Pams.Cols.REL_DECISION, primary_key=True, default="")
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, related_name=Administres_Pams.Cols.REL_DECISION)
poste = models.ForeignKey(Poste, on_delete=models.CASCADE, related_name= Postes_Pams.Cols.O2M_DECISION)
poste_pam = models.OneToOneField(Postes_Pams, on_delete=models.CASCADE, related_name= Postes_Pams.Cols.O2M_DECISION, default="")
de_decision = models.CharField('Décision', max_length=50, choices=DecisionChoices.choices)
de_date_decision = models.DateTimeField('Date de décision', auto_now=True)
de_notes_gestionnaire = models.TextField(null=True, blank=True)
de_notes_partagees = models.TextField(null=True, blank=True)
notation = models.ForeignKey(Notation, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
constraints = [
models.CheckConstraint(
name='%(app_label)s_%(class)s_de_decision_valid',
check=models.Q(de_decision__in=DecisionChoices.values)
)
]
verbose_name = 'Décision'
verbose_name_plural = 'Décisions'

View File

@@ -0,0 +1,21 @@
from django.db import models
class Domaine(models.Model):
"""
Modèle des domaines
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'd_code'
d_code = models.CharField('code', primary_key=True, max_length=100)
def as_dict(self):
return {
'id': self.d_code,
'code': self.d_code,
'libelle': self.d_code,
}

View File

@@ -0,0 +1,11 @@
from django.db import models
class FichiersExporte(models.Model):
"""Modèle de fichiers exportés"""
nom_fichier = models.CharField(null=True, blank=True, max_length=100)
date_export = models.DateTimeField(null=True, blank=True, auto_now=True)
class Meta:
verbose_name = 'Fichier exporté'
verbose_name_plural = 'Fichiers exportés'

View File

@@ -0,0 +1,28 @@
from django.db import models
from .domaine import Domaine
class Filiere(models.Model):
"""
Modèle des filières
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'f_code'
# relations one-to-one ou many-to-one
REL_DOMAINE = 'domaine'
f_code = models.CharField('code', primary_key=True, max_length=100)
domaine = models.ForeignKey(Domaine, on_delete=models.SET_NULL, null=True, blank=True)
def as_dict(self):
return {
'id': self.f_code,
'domaineId': self.domaine_id,
'code': self.f_code,
'libelle': self.f_code,
}

View File

@@ -0,0 +1,71 @@
from bulk_update_or_create import BulkUpdateOrCreateQuerySet
from django.db import models
from .administre import Administre, Administres_Pams
class FMOB(models.Model):
"""Modèle de FMOB
"""
CHOICES_STATUT = [('Annulé', 'Annulé'), ('Classé sans suite', 'Classé sans suite'), ('Non réceptionné', 'Non réceptionné'),
('Réceptionné', 'Réceptionné')]
objects = BulkUpdateOrCreateQuerySet.as_manager()
fmob_id = models.CharField(max_length=100, primary_key=True)
administre = models.OneToOneField(Administres_Pams, on_delete=models.CASCADE, null=True, blank=True, related_name='fmobs')
#administre = models.ForeignKey(Administre, on_delete=models.CASCADE, null=True, blank=True, related_name=Administre.Cols.O2M_FMOB, db_constraint=False)
fmob_millesime = models.IntegerField(null=True, blank=True)
fmob_millesime_femp = models.IntegerField(null=True, blank=True)
fmob_reception_drhat_fmob = models.BooleanField(default=False)
fmob_annulation_fmob = models.BooleanField(default=False)
fmob_annulation_femp = models.BooleanField(default=False)
fmob_sans_suite_militaire_fmob = models.BooleanField(default=False)
fmob_sans_suite_militaire_femp = models.BooleanField(default=False)
fmob_date_visa_militaire = models.DateField(max_length=100, null=True, blank=True)
fmob_depart_institution_soff = models.BooleanField(default=False)
fmob_mobilite_bassin_externe = models.BooleanField(default=False)
fmob_mobilite_bassin_interne = models.BooleanField(default=False)
fmob_mobilite_centre_interet_adt = models.BooleanField(default=False)
fmob_mobilite_dans_specialite = models.BooleanField(default=False)
fmob_mobilite_hors_metropole = models.BooleanField(default=False)
fmob_mobilite_recrutement_particulier_administre = models.BooleanField(default=False)
fmob_motif_edition_la = models.CharField(max_length=100, null=True, blank=True)
fmob_motif_edition_ll = models.CharField(max_length=100, null=True, blank=True)
fmob_reception_drhat_fmob = models.BooleanField(default=False)
fmob_reconnaissance_parcours_pro_administre = models.BooleanField(default=False)
fmob_proposition_affectation_verrouille = models.CharField(max_length=100, null=True, blank=True)
fmob_reception_drhat_femp = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_interne = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_externe = models.BooleanField(default=False)
fmob_avis_cdc_mutation_administre = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_centre_interet = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_specialite = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_hors_metropole = models.BooleanField(default=False)
fmob_avis_cdc_mobilite_recrutement_particulier_admin = models.BooleanField(default=False)
fmob_date_deb_fmob = models.CharField(max_length=100, null=True, blank=True)
fmob_date_fin_fmob = models.CharField(max_length=100, null=True, blank=True)
fmob_date_signature_admin_fmob = models.DateField(max_length=100, null=True, blank=True)
fmob_date_signature_admin_femp = models.DateField(max_length=100, null=True, blank=True)
fmob_date_signature_chef_de_corps = models.DateField(max_length=100, null=True, blank=True)
fmob_remarques_eventuelles_administres = models.TextField(null=True, blank=True)
fmob_avis_commandant_formation = models.TextField(null=True, blank=True)
fmob_fonction_1 = models.CharField(max_length=100, null=True, blank=True)
fmob_fonction_2 = models.CharField(max_length=100, null=True, blank=True)
fmob_fonction_3 = models.CharField(max_length=100, null=True, blank=True)
fmob_fonction_4 = models.CharField(max_length=100, null=True, blank=True)
fmob_fonction_5 = models.CharField(max_length=100, null=True, blank=True)
fmob_commune_1 = models.CharField(max_length=100, null=True, blank=True)
fmob_commune_2 = models.CharField(max_length=100, null=True, blank=True)
fmob_commune_3 = models.CharField(max_length=100, null=True, blank=True)
fmob_commune_4 = models.CharField(max_length=100, null=True, blank=True)
fmob_commune_5 = models.CharField(max_length=100, null=True, blank=True)
fmob_prio_1 = models.BooleanField(default=False)
fmob_prio_2 = models.BooleanField(default=False)
fmob_prio_3 = models.BooleanField(default=False)
fmob_prio_4 = models.BooleanField(default=False)
fmob_prio_5 = models.BooleanField(default=False)
fmob_avis_mutabilite = models.CharField(max_length=100, null=True, blank=True)
fmob_obs = models.CharField(max_length=100, null=True, blank=True)
fmob_fe_future = models.CharField(max_length=100, null=True, blank=True)
fmob_statut = models.CharField(max_length=100, null=True, blank=True, choices=CHOICES_STATUT)
fmob_commentaire_ac = models.TextField(null=True, blank=True)

View File

@@ -0,0 +1,15 @@
from django.db import models
class Fonction(models.Model):
"""
Modèle des fonctions
"""
fon_id = models.CharField(max_length=100, primary_key=True)
fon_libelle = models.CharField(max_length=100)
def as_dict(self):
return {
"id": self.fon_id,
"libelle": self.fon_libelle,
}

View File

@@ -0,0 +1,96 @@
from django.db import models
from .garnison import Garnison
from .user import CustomUser
class GroupeFe(models.Model):
"""
Modèle de groupes FE
"""
groupe_fe_nom = models.CharField(primary_key=True, max_length=100, verbose_name='Groupe de FE')
def __str__(self):
return self.groupe_fe_nom
class PcpFeGroupe(models.Model):
"""
Modèle d'association des groupes de FE aux gestionnaires PCP
"""
CATEGORIE_CHOICES = [('MDR', 'MDR'), ('OFF', 'OFF'), ('SOFF', 'SOFF'), ('OGX', 'OGX')]
pcp_fe_id = models.AutoField(primary_key=True)
pcp_fe_groupe = models.ForeignKey(GroupeFe, null=True, blank=True, on_delete=models.CASCADE,
verbose_name='Nom du groupe')
pcp_fe_categorie = models.CharField(max_length=5, choices=CATEGORIE_CHOICES, null=True, blank=True, verbose_name='Catégorie')
gestionnaire = models.ForeignKey(CustomUser, on_delete=models.CASCADE, null=True, blank=True, db_constraint=False)
def __str__(self):
return str(self.gestionnaire.first_name) + ' ' + str(self.gestionnaire.last_name.upper()) + ' / ' + self.pcp_fe_groupe.groupe_fe_nom + ' / ' + self.pcp_fe_categorie
class Meta:
verbose_name = 'Lien gest PCP / groupe FE'
verbose_name_plural = 'Liens gest PCP / groupe FE'
class FormationEmploi(models.Model):
"""
Modèle des formations d'emplois
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'fe_code'
LIBELLE = 'fe_libelle'
ZONE_DEFENSE = 'zone_defense'
REL_MERE = 'mere'
REL_POSTE = 'poste'
M2M_GESTIONNAIRE = 'gestionnaires'
fe_code = models.CharField(primary_key=True, max_length=100)
groupe_fe = models.ForeignKey(GroupeFe, blank=True, on_delete=models.SET_NULL, null=True)
gestionnaires = models.ManyToManyField(CustomUser, related_name=CustomUser.Cols.M2M_FORMATION_EMPLOIS, blank=True)
fe_code_postal = models.CharField(max_length=100, null=True, blank=True)
fe_garnison_lieu = models.CharField(max_length=100, null=True, blank=True)
fe_libelle = models.CharField(max_length=100, null=True, blank=True)
fe_taux_armement_cible_off = models.FloatField(null=True, blank=True, verbose_name="TA cible OFF")
fe_nb_poste_reo_off = models.IntegerField(null=True, blank=True)
fe_nb_poste_reevalue_off = models.IntegerField(null=True, blank=True)
fe_nb_poste_vacant_off = models.IntegerField(null=True, blank=True)
fe_nb_poste_occupe_off = models.IntegerField(null=True, blank=True)
fe_taux_armement_cible_soff = models.FloatField(null=True, blank=True, verbose_name="TA cible SOFF")
fe_nb_poste_reo_soff = models.IntegerField(null=True, blank=True)
fe_nb_poste_reevalue_soff = models.IntegerField(null=True, blank=True)
fe_nb_poste_vacant_soff = models.IntegerField(null=True, blank=True)
fe_nb_poste_occupe_soff = models.IntegerField(null=True, blank=True)
fe_taux_armement_cible_mdr = models.FloatField(null=True, blank=True, verbose_name="TA cible MDR")
fe_nb_poste_reo_mdr = models.IntegerField(null=True, blank=True)
fe_nb_poste_reevalue_mdr = models.IntegerField(null=True, blank=True)
fe_nb_poste_vacant_mdr = models.IntegerField(null=True, blank=True)
fe_nb_poste_occupe_mdr = models.IntegerField(null=True, blank=True)
fe_mere_credo = models.CharField('Code FE mère', max_length=100, null=True, blank=True)
fe_mere_la = models.CharField('Libelle FE mère', max_length=100, null=True, blank=True)
fe_fot = models.CharField(max_length=100, null=True, blank=True)
fe_abo_fe = models.CharField(max_length=100, null=True, blank=True)
fe_pilier_niv1 = models.CharField(max_length=100, null=True, blank=True)
fe_code_niv_org4 = models.CharField('Code niveau d\'org 4', max_length=100, null=True, blank=True)
fe_niv_org4 = models.CharField('Libelle niveau d\'org 4', max_length=100, null=True, blank=True)
fe_code_niv_org4_mdr = models.CharField('Code niveau d\'org 4 (MDR)', max_length=100, null=True, blank=True)
fe_niv_org4_mdr = models.CharField('Libelle niveau d\'org 4 (MDR)', max_length=100, null=True, blank=True)
zone_defense = models.CharField('zone de défense', db_column='fe_zone_defense', max_length=64, blank=True, null=True)
mere = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='FE mère', db_column='fe_mere_id')
def as_dict(self):
return {
"id": self.fe_code,
"code": self.fe_code,
"libelle": self.fe_libelle,
"mere_code": self.fe_mere_credo,
"mere_la": self.fe_mere_la,
"groupe_fe": self.groupe_fe_id,
"fe_garnison_lieu": self.fe_garnison_lieu,
"zone_defense": self.zone_defense
}

View File

@@ -0,0 +1,21 @@
from django.db import models
class Garnison(models.Model):
"""
Modèle des garnisons
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'gar_id'
LIEU = 'gar_lieu'
CODE_POSTAL = 'gar_code_postal'
gar_id = models.CharField(max_length=100, primary_key=True)
gar_lieu = models.CharField(max_length=100, verbose_name="Garnison")
gar_code_postal = models.CharField(max_length=100, null=True, blank=True)
def __str__(self):
return self.gar_lieu

View File

@@ -0,0 +1,21 @@
from django.db import models
# FIXME le code grade et libellé sont inversés
class Grade(models.Model):
"""
Modèle des grades
"""
gr_code = models.CharField(max_length=100, primary_key=True)
gr_categorie = models.CharField(max_length=100, null=True, blank=True)
gr_ordre = models.IntegerField(null=True, blank=True)
def as_dict(self):
return {
"id": self.gr_code,
"code": self.gr_code,
# TODO corriger ci-dessous : catégorie (ex : "Sous-officiers"), libellé long (ex : "SERGENT CHEF") + ordre
"categorie": self.gr_categorie,
"ordre": self.gr_ordre
}

View File

@@ -0,0 +1,231 @@
"""
Ce dossier contient tous les modèles de la base de données d'OGURE
"""
from bulk_update_or_create import BulkUpdateOrCreateQuerySet
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .administre import Administre, Administres_Pams
from .commun import NiveauFonctionnelChoices, SpecifiqueChoices
from .competence import Competence
from .domaine import Domaine
from .filiere import Filiere
from .garnison import Garnison
from .grade import Grade
from .poste import Poste, Postes_Pams
from .sous_vivier import SousVivier
from .pam import PAM
"""
Script de création des diffferentes tables de la base
OGURE NG utilisation de l'ORM de Django pour réaliser cette étape
"""
# TODO: supprimer ce modèle et le remplacer par le nouveau modèle des Sous-viviers
# Modèle de l'association des sous-viviers à la catégorie et la filière
class SousVivierAssociation(models.Model):
"""Modèle de l'association des sous-viviers à la catégorie et la filière
"""
CATEGORIE_CHOICES = [('MDR', 'MDR'), ('SOFF', 'SOFF'), ('OFF', 'OFF'), ('OGX', 'OGX')]
sva_id = models.IntegerField(primary_key=True)
sous_vivier = models.ForeignKey(SousVivier, on_delete=models.SET_NULL, null=True, blank=True)
filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True, blank=True)
sva_categorie = models.CharField(max_length=5, choices=CATEGORIE_CHOICES)
sva_arme = models.CharField(max_length=20, null=True, blank=True)
# Modèle des Groupes de marques
class MarquesGroupe(models.Model):
"""Modèle des Groupes de marques
"""
gm_id = models.IntegerField(primary_key=True)
gm_type = models.CharField(max_length=100)
gm_code = models.CharField(max_length=100)
gm_libelle = models.CharField(max_length=100)
gm_ordre = models.IntegerField(null=True, blank=True)
gm_selection_multiple = models.BooleanField(null=True, blank=True)
def as_dict(self):
return {
"id": self.gm_id,
"type": self.gm_type,
"code": self.gm_code,
"libelle": self.gm_libelle,
"selectionMultiple": self.gm_selection_multiple,
"ordre": self.gm_ordre
}
# Modèle des Marques
class Marque(models.Model):
"""Modèle des Marques
"""
mar_id = models.TextField(primary_key=True)
groupe_marques = models.ForeignKey(MarquesGroupe, on_delete=models.CASCADE)
mar_code = models.CharField(max_length=100)
mar_libelle = models.CharField(max_length=100)
mar_ordre = models.IntegerField(null=True, blank=True)
def as_dict(self):
return {
"id": self.mar_id,
"groupeMarquesId": self.groupe_marques_id,
"code": self.mar_code,
"libelle": self.mar_libelle,
"ordre": self.mar_ordre
}
class ZoneGeographique(models.Model):
"""Modèle des Zones Geographiques
"""
zone_id = models.CharField(max_length=100, primary_key=True)
zone_libelle = models.CharField(max_length=100)
def as_dict(self):
return {
"id": self.zone_id,
"libelle": self.zone_libelle
}
# Modèle Liste de Préference
class PreferencesListe(models.Model):
"""Modèle Liste de Préference
"""
lp_id = models.IntegerField(primary_key=True)
administre_pam = models.ForeignKey(Administres_Pams, on_delete=models.CASCADE, related_name= 'administre_pam_pref', default="")
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, related_name= 'administre_pref', default="")
pam = models.ForeignKey(PAM, on_delete=models.CASCADE, default="")
poste = models.ForeignKey(Poste, on_delete=models.CASCADE)
lp_rang_poste = models.IntegerField()
# Modèle pour les notations
class Notation(models.Model):
"""Modèle pour les notations
"""
no_id = models.AutoField(primary_key=True)
administre = models.ForeignKey(Administre, on_delete=models.CASCADE)
poste = models.ForeignKey(Poste, on_delete=models.CASCADE)
administre_pam = models.ForeignKey(Administres_Pams, on_delete=models.CASCADE, default="", related_name='administre_pam')
poste_pam = models.ForeignKey(Postes_Pams, on_delete=models.CASCADE, default="", related_name='poste_pam')
pam = models.ForeignKey(PAM, on_delete=models.SET_NULL, null=True, blank=True)
no_rang_administre = models.IntegerField(null=True, blank=True)
no_rang_poste = models.IntegerField(null=True, blank=True)
no_date_execution = models.DateTimeField(auto_now=True)
no_score_administre = models.FloatField(null=True, blank=True)
no_score_poste = models.FloatField(null=True, blank=True)
no_flag_cple_ideal = models.BooleanField(default=False)
class Administre_Notation(models.Model):
"""Modèle le lien entre Notation et Administre
"""
id = models.CharField(max_length=100, primary_key=True)
administre = models.ForeignKey(Administre, on_delete=models.CASCADE, null=True, blank=True, related_name='adm_not', db_constraint=False)
no_annne_de_notation = models.CharField(max_length=100, null=True, blank=True)
no_nr_ou_iris = models.CharField(max_length=100, null=True, blank=True)
no_rac_ou_iris_cumule = models.CharField(max_length=100, null=True, blank=True)
no_rf_qsr = models.CharField(max_length=100, null=True, blank=True)
no_aptitude_emploie_sup = models.CharField(max_length=100, null=True, blank=True)
no_potentiel_responsabilite_sup = models.CharField(max_length=100, null=True, blank=True)
no_age_annees = models.CharField(max_length=100, null=True, blank=True)
# Modèle du référentiel organique
class RefOrg(models.Model):
"""Modèle de référentiel organique"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'ref_org_code'
# relations one-to-one ou many-to-one
REL_REF_GEST = 'ref_gest'
# TODO: ajouter la condition d'unicité des lignes (utiliser class Meta)
ref_org_code = models.CharField('Code niveau d\'org', max_length=100, primary_key=True)
ref_org_code_niv_org1 = models.CharField('Code niveau d\'org 1', max_length=100, null=True, blank=True)
ref_org_lib_niv_org1 = models.CharField('Livelle niveau d\'org 1', max_length=100, null=True, blank=True)
ref_org_code_niv_org2 = models.CharField('Code niveau d\'org 2', max_length=100, null=True, blank=True)
ref_org_lib_niv_org2 = models.CharField('Libelle niveau d\'org 2', max_length=100, null=True, blank=True)
ref_org_code_niv_org3 = models.CharField('Code niveau d\'org 3', max_length=100, null=True, blank=True)
ref_org_lib_niv_org3 = models.CharField('Libelle niveau d\'org 3', max_length=100, null=True, blank=True)
ref_org_code_niv_org4 = models.CharField('Code niveau d\'org 4', max_length=100, null=True, blank=True)
ref_org_lib_niv_org4 = models.CharField('Libell niveau d\'org 4', max_length=100, null=True, blank=True)
ref_org_niv_org = models.IntegerField('Niveau d\'org', null=True, blank=True)
ref_org_ref_fe = models.BooleanField('Pameur BMOB', default=False, null=True)
ref_org_ref_sv_fil = models.BooleanField('Gestionnaire BGCAT', default=False, null=True)
ref_org_droit_lect = models.BooleanField('Droit de lecture', default=False, null=True)
ref_org_droit_ecr = models.BooleanField("Droit d'écriture", default=False, null=True)
ref_org_expert_hme = models.BooleanField('Expert HME', default=False, null=True)
ref_org_bvt = models.BooleanField('Gestionnaire BVT', default=False, null=True)
ref_org_itd = models.BooleanField('Gestionnaire ITD', default=False, null=True)
class Meta:
verbose_name = 'Référentiel organique'
verbose_name_plural = 'Référentiels organiques'
# Modèle du référentiel gestionnaire
class RefGest(models.Model):
"""Modèle de référentiel gestionnaire"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'ref_gest_sap'
# relations one-to-one ou many-to-one
REL_ORG = 'ref_gest_org'
ref_gest_sap = models.IntegerField('Id SAP', primary_key=True)
ref_gest_username = models.CharField('Nom d\'utilisateur', max_length=100, null=True, blank=True)
ref_gest_email = models.CharField('Adresse mail', max_length=100, null=True, blank=True)
ref_gest_first_name = models.CharField('Prénom', max_length=100, null=True, blank=True)
ref_gest_last_name = models.CharField('Nom', max_length=100, null=True, blank=True)
ref_gest_grade = models.CharField('Grade', max_length=100, null=True, blank=True)
ref_gest_niv_org = models.CharField('Niveau d\'org', max_length=100, null=True, blank=True)
ref_gest_org = models.ForeignKey(RefOrg, verbose_name='Référentiel organique', related_name=RefOrg.Cols.REL_REF_GEST, on_delete=models.CASCADE,
db_column='ref_gest_org_id', null=True, blank=True)
class Meta:
verbose_name = 'Référentiel gestionnaire'
verbose_name_plural = 'Référentiels gestionnaires'
# Modèle du référentiel sous-vivier filière
class RefSvFil(models.Model):
"""Modèle de référentiel sous-vivier filière
"""
class Cols():
""" Constantes pour les noms de colonnes """
# relations one-to-one ou many-to-one
REL_SOUS_VIVIER = 'sous_vivier'
ref_sv_fil_code = models.CharField('Code niveau d\'org', max_length=100, null=True, blank=True)
ref_sv_fil_dom_gest = models.CharField('Domaine de gestion', max_length=100, null=True, blank=True)
ref_sv_fil_dom = models.CharField('Domaine', max_length=100, null=True, blank=True)
ref_sv_fil_fil = models.CharField('Filière', max_length=100, null=True, blank=True)
ref_sv_fil_cat = models.CharField('Catégorie', max_length=100, null=True, blank=True)
sous_vivier = models.ForeignKey(SousVivier, verbose_name='Sous-vivier', on_delete=models.SET_NULL, null=True, blank=True)
def as_dict(self):
return {
"ref_sv_fil_code": self.ref_sv_fil_code,
"ref_sv_fil_dom_gest": self.ref_sv_fil_dom_gest,
"ref_sv_fil_dom": self.ref_sv_fil_dom,
"ref_sv_fil_fil": self.ref_sv_fil_fil,
"ref_sv_fil_cat": self.ref_sv_fil_cat,
}
class Meta:
verbose_name = 'Référentiel sous-vivier/filière'
verbose_name_plural = 'Référentiels sous-viviers/filières'

View File

@@ -0,0 +1,18 @@
from django.db import models
class PAM(models.Model):
"""Modèle pour Le PAM
"""
pam_id = models.CharField(primary_key=True,max_length=100, default=False)
pam_date = models.CharField(max_length=100)
pam_libelle = models.CharField(max_length=100)
pam_statut = models.CharField(max_length=100)
def as_dict(self):
return {
"pam_id": self.pam_id,
"pam_libelle": self.pam_libelle,
"pam_statut": self.pam_statut,
}

View File

@@ -0,0 +1,189 @@
from bulk_update_or_create import BulkUpdateOrCreateQuerySet
from django.db import models
from .administre import Administre
from .commun import NiveauFonctionnelChoices, SpecifiqueChoices
from .competence import Competence
from .domaine import Domaine
from .filiere import Filiere
from .fonction import Fonction
from .formation_emploi import FormationEmploi
from .sous_vivier import SousVivier
from .pam import PAM
class AvisPosteChoices(models.TextChoices):
"""
[Poste] choix pour les avis
attributs supplémentaires :
- calc_enabled : est-ce que cet avis permet le calcul ?
- dec_enabled : est-ce que cet avis permet de créer une décision ?
"""
# (valeur, calc_enabled, dec_enabled), libellé
P1 = ('P1', True, True), 'Priorité 1'
P2 = ('P2', True, True), 'Priorité 2'
P3 = ('P3', True, True), 'Priorité 3'
P4 = ('P4', True, True), 'Priorité 4'
GELE = ('GELE', False, False), 'Gelé'
NON_ETUDIE = ('NON_ETUDIE', False, False), 'Non étudié'
def __new__(cls, value):
obj = str.__new__(cls, value[0])
obj._value_ = value[0]
obj.calc_enabled = value[1]
obj.dec_enabled = value[2]
return obj
class DirectCommissionneChoices(models.TextChoices):
"""
[Poste] choix pour direct/commissionné
"""
DIRECT = 'DIRECT', 'Direct'
COMMISSIONNE = 'COMMISSIONNE', 'Commissionné'
class PropositionsArmementChoices(models.TextChoices):
"""
[Poste] choix pour les propositions d'armement
"""
PROPOSE = 'PROPOSE', 'Propositions'
VALIDE = 'VALIDE', 'Propositions validées'
class Poste(models.Model):
"""
Modèle des postes
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'p_id'
CATEGORIE = 'p_categorie'
FONCTION = 'p_code_fonction'
NIVEAU_FONCTIONNEL = 'p_nf'
# relations many-to-many
M2M_COMPETENCES = 'competences'
M2M_SOUS_VIVIERS = 'sous_viviers'
M2M_PAM = 'p_pam'
# relations one-to-many
O2M_DECISION = 'decisions'
# relations one-to-one ou many-to-one
REL_ADMINISTRE = 'p_administre'
REL_DOMAINE = 'p_domaine'
REL_FILIERE = 'p_filiere'
REL_FONCTION = 'fonction'
REL_FORMATION_EMPLOI = 'formation_emploi'
objects = BulkUpdateOrCreateQuerySet.as_manager()
p_id = models.CharField(max_length=100, primary_key=True)
p_pam = models.ManyToManyField(PAM, through='Postes_Pams')
p_annee = models.CharField(max_length=100, default=False)
p_administre = models.ForeignKey(Administre, on_delete=models.SET_NULL, null=True, blank=True)
fonction = models.ForeignKey(Fonction, on_delete=models.SET_NULL, null=True, blank=True)
sous_viviers = models.ManyToManyField(SousVivier, related_name=SousVivier.Cols.M2M_POSTES, blank=True)
formation_emploi = models.ForeignKey(FormationEmploi, on_delete=models.SET_NULL, related_name=FormationEmploi.Cols.REL_POSTE, null=True, blank=True)
competences = models.ManyToManyField(Competence, blank=True)
p_domaine = models.ForeignKey(Domaine, on_delete=models.SET_NULL, null=True, blank=True)
p_filiere = models.ForeignKey(Filiere, on_delete=models.SET_NULL, null=True, blank=True)
p_fonction = models.CharField(max_length=100, null=True, blank=True)
p_code_fonction = models.CharField(max_length=100, null=True, blank=True)
p_nf = models.CharField(max_length=100, null=True, blank=True, choices=NiveauFonctionnelChoices.choices)
p_categorie = models.CharField(max_length=100, null=True, blank=True)
p_dep = models.CharField('Département', max_length=2, null=True, blank=True)
p_liste_id_marques = models.CharField('Marques PAM en cours', max_length=100, null=True, blank=True)
p_eip = models.CharField(max_length=100, null=True, blank=True)
p_avis = models.CharField(max_length=100, choices=AvisPosteChoices.choices, null=True, default=AvisPosteChoices.NON_ETUDIE)
p_avis_fe = models.CharField(max_length=100, choices=AvisPosteChoices.choices, null=True, default=AvisPosteChoices.NON_ETUDIE)
p_notes_gestionnaire = models.TextField(null=True, blank=True)
p_notes_partagees = models.TextField(null=True, blank=True)
p_ciat = models.BooleanField('CIAT', default=False, null=True)
p_specifique = models.CharField('PPE / SHM / ITD', max_length=250, choices=SpecifiqueChoices.choices, null=True, blank=True)
p_direct_commissionne = models.CharField('Direct Commissionne', max_length=100, choices=DirectCommissionneChoices.choices, null=True, blank=True)
propositions_armement = models.CharField("propositions d'armement", db_column='p_propositions_armement', max_length=10, choices=PropositionsArmementChoices.choices, null=True, blank=True)
p_priorisation_pcp = models.TextField('Priorisation PCP', null=True, blank=True)
p_nfs = models.CharField('Domaine de gestion (BVT)', max_length=100, null=True, blank=True)
p_itd_cellule = models.CharField(max_length=100, null=True, blank=True)
p_itd_affecte = models.BooleanField(null=True, blank=True)
@property
def p_poids_competences(self):
"le poids des competences"
if self.p_specifique:
return 65 if self.competences.exists() else 0
else:
return 60 if self.competences.exists() else 0
@property
def p_poids_filiere(self):
"le poids des filieres"
if self.p_specifique:
return 0 if self.competences.exists() else 20
else:
return 10 if self.competences.exists() else 75
@property
def p_poids_nf(self):
"le poids des niveaux fonctionnels"
if self.p_specifique:
return 35 if self.competences.exists() else 80
else:
return 30 if self.competences.exists() else 25
@property
def p_poids_filiere_nf_competences(self):
"le poids des filieres, nf et competences"
if self.p_specifique:
return (0, 35, 65) if self.competences.exists() else (20, 80, 0)
else:
return (10, 30, 60) if self.competences.exists() else (75, 25, 0)
class Meta:
constraints = [
models.CheckConstraint(
name='%(app_label)s_%(class)s_p_direct_commissionne_valid',
check=models.Q(p_direct_commissionne__in=DirectCommissionneChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_p_nf_valid',
check=models.Q(p_nf__in=NiveauFonctionnelChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_p_specifique_valid',
check=models.Q(p_specifique__in=SpecifiqueChoices.values)
),
models.CheckConstraint(
name='%(app_label)s_%(class)s_propositions_armement_valid',
check=models.Q(propositions_armement__in=PropositionsArmementChoices.values)
),
]
class Postes_Pams(models.Model):
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'id'
REL_PAM = 'p_pam'
REL_POSTE = 'poste'
O2M_DECISION = 'decisions'
objects = BulkUpdateOrCreateQuerySet.as_manager()
id = models.CharField(primary_key=True, max_length=100)
p_pam = models.ForeignKey(PAM, on_delete = models.CASCADE, related_name='p_pam')
poste = models.ForeignKey(Poste, on_delete=models.CASCADE, related_name='poste')
p_avis_pam = models.CharField(max_length=100, choices=AvisPosteChoices.choices, null=True, default=AvisPosteChoices.NON_ETUDIE)
p_avis_fe_pam = models.CharField(max_length=100, choices=AvisPosteChoices.choices, null=True, default=AvisPosteChoices.NON_ETUDIE)
p_direct_commissionne_pam = models.CharField('Direct Commissionne', max_length=100, choices=DirectCommissionneChoices.choices, null=True, blank=True)
p_notes_gestionnaire_pam = models.TextField(null=True, blank=True)
p_priorisation_pcp_pam = models.TextField('Priorisation PCP', null=True, blank=True)
info_reo = models.CharField(max_length=100, default="")

View File

@@ -0,0 +1,39 @@
from django.conf import settings
from django.db import models
from .user import CustomUser
class SousVivier(models.Model):
"""
Modèle des sous-viviers de militaires et de postes
"""
class Cols():
""" Constantes pour les noms de colonnes """
PK = 'sv_id'
LIBELLE = 'sv_libelle'
DOMAINE = 'sv_dom'
FILIERE = 'sv_fil'
CATEGORIE = 'sv_cat'
# relations many-to-many
M2M_GESTIONNAIRES = 'gestionnaires'
M2M_POSTES = 'poste'
sv_id = models.CharField(max_length=100, primary_key=True)
sv_libelle = models.CharField(max_length=100)
gestionnaires = models.ManyToManyField(CustomUser, related_name=CustomUser.Cols.M2M_SOUS_VIVIERS, blank=True)
sv_dom = models.CharField(max_length=100, null=True, blank=True)
sv_fil = models.CharField(max_length=100, null=True, blank=True)
sv_cat = models.CharField(max_length=100, null=True, blank=True)
def as_dict(self):
mails = []
for i in self.gestionnaires.all() :
mails.append(i.email)
return {
"sv_id": self.sv_id,
"sv_libelle": self.sv_libelle,
"gestionnaires": mails,
}

View File

@@ -0,0 +1,36 @@
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models
from django.utils import timezone
class CustomUser(AbstractUser):
"""
Modèle de l'utilisateur de l'application
"""
class Cols():
""" Constantes pour les noms de colonnes """
# relations one-to-many
O2M_GROUPE_FE_PCP = 'pcpfegroupe_set'
O2M_SOUS_VIVIER = 'sousvivier_set'
M2M_SOUS_VIVIERS = 'sous_vivier'
# relations many-to-many
M2M_FORMATION_EMPLOIS = 'formation_emploi'
# id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
id = models.IntegerField(auto_created=True, primary_key=True, verbose_name='Id SAP')
password = models.CharField(max_length=128, verbose_name='password')
last_login = models.DateTimeField(blank=True, null=True, verbose_name='last login')
is_superuser = models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')
username = models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[UnicodeUsernameValidator()], verbose_name='username')
first_name = models.CharField(null=True, blank=True, max_length=150, verbose_name='first name')
last_name = models.CharField(null=True, blank=True, max_length=150, verbose_name='last name')
email = models.EmailField(null=True, blank=True, max_length=254, verbose_name='email address')
is_staff = models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')
is_active = models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')
date_joined = models.DateTimeField(default=timezone.now, verbose_name='date joined')
grade = models.CharField(blank=True, max_length=100, null=True)
REQUIRED_FIELDS = ['id']

View File

@@ -0,0 +1,13 @@
"""Ce fichier permet de modifier la façon dont les grands ensembles de résultats sont divisés en plusieurs pages de données
"""
from rest_framework import pagination
class HeavyDataPagination(pagination.PageNumberPagination):
"""Cette classe modifie des aspects particuliers du style de pagination, elle remplace les classes de pagination et définit les attributs à modifier.
"""
page_size = 2
page_size_query_param = 'page_size'
max_page_size = 10000
page_query_param = 'page'

View File

@@ -0,0 +1,327 @@
"""Ce fichier contient les utilitaires du reporting"""
# import des pré requis
from ast import For
import json
from datetime import date
from logging import getLevelName
import time
import pandas as pd
from django.utils import timezone
from django.db.models import Sum
from .models import Administre, Domaine, FMOB, Fonction, FormationEmploi, Garnison, Grade, Poste, Notation, \
PreferencesListe, Marque, MarquesGroupe, Filiere, Decision, SousVivier, SousVivierAssociation, \
AvisPosteChoices as AvisPoste
from .utils_insertion import insert_Notation
import numpy as np
def reporting_taux_armement_pcp(fe_id, f_id, d_id, nf, categorie):
"""
Renvoie les indicateurs figurant dans la vue Taux Armement FE pour les PCP
:type fe_id: list
:param fe_id: codes CREDO des FE sur lesquelles on filtre
:type f_id: list
:param f_id: trigrammes des filières sur lesquelles on filtre
:type d_id: list
:param d_id: trigrammes des domaines sur lesquels on filtre
:type nf: list
:param nf: niveau fonctionnel sur lesquels on filtre
:type categorie: list
:param categorie: catégorie sur lesquelles on filtre
:return: - **nb_militaires_actuel** (*int*): Nombre de militaires présents dans la/les FE selon les filtres cochés.
- **nb_postes_actuel** (*int*): Nombre de postes présents dans la/les FE selon les filtres cochés.
- **ecart_actuel** (*int*): Différence entre le nombre de postes et de militaires présents actuellement.
- **taux_armement_actuel** (*int*): Taux d'armement de la/les FE selon les filtres cochés.
- **nb_militaires_entrants** (*int*): Nombre de militaires entrants dans la FE selon les filtres cochés.
- **nb_militaires_sortants** (*int*): Nombre de militaires sortants de la FE selon les filtres cochés.
- **nb_militaires_projete** (*int*): Nombre de militaires attendus dans la FE après les mobilités selon les filtres cochés.
- **taux_armement_projete** (*int*): Taux d'armement projete selon les filtres cochés.
- **taux_armement_cible** (*int*): Taux d'armement cible pour la FE choisie selon la catégorie choisie, None si plusieurs catégories ont été sélectionnées.
"""
taux_armement_cible = None
if len(fe_id) == 1 and len(categorie) == 1:
fe = FormationEmploi.objects.get(fe_code__in=fe_id)
if 'MDR' in categorie:
taux_armement_cible = fe.fe_taux_armement_cible_mdr
if 'OFF' in categorie:
taux_armement_cible = fe.fe_taux_armement_cible_off
if 'SOFF' in categorie:
taux_armement_cible = fe.fe_taux_armement_cible_soff
nb_militaires_actuel = Administre.objects.filter(formation_emploi_id__in=fe_id, a_domaine__in=d_id, a_filiere__in=f_id, a_nf__in=nf, a_categorie__in=categorie).count() or 0
nb_militaires_sortants = Administre.objects.filter(a_domaine_id__in=d_id, a_filiere_id__in=f_id, a_nf__in=nf, a_categorie__in=categorie, formation_emploi_id__in=fe_id, a_statut_pam__in=['A_MUTER', 'NON_DISPONIBLE', 'PARTANT']).count() or 0
nb_militaires_entrants = Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__formation_emploi_id__in=fe_id).count() or 0
nb_militaires_projete = nb_militaires_actuel + nb_militaires_entrants - nb_militaires_sortants
nb_postes_actuel = Poste.objects.filter(formation_emploi_id__in=fe_id, p_filiere__in=f_id, p_domaine__in=d_id, p_nf__in=nf, p_categorie__in=categorie).count() or 0
if nb_postes_actuel == 0:
nb_postes_actuel = 0
taux_armement_actuel = 0
taux_armement_projete = 0
else:
ecart_actuel = nb_postes_actuel - nb_militaires_actuel
ecart_projete = nb_postes_actuel - nb_militaires_projete
taux_armement_actuel = nb_militaires_actuel / nb_postes_actuel * 100
taux_armement_projete = nb_militaires_projete / nb_postes_actuel * 100
ecart_actuel = nb_postes_actuel - nb_militaires_actuel
ecart_projete = nb_postes_actuel - nb_militaires_projete
return nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible
def reporting_taux_armement_gestionnaire(fe_id, f_id, d_id, nf, categorie, sv_id):
"""
Renvoie les indicateurs figurant dans la vue Taux Armement FE pour les gestionnaires
:type fe_id: list
:param fe_id: codes CREDO des FE sur lesquelles on filtre
:type f_id: list
:param f_id: trigrammes des filières sur lesquelles on filtre
:type d_id: list
:param d_id: trigrammes des domaines sur lesquels on filtre
:type nf: list
:param nf: niveau fonctionnel sur lesquels on filtre
:type sv_id: chaine de caractères
:param categorie: sous-vivier du gestionnaire
:type categorie: list
:param categorie: catégorie sur lesquelles on filtre
:return: - **nb_militaires_actuel** (*int*): Nombre de militaires présents dans la/les FE selon les filtres cochés.
- **nb_postes_actuel** (*int*): Nombre de postes présents dans la/les FE selon les filtres cochés.
- **ecart_actuel** (*int*): Différence entre le nombre de postes et de militaires présents actuellement.
- **taux_armement_actuel** (*int*): Taux d'armement de la/les FE selon les filtres cochés.
- **nb_militaires_entrants** (*int*): Nombre de militaires entrants dans la FE selon les filtres cochés.
- **nb_militaires_sortants** (*int*): Nombre de militaires sortants de la FE selon les filtres cochés.
- **nb_militaires_projete** (*int*): Nombre de militaires attendus dans la FE après les mobilités selon les filtres cochés.
- **taux_armement_projete** (*int*): Taux d'armement projete selon les filtres cochés.
- **taux_armement_cible** (*int*): Taux d'armement cible pour la FE choisie selon la catégorie choisie, None si plusieurs catégories ont été sélectionnées.
"""
taux_armement_cible = None
if len(fe_id) == 1 and len(categorie) == 1:
fe = FormationEmploi.objects.get(fe_code__in=fe_id)
if 'MDR' in categorie:
taux_armement_cible = fe.fe_taux_armement_cible_mdr
if 'OFF' in categorie:
taux_armement_cible = fe.fe_taux_armement_cible_off
if 'SOFF' in categorie:
taux_armement_cible = fe.fe_taux_armement_cible_soff
nb_militaires_actuel = Administre.objects.filter(sous_vivier_id=sv_id, formation_emploi_id__in=fe_id, a_domaine__in=d_id, a_filiere__in=f_id, a_nf__in=nf, a_categorie__in=categorie).count() or 0
nb_militaires_sortants = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_id__in=d_id, a_filiere_id__in=f_id, a_nf__in=nf, a_categorie__in=categorie, formation_emploi_id__in=fe_id, a_statut_pam__in=['A_MUTER', 'NON_DISPONIBLE', 'PARTANT']).count() or 0
nb_militaires_entrants = Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__formation_emploi_id__in=fe_id).count() or 0
nb_militaires_projete = nb_militaires_actuel + nb_militaires_entrants - nb_militaires_sortants
nb_postes_actuel = Poste.objects.filter(sous_viviers=sv_id, formation_emploi_id__in=fe_id, p_filiere__in=f_id, p_domaine__in=d_id, p_nf__in=nf, p_categorie__in=categorie).count() or 0
if nb_postes_actuel == 0:
nb_postes_actuel = 0
taux_armement_actuel = 0
taux_armement_projete = 0
else:
ecart_actuel = nb_postes_actuel - nb_militaires_actuel
ecart_projete = nb_postes_actuel - nb_militaires_projete
taux_armement_actuel = nb_militaires_actuel / nb_postes_actuel * 100
taux_armement_projete = nb_militaires_projete / nb_postes_actuel * 100
ecart_actuel = nb_postes_actuel - nb_militaires_actuel
ecart_projete = nb_postes_actuel - nb_militaires_projete
return nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible
def reporting_suivi_pam_admin(sv_id, f_id, d_id, nf, categorie):
"""
Renvoie les indicateurs figurant dans la vue Suivi PAM gestionnaire pour les administrés
:type f_id: list
:param f_id: trigrammes des filières sur lesquelles on filtre
:type d_id: list
:param d_id: trigrammes des domaines sur lesquels on filtre
:type nf: list
:param nf: niveau fonctionnel sur lesquels on filtre
:type sv_id: chaine de caractères
:param categorie: sous-vivier du gestionnaire
:type categorie: list
:param categorie: catégorie sur lesquelles on filtre
:return: - **nb_a_etudier** (*int*): Nombre de militaires à étudier selon les filtres cochés.
- **nb_a_muter** (*int*): Nombre de militaires à muter selon les filtres cochés.
- **nb_a_maintenir** (*int*): Nombre de militaires à maintenir selon les filtres cochés.
- **nb_non_etudie_administres** (*int*): Nombre de militaires non étudiés selon les filtres cochés.
- **nb_a_partant** (*int*): Nombre de militaires partant selon les filtres cochés.
- **nb_a_non_dispo** (*int*): Nombre de militaires non disponibles selon les filtres cochés.
- **nb_prepos_administres** (*int*): Nombre de militaires prépositionnés selon les filtres cochés.
- **nb_pos_administres** (*int*): Nombre de militaires positionnés selon les filtres cochés.
- **nb_omi_active_administres** (*int*): Nombre de militaires avec un OMI activé selon les filtres cochés.
- **nb_omi_en_cours_administres** (*int*): Nombre de militaires avec un OMI en cours selon les filtres cochés.
- **reste_a_realiser_administres** (*int*): Nombre de militaires à muter ou à étudier pour lesquels aucune décision n'a été prise.
- **reste_a_realiser_a_etudier** (*int*): Nombre de militaires à étudier pour lesquels aucune décision n'a été prise.
- **reste_a_realiser_a_muter** (*int*): Nombre de militaires à muter pour lesquels aucune décision n'a été prise.
"""
nb_militaires_fe = Administre.objects.filter(a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie).count()
a_etudier = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie, a_statut_pam="A_ETUDIER")
a_muter = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie, a_statut_pam="A_MUTER")
a_maintenir = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie, a_statut_pam="A_MAINTENIR")
non_etudie_administres = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie, a_statut_pam="NON_ETUDIE")
a_partant = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie, a_statut_pam="PARTANT")
a_non_dispo = Administre.objects.filter(sous_vivier_id=sv_id, a_domaine_futur_id__in=d_id, a_filiere_futur_id__in=f_id, a_nf_futur__in=nf, a_categorie__in=categorie, a_statut_pam="NON_DISPONIBLE")
nb_a_etudier = a_etudier.count() or 0
nb_a_muter = a_muter.count() or 0
nb_a_maintenir = a_maintenir.count() or 0
nb_non_etudie_administres = non_etudie_administres.count() or 0
nb_a_partant = a_partant.count() or 0
nb_a_non_dispo = a_non_dispo.count() or 0
nb_prepos_administres = Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__sous_vivier_id=sv_id, administre__a_statut_pam__in=["A_ETUDIER", "A_MUTER"], de_decision='PREPOSITIONNE').count() or 0
nb_pos_administres = Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__sous_vivier_id=sv_id, administre__a_statut_pam__in=["A_ETUDIER", "A_MUTER"], de_decision='POSITIONNE').count() or 0
nb_omi_active_administres = Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__sous_vivier_id=sv_id, administre__a_statut_pam__in=["A_ETUDIER", "A_MUTER"], de_decision='OMI_ACTIVE').count() or 0
nb_omi_en_cours_administres = Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__sous_vivier_id=sv_id, administre__a_statut_pam__in=["A_ETUDIER", "A_MUTER"], de_decision='OMI_EN_COURS').count() or 0
reste_a_realiser_a_etudier = nb_a_etudier - Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__sous_vivier_id=sv_id, administre__a_statut_pam="A_ETUDIER").count() or 0
reste_a_realiser_a_muter = nb_a_muter - Decision.objects.filter(administre__a_domaine_futur_id__in=d_id, administre__a_filiere_futur_id__in=f_id, administre__a_nf_futur__in=nf, administre__a_categorie__in=categorie, administre__sous_vivier_id=sv_id, administre__a_statut_pam="A_MUTER").count() or 0
reste_a_realiser_administres = (nb_a_etudier + nb_a_muter) - (nb_prepos_administres + nb_pos_administres + nb_omi_en_cours_administres + nb_omi_active_administres)
return nb_a_etudier, nb_a_muter, nb_a_maintenir, nb_non_etudie_administres, nb_a_partant, nb_a_non_dispo, nb_prepos_administres, nb_pos_administres, nb_omi_active_administres, nb_omi_en_cours_administres, reste_a_realiser_administres, reste_a_realiser_a_etudier, reste_a_realiser_a_muter
def reporting_suivi_pam_poste(sv_id, f_id, d_id, nf, categorie):
"""
Renvoie les indicateurs figurant dans la vue Suivi PAM gestionnaire pour les postes
:type f_id: list
:param f_id: trigrammes des filières sur lesquelles on filtre
:type d_id: list
:param d_id: trigrammes des domaines sur lesquels on filtre
:type nf: list
:param nf: niveau fonctionnel sur lesquels on filtre
:type sv_id: chaine de caractères
:param categorie: sous-vivier du gestionnaire
:type categorie: list
:param categorie: catégorie sur lesquelles on filtre
:return: - **nb_p1** (*int*): Nombre de postes P1 selon les filtres cochés.
- **nb_p2** (*int*): Nombre de postes P2 selon les filtres cochés.
- **nb_p3** (*int*): Nombre de postes P3 selon les filtres cochés.
- **nb_p4** (*int*): Nombre de postes P4 selon les filtres cochés.
- **nb_gele** (*int*): Nombre de postes gelés selon les filtres cochés.
- **nb_non_etudie_postes** (*int*): Nombre de postes non étudiés selon les filtres cochés.
- **nb_prepos_postes** (*int*): Nombre de postes prépositionnés selon les filtres cochés.
- **nb_pos_postes** (*int*): Nombre de postes positionnés selon les filtres cochés.
- **nb_omi_active_postes** (*int*): Nombre de postes avec un OMI activé selon les filtres cochés.
- **nb_omi_en_cours_postes** (*int*): Nombre de postes avec un OMI en cours selon les filtres cochés.
- **reste_a_realiser_postes** (*int*): Nombre de postes de P1 à P4 pour lesquels aucune décision n'a été prise.
- **reste_a_realiser_p1** (*int*): Nombre de postes P1 pour lesquels aucune décision n'a été prise.
- **reste_a_realiser_p2** (*int*): Nombre de postes P2 pour lesquels aucune décision n'a été prise.
- **reste_a_realiser_p3** (*int*): Nombre de postes P3 pour lesquels aucune décision n'a été prise.
- **reste_a_realiser_p4** (*int*): Nombre de postes P4 pour lesquels aucune décision n'a été prise.
"""
nb_p1 = Poste.objects.filter(sous_viviers=sv_id, p_domaine__in=d_id, p_filiere__in=f_id, p_nf__in=nf, p_categorie__in=categorie, p_avis=AvisPoste.P1).count() or 0
nb_p2 = Poste.objects.filter(sous_viviers=sv_id, p_domaine__in=d_id, p_filiere__in=f_id, p_nf__in=nf, p_categorie__in=categorie, p_avis=AvisPoste.P2).count() or 0
nb_p3 = Poste.objects.filter(sous_viviers=sv_id, p_domaine__in=d_id, p_filiere__in=f_id, p_nf__in=nf, p_categorie__in=categorie, p_avis=AvisPoste.P3).count() or 0
nb_p4 = Poste.objects.filter(sous_viviers=sv_id, p_domaine__in=d_id, p_filiere__in=f_id, p_nf__in=nf, p_categorie__in=categorie, p_avis=AvisPoste.P4).count() or 0
nb_gele = Poste.objects.filter(sous_viviers=sv_id, p_domaine__in=d_id, p_filiere__in=f_id, p_nf__in=nf, p_categorie__in=categorie, p_avis=AvisPoste.GELE).count() or 0
nb_non_etudie_postes = Poste.objects.filter(sous_viviers=sv_id, p_domaine__in=d_id, p_filiere__in=f_id, p_nf__in=nf, p_categorie__in=categorie, p_avis=AvisPoste.NON_ETUDIE).count() or 0
nb_prepos_postes = Decision.objects.filter(poste__p_domaine__in=d_id, poste__p_filiere__in=f_id, poste__p_nf__in=nf, poste__p_categorie__in=categorie, poste__sous_viviers=sv_id, de_decision='PREPOSITIONNE').count() or 0
nb_pos_postes = Decision.objects.filter(poste__p_domaine__in=d_id, poste__p_filiere__in=f_id, poste__p_nf__in=nf, poste__p_categorie__in=categorie, poste__sous_viviers=sv_id, de_decision='POSITIONNE').count() or 0
nb_omi_active_postes = Decision.objects.filter(poste__p_domaine__in=d_id, poste__p_filiere__in=f_id, poste__p_nf__in=nf, poste__p_categorie__in=categorie, poste__sous_viviers=sv_id, de_decision='OMI_ACTIVE').count() or 0
nb_omi_en_cours_postes = Decision.objects.filter(poste__p_domaine__in=d_id, poste__p_filiere__in=f_id, poste__p_nf__in=nf, poste__p_categorie__in=categorie, poste__sous_viviers=sv_id, de_decision='OMI_EN_COURS').count() or 0
reste_a_realiser_p1 = 0
reste_a_realiser_p2 = 0
reste_a_realiser_p3 = 0
reste_a_realiser_p4 = 0
reste_a_realiser_postes = (nb_p1 + nb_p2 + nb_p3 + nb_p4) - (nb_prepos_postes + nb_pos_postes + nb_omi_active_postes + nb_omi_en_cours_postes)
if reste_a_realiser_postes > nb_p4:
reste_a_realiser_p4 = nb_p4
if reste_a_realiser_postes - nb_p4 > nb_p3:
reste_a_realiser_p3 = nb_p3
if reste_a_realiser_postes - nb_p4 - nb_p3 > nb_p2:
reste_a_realiser_p2 = nb_p2
if reste_a_realiser_postes - nb_p4 - nb_p3 - nb_p2 >= nb_p1:
reste_a_realiser_p1 = nb_p1
else:
reste_a_realiser_p1 = reste_a_realiser_postes - nb_p4 - nb_p3 - nb_p2
else:
reste_a_realiser_p2 = reste_a_realiser_postes - nb_p4 - nb_p3
reste_a_realiser_p1 = 0
else:
reste_a_realiser_p3 = reste_a_realiser_postes - nb_p4
reste_a_realiser_p2 = 0
reste_a_realiser_p1 = 0
else:
reste_a_realiser_p4 = nb_p4 - reste_a_realiser_postes
reste_a_realiser_p2 = 0
reste_a_realiser_p3 = 0
reste_a_realiser_p4 = 0
return nb_p1, nb_p2, nb_p3, nb_p4, nb_gele, nb_non_etudie_postes, reste_a_realiser_postes, reste_a_realiser_p1, reste_a_realiser_p2, reste_a_realiser_p3, reste_a_realiser_p4, nb_prepos_postes, nb_pos_postes, nb_omi_active_postes, nb_omi_en_cours_postes
# TODO : Supprimer cette fonction car non utilisée dans le code et utilise SousvivierAssociation
def poste_vacant_vivier(sv_id, fe_id):
"""
Renvoie le nombre de postes vacants dans un sous-vivier pour une FE sélectionnée
:type sv_id: chaine de caractères
:param sv_id: sous-vivier du gestionnaire
:type fe_id: chaine de caractères
:param fe_id: FE sur laquelle le gestionnaire a filtré
:return: - **1** (*int*): Nombre de postes vacants pour la FE sélectionnée.
"""
sva = SousVivierAssociation.objects.filter(sous_vivier_id=sv_id).first()
categorie = sva.sva_categorie
fe = FormationEmploi.objects.get(fe_code=fe_id)
if categorie == 'MDR':
nb_poste_reevalue_fe = fe.fe_nb_poste_reevalue_mdr or 0
nb_poste_vacant_fe = fe.fe_nb_poste_vacant_mdr or 0
if categorie == 'OFF':
nb_poste_reevalue_fe = fe.fe_nb_poste_reo_off or 0
nb_poste_vacant_fe = fe.fe_nb_poste_vacant_off or 0
if categorie == 'SOFF':
nb_poste_reevalue_fe = fe.fe_nb_poste_reevalue_soff or 0
nb_poste_vacant_fe = fe.fe_nb_poste_vacant_soff or 0
nb_poste_vivier_dans_fe = Poste.objects.filter(formation_emploi_id=fe_id, sous_viviers=sv_id).aggregate(Sum('p_id')) or 0
if nb_poste_reevalue_fe == 0:
return 0
else:
proportion = nb_poste_vivier_dans_fe / nb_poste_reevalue_fe
nb_poste_vacant_vivier = proportion * nb_poste_vacant_fe
if nb_poste_vacant_vivier > np.floor(nb_poste_vacant_vivier) + 0.5:
return np.ceil(nb_poste_vacant_vivier)
else:
return np.floor(nb_poste_vacant_vivier)

View File

@@ -0,0 +1,350 @@
"""Ce fichier contient des fonctions qui permettent de convertir des données complexes,
telles que des querysets et des instances de modèle, en types de données Python qui peuvent ensuite
être facilement rendus en JSON, XML ou d'autres types de contenu.
"""
from rest_framework import serializers
from .models import Competence, PcpFeGroupe, PreferencesListe, Poste, Notation, Administre, CustomUser, Grade, MarquesGroupe, \
Garnison, \
SousVivier, Domaine, Filiere, FMOB, Fonction, FormationEmploi, Marque, Decision
from . import constants
class FileSVSerializer(serializers.Serializer):
"""Cette classe ne sera utilisée que pour valider si le fichier SV est un vrai fichier.
"""
SV = serializers.FileField()
class Meta:
fields = ['sv_file']
class ScoringValidator(serializers.Serializer):
""" pour interprêter et valider le contenu JSON """
sous_vivier_id = serializers.CharField(max_length=100, required=True, allow_null=False, allow_blank=False)
class FileCompetenceSerializer(serializers.Serializer):
"""Cette classe ne sera utilisée que pour valider si les fichiers competence_v1 et competence_v2 sont un vrais fichier.
"""
competence_v1 = serializers.FileField()
competence_v2 = serializers.FileField()
class Meta:
fields = ['competence_file_v1', 'competence_file_v2']
class FileSerializer(serializers.Serializer):
"""Cette classe ne sera utilisée que pour valider si les fichiers Donnees_BO_ADT, REO, ZPROPAF, FMOB, domaine_filiere, insee_mapin et diplomes sont des vrais fichiers.
"""
Donnees_BO_ADT = serializers.FileField(help_text="Fichier BO ADT 1.1")
REO = serializers.FileField()
ZPROPAF = serializers.FileField()
FMOB = serializers.FileField()
domaine_filiere = serializers.FileField()
insee_maping = serializers.FileField()
diplomes = serializers.FileField()
class Meta:
fields = ['Donnees_BO_ADT', 'REO', 'ZPROPAF', 'FMOB', 'domaine_filiere', 'insee_maping', 'diplomes']
class AlimentationReferentielSerializer(serializers.Serializer):
""" Valide que les données sont bien des fichiers. """
referentiel_fe = serializers.FileField(help_text="Référentiel FE")
class Meta:
fields = ['referentiel_fe']
class NotationSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets Notation en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Notation
fields = '__all__'
class GestionnaireSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets customusers en type json contenant uniquement les informations mentionnées dans la variable fields.
"""
class Meta:
model = CustomUser
fields = ['grade', 'first_name', 'last_name', 'email']
class PreferencesListeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets preferencesliste en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = PreferencesListe
fields = '__all__'
class FmobSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets Fmob en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = FMOB
fields = '__all__'
class SimpleDecisionSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets decision en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Decision
fields = '__all__'
class AdministreDecisionSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets decisions en type json contenant les informations mentionnées dans la variable fields.
Et à l'ajout des variables poste_fe_code, poste_fe_libelle, poste_fe_garnison liés à la decision au json.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
poste_fe_code = serializers.CharField(source='poste.formation_emploi.fe_code', read_only=True)
poste_fe_libelle = serializers.CharField(source='poste.formation_emploi.fe_libelle', read_only=True)
poste_fe_garnison = serializers.CharField(source='poste.formation_emploi.garnison.gar_lieu', read_only=True)
class Meta:
model = Decision
fields = '__all__'
class MarquesGroupeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets marquegroupes en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = MarquesGroupe
fields = '__all__'
class MarqueSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets marque en type json contenant les informations de marque et les champs de marquesgroupe liés à chaque marque.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
groupe_marques = MarquesGroupeSerializer()
class Meta:
model = Marque
fields = '__all__'
class DomaineSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets domaines en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Domaine
fields = '__all__'
class FiliereSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets filières en type json contenant les informations de filière
et les champs du domaine liés à chaque filiere.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
domaine = DomaineSerializer()
class Meta:
model = Filiere
fields = '__all__'
class GradeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets grades en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Grade
fields = '__all__'
class GarnisonSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets garnisons en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Garnison
fields = '__all__'
class FormationEmploiSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets FormationEmploi en type json contenant les informations de FormationEmploi
et le champs garnison lieu liés à chaque FormationEmploi.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class MereSerializer(serializers.ModelSerializer):
"""Classe de représentation de la FE mère"""
fe_code = serializers.ReadOnlyField()
fe_libelle = serializers.ReadOnlyField()
class Meta:
model = FormationEmploi
fields = ['fe_code', 'fe_libelle']
garnison = serializers.CharField(source=f'{FormationEmploi.Cols.REL_GARNISON}.gar_lieu')
mere = MereSerializer(read_only=True)
class Meta:
model = FormationEmploi
fields = '__all__'
class FonctionSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets fonctions en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Fonction
fields = '__all__'
class SousVivierSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets SousVivier en type json contenant les informations du SousVivier
et la/le gestionnaire responsable de chaque SousVivier.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
gestionnaire = GestionnaireSerializer()
class Meta:
model = SousVivier
fields = '__all__'
class SousVivierAssociationSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la creation d'un json contenant les champs du SousVivier
et les champs de filière liés à chaque SousVivier.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
filiere = FiliereSerializer()
sous_vivier = SousVivierSerializer()
class Meta:
fields = '__all__'
class CompetenceSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets competences en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Competence
fields = '__all__'
class PosteSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets postes en type json contenant les champs du poste
et les champs de fonction, formation_emploi, sous_vivier et decisions liés à chaque poste. Cette classe va également ordonner le json par p_id et valider certaines variables.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
fonction = FonctionSerializer()
formation_emploi = FormationEmploiSerializer()
sous_vivier = SousVivierSerializer(allow_null=True)
sous_vivier_id = serializers.IntegerField(write_only=True, allow_null=True)
decisions = SimpleDecisionSerializer(many=True)
p_nb_prepositionne = serializers.IntegerField(read_only=True)
p_nb_positionne = serializers.IntegerField(read_only=True)
p_nb_omi_en_cours = serializers.IntegerField(read_only=True)
p_nb_omi_active = serializers.IntegerField(read_only=True)
class Meta:
model = Poste
ordering = ['p_id']
fields = '__all__'
class AdministreSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets administres en type json contenant les champs de l'administré
et les champs de fonction, formation_emploi, sous_vivier, decisions, grade et Fmob liés à chaque administré. Cette classe va également ordonner le json par a_id_sap.
Les variables qui seront affichées dans le json sont celles mentionnées dans la variable fields.
"""
a_id_sap = serializers.ReadOnlyField()
formation_emploi = FormationEmploiSerializer(read_only=True)
sous_vivier = SousVivierSerializer(read_only=True)
sous_vivier_id = serializers.IntegerField(write_only=True, allow_null=True)
fonction = FonctionSerializer(read_only=True)
grade = GradeSerializer(read_only=True)
decision = AdministreDecisionSerializer(read_only=True)
fmobs = FmobSerializer(many=True)
class Meta:
model = Administre
ordering = ['a_id_sap']
fields = '__all__'
"""
fields = ["a_id_sap",
"formation_emploi",
"formation_emploi_id",
"fonction",
"fonction_id",
"sous_vivier",
"sous_vivier_id",
"grade",
"decision",
"fmobs",
"a_liste_id_marques",
"a_liste_id_competences",
"a_nom",
"a_prenom",
"a_sexe",
"a_fonction",
"a_code_fonction",
"a_domaine",
"a_filiere",
"a_nf",
"a_categorie",
"a_domaine_futur",
"a_filiere_futur",
"a_nf_futur",
"a_bureau_gestion",
"a_date_entree_service",
"a_arme",
"a_rg_origine_recrutement",
"a_date_naissance",
"a_liste_depts_souhaites",
"a_interruption_service",
"a_situation_fam",
"a_date_rdc",
"a_date_dernier_acr",
"a_sap_conjoint",
"a_statut_pam",
"a_notes_gestionnaire",
"a_notes_partagees"]
read_only_fields = ("a_id_sap",)
"""
class DecisionSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets decisions en type json contenant les champs de decision
et les champs de notation, poste et administre liés à chaque decision . Cette classe va également ordonner le json par de_date_decision.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
notation = NotationSerializer()
poste = PosteSerializer()
administre = AdministreSerializer()
class Meta:
model = Decision
fields = '__all__'
ordering = ['de_date_decision']
class PcpFeGroupeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets PcpFeGroupe en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = PcpFeGroupe
fields = '__all__'

View File

@@ -0,0 +1,17 @@
from .administre import *
from .alimentation import *
from .commun import *
from .current_user import *
from .decision import *
from .domaine import *
from .exportation_fichiers import *
from .filiere import *
from .fmob import *
from .fonction import *
from .formation_emploi import *
from .grade import *
from .initial import *
from .notation import *
from .pcp_fe_groupe import *
from .sous_vivier import *
from .suppression import *

View File

@@ -0,0 +1,41 @@
from typing import Optional, Tuple
from rest_framework import serializers
from ..models import (Administre, Decision, DecisionChoices, DecisionTree,
FormationEmploi, Poste, FMOB, Administres_Pams)
from ..utils.attributes import safe_rgetattr
from ..utils.decisions import KEY_CREATE, KEY_UPDATE, get_available_decisions
from ..utils.permissions import (KEY_READ, KEY_WRITE, Profiles,
get_profiles_by_adm)
from .commun import AssignmentState
from .fmob import FmobSerializer
from .fonction import FonctionSerializer
from .formation_emploi import FormationEmploiSerializer
from .grade import GradeSerializer
from .sous_vivier import SousVivierSerializer
CTX_KEY_DECISIONS = 'decisions'
CTX_KEY_PROFILES = 'profiles'
class AdministreSerializer(serializers.ModelSerializer):
"""
Cette classe sera responsable de la conversion des objets administres en type json contenant les champs de l'administré
et les champs de fonction, formation_emploi, sous_vivier, decisions, grade et Fmob liés à chaque administré. Cette classe va également ordonner le json par a_id_sap.
Les variables qui seront affichées dans le json sont celles mentionnées dans la variable fields.
"""
a_id_sap = serializers.ReadOnlyField()
formation_emploi = FormationEmploiSerializer(read_only=True)
sous_vivier = SousVivierSerializer(read_only=True)
sous_vivier_id = serializers.IntegerField(write_only=True, allow_null=True)
fonction = FonctionSerializer(read_only=True)
grade = GradeSerializer(read_only=True)
class Meta:
model = Administre
ordering = [Administre.Cols.PK]
fields = '__all__'

View File

@@ -0,0 +1,86 @@
from rest_framework import serializers
class FileSVSerializer(serializers.Serializer):
"""Cette classe ne sera utilisée que pour valider si le fichier SV est un vrai fichier.
"""
SV = serializers.FileField()
class Meta:
fields = ['sv_file']
class ChargementCompetencesSerializer(serializers.Serializer):
"""
Cette classe ne sera utilisée que pour valider si les fichiers competence_v1 et competence_v2 sont un vrais fichier.
"""
ref_skills = serializers.FileField(label="Référentiel des compétences", help_text="Fichier 'Référenciel des compétences_corrigé v2.xlsx'", required = False)
specific_skills = serializers.FileField(label="Compétences particulières", help_text="Fichier 'COMPETENCES PARTICULIERES.xlsx'", required = False)
class Meta:
fields = '__all__'
class AlimentationSerializer(serializers.Serializer):
"""Cette classe ne sera utilisée que pour valider si les fichiers Donnees_BO_ADT, REO, Ref Gest, Ref Org, Ref Sv Fil, Ref FE, FMOB, domaine_filiere, insee_mapin et diplomes sont des vrais fichiers.
"""
Donnees_BO_ADT = serializers.FileField(label="Données BO", help_text="Fichier 'Données BO_Anonymes.xlsx'", required=False)
REO = serializers.FileField(label="REO", help_text="Fichier 'REO2022_OGURE_NG.xlsx'", required=False)
REO_PAM_SUIVANT = serializers.FileField(label="REO A + 1",help_text="Fichier REO2023_OGURE_NG.xlsx", required=False)
REO_OCV = serializers.FileField(label="Requêtes OCV", help_text="Fichier 'Copie requete ocv postes et mise à poste v2.xlsx'", required=False)
referentiel_gestionnaire = serializers.FileField(label="Référentiel de gestionnaires", help_text="Fichier 'Référentiel gestionnaires DRHAT.xlsx'", required=False)
referentiel_organique = serializers.FileField(label="Référentiel organique", help_text="Fichier 'Référentiel organique DRHAT.xlsx'", required=False)
refeferentiel_sous_vivier_filiere = serializers.FileField(label="Référentiel de sous-viviers/filières", help_text="Fichier 'Référentiel sous-viviers filières_V3.xlsx'", required=False)
referentiel_fe = serializers.FileField(label="Référentiel FE", help_text="Fichier 'Référentiel FE anonymisé_V2.xlsx'", required=False)
FMOB = serializers.FileField(label="Formulaire de mobilité", help_text="Fichier 'FMOB_FEMP modifié.xlsx'", required=False)
FMOB_PAM_SUIVANT = serializers.FileField(label="Formulaire de mobilité A + 1", help_text="Fichier 'FMOB PAM A1 (2023).xlsx", required=False)
domaine_filiere = serializers.FileField(label="Domaines - filières", help_text="Fichier 'DOMAINES FILIERES DRHAT.xlsx'", required=False)
insee_maping = serializers.FileField(label="INSEE", help_text="Fichier 'INSEE.xlsx'", required=False)
diplomes = serializers.FileField(label="Diplômes", help_text="Fichier 'Diplomes 2021.xlsx'", required=False)
FUD = serializers.FileField(label="FUD", help_text="Fichier 'FUD.xlsx'", required=False)
ref_zones_geo = serializers.FileField(label="Référentiel des zones géographiques", help_text="Fichier 'Zones_Geographique.xlsx'", required=False)
class Meta:
fields = '__all__'
class AlimentationReferentielSerializer(serializers.Serializer):
""" Valide que les données sont bien des fichiers. """
referentiel_fe = serializers.FileField(label='Référentiel FE', help_text='Référentiel FE anonymisé_V2.xlsx')
class Meta:
fields = '__all__'
class AlimentationZoneGeographiqueSerializer(serializers.Serializer):
""" Valide que les données sont bien des fichiers. """
ref_zones_geo = serializers.FileField(label="Référentiel des zones géographiques", help_text="Fichier 'Zones_Geographique.xlsx'", required=False)
class Meta:
fields = '__all__'
class AlimentationCommentairesSerializer(serializers.Serializer):
""" Valide que les données sont bien des fichiers. """
commentaires = serializers.FileField(label='Commentaires', help_text='20220427_NP_DRHAT_SDG_BCCM_com-OGURE.xlsx', required=False)
class Meta:
fields = '__all__'
class AlimentationRefsDroitSerializer(serializers.Serializer):
""" Valide que les données sont bien des fichiers. """
referentiel_gestionnaire = serializers.FileField(label="Référentiel de gestionnaires", help_text="Fichier 'Référentiel gestionnaires DRHAT.xlsx'", required=False)
referentiel_organique = serializers.FileField(label="Référentiel organique", help_text="Fichier 'Référentiel organique DRHAT.xlsx'", required=False)
refeferentiel_sous_vivier_filiere = serializers.FileField(label="Référentiel de sous-viviers/filières", help_text="Fichier 'Référentiel sous-viviers filières_V3.xlsx'", required=False)
referentiel_fe = serializers.FileField(label="Référentiel FE", help_text="Fichier 'Référentiel FE anonymisé_V2.xlsx'", required=False)
class Meta:
fields = '__all__'

View File

@@ -0,0 +1,29 @@
from enum import Enum
from rest_framework import serializers
class AssignmentState(str, Enum):
""" état d'affectation BMOB (lié au changement de BMOB) """
# reste dans la même FE
RESTANT = 'RESTANT'
# entre dans une FE du périmètre du PCP
ENTRANT = 'ENTRANT'
# sort d'une FE du périmètre du PCP
SORTANT = 'SORTANT'
# ENTRANT + SORTANT (mais pas RESTANT car ce n'est pas la même FE)
ENTRANT_SORTANT = 'ENTRANT_SORTANT'
def __repr__(self):
return "%s.%s" % (self.__class__.__name__, self._name_)
class ChoicesSerializer(serializers.Serializer):
""" Représente un élément qui hérite de Choices sous forme {code, libelle} """
code = serializers.ReadOnlyField(source='value')
libelle = serializers.ReadOnlyField(source='label')

View File

@@ -0,0 +1,72 @@
from rest_framework import serializers
from ..models import CustomUser, SousVivier, FormationEmploi
from ..utils.permissions import KEY_READ, KEY_WRITE
from .pcp_fe_groupe import PcpFeGroupeSerializer
CTX_KEY_SOUS_VIVIERS_SUPER = 'sous_viviers_super'
CTX_KEY_SOUS_VIVIERS = 'sous_viviers'
CTX_KEY_FORMATION_EMPLOIS = 'formation_emplois'
CTX_KEY_PCP_FE_GROUPE = 'fe_groupe'
CTX_KEY_PROFILES = 'profiles'
class _SousVivierUserSerializer(serializers.ModelSerializer):
""" Classe de représentation d'un sous-vivier de l'utilisateur """
class Meta:
model = SousVivier
exclude = ('gestionnaires',)
def to_representation(self, instance):
res = super().to_representation(instance)
ids_super = self.context.get(CTX_KEY_SOUS_VIVIERS_SUPER)
if ids_super and instance.pk in ids_super:
res['admin'] = True
return res
class _FormationEmploiUserSerializer(serializers.ModelSerializer):
""" Classe de représentation d'un sous-vivier de l'utilisateur """
class Meta:
model = FormationEmploi
fields = [FormationEmploi.Cols.PK, FormationEmploi.Cols.LIBELLE, 'fe_mere_credo', 'fe_mere_la', 'gestionnaire_id']
gestionnaire_id = serializers.ReadOnlyField()
def to_representation(self, instance):
res = super().to_representation(instance)
return res
class UserInfoSerializer(serializers.ModelSerializer):
""" Gère la forme publique de l'utilisateur connecté. """
class Meta:
model = CustomUser
fields = ['id', 'username', 'first_name', 'last_name', 'email', 'grade', 'administre']
administre = serializers.ReadOnlyField(source='administre_id')
def to_representation(self, instance):
res = super().to_representation(instance)
svs_super = self.context.get(CTX_KEY_SOUS_VIVIERS_SUPER) or ()
svs = self.context.get(CTX_KEY_SOUS_VIVIERS) or ()
fes = self.context.get(CTX_KEY_FORMATION_EMPLOIS) or ()
all_svs = set(list(svs) + list(svs_super))
all_fes = set(list(fes))
svs_ids_specific_super = {sv.pk for sv in svs_super if sv not in svs}
res['sous_viviers'] = _SousVivierUserSerializer(all_svs, many=True, context={
CTX_KEY_SOUS_VIVIERS_SUPER: svs_ids_specific_super
}).data
res['formation_emplois'] = _FormationEmploiUserSerializer(all_fes, many=True).data
res['fe_groupe'] = PcpFeGroupeSerializer(self.context.get(CTX_KEY_PCP_FE_GROUPE) or ()).data
profiles = self.context.get(CTX_KEY_PROFILES) or {}
res['profils'] = {'lecture': profiles.get(KEY_READ) or (), 'ecriture': profiles.get(KEY_WRITE) or ()}
return res

View File

@@ -0,0 +1,35 @@
from rest_framework import serializers
from ..models import Decision, DecisionChoices
from .administre import AdministreSerializer
from .initial import PosteSerializer
from .notation import NotationSerializer
class DecisionSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets decisions en type json contenant les champs de decision
et les champs de notation, poste et administre liés à chaque decision . Cette classe va également ordonner le json par de_date_decision.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
notation = NotationSerializer()
# poste = PosteSerializer()
# administre = AdministreSerializer()
de_date_decision = serializers.ReadOnlyField()
class Meta:
model = Decision
fields = '__all__'
ordering = ['de_date_decision']
class CreateDecisionSerializer(serializers.ModelSerializer):
""" Valide les données pour une création de décision """
administre_id = serializers.IntegerField(write_only=True)
poste_id = serializers.CharField(max_length=100, write_only=True)
de_decision = serializers.ChoiceField(choices=DecisionChoices.choices)
delete_former = serializers.BooleanField(write_only=True, required=False)
class Meta:
model = Decision
fields = ['administre_id', 'poste_id', 'de_decision', 'delete_former']

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from ..models.domaine import Domaine
class DomaineSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets domaines en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Domaine
fields = '__all__'

View File

@@ -0,0 +1,11 @@
from tabnanny import verbose
from rest_framework import serializers
class ExportationSerializer(serializers.Serializer):
"""Cette classe ne sera utilisée que pour valider si les fichiers Donnees_BO_ADT, REO, Ref Gest, Ref Org, Ref Sv Fil, Ref FE, FMOB, domaine_filiere, insee_mapin et diplomes sont des vrais fichiers.
"""
FICHIERS_CHOICES = (("1", "Données BO"), ("2", "REO"),)
fichier_exporte = serializers.ChoiceField(label='Fichier à exporter', help_text='Selectionnez le fichier que vous souhaitez exporter', choices=FICHIERS_CHOICES)
class Meta:
fields = '__all__'

View File

@@ -0,0 +1,16 @@
from rest_framework import serializers
from ..models.filiere import Filiere
from .domaine import DomaineSerializer
class FiliereSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets filières en type json contenant les informations de filière
et les champs du domaine liés à chaque filiere.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
domaine = DomaineSerializer()
class Meta:
model = Filiere
fields = '__all__'

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from ..models import FMOB
class FmobSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets Fmob en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = FMOB
fields = '__all__'

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from ..models import Fonction
class FonctionSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets fonctions en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Fonction
fields = '__all__'

View File

@@ -0,0 +1,25 @@
from rest_framework import serializers
from ..models import FormationEmploi, Garnison
class FormationEmploiSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets FormationEmploi en type json contenant les informations de FormationEmploi
et le champs garnison lieu liés à chaque FormationEmploi.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class MereSerializer(serializers.ModelSerializer):
"""Classe de représentation de la FE mère"""
fe_code = serializers.ReadOnlyField()
fe_libelle = serializers.ReadOnlyField()
class Meta:
model = FormationEmploi
fields = ['fe_code', 'fe_libelle']
mere = MereSerializer(read_only=True)
class Meta:
model = FormationEmploi
exclude = ('gestionnaires',)

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from ..models import Grade
class GradeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets grades en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Grade
fields = '__all__'

View File

@@ -0,0 +1,259 @@
"""Ce fichier contient des fonctions qui permettent de convertir des données complexes,
telles que des querysets et des instances de modèle, en types de données Python qui peuvent ensuite
être facilement rendus en JSON, XML ou d'autres types de contenu.
"""
from rest_framework import serializers
from typing import Optional, Tuple
from .. import constants
from ..models import (Competence, Decision, Garnison, Marque, MarquesGroupe,
Poste, PAM, Administres_Pams, FormationEmploi, Postes_Pams,
DecisionChoices, DecisionTree, FMOB)
from .commun import AssignmentState
from ..utils.attributes import safe_rgetattr
from ..utils.decisions import KEY_CREATE, KEY_UPDATE, get_available_decisions
from ..utils.permissions import (KEY_READ, KEY_WRITE, Profiles,
get_profiles_by_adm)
from .commun import ChoicesSerializer
from .filiere import FiliereSerializer
from .fonction import FonctionSerializer
from .formation_emploi import FormationEmploiSerializer
from .sous_vivier import SousVivierSerializer
from .fmob import FmobSerializer
from .administre import AdministreSerializer
CTX_KEY_DECISIONS = 'decisions'
CTX_KEY_PROFILES = 'profiles'
class ScoringValidator(serializers.Serializer):
""" pour interprêter et valider le contenu JSON """
sous_vivier_id = serializers.CharField(max_length=100, required=True, allow_null=False, allow_blank=False)
pam_id = serializers.CharField(max_length=100, required=True, allow_null=False, allow_blank=False)
class ScoringSelectifValidator(serializers.Serializer):
""" pour interprêter et valider le contenu JSON """
sous_vivier_id = serializers.CharField(max_length=100, required=True, allow_null=False, allow_blank=False)
administre_id = serializers.ListField()
poste_id = serializers.ListField()
pam_id = serializers.CharField(max_length=100)
class SimpleDecisionPosteSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets decision en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
grade = serializers.ReadOnlyField(source='administre.grade.gr_code')
nom = serializers.ReadOnlyField(source='administre.a_nom')
prenom = serializers.ReadOnlyField(source='administre.a_prenom')
class Meta:
model = Decision
fields = '__all__'
class AdministresPamsSerializer(serializers.ModelSerializer):
class DecisionLocalSerializer(serializers.ModelSerializer):
"""Classe de représentation locale d'une décision"""
class PosteLocalSerializer(serializers.Serializer):
"""Classe de représentation locale d'un poste"""
class FormationEmploiLocalSerializer(FormationEmploiSerializer):
"""Classe de représentation locale d'une FE"""
class Meta:
model = FormationEmploi
fields = ['fe_code', 'fe_libelle', 'fe_mere_credo', 'fe_mere_la', 'fe_garnison_lieu', 'mere']
p_id = serializers.ReadOnlyField()
formation_emploi = FormationEmploiLocalSerializer(read_only=True)
class Meta:
model = Poste
fields = '__all__'
poste = PosteLocalSerializer(read_only=True)
class Meta:
model = Decision
fields = '__all__'
administre = AdministreSerializer()
pam_id = serializers.ReadOnlyField(source='pam.pam_id')
pam_date = serializers.ReadOnlyField(source='pam.pam_date')
pam_libelle = serializers.ReadOnlyField(source='pam.pam_libelle')
pam_statut = serializers.ReadOnlyField(source='pam.pam_statut')
fmobs = FmobSerializer()
decision = DecisionLocalSerializer()
profils = serializers.SerializerMethodField()
decisions_possibles = serializers.SerializerMethodField()
etat_fe_bmob = serializers.SerializerMethodField()
def get_decisions_possibles(self, obj: Administres_Pams) -> Tuple[DecisionChoices]:
decisions_by_adm = self.context.get(CTX_KEY_DECISIONS) or get_available_decisions((obj,), user=self.context['request'].user)
decisions = decisions_by_adm.get(obj.id) or {}
return {'creation': decisions.get(KEY_CREATE) or (), 'maj': decisions.get(KEY_UPDATE) or ()}
def get_profils(self, obj: Administres_Pams) -> Tuple[Profiles]:
profiles_by_adm = self.context.get(CTX_KEY_PROFILES) or get_profiles_by_adm(self.context['request'].user, obj)
profiles = profiles_by_adm.get(obj.id) or {}
return {'lecture': profiles.get(KEY_READ) or (), 'ecriture': profiles.get(KEY_WRITE) or ()}
def get_etat_fe_bmob(self, obj: Administres_Pams) -> Optional[AssignmentState]:
decision = safe_rgetattr(obj, f'{Administres_Pams.Cols.REL_DECISION}.{Decision.Cols.STATUT}')
if not decision or DecisionTree.ME not in DecisionChoices(decision).trees:
return None
profiles_by_adm = self.context.get(CTX_KEY_PROFILES) or get_profiles_by_adm(self.context['request'].user, obj)
profiles = (profiles_by_adm.get(obj.id) or {}).get(KEY_READ) or ()
if Profiles.PCP in profiles:
return AssignmentState.RESTANT
current = Profiles.PCP_ACTUEL in profiles
future = Profiles.PCP_FUTUR in profiles
if current and future:
return AssignmentState.ENTRANT_SORTANT
if current:
return AssignmentState.SORTANT
if future:
return AssignmentState.ENTRANT
return None
def to_representation(self, instance):
ret = super().to_representation(instance)
ret.update(ret.pop('administre'))
ret['a_statut_pam'] = ret.pop('a_statut_pam_annee')
if 'fmobs' in ret and ret['fmobs']:
ret['fmobs'] = [ret['fmobs']]
return ret
class Meta:
model = Administres_Pams
fields = '__all__'
class PAMSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets decision en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = PAM
fields = '__all__'
class MarquesGroupeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets marquegroupes en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = MarquesGroupe
fields = '__all__'
class MarqueSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets marque en type json contenant les informations de marque et les champs de marquesgroupe liés à chaque marque.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
groupe_marques = MarquesGroupeSerializer()
class Meta:
model = Marque
fields = '__all__'
class GarnisonSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets garnisons en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Garnison
fields = '__all__'
class SousVivierAssociationSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la creation d'un json contenant les champs du SousVivier
et les champs de filière liés à chaque SousVivier.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
filiere = FiliereSerializer()
sous_vivier = SousVivierSerializer()
class Meta:
fields = '__all__'
class CompetenceSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets competences en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Competence
fields = '__all__'
class PosteSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets postes en type json contenant les champs du poste
et les champs de fonction, formation_emploi, sous_vivier et decisions liés à chaque poste. Cette classe va également ordonner le json par p_id et valider certaines variables.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
p_id = serializers.ReadOnlyField()
fonction = FonctionSerializer()
formation_emploi = FormationEmploiSerializer()
sous_viviers = SousVivierSerializer(many=True)
p_nb_prepositionne = serializers.IntegerField(read_only=True)
p_nb_positionne = serializers.IntegerField(read_only=True)
p_nb_omi_en_cours = serializers.IntegerField(read_only=True)
p_nb_omi_active = serializers.IntegerField(read_only=True)
p_poids_competences = serializers.IntegerField(read_only=True)
p_poids_filiere = serializers.IntegerField(read_only=True)
p_poids_nf = serializers.IntegerField(read_only=True)
class Meta:
model = Poste
ordering = ['p_id']
fields = '__all__'
class PostesPamsSerializer(serializers.ModelSerializer):
pam_id = serializers.ReadOnlyField(source='p_pam.pam_id')
pam_date = serializers.ReadOnlyField(source='p_pam.pam_date')
pam_libelle = serializers.ReadOnlyField(source='p_pam.pam_libelle')
pam_statut = serializers.ReadOnlyField(source='p_pam.pam_statut')
decisions = SimpleDecisionPosteSerializer()
poste = PosteSerializer()
def to_representation(self, instance):
ret = super().to_representation(instance)
ret.update(ret.pop('poste'))
if ret['decisions']:
ret['decisions'] = [ret['decisions']]
else :
ret['decisions'] = []
return ret
class Meta:
model = Postes_Pams
fields = '__all__'
class RefStatutPamChoicesSerializer(ChoicesSerializer):
""" Représente un élément StatutPamChoices dans les données de référence """
inclusAlgo = serializers.ReadOnlyField(source='calc_enabled')
affectationManuellePossible = serializers.ReadOnlyField(source='dec_enabled')
class RefAvisPosteChoicesSerializer(RefStatutPamChoicesSerializer):
""" Représente un élément AvisPosteChoices dans les données de référence """

View File

@@ -0,0 +1,11 @@
from rest_framework import serializers
from ..models import Notation
class NotationSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets Notation en type json contenant uniquement les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = Notation
fields = '__all__'

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from ..models import PcpFeGroupe
class PcpFeGroupeSerializer(serializers.ModelSerializer):
"""Cette classe sera responsable de la conversion des objets PcpFeGroupe en type json contenant les informations mentionnées dans la variable fields.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = PcpFeGroupe
fields = '__all__'

View File

@@ -0,0 +1,15 @@
from rest_framework import serializers
from ..models import SousVivier
class SousVivierSerializer(serializers.ModelSerializer):
"""
Cette classe sera responsable de la conversion des objets SousVivier en type json contenant les informations du SousVivier
et la/le gestionnaire responsable de chaque SousVivier.
Si fields='__all__', alors toutes les variables liées à cette classe seront affichées dans le json.
"""
class Meta:
model = SousVivier
exclude = (SousVivier.Cols.M2M_GESTIONNAIRES,)

View File

@@ -0,0 +1,9 @@
from rest_framework import serializers
class SuppressionAdministresSerializer(serializers.Serializer):
""" Valide que les données sont bien des fichiers. """
administres = serializers.FileField(label='Administrés à supprimer', help_text="Fichier 'Militaires à supprimer d\'OGURE.xlsx'", required=False)
class Meta:
fields = '__all__'

View File

@@ -0,0 +1,198 @@
"""
Les signaux permettent à certains expéditeurs davertir un ensemble de destinataires quune action a eu lieu.
Dans ce projet, les signaux ont été utilisés pour mettre à jour les compteurs des postes appartenant à la FE lorsque le taux d'armement change en fonction de la catégorie.
"""
import numpy as np
from . import constants
from .models import FormationEmploi, Poste, SousVivierAssociation, Administre
from django.db.models.signals import post_save
def update_taux_mdr(sender, instance, **kwargs):
"""Met à jour les compteurs des postes appartenant à la FE dont on change le taux d'armement cible pour la catégorie MDR
:type instance: objet du modèle FormationEmploi
:param instance: FE dont on change le taux d'armement cible pour la catégorie MDR
"""
taux = instance.fe_taux_armement_cible_mdr
fe_id = instance.fe_code
postes = Poste.objects.filter(formation_emploi_id=fe_id, p_categorie='MDR')
if (len(postes) > 0) and (taux is not None):
taux = taux * 0.01
for poste in postes:
new_nb_reevalue = poste.p_nb_reevalue * taux
new_nb_vacant = new_nb_reevalue - poste.p_nb_occupe
if new_nb_reevalue >= poste.p_nb_reevalue:
new_nb_p4 = poste.p_nb_p4 + new_nb_vacant - poste.p_nb_vacant
poste.p_nb_p4 = new_nb_p4
else:
if new_nb_reevalue > poste.p_nb_occupe:
new_nb_non_etudie = poste.p_nb_non_etudie + poste.p_nb_reevalue - new_nb_reevalue
poste.p_nb_non_etudie = new_nb_non_etudie
delta_vacant = new_nb_vacant - poste.p_nb_vacant
if delta_vacant <= poste.p_nb_p4:
new_poste_p4 = poste.p_nb_p4 - delta_vacant
poste.p_nb_p4 = new_poste_p4
else:
delta_vacant -= poste.p_nb_p4
poste.p_nb_p4 = 0
if delta_vacant <= poste.p_nb_p3:
new_poste_p3 = poste.p_nb_p3 - delta_vacant
poste.p_nb_p3 = new_poste_p3
else:
delta_vacant -= poste.p_nb_p3
poste.p_nb_p3 = 0
if delta_vacant <= poste.p_nb_p2:
new_poste_p2 = poste.p_nb_p2 - delta_vacant
poste.p_nb_p2 = new_poste_p2
else:
delta_vacant -= poste.p_nb_p2
poste.p_nb_p2 = 0
new_nb_p1 = poste.p_nb_p1 - delta_vacant
poste.p_nb_p1 = new_nb_p1
else:
poste.p_nb_p3 = 0
poste.p_nb_p2 = 0
poste.p_nb_p1 = 0
poste.p_nb_p4 = 0
poste.p_nb_reevalue = new_nb_reevalue
poste.p_nb_vacant = new_nb_vacant
poste.save(
update_fields=['p_nb_reevalue', 'p_nb_vacant', 'p_nb_non_etudie', 'p_nb_p4', 'p_nb_p3', 'p_nb_p2',
'p_nb_p1'])
post_save.connect(update_taux_mdr, sender=FormationEmploi)
def update_taux_off(sender, instance, **kwargs):
"""Met à jour les compteurs des postes appartenant à la FE dont on change le taux d'armement cible pour la catégorie OFF
:type instance: objet du modèle FormationEmploi
:param instance: FE dont on change le taux d'armement cible pour la catégorie OFF
"""
taux = instance.fe_taux_armement_cible_off
fe_id = instance.fe_code
postes = Poste.objects.filter(formation_emploi_id=fe_id, p_categorie='OFF')
if (len(postes) > 0) and (taux is not None):
taux = taux * 0.01
for poste in postes:
new_nb_reevalue = poste.p_nb_reevalue * taux
new_nb_vacant = new_nb_reevalue - poste.p_nb_occupe
if new_nb_reevalue >= poste.p_nb_reevalue:
new_nb_p4 = poste.p_nb_p4 + new_nb_vacant - poste.p_nb_vacant
poste.p_nb_p4 = new_nb_p4
else:
if new_nb_reevalue > poste.p_nb_occupe:
new_nb_non_etudie = poste.p_nb_non_etudie + poste.p_nb_reevalue - new_nb_reevalue
poste.p_nb_non_etudie = new_nb_non_etudie
delta_vacant = new_nb_vacant - poste.p_nb_vacant
if delta_vacant <= poste.p_nb_p4:
new_poste_p4 = poste.p_nb_p4 - delta_vacant
poste.p_nb_p4 = new_poste_p4
else:
delta_vacant -= poste.p_nb_p4
poste.p_nb_p4 = 0
if delta_vacant <= poste.p_nb_p3:
new_poste_p3 = poste.p_nb_p3 - delta_vacant
poste.p_nb_p3 = new_poste_p3
else:
delta_vacant -= poste.p_nb_p3
poste.p_nb_p3 = 0
if delta_vacant <= poste.p_nb_p2:
new_poste_p2 = poste.p_nb_p2 - delta_vacant
poste.p_nb_p2 = new_poste_p2
else:
delta_vacant -= poste.p_nb_p2
poste.p_nb_p2 = 0
new_nb_p1 = poste.p_nb_p1 - delta_vacant
poste.p_nb_p1 = new_nb_p1
else:
poste.p_nb_p3 = 0
poste.p_nb_p2 = 0
poste.p_nb_p1 = 0
poste.p_nb_p4 = 0
poste.p_nb_reevalue = new_nb_reevalue
poste.p_nb_vacant = new_nb_vacant
poste.save(
update_fields=['p_nb_reevalue', 'p_nb_vacant', 'p_nb_non_etudie', 'p_nb_p4', 'p_nb_p3', 'p_nb_p2',
'p_nb_p1'])
post_save.connect(update_taux_off, sender=FormationEmploi)
def update_taux_soff(sender, instance, **kwargs):
"""Met à jour les compteurs des postes appartenant à la FE dont on change le taux d'armement cible pour la catégorie SOFF
:type instance: objet du modèle FormationEmploi
:param instance: FE dont on change le taux d'armement cible pour la catégorie SOFF
"""
taux = instance.fe_taux_armement_cible_soff
fe_id = instance.fe_code
postes = Poste.objects.filter(formation_emploi_id=fe_id, p_categorie='SOFF')
if (len(postes) > 0) and (taux is not None):
taux = taux * 0.01
for poste in postes:
new_nb_reevalue = np.ceil(poste.p_nb_reo * taux)
if new_nb_reevalue != poste.p_nb_reevalue:
if new_nb_reevalue > poste.p_nb_reevalue:
new_nb_p4 = poste.p_nb_p4 + new_nb_reevalue - poste.p_nb_reevalue
new_nb_non_etudie = (new_nb_reevalue - new_nb_p4) if (new_nb_reevalue - new_nb_p4) > 0 else 0
elif new_nb_reevalue > poste.p_nb_occupe:
new_nb_p4 = new_nb_reevalue - poste.p_nb_occupe
new_nb_non_etudie = new_nb_reevalue - new_nb_p4
poste.p_nb_p3 = 0
poste.p_nb_p2 = 0
poste.p_nb_p1 = 0
else:
new_nb_p4 = 0
new_nb_non_etudie = 0
poste.p_nb_p3 = 0
poste.p_nb_p2 = 0
poste.p_nb_p1 = 0
poste.p_nb_p4 = 0
new_nb_reevalue = poste.p_nb_occupe
poste.p_nb_non_etudie = new_nb_non_etudie
poste.p_nb_p4 = new_nb_p4
poste.p_nb_reevalue = new_nb_reevalue
poste.save(
update_fields=['p_nb_reevalue', 'p_nb_non_etudie', 'p_nb_p4', 'p_nb_p3', 'p_nb_p2',
'p_nb_p1'])
post_save.connect(update_taux_soff, sender=FormationEmploi)
def add_sva(sender, instance, **kwargs):
"""Met à jour les sous-viviers des militaires ayant la même filière de la nouvelle instance de SousVivierAssociation créée
:param instance: La nouvelle instance de SousVivierAssociation créée
:type instance: objet du modèle SousVivierAssociation
"""
filiere_id = instance.filiere_id
sva_categorie = instance.sva_categorie
sous_vivier_id = instance.sous_vivier_id
administres = Administre.objects.filter(a_filiere_id=filiere_id, a_categorie=sva_categorie)
administres.update(sous_vivier_id=sous_vivier_id)
post_save.connect(add_sva, sender=SousVivierAssociation)

View File

@@ -0,0 +1,25 @@
{% extends "admin/base_site.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<p>
Entrer le nom du groupe des formations d'emploi
</p>
<ul>
{% for fe in formations_emplois %}
<li>
{{ fe.fe_code }} — {{ fe.fe_libelle }}
</li>
{% endfor %}
</ul>
{% for obj in formations_emplois %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}">
{% endfor %}
{# <input type="text" name="taux_armement_cli" value="POMME"/>#}
{{ form.nom_groupe_fe }}
<br/>
<input type="hidden" name="action" value="{{ do_action }}"/>
<input type="submit" name="apply" value="Modifier le nom du groupe"/>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "admin/base_site.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<p>
Entrer le nouveau taux cible d'armement pour la catégorie MDR pour les formations-emplois sélectionnées
</p>
<ul>
{% for fe in formations_emplois %}
<li>
{{ fe.fe_code }} — {{ fe.fe_libelle }}
</li>
{% endfor %}
</ul>
{% for obj in formations_emplois %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}">
{% endfor %}
{# <input type="text" name="taux_armement_cli" value="POMME"/>#}
{{ form.taux_armement_cible }}
<br/>
<input type="hidden" name="action" value="{{ do_action }}"/>
<input type="submit" name="apply" value="Modifier le taux cible"/>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "admin/base_site.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<p>
Entrer le nouveau taux cible d'armement pour la catégorie OFF pour les formations-emplois sélectionnées
</p>
<ul>
{% for fe in formations_emplois %}
<li>
{{ fe.fe_code }} — {{ fe.fe_libelle }}
</li>
{% endfor %}
</ul>
{% for obj in formations_emplois %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}">
{% endfor %}
{# <input type="text" name="taux_armement_cli" value="POMME"/>#}
{{ form.taux_armement_cible }}
<br/>
<input type="hidden" name="action" value="{{ do_action }}"/>
<input type="submit" name="apply" value="Modifier le taux cible"/>
</form>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "admin/base_site.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<p>
Entrer le nouveau taux cible d'armement pour la catégorie SOFF pour les formations-emplois sélectionnées
</p>
<ul>
{% for fe in formations_emplois %}
<li>
{{ fe.fe_code }} — {{ fe.fe_libelle }}
</li>
{% endfor %}
</ul>
{% for obj in formations_emplois %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}">
{% endfor %}
{# <input type="text" name="taux_armement_cli" value="POMME"/>#}
{{ form.taux_armement_cible }}
<br/>
<input type="hidden" name="action" value="{{ do_action }}"/>
<input type="submit" name="apply" value="Modifier le taux cible"/>
</form>
{% endblock %}

View File

@@ -0,0 +1,137 @@
"""
Ce fichier n'est utilisé que pour les tests.
"""
from django.test import TestCase
from numpy.core.fromnumeric import shape
import pandas as pd
from .utils_scoring import encoding,notePonderee,notations_test
# Create your tests here.
class InsertModelTest(TestCase):
# Fonction pour la table Garnisons
def test_scoring(self):
"""Pour tester la fonction de scoring
"""
mut_test = notations_test()
data_test = mut_test.to_dict('records')
data_source = [{
'mu_id': 0,
'poste_id': 1,
'administres_id_sap': 9984,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.5,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''},
{
'mu_id': 0,
'poste_id': 1,
'administres_id_sap': 56754,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.0,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''},
{
'mu_id': 0,
'poste_id': 1,
'administres_id_sap': 78905,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.0,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
},
{
'mu_id': 0,
'poste_id': 2,
'administres_id_sap': 9984,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.0,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
},
{
'mu_id': 0,
'poste_id': 2,
'administres_id_sap': 56754,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.5,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
},
{
'mu_id': 0,
'poste_id': 2,
'administres_id_sap': 78905,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.0,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
},
{
'mu_id': 0,
'poste_id': 3,
'administres_id_sap': 9984,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.0,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
},
{
'mu_id': 0,
'poste_id': 3,
'administres_id_sap': 56754,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.0,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
},
{
'mu_id': 0,
'poste_id': 3,
'administres_id_sap': 78905,
'pam_id': 1,
'mu_date_execution': '14/06/2021',
'mu_note_militaire': 0.5,
'mu_flag_cple_ideal': 0,
'mu_decision': '',
'mu_date_decision': '',
'mu_notes_gestionnaire': '',
'mu_notes_partagees': ''
}]
if data_source == data_test :
test_value = True
else :
test_value = False
self.assertIs(test_value,True)

View File

@@ -0,0 +1,5 @@
from .initial import *
from .models import *
from .utils import *
from .views import *
from .droits import *

View File

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

View File

@@ -0,0 +1,590 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from ...models import (Administre, CustomUser, Decision, DecisionChoices,
Domaine, Filiere, FormationEmploi, Garnison, Poste,
RefGest, RefOrg, RefSvFil, SousVivier)
class TestSocleVerifDroits(APITestCase):
""" Classe permettant de tester la vérification des droits de lecture et d'écriture d'un gestionnaire """
@classmethod
def setUpClass(cls):
super().setUpClass()
# Création des gestionnaires
cls.gest1 = CustomUser.objects.create(username='gest1', password='password', id=1)
cls.gest2 = CustomUser.objects.create(username='gest2', password='password', id=2)
cls.gest3 = CustomUser.objects.create(username='gest3', password='password', id=3)
cls.gest4 = CustomUser.objects.create(username='gest4', password='password', id=4)
cls.gest5 = CustomUser.objects.create(username='gest5', password='password', id=5)
cls.gest6 = CustomUser.objects.create(username='gest6', password='password', id=6)
cls.gest7 = CustomUser.objects.create(username='gest7', password='password', id=7)
cls.gest8 = CustomUser.objects.create(username='gest8', password='password', id=8)
cls.gest9 = CustomUser.objects.create(username='gest9', password='password', id=9)
cls.gest10 = CustomUser.objects.create(username='gest10', password='password', id=10)
# Création des objets du référentiel organique
cls.ref_org30 = RefOrg.objects.create(
ref_org_code='org30',
ref_org_code_niv_org3='org30',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_itd=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
) # Référentiel organique avec les droits de lecture et d'écriture (attribué aux gestionnaires 1)
cls.ref_org40a = RefOrg.objects.create(
ref_org_code='org40a',
ref_org_code_niv_org3='org30',
ref_org_code_niv_org4='org40a',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_itd=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
)
cls.ref_org40b = RefOrg.objects.create(
ref_org_code='org40b',
ref_org_code_niv_org3='org30',
ref_org_code_niv_org4='org40b',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_itd=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
)
cls.ref_org41 = RefOrg.objects.create(
ref_org_code='org41',
ref_org_code_niv_org4='org41',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True
) # Référentiel organique sans les droits de lecture et d'écriture (attribué aux gestionnaires 3)
cls.ref_org42 = RefOrg.objects.create(
ref_org_code='org42',
ref_org_code_niv_org4='org42',
ref_org_droit_lect=True
) # Référentiel organique avec les droits de lecture mais ni pcp ni fil ni itd (attribué aux gestionnaires 4)
cls.ref_org43 = RefOrg.objects.create(
ref_org_code='org43',
ref_org_code_niv_org4='org43',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
)
cls.ref_org44 = RefOrg.objects.create(
ref_org_code='org44',
ref_org_code_niv_org4='org44',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True
) # Référentiel organique sans les droits d'écriture (attribué aux gestionnaires 6)
cls.ref_org45 = RefOrg.objects.create(
ref_org_code='org45',
ref_org_code_niv_org4='org45',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
) # Référentiel organique sans FE ni FIL (attribué aux gestionnaires 7)
cls.ref_org36 = RefOrg.objects.create(
ref_org_code='org36',
ref_org_code_niv_org3='org36',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
) # Référentiel organique ne possedant aucune instances dans les ref FE et FIL (attribué aux gestionnaires 8)
cls.ref_org37 = RefOrg.objects.create(
ref_org_code='org37',
ref_org_code_niv_org3='org37',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
)
cls.ref_org47a = RefOrg.objects.create(
ref_org_code='org47a',
ref_org_code_niv_org3='org37',
ref_org_code_niv_org4='org47a',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
)
cls.ref_org47b = RefOrg.objects.create(
ref_org_code='org47b',
ref_org_code_niv_org3='org37',
ref_org_code_niv_org4='org47b',
ref_org_ref_fe=True,
ref_org_ref_sv_fil=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
)
cls.ref_org48 = RefOrg.objects.create(
ref_org_code='org48',
ref_org_code_niv_org4='org48',
ref_org_itd=True,
ref_org_droit_lect=True,
ref_org_droit_ecr=True
) # Référentiel organique avec tous les droits mais seulement pour un gestionnaire ITD (attribué au gestionnaire 10)
# Création des objets du référentiel gestionnaire
cls.ref_gest1 = RefGest.objects.create(ref_gest_sap=cls.gest1.id, ref_gest_org=cls.ref_org30)
cls.ref_gest2 = RefGest.objects.create(ref_gest_sap=cls.gest2.id) # Gestionnaire ne possedant pas de cod_niv_org
cls.ref_gest3 = RefGest.objects.create(ref_gest_sap=cls.gest3.id, ref_gest_org=cls.ref_org41)
cls.ref_gest4 = RefGest.objects.create(ref_gest_sap=cls.gest4.id, ref_gest_org=cls.ref_org42)
cls.ref_gest5 = RefGest.objects.create(ref_gest_sap=cls.gest5.id, ref_gest_org=cls.ref_org43)
cls.ref_gest6 = RefGest.objects.create(ref_gest_sap=cls.gest6.id, ref_gest_org=cls.ref_org44)
cls.ref_gest7 = RefGest.objects.create(ref_gest_sap=cls.gest7.id, ref_gest_org=cls.ref_org45)
cls.ref_gest8 = RefGest.objects.create(ref_gest_sap=cls.gest8.id, ref_gest_org=cls.ref_org36)
cls.ref_gest9 = RefGest.objects.create(ref_gest_sap=cls.gest9.id, ref_gest_org=cls.ref_org37)
cls.ref_gest10 = RefGest.objects.create(ref_gest_sap=cls.gest10.id, ref_gest_org=cls.ref_org48)
# Création du référentiel FE et des DOM/FIL
cls.gar1 = Garnison.objects.create(gar_id='Gar1', gar_lieu='lieu1')
cls.fe1 = FormationEmploi.objects.create(fe_code='Fe1', fe_code_niv_org4='org40a', fe_garnison_lieu=cls.gar1.gar_lieu)
cls.fe2 = FormationEmploi.objects.create(fe_code='Fe2', fe_code_niv_org4='org43', fe_garnison_lieu=cls.gar1.gar_lieu)
cls.fe3 = FormationEmploi.objects.create(fe_code='Fe3', fe_code_niv_org4='org47a', fe_garnison_lieu=cls.gar1.gar_lieu)
cls.dom1 = Domaine.objects.create(d_code='Dom1')
cls.fil1 = Filiere.objects.create(f_code='Fil1')
# Création des administrés, postes et décisions
cls.adm1 = Administre.objects.create(a_id_sap=1, formation_emploi=cls.fe1, a_domaine=cls.dom1, a_filiere=cls.fil1, a_categorie='Cat1', a_nom='nom', a_prenom='prénom', a_sexe='M', a_eip='eip1') # Administré du fe1
cls.adm2 = Administre.objects.create(a_id_sap=2, formation_emploi=cls.fe1, a_domaine=cls.dom1, a_filiere=cls.fil1, a_categorie='Cat1', a_nom='nom', a_prenom='prénom', a_sexe='M', a_eip='eip2') # Idem
cls.adm3 = Administre.objects.create(a_id_sap=3, formation_emploi=cls.fe2, a_domaine=cls.dom1, a_filiere=cls.fil1, a_categorie='Cat1', a_nom='nom', a_prenom='prénom', a_sexe='M', a_eip='eip3') # Administré du fe2
cls.adm4 = Administre.objects.create(a_id_sap=4, formation_emploi=cls.fe2, a_domaine=cls.dom1, a_filiere=cls.fil1, a_categorie='Cat1', a_nom='nom', a_prenom='prénom', a_sexe='M', a_eip='eip4') # Idem
cls.poste1 = Poste.objects.create(p_id='Poste1', formation_emploi=cls.fe1, p_domaine=cls.dom1, p_filiere=cls.fil1, p_categorie='Cat1', p_eip='eip1')
cls.poste2 = Poste.objects.create(p_id='Poste2', formation_emploi=cls.fe1, p_domaine=cls.dom1, p_filiere=cls.fil1, p_categorie='Cat1', p_eip='eip2')
cls.poste3 = Poste.objects.create(p_id='Poste3', formation_emploi=cls.fe2, p_domaine=cls.dom1, p_filiere=cls.fil1, p_categorie='Cat1', p_eip='eip3')
cls.poste4 = Poste.objects.create(p_id='Poste4', formation_emploi=cls.fe2, p_domaine=cls.dom1, p_filiere=cls.fil1, p_categorie='Cat1', p_eip='eip4')
cls.poste5 = Poste.objects.create(p_id='Poste5', p_specifique='ITD', p_eip='eip5')
cls.dec1 = Decision.objects.create(administre=cls.adm1, poste=cls.poste1, de_decision=DecisionChoices.PROPOSITION_FE)
cls.dec2 = Decision.objects.create(administre=cls.adm3, poste=cls.poste3, de_decision=DecisionChoices.PROPOSITION_FE)
# Création du référentiel sous-vivier filiere
cls.ref_sv_fil1 = RefSvFil.objects.create(ref_sv_fil_code='org40b', ref_sv_fil_dom='Dom1', ref_sv_fil_fil='Fil1', ref_sv_fil_cat='Cat1')
# Création des clients de test
cls.c0 = APIClient() # Client de test qui ne sera pas authentifié
cls.c1 = APIClient()
cls.c2 = APIClient()
cls.c3 = APIClient()
cls.c4 = APIClient()
cls.c5 = APIClient()
cls.c6 = APIClient()
cls.c7 = APIClient()
cls.c8 = APIClient()
cls.c9 = APIClient()
cls.c10 = APIClient()
# Authentification des clients de test
cls.c1.force_authenticate(cls.gest1)
cls.c2.force_authenticate(cls.gest2)
cls.c3.force_authenticate(cls.gest3)
cls.c4.force_authenticate(cls.gest4)
cls.c5.force_authenticate(cls.gest5)
cls.c6.force_authenticate(cls.gest6)
cls.c7.force_authenticate(cls.gest7)
cls.c8.force_authenticate(cls.gest8)
cls.c9.force_authenticate(cls.gest9)
cls.c10.force_authenticate(cls.gest10)
cls.list_clients = [cls.c1, cls.c2, cls.c3, cls.c4, cls.c5, cls.c6, cls.c7, cls.c8, cls.c9, cls.c10]
# Création des données utilsées pour les requêtes
cls.patch_datas = {
'data_p1': {
'p_id': 'Poste1',
'p_eip': 'eip2'
},
'data_a1': {
'a_id_sap': 1,
'a_eip': 'eip2'
},
'data_d1': {
'pk': cls.adm1.pk,
'de_decision': DecisionChoices.POSITIONNE
},
'data_p3': {
'p_id': 'Poste3',
'p_eip': 'eip4'
},
'data_a3': {
'a_id_sap': 3,
'a_eip': 'eip4'
},
'data_d3': {
'pk': cls.adm3.pk,
'de_decision': DecisionChoices.POSITIONNE
},
'data_p5': {
'p_id': 'Poste5',
'p_eip': 'eip6'
}
}
cls.put_datas = {
'data_p1': [
{'p_id': 'Poste1',
'p_eip': 'eip2'
},
{'p_id': 'Poste2',
'p_eip': 'eip3'
}
],
'data_a1': [
{'a_id_sap': 1,
'a_eip': 'eip2'
},
{'a_id_sap': 2,
'a_eip': 'eip3'
},
],
'data_p3': [
{'p_id': 'Poste3',
'p_eip': 'eip4'
},
{'p_id': 'Poste4',
'p_eip': 'eip5'
}
],
'data_a3': [
{'a_id_sap': 3,
'a_eip': 'eip4'
},
{'a_id_sap': 4,
'a_eip': 'eip5'
},
]
}
# Définition des liens sur lesquels les requêtes seront effectuées
cls.urls = {
'list_p': reverse('Poste-list'),
'list_a': reverse('Administre-list'),
'list_d': reverse('Decision-list'),
'instance_p1': reverse('Poste-list') + 'Poste1/',
'instance_a1': reverse('Administre-list') + '1/',
'instance_d1': reverse('Decision-list') + '1/',
'instance_p3': reverse('Poste-list') + 'Poste3/',
'instance_a3': reverse('Administre-list') + '3/',
'instance_d3': reverse('Decision-list') + '3/',
'instance_p5': reverse('Poste-list') + 'Poste5/'
}
@classmethod
def tearDownClass(cls):
super().tearDownClass()
for client in cls.list_clients:
client.logout()
def test_get_not_authenticated(self):
"""
Teste la requete GET sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire n'est pas authentifié
"""
# Le test passe mais il y a également interdiction car c5 n'a pas de 'code_niv_org'...
get_resp_p = self.c0.get(path=self.urls['list_p'], follow=True)
get_resp_a = self.c0.get(path=self.urls['list_a'], follow=True)
get_resp_d = self.c0.get(path=self.urls['list_d'], follow=True)
self.assertEqual(get_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_get_ok(self):
"""
Teste la requete GET sur les vues PosteView, AdministreView et DecisionView sachant que :
- Le gestionnaire est authentifié,
- Qu'il a les droits de lecture
"""
get_resp_p = self.c1.get(path=self.urls['list_p'], follow=True)
get_resp_a = self.c1.get(path=self.urls['list_a'], follow=True)
get_resp_d = self.c1.get(path=self.urls['list_d'], follow=True)
self.assertEqual(get_resp_p.status_code, status.HTTP_200_OK)
self.assertEqual(get_resp_a.status_code, status.HTTP_200_OK)
self.assertEqual(get_resp_d.status_code, status.HTTP_200_OK)
def test_get_no_code_niv_org(self):
"""
Teste la requete GET sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire n'a pas d'attribut code_niv_org
"""
get_resp_p = self.c2.get(path=self.urls['list_p'], follow=True)
get_resp_a = self.c2.get(path=self.urls['list_a'], follow=True)
get_resp_d = self.c2.get(path=self.urls['list_d'], follow=True)
self.assertEqual(get_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_get_no_reading_rights(self):
"""
Teste la requete GET sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire n'a pas les droits de lecture
"""
get_resp_p = self.c3.get(path=self.urls['list_p'], follow=True)
get_resp_a = self.c3.get(path=self.urls['list_a'], follow=True)
get_resp_d = self.c3.get(path=self.urls['list_d'], follow=True)
self.assertEqual(get_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_get_not_pcp_not_fil(self):
"""
Teste la requete GET sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire est ni pcp ni filiere
"""
get_resp_p = self.c4.get(path=self.urls['list_p'], follow=True)
get_resp_a = self.c4.get(path=self.urls['list_a'], follow=True)
get_resp_d = self.c4.get(path=self.urls['list_d'], follow=True)
self.assertEqual(get_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(get_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_put_ok_fe_fil(self):
"""
Teste la requete PUT sur des postes et des administrés sachant que :
- Le gestionnaire est authentifié,
- Qu'il a les droits de lecture et d'écriture,
- Qu'il est PCP et FIL,
- Qu'il est associé à deux code_niv_org4,
- Les instances qu'il édite sont dans son FE et dans sa filiere
"""
# Pas de test put pour les décisions car la modification de plusieurs décisions n'est pas possible dans Ogure
put_resp_p = self.c1.put(path=self.urls['list_p'], data=self.put_datas['data_p1'], follow=True)
put_resp_a = self.c1.put(path=self.urls['list_a'], data=self.put_datas['data_a1'], follow=True)
self.assertEqual(put_resp_p.status_code, status.HTTP_200_OK)
self.assertEqual(put_resp_a.status_code, status.HTTP_200_OK)
def test_put_ok_fe(self):
"""
Teste la requete PUT sur des postes et des administrés sachant que :
- Le gestionnaire est authentifié,
- Qu'il a les droits de lecture et d'écriture,
- Qu'il est PCP et FIL,
- Qu'il est associé à un code_niv_org4,
- Les instances qu'il édite sont dans son FE MAIS PAS dans sa FIL
"""
# Pas de test put pour les décisions car la modification de plusieurs décisions n'est pas possible dans Ogure
put_resp_p = self.c5.put(path=self.urls['list_p'], data=self.put_datas['data_p3'], follow=True)
put_resp_a = self.c5.put(path=self.urls['list_a'], data=self.put_datas['data_a3'], follow=True)
self.assertEqual(put_resp_p.status_code, status.HTTP_200_OK)
self.assertEqual(put_resp_a.status_code, status.HTTP_200_OK)
def test_put_no_writing_rights(self):
"""
Teste la requete PUT sur des postes et des administrés sachant qu'il n'a pas les droits d'écriture'
"""
put_resp_p = self.c6.put(path=self.urls['list_p'], data=self.put_datas['data_p1'], follow=True)
put_resp_a = self.c6.put(path=self.urls['list_a'], data=self.put_datas['data_a1'], follow=True)
self.assertEqual(put_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(put_resp_a.status_code, status.HTTP_403_FORBIDDEN)
def test_put_no_fe_no_fil(self):
"""
Teste la requete PUT sur des postes et des administrés sachant qu'il n'a pas de FE ni de FIL associé'
"""
put_resp_p = self.c7.put(path=self.urls['list_p'], data=self.put_datas['data_p1'], follow=True)
put_resp_a = self.c7.put(path=self.urls['list_a'], data=self.put_datas['data_a1'], follow=True)
self.assertEqual(put_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(put_resp_a.status_code, status.HTTP_403_FORBIDDEN)
def test_put_no_instances_ok(self):
"""
Teste la requete PUT sur des postes et des administrés sachant qu'il n'a pas d'instances dans son FE ou dans sa FIL
"""
put_resp_p = self.c8.put(path=self.urls['list_p'], data=self.put_datas['data_p1'], follow=True)
put_resp_a = self.c8.put(path=self.urls['list_a'], data=self.put_datas['data_a1'], follow=True)
self.assertEqual(put_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(put_resp_a.status_code, status.HTTP_403_FORBIDDEN)
def test_put_not_ok(self):
"""
Teste la requete PUT sur des postes et des administrés sachant que les instances modifiées ne sont ni dans le FE ni dans la FIL du gestionnaire
"""
put_resp_p = self.c9.put(path=self.urls['list_p'], data=self.put_datas['data_p1'], follow=True)
put_resp_a = self.c9.put(path=self.urls['list_a'], data=self.put_datas['data_a1'], follow=True)
self.assertEqual(put_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(put_resp_a.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_no_code_niv_org(self):
"""
Teste la requete PATCH sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire n'a pas d'attribut code_niv_org
"""
patch_resp_p = self.c2.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c2.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c2.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_no_reading_rights(self):
"""
Teste la requete PATCH sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire n'a pas les droits de lecture
"""
patch_resp_p = self.c3.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c3.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c3.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_not_pcp_not_fil_not_itd(self):
"""
Teste la requete PATCH sur les vues PosteView, AdministreView et DecisionView sachant que le gestionnaire est ni pcp ni filiere
"""
patch_resp_p = self.c4.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c4.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c4.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_ok_fe_fil(self):
"""
Teste la requete PATCH sur un poste, un administré et une décision sachant que :
- Le gestionnaire est authentifié,
- Qu'il a les droits de lecture et d'écriture,
- Qu'il est PCP et FIL,
- Qu'il est associé à deux code_niv_org4,
- Les instances qu'il édite sont dans son FE et dans sa filiere
"""
patch_resp_p = self.c1.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c1.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c1.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_200_OK)
self.assertEqual(patch_resp_a.status_code, status.HTTP_200_OK)
self.assertEqual(patch_resp_d.status_code, status.HTTP_200_OK)
def test_patch_ok_fe(self):
"""
Teste la requete PATCH sur un poste, un administré et une décision sachant que :
- Le gestionnaire est authentifié,
- Qu'il a les droits de lecture et d'écriture,
- Qu'il est PCP et FIL,
- Qu'il est associé à un code_niv_org4,
- Les instances qu'il édite sont dans son FE MAIS PAS dans sa FIL
"""
patch_resp_p = self.c5.patch(path=self.urls['instance_p3'], data=self.patch_datas['data_p3'], follow=True)
patch_resp_a = self.c5.patch(path=self.urls['instance_a3'], data=self.patch_datas['data_a3'], follow=True)
patch_resp_d = self.c5.patch(path=self.urls['instance_d3'], data=self.patch_datas['data_d3'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_200_OK)
self.assertEqual(patch_resp_a.status_code, status.HTTP_200_OK)
self.assertEqual(patch_resp_d.status_code, status.HTTP_200_OK)
def test_patch_ok_itd(self):
"""
Teste la requete PATCH sur un poste sachant que :
- Le gestionnaire est authentifié,
- Qu'il a les droits de lecture et d'écriture,
- Qu'il est ITD,
- Qu'il est associé à un code_niv_org4,
- Le poste édité est marqué ITD
"""
patch_resp_p = self.c10.patch(path=self.urls['instance_p5'], data=self.patch_datas['data_p5'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_200_OK)
def test_patch_no_writing_rights(self):
"""
Teste la requete PATCH sur des postes et des administrés sachant qu'il n'a pas les droits d'écriture'
"""
patch_resp_p = self.c6.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c6.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c6.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_no_niv_org(self):
"""
Teste la requete PATCH sur des postes et des administrés sachant qu'il n'a pas d'attribut niv_org'
"""
patch_resp_p = self.c7.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c7.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c7.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_no_instances_ok(self):
"""
Teste la requete PATCH sur des postes et des administrés sachant qu'il n'a pas d'instances dans son FE ou dans sa FIL
"""
patch_resp_p = self.c8.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c8.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c8.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_not_ok_fe_fil(self):
"""
Teste la requete PATCH sur un poste, un administré et une décision sachant que les instances modifiées ne sont ni dans le FE ni dans la FIL du gestionnaire
"""
patch_resp_p = self.c9.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
patch_resp_a = self.c9.patch(path=self.urls['instance_a1'], data=self.patch_datas['data_a1'], follow=True)
patch_resp_d = self.c9.patch(path=self.urls['instance_d1'], data=self.patch_datas['data_d1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(patch_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_patch_not_ok_itd(self):
"""
Teste la requete PATCH sur un poste sachant que l'instance modifiée n'est pas marquée ITD
"""
patch_resp_p = self.c10.patch(path=self.urls['instance_p1'], data=self.patch_datas['data_p1'], follow=True)
self.assertEqual(patch_resp_p.status_code, status.HTTP_403_FORBIDDEN)
def test_delete_ok(self):
delete_resp_p = self.c1.delete(path=self.urls['instance_p1'], follow=True)
delete_resp_a = self.c1.delete(path=self.urls['instance_a1'], follow=True)
self.assertEqual(delete_resp_p.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(delete_resp_a.status_code, status.HTTP_204_NO_CONTENT)
def test_delete_decision_ok(self):
# On teste la suppression de décision séparément des autres suppressions sans quoi instance_d ferait référence à un administré supprimé
delete_resp_d = self.c1.delete(path=self.urls['instance_d1'], follow=True)
self.assertEqual(delete_resp_d.status_code, status.HTTP_204_NO_CONTENT)
def test_delete_not_ok(self):
# delete_resp_p = self.c9.delete(path=self.urls['instance_p1'], follow=True)
# delete_resp_a = self.c9.delete(path=self.urls['instance_a1'], follow=True)
delete_resp_d = self.c9.delete(path=self.urls['instance_d1'], follow=True)
# self.assertEqual(delete_resp_p.status_code, status.HTTP_403_FORBIDDEN)
# self.assertEqual(delete_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(delete_resp_d.status_code, status.HTTP_403_FORBIDDEN)
def test_head_ok(self):
head_resp_p = self.c1.head(path=self.urls['list_p'], follow=True)
head_resp_a = self.c1.head(path=self.urls['list_a'], follow=True)
head_resp_d = self.c1.head(path=self.urls['list_d'], follow=True)
self.assertEqual(head_resp_p.status_code, status.HTTP_200_OK)
self.assertEqual(head_resp_a.status_code, status.HTTP_200_OK)
self.assertEqual(head_resp_d.status_code, status.HTTP_200_OK)
def test_head_no_reading_rights(self):
head_resp_p = self.c3.head(path=self.urls['list_p'], follow=True)
head_resp_a = self.c3.head(path=self.urls['list_a'], follow=True)
head_resp_d = self.c3.head(path=self.urls['list_d'], follow=True)
self.assertEqual(head_resp_p.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(head_resp_a.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(head_resp_d.status_code, status.HTTP_403_FORBIDDEN)

View File

@@ -0,0 +1,140 @@
"""
Ce fichier n'est utilisé que pour les tests.
"""
# from ..utils_scoring import notations_test
# from django.test import TestCase
# from numpy import fliplr
# from numpy.core.fromnumeric import shape
# import pandas as pd
# from .utils_scoring import encoding,notePonderee,notations_test
# Create your tests here.
# class InsertModelTest(TestCase):
# # Fonction pour la table Garnisons
# def test_scoring(self):
# """Pour tester la fonction de scoring
# """
# mut_test = notations_test()
# data_test = mut_test.to_dict('records')
# data_source = [{
# 'mu_id': 0,
# 'poste_id': 1,
# 'administres_id_sap': 9984,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.5,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''},
# {
# 'mu_id': 0,
# 'poste_id': 1,
# 'administres_id_sap': 56754,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.0,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''},
# {
# 'mu_id': 0,
# 'poste_id': 1,
# 'administres_id_sap': 78905,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.0,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# },
# {
# 'mu_id': 0,
# 'poste_id': 2,
# 'administres_id_sap': 9984,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.0,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# },
# {
# 'mu_id': 0,
# 'poste_id': 2,
# 'administres_id_sap': 56754,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.5,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# },
# {
# 'mu_id': 0,
# 'poste_id': 2,
# 'administres_id_sap': 78905,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.0,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# },
# {
# 'mu_id': 0,
# 'poste_id': 3,
# 'administres_id_sap': 9984,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.0,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# },
# {
# 'mu_id': 0,
# 'poste_id': 3,
# 'administres_id_sap': 56754,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.0,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# },
# {
# 'mu_id': 0,
# 'poste_id': 3,
# 'administres_id_sap': 78905,
# 'pam_id': 1,
# 'mu_date_execution': '14/06/2021',
# 'mu_note_militaire': 0.5,
# 'mu_flag_cple_ideal': 0,
# 'mu_decision': '',
# 'mu_date_decision': '',
# 'mu_notes_gestionnaire': '',
# 'mu_notes_partagees': ''
# }]
# if data_source == data_test :
# test_value = True
# else :
# test_value = False
# self.assertIs(test_value,True)

View File

@@ -0,0 +1,3 @@
from .administre import *
from .calcul import *
from .poste import *

View File

@@ -0,0 +1,39 @@
from ...models import Administre, StatutPamChoices
from django.db.utils import IntegrityError
from django.test import TestCase
class AdministreTestCase(TestCase):
def test_attributes_statut_pam(self):
""" vérifie que le comportement reste correct avec l'ajout de nouveaux attributs """
attr_calc = 'calc_enabled'
attr_dec = 'dec_enabled'
for choice in StatutPamChoices:
self.assertIsInstance(choice.label, str, f"{choice} : le libellé n'a pas le bon type")
self.assertIsInstance(choice.value, str, f"{choice} : la valeur n'a pas le bon type")
self.assertEqual(choice.value, choice, f"{choice} : n'est pas égal à sa valeur")
self.assertIsInstance(getattr(choice, attr_calc), bool, f"{choice} : l'attribut {attr_calc} n'a pas le bon type")
self.assertIsInstance(getattr(choice, attr_dec), bool, f"{choice} : l'attribut {attr_dec} n'a pas le bon type")
def test_constraint_statut_pam(self):
""" vérifie qu'il n'est pas possible de stocker n'importe quoi en base """
valides = StatutPamChoices.values
invalide = 'autre'
def given():
self.assertTrue(valides, 'il devrait exister des choix valides')
self.assertNotIn(invalide, valides, 'le test nécessite une valeur invalide')
given()
id_sap = 1
# valide : création OK
for valide in valides:
Administre.objects.create(pk=id_sap, a_statut_pam=valide)
id_sap = id_sap + 1
# invalide : création KO
with self.assertRaises(IntegrityError):
Administre.objects.create(pk=id_sap, a_statut_pam=invalide)

View File

@@ -0,0 +1,34 @@
from django.db.utils import IntegrityError
from django.test import TestCase
from django.utils import timezone
from ...models.calcul import Calcul, SousVivier
from ...models.calcul import StatutCalculChoices as StatutCalcul
class CalculTestCase(TestCase):
@classmethod
def setUpTestData(cls):
for i in range(1, 15):
SousVivier.objects.create(pk=str(i))
def test_constraint_statut(self):
valides = StatutCalcul.values
invalide = 'autre'
def given():
self.assertTrue(valides, 'il devrait exister des choix valides')
self.assertNotIn(invalide, valides, 'le test nécessite une valeur invalide')
given()
i = 1
# valide : création OK
for valide in valides:
Calcul.objects.create(pk=str(i), ca_date_debut=timezone.now(), ca_statut=valide, ca_statut_pourcentage=0)
i = i + 1
# invalide : création KO
with self.assertRaises(IntegrityError):
Calcul.objects.create(pk=str(i), ca_date_debut=timezone.now(), ca_statut=invalide, ca_statut_pourcentage=0)

View File

@@ -0,0 +1,34 @@
from ...models import AvisPosteChoices as AvisPoste, Poste, PropositionsArmementChoices as PropositionsArmement
from django.db.utils import IntegrityError
from django.test import TestCase
class PosteTestCase(TestCase):
def test_attributes_avis(self):
""" vérifie que le comportement reste correct avec l'ajout de nouveaux attributs """
attr_calc = 'calc_enabled'
attr_dec = 'dec_enabled'
for choice in AvisPoste:
self.assertIsInstance(choice.label, str, f"{choice} : le libellé n'a pas le bon type")
self.assertIsInstance(choice.value, str, f"{choice} : la valeur n'a pas le bon type")
self.assertEqual(choice.value, choice, f"{choice} : n'est pas égal à sa valeur")
self.assertIsInstance(getattr(choice, attr_calc), bool, f"{choice} : l'attribut {attr_calc} n'a pas le bon type")
self.assertIsInstance(getattr(choice, attr_dec), bool, f"{choice} : l'attribut {attr_dec} n'a pas le bon type")
def test_constraint_propositions_armement(self):
valides = PropositionsArmement.values
invalide = 'autre'
def given():
self.assertTrue(valides, 'il devrait exister des choix valides')
self.assertNotIn(invalide, valides, 'le test nécessite une valeur invalide')
given()
# valide : création OK
for valide in valides:
Poste.objects.create(pk=valide, propositions_armement=valide)
# invalide : création KO
with self.assertRaises(IntegrityError):
Poste.objects.create(pk=invalide, propositions_armement=invalide)

View File

@@ -0,0 +1,75 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
import pandas as pd
from rest_framework.test import APIClient, APITestCase
from ...utils_extraction import to_table_pam
from ...models import (Administre, CustomUser, Decision, DecisionChoices,
Domaine, Filiere, FormationEmploi, Garnison, Poste,
RefGest, RefOrg, RefSvFil, SousVivier, Administres_Pams, Postes_Pams, PAM)
import datetime
class PamTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.date = datetime.date(2023,1,1)
cls.pam_2022 = PAM.objects.create(pam_id='2022',pam_date=datetime.date(2022,7,15), pam_libelle= "PAM de l'année 2022", pam_statut='PAM en cours')
cls.pam_2023 = PAM.objects.create(pam_id='2023',pam_date=datetime.date(2022,7,15), pam_libelle= "PAM de l'année 2023", pam_statut='PAM A+1')
def test_extraction_pam(self):
"""
Construit la table PAM qui servira à récuperer l'année du PAM et piloter l'affichage des données en fonction du bon pam pour chaque tables
Cloture le PAM en le sauvegardant dans un fichier excel
"""
annee_pam = str(self.date.year)
annee_pam_suivant = str(self.date.year + 1)
pam=pd.DataFrame(columns = ['pam_id','pam_date', 'pam_libelle','pam_statut'])
pam_id = annee_pam
pam_id_suivant = annee_pam_suivant
pam_libelle = f"PAM de l'année {annee_pam}"
pam_libelle_suivant = f"PAM de l'année {annee_pam_suivant}"
pam_date = self.date
pam_date_suivant = pam_date
pam_statut = "PAM en cours"
pam_statut_suivant = "PAM A+1"
sorg_id = "SORG"
sorg_libelle = "SORG"
sorg_statut = "SORG"
pam['pam_id'] = [pam_id,pam_id_suivant, sorg_id]
pam['pam_date'] = [pam_date,pam_date_suivant, pam_date]
pam['pam_libelle'] = [pam_libelle,pam_libelle_suivant,sorg_libelle]
pam['pam_statut'] = [pam_statut,pam_statut_suivant,sorg_statut]
return pam
def test_insertion_pam(self):
self.liste_create = []
self.liste_update = []
self.update_header = ['pam_date','pam_libelle','pam_statut']
self.df = self.test_extraction_pam()
for i in range(self.df.shape[0]):
self.pams = PAM.objects.filter(pam_id=self.df.at[i,'pam_id'])
self.pam = PAM(pam_id=self.df.at[i, 'pam_id'],pam_date=self.df.at[i, 'pam_date'], pam_libelle=self.df.at[i, 'pam_libelle'],
pam_statut=self.df.at[i, 'pam_statut'])
if self.pam.pam_id in self.pams.values_list('pam_id', flat = True):
self.liste_update.append(self.pam)
else:
cloture=PAM.objects.filter(pam_statut="PAM en cours")
cloture.update(pam_statut ="PAM clôturé")
self.liste_create.append(self.pam)
if self.liste_create:
PAM.objects.bulk_create(self.liste_create)
if self.liste_update:
PAM.objects.bulk_update(self.liste_update, fields=self.update_header)

View File

@@ -0,0 +1,6 @@
from .alimentation import *
from .decisions import *
from .decorators import *
from .functions import *
from .logging import *
from .predicates import *

View File

@@ -0,0 +1,56 @@
from django.test import SimpleTestCase
from ...utils.alimentation import BOCols as Cols
class BOColsTestCase(SimpleTestCase):
""" tests pour BOCols """
def test_attributes(self):
""" vérifie que les nouveaux attributs sont renseignés correctement """
for col in Cols:
self.assertIsInstance(col.value, str, f"{col} : la valeur n'a pas le bon type")
def test_columns(self):
""" vérifie les colonnes renvoyées """
members = list(Cols)
col_names = [col.value for col in members]
self.assertCountEqual(col_names, Cols.columns(), "les noms de colonnes (sans argument) ne sont pas les bons")
col = members[0]
col_names = [col.value]
self.assertCountEqual(col_names, Cols.columns(col), "les noms de colonnes (un argument) ne sont pas les bons")
col_list = members[0:2]
col_names = [col.value for col in col_list]
self.assertCountEqual(col_names, Cols.columns(*col_list), "les noms de colonnes (plusieurs arguments) ne sont pas les bons")
def test_col_mapping(self):
""" vérifie les correspondances renvoyés """
members = list(Cols)
std_mapping = {'a': 1, 'b': 'test'}
enum_mapping = {members[0]: (2, 3), members[1]: members[1].name}
mapping_before = {**enum_mapping, **std_mapping}
mapping_after = Cols.col_mapping(mapping_before)
self.assertTrue(all(k in mapping_after for k in std_mapping.keys()), "toutes les clés standard doivent faire partie du mapping final")
self.assertTrue(all(v == mapping_after.get(k) for k, v in std_mapping.items()), "les valeurs de clés standard ne doivent pas changer")
self.assertTrue(all(k.value in mapping_after for k in enum_mapping.keys()), f"tous les noms de colonnes de clés de type {Cols.__name__} doivent faire partie du mapping final")
self.assertTrue(all(v == mapping_after.get(k.value) for k, v in enum_mapping.items()), f"les valeurs de clés de type {Cols.__name__} ne doivent pas changer")
def test_converters(self):
""" vérifie les convertisseurs renvoyés """
members = list(Cols)
col_names = [col.value for col in members if col.converter]
self.assertCountEqual(col_names, Cols.converters().keys(), "les convertisseurs (sans argument) ne sont pas les bons")
col = members[0]
col_names = [col.value]
self.assertCountEqual(col_names, Cols.converters(col).keys(), "les convertisseurs (un argument) ne sont pas les bons")
col_list = members[0:2]
col_names = [col.value for col in col_list]
self.assertCountEqual(col_names, Cols.converters(*col_list).keys(), "les convertisseurs (plusieurs arguments) ne sont pas les bons")

View File

@@ -0,0 +1,283 @@
from typing import Callable, List, Optional, Tuple, Union
from unittest import mock
from django.test import TestCase
from ...models import Administre
from ...models import AvisPosteChoices as AvisPoste
from ...models import CustomUser, Decision, DecisionChoices, Poste, SousVivier
from ...models import StatutPamChoices as StatutPam
from ...utils.decisions import (DECISIONS, KEY_CHOICES, KEY_CREATE,
KEY_PROFILES, KEY_UPDATE, ExtraDecisions,
get_all_decisions, get_available_decisions)
from ...utils.permissions import (KEY_READ, KEY_WRITE, Profiles,
get_profiles_by_adm)
class DecisionsTest(TestCase):
def setUp(self):
""" vérifie qu'il n'existe aucun administré ou poste avant le test """
self.assertEqual(Administre.objects.exists(), False, "pas d'administré avant un test")
self.assertEqual(Poste.objects.exists(), False, "pas de poste avant un test")
def tearDown(self):
""" supprime tous les administrés, les postes, les décisions en fin de test """
Decision.objects.all().delete()
Poste.objects.all().delete()
Administre.objects.all().delete()
def __load_administres(self) -> List[Administre]:
""" charge les administrés de la base à partir des IDs """
self.assertTrue(self.adm_ids, "il est nécessaire de renseigner des IDs d'administrés pour le chargement")
return list(Administre.objects.filter(pk__in=self.adm_ids or ()))
def __setup_decisions(self, decision: Optional[Union[DecisionChoices, ExtraDecisions]]) -> None:
""" supprime et recrée (si nécessaire) les postes et les décisions des administrés """
Decision.objects.all().delete()
Poste.objects.all().delete()
if decision and decision != ExtraDecisions.EMPTY:
for adm_id in self.adm_ids:
poste = Poste.objects.create(pk=str(adm_id), p_avis=AvisPoste.P1)
Decision.objects.create(administre_id=adm_id, poste=poste, de_decision=decision)
def __do_for_profiles(self, action: Callable[[Profiles], None], profiles: Tuple[Profiles], decisions: Tuple[Union[DecisionChoices, ExtraDecisions]] = ()) -> None:
"""
exécute une action (un test) avec un sous-test :
- soit pour chaque profil (utilise un mock)
- soit pour chaque profil et chaque statut de décision (utilise un mock)
implicite : self.adm_ids
"""
def run_subTest(profile: Profiles, decision: Optional[Union[DecisionChoices, ExtraDecisions]] = None) -> None:
with self.subTest(profile=profile.name, **({'decision': decision.name} if decision else {})):
@mock.patch(
f'{get_available_decisions.__module__}.{get_profiles_by_adm.__name__}',
return_value={adm_id: {KEY_READ: (), KEY_WRITE: (profile,)} for adm_id in self.adm_ids}
)
def do_with_profile(mock):
action(profile, decision)
do_with_profile()
for p in profiles:
if decisions:
for decision in decisions:
self.__setup_decisions(decision)
run_subTest(p, decision)
else:
run_subTest(p)
def __assert_common(self, decisions_by_adm):
"""
assertions communes à propos des décisions renvoyées pour les administrés
implicite : self.adm_ids
"""
self.assertIsInstance(decisions_by_adm, dict, "le résultat n'est jamais None")
for adm_id in self.adm_ids:
self.assertTrue(adm_id in decisions_by_adm, "tous les administrés doivent être présents dans le résultat")
decisions = decisions_by_adm.get(adm_id)
self.assertIsInstance(decisions, dict, "le format des décisions est un dictionnaire")
for key in (KEY_CREATE, KEY_UPDATE):
self.assertTrue(key in decisions, f"les décisions doivent contenir la clé {key}")
self.assertIsInstance(decisions.get(key), tuple, f"la valeur de {key} des décisions n'a pas le type attendu")
def test_tree_attribute(self):
""" vérifie que le comportement reste correct avec l'ajout de nouveaux attributs """
attr_trees = 'trees'
for choice in DecisionChoices:
self.assertIsInstance(choice.label, str, f"{choice} : le libellé n'a pas le bon type")
self.assertIsInstance(choice.value, str, f"{choice} : la valeur n'a pas le bon type")
self.assertEqual(choice.value, choice, f"{choice} : n'est pas égal à sa valeur")
self.assertIsInstance(getattr(choice, attr_trees), tuple, f"{choice} : l'attribut {attr_trees} n'a pas le bon type")
def test_champ_decisions(self):
""" vérifie la structure de DECISIONS """
to_test = DECISIONS
self.assertIsInstance(to_test, dict, "la structure n'a pas le type attendu")
self.assertTrue(to_test, "la structure ne doit pas être vide")
for key, value in to_test.items():
self.assertTrue(key is None or isinstance(key, DecisionChoices), f"la décision {key} n'a pas le type attendu")
self.assertIsInstance(value, dict, f"la valeur pour la décision {key} n'a pas le type attendu")
profiles = value.get(KEY_PROFILES)
self.assertTrue(profiles is None or isinstance(profiles, tuple), f"la valeur de {KEY_PROFILES} de la valeur pour la décision {key} n'a pas le type attendu")
for p in profiles:
self.assertIsInstance(p, Profiles, f"un élément de la clé {KEY_PROFILES} de la valeur pour la décision {key} n'a pas le type attendu")
choices = value.get(KEY_CHOICES)
self.assertTrue(choices is None or isinstance(choices, tuple), f"la valeur de {KEY_CHOICES} de la valeur pour la décision {key} n'a pas le type attendu")
for c in choices:
self.assertIsInstance(c, (DecisionChoices, ExtraDecisions), f"un élément de la clé {KEY_CHOICES} de la valeur pour la décision {key} n'a pas le type attendu")
def test_get_all_decisions(self):
""" vérifie les valeurs de get_all_decisions """
to_test = get_all_decisions()
self.assertIsInstance(to_test, tuple, "le résultat n'a pas le type attendu")
self.assertTrue(to_test, "le résultat ne doit pas être vide")
for value in to_test:
self.assertIsInstance(value, DecisionChoices, f"la valeur {value} n'a pas le type attendu")
def test_pam_wihout_choice(self):
""" pour ces statuts PAM il n'y a aucun choix """
user = CustomUser()
self.adm_ids = tuple(a.a_id_sap for a in (
Administre.objects.create(pk=i + 1, a_statut_pam=status) for i, status in enumerate(StatutPam) if not status.dec_enabled
))
def action(_p, _d):
decisions_by_adm = get_available_decisions(self.__load_administres(), user=user)
self.__assert_common(decisions_by_adm)
for adm_id in self.adm_ids:
decisions = decisions_by_adm.get(adm_id)
self.assertFalse(decisions.get(KEY_CREATE))
self.assertFalse(decisions.get(KEY_UPDATE))
self.__do_for_profiles(action, profiles=tuple(Profiles))
def test_pam_with_choices(self):
""" pour ces statuts PAM c'est l'arbre de décision qui est utilisé """
user = CustomUser()
self.adm_ids = tuple(a.a_id_sap for a in (
Administre.objects.create(pk=i + 1, a_statut_pam=status) for i, status in enumerate(StatutPam) if status.dec_enabled
))
D = DecisionChoices
EX = ExtraDecisions
def action(p, d):
decisions_by_adm = get_available_decisions(self.__load_administres(), user=user)
self.__assert_common(decisions_by_adm)
for adm_id in self.adm_ids:
decisions = decisions_by_adm.get(adm_id)
# create
decisions_create = decisions.get(KEY_CREATE)
if p == Profiles.FILIERE:
self.assertCountEqual(decisions_create, (D.PROPOSITION_FE, D.HME_PROPOSITION_VIVIER))
else:
self.assertFalse(decisions_create)
# update
decisions_update = decisions.get(KEY_UPDATE)
if not d and p == Profiles.FILIERE:
self.assertCountEqual(decisions_update, (D.PROPOSITION_FE, D.HME_PROPOSITION_VIVIER))
elif d == D.PROPOSITION_FE and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
self.assertCountEqual(decisions_update, (D.DIALOGUE_EN_COURS, D.FOREMP_EN_COURS))
elif d == D.DIALOGUE_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
self.assertCountEqual(decisions_update, (D.DIALOGUE_TERMINE, D.DIALOGUE_INFRUCTUEUX))
elif d == D.DIALOGUE_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
self.assertCountEqual(decisions_update, (D.FOREMP_EN_COURS,))
elif d == D.DIALOGUE_INFRUCTUEUX and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
self.assertCountEqual(decisions_update, (D.FOREMP_EN_COURS, D.REMIS_A_DISPOSITION))
elif d == D.FOREMP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
self.assertCountEqual(decisions_update, (D.FOREMP_TERMINE,))
elif d == D.FOREMP_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL):
self.assertCountEqual(decisions_update, (D.PREPOSITIONNE, D.REMIS_A_DISPOSITION))
elif d == D.PREPOSITIONNE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.POSITIONNE, D.REMIS_A_DISPOSITION))
elif d == D.POSITIONNE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.OMIP_EN_COURS, D.OMI_EN_COURS, D.REMIS_A_DISPOSITION))
elif d == D.OMIP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.OMIP_TERMINE, D.REMIS_A_DISPOSITION))
elif d == D.OMIP_TERMINE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.ATTENTE_AVIONAGE, D.OMI_EN_COURS, D.REMIS_A_DISPOSITION))
elif d == D.ATTENTE_AVIONAGE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.OMI_EN_COURS,))
elif d == D.OMI_EN_COURS and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.OMI_ACTIVE, D.REMIS_A_DISPOSITION))
elif d == D.OMI_ACTIVE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.OMI_ANNULE,))
elif d == D.OMI_ANNULE and p in (Profiles.PCP, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.REMIS_A_DISPOSITION,))
elif d == D.REMIS_A_DISPOSITION and p == Profiles.FILIERE:
self.assertCountEqual(decisions_update, (EX.EMPTY,))
elif d == D.HME_PROPOSITION_VIVIER and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_DIALOGUE_INITIE,))
elif d == D.HME_ETUDE_DESISTEMENT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_DESISTEMENT,))
elif d == D.HME_DESISTEMENT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.REMIS_A_DISPOSITION,))
elif d == D.HME_DIALOGUE_INITIE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_DIALOGUE_EN_COURS,))
elif d == D.HME_DIALOGUE_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_DIALOGUE_TERMINE, D.HME_DIALOGUE_INFRUCTUEUX))
elif d == D.HME_DIALOGUE_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_PREPOSITIONNE,))
elif d == D.HME_DIALOGUE_INFRUCTUEUX and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_ETUDE_DESISTEMENT, D.HME_PREPOSITIONNE, D.REMIS_A_DISPOSITION))
elif d == D.HME_FOREMP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_FOREMP_TERMINE, D.HME_OMI_EN_COURS))
elif d == D.HME_FOREMP_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_OMIP_EN_COURS, D.HME_OMI_EN_COURS))
elif d == D.HME_PREPOSITIONNE and p == Profiles.HME:
self.assertCountEqual(decisions_update, (D.HME_VALIDATION_EXPERT, D.HME_REFUS_EXPERT))
elif d == D.HME_POSITIONNE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_OMIP_EN_COURS, D.HME_OMI_EN_COURS, D.HME_FOREMP_EN_COURS))
elif d == D.HME_VALIDATION_EXPERT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_POSITIONNE,))
elif d == D.HME_REFUS_EXPERT and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_PREPOSITIONNE, D.REMIS_A_DISPOSITION))
elif d == D.HME_OMIP_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_OMIP_TERMINE,))
elif d == D.HME_OMIP_TERMINE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_ATTENTE_AVIONAGE, D.HME_OMI_EN_COURS))
elif d == D.HME_ATTENTE_AVIONAGE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_OMI_EN_COURS,))
elif d == D.HME_OMI_EN_COURS and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, (D.HME_OMI_ACTIVE,))
elif d == D.HME_OMI_ACTIVE and p in (Profiles.PCP, Profiles.PCP_ACTUEL, Profiles.PCP_FUTUR):
self.assertCountEqual(decisions_update, ())
else:
self.assertFalse(decisions_update)
self.__do_for_profiles(action, profiles=tuple(Profiles), decisions=(EX.EMPTY, *get_all_decisions()))

View File

@@ -0,0 +1,85 @@
from .samples import MyClass
from ...utils.decorators import class_logger, decorate_functions, CLASS_ATTR_LOGGER
from ...utils.functions import find_class
from ...utils.logging import get_logger_name
from ...utils.predicates import func_name_is_in
from django.test import SimpleTestCase
from typing import Tuple
import functools
def double_result(func):
""" décorateur pour doubler les résultats """
@functools.wraps(func)
def inner(*args, **kwargs):
return 2 * func(*args, **kwargs)
return inner
class DecoratorTest(SimpleTestCase):
def test_decorate_functions(self):
num = 5
a = 1
b = 2
res = a + b
res_x2 = res * 2
def given():
msg = 'le résultat initial est bien celui attendu'
self.assertEqual(MyClass.class_met(a, b), res, msg)
self.assertEqual(MyClass.static_met(a, b), res, msg)
self.assertEqual(MyClass(num).std_met(a, b), res, msg)
given()
# tout est décoré
Type = decorate_functions(double_result, lambda func: True)(MyClass)
self.assertEqual(Type.class_met(a, b), res_x2)
self.assertEqual(Type.static_met(a, b), res_x2)
self.assertEqual(Type(num).std_met(a, b), res_x2)
# une seule méthode est décorée (1ère)
Type = decorate_functions(double_result, func_name_is_in(MyClass.class_met.__name__,))(MyClass)
self.assertEqual(Type.class_met(a, b), res_x2)
self.assertEqual(Type.static_met(a, b), res)
self.assertEqual(Type(num).std_met(a, b), res)
# une seule méthode est décorée (2ème)
Type = decorate_functions(double_result, func_name_is_in(MyClass.static_met.__name__,))(MyClass)
self.assertEqual(Type.class_met(a, b), res)
self.assertEqual(Type.static_met(a, b), res_x2)
self.assertEqual(Type(num).std_met(a, b), res)
# une seule méthode est décorée (3ème)
Type = decorate_functions(double_result, func_name_is_in(MyClass.std_met.__name__,))(MyClass)
self.assertEqual(Type.class_met(a, b), res)
self.assertEqual(Type.static_met(a, b), res)
self.assertEqual(Type(num).std_met(a, b), res_x2)
# une seule méthode est décorée deux fois (3ème)
Type = decorate_functions(double_result, func_name_is_in(MyClass.std_met.__name__,))(Type)
self.assertEqual(Type.class_met(a, b), res)
self.assertEqual(Type.static_met(a, b), res)
self.assertEqual(Type(num).std_met(a, b), 2 * res_x2)
# pas de changement au niveau de la classe d'origine
self.assertEqual(MyClass.class_met(a, b), res)
self.assertEqual(MyClass.static_met(a, b), res)
self.assertEqual(MyClass(num).std_met(a, b), res)
def test_class_logger(self):
def given():
self.assertEqual(hasattr(MyClass, CLASS_ATTR_LOGGER), False, "la classe initiale n'a pas de logger")
given()
Type = class_logger(MyClass)
self.assertEqual(hasattr(Type, CLASS_ATTR_LOGGER), True)
self.assertEqual(getattr(Type, CLASS_ATTR_LOGGER).name, get_logger_name(MyClass))
# 2ème décoration
Type = class_logger(Type)
self.assertEqual(hasattr(Type, CLASS_ATTR_LOGGER), True)
self.assertEqual(getattr(Type, CLASS_ATTR_LOGGER).name, get_logger_name(MyClass))
# pas de changement au niveau de la classe d'origine
self.assertEqual(hasattr(MyClass, CLASS_ATTR_LOGGER), False)

View File

@@ -0,0 +1,30 @@
from .samples import my_func, MyClass
from ...utils.functions import find_class
from django.test import SimpleTestCase
class FunctionsTest(SimpleTestCase):
def test_find_class(self):
MainType = MyClass
main_type = MainType(11)
SubType = MyClass.MySubClass
sub_type = SubType(13)
def local_func(a: int, b: int):
return a + b
# fonction native : None
self.assertIsNone(find_class(str.join))
# fonctions : None
self.assertIsNone(find_class(local_func))
self.assertIsNone(find_class(my_func))
# méthodes
self.assertEqual(find_class(MainType.class_met), MainType)
self.assertEqual(find_class(MainType.static_met), MainType)
self.assertEqual(find_class(main_type.std_met), MainType)
self.assertEqual(find_class(SubType.sub_class_met), SubType)
self.assertEqual(find_class(SubType.sub_static_met), SubType)
self.assertEqual(find_class(sub_type.sub_std_met), SubType)

View File

@@ -0,0 +1,104 @@
import random
import string
from unittest import mock
from django.test import SimpleTestCase
from ...utils.logging import (TAG_DATA_FEED, TAG_PERF, get_logger,
get_logger_name)
from .samples import MyClass, my_func
class LoggingTest(SimpleTestCase):
def test_get_logger_name(self):
module_name = __name__
ext_module_name = my_func.__module__
MainType = MyClass
main_type = MainType(11)
main_type_logger = f'{ext_module_name}.{MainType.__qualname__}'
SubType = MyClass.MySubClass
sub_type = SubType(13)
sub_type_logger = f'{ext_module_name}.{SubType.__qualname__}'
def local_func(a: int, b: int):
return a + b
# None
self.assertIsNone(get_logger_name())
self.assertIsNone(get_logger_name(None))
# classes natives
self.assertIsNone(get_logger_name(str))
self.assertIsNone(get_logger_name(int))
self.assertIsNone(get_logger_name(dict))
self.assertIsNone(get_logger_name(list))
self.assertIsNone(get_logger_name(tuple))
# instances de classes natives (mais pas str)
self.assertIsNone(get_logger_name(1))
self.assertIsNone(get_logger_name({'a': 'b'}))
self.assertIsNone(get_logger_name(['c']))
self.assertIsNone(get_logger_name(('d',)))
# fonctions natives
self.assertIsNone(get_logger_name(str.join))
# str
self.assertEqual(get_logger_name('test'), 'test')
self.assertEqual(get_logger_name(''), None)
# classes
self.assertEqual(get_logger_name(MainType), main_type_logger)
self.assertEqual(get_logger_name(SubType), sub_type_logger)
# instances de classes
self.assertEqual(get_logger_name(main_type), main_type_logger)
self.assertEqual(get_logger_name(sub_type), sub_type_logger)
# fonctions et méthodes
self.assertEqual(get_logger_name(local_func), module_name)
self.assertEqual(get_logger_name(my_func), ext_module_name)
self.assertEqual(get_logger_name(MainType.class_met), main_type_logger)
self.assertEqual(get_logger_name(MainType.static_met), main_type_logger)
self.assertEqual(get_logger_name(main_type.std_met), main_type_logger)
self.assertEqual(get_logger_name(SubType.sub_class_met), sub_type_logger)
self.assertEqual(get_logger_name(SubType.sub_static_met), sub_type_logger)
self.assertEqual(get_logger_name(sub_type.sub_std_met), sub_type_logger)
def test_get_logger_without_tags(self):
func_1 = MyClass.class_met
func_2 = MyClass.static_met
def given():
self.assertEqual(get_logger_name(func_1), get_logger_name(func_2), 'le nom de logger doit être le même')
given()
msg = "l'instance doit être la même"
logger_name = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
self.assertIs(get_logger(logger_name), get_logger(logger_name), msg)
self.assertIs(get_logger(func_1), get_logger(func_2), msg)
def test_get_logger_with_tags(self):
func_1 = MyClass.class_met
func_2 = MyClass.static_met
def given():
self.assertEqual(get_logger_name(func_1), get_logger_name(func_2), 'le nom de logger doit être le même')
given()
msg = "l'instance doit être la même"
for tags in [TAG_PERF, TAG_DATA_FEED, ('1', '2'), ['3', '4'], set(['5', '6'])]:
with self.subTest(tags=tags):
logger_name = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
self.assertIs(get_logger(logger_name, tags), get_logger(logger_name, tags), msg)
self.assertIs(get_logger(func_1, tags), get_logger(func_2, tags), msg)
prev_logger = None
logger_name = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
for i, tags in enumerate([('1', '2', '3', '4'), ['3', '4', '1', '2'], set(['2', '1', '3', '4', '1'])]):
curr_logger = get_logger(logger_name, tags)
if i > 0:
self.assertIs(curr_logger, prev_logger, msg)
prev_logger = get_logger(logger_name, tags)

View File

@@ -0,0 +1,127 @@
from .samples import my_func, _my_private_func, MyClass
from ...utils.predicates import func_class_name_is_in, func_class_name_is_not_in, func_name_is_in, func_name_is_not_in, func_is_public
from django.test import SimpleTestCase
class PredicatesTest(SimpleTestCase):
def __test_func_class_name_is_in(self, negation: bool = False):
""" teste func_class_name_is_in et func_class_name_is_not_in """
func = func_class_name_is_not_in if negation else func_class_name_is_in
result = negation is not True
own_name = self.__class__.__name__
MainType = MyClass
main_type = MainType(11)
main_type_name = MainType.__name__
SubType = MyClass.MySubClass
sub_type = SubType(13)
sub_type_name = SubType.__name__
def local_func(a: int, b: int):
return a + b
# fonction native : None
self.assertEqual(func(str.__name__)(str.join), False)
# fonction locale : None
self.assertEqual(func(own_name)(local_func), False)
# méthodes
predicate = func(main_type_name)
self.assertEqual(predicate(MainType.class_met), result)
self.assertEqual(predicate(MainType.static_met), result)
self.assertEqual(predicate(main_type.std_met), result)
self.assertEqual(predicate(SubType.sub_class_met), not result)
self.assertEqual(predicate(SubType.sub_static_met), not result)
self.assertEqual(predicate(sub_type.sub_std_met), not result)
predicate = func(sub_type_name)
self.assertEqual(predicate(MainType.class_met), not result)
self.assertEqual(predicate(MainType.static_met), not result)
self.assertEqual(predicate(main_type.std_met), not result)
self.assertEqual(predicate(SubType.sub_class_met), result)
self.assertEqual(predicate(SubType.sub_static_met), result)
self.assertEqual(predicate(sub_type.sub_std_met), result)
def __test_func_name_is_in(self, negation: bool = False):
""" teste func_name_is_in et func_name_is_not_in """
func = func_name_is_not_in if negation else func_name_is_in
result = negation is not True
MainType = MyClass
main_type = MainType(11)
SubType = MyClass.MySubClass
sub_type = SubType(13)
def local_func(a: int, b: int):
return a + b
# fonction native
self.assertEqual(func('join')(str.join), result)
self.assertEqual(func('other', 'join')(str.join), result)
# autre fonction, fonction locale
self.assertEqual(func('local_func')(local_func), result)
self.assertEqual(func('other', 'local_func')(local_func), result)
self.assertEqual(func('my_func')(my_func), result)
self.assertEqual(func('other', 'my_func')(my_func), result)
# méthodes
predicate = func('class_met', 'std_met', 'sub_static_met')
self.assertEqual(predicate(MainType.class_met), result)
self.assertEqual(predicate(MainType.static_met), not result)
self.assertEqual(predicate(main_type.std_met), result)
self.assertEqual(predicate(SubType.sub_class_met), not result)
self.assertEqual(predicate(SubType.sub_static_met), result)
self.assertEqual(predicate(sub_type.sub_std_met), not result)
predicate = func('static_met', 'sub_class_met', 'sub_std_met')
self.assertEqual(predicate(MainType.class_met), not result)
self.assertEqual(predicate(MainType.static_met), result)
self.assertEqual(predicate(main_type.std_met), not result)
self.assertEqual(predicate(SubType.sub_class_met), result)
self.assertEqual(predicate(SubType.sub_static_met), not result)
self.assertEqual(predicate(sub_type.sub_std_met), result)
def test_func_class_name_is_in(self):
self.__test_func_class_name_is_in()
def test_func_class_name_is_not_in(self):
self.__test_func_class_name_is_in(negation=True)
def test_func_name_is_in(self):
self.__test_func_name_is_in()
def test_func_name_is_not_in(self):
self.__test_func_name_is_in(negation=True)
def test_func_is_public(self):
MainType = MyClass
main_type = MainType(11)
SubType = MyClass.MySubClass
sub_type = SubType(13)
def local_func(a: int, b: int):
return a + b
# fonction native
self.assertEqual(func_is_public(str.join), True)
# autre fonction, fonction locale
self.assertEqual(func_is_public(local_func), True)
self.assertEqual(func_is_public(my_func), True)
self.assertEqual(func_is_public(_my_private_func), False)
# méthodes
self.assertEqual(func_is_public(MainType._protected_met), False)
self.assertEqual(func_is_public(MainType.class_met), True)
self.assertEqual(func_is_public(MainType.static_met), True)
self.assertEqual(func_is_public(main_type.std_met), True)
self.assertEqual(func_is_public(SubType.sub_class_met), True)
self.assertEqual(func_is_public(SubType.sub_static_met), True)
self.assertEqual(func_is_public(sub_type.sub_std_met), True)

View File

@@ -0,0 +1,58 @@
# quelques définitions pour les tests ==>
def my_func(a: int, b: int):
""" fonction sans classe """
return a + b
def _my_private_func(a: int, b: int):
""" fonction sans classe (non importée par défaut avec import *) """
return a + b
class MyClass():
""" classe pour tests """
num = None
def __init__(self, num):
""" la classe est paramétrée """
self.num = num
def _protected_met(self, a: int, b: int):
""" méthode standard (protected par convention) """
return a + b
class MySubClass():
sub_num = None
def __init__(self, num):
""" la classe est paramétrée """
self.sub_num = num
@classmethod
def sub_class_met(cls):
""" méthode de classe """
return True
@staticmethod
def sub_static_met():
""" méthode statique """
return True
def sub_std_met():
""" méthode standard """
return True
@classmethod
def class_met(cls, a: int, b: int):
""" méthode de classe """
return a + b
@staticmethod
def static_met(a: int, b: int):
""" méthode statique """
return a + b
def std_met(self, a: int, b: int):
""" méthode standard """
return a + b

View File

@@ -0,0 +1,2 @@
from .decision import *
from .notation import *

View File

@@ -0,0 +1,2 @@
USERNAME = 'test'
PASSWORD = 'test'

View File

@@ -0,0 +1,147 @@
from unittest import mock
from backend.models import Administre, Decision, DecisionChoices, Poste
from backend.models import StatutPamChoices as StatutPam
from backend.utils.decisions import (KEY_CREATE, KEY_UPDATE,
get_available_decisions)
from backend.utils.permissions import (KEY_READ, KEY_WRITE, Profiles,
get_profiles_by_adm)
from backend.views import DecisionView
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase
from .constants import PASSWORD, USERNAME
from .test_utils import TestUtilsMixin, disable_gestionnaire_permission
PK_SV_1 = 'SV1'
VIEW_TYPE = DecisionView
class DecisionViewTest(APITestCase, TestUtilsMixin):
basename = 'Decision'
@classmethod
def setUpTestData(cls):
user = get_user_model().objects.create(id=1, username=USERNAME, is_superuser=True)
user.set_password(PASSWORD)
user.save()
def setUp(self):
""" vérifie qu'il n'existe aucun administré ou poste avant le test """
self.assertEqual(Administre.objects.exists(), False, "pas d'administré avant un test")
self.assertEqual(Poste.objects.exists(), False, "pas de poste avant un test")
logged_in = self.client.login(username=USERNAME, password=PASSWORD)
self.assertTrue(logged_in, "l'utilisateur devrait être connecté")
def tearDown(self):
""" supprime tous les administrés, les postes, les décisions en fin de test """
self.client.logout()
Decision.objects.all().delete()
Poste.objects.all().delete()
Administre.objects.all().delete()
def test_list_anonymous(self):
""" vérifie que l'accès anonyme est interdit """
self.client.logout()
url = self._api_url()
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@disable_gestionnaire_permission(VIEW_TYPE)
def test_list_authenticated(self):
""" vérifie que l'utilisateur authentifié peut récupérer des décisions """
url = self._api_url()
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@disable_gestionnaire_permission(VIEW_TYPE)
def test_crud(self):
""" test de création, MAJ, suppression """
try:
statut_pam = next(x for x in StatutPam if x.dec_enabled)
self.assertIsNotNone(statut_pam, 'il devrait exister un statut permettant les décisions')
self.user = get_user_model().objects.first()
self.adm = Administre.objects.create(pk=1, a_statut_pam=statut_pam)
@mock.patch(
f'{get_available_decisions.__module__}.{get_profiles_by_adm.__name__}',
return_value={self.adm.pk: {KEY_READ: (), KEY_WRITE: (Profiles.FILIERE, Profiles.PCP)}}
)
def do_with_mock_profiles(mock):
user = self.user
adm = self.adm
decisions = get_available_decisions((adm,), user=user).get(adm.pk)
self.assertTrue(decisions.get(KEY_CREATE), 'il devrait exister des décisions possibles (création)')
def create(poste_id, de_decision, adm_id=adm.pk, delete_former=None):
return self.client.post(
self._api_url(),
{'administre_id': adm_id, 'poste_id': poste_id, 'de_decision': de_decision, **({'delete_former': delete_former} if isinstance(delete_former, bool) else {})}
)
postes = (Poste.objects.create(pk='11'), Poste.objects.create(pk='13'))
poste_1 = postes[0].pk
poste_2 = postes[1].pk
dec_status_1 = decisions.get(KEY_CREATE)[0]
# création initiale
response = create(poste_1, dec_status_1)
qs_decision1 = Decision.objects.filter(pk=adm.pk, poste_id=poste_1, de_decision=dec_status_1)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(1, Decision.objects.count(), "il doit exister une seule décision")
self.assertTrue(qs_decision1.exists(), "la décision n'a pas les bonnes données")
# création bis, pas possible sans forcer
response = create(poste_2, dec_status_1)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(1, Decision.objects.count(), "il doit exister une seule décision")
self.assertTrue(qs_decision1.exists(), "la première décision doit encore exister")
# création bis en forçant
notes = 'notes'
qs_decision1.update(de_notes_gestionnaire='notes')
self.assertEqual(notes, qs_decision1.first().de_notes_gestionnaire, "les notes doivent être sauvegardées pour le prochain test")
response = create(poste_2, dec_status_1, delete_former=True)
qs_decision2 = Decision.objects.filter(pk=adm.pk, poste_id=poste_2, de_decision=dec_status_1)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(1, Decision.objects.count(), "il doit exister une seule décision")
self.assertFalse(qs_decision1.exists(), "la première décision ne doit plus exister")
self.assertTrue(qs_decision2.exists(), "la deuxième décision n'a pas les bonnes données")
self.assertIsNone(qs_decision2.first().de_notes_gestionnaire, "il ne doit plus exister de notes dans la deuxième décision")
# MAJ
adm = Administre.objects.filter(pk=adm.pk).first()
decisions = get_available_decisions((adm,), user=user).get(adm.pk)
self.assertTrue(decisions.get(KEY_UPDATE), 'il devrait exister des décisions possibles (MAJ) pour le prochain test')
dec_status_2 = decisions.get(KEY_UPDATE)[0]
response = self.client.patch(self._api_url(adm.pk), data={'de_decision': dec_status_2})
qs_decision3 = Decision.objects.filter(pk=adm.pk, poste_id=poste_2, de_decision=dec_status_2)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(1, Decision.objects.count(), "il doit exister une seule décision")
self.assertTrue(qs_decision3.exists(), "la deuxième décision doit changer de statut")
# suppression
response = self.client.delete(self._api_url(adm.pk))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(0, Decision.objects.count(), "il ne doit plus exister de décision")
do_with_mock_profiles()
finally:
self.user = None
self.adm = None

View File

@@ -0,0 +1,46 @@
from backend.views import NotationView
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase
from .constants import PASSWORD, USERNAME
from .test_utils import TestUtilsMixin, disable_gestionnaire_permission
VIEW_TYPE = NotationView
class NotationViewTest(APITestCase, TestUtilsMixin):
basename = 'Notation'
@classmethod
def setUpTestData(cls):
user = get_user_model().objects.create(id=1, username=USERNAME)
user.set_password(PASSWORD)
user.save()
def setUp(self):
logged_in = self.client.login(username=USERNAME, password=PASSWORD)
self.assertTrue(logged_in, "l'utilisateur devrait être connecté")
def tearDown(self):
self.client.logout()
def test_list_anonymous(self):
""" vérifie que l'accès anonyme est interdit """
self.client.logout()
url = self._api_url()
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@disable_gestionnaire_permission(VIEW_TYPE)
def test_list_authenticated(self):
""" vérifie que l'utilisateur authentifié peut récupérer des décisions """
url = self._api_url()
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -0,0 +1,44 @@
from django.urls import reverse
from typing import Any
from unittest import mock
def disable_gestionnaire_permission(type):
"""
Utilise "mock" pour ignorer le gestionnaire de permissions dans la vue.
TODO trouver un mécanisme de mock plus précis
:param type: le type de la vue
:type type: class:`ModelViewSet` par exemple
:return: résultat du mock
:rtype: voir mock
"""
return mock.patch.object(type, 'permission_classes', [p for p in type.permission_classes if p.__name__ != 'GestionnairePermission'])
def api_url(basename: str, pk: Any = None) -> str:
"""
renvoie l'URL selon qu'on a besoin d'un ID ou non
nécessite l'utilisation de SimpleRouter ou DefaultRouter (voir urls.py)
:param basename: valeur de "basename"
:type basename: str
:param pk: valeur qui permet de savoir s'il s'agit d'une URL "list" ou "detail" (voir https://www.django-rest-framework.org/api-guide/routers/#simplerouter)
:type pk: Any
:return: URL
:rtype: str
"""
return reverse(f'{basename}-list') if pk is None else reverse(f'{basename}-detail', args=[pk])
class TestUtilsMixin:
"""
mixin à ajouter pour les tests
"""
def _api_url(self, pk: Any = None) -> str:
""" voir fonction "api_url", nécessite un champ "basename" pour remplir le paramètre """
return api_url(self.basename, pk)

View File

@@ -0,0 +1,48 @@
""" Ce fichier est pour organiser les URLs de l'application web. Le code de ce module est une correspondance entre expressions de chemins dURL et fonctions Python.
"""
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'notations', views.NotationView, basename='Notation')
router.register(r'decisions', views.DecisionView, basename='Decision')
router.register(r'groupes_marques', views.MarquesGroupeView, basename='MarquesGroupe')
router.register(r'marques', views.MarqueView, basename='Marque')
router.register(r'domaines', views.DomaineView, basename='Domaine')
router.register(r'filieres', views.FiliereView, basename='Filiere')
router.register(r'administres', views.AdministreView, basename='Administre')
router.register(r'administres_pams', views.AdministrePAMView, basename='Administre')
router.register(r'postes', views.PosteView, basename='Poste')
router.register(r'postes_pams', views.PostePAMView, basename='Poste')
router.register(r'liste_preferences', views.ListesPreferencesView, basename='PreferencesListe')
router.register(r'fmob', views.FmobView, basename='FMOB')
router.register(r'formation_emploi', views.FormationEmploiView, basename='FormationEmploi')
router.register(r'sous_vivier_association', views.SousVivierAssociationView, basename='SousVivierAssociation')
router.register(r'fe_pcp', views.PcpFeGroupeView, basename='PcpFeGroupe')
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('alimentation/', views.AlimentationView.as_view()),
path('nettoyage_pam/', views.NettoyagePamView.as_view()),
path('chargement_pam/', views.AlimentationPamView.as_view()),
path('alimentation_ref_fe/', views.AlimentationReferentielView.as_view()),
path('alimentation_zones_geo/', views.AlimentationZoneGeographiqueView.as_view()),
path('alimentation_ref_droits/', views.AlimentationReferentielsDroitView.as_view()),
path('alimentation_commentaires/', views.AlimentationCommentairesView.as_view()),
path('suppression_administres/', views.SuppressionAdministresView.as_view()),
path('exportation_fichiers/', views.ExportationFichiersView.as_view()),
path('scoring/', views.ScoringView.as_view()),
path('arret/', views.ArretCalcul.as_view()),
path('suppression/', views.SuppressionAdministresView.as_view()),
path('references/', views.ReferencesView.as_view()),
path('me/', views.CurrentUserView.as_view()),
path('auth/', include('rest_framework.urls', namespace='rest_framework')),
path('chargement_sv/', views.ChargementSVView.as_view()),
path('chargement_competences/', views.ChargementCompetenceView.as_view()),
path('fiche_detaillee/', views.FicheDetailleeView.as_view()),
path('reporting/', views.ReportingView.as_view()),
path('', include(router.urls)),
]

View File

@@ -0,0 +1,155 @@
"""
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
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.
"""
# print(string)
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:
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).exclude(
p_nb_occupe=0)
poste = poste_qs.first()
print(fe_code)
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_non_etudie > 0:
print(poste)
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()
print(model_to_dict(fe))
if poste and poste.p_nb_p4 > 0:
print(poste)
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)
print(list_error)
return list_error

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)

View File

@@ -0,0 +1,192 @@
"""
Ce module contient la fonction permettant de démarrer le scoring sur le sous-vivier désiré
"""
import time
from django.utils import timezone
from .models import Calcul
from .models import StatutCalculChoices as StatutCalcul
from .utils.logging import get_logger
from .utils_extraction import to_table_liste_preference, to_table_liste_preference_selectif
from .utils_insertion import (insert_liste_preference, insert_matching,
insert_Notation)
from .utils_matching import (matching_parfait, preprocess_matching,
preprocess_matchingSelectif)
from .utils_scoring import notations, notations_liste
logger = get_logger(__name__)
# Fonction de lancement du scoring
def lancer_calculs(pam_id,sv_id):
"""
Fonction de lancement du calcul du scoring
:param sv_id: L'id de sous vivier étudié
:type sv_id: char
"""
Statut = StatutCalcul
statut = Statut.TERMINE
pourcentage_incr = 16.67
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
start_time = time.time()
logger.info('---------------------Scoring beginning------------------------')
notations_list = notations(pam_id,sv_id)
logger.info('---------------------Scoring ending---------------------------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Scoring time -- %d seconds -----------------", time.time() - start_time)
start_time_insert = time.time()
logger.info('---------------------insert scoring results begining-----------')
insert_Notation(notations_list,pam_id,sv_id)
logger.info('---------------------insert scoring results ending-----------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Insert time -- %d seconds -----------------", time.time() - start_time_insert)
start_time2 = time.time()
logger.info('---------------------Preference list creation beginning-----------------')
df_preference = to_table_liste_preference(pam_id,sv_id)
logger.info('---------------------Preference list creation ending-------------------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
try:
start_time3 = time.time()
logger.info('---------------------Matching beginning------------------------')
proposant, disposant, capacite = preprocess_matching(pam_id, sv_id, df_preference)
results = matching_parfait(proposant, disposant, capacite)
logger.info('---------------------Matching ending---------------------------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Matching time -- %d seconds -----------------", time.time() - start_time3)
start_time_insert = time.time()
logger.info('---------------------insert matching results begining-----------')
insert_matching(results, pam_id)
logger.info('---------------------insert matching results ending-----------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage = 100
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Insert time -- %d seconds -----------------", time.time() - start_time_insert)
except Exception as e:
logger.warning('Arret du scoring, veuillez renseigner au moins un département',e)
# Fonction de lancement du scoring sélectif
def lancer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id):
"""
Fonction de lancement du calcul du scoring sélectif
:param sv_id: L'id de sous vivier étudié
:param l_a_id: Liste d'ids d'administrés
:param l_p_id: Liste d'ids de postes
:type sv_id: char
"""
Statut = StatutCalcul
pourcentage_incr = 16.67
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id)
start_time = time.time()
logger.info('---------------------Scoring beginning------------------------')
notations_list = notations_liste(sv_id, pam_id, l_a_id, l_p_id)
logger.info('---------------------Scoring ending---------------------------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Scoring time -- %d seconds -----------------", time.time() - start_time)
start_time_insert = time.time()
logger.info('---------------------insert scoring results begining-----------')
insert_Notation(notations_list, pam_id, sv_id)
logger.info('---------------------insert scoring results ending-----------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Insert time -- %d seconds -----------------", time.time() - start_time_insert)
start_time2 = time.time()
logger.info('---------------------Preference list creation beginning-----------------')
df_preference = to_table_liste_preference_selectif(sv_id, pam_id,l_a_id,l_p_id)
logger.info('---------------------Preference list creation ending-------------------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
try:
start_time3 = time.time()
logger.info('---------------------Matching beginning------------------------')
proposant, disposant, capacite = preprocess_matchingSelectif(sv_id, pam_id,l_a_id,l_p_id, df_preference)
results = matching_parfait(proposant, disposant, capacite)
logger.info('---------------------Matching ending---------------------------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage += pourcentage_incr
calcul.ca_statut_pourcentage = round(calcul.ca_statut_pourcentage, 2)
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Matching time -- %d seconds -----------------", time.time() - start_time3)
start_time_insert = time.time()
logger.info('---------------------insert matching results begining-----------')
insert_matching(results, pam_id)
logger.info('---------------------insert matching results ending-----------')
calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id)
calcul.ca_statut_pourcentage = 100
calcul.save()
if calcul.ca_statut == Statut.EN_ATTENTE_ARRET:
raise Exception('Arret du calcul')
logger.info("------------------Insert time -- %d seconds -----------------", time.time() - start_time_insert)
except Exception as e:
logger.warning('Arret du scoring, veuillez renseigner au moins un département',e)
return

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
"""
Ce module contient les Utilitaires de matching
"""
# import des pré requis
import time
import pandas as pd
from matching.games import HospitalResident
from .models import Notation, Poste, PreferencesListe, StatutPamChoices, AvisPosteChoices as AvisPoste
from .utils.logging import get_logger
logger = get_logger(__name__)
# Fonction de preprocessing
def preprocess_matching(pam_id,sv_id,df_pref):
"""
Retourne les liste de souhait des militaires et des postes
:type sv_id: char
:param sv_id: id du sous vivier étudié
:return: - **gs_proposants** (*liste*): liste des proposants
- **gs_disposants** (*liste*): liste des disposants
- **gs_capacite** (*liste*): liste des capacités pour chaque disposants
"""
logger.debug('preprocessing matching %s', sv_id)
statut_adm = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER]
statut_poste =[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4]
# liste_preference = PreferencesListe.objects.filter(pam_id=pam_id,administre_pam__administre__sous_vivier_id=sv_id).values()
notation = Notation.objects.filter(pam_id=pam_id,
administre_pam__administre__sous_vivier_id=sv_id,
administre_pam__a_statut_pam_annee__in=statut_adm,
administre_pam__decision__de_decision__isnull=True,
poste_pam__poste__sous_viviers=sv_id,
poste_pam__decisions__de_decision__isnull=True,
poste_pam__p_avis_pam__in=statut_poste).values()
liste_preference_df = df_pref
notation_df = pd.DataFrame.from_records(notation)
liste_admin_lp = liste_preference_df.drop_duplicates(subset=['administre_id'])
gs_proposants = {}
gs_disposants = {}
gs_capacite = {}
l2 = []
if not liste_preference_df.empty:
for admin in liste_admin_lp['administre_id']:
l = []
new_lp_df = liste_preference_df.loc[liste_preference_df['administre_id'] == admin].drop_duplicates(subset=['poste_id'])
new_lp_df = new_lp_df.drop_duplicates(subset=['lp_rang_poste'])
new_lp_df = new_lp_df.sort_values(
by=['lp_rang_poste']).reset_index()
for i in range(len(new_lp_df)):
l.append(new_lp_df.at[i, 'poste_id'])
l2.append(new_lp_df.at[i, 'poste_id'])
gs_proposants[admin] = l
l2 = list(set(l2))
notation_df = notation_df[notation_df.administre_id.isin(liste_admin_lp['administre_id'])]
for poste in l2:
l1 = []
new_no_df = notation_df.loc[notation_df['poste_id'] == poste].sort_values(by=['no_score_administre'],
ascending=False).reset_index()
for i in range(len(new_no_df)):
if poste in gs_proposants[new_no_df.at[i, 'administre_id']]:
l1.append(new_no_df.at[i, 'administre_id'])
gs_disposants[poste] = l1
gs_capacite[poste] = 1
logger.debug("Administre proposants : %s", gs_proposants)
logger.debug("Poste disposants : %s", gs_disposants)
logger.debug('end preprocessing matching func')
return gs_proposants, gs_disposants, gs_capacite
# Fonction de preprocessing
def preprocess_matchingSelectif(sv_id, pam_id,l_a_id,l_p_id, df_pref):
"""
Retourne les liste de souhait des militaires et des postes
:type sv_id: char
:param sv_id: id du sous vivier étudié
:return: - **gs_proposants** (*liste*): liste des proposants
- **gs_disposants** (*liste*): liste des disposants
- **gs_capacite** (*liste*): liste des capacités pour chaque disposants
"""
logger.debug('preprocessing matching %s', sv_id)
statut_adm = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER]
statut_poste =[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4]
notation = Notation.objects.filter(pam_id=pam_id,
administre_pam__administre__sous_vivier_id=sv_id,
administre_pam__a_statut_pam_annee__in=statut_adm,
administre_pam__administre_id__in=l_a_id,
administre_pam__decision__de_decision__isnull=True,
poste_pam__poste__sous_viviers=sv_id,
poste_pam__decisions__de_decision__isnull=True,
poste_pam__p_avis_pam__in=statut_poste).values()
liste_preference_df = df_pref
notation_df = pd.DataFrame.from_records(notation)
liste_admin_lp = liste_preference_df.drop_duplicates(subset=['administre_id'])
gs_proposants = {}
gs_disposants = {}
gs_capacite = {}
l2 = []
if not liste_preference_df.empty:
for admin in liste_admin_lp['administre_id']:
l = []
new_lp_df = liste_preference_df.loc[liste_preference_df['administre_id'] == admin].drop_duplicates(subset=['poste_id'])
new_lp_df = new_lp_df.drop_duplicates(subset=['lp_rang_poste'])
new_lp_df = new_lp_df.sort_values(
by=['lp_rang_poste']).reset_index()
for i in range(len(new_lp_df)):
l.append(new_lp_df.at[i, 'poste_id'])
l2.append(new_lp_df.at[i, 'poste_id'])
gs_proposants[admin] = l
l2 = list(set(l2))
notation_df = notation_df[notation_df.administre_id.isin(liste_admin_lp['administre_id'])]
for poste in l2:
l1 = []
new_no_df = notation_df.loc[notation_df['poste_id'] == poste].sort_values(by=['no_score_administre'],
ascending=False).reset_index()
for i in range(len(new_no_df)):
if poste in gs_proposants[new_no_df.at[i, 'administre_id']]:
l1.append(new_no_df.at[i, 'administre_id'])
gs_disposants[poste] = l1
gs_capacite[poste] = 1
logger.debug("Administre proposants : %s", gs_proposants)
logger.debug("Poste disposants : %s", gs_disposants)
logger.debug('end preprocessing matching func')
return gs_proposants, gs_disposants, gs_capacite
# Fonction de matching
def matching_parfait(gs_proposants, gs_disposants, gs_capacite):
"""
Retourne le dictionnaire python presentant le resultats du matching
:type gs_proposants: liste
:param gs_proposants: liste des proposants
:type gs_disposants: liste
:param gs_disposants: liste des disposants
:type gs_capacite: liste
:param gs_capacite: liste des capacités pour chaque disposants
:return: - **matching** (*dictionnaire*): dictionnaire python presentant le resultats du matching
"""
logger.debug('start matching parfait')
# Traitement des entrées
game = HospitalResident.create_from_dictionaries(
gs_proposants, gs_disposants, gs_capacite
)
# Creation du jeu
matching = game.solve(optimal='hospital')
# Test de validation et de stabilité
logger.debug('Validity Check : %s', game.check_validity())
assert game.check_validity()
logger.debug('Stability Check : %s', game.check_stability())
assert game.check_stability()
# Creation des listes d'affectés et de non afféctes
matched_residents = []
for _, residents in matching.items():
for resident in residents:
matched_residents.append(resident.name)
unmatched_residents = set(gs_proposants.keys()) - set(matched_residents)
# KPI matching
logger.debug("Nb demandeurs : %s", len(gs_proposants))
logger.debug("Poste demandeurs : %s", gs_proposants)
logger.debug("Nb affectés : %s", len(matched_residents))
logger.debug("Administres affectés : %s", matched_residents)
logger.debug("Nb non affecte : %s", len(unmatched_residents))
logger.debug("Administres non affecte : %s", unmatched_residents)
logger.debug('end matching parfait')
return (matching)

Some files were not shown because too many files have changed in this diff Show More