commit 4c456eafc38b896d516e08e05023e14af7dea007 Author: edouard Date: Tue Nov 8 21:19:51 2022 +0100 init diff --git a/.env b/.env new file mode 100644 index 0000000..433e540 --- /dev/null +++ b/.env @@ -0,0 +1,19 @@ +## do not put this file under version control! +##SECRET_KEY='c_r-e8v1divj8y+hu@-w=n#$xj#ciuejybd3_(k2h789(mcv8$' +DEBUG=1 + +SQL_DATABASE=Ogure-DB +SQL_USER=postgres +SQL_PASSWORD=postgres +SQL_HOST=db +SQL_PORT=5432 + + +## Super-User Credentials +SUPER_USER_NAME= 'root' +SUPER_USER_PASSWORD= 'root' +SUPER_USER_EMAIL= 'admin@email.com' +SUPER_USER_ID=1 + +##python +PYTHONPATH="$PYTHONPATH:/usr/lib/python3.8/dist-packages:/usr/local/lib/python3.8/dist-packages diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..86bf35a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.7.5-alpine +RUN pip install --upgrade pip +RUN apk add g++ gcc +COPY ./backend-django backend-django +WORKDIR /backend-django + +ENV PYTHONPATH="$PYTHONPATH:/usr/lib/python3.8/dist-packages:/usr/local/lib/python3.8/dist-packages" + +COPY ./requirements.txt . +RUN pip install -r requirements.txt + + +COPY ./entrypoint.sh / +ENTRYPOINT ["sh", "/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..746f3e1 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# test_Docker + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://scm-intradef.picsel.defense.gouv.fr/e.de-villers/test_docker.git +git branch -M master +git push -uf origin master +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://scm-intradef.picsel.defense.gouv.fr/e.de-villers/test_docker/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/backend-django/.env.db b/backend-django/.env.db new file mode 100644 index 0000000..45be0ac --- /dev/null +++ b/backend-django/.env.db @@ -0,0 +1,3 @@ +POSTGRES_DB=postgres +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres \ No newline at end of file diff --git a/backend-django/.env.prod b/backend-django/.env.prod new file mode 100644 index 0000000..3952b42 --- /dev/null +++ b/backend-django/.env.prod @@ -0,0 +1,17 @@ +DEBUG=1 +SECRET_KEY=foo +DJANGO_ALLOWED_HOSTS=* +SQL_ENGINE=django.db.backends.postgresql +SQL_DATABASE=ogure +SQL_USER=postgres +SQL_PASSWORD=postgres +SQL_HOST=127.0.0.1 +SQL_PORT=5432 +DATABASE=postgres +DJANGO_SUPERUSER_USERNAME="admin" +DJANGO_SUPERUSER_EMAIL="admin@ogure.app" +DJANGO_SUPERUSER_PASSWORD="admin" +DJANGO_CSRF_COOKIE_SAMESITE='Strict' +DJANGO_SESSION_COOKIE_SAMESITE='Strict' +DJANGO_CSRF_COOKIE_HTTPONLY=False +DJANGO_SESSION_COOKIE_HTTPONLY=True \ No newline at end of file diff --git a/backend-django/.gitignore b/backend-django/.gitignore new file mode 100644 index 0000000..b70e10a --- /dev/null +++ b/backend-django/.gitignore @@ -0,0 +1,15 @@ +/venv/ +/frontend/.vscode +env/ +venv/ +node_modules +dist +static +doc +staticfiles/ +.idea/ +db.sqlite3 +db.sqlite3-journal +__pycache__ +.history +staticfiles diff --git a/backend-django/README.md b/backend-django/README.md new file mode 100644 index 0000000..4d277f3 --- /dev/null +++ b/backend-django/README.md @@ -0,0 +1,64 @@ +# backend-django + +## pre-requis + +- python 3.7.9 + +- packages pypi + création virtual_env + installation des packages depuis fichiers locaux + +voir install/README.md section backend-django + +- base postgres "Ogure-DB" avec "admin" ogure/ogure + +voir database/README.md section postgresql + + +## setup virtual env "ogure_ng_venv" + +````bash +cd backend-django + +python3.7 -m venv ogure_ng_venv + +source ogure_ng_venv/bin/activate + +pip3.7 install -r requirements.txt --no-index --find-links file:///home/ogure/ogure-ng/install/pypi/backend-django + +python3.7 manage.py makemigrations + pgAdmin - DB vide... + +python3.7 manage.py migrate + pgAdmin - DB avec 32 tables... + +python3.7 manage.py createsuperuser + admin/password2022 + +python3.7 manage.py collectstatic + staticfiles/ + +python3.7 manage.py runserver 0:8000 + +```` + + +```` +// deploy 'staticfiles' vers nginx +// verifier que staticfiles est bien reference dans les fichiers nginx.conf + +sudo ./deploy.sh +```` + + +test depuis poste IAG http://:8000/admin + +login avec admin/******** -> OK + +création Utilisateurs + +user1: + ogure/******** -> OK + + + diff --git a/backend-django/backend/__init__.py b/backend-django/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend-django/backend/admin.py b/backend-django/backend/admin.py new file mode 100644 index 0000000..934c3d3 --- /dev/null +++ b/backend-django/backend/admin.py @@ -0,0 +1,380 @@ +""" +La classe Admin est la représentation d’un modèle dans l’interface d’administration +""" + +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) diff --git a/backend-django/backend/apps.py b/backend-django/backend/apps.py new file mode 100644 index 0000000..325c4d7 --- /dev/null +++ b/backend-django/backend/apps.py @@ -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' diff --git a/backend-django/backend/constants.py b/backend-django/backend/constants.py new file mode 100644 index 0000000..b4e0b0d --- /dev/null +++ b/backend-django/backend/constants.py @@ -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'} diff --git a/backend-django/backend/filters.py b/backend-django/backend/filters.py new file mode 100644 index 0000000..cc96801 --- /dev/null +++ b/backend-django/backend/filters.py @@ -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 diff --git a/backend-django/backend/form.py b/backend-django/backend/form.py new file mode 100644 index 0000000..41118f7 --- /dev/null +++ b/backend-django/backend/form.py @@ -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'] diff --git a/backend-django/backend/migrations/__init__.py b/backend-django/backend/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend-django/backend/models.py b/backend-django/backend/models.py new file mode 100644 index 0000000..59627d8 --- /dev/null +++ b/backend-django/backend/models.py @@ -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' diff --git a/backend-django/backend/models/__init__.py b/backend-django/backend/models/__init__.py new file mode 100644 index 0000000..103509c --- /dev/null +++ b/backend-django/backend/models/__init__.py @@ -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 * diff --git a/backend-django/backend/models/administre.py b/backend-django/backend/models/administre.py new file mode 100644 index 0000000..5cb29c2 --- /dev/null +++ b/backend-django/backend/models/administre.py @@ -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) diff --git a/backend-django/backend/models/calcul.py b/backend-django/backend/models/calcul.py new file mode 100644 index 0000000..6c2d79e --- /dev/null +++ b/backend-django/backend/models/calcul.py @@ -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) + ), + ] diff --git a/backend-django/backend/models/commun.py b/backend-django/backend/models/commun.py new file mode 100644 index 0000000..2513aab --- /dev/null +++ b/backend-django/backend/models/commun.py @@ -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' diff --git a/backend-django/backend/models/competence.py b/backend-django/backend/models/competence.py new file mode 100644 index 0000000..669a88e --- /dev/null +++ b/backend-django/backend/models/competence.py @@ -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, + } diff --git a/backend-django/backend/models/decision.py b/backend-django/backend/models/decision.py new file mode 100644 index 0000000..ae95bc2 --- /dev/null +++ b/backend-django/backend/models/decision.py @@ -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' diff --git a/backend-django/backend/models/domaine.py b/backend-django/backend/models/domaine.py new file mode 100644 index 0000000..e80ad5f --- /dev/null +++ b/backend-django/backend/models/domaine.py @@ -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, + } diff --git a/backend-django/backend/models/fichier_exporte.py b/backend-django/backend/models/fichier_exporte.py new file mode 100644 index 0000000..700182e --- /dev/null +++ b/backend-django/backend/models/fichier_exporte.py @@ -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' diff --git a/backend-django/backend/models/filiere.py b/backend-django/backend/models/filiere.py new file mode 100644 index 0000000..47fb6fd --- /dev/null +++ b/backend-django/backend/models/filiere.py @@ -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, + } diff --git a/backend-django/backend/models/fmob.py b/backend-django/backend/models/fmob.py new file mode 100644 index 0000000..9c4bf80 --- /dev/null +++ b/backend-django/backend/models/fmob.py @@ -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) diff --git a/backend-django/backend/models/fonction.py b/backend-django/backend/models/fonction.py new file mode 100644 index 0000000..43e2597 --- /dev/null +++ b/backend-django/backend/models/fonction.py @@ -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, + } \ No newline at end of file diff --git a/backend-django/backend/models/formation_emploi.py b/backend-django/backend/models/formation_emploi.py new file mode 100644 index 0000000..cad1ea8 --- /dev/null +++ b/backend-django/backend/models/formation_emploi.py @@ -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 + } diff --git a/backend-django/backend/models/garnison.py b/backend-django/backend/models/garnison.py new file mode 100644 index 0000000..47f978a --- /dev/null +++ b/backend-django/backend/models/garnison.py @@ -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 diff --git a/backend-django/backend/models/grade.py b/backend-django/backend/models/grade.py new file mode 100644 index 0000000..9de71c9 --- /dev/null +++ b/backend-django/backend/models/grade.py @@ -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 + } diff --git a/backend-django/backend/models/initial.py b/backend-django/backend/models/initial.py new file mode 100644 index 0000000..b1c43d8 --- /dev/null +++ b/backend-django/backend/models/initial.py @@ -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' diff --git a/backend-django/backend/models/pam.py b/backend-django/backend/models/pam.py new file mode 100644 index 0000000..a6c76b9 --- /dev/null +++ b/backend-django/backend/models/pam.py @@ -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, + } \ No newline at end of file diff --git a/backend-django/backend/models/poste.py b/backend-django/backend/models/poste.py new file mode 100644 index 0000000..cee6111 --- /dev/null +++ b/backend-django/backend/models/poste.py @@ -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="") diff --git a/backend-django/backend/models/sous_vivier.py b/backend-django/backend/models/sous_vivier.py new file mode 100644 index 0000000..cdfab20 --- /dev/null +++ b/backend-django/backend/models/sous_vivier.py @@ -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, + } diff --git a/backend-django/backend/models/user.py b/backend-django/backend/models/user.py new file mode 100644 index 0000000..50dde40 --- /dev/null +++ b/backend-django/backend/models/user.py @@ -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'] diff --git a/backend-django/backend/paginations.py b/backend-django/backend/paginations.py new file mode 100644 index 0000000..0c62aa3 --- /dev/null +++ b/backend-django/backend/paginations.py @@ -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' diff --git a/backend-django/backend/reporting.py b/backend-django/backend/reporting.py new file mode 100644 index 0000000..f4a0727 --- /dev/null +++ b/backend-django/backend/reporting.py @@ -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) diff --git a/backend-django/backend/serializers.py b/backend-django/backend/serializers.py new file mode 100644 index 0000000..fe9b329 --- /dev/null +++ b/backend-django/backend/serializers.py @@ -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__' diff --git a/backend-django/backend/serializers/__init__.py b/backend-django/backend/serializers/__init__.py new file mode 100644 index 0000000..7f97664 --- /dev/null +++ b/backend-django/backend/serializers/__init__.py @@ -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 * diff --git a/backend-django/backend/serializers/administre.py b/backend-django/backend/serializers/administre.py new file mode 100644 index 0000000..797988d --- /dev/null +++ b/backend-django/backend/serializers/administre.py @@ -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__' diff --git a/backend-django/backend/serializers/alimentation.py b/backend-django/backend/serializers/alimentation.py new file mode 100644 index 0000000..2449cf2 --- /dev/null +++ b/backend-django/backend/serializers/alimentation.py @@ -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__' diff --git a/backend-django/backend/serializers/commun.py b/backend-django/backend/serializers/commun.py new file mode 100644 index 0000000..bc93afa --- /dev/null +++ b/backend-django/backend/serializers/commun.py @@ -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') diff --git a/backend-django/backend/serializers/current_user.py b/backend-django/backend/serializers/current_user.py new file mode 100644 index 0000000..b88e9ca --- /dev/null +++ b/backend-django/backend/serializers/current_user.py @@ -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 diff --git a/backend-django/backend/serializers/decision.py b/backend-django/backend/serializers/decision.py new file mode 100644 index 0000000..d9cf79d --- /dev/null +++ b/backend-django/backend/serializers/decision.py @@ -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'] diff --git a/backend-django/backend/serializers/domaine.py b/backend-django/backend/serializers/domaine.py new file mode 100644 index 0000000..373311e --- /dev/null +++ b/backend-django/backend/serializers/domaine.py @@ -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__' diff --git a/backend-django/backend/serializers/exportation_fichiers.py b/backend-django/backend/serializers/exportation_fichiers.py new file mode 100644 index 0000000..360d1db --- /dev/null +++ b/backend-django/backend/serializers/exportation_fichiers.py @@ -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__' \ No newline at end of file diff --git a/backend-django/backend/serializers/filiere.py b/backend-django/backend/serializers/filiere.py new file mode 100644 index 0000000..07606ad --- /dev/null +++ b/backend-django/backend/serializers/filiere.py @@ -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__' diff --git a/backend-django/backend/serializers/fmob.py b/backend-django/backend/serializers/fmob.py new file mode 100644 index 0000000..473f874 --- /dev/null +++ b/backend-django/backend/serializers/fmob.py @@ -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__' diff --git a/backend-django/backend/serializers/fonction.py b/backend-django/backend/serializers/fonction.py new file mode 100644 index 0000000..1d219e0 --- /dev/null +++ b/backend-django/backend/serializers/fonction.py @@ -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__' diff --git a/backend-django/backend/serializers/formation_emploi.py b/backend-django/backend/serializers/formation_emploi.py new file mode 100644 index 0000000..b948c9c --- /dev/null +++ b/backend-django/backend/serializers/formation_emploi.py @@ -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',) diff --git a/backend-django/backend/serializers/grade.py b/backend-django/backend/serializers/grade.py new file mode 100644 index 0000000..37762d7 --- /dev/null +++ b/backend-django/backend/serializers/grade.py @@ -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__' diff --git a/backend-django/backend/serializers/initial.py b/backend-django/backend/serializers/initial.py new file mode 100644 index 0000000..325b914 --- /dev/null +++ b/backend-django/backend/serializers/initial.py @@ -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 """ diff --git a/backend-django/backend/serializers/notation.py b/backend-django/backend/serializers/notation.py new file mode 100644 index 0000000..16345d2 --- /dev/null +++ b/backend-django/backend/serializers/notation.py @@ -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__' \ No newline at end of file diff --git a/backend-django/backend/serializers/pcp_fe_groupe.py b/backend-django/backend/serializers/pcp_fe_groupe.py new file mode 100644 index 0000000..b145f46 --- /dev/null +++ b/backend-django/backend/serializers/pcp_fe_groupe.py @@ -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__' diff --git a/backend-django/backend/serializers/sous_vivier.py b/backend-django/backend/serializers/sous_vivier.py new file mode 100644 index 0000000..571032d --- /dev/null +++ b/backend-django/backend/serializers/sous_vivier.py @@ -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,) diff --git a/backend-django/backend/serializers/suppression.py b/backend-django/backend/serializers/suppression.py new file mode 100644 index 0000000..67667e6 --- /dev/null +++ b/backend-django/backend/serializers/suppression.py @@ -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__' \ No newline at end of file diff --git a/backend-django/backend/signals.py b/backend-django/backend/signals.py new file mode 100644 index 0000000..c8339fc --- /dev/null +++ b/backend-django/backend/signals.py @@ -0,0 +1,198 @@ +""" + Les signaux permettent à certains expéditeurs d’avertir un ensemble de destinataires qu’une 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) diff --git a/backend-django/backend/templates/admin/groupe_fe.html b/backend-django/backend/templates/admin/groupe_fe.html new file mode 100644 index 0000000..c61de07 --- /dev/null +++ b/backend-django/backend/templates/admin/groupe_fe.html @@ -0,0 +1,25 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
+ {% csrf_token %} +

+ Entrer le nom du groupe des formations d'emploi +

+
    + {% for fe in formations_emplois %} +
  • + {{ fe.fe_code }} — {{ fe.fe_libelle }} +
  • + {% endfor %} +
+ {% for obj in formations_emplois %} + + {% endfor %} + {# #} + {{ form.nom_groupe_fe }} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/backend-django/backend/templates/admin/taux_cible_mdr.html b/backend-django/backend/templates/admin/taux_cible_mdr.html new file mode 100644 index 0000000..9dead2e --- /dev/null +++ b/backend-django/backend/templates/admin/taux_cible_mdr.html @@ -0,0 +1,25 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
+ {% csrf_token %} +

+ Entrer le nouveau taux cible d'armement pour la catégorie MDR pour les formations-emplois sélectionnées +

+
    + {% for fe in formations_emplois %} +
  • + {{ fe.fe_code }} — {{ fe.fe_libelle }} +
  • + {% endfor %} +
+ {% for obj in formations_emplois %} + + {% endfor %} + {# #} + {{ form.taux_armement_cible }} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/backend-django/backend/templates/admin/taux_cible_off.html b/backend-django/backend/templates/admin/taux_cible_off.html new file mode 100644 index 0000000..98036d5 --- /dev/null +++ b/backend-django/backend/templates/admin/taux_cible_off.html @@ -0,0 +1,25 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
+ {% csrf_token %} +

+ Entrer le nouveau taux cible d'armement pour la catégorie OFF pour les formations-emplois sélectionnées +

+
    + {% for fe in formations_emplois %} +
  • + {{ fe.fe_code }} — {{ fe.fe_libelle }} +
  • + {% endfor %} +
+ {% for obj in formations_emplois %} + + {% endfor %} + {# #} + {{ form.taux_armement_cible }} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/backend-django/backend/templates/admin/taux_cible_soff.html b/backend-django/backend/templates/admin/taux_cible_soff.html new file mode 100644 index 0000000..f10c88d --- /dev/null +++ b/backend-django/backend/templates/admin/taux_cible_soff.html @@ -0,0 +1,25 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
+ {% csrf_token %} +

+ Entrer le nouveau taux cible d'armement pour la catégorie SOFF pour les formations-emplois sélectionnées +

+
    + {% for fe in formations_emplois %} +
  • + {{ fe.fe_code }} — {{ fe.fe_libelle }} +
  • + {% endfor %} +
+ {% for obj in formations_emplois %} + + {% endfor %} + {# #} + {{ form.taux_armement_cible }} +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/backend-django/backend/tests.py b/backend-django/backend/tests.py new file mode 100644 index 0000000..6eea14e --- /dev/null +++ b/backend-django/backend/tests.py @@ -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) \ No newline at end of file diff --git a/backend-django/backend/tests/__init__.py b/backend-django/backend/tests/__init__.py new file mode 100644 index 0000000..ac4fb5e --- /dev/null +++ b/backend-django/backend/tests/__init__.py @@ -0,0 +1,5 @@ +from .initial import * +from .models import * +from .utils import * +from .views import * +from .droits import * diff --git a/backend-django/backend/tests/droits/__init__.py b/backend-django/backend/tests/droits/__init__.py new file mode 100644 index 0000000..7f945e2 --- /dev/null +++ b/backend-django/backend/tests/droits/__init__.py @@ -0,0 +1 @@ +from .socle import * diff --git a/backend-django/backend/tests/droits/socle.py b/backend-django/backend/tests/droits/socle.py new file mode 100644 index 0000000..a4a229f --- /dev/null +++ b/backend-django/backend/tests/droits/socle.py @@ -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) diff --git a/backend-django/backend/tests/initial.py b/backend-django/backend/tests/initial.py new file mode 100644 index 0000000..bbd1337 --- /dev/null +++ b/backend-django/backend/tests/initial.py @@ -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) + diff --git a/backend-django/backend/tests/models/__init__.py b/backend-django/backend/tests/models/__init__.py new file mode 100644 index 0000000..1dd5f97 --- /dev/null +++ b/backend-django/backend/tests/models/__init__.py @@ -0,0 +1,3 @@ +from .administre import * +from .calcul import * +from .poste import * diff --git a/backend-django/backend/tests/models/administre.py b/backend-django/backend/tests/models/administre.py new file mode 100644 index 0000000..3eb7c18 --- /dev/null +++ b/backend-django/backend/tests/models/administre.py @@ -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) diff --git a/backend-django/backend/tests/models/calcul.py b/backend-django/backend/tests/models/calcul.py new file mode 100644 index 0000000..0fa9b34 --- /dev/null +++ b/backend-django/backend/tests/models/calcul.py @@ -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) diff --git a/backend-django/backend/tests/models/poste.py b/backend-django/backend/tests/models/poste.py new file mode 100644 index 0000000..5ddb53e --- /dev/null +++ b/backend-django/backend/tests/models/poste.py @@ -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) diff --git a/backend-django/backend/tests/pam/__init__.py b/backend-django/backend/tests/pam/__init__.py new file mode 100644 index 0000000..7ad2b53 --- /dev/null +++ b/backend-django/backend/tests/pam/__init__.py @@ -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) diff --git a/backend-django/backend/tests/utils/__init__.py b/backend-django/backend/tests/utils/__init__.py new file mode 100644 index 0000000..438c844 --- /dev/null +++ b/backend-django/backend/tests/utils/__init__.py @@ -0,0 +1,6 @@ +from .alimentation import * +from .decisions import * +from .decorators import * +from .functions import * +from .logging import * +from .predicates import * diff --git a/backend-django/backend/tests/utils/alimentation.py b/backend-django/backend/tests/utils/alimentation.py new file mode 100644 index 0000000..9bf01d7 --- /dev/null +++ b/backend-django/backend/tests/utils/alimentation.py @@ -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") diff --git a/backend-django/backend/tests/utils/decisions.py b/backend-django/backend/tests/utils/decisions.py new file mode 100644 index 0000000..355cd96 --- /dev/null +++ b/backend-django/backend/tests/utils/decisions.py @@ -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())) diff --git a/backend-django/backend/tests/utils/decorators.py b/backend-django/backend/tests/utils/decorators.py new file mode 100644 index 0000000..ad3a51c --- /dev/null +++ b/backend-django/backend/tests/utils/decorators.py @@ -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) diff --git a/backend-django/backend/tests/utils/functions.py b/backend-django/backend/tests/utils/functions.py new file mode 100644 index 0000000..ca38f8b --- /dev/null +++ b/backend-django/backend/tests/utils/functions.py @@ -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) diff --git a/backend-django/backend/tests/utils/logging.py b/backend-django/backend/tests/utils/logging.py new file mode 100644 index 0000000..f783d37 --- /dev/null +++ b/backend-django/backend/tests/utils/logging.py @@ -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) diff --git a/backend-django/backend/tests/utils/predicates.py b/backend-django/backend/tests/utils/predicates.py new file mode 100644 index 0000000..89d337a --- /dev/null +++ b/backend-django/backend/tests/utils/predicates.py @@ -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) diff --git a/backend-django/backend/tests/utils/samples.py b/backend-django/backend/tests/utils/samples.py new file mode 100644 index 0000000..193265d --- /dev/null +++ b/backend-django/backend/tests/utils/samples.py @@ -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 diff --git a/backend-django/backend/tests/views/__init__.py b/backend-django/backend/tests/views/__init__.py new file mode 100644 index 0000000..b342bda --- /dev/null +++ b/backend-django/backend/tests/views/__init__.py @@ -0,0 +1,2 @@ +from .decision import * +from .notation import * diff --git a/backend-django/backend/tests/views/constants.py b/backend-django/backend/tests/views/constants.py new file mode 100644 index 0000000..5e53f75 --- /dev/null +++ b/backend-django/backend/tests/views/constants.py @@ -0,0 +1,2 @@ +USERNAME = 'test' +PASSWORD = 'test' diff --git a/backend-django/backend/tests/views/decision.py b/backend-django/backend/tests/views/decision.py new file mode 100644 index 0000000..c97aa41 --- /dev/null +++ b/backend-django/backend/tests/views/decision.py @@ -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 diff --git a/backend-django/backend/tests/views/notation.py b/backend-django/backend/tests/views/notation.py new file mode 100644 index 0000000..22ad66d --- /dev/null +++ b/backend-django/backend/tests/views/notation.py @@ -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) diff --git a/backend-django/backend/tests/views/test_utils.py b/backend-django/backend/tests/views/test_utils.py new file mode 100644 index 0000000..c3187d7 --- /dev/null +++ b/backend-django/backend/tests/views/test_utils.py @@ -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) diff --git a/backend-django/backend/urls.py b/backend-django/backend/urls.py new file mode 100644 index 0000000..0376f5d --- /dev/null +++ b/backend-django/backend/urls.py @@ -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 d’URL 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)), + +] diff --git a/backend-django/backend/utils.py b/backend-django/backend/utils.py new file mode 100644 index 0000000..a222a8c --- /dev/null +++ b/backend-django/backend/utils.py @@ -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 \ No newline at end of file diff --git a/backend-django/backend/utils/__init__.py b/backend-django/backend/utils/__init__.py new file mode 100644 index 0000000..fdc689b --- /dev/null +++ b/backend-django/backend/utils/__init__.py @@ -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 * diff --git a/backend-django/backend/utils/alimentation.py b/backend-django/backend/utils/alimentation.py new file mode 100644 index 0000000..102a04e --- /dev/null +++ b/backend-django/backend/utils/alimentation.py @@ -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) => + """ + 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) => + """ + 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) => + """ + 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 + + diff --git a/backend-django/backend/utils/alimentation_decorators.py b/backend-django/backend/utils/alimentation_decorators.py new file mode 100644 index 0000000..172c406 --- /dev/null +++ b/backend-django/backend/utils/alimentation_decorators.py @@ -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) diff --git a/backend-django/backend/utils/attributes.py b/backend-django/backend/utils/attributes.py new file mode 100644 index 0000000..7b3991e --- /dev/null +++ b/backend-django/backend/utils/attributes.py @@ -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) diff --git a/backend-django/backend/utils/decisions.py b/backend-django/backend/utils/decisions.py new file mode 100644 index 0000000..2710f07 --- /dev/null +++ b/backend-django/backend/utils/decisions.py @@ -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 {: {: , : } } + :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 {: } + 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 diff --git a/backend-django/backend/utils/decorator.py b/backend-django/backend/utils/decorator.py new file mode 100644 index 0000000..bb49d24 --- /dev/null +++ b/backend-django/backend/utils/decorator.py @@ -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 diff --git a/backend-django/backend/utils/decorators.py b/backend-django/backend/utils/decorators.py new file mode 100644 index 0000000..b029bcc --- /dev/null +++ b/backend-django/backend/utils/decorators.py @@ -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 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 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 diff --git a/backend-django/backend/utils/extraction/__init__.py b/backend-django/backend/utils/extraction/__init__.py new file mode 100644 index 0000000..d5c7511 --- /dev/null +++ b/backend-django/backend/utils/extraction/__init__.py @@ -0,0 +1 @@ +from .administre import * diff --git a/backend-django/backend/utils/extraction/administre.py b/backend-django/backend/utils/extraction/administre.py new file mode 100644 index 0000000..6b55341 --- /dev/null +++ b/backend-django/backend/utils/extraction/administre.py @@ -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 + + diff --git a/backend-django/backend/utils/functions.py b/backend-django/backend/utils/functions.py new file mode 100644 index 0000000..cd7d6da --- /dev/null +++ b/backend-django/backend/utils/functions.py @@ -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 != '': + cls = getattr(cls, name, None) + if not inspect.isclass(cls): + return None + return cls diff --git a/backend-django/backend/utils/initial.py b/backend-django/backend/utils/initial.py new file mode 100644 index 0000000..a9a3a49 --- /dev/null +++ b/backend-django/backend/utils/initial.py @@ -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 diff --git a/backend-django/backend/utils/insertion/__init__.py b/backend-django/backend/utils/insertion/__init__.py new file mode 100644 index 0000000..fb607c2 --- /dev/null +++ b/backend-django/backend/utils/insertion/__init__.py @@ -0,0 +1,2 @@ +from .administre import * +from .commun import * diff --git a/backend-django/backend/utils/insertion/administre.py b/backend-django/backend/utils/insertion/administre.py new file mode 100644 index 0000000..d7c5a8b --- /dev/null +++ b/backend-django/backend/utils/insertion/administre.py @@ -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 + diff --git a/backend-django/backend/utils/insertion/commun.py b/backend-django/backend/utils/insertion/commun.py new file mode 100644 index 0000000..bf7fe62 --- /dev/null +++ b/backend-django/backend/utils/insertion/commun.py @@ -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 diff --git a/backend-django/backend/utils/logging.py b/backend-django/backend/utils/logging.py new file mode 100644 index 0000000..c5a9fda --- /dev/null +++ b/backend-django/backend/utils/logging.py @@ -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 : . si possible, sinon + + :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 diff --git a/backend-django/backend/utils/permissions.py b/backend-django/backend/utils/permissions.py new file mode 100644 index 0000000..0c51be3 --- /dev/null +++ b/backend-django/backend/utils/permissions.py @@ -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 {: , : } } + :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 {: } + :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 {: } + :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 {: {: , : } } + :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 diff --git a/backend-django/backend/utils/predicates.py b/backend-django/backend/utils/predicates.py new file mode 100644 index 0000000..fd5c6eb --- /dev/null +++ b/backend-django/backend/utils/predicates.py @@ -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('_') diff --git a/backend-django/backend/utils/view_predicates.py b/backend-django/backend/utils/view_predicates.py new file mode 100644 index 0000000..4d53ee8 --- /dev/null +++ b/backend-django/backend/utils/view_predicates.py @@ -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) diff --git a/backend-django/backend/utils_calcul.py b/backend-django/backend/utils_calcul.py new file mode 100644 index 0000000..d19fee1 --- /dev/null +++ b/backend-django/backend/utils_calcul.py @@ -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 + + diff --git a/backend-django/backend/utils_extraction.py b/backend-django/backend/utils_extraction.py new file mode 100644 index 0000000..da6585e --- /dev/null +++ b/backend-django/backend/utils_extraction.py @@ -0,0 +1,1367 @@ +""" +Ce module contient les Utilitaires d'extraction des données à partir des fichiers +""" +import datetime +import logging +from multiprocessing.connection import answer_challenge +import time +from enum import Enum, auto +from typing import Callable, Dict + +import numpy as np +import pandas as pd +from django.core.files.base import File +from django.db.models import Q + +from .models import FMOB, Administre +from .models import AvisPosteChoices as AvisPoste +from .models import (Diplome, Domaine, Filiere, Fonction, FormationEmploi, + Garnison, Grade, Marque, MarquesGroupe, Notation, Poste, + PreferencesListe, Administres_Pams, Postes_Pams) +from .models import StatutPamChoices as StatutPam +from .utils.alimentation import BOCols, FmobCols, InseeCols, ReoCols +from .utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from .utils.decorators import execution_time + +logger = get_data_logger(__name__) + +pd.options.mode.chained_assignment = None # default='warn' + +# valeur arbitraire pour symboliser np.nan. En effet un ensemble d'opérations comme +# fillna(np.nan).astype(str).replace([np.nan, 'nan'], [None, None])) +# n'est pas pratique car 'nan' pourrait être une valeur du fichier (qui ne représente pas None) +APP_NAN = '__ogure_nan__' + +# expression régulière pour représenter un caractère alphanumérique +REGEX_NON_ALPHANUM = '[^a-zA-Z0-9]' + + +class FileTypes(str, Enum): + """ enum pour les types de fichiers chargés par l'utilisateur """ + + # militaires à supprimer + ADM_SUPPR = auto() + + # données BO + BO = auto() + + # commentaires + COMMENTS = auto() + + # diplômes + DIPLOME = auto() + + # filières et domaines + DOM_FIL = auto() + + # FMOB + FMOB_FEMP = auto() + + # FMOB PAM A+1 + FMOB_FEMP_PAM_SUIVANT = auto() + + # FUD + FUD = auto() + + # INSEE + INSEE = auto() + + # référentiel FE + REF_FE = auto() + + # référentiel de zones géographiques SHM + REF_GEO = auto() + + # référentiel de gestionnaires + REF_GEST = auto() + + # référentiel organique + REF_ORG = auto() + + # référentiel de sous-viviers/filières + REF_SV_FIL = auto() + + # REO + REO = auto() + + # REO PAM A +1 + REO_PAM_SUIVANT = auto() + + # REO (OCV) + REO_OCV = auto() + + + def __repr__(self): + return self.__str__() + + + +class DataFrameTypes(Enum): + """ enum pour les data frames issus de lectures de fichiers """ + + # nom (type de fichier, libellé) + ADM_SUPPR = (FileTypes.ADM_SUPPR, 'militaires à supprimer') + BO = (FileTypes.BO, 'BO') + COMMENTS = (FileTypes.COMMENTS, 'commentaires') + DIPLOME = (FileTypes.DIPLOME, 'diplômes') + DOM_FIL = (FileTypes.DOM_FIL, 'filières / domaines') + FEMP = (FileTypes.FMOB_FEMP, 'FEMP') + FMOB = (FileTypes.FMOB_FEMP, 'FMOB') + FUD = (FileTypes.FUD, 'FUD') + INSEE = (FileTypes.INSEE, 'mapping INSEE') + REF_FE = (FileTypes.REF_FE, 'référentiel FE') + REF_GEO = (FileTypes.REF_GEO, 'référentiel de zones géographiques SHM') + REF_GEST = (FileTypes.REF_GEST, 'référentiel de gestionnaires') + REF_ORG = (FileTypes.REF_ORG, 'référentiel organique') + REF_SV_FIL = (FileTypes.REF_SV_FIL, 'référentiel de sous-viviers/filières') + REO = (FileTypes.REO, 'REO') + REO_OCV = (FileTypes.REO_OCV, 'postes OCV') + + #Dataframe PAM A + 1 + FEMP_PAM_SUIVANT = (FileTypes.FMOB_FEMP_PAM_SUIVANT, 'FEMP_PAM_SUIVANT') + FMOB_PAM_SUIVANT = (FileTypes.FMOB_FEMP_PAM_SUIVANT, 'FMOB_PAM_SUIVANT') + REO_PAM_SUIVANT = (FileTypes.REO_PAM_SUIVANT, 'REO_PAM_SUIVANT') + + + def __repr__(self): + return self.__str__() + + + +class Files(): + """ + Regroupe les constantes de fichiers + TODO combiner avec FileTypes ? + """ + + class AdmSuppr(): + """ Constantes pour les colonnes du fichier de militaires à supprimer """ + ID_SAP = 'Matricule SAP' # A + + class DonneesBo(): + """ Constantes pour les colonnes du fichier de données BO """ + + CREDO_FE = "CREDO FE act" # X + DATE_ARRIVEE_FE = "Date arrivée FE" + DATE_DEBUT_GRADE = "Grade act DD" + DATE_ENTREE_SERVICE = "Entrée en Service" + DATE_FONCTION_1 = "Fonction -1 DD" + DATE_FONCTION_2 = "Fonction -2 DD" + DATE_FONCTION_3 = "Fonction -3 DD" + DATE_FONCTION_4 = "Fonction -4 DD" + DATE_FONCTION_5 = "Fonction -5 DD" + DATE_FONCTION_6 = "Fonction -6 DD" + DATE_FONCTION_7 = "Fonction -7 DD" + DATE_FONCTION_8 = "Fonction -8 DD" + DATE_FONCTION_9 = "Fonction -9 DD" + DATE_LIEN_SERVICE = "Lien au service DF" + DATE_NAISSANCE = "Naissance" + DATE_POSITION_STATUAIRE = "Date Position statutaire" + DATE_STATUT_CONCERTO = "Situation administrative act DD" # DW + DATE_STATUT_CONCERTO_FUTUR = "Date Position statu future" # DY + ID_SAP = 'Matricule SAP' # A + STATUT_CONCERTO = "Situation admi actuelle" # DX + STATUT_CONCERTO_FUTUR = "Position statutaire future" # DZ + + + class FMOB(): + """ Constantes pour les colonnes du fichier de FMOB """ + + ID_SAP = 'Matricule SAP' + + + class REO(): + """ Constantes pour les colonnes du fichier de FMOB """ + + CODE_POSTAL = 'Code Postal /OB G' + + + + +def open_excel(io, sheetname, engine=None, header=0, usecols=None, skiprows=0, dtype=None, converters=None): + """ + Ouvre un onglet spécifique d'un fichier excel + + :type io: chaine de caractères + :param io: chemin du fichier excel + + :type sheetname: chaine de caractères + :param sheetname: nom de l'onglet à ouvrir + + :type header: entier + :param header: indice de la ligne comportant le nom des colonnes + + :return: - **df** (*DataFrame*): tableau de l'onglet à ouvrir. + """ + + df = pd.read_excel(io, sheet_name=sheetname, engine=engine, header=header, usecols=usecols, skiprows=skiprows, dtype=dtype, converters=converters) + + return df + + +def read_files_by_type(files_by_type: Dict[FileTypes, File]) -> Dict[DataFrameTypes, pd.DataFrame]: + """ + Lit les fichiers donnés et renvoie des data frames. + + :param files_by_type: fichiers indexés par type + :type files_by_type: Dict[FileTypes, File] + + :return: data frame indexés par type + :rtype: Dict[DataFrameTypes, pd.DataFrame] + """ + df_by_type = {} + + # FIXME: (front) Forcer l'engine en fonction de l'extension du fichier + logger.info('------------------ Start File Processing -----------------------') + + @execution_time(logger_factory=data_perf_logger_factory) + def read_file(label: str, file: File, read_func: Callable[[None], pd.DataFrame]) -> pd.DataFrame: + """ Utilise la fonction pour lire le fichier """ + logger.debug('Lecture du fichier %s', label) + try: + df = read_func(file) + logger.info('Lecture du fichier %s ------> Succès', label.ljust(40)) + except BaseException: + logger.error('Lecture du fichier %s ------> Echec', label.ljust(40)) + raise + return df + + def read_file_add_df(df_type: DataFrameTypes, read_func: Callable[[None], pd.DataFrame]) -> None: + """ + Vérifie qu'il existe un fichier pour le type de DataFrame donné. + Si oui, utilise la fonction pour le lire et ajoute le DataFrame au résultat. + + :param df_type: type de DataFrame + :type df_type: DataFrameTypes + + :param read_func: fonction de lecture du fichier + :type read_func: Callable[[None], pd.DataFrame] + """ + file_type = df_type.value[0] + file = files_by_type.get(file_type) + if file: + df_by_type[df_type] = read_file(df_type.value[1], file, read_func) + + read_file_add_df(DataFrameTypes.ADM_SUPPR, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.BO, lambda f: open_excel(f, sheetname=0, engine='openpyxl', usecols=BOCols.columns(), converters=BOCols.converters())) + read_file_add_df(DataFrameTypes.COMMENTS, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.DIPLOME, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.DOM_FIL, lambda f: open_excel(f, sheetname='Feuil2', engine='openpyxl')) + read_file_add_df(DataFrameTypes.FEMP, lambda f: open_excel(f, sheetname='FEMP', engine='openpyxl', converters=FmobCols.converters())) + read_file_add_df(DataFrameTypes.FMOB, lambda f: open_excel(f, sheetname='FMOB', engine='openpyxl', converters=FmobCols.converters())) + read_file_add_df(DataFrameTypes.FEMP_PAM_SUIVANT, lambda f: open_excel(f, sheetname='FEMP', engine='openpyxl', converters=FmobCols.converters())) + read_file_add_df(DataFrameTypes.FMOB_PAM_SUIVANT, lambda f: open_excel(f, sheetname='FMOB', engine='openpyxl', converters=FmobCols.converters())) + read_file_add_df(DataFrameTypes.FUD, lambda f: open_excel(f, sheetname=0, engine='openpyxl', skiprows=1)) + read_file_add_df(DataFrameTypes.INSEE, lambda f: open_excel(f, sheetname=0, engine='openpyxl', usecols=InseeCols.columns(), converters=InseeCols.converters())) + read_file_add_df(DataFrameTypes.REF_FE, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.REF_GEO, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.REF_GEST, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.REF_ORG, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.REF_SV_FIL, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + read_file_add_df(DataFrameTypes.REO, lambda f: open_excel(f, sheetname=0, engine='openpyxl', converters=ReoCols.converters())) + read_file_add_df(DataFrameTypes.REO_PAM_SUIVANT, lambda f: open_excel(f, sheetname=0, engine='openpyxl', converters=ReoCols.converters())) + read_file_add_df(DataFrameTypes.REO_OCV, lambda f: open_excel(f, sheetname=0, engine='openpyxl')) + + + logger.info('------------------- End File Processing ------------------------') + return df_by_type + +#Fonction pour la table PAM +def to_table_pam(date): + """ + 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(date.year) + annee_pam_suivant = str(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 = date + pam_date_suivant = pam_date + pam_statut = "PAM en cours" + pam_statut_suivant = "PAM A+1" + + pam['pam_id'] = [pam_id,pam_id_suivant] + pam['pam_date'] = [pam_date,pam_date_suivant] + pam['pam_libelle'] = [pam_libelle,pam_libelle_suivant] + pam['pam_statut'] = [pam_statut,pam_statut_suivant] + + return pam + +# Fonction pour la table Domaine +def to_table_domaines(df_domaines): + """ + Transformation de la table domaines. Sélection et renommage des champs. + + :param df_domaines: tableau domaines + :type df_domaines: class:`pandas.DataFrame` + + :return: tableau transformé contenant les information de domaines + :rtype: class:`pandas.DataFrame` + """ + col_pk = Domaine.Cols.PK + col_mapping = {'DOMAINE': col_pk} + df = df_domaines[col_mapping.keys()] + df = (df.rename(columns=col_mapping) + .dropna(subset=[col_pk]) + .drop_duplicates(subset=[col_pk])) + df[col_pk] = df[col_pk].str.replace(REGEX_NON_ALPHANUM, '_', regex=True) + return df + + +# Fonction pour la table Filières +def to_table_filieres(df_filiere): + """ + Transformation de la table filieres. Sélection et renommage des champs. + + + :type df_filiere: dataframe + :param df_filiere: tableau filieres + + :return: - **filieres** (*DataFrame*): Tableau transformé contenant les information de filieres. + + """ + col_fil = ['DOMAINE', 'FILIERE'] + filieres = df_filiere[col_fil] + filieres.rename(columns={'DOMAINE': 'domaine_id', 'FILIERE': 'f_code'}, inplace=True) + filieres = (filieres.fillna(APP_NAN) + .replace({APP_NAN: None})) + + return filieres + + +# Fonction pour la table Garnison +def to_table_garnisons(df_insee, df_donneebo): + """Création de la table Garnison à partir des tables insee et donnee bo. Sélection et renommage des champs. + + + :type df_insee: dataframe + :param df_insee: table insee + + :type df_donneebo: dataframe + :param df_donneebo: table donnees BO + + + :return: - **garnisons** (*DataFrame*): Tableau créé contenant les information de garnisons. +. + + """ + garnisons = df_donneebo.drop_duplicates(subset="Garnison act", keep='first') + garnisons = garnisons.merge(df_insee, how="left", left_on="Matricule SAP", right_on="Matricule SAP") + garnisons = garnisons[['CODE INSEE', 'Garnison act', 'CODE POSTAL']] + garnisons.drop_duplicates(subset="CODE INSEE", keep='first', inplace=True) + garnisons.columns = ["gar_id", "gar_lieu", "gar_code_postal"] + garnisons.reset_index(drop=True, inplace=True) + + return garnisons + + +# Fonction pour la table affectation +def to_table_affectation(df_donneebo): + """Création de la table Affectation à partir de la table donnee bo. Sélection et renommage des champs. + + :type df_donneebo: dataframe + :param df_donneebo: table donnees BO + + :type to_table_administre_df: dataframe + :param to_table_administre_df: le retour de la fonction to_table_administre + + + :return: - **all_affectation** (*DataFrame*): Tableau créé contenant les information des affectations. +. + + """ + all_affec = [] + administres_fields = ('a_id_sap',) + administres = pd.DataFrame.from_records(Administre.objects.all().values(*administres_fields)) + all_donnees = df_donneebo.merge(administres, how='inner', left_on='Matricule SAP', right_on='a_id_sap') + for i in range(1, 10): + frames = ['Matricule SAP', 'Affectation -' + str(i) + ' L', 'Affectation -' + str(i) + ' DD'] + df_temp = all_donnees[frames] + df_temp.columns = ['Matricule SAP', 'Affectation L', 'Affectation D'] + df_temp.dropna(subset=['Affectation L'], inplace=True) + all_affec.append(df_temp) + + all_affectation = pd.concat(all_affec, ignore_index=True) + all_affectation.drop_duplicates(inplace=True, ignore_index=True) + + return all_affectation + + +# Fonction pour la table affectation +def to_table_diplome(df_diplome): + """Création de la table Affectation à partir de la table donnee bo. Sélection et renommage des champs. + + :type df_diplome: dataframe + :param df_diplome: table des diplomes + + + :return: - **all_diplome** (*DataFrame*): Tableau créé contenant les information des diplomes. +. + + """ + all_dip = [] + administres_fields = ('a_id_sap',) + administres = pd.DataFrame.from_records(Administre.objects.all().values(*administres_fields)) + all_diplomes = df_diplome.merge(administres, how='inner', left_on='Matricule SAP', right_on='a_id_sap') + for i in range(1, 11): + frames = ['Matricule SAP', 'Diplôme militaire -' + str(i) + ' L', 'Diplôme militaire -' + str(i) + ' D', 'Diplôme militaire -' + str(i) + ' note', + 'Diplôme militaire -' + str(i) + ' niveau'] + df_temp = all_diplomes[frames] + df_temp.columns = ['Matricule SAP', 'Diplôme militaire L', 'Diplôme militaire D', 'Diplôme militaire note', 'Diplôme militaire niveau'] + df_temp.dropna(subset=['Diplôme militaire L'], inplace=True) + all_dip.append(df_temp) + + all_diplome = pd.concat(all_dip, ignore_index=True) + all_diplome.drop_duplicates(inplace=True, ignore_index=True) + all_diplome = all_diplome.groupby(['Matricule SAP', 'Diplôme militaire L', 'Diplôme militaire D'])['Diplôme militaire note'].agg('max').reset_index() + + return all_diplome + + +def to_table_fud(df_fud): + """Création de la table Affectation à partir de la table donnee bo. Sélection et renommage des champs. + + :type df_fud: dataframe + :param df_fud: table fud + + :type to_table_administre_df: dataframe + :param to_table_administre_df: table administre de la fonction to_table_administre dans extraction + + :return: - **all_fuds** (*DataFrame*): Tableau créé contenant les information des FUDs. +. + + """ + all_fud = [] + administres_fields = ('a_id_sap',) + administres = pd.DataFrame.from_records(Administre.objects.all().values(*administres_fields)) + all_fuds = df_fud.merge(administres, how='inner', left_on='Matricule SAP', right_on='a_id_sap') + for i in range(1, 11): + frames = ['Matricule SAP', 'FUD -' + str(i) + ' L', 'FUD -' + str(i) + ' DD', 'FUD -' + str(i) + ' DF'] + df_temp = all_fuds[frames] + df_temp.columns = ['Matricule SAP', 'FUD L', 'FUD DD', 'FUD DF'] + df_temp.dropna(subset=['FUD L'], inplace=True) + all_fud.append(df_temp) + + all_fud = pd.concat(all_fud, ignore_index=True) + all_fud.drop_duplicates(inplace=True, ignore_index=True) + + return all_fud + + +def to_table_administre_notation(df_donne_bo): + """Création de la table Notation à partir de la table donnee bo. Sélection et renommage des champs. + + :type df_donne_bo: dataframe + :param df_donne_bo: table donnee bo + + :type to_table_administre_df: dataframe + :param to_table_administre_df: table administre de la fonction to_table_administre dans extraction + + :return: - **all_fuds** (*DataFrame*): Tableau créé contenant les information des FUDs. +. + + """ + + donne_bo = df_donne_bo.copy() + donne_bo.drop_duplicates(['Matricule SAP'], inplace=True, ignore_index=True) + all_notation = [] + frames = ['Matricule SAP', 'Année notation A', 'IRIS / RAC retenu A', 'NR/NGC cumulé A', + 'QSR A', 'Apt resp / Emp sup A', 'Potentiel responsabilités catégorie sup A', + 'Age en années (au 31/12)'] + df_temp = donne_bo[frames] + df_temp.columns = ['Matricule SAP', 'no_annne_de_notation', 'no_rac_ou_iris_cumule', 'no_nr_ou_iris', 'no_rf_qsr', 'no_aptitude_emploie_sup', + 'no_potentiel_responsabilite_sup', 'no_age_annees'] + all_notation.append(df_temp) + for i in range(1, 6): + frames = ['Matricule SAP', 'Année notation A-' + str(i), 'IRIS / RAC retenu A-' + str(i), 'NR/NGC cumulé A-' + str(i), + 'QSR A-' + str(i), 'Apt resp / Emp sup A-' + str(i), 'Potentiel responsabilités catégorie sup A-' + str(i), + 'Age en années (au 31/12)'] + df_temp = donne_bo[frames] + df_temp.columns = ['Matricule SAP', 'no_annne_de_notation', 'no_rac_ou_iris_cumule', 'no_nr_ou_iris', 'no_rf_qsr', 'no_aptitude_emploie_sup', + 'no_potentiel_responsabilite_sup', 'no_age_annees'] + df_temp['no_age_annees'] = df_temp['no_age_annees'].astype(int) - i + all_notation.append(df_temp) + + all_notations = pd.concat(all_notation, ignore_index=True) + all_notations.drop_duplicates(inplace=True, ignore_index=True) + all_notations = (all_notations.fillna(APP_NAN) + .replace({APP_NAN: None})) + + administres_fields = ('a_id_sap',) + administres = pd.DataFrame.from_records(Administre.objects.all().values(*administres_fields)) + all_notations = all_notations.merge(administres, how='inner', left_on='Matricule SAP', right_on='a_id_sap') + all_notations = all_notations.reset_index(drop= True) + + all_notations = all_notations.fillna(np.NAN).replace('None',np.NAN) + all_notations = all_notations.rename(columns = {'Matricule SAP':'administre_id'}) + all_notations['key'] = all_notations['administre_id'].astype(float).astype(str) + '_' + all_notations['no_age_annees'].astype(float).astype(str) + + return all_notations + + +# Fonction d'extraction du fichier contenant les militaires à supprimer +def to_table_suppression_administres(df_adm_suppr): + """ + Création du dataframe adm_suppr. + + :return: - **adm_suppr** (*DataFrame*): Tableau créé contenant les information des administrés à supprimer. + """ + df_adm_suppr = (df_adm_suppr.drop_duplicates(subset=[Files.AdmSuppr.ID_SAP], keep='first') + .dropna(subset=[Files.AdmSuppr.ID_SAP])) + administres = pd.DataFrame.from_records(Administre.objects.all().values('a_id_sap')) + all_adm_suppr = df_adm_suppr.merge(administres, how='inner', left_on=Files.AdmSuppr.ID_SAP, right_on='a_id_sap') + adm_suppr = pd.DataFrame(columns=['a_id_sap']) + adm_suppr['a_id_sap'] = all_adm_suppr[Files.AdmSuppr.ID_SAP].astype(int) + adm_suppr = adm_suppr.reset_index(drop=True) + + return adm_suppr + + +# Fonction pour la table Fonction +def to_table_fonctions(reo): + """ + Création de la table Fonction à partir dela talbe REO. Sélection et renommage des champs. + :type reo: dataframe + :param reo: table reo + :return: - **fonctions** (*DataFrame*): Tableau créé contenant les information de fonction. + """ + # sélection et renommage des champs + fonctions = reo[[ReoCols.FONCTION_ID.value, ReoCols.FONCTION_LIBELLE.value]] + fonctions.drop_duplicates(keep='first', inplace=True) + fonctions = fonctions.rename(columns={ReoCols.FONCTION_ID.value: 'fon_id', ReoCols.FONCTION_LIBELLE.value: 'fon_libelle'}) + fonctions['fon_id'] = fonctions['fon_id'].astype(int, errors='ignore') + fonctions = (fonctions.drop_duplicates(subset=['fon_id'], keep='first') + .dropna() + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + return fonctions + + + +# Fonction pour la table RefGest +def to_table_ref_gest(df_ref_gest): + """ + Création de la table RefGest à partir de la table Référentiel gestionnaires DRHAT anonymisé. Sélection et renommage des champs. + + :type df_ref_gest: dataframe + :param df_ref_gest: table Référentiel gestionnaires DRHAT anonymisé + + :return: - **ref_gest** (*DataFrame*): Tableau créé contenant les information du référentiel gestionnaire. + + """ + # sélection et renommage des champs + ref_gest = df_ref_gest.iloc[:, 0:8] + ref_gest.columns = ['ref_gest_sap', + 'ref_gest_username', + 'ref_gest_email', + 'ref_gest_first_name', + 'ref_gest_last_name', + 'ref_gest_grade', + 'ref_gest_niv_org', + 'ref_gest_org_id'] + + ref_gest = (ref_gest.drop_duplicates(subset=['ref_gest_sap'], keep='first') + .dropna(subset=['ref_gest_sap']) + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + + return ref_gest + + +# Fonction pour la table RefOrg +def to_table_ref_org(df_ref_org): + """ + Création de la table RefOrg à partir de la table Référentiel organique. Sélection et renommage des champs. + + :type df_ref_org: dataframe + :param df_ref_org: table Référentiel organique + + :return: - **ref_org** (*DataFrame*): Tableau créé contenant les information du référentiel organique. + + """ + # sélection et renommage des champs + ref_org = df_ref_org.iloc[:, 0:16] + ref_org.columns = ['ref_org_code_niv_org1', + 'ref_org_lib_niv_org1', + 'ref_org_code_niv_org2', + 'ref_org_lib_niv_org2', + 'ref_org_code_niv_org3', + 'ref_org_lib_niv_org3', + 'ref_org_code_niv_org4', + 'ref_org_lib_niv_org4', + 'ref_org_niv_org', + 'ref_org_ref_fe', + 'ref_org_ref_sv_fil', + 'ref_org_droit_lect', + 'ref_org_droit_ecr', + 'ref_org_expert_hme', + 'ref_org_bvt', + 'ref_org_itd'] + + # Création d'une clé primaire (inexistante dans le référentiel Excel importé) + # Pour cela, on parcourt toutes les lignes du référentiel + # A chaque ligne, la clé primaire sera le code_niv_org non vide d'indice le plus élevé + + list_pk = [] + + for i in range(ref_org.shape[0]): + + code_niv_org4 = ref_org.loc[i, 'ref_org_code_niv_org4'] # code_niv_org d'indice 4 + code_niv_org3 = ref_org.loc[i, 'ref_org_code_niv_org3'] # code_niv_org d'indice 3 + code_niv_org2 = ref_org.loc[i, 'ref_org_code_niv_org2'] # code_niv_org d'indice 2 + code_niv_org1 = ref_org.loc[i, 'ref_org_code_niv_org1'] # code_niv_org d'indice 1 + + if not pd.isnull(code_niv_org4): + list_pk.append(code_niv_org4) + elif not pd.isnull(code_niv_org3): + list_pk.append(code_niv_org3) + elif not pd.isnull(code_niv_org2): + list_pk.append(code_niv_org2) + else: + list_pk.append(code_niv_org1) + + ref_org.insert(0, 'ref_org_code', list_pk) + + for c in ['ref_org_ref_fe', + 'ref_org_ref_sv_fil', + 'ref_org_droit_lect', + 'ref_org_droit_ecr', + 'ref_org_expert_hme', + 'ref_org_bvt', + 'ref_org_itd']: + + ref_org[c] = pd.notnull(ref_org[c]) + + ref_org = (ref_org.drop_duplicates(subset=['ref_org_code'], keep='first') + .dropna(subset=['ref_org_code']) + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + + return ref_org + + +# Fonction pour la table RefSvFil +def to_table_ref_sv_fil(df_ref_sv_fil): + """ + Création de la table RefSvFil à partir de la table Référentiel sous-vivier filiere. Sélection et renommage des champs. + + :type df_ref_sv_fil: dataframe + :param df_ref_sv_fil: table Référentiel sous-vivier filiere + + :return: - **ref_sv_fil** (*DataFrame*): Tableau créé contenant les information du référentiel sous-vivier filiere. + + """ + # sélection et renommage des champs + ref_sv_fil = df_ref_sv_fil.iloc[:, 0:12] + ref_sv_fil.columns = ['ref_sv_fil_code_niv_org1', + 'ref_sv_fil_lib_niv_org1', + 'ref_sv_fil_code_niv_org2', + 'ref_sv_fil_lib_niv_org2', + 'ref_sv_fil_code_niv_org3', + 'ref_sv_fil_lib_niv_org3', + 'ref_sv_fil_code_niv_org4', + 'ref_sv_fil_lib_niv_org4', + 'ref_sv_fil_dom_gest', + 'ref_sv_fil_dom', + 'ref_sv_fil_fil', + 'ref_sv_fil_cat'] + + # On parcourt toutes les lignes du référentiel + # A chaque ligne, la clé primaire sera le code_niv_org non vide d'indice le plus élevé + + list_pk = [] + for i in range(ref_sv_fil.shape[0]): + + code_niv_org4 = ref_sv_fil.loc[i, 'ref_sv_fil_code_niv_org4'] # code_niv_org d'indice 4 + code_niv_org3 = ref_sv_fil.loc[i, 'ref_sv_fil_code_niv_org3'] # code_niv_org d'indice 3 + code_niv_org2 = ref_sv_fil.loc[i, 'ref_sv_fil_code_niv_org2'] # code_niv_org d'indice 2 + code_niv_org1 = ref_sv_fil.loc[i, 'ref_sv_fil_code_niv_org1'] # code_niv_org d'indice 1 + + if not pd.isnull(code_niv_org4): + list_pk.append(code_niv_org4) + elif not pd.isnull(code_niv_org3): + list_pk.append(code_niv_org3) + elif not pd.isnull(code_niv_org2): + list_pk.append(code_niv_org2) + else: + list_pk.append(code_niv_org1) + + # Ici, ref_sv_fil_code n'est pas une clé primaire car pour un même code il peut y avoir plusieurs couples DOM/FIL + # A la différence de ce qui est fait dans to_table_ref_org, on ne supprime donc pas les doublons de la colonne 'ref_sv_fil_code' + ref_sv_fil.insert(0, 'ref_sv_fil_code', list_pk) + ref_sv_fil = (ref_sv_fil.drop_duplicates(keep='first') + .dropna(subset=['ref_sv_fil_code']) + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True) + .drop(ref_sv_fil.columns[1:9], axis=1) + .reset_index(drop=True)) + ref_sv_fil['sous_vivier_id'] = ref_sv_fil['ref_sv_fil_dom'].astype(str) + ", " + ref_sv_fil["ref_sv_fil_fil"].astype(str) + ", " + ref_sv_fil["ref_sv_fil_cat"].astype(str) + + return ref_sv_fil + + +# Fonction pour la table SousVivier +def to_table_sous_vivier(df_ref_sv_fil): + """ + Création de la table SousVivier à partir de la table Référentiel sous-vivier filiere. Sélection et renommage des champs. + + :type df_ref_sv_fil: dataframe + :param df_ref_sv_fil: table Référentiel sous-vivier filiere + + :return: - **sous_vivier** (*DataFrame*): Tableau créé contenant les information de la table SousVivier. + + """ + # sélection et renommage des champs + sous_vivier = df_ref_sv_fil.iloc[:, [9, 10, 11]] + sous_vivier.columns = ['sv_dom', 'sv_fil', 'sv_cat'] + sous_vivier = (sous_vivier.drop_duplicates(keep='first') + .dropna() + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + + sous_vivier.insert(0, 'sv_id', '') + sous_vivier['sv_id'] = sous_vivier['sv_dom'].astype(str) + ", " + sous_vivier["sv_fil"].astype(str) + ", " + sous_vivier["sv_cat"].astype(str) + sous_vivier.insert(1, 'sv_libelle', '') + sous_vivier['sv_libelle'] = sous_vivier['sv_dom'].astype(str) + ", " + sous_vivier["sv_fil"].astype(str) + ", " + sous_vivier["sv_cat"].astype(str) + return sous_vivier + + + +# Fonction pour le référentiel FE +def to_table_ref_fe(df_ref_fe): + """ + Adaptation du dataframe issu de la table Référentiel FE. Sélection et renommage des champs. + + :type df_ref_fe: dataframe + :param df_ref_fe: table Référentiel FE + + :return: - **ref_fe** (*DataFrame*): Tableau créé contenant les information du référentiel FE. + + """ + # sélection et renommage des champs + ref_fe = df_ref_fe.iloc[:, [1, 3, 4, 5, 9, 12, 15, 16, 17, 18]] + ref_fe.columns = ['fe_credo', 'fe_mere_credo', 'fe_mere_la', 'fe_fot', + 'fe_abo_fe', 'fe_pilier_niv1', 'fe_code_niv_org4', + 'fe_niv_org4', 'fe_code_niv_org4_mdr', 'fe_niv_org4_mdr'] + + ref_fe = (ref_fe.drop_duplicates(subset=['fe_credo'], keep='first') + .dropna(subset=['fe_credo']) + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + + return ref_fe + + +# Fonction pour la table Grade +def to_table_grades(bo): + """ + Création de la table Grades à partir de la table donnee BO. Sélection et renommage des champs. + + + :type bo: dataframe + :param bo: table donnees BO + + :return: - **grades** (*DataFrame*): Tableau créé contenant les information de grades. + + """ + # sélection et renommage des champs + grades = pd.DataFrame(bo['GRADE TA'].value_counts().index) + grades['gr_id'] = grades.index + grades.columns = ["gr_code", "gr_id"] + return grades + + +# Fonction pour la table FE avec les données REO et Donnes BO +def to_table_fe(df_ref_fe): + + ref_fe = df_ref_fe.iloc[:, [1, 2, 3, 4, 5, 9, 10, 11, 12, 15, 16, 17, 18]] + ref_fe.columns = ['fe_credo', 'fe_libelle', 'fe_mere_credo', 'fe_mere_la', 'fe_fot', + 'fe_abo_fe', 'fe_garnison_lieu', 'fe_code_postal', 'fe_pilier_niv1', 'fe_code_niv_org4', + 'fe_niv_org4', 'fe_code_niv_org4_mdr', 'fe_niv_org4_mdr'] # vérifier que garnison_id_id existe dans table Garnison + # Pour chaque garnison L, on véfie si il existe dans gar_lieu de la table Garnison, si oui on récupère l'id, si non =>erreur + ref_fe['fe_nb_poste_reo_mdr'] = 0 + ref_fe['fe_nb_poste_reevalue_mdr'] = 0 + ref_fe['fe_nb_poste_vacant_mdr'] = 0 + ref_fe['fe_nb_poste_occupe_mdr'] = 0 + ref_fe['fe_nb_poste_reo_off'] = 0 + ref_fe['fe_nb_poste_reevalue_off'] = 0 + ref_fe['fe_nb_poste_vacant_off'] = 0 + ref_fe['fe_nb_poste_occupe_off'] = 0 + ref_fe['fe_nb_poste_reo_soff'] = 0 + ref_fe['fe_nb_poste_reevalue_soff'] = 0 + ref_fe['fe_nb_poste_vacant_soff'] = 0 + ref_fe['fe_nb_poste_occupe_soff'] = 0 + + ref_fe = (ref_fe.drop_duplicates(subset=['fe_credo'], keep='first') + .dropna(subset=['fe_credo']) + .fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + + return ref_fe + + +# Fonction pour la table MarquesGroupe +def to_table_groupesMarques(): + """ + Création de la table GroupesMarques en dur. + + + :return: - **groupesMarques** (*DataFrame*): Tableau créé contenant les information de groupesMarques. + + """ + + groupesMarques = pd.DataFrame( + columns=['gm_id', 'gm_type', 'gm_code', 'gm_libelle', 'gm_selectionMultiple', 'gm_ordre']) + groupesMarques['gm_id'] = ['1', '2', '3', '4', '5'] + groupesMarques['gm_code'] = ['PRIO', 'GEN', 'ADM', 'POS', 'AFF'] + groupesMarques['gm_type'] = ['C', 'C', 'M', 'P', 'A'] + groupesMarques['gm_libelle'] = ['CIAT', 'Générique', 'Administrés', 'Poste', 'Affectations'] + groupesMarques['gm_selection_multiple'] = np.array([False, True, True, True, True], dtype=object) + groupesMarques['gm_ordre'] = [20, 21, 10, 10, 10] + return groupesMarques + + +# Fonction pour la table Marques +def to_table_marques(): + """ + Création de la table Marques en dur. + + + :return: - **marques** (*DataFrame*): Tableau créé contenant les information de marques. + """ + marques = pd.DataFrame(columns=['groupe_marques_id', 'mar_id', 'mar_code', 'mar_libelle', 'mar_ordre']) + marques['groupe_marques_id'] = ['1', '1', '1', '1', '2','2', '2', '2', '2', '2', '2', '2', '2', '3','3','3','3','4','4','4','4'] + marques['mar_code'] = ['P1', 'P2', 'P3', 'P4','AGS','ETOILE', 'ETOILE2','DRAPEAU', 'DRAPEAU2', + 'ALERTE', 'QUESTION','QUESTION2', 'COCHE', 'BDG','AGR','FDC','RDC','DRAPEAU3', 'DRAPEAU4', 'DRAPEAU5','DRAPEAU6',] + marques['mar_libelle'] = ['1 Passage CIAT', '2 Passage CIAT','3 Passage CIAT','4 Passage CIAT', + 'Agrément STATUT','Affectation poste ITD','Affectation poste CIAT','TRV1','TRV2', 'Attention particulière', 'Attente recrutement','Attente concours', + 'PLS', + 'Blessé de guerre', + 'Agrément REC part', 'Fin de carrière', + 'Couple','TRV1','TRV2','TCOS','C2'] + marques['mar_ordre'] = [1, 2, 3, 4, 1, 2, 3, 4, 5,6,7, 8, 9, 4,3, 2, 1,1,2,3,4] + marques['mar_id'] = ['1_P1', '1_P2', '1_P3', '1_P4', '2_AGS','2_E','2_E2', '2_D', '2_D2', + '2_ALERTE', '2_QUESTION','2_QUESTION2', '2_COCHE', '3_BDG','3_AGR','3_FDC', '3_RDC','4_D3','4_D4','4_D5','4_D6',] + return marques + + +# Fonction pour la table Marques +def to_table_zone_geographique(df_zones): + """ + Création de la table Zones Geographique en dur. + + + :return: - **zones** (*DataFrame*): Tableau créé contenant les information de zones geographiques. + """ + df_zones.drop_duplicates(inplace=True) + zones = pd.DataFrame(columns=['zone_id', 'zone_libelle']) + zones['zone_id'] = df_zones['Zones géographiques SHM'].str.replace(' ', '') + zones['zone_libelle'] = df_zones['Zones géographiques SHM'] + + return zones + + +# Fonction pour la table Marques +def to_table_commentaires(df_com): + """ + Création du dataframe commentaires. + + :return: - **commentaires** (*DataFrame*): Tableau créé contenant les information des commentaires. + """ + df_com.drop_duplicates(inplace=True) + administres = pd.DataFrame.from_records(Administre.objects.all().values('a_id_sap')) + all_commentaires = df_com.merge(administres, how='inner', left_on='IDSAP', right_on='a_id_sap') + commentaires = pd.DataFrame(columns=['a_id_sap', 'a_notes_gestionnaire']) + commentaires['a_id_sap'] = all_commentaires['IDSAP'].astype(int) + commentaires['a_notes_gestionnaire'] = all_commentaires['COMMENTAIRES'] + commentaires = commentaires.reset_index(drop=True) + + return commentaires + + +# Fonction pour la table Poste +def to_table_postes(reo): + """ + Création de la table postes à partir de la table REO. Sélection et renommage des champs. + :type reo: dataframe + :param reo: table REO + :return: - **postes** (*dataframe*): Tableau créé contenant les information de postes. + """ + postes = reo.copy() + # administres = pd.DataFrame.from_records(Administre.objects.all().values()) + # Ancien REO postes = postes[['ID du poste /MC', "Code Poste CREDO", "FE mère CREDO", "FE CREDO", "FE mère LA", "FE LA","Fonction du Poste L", "Domaine emploi LA", "Filière emploi LA", "NF emploi C", "Emploi L", "Catégorie du poste LA", "Marquant DUO/HDUO", "Origine du poste C", "Catégorie/regroupement grade L", "Grade LA"]] + postes = postes[[ + ReoCols.ANNEE_PROJET.value, + ReoCols.FORMATION_EMPLOI.value, + ReoCols.CODE_POSTAL.value, + ReoCols.CATEGORIE.value, + ReoCols.EIP.value, + ReoCols.DOMAINE.value, + ReoCols.FILIERE.value, + ReoCols.CODE_NF.value, + ReoCols.ID_POSTE.value, + ReoCols.FONCTION_ID.value, + ReoCols.FONCTION_LIBELLE.value, + ReoCols.DOMAINE_GESTION.value, + ]] + postes = (postes.dropna(subset=[ReoCols.ID_POSTE.value]) + .drop_duplicates(subset=[ReoCols.ID_POSTE.value], keep='first') + .rename(columns={ + ReoCols.ID_POSTE.value: 'p_id', + ReoCols.FORMATION_EMPLOI.value: 'formation_emploi_id', + ReoCols.DOMAINE.value: 'p_domaine', + ReoCols.ANNEE_PROJET.value: 'p_annee', + ReoCols.FILIERE.value: 'p_filiere', + ReoCols.CODE_NF.value: 'p_nf', + ReoCols.FONCTION_ID.value: 'fonction_id', + ReoCols.FONCTION_LIBELLE.value: 'p_fonction', + ReoCols.CATEGORIE.value: 'p_categorie', + ReoCols.DOMAINE_GESTION.value: 'p_nfs', + ReoCols.CODE_POSTAL.value: 'p_dep', + ReoCols.EIP.value:'p_eip'})) + postes['p_dep'] = postes['p_dep'].fillna('').str.slice(0, 2, 1).replace('', None) + postes = (postes.fillna(APP_NAN) + .replace({APP_NAN: None}) + .reset_index(drop=True)) + + # initialisation des colonnes vides + # traitement de departement + postes['p_date_fin'] = None + postes['p_notes_gestionnaire'] = None + postes['p_notes_partagees'] = None + postes['p_liste_id_marques'] = None + postes['p_flag_particulier'] = None + postes['statut_decision'] = None + + return postes + +# Fonction qui permet de faire l'extraction de rèquete poste-ocv +def to_table_reo_ocv(reo_ocv_df): + """ + Selection des colonnes des attributs . Sélection et renommage des champs. + :type reo: dataframe + :param reo: table REO + :return: - **postes** (*dataframe*): Tableau créé contenant les information de postes. + """ + postes = pd.DataFrame.from_records(Poste.objects.all().values()) + postes_ocv = reo_ocv_df.copy() + postes_ocv = postes_ocv[['Code Poste CREDO', 'Identifiant SAP', 'Marquant DUO/HDUO']] + postes_ocv.dropna(subset=['Code Poste CREDO', 'Identifiant SAP'], inplace=True) + postes_ocv = postes_ocv[postes_ocv['Marquant DUO/HDUO'] != "HDUO"] + postes = postes.merge(postes_ocv, how='inner', left_on='p_id', right_on='Code Poste CREDO') + return postes + +# Fonction qui concatène l'EIP et la formation d'emploi pour avoir un champ unique de poste +def concat_EIP_FE(x): + """ + Construit la table des postes à partir du fichier REO homologué + + + :type x: DataFrame + :param x: contient les colonnes a_eip et formationsEmploi_id à concaténer + + :return: - **dataframe** (*dataframe*): DataFrame avec un champ supplémentaire comportant l'information des deux colonnes. + + """ + str_a_eip = str(x["a_eip"]) + eip = str_a_eip[0:3] + str_a_eip[4:7] + str_a_eip[8:] + fe = str(x["formation_emploi_id"]) + return eip + '_' + fe + + +# Fonction pour la table FMOB de fichier FMOB +def to_table_fmob_fmob(FMOB): + """ + Construit la table FMOB à partir des tables FMOB. + + :type FMOB: DataFrame + :param FMOB: table FMOB + + :return: - **fmob** (*dataframe*): Tableau créé contenant les information de FMOB provenant de FMOB. + """ + + # import du fichier contenant les données + logger.info("----- Début de l'extraction du fichier FMOB pour la table FMOB ----- ") + logger.info('Nombre total de lignes du fichier FMOB : %s', FMOB.shape[0]) + FMOB.dropna(subset=["Matricule SAP", "Millésime FMOB"], axis=0, inplace=True) + logger.info("Nombre total de lignes du fichier FMOB après la suppression des cellules vides dans les colonnes Matricule SAP et Millésime FMOB: %s", FMOB.shape[0]) + FMOB.drop_duplicates(subset=["Matricule SAP"], inplace=True) + logger.info('Nombre total de lignes du fichier FMOB après suppression des doublons dans la colonne Matricule SAP : %s', FMOB.shape[0]) + FMOB.reset_index(inplace=True, drop=True) + + + # sélection et renommage des champs + try: # ADD + col_fmob = ["Matricule SAP", "Millésime FMOB", "Annulation FMOB","Avis CDC à la mutation de l'administré","Avis CDC Mobilité bassin externe","Avis CDC Mobilité bassin interne", + "Avis CDC Mobilité centre intérêt ADT","Avis CDC Mobilité dans la spécialité","Avis CDC Mobilité hors métropole","Avis CDC Mobilité recrutement particulier administré","Sans Suite FMOB", "Date_Deb FMOB", + "Date_Fin FMOB","Date de signature de l'administré", "Date de signature du chef de corps", "Date de VISA du militaire", + "Départ de l'institution SOFF","Mobilité bassin externe","Mobilité bassin interne","Mobilité centre intérêt ADT","Mobilité dans la spécialité", + "Mobilité hors métropole","Mobilité recrutement particulier administré","Motif d'édition LA", "Reconnaissance parcours pro administré", + "Remarques éventuelles de l'intéressé","Avis du commandant de formation", 'Réception DRHAT FMOB'] + fmob = FMOB[col_fmob] + except Exception as e: + raise Exception("Erreur lors de l'extraction du fichier FMOB : les noms de colonnes du fichier ne sont pas corrects") from e + + fmob['fmob_id'] = fmob.index + #changer le fmob_id # ADD & rename + + + + fmob.rename(columns={'Matricule SAP': 'administre_id', 'Millésime FMOB': 'fmob_millesime', + 'Annulation FMOB': "fmob_annulation_fmob", + "Avis CDC à la mutation de l'administré":"fmob_avis_cdc_mutation_administre", + "Avis CDC Mobilité bassin externe":"fmob_avis_cdc_mobilite_externe", + "Avis CDC Mobilité bassin interne":"fmob_avis_cdc_mobilite_interne", + "Avis CDC Mobilité centre intérêt ADT":"fmob_avis_cdc_mobilite_centre_interet", + "Avis CDC Mobilité dans la spécialité":"fmob_avis_cdc_mobilite_specialite", + "Avis CDC Mobilité hors métropole":"fmob_avis_cdc_mobilite_hors_metropole", + "Avis CDC Mobilité recrutement particulier administré":"fmob_avis_cdc_mobilite_recrutement_particulier_admin", + 'Sans Suite FMOB': "fmob_sans_suite_fmob", + 'Date de VISA du militaire': "fmob_date_visa_militaire", + "Départ de l'institution SOFF": "fmob_depart_institution_soff", + "Mobilité bassin externe":"fmob_mobilite_bassin_externe", + "Mobilité bassin interne":"fmob_mobilite_bassin_interne", + "Mobilité centre intérêt ADT":"fmob_mobilite_centre_interet_adt", + "Mobilité dans la spécialité":"fmob_mobilite_dans_specialite", + "Mobilité hors métropole":"fmob_mobilite_hors_metropole", + "Mobilité recrutement particulier administré":"fmob_mobilite_recrutement_particulier_administre", + 'Date_Deb FMOB': "fmob_date_deb_fmob", + 'Date_Fin FMOB': "fmob_date_fin_fmob", + 'Réception DRHAT FMOB' : "fmob_reception_drhat_fmob", + "Date de signature de l'administré": "fmob_date_signature_admin_fmob", + "Date de signature du chef de corps": "fmob_date_signature_chef_de_corps", + "Motif d'édition LA": "fmob_motif_edition_la", + "Reconnaissance parcours pro administré": "fmob_reconnaissance_parcours_pro_administre", + "Remarques éventuelles de l'intéressé" : "fmob_remarques_eventuelles_administres", + "Avis du commandant de formation" : "fmob_avis_commandant_formation", + }, inplace=True, errors='raise') + # Check if O or N + c_x = ['fmob_annulation_fmob', 'fmob_sans_suite_fmob','fmob_reconnaissance_parcours_pro_administre','fmob_reception_drhat_fmob'] + + for c in c_x: + fmob[c] = fmob[c].apply(lambda x: True if x == 'X ' else False) + + #fmob['fmob_depart_institution_soff'] = fmob['fmob_depart_institution_soff'].apply(lambda x: True if x == 'O' else False) + + # ADD and check O or N : + c_o = ['fmob_depart_institution_soff','fmob_avis_cdc_mutation_administre','fmob_avis_cdc_mobilite_externe','fmob_avis_cdc_mobilite_interne', + 'fmob_avis_cdc_mobilite_centre_interet','fmob_avis_cdc_mobilite_specialite','fmob_avis_cdc_mobilite_hors_metropole', + 'fmob_avis_cdc_mobilite_recrutement_particulier_admin','fmob_mobilite_bassin_externe','fmob_mobilite_bassin_interne', + 'fmob_mobilite_centre_interet_adt','fmob_mobilite_dans_specialite','fmob_mobilite_hors_metropole', + 'fmob_mobilite_recrutement_particulier_administre'] + + for c in c_o: + fmob[c] = fmob[c].apply(lambda x: True if x == 'O ' else False) + + #fmob['fmob_depart_institution_soff'] = fmob['fmob_depart_institution_soff'].apply(lambda x: True if x == 'O' else False) + + fmob = (fmob.fillna(APP_NAN) + .replace({APP_NAN: None})) + + # Colonne Statut FMOB + conditions = [ + (fmob['fmob_annulation_fmob'] == True), + (fmob['fmob_sans_suite_fmob'] == True), + (fmob['fmob_reception_drhat_fmob'] == False), + (fmob['fmob_reception_drhat_fmob'] == True), + ] + + values = ['Annulé', 'Classé sans suite', 'Non réceptionné', 'Réceptionné'] + + fmob['fmob_statut'] = np.select(conditions, values) + + logger.info("----- Fin de l'extraction du fichier FMOB pour la table FMOB -----") + + return fmob + +# Fonction pour la table FMOB du fichier FEMP +def to_table_fmob_femp(FEMP): + """ + Construit la table FMOB à partir des tables FEMP. + + :type FEMP: DataFrame + :param FEMP: table FEMP + + + :return: - **femp** (*dataframe*): Tableau créé contenant les information de FMOB provenant de FEMP. + """ + + # import du fichier contenant les données + logger.info("----- Début de l'extraction du fichier FEMP pour la table FMOB ----- ") + logger.info('Nombre total de lignes du fichier FEMP : %s', FEMP.shape[0]) + FEMP.dropna(subset=["Matricule SAP", "Date_Deb FEMP"], axis=0, inplace=True) + logger.info("Nombre total de lignes du fichier FEMP après la suppression des cellules vides dans les colonnes Matricule SAP et Date_Deb FEMP: %s", FEMP.shape[0]) + FEMP.drop_duplicates(subset=["Matricule SAP"], inplace=True) + logger.info('Nombre total de lignes du fichier FEMP après suppression des doublons dans la colonne Matricule SAP : %s', FEMP.shape[0]) + FEMP.reset_index(inplace=True, drop=True) + + # sélection et renommage des champs + + try: + col_femp = ["Matricule SAP", "Annulation FEMP", "Sans Suite FEMP", "Date de signature de l'administré", + "Proposition d'affectation.Verrouillé","Commentaires AC","Millésime FEMP"] + femp = FEMP[col_femp] + except Exception as e: + raise Exception("Erreur lors de l'extraction du fichier FEMP : les noms de colonnes du fichier ne sont pas corrects") from e + + femp.rename(columns={FmobCols.ID_SAP.value: "administre_id", "Annulation FEMP": "fmob_annulation_femp", + "Proposition d'affectation.Verrouillé": "fmob_proposition_affectation_verrouille", + "Sans Suite FEMP": "fmob_sans_suite_femp", + "Date de signature de l'administré": "fmob_date_signature_admin_femp", + "Commentaires AC" : "fmob_commentaire_ac", "Millésime FEMP":"fmob_millesime_femp"}, inplace=True, errors='raise') + + c_x = ['fmob_annulation_femp', 'fmob_sans_suite_femp', "fmob_proposition_affectation_verrouille"] + + for c in c_x: + femp[c] = femp[c].apply(lambda x: True if x == 'X ' else False) + + femp = (femp.fillna(APP_NAN) + .replace({APP_NAN: None})) + + logger.info("----- Fin de l'extraction du fichier FEMP pour la table FMOB -----") + + return femp + + +# Fonction pour la table Preference list +def to_table_liste_preference(pam_id,sv_id): + """ + Construit la table preferences en utilisant le sous-vivier id. + + :type sv_id: entier + :param sv_id: ID du sous-vivier + + + :return: - **new_souhaits_formates** (*dataframe*): thTableau créé contenant les preferences des administres. + """ + # Lire tous les administres avec les souhaits dans le sous_viviers et l'annee du Pam avec AVIS A_MUTER ou A Etudier + + statut_administre = [StatutPam.A_MUTER, StatutPam.A_ETUDIER] + + souhaits_columns = ('id','administre__a_id_sap', 'administre__a_nf_futur', 'administre__a_filiere_futur_id','a_liste_depts_souhaites_pam','pam_id') + souhaits_columns_name = ('administre_pam_id','a_id_sap', 'a_nf_futur', 'a_filiere_futur_id','a_liste_depts_souhaites_pam','pam_id') + + souhaits_db = Administres_Pams.objects.filter(pam_id=pam_id, administre__sous_vivier_id=sv_id, + a_statut_pam_annee__in=statut_administre, + decision__de_decision__isnull=True).values_list(*souhaits_columns) + if not souhaits_db.exists : + logger.debug(f"Pas d'administrés avec le bon statut {statut_administre} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun administrés avec le bon statut {statut_administre} dans le sous vivier {sv_id} et le pam {pam_id}.") + + souhaits = pd.DataFrame.from_records(souhaits_db, columns = souhaits_columns_name) + + # Remplacement des None par un None connue + souhaits = souhaits.replace('None', np.nan) + # Remplacement du Vide par un None connue + souhaits = souhaits.replace('', np.nan) + + # Lire tous les postes dans le sous_viviers et l'annee du Pam avec AVIS P1 ou P2 ou P3 ou P4 + + statut_poste = [AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4] + + poste_columns = ('id','poste__p_id', 'poste__p_dep','poste__p_filiere_id','poste__p_nf','poste__p_eip','p_pam_id') + poste_columns_name = ('poste_pam_id','poste_id', 'p_dep', 'p_filiere_id','p_nf','p_eip','pam_id') + + + postes_db = Postes_Pams.objects.filter(Q(p_pam_id=pam_id) + & Q(poste__sous_viviers=sv_id) + & Q(decisions__de_decision__isnull=True) + & Q(p_avis_pam__in=statut_poste)).values_list(*poste_columns) + + if not postes_db.exists : + logger.debug(f"Pas de postes avec le bon statut {statut_poste} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun postes avec le bon statut {statut_administre} dans le sous vivier {sv_id} et le pam {pam_id}.") + + + postes = pd.DataFrame.from_records(postes_db, columns=poste_columns_name) + + len_administres_traites = len(souhaits) + len_postes_traites = len(postes) + logger.debug('Nb admin traités : ' + str(len_administres_traites)) + logger.debug('Nb groupe_poste traités : ' + str(len_postes_traites)) + + # Supprimer tous les administrateurs sans souhaits + souhaits_nonull = souhaits.dropna(subset=["a_liste_depts_souhaites_pam"], axis=0) + + souhaits_null = souhaits[~souhaits.index.isin(souhaits_nonull.index)]['a_id_sap'] + + len_administres_restants = len(souhaits_nonull) + + logger.debug('Nb admin sans souhaits : ' + str(len_administres_traites - len_administres_restants)) + logger.debug('Les administrés sans souhaits sont : ' + str(list(souhaits_null))) + + if len_administres_restants == 0: + logger.debug(f"Pas d'administrés avec des souhaits dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun administrés n'a renseigné de souhaits - Veuillez vérifier que (Domaine/Filière/NF) et/ou (départements) entre Postes et Administrés soient les mêmes (pas de matching parfait).") + + souhaits_nonull = souhaits_nonull.reset_index(drop=True) + + souhaits_nonull['departement'] = souhaits_nonull['a_liste_depts_souhaites_pam'].apply(lambda x : x.strip().split(',')) + + souhaits_nonull = souhaits_nonull.explode('departement', ignore_index = True) + + souhaits_nonull['departement'] = souhaits_nonull['departement'].replace('AUTRE_ETRANGER', '0') + + souhaits_poste = souhaits_nonull.merge(postes, + how='inner', + left_on=['departement', 'a_filiere_futur_id', 'a_nf_futur'], + right_on=['p_dep', 'p_filiere_id', 'p_nf']) + + if souhaits_poste.empty: + logger.debug(f"Aucun poste ne correspond au souhait du département des administrés dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun poste ne correspond au souhait des administrés dans le sous vivier - Veuillez vérifier que (Domaine/Filière/NF) et/ou (départements) entre Postes et Administrés soient les mêmes (pas de matching parfait).") + + souhaits_poste = souhaits_poste.dropna(subset=['p_eip']) + + souhaits_poste = souhaits_poste.reset_index(drop=True) + + a_id_sap_groupe = souhaits_poste['a_id_sap'].unique() + + new_souhaits_formates = pd.DataFrame(columns=['administre_pam_id', 'a_id_sap', 'departement', 'poste_id', 'poste_pam_id']) + + for a in a_id_sap_groupe: + df = souhaits_poste[souhaits_poste['a_id_sap'] == a][ + ['administre_pam_id','a_id_sap', 'departement', 'poste_id', 'poste_pam_id']] + df.reset_index(drop=False, inplace=True) + df['lp_rang_poste'] = df.index + 1 + + new_souhaits_formates = pd.concat([new_souhaits_formates, df], ignore_index=True) + + # comment choisir l'id ? + new_souhaits_formates['lp_id'] = new_souhaits_formates.index + + new_souhaits_formates['pam_id'] = pam_id + + new_souhaits_formates = new_souhaits_formates.rename(columns={'a_id_sap': 'administre_id'}) + logger.debug('Ranking ending') + return new_souhaits_formates + + + + + +def to_table_liste_preference_selectif(sv_id,pam_id,l_a_id,l_p_id): + """ + Construit la table preferences en utilisant le sous-vivier id. + + :type sv_id: entier + :param sv_id: ID du sous-vivier + + + :return: - **new_souhaits_formates** (*dataframe*): thTableau créé contenant les preferences des administres. + """ + # Lire tous les administres avec les souhaits dans le sous_viviers et l'annee du Pam avec AVIS A_MUTER ou A Etudier + + statut_administre = [StatutPam.A_MUTER, StatutPam.A_ETUDIER] + + souhaits_columns = ('id','administre__a_id_sap', 'administre__a_nf_futur', 'administre__a_filiere_futur_id','a_liste_depts_souhaites_pam','pam_id') + souhaits_columns_name = ('administre_pam_id','a_id_sap', 'a_nf_futur', 'a_filiere_futur_id','a_liste_depts_souhaites_pam','pam_id') + + souhaits_db = Administres_Pams.objects.filter(pam_id=pam_id, administre__sous_vivier_id=sv_id, + a_statut_pam_annee__in=statut_administre, administre_id__in=l_a_id, + decision__de_decision__isnull=True).values_list(*souhaits_columns) + if not souhaits_db.exists : + logger.debug(f"Pas d'administrés avec le bon statut {statut_administre} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun administrés avec le bon statut {statut_administre} dans le sous vivier {sv_id} et le pam {pam_id}.") + + souhaits = pd.DataFrame.from_records(souhaits_db, columns = souhaits_columns_name) + + # Remplacement des None par un None connue + souhaits = souhaits.replace('None', np.nan) + # Remplacement du Vide par un None connue + souhaits = souhaits.replace('', np.nan) + + # Lire tous les postes dans le sous_viviers et l'annee du Pam avec AVIS P1 ou P2 ou P3 ou P4 + + statut_poste = [AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4] + + poste_columns = ('id','poste__p_id', 'poste__p_dep','poste__p_filiere_id','poste__p_nf','poste__p_eip','p_pam_id') + poste_columns_name = ('poste_pam_id','poste_id', 'p_dep', 'p_filiere_id','p_nf','p_eip','pam_id') + + + postes_db = Postes_Pams.objects.filter(Q(p_pam_id=pam_id) + & Q(poste__sous_viviers=sv_id) + & Q(decisions__de_decision__isnull=True) + & Q(poste_id__in=l_p_id) + & Q(p_avis_pam__in=statut_poste)).values_list(*poste_columns) + + if not postes_db.exists : + logger.debug(f"Pas de postes avec le bon statut {statut_poste} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun postes avec le bon statut {statut_administre} dans le sous vivier {sv_id} et le pam {pam_id}.") + + + postes = pd.DataFrame.from_records(postes_db, columns=poste_columns_name) + + len_administres_traites = len(souhaits) + len_postes_traites = len(postes) + logger.debug('Nb admin traités : ' + str(len_administres_traites)) + logger.debug('Nb groupe_poste traités : ' + str(len_postes_traites)) + + # Supprimer tous les administrateurs sans souhaits + souhaits_nonull = souhaits.dropna(subset=["a_liste_depts_souhaites_pam"], axis=0) + + souhaits_null = souhaits[~souhaits.index.isin(souhaits_nonull.index)]['a_id_sap'] + + len_administres_restants = len(souhaits_nonull) + + logger.debug('Nb admin sans souhaits : ' + str(len_administres_traites - len_administres_restants)) + logger.debug('Les administrés sans souhaits sont : ' + str(list(souhaits_null))) + + if len_administres_restants == 0: + logger.debug(f"Pas d'administrés avec des souhaits dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun administrés n'a renseigné de souhaits - Veuillez vérifier que (Domaine/Filière/NF) et/ou (départements) entre Postes et Administrés soient les mêmes (pas de matching parfait).") + + souhaits_nonull = souhaits_nonull.reset_index(drop=True) + + souhaits_nonull['departement'] = souhaits_nonull['a_liste_depts_souhaites_pam'].apply(lambda x : x.strip().split(',')) + + souhaits_nonull = souhaits_nonull.explode('departement', ignore_index = True) + + souhaits_nonull['departement'] = souhaits_nonull['departement'].replace('AUTRE_ETRANGER', '0') + + souhaits_poste = souhaits_nonull.merge(postes, + how='inner', + left_on=['departement', 'a_filiere_futur_id', 'a_nf_futur'], + right_on=['p_dep', 'p_filiere_id', 'p_nf']) + + if souhaits_poste.empty: + logger.debug(f"Aucun poste ne correspond au souhait du département des administrés dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun poste ne correspond au souhait des administrés dans le sous vivier - Veuillez vérifier que (Domaine/Filière/NF) et/ou (départements) entre Postes et Administrés soient les mêmes (pas de matching parfait).") + + souhaits_poste = souhaits_poste.dropna(subset=['p_eip']) + + souhaits_poste = souhaits_poste.reset_index(drop=True) + + a_id_sap_groupe = souhaits_poste['a_id_sap'].unique() + + new_souhaits_formates = pd.DataFrame(columns=['administre_pam_id', 'a_id_sap', 'departement', 'poste_id', 'poste_pam_id']) + + for a in a_id_sap_groupe: + df = souhaits_poste[souhaits_poste['a_id_sap'] == a][ + ['administre_pam_id','a_id_sap', 'departement', 'poste_id', 'poste_pam_id']] + df.reset_index(drop=False, inplace=True) + df['lp_rang_poste'] = df.index + 1 + + new_souhaits_formates = pd.concat([new_souhaits_formates, df], ignore_index=True) + + # comment choisir l'id ? + new_souhaits_formates['lp_id'] = new_souhaits_formates.index + + new_souhaits_formates['pam_id'] = pam_id + + new_souhaits_formates = new_souhaits_formates.rename(columns={'a_id_sap': 'administre_id'}) + logger.debug('Ranking ending') + return new_souhaits_formates \ No newline at end of file diff --git a/backend-django/backend/utils_insertion.py b/backend-django/backend/utils_insertion.py new file mode 100644 index 0000000..c337c30 --- /dev/null +++ b/backend-django/backend/utils_insertion.py @@ -0,0 +1,2502 @@ +""" +Ce module contient les Utilitaires d'insertion des données par groupe dans une table fixé +""" +# import des pré requis +from ast import Mod +import time +import logging +import pandas as pd +import numpy as np +from django.db.models import Choices, Model, Q +from django.forms import model_to_dict +from itertools import islice +import math +from django.contrib.auth import get_user_model +from .models import Poste,Postes_Pams, Administre, Grade, MarquesGroupe, Garnison, SousVivier, Domaine, PAM, Administres_Pams,\ + Filiere, FMOB, Fonction, FormationEmploi, Marque, Notation, PreferencesListe, SousVivierAssociation, \ + ZoneGeographique, RefGest, RefOrg, RefSvFil, Diplome, Affectation, FUD, Administre_Notation, \ + StatutPamChoices as StatutPam, CustomUser +from typing import List, Optional, Tuple, Union +# Definition genrique des fonctions suivantes +from .utils import intOrNone, cleanString, generate_sv_id +from .utils.permissions import get_lvl4_org_codes_by_any_code +from .utils.alimentation_decorators import get_data_logger, data_perf_logger_factory +from .utils.decorators import execution_time +from .utils.insertion.commun import batch_iterator, is_same_model +from .utils_extraction import APP_NAN + +logger = get_data_logger(__name__) + +# TODO: formaliser les logs + +# fonction d'insertion dans la table marques +def insert_PAM(df): + """ + Insertion des données de la table Marque dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + liste_update = [] + ModelType = PAM + update_header = ['pam_date','pam_libelle','pam_statut'] + for i in range(df.shape[0]): + pams = ModelType.objects.filter(pam_id=df.at[i,'pam_id']) + pam = PAM(pam_id=df.at[i, 'pam_id'],pam_date=df.at[i, 'pam_date'], pam_libelle=df.at[i, 'pam_libelle'], + pam_statut=df.at[i, 'pam_statut']) + + if pam.pam_id in pams.values_list('pam_id', flat = True): + liste_update.append(pam) + + else: + cloture=PAM.objects.filter(pam_statut="PAM en cours") + cloture.delete() + liste_create.append(pam) + + if liste_create: + PAM.objects.bulk_create(liste_create) + if liste_update: + PAM.objects.bulk_update(liste_update, fields=update_header) + + return 1 + + +# fonction d'insertion dans la table Grade +def insert_Grade(): + """ + Insertion des données de la table Grade dans la base. Cette fonction a été écrite en dur + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + grades = [ + { + "id": "SDT", + "code": "SDT", + "categorie": "Militaires du rang", + "ordre": "100" + }, + { + "id": "SDT TA", + "code": "SDT TA", + "categorie": "Militaires du rang", + "ordre": "101" + }, + { + "id": "SDT PF", + "code": "SDT PF", + "categorie": "Militaires du rang", + "ordre": "102" + }, + { + "id": "1CL", + "code": "1CL", + "categorie": "Militaires du rang", + "ordre": "110" + }, + { + "id": "1CL TA", + "code": "1CL TA", + "categorie": "Militaires du rang", + "ordre": "111" + }, + { + "id": "1CL PF", + "code": "1CL PF", + "categorie": "Militaires du rang", + "ordre": "112" + }, + { + "id": "CPL", + "code": "CPL", + "categorie": "Militaires du rang", + "ordre": "120" + }, + { + "id": "CPL TA", + "code": "CPL TA", + "categorie": "Militaires du rang", + "ordre": "121" + }, + { + "id": "CPL PF", + "code": "CPL PF", + "categorie": "Militaires du rang", + "ordre": "122" + }, + { + "id": "CCH", + "code": "CCH", + "categorie": "Militaires du rang", + "ordre": "130" + }, + { + "id": "CCH TA", + "code": "CCH TA", + "categorie": "Militaires du rang", + "ordre": "131" + }, + { + "id": "CCH PF", + "code": "CCH PF", + "categorie": "Militaires du rang", + "ordre": "132" + }, + + { + "id": "CC1", + "code": "CC1", + "categorie": "Militaires du rang", + "ordre": "140" + }, + + { + "id": "CC1 TA", + "code": "CC1 TA", + "categorie": "Militaires du rang", + "ordre": "141" + }, + { + "id": "CC1 PF", + "code": "CC1 PF", + "categorie": "Militaires du rang", + "ordre": "142" + }, + { + "id": "SGT", + "code": "SGT", + "categorie": "Sous-officiers", + "ordre": "210" + }, + { + "id": "SGT TA", + "code": "SGT TA", + "categorie": "Sous-officiers", + "ordre": "211" + }, + { + "id": "SGT PF", + "code": "SGT PF", + "categorie": "Sous-officiers", + "ordre": "212" + }, + { + "id": "SCH", + "code": "SCH", + "categorie": "Sous-officiers", + "ordre": "220" + }, + { + "id": "SCH TA", + "code": "SCH TA", + "categorie": "Sous-officiers", + "ordre": "221" + }, + { + "id": "SCH PF", + "code": "SCH PF", + "categorie": "Sous-officiers", + "ordre": "222" + }, + { + "id": "ADJ", + "code": "ADJ", + "categorie": "Sous-officiers", + "ordre": "230" + }, + { + "id": "ADJ TA", + "code": "ADJ TA", + "categorie": "Sous-officiers", + "ordre": "231" + }, + { + "id": "ADJ PF", + "code": "ADJ PF", + "categorie": "Sous-officiers", + "ordre": "232" + }, + { + "id": "ADC", + "code": "ADC", + "categorie": "Sous-officiers", + "ordre": "240" + }, + { + "id": "ADC TA", + "code": "ADC TA", + "categorie": "Sous-officiers", + "ordre": "241" + }, + { + "id": "ADC TS", + "code": "ADC TS", + "categorie": "Sous-officiers", + "ordre": "242" + }, + { + "id": "ADC PF", + "code": "ADC PF", + "categorie": "Sous-officiers", + "ordre": "243" + }, + { + "id": "MAJ", + "code": "MAJ", + "categorie": "Sous-officiers", + "ordre": "250" + }, + { + "id": "MAJ TA", + "code": "MAJ TA", + "categorie": "Sous-officiers", + "ordre": "251" + }, + { + "id": "MAJ PF", + "code": "MAJ PF", + "categorie": "Sous-officiers", + "ordre": "252" + }, + { + "id": "ASP", + "code": "ASP", + "categorie": "Officiers subalternes", + "ordre": "310" + }, + { + "id": "ASP TA", + "code": "ASP TA", + "categorie": "Officiers subalternes", + "ordre": "311" + }, + { + "id": "ASP PF", + "code": "ASP PF", + "categorie": "Officiers subalternes", + "ordre": "312" + }, + { + "id": "SLT", + "code": "SLT", + "categorie": "Officiers subalternes", + "ordre": "320" + }, + { + "id": "SLT TA", + "code": "SLT TA", + "categorie": "Officiers subalternes", + "ordre": "321" + }, + { + "id": "SLT PF", + "code": "SLT PF", + "categorie": "Officiers subalternes", + "ordre": "322" + }, + { + "id": "EOX", + "code": "EOX", + "categorie": "Officiers subalternes", + "ordre": "327" + }, + { + "id": "LTN", + "code": "LTN", + "categorie": "Officiers subalternes", + "ordre": "330" + }, + { + "id": "LTN TA", + "code": "LTN TA", + "categorie": "Officiers subalternes", + "ordre": "331" + }, + { + "id": "LTN PF", + "code": "LTN PF", + "categorie": "Officiers subalternes", + "ordre": "332" + }, + { + "id": "CNE", + "code": "CNE", + "categorie": "Officiers subalternes", + "ordre": "340" + }, + { + "id": "CNE TA", + "code": "CNE TA", + "categorie": "Officiers subalternes", + "ordre": "341" + }, + { + "id": "CNE PF", + "code": "CNE PF", + "categorie": "Officiers subalternes", + "ordre": "342" + }, + { + "id": "CNE TS", + "code": "CNE TS", + "categorie": "Officiers subalternes", + "ordre": "343" + }, + { + "id": "CASP", + "code": "CASP", + "categorie": "Officiers subalternes", + "ordre": "390" + }, + { + "id": "CASP TA", + "code": "CASP TA", + "categorie": "Officiers subalternes", + "ordre": "390" + }, + { + "id": "CASP PF", + "code": "CASP PF", + "categorie": "Officiers subalternes", + "ordre": "390" + }, + { + "id": "CR3", + "code": "CR3", + "categorie": "Officiers subalternes", + "ordre": "391" + }, + { + "id": "CR3 TA", + "code": "CR3 TA", + "categorie": "Officiers subalternes", + "ordre": "391" + }, + { + "id": "CR3 PF", + "code": "CR3 PF", + "categorie": "Officiers subalternes", + "ordre": "391" + }, + { + "id": "CR2", + "code": "CR2", + "categorie": "Officiers subalternes", + "ordre": "392" + }, + { + "id": "CR2 TA", + "code": "CR2 TA", + "categorie": "Officiers subalternes", + "ordre": "392" + }, + { + "id": "CR2 PF", + "code": "CR2 PF", + "categorie": "Officiers subalternes", + "ordre": "392" + }, + { + "id": "CR1", + "code": "CR1", + "categorie": "Officiers subalternes", + "ordre": "393" + }, + { + "id": "CR1 TA", + "code": "CR1 TA", + "categorie": "Officiers subalternes", + "ordre": "393" + }, + { + "id": "CR1 PF", + "code": "CR1 PF", + "categorie": "Officiers subalternes", + "ordre": "393" + }, + { + "id": "CDT", + "code": "CDT", + "categorie": "Officiers supérieurs", + "ordre": "400" + }, + { + "id": "CDT PF", + "code": "CDT PF", + "categorie": "Officiers supérieurs", + "ordre": "401" + }, + { + "id": "CDT TA", + "code": "CDT TA", + "categorie": "Officiers supérieurs", + "ordre": "402" + }, + { + "id": "LCL", + "code": "LCL", + "categorie": "Officiers supérieurs", + "ordre": "410" + }, + { + "id": "LCL TA", + "code": "LCL TA", + "categorie": "Officiers supérieurs", + "ordre": "411" + }, + { + "id": "LCL PF", + "code": "LCL PF", + "categorie": "Officiers supérieurs", + "ordre": "412" + }, + { + "id": "COL", + "code": "COL", + "categorie": "Officiers supérieurs", + "ordre": "420" + }, + { + "id": "COL PF", + "code": "COL PF", + "categorie": "Officiers supérieurs", + "ordre": "421" + }, + { + "id": "COL TA", + "code": "COL TA", + "categorie": "Officiers supérieurs", + "ordre": "422" + } + ] + for i in range(len(grades)): + grade = Grade.objects.filter(gr_code=grades[i]['code']) + grade_create = Grade(gr_code=grades[i]['code'], gr_categorie=grades[i]['categorie'], + gr_ordre=grades[i]['ordre']) + if grade.exists(): + # Update + grade.update(gr_categorie=grades[i]['categorie'], gr_ordre=grades[i]['ordre']) + else: + liste_create.append(grade_create) + + Grade.objects.bulk_create(liste_create) + return 1 + + +def update_domaine(df): + """ + Met à jour la table des domaines à partir du DataFrame. + + :param df: données + :type df: class:`pandas.DataFrame` + """ + ModelType = Domaine + Cols = ModelType.Cols + col_pk = Cols.PK + fields_to_update = () + models_in_db = {m.pk: m for m in ModelType.objects.only('pk', *fields_to_update)} + + batch_size = 100 + dict_create = {} + dict_update = {} + dict_up_to_date = {} + error_count = 0 + for idx, rec in enumerate(df.to_dict('records')): + pk = rec.get(col_pk) + try: + in_db = models_in_db.get(pk) + model = ModelType(pk=pk, **{f: rec.get(f) for f in fields_to_update}) + if not in_db: + # model.full_clean(validate_unique=False) + dict_create.setdefault(pk, model) + elif not is_same_model(fields_to_update, in_db, model): + # model.full_clean(validate_unique=False) + dict_update.setdefault(pk, model) + else: + dict_up_to_date.setdefault(pk, model) + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, idx, pk) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if dict_create: + ModelType.objects.bulk_create(dict_create.values(), batch_size=batch_size) + logger.info('%s(s) créé(s) : %s', ModelType.__name__, len(dict_create)) + + if fields_to_update: + if dict_update: + ModelType.objects.bulk_update(dict_update.values(), batch_size=batch_size, fields=fields_to_update) + logger.info('%s(s) mis(s) à jour : %s', ModelType.__name__, len(dict_update)) + + if dict_up_to_date: + logger.info('%s(s) déjà à jour : %s', ModelType.__name__, len(dict_up_to_date)) + + deleted = ModelType.objects.filter(~Q(pk__in={*dict_create.keys(), *dict_update.keys(), *dict_up_to_date.keys()})).delete()[0] + if deleted: + logger.info('%s(s) supprimé(s) : %s', ModelType.__name__, deleted) + + +# fonction d'insertion dans la table Filiere +def insert_Filiere(df): + """ + Insertion des données de la table Filiere dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + ModelType = Filiere + + liste_create = [] + liste_update = [] + update_header = ['domaine_id'] + error_count = 0 + fil_dict = {o.f_code: o for o in ModelType.objects.all()} + dom_ids = set(Domaine.objects.values_list('pk', flat=True)) + + for i in range(df.shape[0]): + f_code = df.at[i, 'f_code'] + try: + filiere = ModelType.objects.filter(f_code=f_code) + filiere_create = ModelType(f_code=f_code, domaine_id=df.at[i, 'domaine_id']) + dom_id = df.at[i,'domaine_id'] + if dom_id not in dom_ids: + logger.warning("%s[pk=%s] filière ignorée car le domaine d'id '%s' est absent de la base", Filiere.__name__, f_code, dom_id) + continue + if filiere.exists(): + fil_pop = fil_dict.pop(f_code) + liste_update.append(filiere_create) + else: + liste_create.append(filiere_create) + + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', Filiere.__name__, i, f_code) + + if error_count: + logger.warning('%s(s) en erreur : %s',ModelType.__name__, error_count) + + if liste_create: + ModelType.objects.bulk_create(liste_create) + logger.info('%s(s) créé(s) : %s', ModelType.__name__, len(liste_create)) + + if liste_update and update_header: + ModelType.objects.bulk_update(liste_update, fields=update_header) + logger.info('%s(s) mis à jour: %s', ModelType.__name__, len(liste_update)) + + deleted = ModelType.objects.filter(pk__in=fil_dict.keys()).delete()[0] + if deleted: + logger.info('%s(s) supprimé(s) : %s', ModelType.__name__, deleted) + return len(liste_create), len(liste_update), error_count, deleted + +# fonction d'insertion dans la table Sous viviers +def insert_SousVivier(df: pd.DataFrame) -> None: + """ + Insertion des données du Sous Vivier dans la base + + :param df: Dataframe contenant les données pretraitées à inserer + :type df: class:`pandas.DataFrame` + """ + ModelType = SousVivier + Cols = SousVivier.Cols + update_header = [ + Cols.LIBELLE, + Cols.DOMAINE, + Cols.FILIERE, + Cols.CATEGORIE + ] + list_create = [] + list_update = [] + error_count = 0 + sv_dict = {o.sv_id: o for o in ModelType.objects.all()} + + for i in range(df.shape[0]): + sv_id = df.at[i, 'sv_id'] + try: + sous_viviers = ModelType.objects.filter(sv_id=sv_id) + sous_vivier = ModelType(sv_id=sv_id, sv_libelle=df.at[i, 'sv_libelle'], + sv_dom=df.at[i, 'sv_dom'], sv_fil=df.at[i, 'sv_fil'], sv_cat=df.at[i, 'sv_cat']) + sous_vivier.full_clean(validate_unique=False) + if sous_viviers.exists(): + sous_vivier_pop = sv_dict.pop(sv_id) + list_update.append(sous_vivier) + else: + list_create.append(sous_vivier) + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, i, sv_id) + + sv_bvt_id = 'BVT' + sv_bvt = ModelType(sv_id=sv_bvt_id, sv_libelle=sv_bvt_id) + sv_bvt.full_clean(validate_unique=False) + + if ModelType.objects.filter(sv_id=sv_bvt_id).exists(): + sous_vivier_pop = sv_dict.pop(sv_bvt_id) + list_update.append(sv_bvt) + else: + list_create.append(sv_bvt) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if list_create: + ModelType.objects.bulk_create(list_create) + logger.info('%s(s) créé(s) : %s', ModelType.__name__, len(list_create)) + + if list_update and update_header: + ModelType.objects.bulk_update(list_update, fields=update_header) + logger.info('%s(s) mis à jour : %s', ModelType.__name__, len(list_update)) + + deleted = ModelType.objects.filter(pk__in=sv_dict.keys()).delete()[0] + if deleted: + logger.info('%s(s) supprimé(s) : %s', ModelType.__name__, deleted) + + return len(list_create), len(list_update), error_count, deleted + + +def insert_SousVivier_instances(): + """ + Insertion des données sous-viviers dans les tables des postes et des adminsitrés + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + """ + Cols = SousVivier.Cols + list_sv = SousVivier.objects.values_list( + Cols.PK, + Cols.DOMAINE, + Cols.FILIERE, + Cols.CATEGORIE + ) + + sv_bvt_id = 'BVT' + for (sv_id, dom, fil, cat) in list_sv: + if sv_id != sv_bvt_id: + a_qs = Administre.objects.filter(a_domaine_id=dom, a_filiere_id=fil, a_categorie=cat) + p_qs = Poste.objects.filter(p_domaine_id=dom, p_filiere_id=fil, p_categorie=cat) + a_qs.update(sous_vivier_id=sv_id) + list_postes = list(p_qs.values_list('p_id', flat=True)) + SousVivier.objects.get(sv_id=sv_id).poste.set(list_postes) + + if sv_bvt_id in np.array(list_sv)[:,0]: + a_bvt_qs = Administre.objects.filter(a_domaine_gestion=sv_bvt_id) + p_bvt_qs = Poste.objects.filter(p_nfs=sv_bvt_id) + a_bvt_qs.update(sous_vivier_id=sv_bvt_id) + list_postes_bvt = list(p_bvt_qs.values_list('p_id', flat=True)) + SousVivier.objects.get(sv_id=sv_bvt_id).poste.set(list_postes_bvt) + return 1 + + +# fonction d'insertion dans la table RefGest +def insert_RefGest(df: pd.DataFrame) -> None: + """ + Insertion des données de la table RefGest dans la base + + :param df: Dataframe contenant les données pretraitées à inserer + :type df: class:`pandas.DataFrame` + """ + ModelType = RefGest + UserType = get_user_model() + + list_create_ref_gest = [] + list_update_ref_gest = [] + list_tokeep_ref_gest = [] + ref_gest_update_header = ['ref_gest_username', 'ref_gest_email', 'ref_gest_first_name', 'ref_gest_last_name', 'ref_gest_grade', 'ref_gest_niv_org', 'ref_gest_org_id'] + + dict_create_user = {} + dict_update_user = {} + users_to_keep = set() + user_update_header = ['username', 'is_superuser', 'is_staff', 'first_name', 'last_name', 'email', 'grade'] + + users_in_db_by_id = {} + users_in_db_by_username = {} + for o in UserType.objects.all(): + users_in_db_by_id[o.id] = o + users_in_db_by_username[o.username] = o + + def add_user_to_dict(user_id: int, username: str) -> Tuple[bool, Optional[UserType], bool]: + """ + Vérifie l'existence d'un utilisateur en base (par ID ou par username) et ajoute si nécessaire un utilisateur + dans le bon dictionnaire (création ou MAJ). Dans ce cas l'utilisateur est renvoyé par la fonction et il est possible de le modifier. + + Cas possibles : + - l'utilisateur existe mais n'a pas le bon couple (ID, username) : renvoie (False, user, False) car c'est une donnée invalide + - l'utilisateur existe avec le bon couple (ID, username) et est déjà dans le dictionnaire de MAJ : renvoie (True, None, False) + - l'utilisateur existe avec le bon couple (ID, username) et n'est pas encore dans le dictionnaire de MAJ : renvoie (True, user, False) + - l'utilisateur n'existe pas et est déjà dans le dictionnaire de création : renvoie (True, None, True) + - l'utilisateur n'existe pas et n'est pas encore dans le dictionnaire de création : renvoie (True, user, True) + + :param user_id: ID de l'utilisateur + :type user_id: int + + :param username: nom d'utilisateur + :type username: str + + :return: tuple (valide, utilisateur, nouveau) + :rtype: Tuple[bool, Optional[UserType], bool] + """ + user_in_db = users_in_db_by_id.get(user_id) or users_in_db_by_username.get(username) + if user_in_db: + expected = (user_id, username) + actual = (user_in_db.id, user_in_db.username) + if actual != expected: + logger.warning('%s ignoré car le couple (id, username) ne correspond pas : %s au lieu de %s', UserType.__name__, actual, expected) + return (False, user_in_db, False) + + if username not in dict_update_user: + user = user_in_db + dict_update_user[username] = user + return (True, user, False) + else: + return (True, None, False) + + elif username not in dict_create_user: + user = UserType(id=user_id, username=username) + dict_create_user[username] = user + return (True, user, True) + return (True, None, True) + + + error_count = 0 + user_ignore_count = 0 + for i in range(df.shape[0]): + pk = int(df.at[i, 'ref_gest_sap']) + try: + ref_gest = ModelType.objects.filter(ref_gest_sap=pk) + administre = Administre.objects.filter(a_id_sap=pk) + username = df.at[i, 'ref_gest_username'].replace('\xa0', '') + user_id = pk + + users_to_keep.add(pk) + + if pk not in list_tokeep_ref_gest: + list_tokeep_ref_gest.append(pk) + + nom = df.at[i, 'ref_gest_last_name'] + prenom = df.at[i, 'ref_gest_first_name'] + + if prenom == None: + prenom = '' + logger.warning("%s[pk=%s] le gestionnaire n'a pas de prénom", ModelType.__name__, pk) + + if nom == None: + nom = '' + logger.warning("%s[pk=%s] le gestionnaire n'a pas de nom", ModelType.__name__, pk) + + ref_gest_create = ModelType(ref_gest_sap=pk, + ref_gest_username=username, + ref_gest_email=df.at[i, 'ref_gest_email'], + ref_gest_first_name=prenom, + ref_gest_last_name=nom, + ref_gest_grade=df.at[i, 'ref_gest_grade'], + ref_gest_niv_org=df.at[i, 'ref_gest_niv_org'], + ref_gest_org_id=df.at[i, 'ref_gest_org_id']) + + if ref_gest.exists(): + list_update_ref_gest.append(ref_gest_create) + + else: + list_create_ref_gest.append(ref_gest_create) + + valid, user, is_new = add_user_to_dict(user_id, username) + if not valid: + user_ignore_count = user_ignore_count + 1 + continue + if user: + user.first_name = prenom + user.last_name = nom + user.email = df.at[i, 'ref_gest_email'] + user.grade = df.at[i, 'ref_gest_grade'] + if is_new: + user.set_password(str(user_id)) + user.full_clean(validate_unique=False) + + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, i, pk) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if user_ignore_count: + logger.warning('%s(s) ignoré(s) : %s', UserType.__name__, user_ignore_count) + + admin_username = 'admin' + try: + valid, user, is_new = add_user_to_dict(0, admin_username) + if user: + users_to_keep.add(user.id) + if not is_new: + dict_update_user.pop(admin_username, None) + elif valid: + user.first_name = 'Admin' + user.last_name = 'Ogure' + user.is_superuser = True + user.is_staff = True + user.set_password('admin') + user.full_clean(validate_unique=False) + except Exception: + logger.exception("Une erreur est survenue lors du traitement de l'administrateur %s", admin_username) + + batch_size = 100 + if list_create_ref_gest: + ModelType.objects.bulk_create(list_create_ref_gest, batch_size=batch_size) + logger.info('%s(s) créé(s) : %s', ModelType.__name__, len(list_create_ref_gest)) + + if list_update_ref_gest and ref_gest_update_header: + ModelType.objects.bulk_update(list_update_ref_gest, batch_size=batch_size, fields=ref_gest_update_header) + logger.info('%s(s) mis à jour : %s', ModelType.__name__, len(list_update_ref_gest)) + + if dict_create_user: + UserType.objects.bulk_create(dict_create_user.values(), batch_size=batch_size) + logger.info('%s(s) créé(s) : %s', UserType.__name__, len(dict_create_user)) + + if dict_update_user and user_update_header: + UserType.objects.bulk_update(dict_update_user.values(), batch_size=batch_size, fields=user_update_header) + logger.info('%s(s) mis à jour : %s', UserType.__name__, len(dict_update_user)) + + deleted_ref_gest = ModelType.objects.filter(~Q(ref_gest_sap__in=list_tokeep_ref_gest)).delete()[0] + if deleted_ref_gest: + logger.info('%s(s) supprimé(s) : %s', ModelType.__name__, deleted_ref_gest) + + deleted = UserType.objects.filter(~Q(id__in=users_to_keep)).delete()[0] + if deleted: + logger.info('%s(s) supprimé(s) : %s', UserType.__name__, deleted) + + return len(list_create_ref_gest), len(list_update_ref_gest), error_count, deleted_ref_gest, len(dict_create_user), len(dict_update_user), user_ignore_count, deleted + +def insert_RefOrg(df: pd.DataFrame) -> None: + """ + Insertion des données de la table RefOrg dans la base + + :param df: Dataframe contenant les données pretraitées à inserer + :type df: class:`pandas.DataFrame` + """ + ModelType = RefOrg + list_create = [] + list_update = [] + ref_org_dict = {o.ref_org_code: o for o in ModelType.objects.all()} + update_header = ['ref_org_code_niv_org1', + 'ref_org_lib_niv_org1', + 'ref_org_code_niv_org2', + 'ref_org_lib_niv_org2', + 'ref_org_code_niv_org3', + 'ref_org_lib_niv_org3', + 'ref_org_code_niv_org4', + 'ref_org_lib_niv_org4', + 'ref_org_niv_org', + 'ref_org_ref_fe', + 'ref_org_ref_sv_fil', + 'ref_org_droit_lect', + 'ref_org_droit_ecr', + 'ref_org_expert_hme', + 'ref_org_bvt', + 'ref_org_itd'] + error_count = 0 + + for i, row in df.iterrows(): + ref_org_code = row['ref_org_code'] + try: + ref_org = ModelType.objects.filter(ref_org_code=ref_org_code) + ref_org_create = ModelType(ref_org_code=ref_org_code, + ref_org_code_niv_org1=row['ref_org_code_niv_org1'], + ref_org_lib_niv_org1=row['ref_org_lib_niv_org1'], + ref_org_code_niv_org2=row['ref_org_code_niv_org2'], + ref_org_lib_niv_org2=row['ref_org_lib_niv_org2'], + ref_org_code_niv_org3=row['ref_org_code_niv_org3'], + ref_org_lib_niv_org3=row['ref_org_lib_niv_org3'], + ref_org_code_niv_org4=row['ref_org_code_niv_org4'], + ref_org_lib_niv_org4=row['ref_org_lib_niv_org4'], + ref_org_niv_org=int(row['ref_org_niv_org']), + ref_org_ref_fe=row['ref_org_ref_fe'], + ref_org_ref_sv_fil=row['ref_org_ref_sv_fil'], + ref_org_droit_lect=row['ref_org_droit_lect'], + ref_org_droit_ecr=row['ref_org_droit_ecr'], + ref_org_expert_hme=row['ref_org_expert_hme'], + ref_org_bvt=row['ref_org_bvt'], + ref_org_itd=row['ref_org_itd']) + ref_org_create.full_clean(validate_unique=False) + if ref_org.exists(): + ref_org_pop = ref_org_dict.pop(ref_org_code) + list_update.append(ref_org_create) + else: + list_create.append(ref_org_create) + + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, i, ref_org_code) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if list_create: + ModelType.objects.bulk_create(list_create) + logger.info('%s(s) créé(s) : %s', ModelType.__name__, len(list_create)) + + if list_update and update_header: + ModelType.objects.bulk_update(list_update, fields=update_header) + logger.info('%s(s) mis à jour : %s', ModelType.__name__, len(list_update)) + + deleted = ModelType.objects.filter(pk__in=ref_org_dict.keys()).delete()[0] + if deleted: + logger.info('%s(s) supprimé(s) : %s', ModelType.__name__, deleted) + + return len(list_create), len(list_update), error_count, deleted + + +# fonction d'insertion dans la table RefSvFil +def insert_RefSvFil(df): + """ + Insertion des données de la table RefSvFil dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraitées à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + """ + ModelType = RefSvFil + list_create = [] + list_delete = [] + ignore_count = 0 + error_count = 0 + ref_sv_fil_dict = dict((f"{o.ref_sv_fil_code}_{o.ref_sv_fil_dom_gest}_{o.ref_sv_fil_dom}_{o.ref_sv_fil_fil}_{o.ref_sv_fil_cat}_{o.sous_vivier_id}", o) for o in ModelType.objects.all()) + sv_ids = list(SousVivier.objects.all().values_list('pk', flat=True)) + + for i, row in df.iterrows(): + try: + sv_id = row['sous_vivier_id'] + if sv_id not in sv_ids: + logger.warning("%s ignoré car le sous-vivier d'id '%s' n'est pas en base", ModelType.__name__, sv_id) + ignore_count = ignore_count + 1 + + else: + ref_sv_fil_id = f"{row['ref_sv_fil_code']}_{row['ref_sv_fil_dom_gest']}_{row['ref_sv_fil_dom']}_{row['ref_sv_fil_fil']}_{row['ref_sv_fil_cat']}_{sv_id}" + if ref_sv_fil_id in ref_sv_fil_dict: + ref_sv_fil_obj = ref_sv_fil_dict.pop(ref_sv_fil_id) + else: + dom = row['ref_sv_fil_dom'] + fil = row['ref_sv_fil_fil'] + cat = row['ref_sv_fil_cat'] + + ref_sv_fil = ModelType(ref_sv_fil_code=row['ref_sv_fil_code'], + ref_sv_fil_dom_gest=row['ref_sv_fil_dom_gest'], + ref_sv_fil_dom=dom, + ref_sv_fil_fil=fil, + ref_sv_fil_cat=cat, + sous_vivier_id=sv_id) + + list_create.append(ref_sv_fil) + + except Exception: + logger.exception('%s une erreur est survenue à la ligne : %s', ModelType.__name__, i) + error_count = error_count + 1 + + list_delete = [ref_sv_fil_dict[key] for key in ref_sv_fil_dict.keys()] + + size_batch = 100 + if ignore_count: + logger.warning("%s(s) ignoré(s) : %s", ModelType.__name__, ignore_count) + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + if list_delete: + logger.debug('%s(s) supprimé(s) : %s', ModelType.__name__, len(list_delete)) + ModelType.objects.filter(id__in=[o.id for o in list_delete]).delete() + if list_create: + logger.debug("%s(s) créé(s) : %s", ModelType.__name__, len(list_create)) + ModelType.objects.bulk_create(list_create, batch_size=size_batch) + + return len(list_create), len(list_delete), error_count, ignore_count + + +def insert_RefFeMere(df_referentiel_fe): + + Cols = FormationEmploi.Cols + col_pk = Cols.PK + col_pk_mere = Cols.REL_MERE + col_libelle_mere = Cols.LIBELLE + col_zone_def = Cols.ZONE_DEFENSE + + def convertir_fe(df: pd.DataFrame) -> pd.DataFrame: + """ + Fonction de conversion du DataFrame de FE. + + :param df: DataFrame du référentiel FE + :type df: class:`pandas.DataFrame` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + + col_pk_avant = 'FE CREDO' # B + col_pk_mere_avant = 'FE mère CREDO' # D + col_libelle_mere_avant = 'FE mère LA' # E + col_zone_def_avant = 'Zone de Défense' # O + + return ( + df[[col_pk_avant, col_pk_mere_avant, col_libelle_mere_avant, col_zone_def_avant]] + .drop_duplicates(subset=col_pk_avant, keep='first') + .rename(columns={ + col_pk_avant: col_pk, + col_pk_mere_avant: col_pk_mere, + col_libelle_mere_avant: col_libelle_mere, + col_zone_def_avant: col_zone_def + }) + .astype({col_pk: 'str', col_pk_mere: 'str'}) + ) + + def mettre_a_jour_fe(df: pd.DataFrame) -> None: + """ + Met à jour les FE base à partir du DataFrame de FE. + + :param df: DataFrame du référentiel FE + :type df: class:`pandas.DataFrame` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + + + TypeModele = FormationEmploi + Cols = TypeModele.Cols + champs_maj = (Cols.REL_MERE, Cols.ZONE_DEFENSE) + modeles_en_base = {m.pk: m for m in TypeModele.objects.select_related(Cols.REL_MERE).only('pk', *champs_maj)} + + taille_batch = 100 + dict_update = {} + dict_mere_create = {} + + for rec in df.to_dict('records'): + pk = rec.get(col_pk) + id_mere = rec.get(col_pk_mere) + zone_defense = rec.get(col_zone_def) + + en_base = modeles_en_base.get(pk) + + # TODO pas de création pour l'instant (ni de suppression) quand en_base est falsy + if en_base: + mere = None + if id_mere is not None: + mere = modeles_en_base.get(id_mere) or dict_mere_create.get(id_mere) + if not mere: + try: + # les FE mères manquantes seront créées + mere = TypeModele(pk=id_mere, fe_libelle=rec.get(col_libelle_mere)) + except Exception as e: + raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" (mère) a échoué') from e + dict_mere_create.setdefault(id_mere, mere) + + if mere != getattr(en_base, Cols.REL_MERE, None) or zone_defense != getattr(en_base, Cols.ZONE_DEFENSE, None): + try: + modele = TypeModele(pk=pk, mere=mere, zone_defense=zone_defense) + except Exception as e: + raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" a échoué') from e + dict_update.setdefault(pk, modele) + + if dict_mere_create: + TypeModele.objects.bulk_create(dict_mere_create.values(), batch_size=taille_batch) + logger.debug(f"{TypeModele.__name__} créé(s): %s", len(dict_mere_create)) + + if dict_update and champs_maj: + TypeModele.objects.bulk_update(dict_update.values(), batch_size=taille_batch, fields=champs_maj) + logger.debug(f"{TypeModele.__name__} MAJ: %s", len(dict_update)) + + df_referentiel_fe = convertir_fe(df_referentiel_fe) + mettre_a_jour_fe(df_referentiel_fe) + return 1 + # return len(dict_mere_create), len(dict_update) + + +def update_m2m_links_gestionnaire(type: str) -> None: + """ + Met à jour tous les liens many-to-many entre les gestionnaires (= utilisateurs) et des FE ou des sous-viviers. + Nécessite des données pour certains modèles : + - commun : CustomUser, RefGest, RefOrg + - FE : FormationEmploi + - sous-vivier : SousVivier, RefSvFil + + :param type: type + :type type: Literal['FE', 'SV'] + """ + UserType = get_user_model() + if type == 'FE': + ModelType = FormationEmploi + LinkModelType = getattr(UserType, UserType.Cols.M2M_FORMATION_EMPLOIS).through + + def fn_linked_ids(lvl4_codes): + return (ModelType.objects + .filter(Q(fe_code_niv_org4__in=lvl4_codes) | Q(fe_code_niv_org4_mdr__in=lvl4_codes)) + .values_list('pk', flat=True)) + elif type == 'SV': + ModelType = SousVivier + LinkModelType = getattr(UserType, UserType.Cols.M2M_SOUS_VIVIERS).through + + user_bvts = list(RefGest.objects.filter(**{f'{RefGest.Cols.REL_ORG}_id__ref_org_bvt': True}).values_list('pk', flat=True)) + + def fn_linked_ids(lvl4_codes): + return (RefSvFil.objects + .filter(ref_sv_fil_code__in=lvl4_codes) + .filter(**{f'{RefSvFil.Cols.REL_SOUS_VIVIER}_id__isnull': False}) + .values_list(f'{RefSvFil.Cols.REL_SOUS_VIVIER}_id', flat=True) + .distinct()) + else: + raise Exception(f'type invalide : {type}') + + org_codes_by_id = {o[0]: o[1] for o in RefGest.objects.values_list(RefGest.Cols.PK, RefGest.Cols.REL_ORG + '_id')} + user_ids = list(UserType.objects.filter(pk__in=org_codes_by_id.keys()).values_list('pk', flat=True)) + + batch_size = 200 + to_create = [] + error_count = 0 + sv_bvt_id = 'BVT' if 'BVT' in SousVivier.objects.all().values_list(SousVivier.Cols.PK, flat=True) else None + + lvl4_codes_by_code = {} # un cache + for user_id, org_code in org_codes_by_id.items(): + try: + if user_id in user_ids: + lvl4_codes = lvl4_codes_by_code.get(org_code) or lvl4_codes_by_code.setdefault(org_code, get_lvl4_org_codes_by_any_code(org_code)) + for linked_id in fn_linked_ids(lvl4_codes): + to_create.append(LinkModelType(**{f'{UserType.__name__.lower()}_id': user_id, f'{ModelType.__name__.lower()}_id': linked_id})) + if type == 'SV': + if user_id in user_bvts and sv_bvt_id: + to_create.append(LinkModelType(**{f'{UserType.__name__.lower()}_id': user_id, f'{ModelType.__name__.lower()}_id': sv_bvt_id})) + except Exception: + error_count = error_count + 1 + logger.exception("une erreur est survenue lors de l'ajout de lien(s) %s[pk=%s]/%s", UserType.__name__, user_id, ModelType.__name__) + + if error_count: + logger.warning("%s(s) en erreur : %s", LinkModelType.__name__, error_count) + + deleted = LinkModelType.objects.all().delete()[0] + if deleted: + logger.info('lien(s) %s/%s supprimé(s) : %s', UserType.__name__, ModelType.__name__, deleted) + + if to_create: + LinkModelType.objects.bulk_create(to_create, batch_size=batch_size) + logger.info('lien(s) %s/%s créé(s) : %s', UserType.__name__, ModelType.__name__, len(to_create)) + + return 1 + +# fonction d'insertion dans la table diplome +def insert_Diplome(df): + """ + Insertion des données de la table Diplome dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + liste_update = [] + liste_delete = [] + update_header = ['diplome_note'] + diplome_dict = dict ((f"{o.administre_id}_{o.diplome_libelle}_{o.diplome_date}", o) for o in Diplome.objects.all()) + + for i in range(df.shape[0]): + dip_id = f"{df.at[i,'Matricule SAP']}_{df.at[i,'Diplôme militaire L']}_{df.at[i,'Diplôme militaire D']}" + if dip_id in diplome_dict: + diplome = diplome_dict.pop(dip_id) + diplome.diplome_note = df.at[i, 'Diplôme militaire note'] + liste_update.append(diplome) + else: + diplome = Diplome(administre_id = df.at[i,'Matricule SAP'], diplome_libelle=df.at[i, 'Diplôme militaire L'],diplome_date=df.at[i, 'Diplôme militaire D'], + diplome_note=df.at[i, 'Diplôme militaire note']) + liste_create.append(diplome) + + liste_delete = [diplome_dict[key] for key in diplome_dict.keys()] + + size_batch = 100 + if liste_create: + Diplome.objects.bulk_create(liste_create, batch_size=size_batch) + if liste_update: + Diplome.objects.bulk_update(liste_update, fields=update_header, batch_size=size_batch) + if liste_delete: + Diplome.objects.filter(id__in=[o.id for o in liste_delete]).delete() + return len(liste_create), len(liste_update), len(liste_delete) + + +# fonction d'insertion dans la table Affecation +def insert_Affectation(df): + """ + Insertion des données de la table Affecation dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + liste_delete = [] + affectation_dict = dict ((f"{o.administre_id}_{o.affect_libelle}_{o.affect_date}", o) for o in Affectation.objects.all()) + + for i in range(df.shape[0]): + affect_id = f"{df.at[i,'Matricule SAP']}_{df.at[i,'Affectation L']}_{df.at[i,'Affectation D']}" + if affect_id in affectation_dict: + affect_object = affectation_dict.pop(affect_id) + else : + affect = Affectation(administre_id = df.at[i, 'Matricule SAP'], affect_libelle=df.at[i, 'Affectation L'], affect_date=df.at[i, 'Affectation D']) + liste_create.append(affect) + + liste_delete = [affectation_dict[key] for key in affectation_dict.keys()] + + size_batch = 100 + if liste_create: + Affectation.objects.bulk_create(liste_create, batch_size=size_batch) + if liste_delete: + Affectation.objects.filter(id__in=[o.id for o in liste_delete]).delete() + return len(liste_create), len(liste_delete) + + +# fonction d'insertion dans la table Affecation +def insert_Fud(df): + """ + Insertion des données de la table FUD dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + liste_delete = [] + fud_dict = dict ((f"{o.administre_id}_{o.fud_libelle}_{o.fud_date_debut}_{o.fud_date_fin}", o) for o in FUD.objects.all()) + + for i in range(df.shape[0]): + fud_id = f"{df.at[i,'Matricule SAP']}_{df.at[i,'FUD L']}_{df.at[i,'FUD DD']}_{df.at[i,'FUD DF']}" + if fud_id in fud_dict: + fud_object = fud_dict.pop(fud_id) + else: + fud = FUD(administre_id = df.at[i, 'Matricule SAP'], fud_libelle=df.at[i, 'FUD L'], fud_date_debut=df.at[i, 'FUD DD'], fud_date_fin=df.at[i, 'FUD DF']) + liste_create.append(fud) + + liste_delete = [fud_dict[key] for key in fud_dict.keys()] + + size_batch = 100 + if liste_create: + FUD.objects.bulk_create(liste_create, batch_size=size_batch) + if liste_delete: + FUD.objects.filter(id__in=[o.id for o in liste_delete]).delete() + return len(liste_create), len(liste_delete) + + +# fonction d'insertion dans la table Garnison +def insert_Garnison(df): + """ + Insertion des données de la table Garnison dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + """ + liste_create = [] + liste_update = [] + error_count = 0 + update_header = ['gar_lieu', 'gar_code_postal'] + + for i in range(df.shape[0]): + try: + garnisons = Garnison.objects.filter(gar_id=df.at[i, 'gar_id']) + garnison = Garnison(gar_id=df.at[i, 'gar_id'], gar_lieu=df.at[i, 'gar_lieu'], + gar_code_postal=df.at[i, 'gar_code_postal']) + if garnisons.exists(): + liste_update.append(garnison) + else: + liste_create.append(garnison) + except: + error_count +=1 + raise Exception("Une erreur est survenue à la ligne : ", i) + Garnison.objects.bulk_create(liste_create) + Garnison.objects.bulk_update(liste_update, fields=update_header) + return len(liste_create), len(liste_update), error_count + + +# fonction d'insertion dans la table Formations emplois +def insert_FormationEmploi(df: pd.DataFrame) -> None: + """ + Insertion des données de la table Formation Emploi dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + """ + ModelType = FormationEmploi + update_header = ['fe_garnison_lieu', 'fe_code_postal', 'fe_libelle', 'fe_mere_credo', 'fe_mere_la', 'fe_fot', 'fe_abo_fe', 'fe_pilier_niv1', + 'fe_code_niv_org4', 'fe_niv_org4', 'fe_code_niv_org4_mdr', 'fe_niv_org4_mdr', 'fe_nb_poste_reo_mdr'] + + nb_ligne_charger = 0 + nb_ligne_lu = 0 + nb_ligne_erroner = 0 + nb_ligne_ignorer = 0 + + liste_create = [] + liste_update = [] + error_count = 0 + + model_ids_in_db = set(ModelType.objects.values_list('pk', flat=True)) + + for i, row in df.iterrows(): + pk = row['fe_credo'] + nb_ligne_lu += 1 + try: + model = ModelType(**{ + 'fe_code': pk, + 'fe_garnison_lieu': row['fe_garnison_lieu'], + 'fe_code_postal': row['fe_code_postal'], + 'fe_libelle': row['fe_libelle'], + 'fe_mere_credo': row['fe_mere_credo'], + 'fe_mere_la': row['fe_mere_la'], + 'fe_fot': row['fe_fot'], + 'fe_abo_fe': row['fe_abo_fe'], + 'fe_pilier_niv1': row['fe_pilier_niv1'], + 'fe_code_niv_org4': row['fe_code_niv_org4'], + 'fe_niv_org4': row['fe_niv_org4'], + 'fe_code_niv_org4_mdr': row['fe_code_niv_org4_mdr'], + 'fe_niv_org4_mdr': row['fe_niv_org4_mdr'] + }) + nb_ligne_charger += 1 + + if pk in model_ids_in_db: + liste_update.append(model) + else: + liste_create.append(model) + for attr in [ + 'fe_nb_poste_reo_mdr', + 'fe_nb_poste_reevalue_mdr', + 'fe_nb_poste_vacant_mdr', + 'fe_nb_poste_occupe_mdr', + 'fe_nb_poste_reo_off', + 'fe_nb_poste_reevalue_off', + 'fe_nb_poste_vacant_off', + 'fe_nb_poste_occupe_off', + 'fe_nb_poste_reo_soff', + 'fe_nb_poste_reevalue_soff', + 'fe_nb_poste_vacant_soff', + 'fe_nb_poste_occupe_soff', + ]: + setattr(model, attr, row[attr]) + except Exception: + error_count +=1 + nb_ligne_erroner += 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, i, pk) + + if nb_ligne_ignorer: + logger.warning('%s(s) ignorée(s) : %s', ModelType.__name__, nb_ligne_ignorer) + + if nb_ligne_erroner: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, nb_ligne_erroner) + + batch_size = 100 + if liste_create: + ModelType.objects.bulk_create(liste_create, batch_size=batch_size) + logger.info('%s(s) créée(s) : %s', ModelType.__name__, len(liste_create)) + + if liste_update and update_header: + ModelType.objects.bulk_update(liste_update, batch_size=batch_size, fields=update_header) + logger.info('%s(s) mise(s) à jour : %s', ModelType.__name__, len(liste_update)) + + return len(liste_create), len(liste_update), error_count + +# fonction d'insertion dans la table Fonction +def insert_Fonction(df): + """ + Insertion des données de la table Fonction dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + for i in range(df.shape[0]): + fonction = Fonction.objects.filter(fon_id=int(df.at[i, 'fon_id'])) + fonction_create = Fonction(fon_id=int(df.at[i, 'fon_id']), fon_libelle=df.at[i, 'fon_libelle']) + if fonction.exists(): + fonction.update(fon_libelle=df.at[i, 'fon_libelle']) + else: + liste_create.append(fonction_create) + + Fonction.objects.bulk_create(liste_create) + return len(liste_create) + + +# fonction d'insertion dans la table marques +def insert_Marque(df): + """ + Insertion des données de la table Marque dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + for i in range(df.shape[0]): + marque = Marque.objects.filter(mar_id=df.at[i, 'mar_id']) + marque_create = Marque(mar_id=df.at[i, 'mar_id'], groupe_marques_id=df.at[i, 'groupe_marques_id'], + mar_code=df.at[i, 'mar_code'], mar_libelle=df.at[i, 'mar_libelle'], + mar_ordre=df.at[i, 'mar_ordre']) + if marque.exists(): + # Update + marque.update(groupe_marques_id=df.at[i, 'groupe_marques_id'], + mar_code=df.at[i, 'mar_code'], mar_libelle=df.at[i, 'mar_libelle'], + mar_ordre=df.at[i, 'mar_ordre']) + else: + liste_create.append(marque_create) + Marque.objects.bulk_create(liste_create) + return len(liste_create) + + +# fonction d'insertion dans la table Groupes marques +def insert_MarquesGroupe(df): + """ + Insertion des données de la table MarqueGroupe dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + for i in range(df.shape[0]): + gm = MarquesGroupe.objects.filter(gm_id=df.at[i, 'gm_id']) + gm_create = MarquesGroupe(gm_id=df.at[i, 'gm_id'], gm_type=df.at[i, 'gm_type'], gm_code=df.at[i, 'gm_code'], + gm_libelle=df.at[i, 'gm_libelle'], gm_ordre=df.at[i, 'gm_ordre'], + gm_selection_multiple=df.at[i, 'gm_selection_multiple']) + if gm.exists(): + # Update + gm.update(gm_type=df.at[i, 'gm_type'], gm_code=df.at[i, 'gm_code'], gm_libelle=df.at[i, 'gm_libelle'], + gm_ordre=df.at[i, 'gm_ordre'], gm_selection_multiple=df.at[i, 'gm_selection_multiple']) + else: + liste_create.append(gm_create) + + MarquesGroupe.objects.bulk_create(liste_create) + return 1 + +def insert_ZoneGeographique(df: pd.DataFrame) -> None: + """ + Insertion des données de la table ZoneGeographique dans la base + + :param df: Dataframe contenant les données pretraités à inserer + :type df: class:`pandas.DataFrame` + """ + ModelType = ZoneGeographique + liste_create = [] + error_count = 0 + zones_dict = dict((f"{o.zone_id}", o) for o in ModelType.objects.all()) + + for i in range(df.shape[0]): + zone_id = f"{df.at[i,'zone_id']}" + try: + if zone_id in zones_dict: + zone = zones_dict.pop(zone_id) + else: + zone_create = ModelType(zone_id=df.at[i, 'zone_id'], zone_libelle=df.at[i, 'zone_libelle']) + zone_create.full_clean(validate_unique=False) + liste_create.append(zone_create) + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, i, zone_id) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + size_batch = 100 + if liste_create: + ModelType.objects.bulk_create(liste_create, batch_size=size_batch) + logger.info('%s(s) créée(s) : %s', ModelType.__name__, len(liste_create)) + + deleted = ModelType.objects.filter(zone_id__in=zones_dict.keys()).delete()[0] + if deleted: + logger.info('%s(s) supprimée(s) : %s', ModelType.__name__, deleted) + + return len(liste_create),deleted, error_count + + +def insert_Commentaires(df: pd.DataFrame) -> None: + """ + Insertion des données de commentaires dans la table des administrés + + :param df: Dataframe contenant les données pretraités à inserer + :type df: class:`pandas.DataFrame` + """ + try: + data = list(df.to_dict('index').values()) + fields = ['a_notes_gestionnaire'] + objs = [Administre(**data[i]) for i in range(len(data))] + Administre.objects.bulk_update(objs, fields=fields) + + except Exception as e: + logger.exception("Une erreur est survenue lors de l'insertion %s", e) + + return 1 + + +@execution_time(level=logging.INFO, logger_factory=data_perf_logger_factory) +def insert_Poste(df: pd.DataFrame, annee): + """ + Insertion des données de la table Poste dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + """ + logger.info('start_insert_poste') + ModelType = Poste + ModelType_2 = Postes_Pams + Cols = ModelType.Cols + Cols_2 = ModelType_2.Cols + col_pk = Cols.PK + + fields_to_update = [ + 'formation_emploi_id', + 'p_domaine', + 'p_filiere', + 'p_nf', + 'fonction_id', + 'p_fonction', + 'p_categorie', + 'p_nfs', + 'p_dep', + 'p_eip', + 'p_annee', + + ] + + fields_to_update_2 = [ + 'p_pam_id', + 'poste_id', + 'p_avis_pam', + 'p_avis_fe_pam', + 'p_direct_commissionne_pam', + 'p_notes_gestionnaire_pam', + 'p_priorisation_pcp_pam,' + 'info_reo', + ] + + + fields_db = ['p_id'] + fields_to_update + + # Integer ou Float Colonnes à mettre à jour + fields_num = [ + ] + + # 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 } + + + logger.info("Lecture de l'ensemble des postes en base") + # Lire tous les postes de la base de données + poste_in_db_df = pd.DataFrame.from_records(Poste.objects.all().values_list(*tuple(fields_db)), columns=fields_db) + + + if not poste_in_db_df.empty: + # Il va y avoir de modification de type donc c'est mieux de ne pas toucher df + df_comparing = df.copy() + + # Modification de type de quelque champs + + logger.debug('Conversion des types pour la fusion') + poste_in_db_df = (poste_in_db_df.fillna(APP_NAN) + .replace({APP_NAN: None})) + + poste_in_db_df = poste_in_db_df.astype(dict_conv_str) + poste_in_db_df = poste_in_db_df.astype(dict_conv_float) + + + df_comparing = df_comparing.astype(dict_conv_str) + df_comparing = df_comparing.astype(dict_conv_float) + df_comparing['p_nf'] = df_comparing['p_nf'].str.upper() + + compare = pd.DataFrame([df_comparing[fields_db].dtypes,poste_in_db_df[fields_db].dtypes]).T + logger.debug('Comparaison des types pour la fusion') + logger.debug('------------------------------------') + logger.debug(compare[compare[0]!=compare[1]].dropna()) + logger.debug('------------------------------------') + + # Comparaison pour savoir ce qui doit etre creer, mis a jour ou supprimer + + comparing_poste_id = pd.merge(df_comparing, poste_in_db_df, how='outer', on='p_id', suffixes=(None, "_x"), indicator=True) + same_rows = comparing_poste_id[comparing_poste_id['_merge']=='both'].drop('_merge', axis=1) + new_rows = comparing_poste_id[comparing_poste_id['_merge']=='left_only'].drop('_merge', axis=1) + delete_rows = comparing_poste_id[comparing_poste_id['_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_poste_both = pd.merge(same_rows, poste_in_db_df, how='left', on=fields_db, + suffixes=(None, "_x"), indicator=True) + + not_updated_rows = comparing_poste_both[comparing_poste_both['_merge']=='both'].drop('_merge', axis=1) + updated_rows = comparing_poste_both[comparing_poste_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['p_id'].isin(list(updated_rows['p_id']))] + update['db_create_status'] = 0 + create = df.loc[df['p_id'].isin(list(new_rows['p_id']))] + 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 + fe_ids = set(FormationEmploi.objects.values_list('pk', flat=True)) + dom_ids = set(Domaine.objects.values_list('pk', flat=True)) + fil_ids = set(Filiere.objects.values_list('pk', flat=True)) + fon_ids = set(Fonction.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_create_2 = {} + dict_update = {} + set_dom = set() + set_fil = set() + set_fon = set() + annee_a = PAM.objects.get(pam_statut='PAM en cours').pam_id + error_count = 0 + ignore_count = 0 + + + + for df_batch in batch_iterator(df, 1000): + # itération par lot pour limiter le nombre d'objets en mémoire + for idx, row in df_batch.iterrows(): + try: + annee_pam = annee + + pk = str(row[col_pk]) + pk_2 = pk + str(annee_pam) + + if annee_pam == annee_a: + annee_pam_suivant = int(annee)+1 + pk_3 = pk + str(annee_pam_suivant) + + except Exception: + logger.warning("Attention le fichier de REO est obsolète PAM A ou A+1") + try: + fe_id = row['formation_emploi_id'] + if fe_id is not None and fe_id not in fe_ids: + logger.warning("%s[pk=%s] ignoré car formation-emploi absente du référentiel : %s", ModelType.__name__, pk, fe_id) + ignore_count = ignore_count + 1 + continue + + dom_id = row['p_domaine'] + if dom_id is not None and dom_id not in dom_ids: + logger.warning("%s[pk=%s] domaine ignoré car absent du référentiel : %s", ModelType.__name__, pk, dom_id) + set_dom.add(dom_id) + dom_id = None + + fil_id = row['p_filiere'] + if fil_id is not None and fil_id not in fil_ids: + logger.warning("%s[pk=%s] filière ignorée car absente du référentiel : %s", ModelType.__name__, pk, fil_id) + set_fil.add(fil_id) + fil_id = None + + fon_id = row['fonction_id'] + if fon_id is not None and fon_id not in fon_ids: + logger.warning("%s[pk=%s] fonction ignorée car absente du référentiel : %s", ModelType.__name__, pk, fon_id) + set_fon.add(fon_id) + fon_id = None + + model_2 = ModelType_2(**{ + 'id' :pk_2, + 'p_pam_id' :annee_pam, + 'poste_id' :pk, + 'p_avis_pam' :StatutPam.NON_ETUDIE, + 'p_avis_fe_pam' :StatutPam.NON_ETUDIE, + 'p_direct_commissionne_pam' :None, + 'p_notes_gestionnaire_pam' :None, + 'p_priorisation_pcp_pam' :None, + 'info_reo' :"SORG" if row['p_annee']=="SORG" else f'REO {annee_pam}', + }) + + if annee_pam == annee_a: + model_3 = ModelType_2(**{ + 'id' :pk_3, + 'p_pam_id' :annee_pam_suivant, + 'poste_id' :pk, + 'p_avis_pam' :StatutPam.NON_ETUDIE, + 'p_avis_fe_pam' :StatutPam.NON_ETUDIE, + 'p_direct_commissionne_pam' :None, + 'p_notes_gestionnaire_pam' :None, + 'p_priorisation_pcp_pam' :None, + 'info_reo' :"SORG" if row['p_annee']=="SORG" else f'REO {annee_pam}', + }) + + model = ModelType(**{ + 'pk': pk, + f'{Cols.REL_FORMATION_EMPLOI}_id': fe_id, + f'{Cols.REL_DOMAINE}_id': dom_id, + f'{Cols.REL_FILIERE}_id': fil_id, + Cols.NIVEAU_FONCTIONNEL: row['p_nf'].upper(), + f'{Cols.REL_FONCTION}_id': fon_id, + 'p_fonction': row['p_fonction'], + 'p_dep': row['p_dep'], + Cols.CATEGORIE: row['p_categorie'], + 'p_nfs': row['p_nfs'], + 'p_eip': row['p_eip'], + 'p_annee': row['p_annee'], + }) + + if row['db_create_status']: + model_2.full_clean(exclude=fields_not_validated_2, validate_unique=False) + dict_create_2.setdefault(pk_2, model_2) + + if annee_pam == annee_a: + model_3.full_clean(exclude=fields_not_validated_2, validate_unique=False) + dict_create_2.setdefault(pk_3, model_3) + + 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 annee_pam == annee_a: + if not ModelType_2.objects.filter(Q(id=pk_3)).exists(): + model_3.full_clean(exclude=fields_not_validated_2, 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: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, idx, pk) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if ignore_count: + logger.warning('%s(s) ignoré(s) : %s', ModelType.__name__, ignore_count) + + 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 set_fon: + logger.warning('%s %s(s) ignorée(s) : %s', len(set_fon), Filiere.__name__, set_fon) + + if not not_updated_rows.empty: + logger.info('%s(s) déjà à jour : %s', ModelType.__name__, len(not_updated_rows)) + + batch_size = 100 + if dict_create: + logger.info('Création de %s %s(s)...', len(dict_create), ModelType.__name__) + 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(s) créé(s) : %s', ModelType.__name__, len(dict_create)) + else: + logger.info('Aucun %s(s) à créer', ModelType.__name__) + + if dict_create_2: + logger.info('Création de %s %s(s)...', len(dict_create_2), ModelType_2.__name__) + 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(s) créé(s) : %s', ModelType_2.__name__, len(dict_create_2)) + else: + logger.info('Aucun %s(s) à créer', ModelType_2.__name__) + + if dict_update and fields_to_update: + logger.info('Mise à jour de %s %s(s)...', len(dict_update), ModelType.__name__) + 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(s) mis à jour : %s', ModelType.__name__, len(dict_update)) + else: + logger.info('Aucun %s(s) à mettre à jour', ModelType.__name__) + + # pas de suppression ici pour l'instant + return len(dict_create), len(dict_create_2), len(dict_update), error_count, ignore_count + + +def insert_delta(df: pd.DataFrame) -> None: + 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 + poste_init = dict((f"{o.poste_id}", o) for o in Postes_Pams.objects.all().filter(p_pam_id=annee_pam)) + + for i,row in df.iterrows(): + p_suivant = str(row['p_id']) + info_annee = str(row['p_annee']) + + if p_suivant in poste_init: + if Postes_Pams.objects.filter(Q(poste_id=p_suivant) & Q(p_pam_id=annee_pam_suivant)& Q(info_reo="SORG")): + Postes_Pams.objects.filter(Q(poste_id=p_suivant) & Q(p_pam_id=annee_pam_suivant)).update(info_reo=f'CREE {annee_pam_suivant}') + poste_init.pop(p_suivant) + else: + Postes_Pams.objects.filter(Q(poste_id=p_suivant) & Q(p_pam_id=annee_pam_suivant)).update(info_reo=f'REO {annee_pam_suivant}') + poste_init.pop(p_suivant) + + else: + if info_annee !='SORG': + Postes_Pams.objects.filter(Q(poste_id=p_suivant) & Q(p_pam_id=annee_pam_suivant)).update(info_reo=f'CREE {annee_pam_suivant}') + + for i in poste_init: + Postes_Pams.objects.filter(Q(poste_id=i) & Q(p_pam_id=annee_pam_suivant)).update(info_reo='SUP REO') + + + + return 1 + + + +# Insertion de Id sap dans la table poste +def update_poste_ocv(df: pd.DataFrame) -> None: + """ + Met à jour les postes : renseigne l'ID SAP de l'administré à partir des données d'OCV. + + :param df: Dataframe contenant les données pretraités à inserer + :type df: class:`pandas.DataFrame` + """ + start_time_insert = time.time() + update_header = [Poste.Cols.REL_ADMINISTRE] + + adm_ids = list(Administre.objects.values_list('pk', flat=True)) + pos_ids = {t[0]: t[1] for t in Poste.objects.values_list('pk', f'{Poste.Cols.REL_ADMINISTRE}_id')} + + dict_update = {} + up_to_date = set() + ignore_count = 0 + error_count = 0 + for i, row in df.iterrows(): + pk = row['p_id'] + adm_id = int(row['Identifiant SAP']) + try: + if pk not in pos_ids: + logger.warning("%s[pk=%s] non mis à jour car ID de poste inconnu", Poste.__name__, pk) + ignore_count = ignore_count + 1 + continue + + if adm_id not in adm_ids: + logger.warning("%s[pk=%s] non mis à jour car ID SAP inconnu : %s", Poste.__name__, pk, adm_id) + ignore_count = ignore_count + 1 + continue + + if adm_id != pos_ids.get(pk): + dict_update.setdefault(pk, Poste(pk=pk, p_administre_id=adm_id)) + else: + up_to_date.add(pk) + + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', Poste.__name__, i, pk) + + if ignore_count: + logger.warning('%s(s) ignoré(s) : %s', Poste.__name__, ignore_count) + + if error_count: + logger.warning("%s(s) en erreur : %s", Poste.__name__, error_count) + + if update_header: + if dict_update: + Poste.objects.bulk_update(dict_update.values(), batch_size=100, fields=update_header) + logger.info('%s(s) mis à jour : %s', Poste.__name__, len(dict_update)) + + if up_to_date: + logger.info('%s(s) déjà à jour : %s', Poste.__name__, len(up_to_date)) + + logger.debug("insert time %d seconds ", time.time() - start_time_insert) + logger.info('Insert poste ocv end') + + return len(dict_update), error_count, ignore_count + + + +def insert_FMOB_fmob(df): + """ + Insertion des données du fichier FMOB pour la table FMOB dans la base + + :type df: dataframe + :param df: Dataframe contenant les données du fichier FMOB pretraités à inserer + + Returns + ------- + results : list + Liste de toutes les données insérées + + :return: - **results** (*list*): Liste de toutes les données insérées. + + """ + logger.info("----- Début de l'insertion du fichier FMOB pour la table FMOB ----- ") + + liste_create = [] + liste_update = [] + # To do : ADD columns + update_header = ['administre_id', + 'fmob_millesime', + "fmob_annulation_fmob", + "fmob_reception_drhat_fmob", + 'fmob_mobilite_bassin_externe', + 'fmob_mobilite_bassin_interne', + 'fmob_mobilite_centre_interet_adt', + 'fmob_mobilite_dans_specialite', + 'fmob_mobilite_hors_metropole', + 'fmob_mobilite_recrutement_particulier_administre', + "fmob_sans_suite_militaire_fmob", + 'fmob_avis_cdc_mutation_administre', + 'fmob_avis_cdc_mobilite_externe', + 'fmob_avis_cdc_mobilite_interne', + 'fmob_avis_cdc_mobilite_centre_interet', + 'fmob_avis_cdc_mobilite_specialite', + 'fmob_avis_cdc_mobilite_hors_metropole', + 'fmob_avis_cdc_mobilite_recrutement_particulier_admin', + "fmob_date_visa_militaire", + "fmob_depart_institution_soff", + "fmob_date_deb_fmob", + "fmob_date_fin_fmob", + "fmob_date_signature_admin_fmob", + "fmob_date_signature_chef_de_corps", + "fmob_motif_edition_la", + "fmob_reconnaissance_parcours_pro_administre", + "fmob_statut", + "fmob_remarques_eventuelles_administres", + "fmob_avis_commandant_formation", + ] + + + fmob_dict = dict((f"{o.administre_id}", o) for o in FMOB.objects.all()) + adm_ids = list(Administres_Pams.objects.values_list('pk', flat=True)) + + ignore_count = 0 + for i, row in df.iterrows(): + adm_id = str(int(row['administre_id'])) + str(int(row['fmob_millesime'])) + #adm_id = str(row['administre_id']) + str(int(row['fmob_millesime'])) + + if adm_id not in adm_ids: + logger.warning("FMOB d'id %s ignoré car l'administré est absent de la base", adm_id) + ignore_count = ignore_count + 1 + continue + + if str(adm_id) in fmob_dict: + # ADD columns + fmob = fmob_dict.pop(str(adm_id)) + fmob.fmob_millesime=row['fmob_millesime'] + fmob.fmob_annulation_fmob=row['fmob_annulation_fmob'] + fmob.fmob_reception_drhat_fmob=row['fmob_reception_drhat_fmob'] + fmob.fmob_mobilite_bassin_externe=row['fmob_mobilite_bassin_externe'] + fmob.fmob_mobilite_bassin_interne=row['fmob_mobilite_bassin_interne'] + fmob.fmob_mobilite_centre_interet_adt=row['fmob_mobilite_centre_interet_adt'] + fmob.fmob_mobilite_dans_specialite=row['fmob_mobilite_dans_specialite'] + fmob.fmob_mobilite_hors_metropole=row['fmob_mobilite_hors_metropole'] + fmob.fmob_mobilite_recrutement_particulier_administre=row['fmob_mobilite_recrutement_particulier_administre'] + fmob.fmob_sans_suite_militaire_fmob=row['fmob_sans_suite_fmob'] + fmob.fmob_date_visa_militaire=row['fmob_date_visa_militaire'] + fmob.fmob_depart_institution_soff=row['fmob_depart_institution_soff'] + fmob.fmob_avis_cdc_mutation_administre=row['fmob_avis_cdc_mutation_administre'] + fmob.fmob_avis_cdc_mobilite_externe=row['fmob_avis_cdc_mobilite_externe'] + fmob.fmob_avis_cdc_mobilite_interne=row['fmob_avis_cdc_mobilite_interne'] + fmob.fmob_avis_cdc_mobilite_centre_interet=row['fmob_avis_cdc_mobilite_centre_interet'] + fmob.fmob_avis_cdc_mobilite_specialite=row['fmob_avis_cdc_mobilite_specialite'] + fmob.fmob_avis_cdc_mobilite_hors_metropole=row['fmob_avis_cdc_mobilite_hors_metropole'] + fmob.fmob_avis_cdc_mobilite_recrutement_particulier_admin=row['fmob_avis_cdc_mobilite_recrutement_particulier_admin'] + fmob.fmob_date_deb_fmob=row['fmob_date_deb_fmob'] + fmob.fmob_date_fin_fmob=row['fmob_date_fin_fmob'] + fmob.fmob_date_signature_admin_fmob=row['fmob_date_signature_admin_fmob'] + fmob.fmob_date_signature_chef_de_corps=row['fmob_date_signature_chef_de_corps'] + fmob.fmob_motif_edition_la=row['fmob_motif_edition_la'] + fmob.fmob_reconnaissance_parcours_pro_administre=row['fmob_reconnaissance_parcours_pro_administre'] + fmob.fmob_statut=row['fmob_statut'] + fmob.fmob_remarques_eventuelles_administres = row['fmob_remarques_eventuelles_administres'] + fmob.fmob_avis_commandant_formation = row['fmob_avis_commandant_formation'] + + liste_update.append(fmob) + + else: + # ADD columns + + fmob = FMOB(fmob_id=str(int(row['administre_id'])) + '_' + str(int(row['fmob_millesime'])), + administre_id=adm_id, + fmob_millesime=row['fmob_millesime'], + fmob_annulation_fmob=row['fmob_annulation_fmob'], + fmob_reception_drhat_fmob=row['fmob_reception_drhat_fmob'], + fmob_sans_suite_militaire_fmob=row['fmob_sans_suite_fmob'], + fmob_mobilite_bassin_externe=row['fmob_mobilite_bassin_externe'], + fmob_mobilite_bassin_interne=row['fmob_mobilite_bassin_interne'], + fmob_mobilite_centre_interet_adt=row['fmob_mobilite_centre_interet_adt'], + fmob_mobilite_dans_specialite=row['fmob_mobilite_dans_specialite'], + fmob_mobilite_hors_metropole=row['fmob_mobilite_hors_metropole'], + fmob_mobilite_recrutement_particulier_administre=row['fmob_mobilite_recrutement_particulier_administre'], + fmob_date_visa_militaire=row['fmob_date_visa_militaire'], + fmob_depart_institution_soff=row['fmob_depart_institution_soff'], + fmob_avis_cdc_mutation_administre=row['fmob_avis_cdc_mutation_administre'], + fmob_avis_cdc_mobilite_externe=row['fmob_avis_cdc_mobilite_externe'], + fmob_avis_cdc_mobilite_interne=row['fmob_avis_cdc_mobilite_interne'], + fmob_avis_cdc_mobilite_centre_interet=row['fmob_avis_cdc_mobilite_centre_interet'], + fmob_avis_cdc_mobilite_specialite=row['fmob_avis_cdc_mobilite_specialite'], + fmob_avis_cdc_mobilite_hors_metropole=row['fmob_avis_cdc_mobilite_hors_metropole'], + fmob_avis_cdc_mobilite_recrutement_particulier_admin=row['fmob_avis_cdc_mobilite_recrutement_particulier_admin'], + fmob_date_deb_fmob=row['fmob_date_deb_fmob'], + fmob_date_fin_fmob=row['fmob_date_fin_fmob'], + fmob_date_signature_admin_fmob=row['fmob_date_signature_admin_fmob'], + fmob_date_signature_chef_de_corps=row['fmob_date_signature_chef_de_corps'], + fmob_motif_edition_la=row['fmob_motif_edition_la'], + fmob_reconnaissance_parcours_pro_administre=row['fmob_reconnaissance_parcours_pro_administre'], + fmob_statut=row['fmob_statut'], + fmob_proposition_affectation_verrouille=False, + fmob_remarques_eventuelles_administres = row['fmob_remarques_eventuelles_administres'], + fmob_avis_commandant_formation = row['fmob_avis_commandant_formation'], + ) + liste_create.append(fmob) + + if ignore_count: + logger.info('%s(s) ignoré(s) : %s', FMOB.__name__, ignore_count) + + size_batch = 100 + if liste_create: + logger.info('%s(s) créé(s) : %s', FMOB.__name__, len(liste_create)) + FMOB.objects.bulk_create(liste_create, batch_size=size_batch) + + if liste_update: + logger.info('%s(s) mis à jour : %s', FMOB.__name__, len(liste_update)) + FMOB.objects.bulk_update(liste_update, fields=update_header, batch_size=size_batch) + + + logger.info("----- Fin de l'insertion du fichier FMOB pour la table FMOB ----- ") + + return len(liste_create), len(liste_update), ignore_count + +def insert_FMOB_femp(df): + """ + Insertion des données du fichier FEMP pour la table FMOB dans la base + + :type df: dataframe + :param df: Dataframe contenant les données du fichier FEMP pretraités à inserer + + Returns + ------- + results : list + Liste de toutes les données insérées + + :return: - **results** (*list*): Liste de toutes les données insérées. + + """ + logger.info("----- Début de l'insertion du fichier FEMP pour la table FMOB ----- ") + + + liste_create = [] + liste_update = [] + update_header = ['fmob_annulation_femp', 'fmob_proposition_affectation_verrouille', + 'fmob_sans_suite_militaire_femp', 'fmob_date_signature_admin_femp', "fmob_millesime_femp","fmob_commentaire_ac"] + + + fmob_dict = dict((f"{o.administre_id}", o) for o in FMOB.objects.all()) + adm_ids = list(Administres_Pams.objects.values_list('pk', flat=True)) + + ignore_count = 0 + for i, row in df.iterrows(): + adm_id = str(int(row['administre_id'])) + str(int(row['fmob_millesime_femp'])) + if adm_id not in adm_ids: + logger.warning("FEMP d'id %s ignoré car l'administré est absent de la base", adm_id) + ignore_count = ignore_count + 1 + continue + + if str(adm_id) in fmob_dict: + fmob = fmob_dict.pop(str(adm_id)) + fmob.fmob_annulation_femp = row['fmob_annulation_femp'] + fmob.fmob_proposition_affectation_verrouille = row['fmob_proposition_affectation_verrouille'] + fmob.fmob_sans_suite_militaire_femp = row['fmob_sans_suite_femp'] + fmob.fmob_date_signature_admin_femp = row['fmob_date_signature_admin_femp'] + fmob.fmob_millesime_femp = row['fmob_millesime_femp'] + fmob.fmob_commentaire_ac = row['fmob_commentaire_ac'] + + + liste_update.append(fmob) + else: + fmob = FMOB(fmob_id=str(int(row['administre_id'])) + '_' + str(None), + administre_id=adm_id, fmob_annulation_femp=row['fmob_annulation_femp'], + fmob_proposition_affectation_verrouille=row['fmob_proposition_affectation_verrouille'], + fmob_sans_suite_militaire_femp=row['fmob_sans_suite_femp'], + fmob_statut='Non réceptionné', + fmob_date_signature_admin_femp=row['fmob_date_signature_admin_femp'], + fmob_commentaire_ac = row['fmob_commentaire_ac']) + liste_create.append(fmob) + # Administres_Pams.objects.update(pam_id=row['fmob_millesime']) + + if ignore_count: + logger.info('FEMP(s) ignoré(s) : %s', ignore_count) + + size_batch = 100 + if liste_create: + logger.info('%s(s) créé(s) : %s', FMOB.__name__, len(liste_create)) + FMOB.objects.bulk_create(liste_create, batch_size=size_batch) + + if liste_update: + logger.info('%s(s) mis à jour : %s', FMOB.__name__, len(liste_update)) + FMOB.objects.bulk_update(liste_update, fields=update_header, batch_size=size_batch) + + + logger.info("----- Fin de l'insertion du fichier FEMP pour la table FMOB ----- ") + + return len(liste_create), len(liste_update), ignore_count + + + +@execution_time(level=logging.INFO, logger_factory=data_perf_logger_factory) +def insert_administre_notation(df: pd.DataFrame) -> None: + """ + Insertion des données de la table Administre_Notation dans la base + + :param df: Dataframe contenant les données pretraités à inserer + :type df: class:`pandas.DataFrame` + """ + logger.info('start insert_administre_notation') + ModelType = Administre_Notation + liste_create = [] + liste_update = [] + + df['key'] = df['administre_id'].astype(float).astype(str) + '_' + df['no_age_annees'].astype(float).astype(str) + + fields_to_update = [ + 'no_annne_de_notation', + 'no_nr_ou_iris', + 'no_rac_ou_iris_cumule', + 'no_rf_qsr', + 'no_aptitude_emploie_sup', + 'no_potentiel_responsabilite_sup' + ] + + fields_db = ['administre_id','no_age_annees'] + fields_to_update + + # Integer ou Float Colonnes à mettre à jour + fields_num = [ + 'administre_id', + 'no_age_annees', + 'no_annne_de_notation', + 'no_nr_ou_iris', + 'no_rac_ou_iris_cumule' + + ] + + # 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 } + + + logger.info("Lecture de l'ensemble des notations en base") + # Lire tous les postes de la base de données + notation_in_db_df = pd.DataFrame.from_records(Administre_Notation.objects.all().values_list(*tuple(fields_db)), columns=fields_db) + + if not notation_in_db_df.empty: + # Il va y avoir de modification de type donc c'est mieux de ne pas toucher df + df_comparing = df.copy() + + # Modification de type de quelque champs + logger.info('Conversion des types pour la fusion') + notation_in_db_df = notation_in_db_df.fillna(np.NAN).replace('None',np.NAN) + + notation_in_db_df = notation_in_db_df.astype(dict_conv_str) + notation_in_db_df = notation_in_db_df.astype(dict_conv_float) + + notation_in_db_df['key'] = notation_in_db_df['administre_id'].astype(str) + '_' + notation_in_db_df['no_age_annees'].astype(str) + + + df_comparing = df_comparing.astype(dict_conv_str) + df_comparing = df_comparing.astype(dict_conv_float) + + # Comparaison pour savoir ce qui doit etre creer, mis a jour ou supprimer + + comparing_poste_id = pd.merge(df_comparing, notation_in_db_df, how='outer', on='key', suffixes=(None, "_x"), indicator=True) + same_rows = comparing_poste_id[comparing_poste_id['_merge']=='both'].drop('_merge', axis=1) + new_rows = comparing_poste_id[comparing_poste_id['_merge']=='left_only'].drop('_merge', axis=1) + delete_rows = comparing_poste_id[comparing_poste_id['_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_poste_both = pd.merge(same_rows, notation_in_db_df, how='left', on=fields_db, + suffixes=(None, "_x"), indicator=True) + + not_updated_rows = comparing_poste_both[comparing_poste_both['_merge']=='both'].drop('_merge', axis=1) + updated_rows = comparing_poste_both[comparing_poste_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['key'].isin(list(updated_rows['key']))] + update['db_create_status'] = 0 + create = df.loc[df['key'].isin(list(new_rows['key']))] + create['db_create_status'] = 1 + df = pd.concat([update, create]) + else: + df['db_create_status'] = 1 + not_updated_rows = pd.DataFrame([]) + + dict_create = {} + dict_update = {} + error_count = 0 + + for df_batch in batch_iterator(df, 1000): + # itération par lot pour limiter le nombre d'objets en mémoire + for idx, row in df_batch.iterrows(): + pk = f"{row['administre_id']}_{row['no_age_annees']}" + try: + model = ModelType( + id = pk, + administre_id = row['administre_id'], + no_nr_ou_iris=row['no_nr_ou_iris'], + no_rac_ou_iris_cumule=row['no_rac_ou_iris_cumule'], + no_rf_qsr=row['no_rf_qsr'], + no_aptitude_emploie_sup=row['no_aptitude_emploie_sup'], + no_potentiel_responsabilite_sup=row['no_potentiel_responsabilite_sup'], + no_annne_de_notation=row['no_annne_de_notation'], + no_age_annees=row['no_age_annees'] + ) + + if row['db_create_status']: + dict_create.setdefault(pk, model) + else: + dict_update.setdefault(pk, model) + + except Exception: + error_count = error_count + 1 + logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, idx, pk) + + if error_count: + logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if not not_updated_rows.empty: + logger.info('%s(s) déjà à jour : %s', ModelType.__name__, len(not_updated_rows)) + + batch_size = 100 + if dict_create: + logger.info('Création de %s %s(s)...', len(dict_create), ModelType.__name__) + 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(s) créé(s) : %s', ModelType.__name__, len(dict_create)) + else: + logger.info('Aucun %s(s) à créer', ModelType.__name__) + if dict_update and fields_to_update: + logger.info('Mise à jour de %s %s(s)...', len(dict_update), ModelType.__name__) + 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(s) mis à jour : %s', ModelType.__name__, len(dict_update)) + else: + logger.info('Aucun %s(s) à mettre à jour', ModelType.__name__) + + return len(dict_create), len(dict_update), error_count + +def suppression_administres(df: pd.DataFrame) -> None: + """ + Suppression des administrés de la table des administrés + + :param df: Dataframe contenant les données pretraités à supprimer + :type df: class:`pandas.DataFrame` + """ + ModelType = Administre + try: + delete_list = df['a_id_sap'].tolist() + logger.debug('%s(s) SAP à supprimer : %s', ModelType.__name__, len(delete_list)) + deleted = ModelType.objects.filter(Q(a_id_sap__in=delete_list)).delete() + if deleted[0]: + logger.info('Instance(s) supprimée(s) : %s', deleted[0]) + for k, v in deleted[1].items(): + logger.info(' %s(s) supprimé(s) : %s', k, v) + + except Exception as e: + logger.exception("Une erreur est survenue lors de la suppression %s", e) + + return len(delete_list) + + +# fonction d'insertion dans la table Notation +def insert_liste_preference(df): + """ + Insertion des données de la liste preference dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + + """ + liste_create = [] + liste_update = [] + update_header = ['administre_id','administre_pam_id','poste_id','lp_rang_poste','pam_id'] + for i in range(df.shape[0]): + try: + pl = PreferencesListe.objects.get(lp_id=int(df.at[i, 'lp_id'])) + # Update + pl.administre_pam_id = df.at[i, 'administre_pam_id'] + pl.administre_id = df.at[i, 'a_id_sap'] + pl.poste_id = df.at[i, 'poste_id'] + pl.lp_rang_poste = df.at[i, 'lp_rang_poste'] + pl.pam_id = df.at[i, 'pam_id'] + liste_update.append(pl) + except: + pl_create = PreferencesListe(lp_id=df.at[i, 'lp_id'], + poste_id=df.at[i, 'poste_id'], + administre_pam_id=df.at[i, 'administre_pam_id'], + administre_id=df.at[i, 'a_id_sap'], + lp_rang_poste=df.at[i, 'lp_rang_poste'], + pam_id=df.at[i, 'pam_id']) + liste_create.append(pl_create) + + PreferencesListe.objects.bulk_create(liste_create) + PreferencesListe.objects.bulk_update(liste_update, fields=update_header) + return 1 + + +def insert_Notation(df, pam_id, sv_id): + """ + Insertion des données de la table Notation dans la base + + :type df: dataframe + :param df: Dataframe contenant les données pretraités à inserer + + + :return: - **1** (*int*): La fonction renvoie 1 si l'execution a été réalisé avec succés. + + """ + start_time = time.time() + liste_create = [] + liste_update = [] + update_header = ['no_date_execution', 'no_score_administre'] + statut = [StatutPam.A_MUTER,StatutPam.A_ETUDIER] + + # Mettre le flag_cple de toutes les notations du sours-vivier à Faux dans la base de données + Notation.objects.filter(pam_id=pam_id, administre__sous_vivier_id=sv_id, no_flag_cple_ideal=True).update(no_flag_cple_ideal=False) + + # Lire la table notation où les administrés appartiennent au sous_vivier + all_notation_sv_id = Notation.objects.filter(pam_id=pam_id,administre__sous_vivier_id=sv_id).values() + all_notation_sv_id_pd = pd.DataFrame.from_records(all_notation_sv_id) + # Vérifiez si la notation dans la base de données est vide ; si elle est vide, toutes les notations calculées sont différentes. + if not all_notation_sv_id_pd.empty: + # Comparer la nouvelle notation calculée à la notation de la base de données, et créer un dataframe avec seulement les différents lignes + Comparing_notation = pd.merge(df, all_notation_sv_id_pd, how='left', left_on=['administre_pam_id', 'poste_pam_id', 'pam_id', 'no_score_administre'], + right_on=['administre_pam_id', 'poste_pam_id', 'pam_id', 'no_score_administre'], suffixes=(None, "_x"), indicator=True) + Comparing_notation.drop(['no_date_execution_x', 'no_flag_cple_ideal_x'], 1, inplace=True) + Different_rows = Comparing_notation[Comparing_notation['_merge'] != 'both'].drop('_merge', 1) + Different_rows = Different_rows.drop(Different_rows.columns.difference(['poste_pam_id','poste_id','administre_pam_id','administre_id','pam_id','no_date_execution','no_score_administre','no_flag_cple_ideal']), 1) + else : + # Comme la notation est vide dans la base de données, les différentes lignes seront toutes les notations calculées et la notation de la base de données aura seulement les colonnes mais vide + Different_rows = df.copy() + all_notation_sv_id_pd = df.copy()[0:0] + logger.debug("Il existe {} des lignes qui ont changé".format(len(Different_rows))) + + + logger.debug("------------------Comparing Time -- %d seconds -----------------" % (time.time() - start_time)) + start_time = time.time() + + + +# Si certaines lignes sont différentes, vérifiez si elles doivent être mises à jour ou créées. + if not Different_rows.empty : + Merging_notation = pd.merge(Different_rows, all_notation_sv_id_pd, how='left', left_on=['administre_pam_id', 'poste_pam_id', 'pam_id'], + right_on=['administre_pam_id', 'poste_pam_id', 'pam_id'], suffixes=(None, "_x"), indicator=True) + Merging_notation.drop(['no_date_execution_x', 'no_score_administre_x', 'no_flag_cple_ideal_x'], 1, inplace=True) + update = Merging_notation[Merging_notation['_merge'] == 'both'].drop('_merge', 1) + create = Merging_notation[Merging_notation['_merge'] != 'both'].drop('_merge', 1) + + logger.debug("------------------Merging Time -- %d seconds -----------------" % (time.time() - start_time)) + start_time = time.time() + + # Création de la liste des objets notations qui doivent être créées + if not create.empty: + liste_create = list(create.apply(lambda x: Notation(poste_id=x['poste_id'], + poste_pam_id = x['poste_pam_id'], + administre_id=x['administre_id'], + administre_pam_id=x['administre_pam_id'], + pam_id=x['pam_id'], + no_date_execution=x['no_date_execution'], + no_score_administre=x['no_score_administre'], + no_flag_cple_ideal=x['no_flag_cple_ideal']), axis=1)) + + # Création de la liste des objets notations qui doivent être mises à jour + if not update.empty: + liste_update = list(update.apply(lambda x: Notation(no_id=x['no_id'], + poste_id=x['poste_id'], + poste_pam_id = x['poste_pam_id'], + administre_id=x['administre_id'], + administre_pam_id=x['administre_pam_id'], + pam_id=x['pam_id'], + no_date_execution=x['no_date_execution'], + no_score_administre=x['no_score_administre'], + no_flag_cple_ideal=x['no_flag_cple_ideal']), axis=1)) + + logger.debug("------------------Creating Objects Time -- %d seconds -----------------" % (time.time() - start_time)) + start_time = time.time() + + + logger.debug("Dans les {} des lignes qui ont changé :".format(len(Different_rows))) + logger.debug(" Il ya {} des lignes qui doivent être créées".format(len(liste_create))) + logger.debug(" Il ya {} des lignes qui doivent être mises à jour".format(len(liste_update))) + + batch_size = 100 + Notation.objects.bulk_create(liste_create, batch_size=batch_size) + Notation.objects.bulk_update(liste_update, fields=update_header, batch_size=batch_size) + + logger.debug("------------------Inserting Objects Time -- %d seconds -----------------" % (time.time() - start_time)) + + else : + logger.debug("Rien à mettre à jour ou à créer") + + return 1 + + + +# Fonction d'inertion des resultats du matching +def insert_matching(matching_dict, pam_id): + """ + Fonction d'insertion en base des resultats du matching + + :type matching_dict: dictionnaire + :param matching_dict: dictionnaire python presentant le resultats du matching + + Returns + ------- + 'insert_matching_result_done' : str + La fonction renvoie 'insert_matching_result_done' si l'execution a été réalisé avec succés + + :return: - **insert_matching_result_done** (*str*): La fonction renvoie 'insert_matching_result_done' si l'execution a été réalisé avec succés. + + + + """ + for poste, list_admin in matching_dict.items(): + for i in range(len(list_admin)): + couple_ideal = Notation.objects.get(poste_pam_id=str(poste)+str(pam_id), administre_pam_id=str(list_admin[i].name)+str(pam_id)) + couple_ideal.no_flag_cple_ideal = True + couple_ideal.save() + return ('insert_matching_result_done') diff --git a/backend-django/backend/utils_matching.py b/backend-django/backend/utils_matching.py new file mode 100644 index 0000000..a47493a --- /dev/null +++ b/backend-django/backend/utils_matching.py @@ -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) diff --git a/backend-django/backend/utils_scoring.py b/backend-django/backend/utils_scoring.py new file mode 100644 index 0000000..846f83d --- /dev/null +++ b/backend-django/backend/utils_scoring.py @@ -0,0 +1,424 @@ +""" +Ce module contient les Utilitaires de scoring +""" +# import des pré requis +import json +from datetime import date +from django.db.models import Q +import time +import pandas as pd +import numpy as np +from django.utils import timezone +from .models import Administre, Domaine, FMOB, Fonction, FormationEmploi, Garnison, Grade, Poste, Notation, Postes_Pams, \ + PreferencesListe, Marque, MarquesGroupe, Filiere, Competence, StatutPamChoices, AvisPosteChoices as AvisPoste, \ + Administres_Pams +from .utils.logging import get_logger +import itertools + +logger = get_logger(__name__) + +# calcul de la date +today = date.today() + + +# Fonction de lancement du preprocessing et d'encoding +def encoding(a_fil, a_nf, a_comp, p_fil, p_nf, p_comp): + """ + Retourne l'encodage de l' "eip + département" par rapport à un poste donné + + :type a_fil: numpy list + :param a_fil: filière de l'eip actuel militaire + + :type a_nf: numpy list + :param a_nf: nf de l'eip actuel militaire + + :type a_comp: ? + :param a_comp: ? + + :type p_fil: numpy list + :param p_fil: filière du poste vacant + + :type p_nf: numpy list + :param p_nf: nf du poste vacant + + :type p_comp: ? + :param p_comp: ? + + :return: - **encoding** (*numpy list*): encoding de l'eip actuel du militaire et du département où il souhaite travailler par rapport à un poste vacant + """ + b_fil = (a_fil == p_fil).astype(int) + b_nf = (a_nf == p_nf).astype(int) + + b_comp = [len(list(filter(i.__contains__, j)))/len(j) if j!=[None] else 0 for i,j in zip(a_comp,p_comp)] + logger.debug('Calcul des compétences') + + encoding = (b_fil, b_nf, b_comp) + return encoding + + +# fonction de calcul du scores +def notePonderee(a_fil, a_nf, a_comp, p_fil, p_nf, p_comp, p_poids_fil, p_poids_nf, p_poids_comp): + """ + Retourne la note d'une combinaison eip+département par rapport à un poste donné + + :type a_dom: numpy list + :param a_dom: domaine de l'eip actuel du militaire + + :type a_fil: numpy list + :param a_fil: filière de l'eip actuel militaire + + :type a_dep: numpy list + :param a_dep: département où souhaite travailler le militaire + + :type a_nf: numpy list + :param a_nf: nf de l'eip actuel militaire + + :type p_dom: numpy list + :param p_dom: domaine du poste vacant + + :type p_fil: numpy list + :param p_fil: filière du poste vacant + + :type p_dep: numpy list + :param p_dep: département du poste vacant + + :type p_nf: numpy list + :param p_nf: nf du poste vacant + + :type p_poids_dom: numpy list + :param p_poids_dom: poids du domaine + + :type p_poids_fil: numpy list + :param p_poids_fil: poids de la filière + + :type p_poids_dep: numpy list + :param p_poids_dep: poids du département + + :type p_poids_nf: numpy list + :param p_poids_nf: poids du nf + + + :return: - **note** (*float*): note de l'eip par rapport au poste à pourvoir + + """ + + e = encoding(a_fil, a_nf, a_comp, p_fil, p_nf, p_comp) + note = p_poids_fil * e[0] + p_poids_nf * e[1] + p_poids_comp * e[2] + return note + + +def notations(pam_id, sv_id): + """ + Remplit la colonne note de la table Notations + + :type sv_id: char + :param sv_id: sous vivier étudié + + + :return: - **notations** (*dataframe*): table des notations avec les notes complétées + + + """ + # Sélection des postes sur lesquels aucune décision n'est prise + logger.debug('start scoring') + + colonnes_pos = ('id','poste_id', 'poste__competences', 'poste__p_filiere_id', 'poste__p_nf', 'p_avis_pam') + colonnes_pos_name = ('poste_pam_id','poste_id', 'competences', 'p_filiere_id', 'p_nf', 'p_avis_pam') + statut_poste =[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4] + + postes = Postes_Pams.objects.values_list(*colonnes_pos).filter(Q(p_pam_id=pam_id) + & Q(poste__sous_viviers=sv_id) + & Q(decisions__de_decision__isnull=True) + & Q(p_avis_pam__in=statut_poste)) + + if not postes.exists: + logger.debug(f"Pas de poste avec le bon statut {statut_poste} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception("Aucun poste avec le bon statut.") + + pos = pd.DataFrame.from_records(postes, columns=colonnes_pos_name) + pos=pos.groupby(['poste_pam_id','poste_id','p_filiere_id', 'p_nf', 'p_avis_pam'])['competences'].apply(list).reset_index(name='competences') + + + # Remplire les poids des postes + p_poids = ['p_poids_filiere', 'p_poids_nf', 'p_poids_competences'] + try: + pos['p_poids'] = pos.apply(lambda x: Poste.objects.get(p_id=x['poste_id']).p_poids_filiere_nf_competences, axis=1) + pos[p_poids] = pd.DataFrame(pos['p_poids'].tolist(), index=pos.index) + + except Exception as e: + raise Exception("Tous les postes sont déjà affectés à un adminstré : Veuillez en sélectionner de nouveau") + + # Sélection des administrés dont le statut PAM est "à muter" et sur lesquels aucune décision n'est prise + colonnes_adm = ('id','administre__a_id_sap', 'administre__a_filiere_futur_id', + 'administre__a_nf_futur', 'administre__a_liste_id_competences') + + colonnes_adm_name = ('administre_pam_id', 'a_id_sap', 'a_filiere_futur_id', + 'a_nf_futur', 'a_liste_id_competences') + + statut = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER] + + administres = Administres_Pams.objects.filter(pam_id=pam_id, administre__sous_vivier_id=sv_id, a_statut_pam_annee__in=statut, + decision__de_decision__isnull=True).values_list(*colonnes_adm) + + if not administres.exists: + logger.debug(f"Pas de poste avec le bon statut {statut} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun administrés avec le bon statut.") + + adm = pd.DataFrame.from_records(administres, columns=colonnes_adm_name) + + if len(adm) == 0: + raise Exception("Arret du scoring 0 administrés dont le statut PAM est à muter et sur lesquels aucune décision n'est prise") + + logger.debug('Nb admin traités : %s', len(adm)) + logger.debug('Nb groupe_poste traités : %s', len(pos)) + + adm=adm.groupby(['administre_pam_id','a_id_sap','a_filiere_futur_id','a_nf_futur'])['a_liste_id_competences'].apply(list).reset_index(name='a_liste_id_competences') + + + # Ajout de la colonne Key avec la valeur 1 aux deux bases de données, pour pouvoir effectuer la jointure croisée. + adm['key'] = 1 + pos['key'] = 1 + + # Jointure croisée et suppression de la colonne clé + result = pd.merge(pos, adm, on='key').drop("key", 1) + + # Ajout de la date d'exécution + now = timezone.now() + result['no_date_execution'] = now + # Calcul de la notePondéré + result['no_score_administre'] = notePonderee(result['a_filiere_futur_id'].to_numpy(), + result['a_nf_futur'].to_numpy(), + result['a_liste_id_competences'].to_numpy(), + result['p_filiere_id'].to_numpy(), result['p_nf'].to_numpy(), + result['competences'].to_numpy(), + result['p_poids_filiere'].to_numpy(), result['p_poids_nf'].to_numpy(), + result['p_poids_competences'].to_numpy()) + + + # Suppression de toutes les colonnes inutiles + result.drop(result.columns.difference(['poste_pam_id','administre_pam_id','poste_id','a_id_sap','no_score_administre','no_date_execution']), 1, inplace=True) + # Création de la colonne no_flag_cple_ideal + result['no_flag_cple_ideal'] = False + # Changer le nom des colonnes + result = result.rename(columns={'a_id_sap': 'administre_id'}) + result = result.astype({'administre_id': 'object', 'no_flag_cple_ideal': 'object'}) + result['pam_id'] = pam_id + + logger.debug('end scoring') + return result + +def notations_liste(sv_id, pam_id, l_a_id, l_p_id): + """ + Remplit la colonne note de la table Notations + + :type sv_id: char + :param sv_id: sous vivier étudié + + + :return: - **notations** (*dataframe*): table des notations avec les notes complétées + + + """ + # Sélection des postes sur lesquels aucune décision n'est prise + logger.debug('start scoring') + + colonnes_pos = ('id','poste_id', 'poste__competences', 'poste__p_filiere_id', 'poste__p_nf', 'p_avis_pam') + colonnes_pos_name = ('poste_pam_id','poste_id', 'competences', 'p_filiere_id', 'p_nf', 'p_avis_pam') + statut_poste =[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4] + + postes = Postes_Pams.objects.values_list(*colonnes_pos).filter(Q(p_pam_id=pam_id) + & Q(poste__sous_viviers=sv_id) + & Q(decisions__de_decision__isnull=True) + & Q(p_avis_pam__in=statut_poste) + & Q(poste_id__in=l_p_id)) + + if not postes.exists: + logger.debug(f"Pas de poste avec le bon statut {statut_poste} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception("Aucun poste avec le bon statut.") + + pos = pd.DataFrame.from_records(postes, columns=colonnes_pos_name) + pos=pos.groupby(['poste_pam_id','poste_id','p_filiere_id', 'p_nf', 'p_avis_pam'])['competences'].apply(list).reset_index(name='competences') + + + # Remplire les poids des postes + p_poids = ['p_poids_filiere', 'p_poids_nf', 'p_poids_competences'] + try: + pos['p_poids'] = pos.apply(lambda x: Poste.objects.get(p_id=x['poste_id']).p_poids_filiere_nf_competences, axis=1) + pos[p_poids] = pd.DataFrame(pos['p_poids'].tolist(), index=pos.index) + + except Exception as e: + raise Exception("Tous les postes sont déjà affectés à un adminstré : Veuillez en sélectionner de nouveau") + + # Sélection des administrés dont le statut PAM est "à muter" et sur lesquels aucune décision n'est prise + colonnes_adm = ('id','administre__a_id_sap', 'administre__a_filiere_futur_id', + 'administre__a_nf_futur', 'administre__a_liste_id_competences') + + colonnes_adm_name = ('administre_pam_id', 'a_id_sap', 'a_filiere_futur_id', + 'a_nf_futur', 'a_liste_id_competences') + + statut = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER] + + administres = Administres_Pams.objects.filter(pam_id=pam_id, administre__sous_vivier_id=sv_id, a_statut_pam_annee__in=statut,administre_id__in=l_a_id, + decision__de_decision__isnull=True).values_list(*colonnes_adm) + + if not administres.exists: + logger.debug(f"Pas de poste avec le bon statut {statut} dans le sous vivier {sv_id} et le pam {pam_id}.") + raise Exception(f"Aucun administrés avec le bon statut.") + + adm = pd.DataFrame.from_records(administres, columns=colonnes_adm_name) + + if len(adm) == 0: + raise Exception("Arret du scoring 0 administrés dont le statut PAM est à muter et sur lesquels aucune décision n'est prise") + + logger.debug('Nb admin traités : %s', len(adm)) + logger.debug('Nb groupe_poste traités : %s', len(pos)) + + adm=adm.groupby(['administre_pam_id','a_id_sap','a_filiere_futur_id','a_nf_futur'])['a_liste_id_competences'].apply(list).reset_index(name='a_liste_id_competences') + + + # Ajout de la colonne Key avec la valeur 1 aux deux bases de données, pour pouvoir effectuer la jointure croisée. + adm['key'] = 1 + pos['key'] = 1 + + # Jointure croisée et suppression de la colonne clé + result = pd.merge(pos, adm, on='key').drop("key", 1) + + # Ajout de la date d'exécution + now = timezone.now() + result['no_date_execution'] = now + # Calcul de la notePondéré + result['no_score_administre'] = notePonderee(result['a_filiere_futur_id'].to_numpy(), + result['a_nf_futur'].to_numpy(), + result['a_liste_id_competences'].to_numpy(), + result['p_filiere_id'].to_numpy(), result['p_nf'].to_numpy(), + result['competences'].to_numpy(), + result['p_poids_filiere'].to_numpy(), result['p_poids_nf'].to_numpy(), + result['p_poids_competences'].to_numpy()) + + + # Suppression de toutes les colonnes inutiles + result.drop(result.columns.difference(['poste_pam_id','administre_pam_id','poste_id','a_id_sap','no_score_administre','no_date_execution']), 1, inplace=True) + # Création de la colonne no_flag_cple_ideal + result['no_flag_cple_ideal'] = False + # Changer le nom des colonnes + result = result.rename(columns={'a_id_sap': 'administre_id'}) + result = result.astype({'administre_id': 'object', 'no_flag_cple_ideal': 'object'}) + result['pam_id'] = pam_id + + logger.debug('end scoring') + return result + + + +# TODO:Revoir les entrées de la fonction notation_test +# fonction notation pour le test +def notations_test(): + """ + Remplit la colonne note de la table Notations + + + :return: - **notations** (*dataframe*): table des notations avec les notes complétées + + + """ + + # test dataframes + p = pd.DataFrame( + columns=["p_id", "fonction_id", "sous_vivier_id", "formation_emploi_id", "p_liste_id_marques", "p_eip", + "p_date_fin", "p_flag_pam", "p_notes_gestionnaire", "p_notes_partagees", "p_statut_pam", + "p_poids_domaine", + "p_poids_filiere", "p_poids_nf", "p_poids_garnison", "p_gar"]) + p['p_id'] = [1, 2, 3] + p['fonction_id'] = [2, 4, 5] + p['sous_vivier_id'] = [6, 7, 8] + p['formation_emploi_id'] = [9, 5, 10] + p['p_liste_id_marques'] = ["1_P1,1_P2,1_P4", "1_P1,2_C,1_P4", "2_T,1_P2,1_P4"] + p['p_eip'] = ['MAIGMA3a', 'ADMAES3b', 'SICEDR3a'] + p['p_date_fin'] = ['', '', ''] + p['p_flag_pam'] = [1, 1, 0] + p['p_gar'] = [13, 14, 18] + p['p_notes_gestionnaire'] = ['', '', ''] + p['p_notes_partagees'] = ['', '', ''] + p['p_statut_pam'] = ['P4', 'P4', 'P4'] + p['p_poids_domaine'] = [0.25, 0.25, 0.25] + p['p_poids_filiere'] = [0.25, 0.25, 0.25] + p['p_poids_nf'] = [0.25, 0.25, 0.25] + p['p_poids_garnison'] = [0.25, 0.25, 0.25] + + a_bis = {'a_id_sap': [9984, 56754, 78905], + 'formation_emploi_id': [2, 4, 6], + 'fonction_id': [3, 78, 90], + 'sous_vivier_id': [5, 8, 98], + 'grade_id': [4, 9, 5], + 'a_liste_id_marques': ['[,,,]', '[,,,]', '[,,,]'], + 'a_nom': ['Nom_1', 'Nom_2', 'Nom_3'], + 'a_prenom': ['Prenom_1', 'Prenom_2', 'Prenom_3'], + 'a_sexe': ['M', 'F', 'M'], + 'a_id_def': [4, 5, 6], + 'a_eip': ['MAIGMA3a', 'ADMAES3b', 'SICEDR3a'], + 'a_domaine': ['MAI', 'ADM', 'SIC'], + 'a_filiere': ['GMA', 'AES', 'EDR'], + 'a_nf': ['3a', '3b', '3a'], + 'a_domaine_gestion': ['', '', ''], + 'a_date_entree_service': ['', '', ''], + 'a_gar': [13, 14, 18], + 'a_arme': ['', '', ''], + 'a_rg_origine_recrutement': ['', '', ''], + 'a_date_naissance': ['', '', ''], + 'a_diplome_HL': ['', '', ''], + 'a_dernier_diplome': ['', '', ''], + 'a_credo_fe': ['', '', ''], + 'a_date_arrivee_fe': ['', '', ''], + 'a_date_pos_statuaire': ['', '', ''], + 'a_pos_statuaire': ['', '', ''], + 'a_interuption_service': ['', '', ''], + 'a_situation_fam': ['', '', ''], + 'a_nombre_enfants': [2, 6, 9], + 'a_eis': ['', '', ''], + 'a_sap_conjoint': [987, 456, 908], + 'a_flag_particulier': [0, 1, 0], + 'a_flag_pam': [1, 0, 1], + 'a_statut_pam': ['non etudié', 'affecté', 'affecté'], + 'a_notes_gestionnaire': ['', '', ''], + 'a_notes_partagees': ['', '', ''], + 'a_eip_futur': ['', '', ''], + 'a_affectation1': ['', '', ''], + 'a_affectation2': ['', '', ''], + 'a_affectation3': ['', '', ''] + } + a = pd.DataFrame(data=a_bis) + + notation = pd.DataFrame( + columns=["poste_id", "administres_id", "pam_id", "no_date_execution", "no_score_administre", + "no_flag_cple_ideal"]) + + for i in range(len(p)): + p_id = p.loc[i, "p_id"] + p_dom = p.loc[i, "p_eip"][0:3] + p_fil = p.loc[i, "p_eip"][3:6] + p_nf = p.loc[i, "p_eip"][6:] + p_gar = p.loc[i, "p_gar"] + p_poids_dom = 0.25 + p_poids_fil = 0.25 + p_poids_nf = 0.25 + p_poids_gar = 0.25 + + for j in range(len(a)): + a_id_sap = a.at[j, 'a_id_sap'] + a_dom = a.at[j, 'a_eip'][0:3] + a_fil = a.at[j, 'a_eip'][4:7] + a_nf = a.at[j, 'a_eip'][8:] + a_gar = a.at[j, 'a_gar'] + + n = notePonderee(a_dom, a_fil, a_nf, a_gar, p_dom, p_fil, p_nf, p_gar, p_poids_dom, p_poids_fil, p_poids_nf, + p_poids_gar) + + new_row = {'administres_id': a_id_sap, + 'poste_id': p_id, + 'no_date_execution': '14/06/2021', + 'no_note_militaire': n, + "no_flag_cple_ideal": 0 + } + notation = notation.append(new_row, ignore_index=True) + + return notation diff --git a/backend-django/backend/views.py b/backend-django/backend/views.py new file mode 100644 index 0000000..6eec0ad --- /dev/null +++ b/backend-django/backend/views.py @@ -0,0 +1,1657 @@ +import numpy as np +from asgiref.sync import sync_to_async +from django.db.models import ManyToOneRel, Window, F, Q, Sum, Case, When, IntegerField, Prefetch +from django.db.models.functions import Rank +from django.forms import model_to_dict +from django.http import JsonResponse +from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import generics, viewsets, serializers, status +from rest_framework import viewsets, status +from rest_framework.views import APIView +from rest_framework.response import Response +import pandas as pd + +from . import constants +from django.db.models import Q +from .filters import AdministreFilter, RelatedOrderingFilter, PosteFilter +from .paginations import HeavyDataPagination +from .serializers import AdministreSerializer, FileCompetenceSerializer, FmobSerializer, FormationEmploiSerializer, \ + PcpFeGroupeSerializer, \ + PosteSerializer, \ + NotationSerializer, MarqueSerializer, MarquesGroupeSerializer, FileSerializer, \ + FiliereSerializer, DomaineSerializer, FileSVSerializer, DecisionSerializer, \ + SousVivierAssociationSerializer, ScoringValidator, AlimentationReferentielSerializer +from .models import Administre, Competence, FMOB, FormationEmploi, PcpFeGroupe, Poste, Notation, \ + PreferencesListe, Marque, MarquesGroupe, \ + Filiere, Domaine, SousVivier, Grade, CustomUser, Decision, Calcul, SousVivierAssociation, \ + StatutPamChoices as StatutPam +from .utils import cleanString, impact_decisions +from .utils_extraction import to_table_administres, to_table_domaines, to_table_fe, to_table_filieres, \ + to_table_fmob, to_table_fonctions, to_table_garnisons, to_table_grades, \ + to_table_fmob, to_table_fonctions, to_table_garnisons, to_table_grades, \ + to_table_liste_preference, \ + to_table_groupesMarques, to_table_marques, to_table_postes, process_files, open_excel +from .utils_insertion import insert_Administre, insert_Domaine, insert_FMOB, insert_Filiere, insert_Fonction, \ + insert_FormationEmploi, insert_Garnison, insert_Grade, insert_MarquesGroupe, insert_Notation, \ + insert_Marque, insert_Poste, insert_liste_preference, insert_matching +from .utils_matching import matching_parfait, preprocess_matching +from .utils_calcul import lancer_calculs, notations +from .reporting import poste_vacant_vivier, reporting_taux_armement_pcp, reporting_taux_armement_gestionnaire, \ + reporting_suivi_pam_admin, \ + reporting_suivi_pam_poste +import json + +from django.shortcuts import render +from django.contrib.auth.decorators import login_required + + +from datetime import datetime, date +from django.conf import settings +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework import serializers, status +from rest_framework.exceptions import APIException +from typing import Optional, Tuple +import logging +import requests +import time + +logger = logging.getLogger(__name__) + + +def api_exception(status_code: int, message: Optional[str] = None) -> APIException: + return type('MyException', (APIException,), {'status_code': status_code, 'default_detail': message}) + +# Vue de lancement pour le scoring +# Calcul les scores des administrés à muter pour les postes à pourvoir et stocke les résultats dans la table Calcul +class ScoringView(APIView): + """ + Cette classe est dédiée au vue de lancement pour le scoring et au calcul des scores des administrés à muter avec les postes à pourvoir. + """ + def __recuperer_chemin_requete(self, request) -> str: + """ récupère et reconstruit le chemin de la requête (mais pas forcément tel que le client la connaît) + + :type request: rest_framework.request.Request + :param request: Request contenant le port et l'adrresse ip du serveur + + :return: - **return** (*string*): une chaine de caractère contenant le statut modifié du calcul. + + """ + + port = request.get_port() + return f"{request.scheme}://localhost{':' + port if port else ''}{request.path}" + + + def __appeler_api_depiler_calculs(self, request): + """ appelle l'API pour lancer le prochain calcul en attente, ce qui permet d'attribuer l'opération à un worker indéfini + :type request: rest_framework.request.Request + :param request: Request contenant le chemin de la requète + """ + url = self.__recuperer_chemin_requete(request) + headers = request.headers + cookies = request.COOKIES + try: + requests.patch(url, headers=request.headers, cookies=request.COOKIES, timeout=0.001) + except requests.exceptions.ReadTimeout: + pass + except Exception as e: + logger.debug("échec de l'appel à %s: %s", url, e) + + @atomic + def __creer_calcul(self, sv_id: str) -> Calcul: + """ crée un calcul en base sauf s'il existe déjà un calcul en cours ou en attente pour le même sous-vivier + Raises + ------ + ApiException: + un calcul est déjà en cours ou en attente pour le même sous-vivier + IntegrityError: + la création a échoué + + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :return: - **Object** (*Object*): retourne l'objet calcul crée + """ + + Statut = Calcul.Statut + COL_ID = 'sous_vivier_id' + COL_STATUT = 'ca_statut' + COL_DEBUT = 'ca_date_debut' + + colonnes = (COL_ID, COL_DEBUT, COL_STATUT) + df = pd.DataFrame.from_records(Calcul.objects.values_list(*colonnes), columns=colonnes) + + en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_ID].values + if sv_id in en_cours: + raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-vivier') + + en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_ID].values + if sv_id in en_attente: + raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en attente pour ce sous-vivier') + + if en_attente.size: + calcul_autorise = False + else: + nb_max = getattr(settings, 'MAX_CALCULS', None) + calcul_autorise = not isinstance(nb_max, int) or len(en_cours) < nb_max + + date_debut = timezone.now() + Calcul.objects.filter(Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete() + if calcul_autorise: + return Calcul.objects.create(sous_vivier_id=sv_id, ca_date_debut=date_debut,ca_statut_pourcentage =0 , ca_statut=Statut.EN_COURS) + return Calcul.objects.create(sous_vivier_id=sv_id, ca_date_debut=date_debut,ca_statut_pourcentage =0 ,ca_statut=Statut.EN_ATTENTE) + + + @atomic + def __modifier_statut_premier_calcul_en_attente(self) -> Optional[ Tuple[str, datetime] ]: + """ lance le calcul en attente le plus ancien (si possible). Il ne s'agit que d'une modification en base. + (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :type ca_date_debut : date + :param ca_date_debut: date de début calcul en attente + :return: - **Object** (*Object*): retourne l'objet calcul crée + Raises + ------ + ApiException: + un calcul est déjà en cours ou en attente pour le même sous-vivier + IntegrityError: + la création a échoué + """ + + Statut = Calcul.Statut + COL_ID = 'sous_vivier_id' + COL_STATUT = 'ca_statut' + COL_DEBUT = 'ca_date_debut' + + colonnes = (COL_ID, COL_DEBUT, COL_STATUT) + df = pd.DataFrame.from_records(Calcul.objects.order_by(COL_DEBUT).values_list(*colonnes), columns=colonnes) + + en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE] + if en_attente.empty: + return None + + en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_ID].values + nb_max = getattr(settings, 'MAX_CALCULS', None) + calcul_autorise = not isinstance(nb_max, int) or en_cours.size < nb_max + if not calcul_autorise: + return None + + a_lancer = en_attente.iloc[0] + sv_id = a_lancer[COL_ID] + date_attente = a_lancer[COL_DEBUT] + date_debut = timezone.now() + + # la date de début est mise à jour aussi pour éviter de compter le temps d'attente + mis_a_jour = ( + Calcul.objects + .filter(sous_vivier_id=sv_id, ca_date_debut=date_attente, ca_statut=Statut.EN_ATTENTE) + .update(ca_date_debut=date_debut, ca_statut=Statut.EN_COURS) + ) + return (sv_id, date_debut) if mis_a_jour else None + + + def __executer_calcul(self, sv_id: str, date_debut: datetime) -> Calcul: + """ exécute un calcul (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :type ca_date_debut : date + :param ca_date_debut: date de début calcul en attente + :return: - **Object** (*Object*): retourne l'objet calcul ainsi que son statut + """ + + Statut = Calcul.Statut + COL_ID = 'sous_vivier_id' + COL_STATUT = 'ca_statut' + COL_DEBUT = 'ca_date_debut' + + qs_calcul = Calcul.objects.filter(sous_vivier_id=sv_id, ca_date_debut=date_debut) + qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS) + try: + lancer_calculs(sv_id) + except SystemExit: + logger.info("OGURE M'A TUER") + qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE) + raise + except Exception as e: + qs_calcul = Calcul.objects.filter(sous_vivier_id=sv_id, ca_date_debut=date_debut) + if str(e) == "Arret du calcul": + qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ARRETER) + raise + else : + qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ERREUR) + raise + qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE) + return qs_calcul.first() + + + def get(self, request): + """La fonction get vérifie s'il y a un calcul en cours et renvoie des informations sur le calcul. + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier + :return: - **return** (*JsonResponse*): json contenant les informations sur le calcul comme statut, date_debut, date_fin, administres et postes. + """ + debut = None + fin = None + administres = 0 + postes = 0 + try: + sv_id = request.query_params['sous_vivier_id'] + calcul = Calcul.objects.get(sous_vivier_id=sv_id) + debut = calcul.ca_date_debut + fin = calcul.ca_date_fin + administres_statuts = ['A_MUTER'] + administres = Administre.objects.filter(sous_vivier_id=sv_id, a_statut_pam__in=administres_statuts).count() + postes = Poste.objects.filter(Q(sous_vivier_id=sv_id) & ( + Q(p_nb_p1__gt=0) | Q(p_nb_p2__gt=0) | Q(p_nb_p3__gt=0) | Q(p_nb_p4__gt=0) | Q( + p_nb_vacant__gt=0))).count() + statut = calcul.ca_statut + statut_pourcentage = calcul.ca_statut_pourcentage + except: + statut = "AUCUN" + statut_pourcentage = 0 + + return JsonResponse( + {"statut": statut, "statut_pourcentage": statut_pourcentage, "date_debut": debut, "date_fin": fin, "administres": administres, "postes": postes}) + + def patch(self, request, id=None): + """La fonction patch est une méthode privée qui execute des méthodes privées qui exécute le calcul et le dépile. + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier et la date de dbut du calcul + """ + Statut = Calcul.Statut + try: + id_et_debut = self.__modifier_statut_premier_calcul_en_attente() + if id_et_debut is None: + return Response({'message': "aucun calcul n'a été lancé"}) + + try: + calcul = self.__executer_calcul(id_et_debut[0], id_et_debut[1]) + return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN}) + finally: + self.__appeler_api_depiler_calculs(request) + except (Http404, APIException): + raise + except SystemExit: + raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, 'lancement du prochain calcul arrêté') + except: + message = 'impossible de lancer le prochain calcul en attente' + logger.exception(message) + raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message) + + + def post(self, request): + """ lance le calcul sur un sous vivier + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :return: - **return** (*Response*): retourne le statut des postes + """ + Statut = Calcul.Statut + try: + # validation + validator = ScoringValidator(data=request.data) + validator.is_valid(raise_exception=True) + sv_id = validator.validated_data.get('sous_vivier_id') + generics.get_object_or_404(SousVivier.objects, sv_id=sv_id) + + # calcul + calcul = self.__creer_calcul(sv_id) + if calcul.ca_statut == Statut.EN_COURS: + try: + calcul = self.__executer_calcul(sv_id, calcul.ca_date_debut) + finally: + self.__appeler_api_depiler_calculs(request) + + return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN}) + except (Http404, APIException): + raise + except SystemExit: + raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, 'calcul arrêté') + except: + message = 'impossible de lancer le calcul' + logger.exception(message) + raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message) + +class ArretCalul(APIView): + """ + Cette classe est dédiée a l'arret du calcul. + """ + def post(self, request): + """La fonction post change le statut d'un calcul à EN_ATTENTE_ARRET + + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier + + :return: - **return** (*json*): json contenant le statut modifié du calcul. + """ + sv_id = request.data['sous_vivier_id'] + calcul = Calcul.objects.get(sous_vivier_id=sv_id) + if Calcul.objects.filter(sous_vivier_id=sv_id).exists() and calcul.ca_statut == 'EN_COURS': + calcul.ca_statut = 'EN_ATTENTE_ARRET' + calcul.save() + elif Calcul.objects.filter(sous_vivier_id=sv_id).exists() and calcul.ca_statut == 'EN_ATTENTE': + calcul.ca_statut = 'ARRETER' + calcul.save() + return JsonResponse( + {"statut": calcul.ca_statut}) + +# Vue pour la fiche détaillée des administrés +class FicheDetailleeView(APIView): + + """ + Cette classe est dédiée au vue de la fiche détaillée des administrés + """ + def get(self, request): + """La fonction get recupére les infos administrés + + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant de l'administré + + :return: - **Response** (*objet*): informations concernant l'administré, informations sur le formulaire de mobilité. + """ + res = {"result": "error"} + if 'administre_id' in request.query_params: + administre_id = request.query_params['administre_id'] + try: + administre = Administre.objects.get(a_id_sap=administre_id) + res = model_to_dict(administre) + + fmobs = FMOB.objects.filter(administre_id=administre_id).order_by('-fmob_millesime') + + res["fmob"] = None + + if fmobs.exists(): + fmob_dict = model_to_dict(fmobs[0]) + res["fmob"] = fmob_dict + + res["fe"] = None + fe = list(FormationEmploi.objects.filter(fe_code=administre.formation_emploi_id).values( + *('fe_code', 'garnison__gar_code_postal', 'garnison__gar_lieu', 'fe_libelle'))) + # if fe.exists(): + # fe = fe[0] + # garnison_dict = model_to_dict() + # res["garnison"] = garnison_dict + if len(fe): + res["fe"] = fe[0] + res["conjoint"] = None + conjoint = Administre.objects.filter(a_id_sap=administre.a_sap_conjoint) + if conjoint.exists(): + print('has conjoint') + conjoint_dict = model_to_dict(conjoint[0]) + conjoint_dict["fe"] = None + fe_conjoint = list(FormationEmploi.objects.filter(fe_code=conjoint[0].formation_emploi_id).values( + *('fe_code', 'garnison__gar_code_postal', 'garnison__gar_lieu', 'fe_libelle'))) + if len(fe): + conjoint_dict["fe"] = fe_conjoint[0] + res["conjoint"] = conjoint_dict + + except Exception as e: + print(e) + print(e.args) + print(res) + # TODO: return error + + return Response(res) + +# Vue de calcul des indicateurs +# Renvoie les indicateurs pour les vues : +# - taux armement FE pour les PCP +# - taux armement FE pour les gestionnaires +# - suivi pam des gestionnaires pour les administrés +# - suivi pam des gestionnaires pour les postes +class ReportingView(APIView): + """ + Cette classe est dédiée au vue du calcul des indicateurs + - taux armement FE pour les PCP + - taux armement FE pour les gestionnaires + - suivi pam des gestionnaires pour les administrés + - suivi pam des gestionnaires pour les postes + + """ + def get(self, request): + """La fonction get renvoie les indicateurs + + :type request: rest_framework.request.Request + :param request: Request contenant le type, sous_vivier id, niveau fonctinonel et l'id de la formation emploi. + + :return: - **return** (*json*): Si le type est SUIVI_PAM_GESTIONNAIRE le json contiendra l'information sur le suivi pam des + gestionnaires pour les administrés et les postes. Si le type est TAUX_ARMEMENT_FE le json contiendra les indicateurs sur les taux d'armement. + + + """ + type = request.query_params["type"] + + sv_id = [] + if 'sv_id' in request.query_params: + sv_id = request.query_params["sv_id"] + + if 'vue' in request.query_params: + vue = request.query_params["vue"] + + if "nf" in request.query_params: + nf = request.query_params.getlist("nf") + else: + nf = ['1A', '1B', '1C', '2.', '3A', '3B', '3B NFS', '4.', '5A', '5B', '5C', '6A', '6B'] + + if 'fe_id' in request.query_params: # a voir + fe_id = request.query_params.getlist('fe_id') + else: + fe_id = list(FormationEmploi.objects.values_list('fe_code', flat=True)) + + # Vues pour les indicateurs sur le suivi du pam + if (type == "SUIVI_PAM_GESTIONNAIRE"): + if "d_code" in request.query_params: + d_id = request.query_params.getlist("d_code") + else: + filieres = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id', + flat=True).distinct()) + d_id = list(Filiere.objects.filter(f_code__in=filieres).values_list('domaine_id', flat=True).distinct()) + if "f_code" in request.query_params: + f_id = request.query_params.getlist("f_code") + else: + f_id = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id', + flat=True).distinct()) + if "categorie" in request.query_params: + categorie = request.query_params.getlist("categorie") + else: + categorie = list( + SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('sva_categorie', + flat=True).distinct()) + # résultats pour la vue suivi pam des gestionnaires pour les administrés + nb_a_etudier, nb_a_muter, nb_a_maintenir, nb_non_etudie_administres, nb_a_partant, nb_a_non_dispo, nb_prepos_administres, nb_pos_administres, nb_omi_active_administres, nb_omi_en_cours_administres, reste_a_realiser_administres, reste_a_realiser_a_etudier, reste_a_realiser_a_muter = reporting_suivi_pam_admin( + sv_id, f_id, d_id, nf, categorie) + # résultats pour la vue suivi pam des gestionnaires pour les postes + nb_p1, nb_p2, nb_p3, nb_p4, nb_gele, nb_non_etudie_postes, reste_a_realiser_postes, reste_a_realiser_p1, reste_a_realiser_p2, reste_a_realiser_p3, reste_a_realiser_p4, nb_prepos_postes, nb_pos_postes, nb_omi_active_postes, nb_omi_en_cours_postes = reporting_suivi_pam_poste( + sv_id, f_id, d_id, nf, categorie) + return JsonResponse( + {"nb_a_etudier": nb_a_etudier, "nb_a_muter": nb_a_muter, "nb_a_maintenir": nb_a_maintenir, + "nb_a_partant": nb_a_partant, "nb_a_non_dispo": nb_a_non_dispo, + "nb_non_etudie_administres": nb_non_etudie_administres, + "nb_prepos_administres": nb_prepos_administres, "nb_pos_administres": nb_pos_administres, + "nb_omi_en_cours_administres": nb_omi_en_cours_administres, + "nb_omi_active_administres": nb_omi_active_administres, + "reste_a_realiser_administres": reste_a_realiser_administres, + "reste_a_realiser_a_etudier": reste_a_realiser_a_etudier, + "reste_a_realiser_a_muter": reste_a_realiser_a_muter, + "nb_p1": nb_p1, "nb_p2": nb_p2, "nb_p3": nb_p3, "nb_p4": nb_p4, "nb_gele": nb_gele, + "nb_non_etudie_postes": nb_non_etudie_postes, "reste_a_realiser_postes": reste_a_realiser_postes, + "reste_a_realiser_p1": reste_a_realiser_p1, "reste_a_realiser_p2": reste_a_realiser_p2, + "reste_a_realiser_p3": reste_a_realiser_p3, "reste_a_realiser_p4": reste_a_realiser_p4, + "nb_prepos_postes": nb_prepos_postes, "nb_pos_postes": nb_pos_postes, + "nb_omi_en_cours_postes": nb_omi_en_cours_postes, "nb_omi_active_postes": nb_omi_active_postes}, + safe=False) + # Vues pour les indicateurs sur les taux d'armement + elif (type == 'TAUX_ARMEMENT_FE'): + if "d_code" in request.query_params: + d_id = request.query_params.getlist("d_code") + else: + d_id = list(Domaine.objects.all().values_list('d_code', flat=True)) + if "f_code" in request.query_params: + f_id = request.query_params.getlist("f_code") + else: + f_id = list(Filiere.objects.all().values_list('f_code', flat=True)) + if "categorie" in request.query_params: + categorie = request.query_params.getlist("categorie") + else: + categorie = ['MDR', 'SOFF', 'OFF', 'OGX'] + + if len(sv_id) == 0: + # résultats pour la vue taux armement FE pour les PCP + nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_pcp( + fe_id, f_id, d_id, nf, categorie) + else: + # résultats pour la vue taux armement FE pour les gestionnaires + nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_gestionnaire( + fe_id, f_id, d_id, nf, categorie, sv_id) + return JsonResponse({'nb_militaires_actuel': nb_militaires_actuel, "nb_postes_actuel": nb_postes_actuel, + 'ecart_actuel': ecart_actuel, 'taux_armement_actuel': taux_armement_actuel, + 'nb_militaires_entrants': nb_militaires_entrants, + 'nb_militaires_sortants': nb_militaires_sortants, + 'nb_militaires_projete': nb_militaires_projete, + 'taux_armement_projete': taux_armement_projete, + 'taux_armement_cible': taux_armement_cible}, safe=False) + else: + return JsonResponse({"autre vue": "autres indicateurs demandes"}) + + +# Vue pour afficher le nombre de postes vacants par fe et sous-vivier +class PosteVacantView(APIView): + """ + Cette classe est dédiée au vue pour afficher le nombre de postes vacants par fe et sous-vivier + """ + + def get(self, request): + """La fonction get renvoie les postes vaccants + + :type request: rest_framework.request.Request + :param request: Requset contenant l'identifiant de la formation emploi et l'identifiant du sous-vivier. + + + :return: - **return** (*json*): json contenant le nombre de poste vaccant du sous vivier. + """ + + fe_id = request.query_params["fe_id"] + sv_id = request.query_params["sv_id"] + nb_poste_vacant_vivier = poste_vacant_vivier(sv_id, fe_id) + return JsonResponse({'nb_poste_vacant_vivier': nb_poste_vacant_vivier}, safe=False) + + +# Vue de chargement des sous-viviers : +# - Charge et traite le fichier de définition des sous-viviers +# - Attribue les sous-viviers présents aux administrés et postes correspondants +class ChargementSVView(APIView): + """ + Cette classe est dédiée au vue de chargement des sous-viviers. Charge et traite le fichier de définition des sous-viviers et attribue les sous-viviers présents aux administrés et postes correspondants + """ + + serializer_class = FileSVSerializer + + def get(self, request): + """La fonction get renvoie une reponse contenant ok + + :type request: rest_framework.request.Request + :param request: Requset + + + :return: - **return** (*json*): json contenant "ok". + """ + return Response({"get": "ok"}) + + def post(self, request): + """La fonction post charge les sous-viviers + + :type request: rest_framework.request.Request + :param request: Request contenant le fichier SV + + :return: - **Response** (*Response*): Reponse contient la liste des sous-viviers créés, ignorés et où il y a eu une erreur. + """ + serializer = FileSVSerializer(data=request.data) + + if not serializer.is_valid(): + return Response( + data=serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + sv_file = request.data['SV'] + + sv_df = open_excel(sv_file, sheetname="GESTIONNAIRES", engine='openpyxl') + + sv = pd.DataFrame(columns=['gestionnaire_id_sap', 'f_code', 'asso_sv_categorie', 'arme']) + + for i in range(len(sv_df)): + sv.loc[i] = [sv_df.iloc[i, 0], sv_df.iloc[i, 3], sv_df.iloc[i, 4], sv_df.iloc[i, 5]] + + sv.dropna(subset=['f_code'], inplace=True) + sv['arme'] = sv['arme'].replace({np.nan: None}) + sv.reset_index(drop=True, inplace=True) + sv['asso_sv_categorie'].replace({"SOUS-OFFICIER": "SOFF", "OFFICIER": "OFF", "MILITAIRE DU RANG": "MDR"}, + inplace=True) + sv['sv_id'] = sv.f_code + '_' + sv.asso_sv_categorie + sv.f_code = sv.f_code.apply(lambda x: x.split(',')) + sv = sv.explode(column='f_code') + sv.reset_index(drop=True, inplace=True) + errors = [] + created = [] + ignored = [] + sv.fillna(np.nan, inplace=True) + sv.replace([np.nan], [None], inplace=True) + print(sv.head()) + + for i in range(len(sv)): + try: + filiere = Filiere.objects.get(f_code=sv.at[i, "f_code"]) + categorie = sv.at[i, "asso_sv_categorie"] + asso_sv = SousVivierAssociation.objects.filter(sva_categorie=categorie, + filiere__f_code=sv.at[i, "f_code"]) + + # Vérifier que filiere et catégorie n'ont pas déjà de sous vivier + if asso_sv.count() == 0 and categorie is not None: + # Créer le sous-vivier correspondant dans la table SousVivier + sous_vivier = SousVivier(sv_id=sv.at[i, "sv_id"], + sv_libelle=str(sv.at[i, "sv_id"])) + print(model_to_dict(sous_vivier)) + sous_vivier.save() + + print(sv.at[i, "sv_id"]) + + # Insertion du nouveau sous-vivier dans la table d'association (sans le droit arme dans un premier temps) + sva = SousVivierAssociation(sva_id=i, sous_vivier_id=sous_vivier.sv_id, filiere_id=filiere.f_code, + sva_categorie=categorie, sva_arme=sv.at[i, "arme"]) + sva.save() + + # Mise à jour du sous_vivier_id pour les postes et administres ayant la filiere et la categorie en cours + Administre.objects.filter(a_categorie=categorie, a_filiere_id=filiere.f_code).update( + sous_vivier_id=sous_vivier.sv_id) + Poste.objects.filter(p_categorie=categorie, p_filiere_id=filiere.f_code).update( + sous_vivier_id=sous_vivier.sv_id) + created.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"])) + + else: + ignored.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"])) + # Si l'association appartient déjà à un sous_vivier, on ne fait rien + except Filiere.DoesNotExist: + errors.append("Filiere " + sv.at[i, 'f_code'] + "non retrouvée") + + return Response({"created": created, "ignored": ignored, "errors": errors}) + + +# Vue de chargement des competences : +# - Charge et traite les fichiers de compétences +# - Attribue les compétences présentes aux administrés et postes correspondants +class ChargementCompetenceView(APIView): + """ + Cette classe est dédiée au vue de chargement des competences. Charge et traite les fichiers de compétences. + Attribue les compétences présentes aux administrés et postes correspondants + + """ + serializer_class = FileCompetenceSerializer + + def get(self, request): + """La fonction get renvoie une reponse contenant ok + + :type request: rest_framework.request.Request + :param request: Requset + + + :return: - **return** (*json*): json contenant "ok". + """ + return Response({"get": "ok"}) + + def post(self, request): + """ La fonction post charge les competences. + + :type request: rest_framework.request.Request + :param request: Request contenant le fichier de competence + + :return: - **Response** (*Response*): Reponse contient un message de la réussite pour charger la compétence . + """ + serializer = FileCompetenceSerializer(data=request.data) + + if not serializer.is_valid(): + return Response( + data=serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + competence_file_v1 = request.data['competence_v1'] + competence_file_v2 = request.data['competence_v2'] + competence_poste_v1 = open_excel(competence_file_v1, sheetname='Feuil1', engine='openpyxl') + competence_poste_v1.drop_duplicates(subset=['Macro compétence (0 à n par officier)'], inplace=True) + competence_mili = open_excel(competence_file_v2, sheetname="MILITAIRE", engine='openpyxl') + competence_mili.reset_index(drop=True, inplace=True) + competence_poste = open_excel(competence_file_v2, sheetname='REO', engine='openpyxl') + competence_poste.dropna(subset=['COMPETENCE 1'], inplace=True) + competence_poste.reset_index(drop=True, inplace=True) + + print("Preprocessing OK") + + # Ajout des compétences du fichier version 1, dans la table Compétence + for i in range(len(competence_poste_v1)): + comp = Competence(comp_id=cleanString(competence_poste_v1.at[i, 'Macro compétence (0 à n par officier)']), + comp_libelle=competence_poste_v1.at[i, 'Macro compétence (0 à n par officier)']) + comp.save() + + print("Ajout des compétences du fichier 1 dans la table Competence --> OK") + + # Ajout des compétences du fichier version 2, dans la table Compétence + competence_mili_1 = competence_mili[['COMPETENCE 1']].drop_duplicates().reset_index(drop=True) + competence_mili_2 = competence_mili[['COMPETENCE 2']].drop_duplicates().reset_index(drop=True).rename( + columns={'COMPETENCE 2': "COMPETENCE 1"}) + competence_mili_3 = competence_mili[['COMPETENCE 2.1']].drop_duplicates().reset_index(drop=True).rename( + columns={'COMPETENCE 2.1': "COMPETENCE 1"}) + competence_poste_1 = competence_poste[['COMPETENCE 1']].drop_duplicates(subset=["COMPETENCE 1"]).reset_index( + drop=True) + total_competences = pd.concat([competence_mili_1, competence_mili_2, competence_mili_3, + competence_poste_1]).drop_duplicates().reset_index(drop=True) + total_competences.dropna(inplace=True) + total_competences.reset_index(drop=True, inplace=True) + + for i in range(len(total_competences)): + competence = Competence(comp_id=cleanString(total_competences.at[i, "COMPETENCE 1"]), + comp_libelle=total_competences.at[i, 'COMPETENCE 1']) + competence.save() + + print('Ajout des compétences du fichier 2 dans la table Competence --> OK') + + # Ajout des compétences du fichier 2 dans la table Poste + print(competence_poste) + for i in range(len(competence_poste)): + postes = Poste.objects.filter(p_code_fonction=competence_poste.at[i, 'CODE FONCTION'], + formation_emploi_id=competence_poste.at[i, 'CODE FE'], + p_domaine=competence_poste.at[i, 'DOM EIP'], + p_filiere=competence_poste.at[i, "FIL EIP"], + p_nf=competence_poste.at[i, 'NR EIP']) + + postes.update(competence_id=cleanString(competence_poste.at[i, "COMPETENCE 1"])) + print('Ajout des compétences du fichier 2 dans la table Poste --> OK') + # Ajout des compétences du fichier 2 dans la table d'association Administrés et Compétences + liste_admin = [] + for i in range(len(competence_mili)): + sap = competence_mili.at[i, 'N SAP'] + admin_competences = "" + + if not pd.isna(competence_mili.at[i, 'COMPETENCE 1']): + ca1_id = cleanString(competence_mili.at[i, 'COMPETENCE 1']) + admin_competences = admin_competences + ca1_id + "," + + if not pd.isna(competence_mili.at[i, 'COMPETENCE 2']): + ca2_id = cleanString(competence_mili.at[i, 'COMPETENCE 2']) + admin_competences = admin_competences + ca2_id + "," + + if not pd.isna(competence_mili.at[i, 'COMPETENCE 2.1']): + ca3_id = cleanString(competence_mili.at[i, 'COMPETENCE 2.1']) + admin_competences = admin_competences + ca3_id + "," + + liste_admin.append(Administre(a_id_sap=sap, a_liste_id_competences=admin_competences)) + + Administre.objects.bulk_update(liste_admin, fields=['a_liste_id_competences']) + print('Ajout des compétences du fichier 2 dans la table CompetenceAdministre --> OK') + + return JsonResponse({"msg": "Chargement competence ok"}) + + +class CurrentUserView(APIView): + """ + Cette classe est dédiée au vue de l'utilisateur courant: + """ + + def get(self, request): + """La fonction get verifie si l'utilisateur est authentifié + + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant de l'utilisateur + + + :return: - **Response** (*json*): json contenant l'identité et le role de l'utilisateur. En plus de la formation emploi group et le sous-vivier specifique à cet utilisateur. + """ + if request.user.is_authenticated: + current_user = CustomUser.objects.get(id=request.user.id) + try: + administre = model_to_dict(current_user.administre) + except: + administre = None + current_user = model_to_dict(current_user, fields=[field.name for field in CustomUser._meta.get_fields() if + field.name not in ["password", "logentry", "groups", + "user_permissions"]]) + current_user['sous_viviers'] = list(SousVivier.objects.filter(gestionnaire_id=request.user.id).values()) + current_user['identite'] = administre + try: + pcpFeGroupe = model_to_dict(PcpFeGroupe.objects.get(gestionnaire_id=request.user.id)) + except: + pcpFeGroupe = None + + current_user['fe_groupe'] = pcpFeGroupe + current_user['role'] = "ADMINISTRATEUR" if request.user.is_superuser else "GESTIONNAIRE" + return Response(current_user) + return Response({"msg": "Authentification requise"}) + + +class ReferencesView(APIView): + + """ + Cette classe est dédiée au vue de la reference. + """ + def get(self, request): + """ + La fonction get envoie les references + + :return: - **JsonReponse** (*json*): json contenant les groupes marques, les marques, les competences, les domaines et les filieres. + """ + groupesMarques = [obj.as_dict() for obj in MarquesGroupe.objects.all().order_by('gm_code')] + marques = [obj.as_dict() for obj in Marque.objects.all().order_by('mar_code')] + competences = [obj.as_dict() for obj in Competence.objects.all().order_by('comp_id')] + domaines = [obj.as_dict() for obj in Domaine.objects.all().order_by('d_code')] + filieres = [obj.as_dict() for obj in Filiere.objects.all().order_by('domaine__d_code', 'f_code')] + # TODO: Vérifier la présence de l'ordre sur les postes' + grades = [obj.as_dict() for obj in Grade.objects.all()] + FEs = [obj.as_dict() for obj in FormationEmploi.objects.all().order_by('fe_code')] + + return JsonResponse( + {"groupesMarques": groupesMarques, "marques": marques, + "domaines": domaines, "filieres": filieres, + "grades": grades, "competences": competences, "FEs": FEs}, safe=False) + + +# Vue pour l'alimentation et l'update en BDD : +# - Charge et traite les fichiers de données +# - Remplit la base de données pour les modèles Domaines, Filieres, Grade, Fonction, Garnison, FormationEmploi, Marque, MarquesGroupe, Administre, Poste, FMOB, +class AlimentationView(APIView): + """ + Cette classe est dédiée au vue de l'alimentation et l'update en BDD. Charge et traite les fichiers de données et remplit la base de données pour les modèles Domaines, Filieres, Grade, Fonction, Garnison, FormationEmploi, Marque, MarquesGroupe, Administre, Poste, FMOB. + """ + serializer_class = FileSerializer + + def get(self, request): + """La fonction get renvoie une reponse contenant Formulaire d'alimentation d'OGURE NG + + :type request: rest_framework.request.Request + :param request: Request + + + :return: - **return** (*json*): json contenant "Formulaire d'alimentation d'OGURE NG". + """ + return Response("Formulaire d'alimentation d'OGURE NG") + + def post(self, request): + """La fonction post charge les fichiers. + + :type request: rest_framework.request.Request + :param request: Request contenant les fichiers + + :return: - **Response** (*Response*): Reponse contient les erreurs de chargement des données . + """ + # TODO: préparer une variable qui renvoie les erreurs sous 3 axes : données référentielles, administrés, postes + serializer = FileSerializer(data=request.data) + if not serializer.is_valid(): + return Response( + data=serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + logger.info('---------------------Upload begining---------------------------') + start_time = time.time() + + file_donnees_bo = request.data['Donnees_BO_ADT'] + file_zpropaf = request.data['ZPROPAF'] + file_reo = request.data['REO'] + file_fmob = request.data['FMOB'] + file_filiere_domaine = request.data['domaine_filiere'] + file_insee = request.data['insee_maping'] + file_diplomes = request.data['diplomes'] + # file_suivi_pam = request.data['suivi_pam'] + + logger.info('---------------------Upload ending-----------------------------') + logger.debug("--------------upload time -- %d seconds ------------------------", time.time() - start_time) + + # ETL from xlsx source : Ordre : domaines, filieres, garnisons, fonctions, grades, fe, groupesMarques, marques, gestionnaires, administres, flag_pam_admnistres, postes, fmob, listePreferences + filiere_domaine_df, donnees_bo_df, zpropaf_df, reo_df, fmob_df, femp_df, insee_df, diplomes_df = process_files( + file_filiere_domaine, + file_donnees_bo, + file_zpropaf, + file_reo, + file_fmob, + file_insee, + file_diplomes) + + logger.info('---------------------Insert beginning---------------------------') + start_time_insert = time.time() + + insert_Domaine(to_table_domaines(filiere_domaine_df)) + logger.debug('1. Domaine -----------> Success') + insert_Garnison(to_table_garnisons(insee_df, donnees_bo_df)) + logger.debug('2. Garnison ----------> Success') + insert_Fonction(to_table_fonctions(reo_df)) + logger.debug('3. Fonction ----------> Success') + insert_Grade() + logger.debug('4. Grade -------------> Success') + insert_Filiere(to_table_filieres(filiere_domaine_df)) + logger.debug('5. Filières -----------> Success') + insert_MarquesGroupe(to_table_groupesMarques()) + logger.debug('6. MarquesGroupe -----> Success') + insert_Marque(to_table_marques()) + logger.debug('7. Marque ------------> Success') + insert_FormationEmploi(to_table_fe(reo_df, donnees_bo_df)) + logger.debug('8. FormationEmplois ---> Success') + administres_list, administres_error = insert_Administre( + to_table_administres(donnees_bo_df, zpropaf_df, insee_df, diplomes_df)) + logger.debug('9. Administre --------> Success') + + postes_list = insert_Poste(to_table_postes(reo_df)) + logger.debug('10. Poste --------------> Success') + + logger.debug('time: %d s', time.time() - start_time_insert) + fmob_error = insert_FMOB(to_table_fmob(fmob_df, femp_df, zpropaf_df)) + logger.debug('11. Fmob ----------------> Success') + logger.info('---------------------Insert ending -----------------------------') + + logger.info('-------------------- Début des mises à jour --------------------') + # administre : le statut PAM par défaut est A_TRAITER s'il y a un FMOB + (Administre.objects + .filter(a_statut_pam=StatutPam.A_ETUDIER, fmobs__isnull=False) + .update(a_statut_pam=StatutPam.A_TRAITER)) + logger.info('--------------------- Fin des mises à jour ---------------------') + + logger.debug("------------------insert time -- %d seconds --------------------", time.time() - start_time_insert) + # # result = {"administres_error": administres_error, "fmob_error": fmob_error} + # result = {"administres_error": "", "fmob_error": fmob_error} + return Response({"administres_error": ""}) + + +class AlimentationReferentielView(APIView): + """ Vue pour alimenter la base à partir de référentiels """ + + serializer_class = AlimentationReferentielSerializer + + def get(self, request): + return Response("Formulaire d'alimentation d'OGURE NG (référentiel") + + def post(self, request): + """ + Charge le(s) fichier(s) et met à jour la base. + + :param request: requête, contient les fichiers + :type request: class:`rest_framework.request.Request` + + :raises: class:`rest_framework.exceptions.APIException` + :return: réponse + :rtype: class:`rest_framework.response.Response` + """ + + try: + validator = AlimentationReferentielSerializer(data=request.data) + validator.is_valid(raise_exception=True) + + # récupération des fichiers + referentiel_fe = request.data.get('referentiel_fe', None) + + if referentiel_fe: + def process_referentiel_fe(referentiel) -> pd.DataFrame: + """ + Fonction de lecture du référentiel de FE. + + :param referentiel: Fichier du référentiel FE + :type referentiel: XLSX + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + return open_excel(referentiel, sheetname=0, engine='openpyxl') + + def convertir_fe(df: pd.DataFrame) -> pd.DataFrame: + """ + Fonction de conversion du DataFrame de FE. + + :param df: DataFrame du référentiel FE + :type df: class:`pandas.DataFrame` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + + col_pk_avant = 'FE CREDO' + col_mere_avant = 'FE mère CREDO' + col_zone_def_avant = 'Zone de Défense' + + Cols = FormationEmploi.Cols + col_pk_apres = Cols.PK + col_mere_apres = Cols.REL_MERE + col_zone_def_apres = Cols.ZONE_DEFENSE + + return (df[[col_pk_avant, col_mere_avant, col_zone_def_avant]] + .drop_duplicates(subset=col_pk_avant, keep='first') + .rename(columns={ + col_pk_avant: col_pk_apres, + col_mere_avant: col_mere_apres, + col_zone_def_avant: col_zone_def_apres + })) + + def mettre_a_jour_fe(df: pd.DataFrame) -> None: + """ + Met à jour les FE base à partir du DataFrame de FE. + + :param df: DataFrame du référentiel FE + :type df: class:`pandas.DataFrame` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + + TypeModele = FormationEmploi + Cols = TypeModele.Cols + champs_maj = (Cols.REL_MERE, Cols.ZONE_DEFENSE) + modeles_en_base = { m.pk: m for m in TypeModele.objects.select_related(Cols.REL_MERE).only('pk', *champs_maj) } + + taille_batch = 100 + dict_create = {} + dict_update = {} + for rec in df.to_dict('records'): + pk = str(rec.get(Cols.PK, None)) + id_mere = rec.get(Cols.REL_MERE, None) + mere = modeles_en_base.get(str(id_mere), None) if id_mere else None + zone_defense = rec.get(Cols.ZONE_DEFENSE, None) + + en_base = modeles_en_base.get(pk, None) + if not en_base or (mere != getattr(en_base, Cols.REL_MERE, None) or zone_defense != getattr(en_base, Cols.ZONE_DEFENSE, None)): + try: + modele = TypeModele(pk=pk, mere=mere, zone_defense=zone_defense) + except Exception as e: + raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" a échoué') from e + + (dict_create if not en_base else dict_update).setdefault(pk, modele) + + # TODO pas de création pour l'instant (ni de suppression) + # if dict_create: + # FormationEmploi.objects.bulk_create(dict_create.values(), batch_size=taille_batch) + if dict_update and champs_maj: + FormationEmploi.objects.bulk_update(dict_update.values(), batch_size=taille_batch, fields=champs_maj) + logger.debug(f"MAJ: %s", len(dict_update)) + + df_referentiel_fe = process_referentiel_fe(referentiel_fe) + df_referentiel_fe = convertir_fe(df_referentiel_fe) + mettre_a_jour_fe(df_referentiel_fe) + + return Response({'message':'OK'}) + except (Http404, APIException): + raise + except: + message = "impossible d'alimenter le référentiel" + logger.exception(message) + raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message) + +# Vue API des administrés +# Pagination OK +# Ordering OK +# Search NOK +# Filtering NOK + +class AdministreView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue de l'administre. + """ + serializer_class = AdministreSerializer + + filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] + # filterset_fields = ['category', 'in_stock'] + filterset_class = AdministreFilter + ordering_fields = 'a_id_sap' + ordering = ['a_id_sap'] + pagination_class = HeavyDataPagination + + # important : mettre à jour quand le serializer change + def get_queryset(self): + Cols = Administre.Cols + return Administre.objects.select_related( + Cols.REL_FONCTION, Cols.REL_GRADE, + ).prefetch_related( + Cols.M2M_COMPETENCES, + Cols.O2M_FMOB, + Prefetch(Cols.REL_DECISION, queryset=Decision.objects.select_related('poste__formation_emploi__garnison')), + Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_GARNISON, FormationEmploi.Cols.REL_MERE)), + Prefetch(Cols.REL_SOUS_VIVIER, queryset=SousVivier.objects.select_related(SousVivier.Cols.REL_GESTIONNAIRE)), + ) + + @atomic + def put(self, request): + """La fonction put met à jour une liste d'administres. + + :type request: rest_framework.request.Request + :param request: Request contenant la liste d'administres. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des administres . + """ + try: + req_data = request.data + is_list = isinstance(req_data, list) + validator = self.serializer_class(data=req_data, many=is_list, partial=True) + validator.is_valid(raise_exception=True) + + today = date.today() + Cols = Administre.Cols + + def copy_data_item(input, pk): + """Copie et complète si besoin le dictionnaire en entrée""" + result = {**input} + if pk and Cols.PK not in result: + result[Cols.PK] = pk + return result + + if is_list: # Update multiple elements + data = [copy_data_item(item, req_data[idx][Cols.PK]) for idx, item in enumerate(validator.validated_data)] + fields = [key for key in data[0].keys() if key != Cols.PK] + objs = [Administre(**data_item) for data_item in data] + + if Cols.STATUT_PAM in fields: + list_error = [] + for i in range(len(objs)): + old_administre = Administre.objects.get(a_id_sap=objs[i].a_id_sap) + administre = objs[i] + eip = administre.a_eip + fe_code = old_administre.formation_emploi_id + avis = administre.a_statut_pam + old_avis = old_administre.a_statut_pam + try: + categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] + except: + erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' + print(erreur) + raise Exception(erreur) + + liste_error = impact_decisions(old_administre, administre, old_avis, avis, eip, fe_code, categorie) + + Administre.objects.bulk_update(objs, fields=fields) + + else: # Update one element + data = copy_data_item(validator.validated_data, validator.validated_data[Cols.PK]) + # c'est un bug la récupération de a_id_sap en-dessous, non ? + a_id_sap = Administre.objects.update(data) + db_instance = Administre.objects.filter(a_id_sap=a_id_sap).first() + db_instance.tag.clear() + + return Response({'msg': 'updated'}) + except APIException: + raise + except Exception: + message = 'échec de la mise à jour de N administrés' + logger.exception(message) + raise APIException(message) + + def partial_update(self, request, pk=None): + """ La fonction put met à jour un administre. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre. + + :type pk: integer + :param pk: Primary Key de l'administre. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour de l'administre . + """ + try: + logger.debug("Mise à jour administré") + req_data = request.data + + # TODO valider les données (il faut d'abord corriger la MAJ des compétences) + # validator = self.serializer_class(data=req_data, partial=True) + # validator.is_valid(raise_exception=True) + + Cols = Administre.Cols + + # Copie et complète si besoin le dictionnaire en entrée + # administre = {**validator.validated_data} + administre = {**req_data} + administre[Cols.PK] = pk + + fields = [key for key in administre.keys() if key != Cols.PK] # and key != 'a_liste_id_competences'] + + if 'a_domaine_futur' in fields: + administre['a_domaine_futur_id'] = administre['a_domaine_futur'] + del administre['a_domaine_futur'] + + if 'a_filiere_futur' in fields: + administre['a_filiere_futur_id'] = administre['a_filiere_futur'] + del administre['a_filiere_futur'] + + if Cols.STATUT_PAM in fields: + old_administre = Administre.objects.get(a_id_sap=pk) + eip = old_administre.a_eip + fe_code = old_administre.formation_emploi_id + avis = administre[Cols.STATUT_PAM] + old_avis = old_administre.a_statut_pam + try: + categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] + except: + erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' + print(erreur) + raise APIException(erreur) + + liste_error = impact_decisions(old_administre, Administre(**administre), old_avis, avis, eip, fe_code, + categorie) + + + if 'a_liste_id_competences' in fields: + adm=Administre(pk=pk) + #adm=request.data['a_liste_id_competences'] + if administre['a_liste_id_competences']: + adm.a_liste_id_competences.set(administre['a_liste_id_competences'].split(',')) + else: + adm.a_liste_id_competences.set("") + + else: + copy = { k: v for k, v in administre.items() if k != 'a_liste_id_competences'} + adm = Administre(**copy) + + if fields: + Administre.objects.bulk_update([adm], fields=fields) + + return Response({'msg': 'updated'}) + + except (Http404, APIException): + raise + except Exception: + message = "échec de la mise à jour de l'administré" + logger.exception(message) + raise APIException(message) + + + +# GET /api/administres => 0 administrés +# POST /api/administres => Crée 1 nouvel administres => id=1 +# GET /api/administres/1 => 1 administre précédemment créé +# POST /api/administres/1 => 1 administre précédemment créé + + +class PosteView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des postes. + """ + lookup_value_regex = r"[\w.]+" + serializer_class = PosteSerializer + pagination_class = HeavyDataPagination + filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] + # filterset_fields = ['category', 'in_stock'] + filterset_class = PosteFilter + ordering_fields = '__all_related__' + ordering = ['p_id'] + + # important : mettre à jour quand le serializer change + def get_queryset(self): + """Cette fonction permet d'ajouter plus de logique à l'attribut queryset.""" + + Cols = Poste.Cols + return Poste.objects.select_related( + Cols.REL_FONCTION + ).prefetch_related( + Cols.M2M_COMPETENCES, + Cols.O2M_DECISION, + Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_GARNISON, FormationEmploi.Cols.REL_MERE)), + Prefetch(Cols.REL_SOUS_VIVIER, queryset=SousVivier.objects.select_related(SousVivier.Cols.REL_GESTIONNAIRE)), + ).annotate( + p_nb_prepositionne=Sum( + Case(When(decisions__de_decision=constants.DECISION_PREPOSITIONNE, then=1), default=0, + output_field=IntegerField())), + p_nb_positionne=Sum(Case(When(decisions__de_decision=constants.DECISION_POSITIONNE, then=1), default=0, + output_field=IntegerField())), + p_nb_omi_en_cours=Sum(Case(When(decisions__de_decision=constants.DECISION_OMI_EN_COURS, then=1), default=0, + output_field=IntegerField())), + p_nb_omi_active=Sum(Case(When(decisions__de_decision=constants.DECISION_OMI_ACTIVE, then=1), default=0, + output_field=IntegerField())), + ) + + def put(self, request): + """La fonction put met à jour une liste des postes. + + :type request: rest_framework.request.Request + :param request: Request contenant la liste des postes. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des postes . + """ + data = request.data + serialized = self.serializer_class(data=data, many=isinstance(data, list), partial=True) + serialized.is_valid(raise_exception=True) + if isinstance(data, list): # Update multiple elements + try: + fields = [key for key in data[0].keys() if key != 'p_id' ] + objs = [Poste(**data[i]) for i in range(len(data))] + + except: + raise Exception("bulk update error") + + Poste.objects.bulk_update(objs, fields=fields) + else: # Update one element + a_id_sap = Poste.objects.update(serialized.validated_data) + db_instance = Poste.objects.filter( + a_id_sap=a_id_sap).first() + db_instance.tag.clear() + + return Response({'msg': 'updated'}) + + def partial_update(self, request, pk=None): + """ La fonction put met à jour un poste. + + :type request: rest_framework.request.Request + :param request: Request contenant le poste. + + :type pk: integer + :param pk: Primary Key du poste. + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour du poste . + """ + poste = request.data + fields = [key for key in poste.keys() if key != 'p_id']# and key != 'competence'] + poste['p_id'] = pk + + if 'competence' in fields: + pos=Poste(pk=pk) + if poste['competence']: + pos.competences.set(poste['competence'].split(',')) + else: + pos.competences.set("") + else: + copy = { k: v for k, v in poste.items() if k != 'competence'} + pos = Poste(**copy) + + if fields: + Poste.objects.bulk_update([pos], fields=fields) + + + # copy = { k: v for k, v in poste.items() if k != 'competence'} + # pos = Poste(**copy) + + # if fields: + # Poste.objects.bulk_update([copy], fields=fields) + + # if 'competence' in poste.items(): + # pos.competence.set(poste['competence'].split(',')) + # pos.save() + + + return Response({'msg': 'updated'}) + + +class NotationView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des notations. + """ + serializer_class = NotationSerializer + queryset = Notation.objects.all() + pagination_class = HeavyDataPagination + + def list(self, request, pk=None): + """La fonction list envoie le classement des postes pour un administré particulier et inversement. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre ou le poste. + + :return: - **JsonResponse** (*JsonResponse*): Json contenant le classement. + """ + notations_list = [] + q = 'poste' + if 'administre_id' in request.query_params: + administre_id = request.query_params['administre_id'] + administres_keys = ( + 'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere', 'poste__p_eip', + 'poste__formation_emploi__fe_code', 'poste__competences', + 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', 'poste__p_code_fonction', + 'poste__p_avis', + 'poste__p_nb_p1', + 'poste__p_nb_p2', + 'poste__p_nb_p3', + 'poste__p_nb_p4', + 'poste__p_dep', + 'poste__formation_emploi__fe_libelle', 'poste__competences', 'poste__p_fonction', + 'poste__formation_emploi__garnison', + 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', + 'poste__formation_emploi__garnison__gar_lieu', 'no_score_administre', + 'no_flag_cple_ideal') + + counter = 10 + + notation_qs = Notation.objects.filter(administre_id=administre_id).order_by('-no_score_administre')[ + :counter].select_related('poste') + + notation_qs_matching_parfait = Notation.objects.filter(administre_id=administre_id, + no_flag_cple_ideal=True).select_related('poste') + + notation_qs.union(notation_qs_matching_parfait) + notation_qs = notation_qs | notation_qs_matching_parfait + notations_list = list(notation_qs.values(*administres_keys)) + + for notation in notations_list: + poste_id = notation['poste__p_id'] + topCounter = 0 + allNotationsInvolved = Notation.objects.filter(poste_id=poste_id) + for note in allNotationsInvolved: + topList = list( + Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre')[:10].values( + 'poste_id')) + topPostes = [poste['poste_id'] for poste in topList] + if poste_id in topPostes: + topCounter += 1 + + notation['poste__nb_top'] = topCounter + + if 'poste_id' in request.query_params: + q = "administre" + poste_id = request.query_params['poste_id'] + poste_unit = Poste.objects.get(p_id=poste_id) + try: + counter = poste_unit.p_nb_p1 + poste_unit.p_nb_p2 + poste_unit.p_nb_p3 + poste_unit.p_nb_p4 + if counter < 10: + counter = 10 + except: + counter = 10 + postes_keys = ( + 'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__a_statut_pam', + 'administre__grade_id', 'administre__a_liste_id_marques', 'administre__decision__de_decision', + 'administre__decision__de_date_decision', 'no_score_administre', 'no_flag_cple_ideal', 'administre__a_notes_gestionnaire', + 'administre__a_fonction', 'administre__a_code_fonction', + 'administre__a_liste_id_marques', + 'administre__a_liste_id_competences', + 'administre__decision__poste_id') + notation_qs = Notation.objects.filter(poste_id=poste_id).order_by('-no_score_administre')[ + :counter].select_related( + 'administre') + notation_qs_matching_parfait = Notation.objects.filter(poste_id=poste_id, + no_flag_cple_ideal=True).select_related('administre') + notation_qs.union(notation_qs_matching_parfait) + notations_list = list( + notation_qs.values(*postes_keys)) + + for notation in notations_list: + administre_id = notation['administre__a_id_sap'] + topCounter = 0 + allNotationsInvolved = Notation.objects.filter(administre_id=administre_id) + for note in allNotationsInvolved: + topList = list( + Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre')[:10].values( + 'administre_id')) + topAdministres = [administre['administre_id'] for administre in topList] + if administre_id in topAdministres: + topCounter += 1 + + notation['administre__nb_top'] = topCounter + + result = [] + for notation in notations_list: + res_notation = {q: {}} + for key in notation: + if (q + "__") in key: + res_notation[q][key.replace(q + '__', '')] = notation[key] + else: + res_notation[key] = notation[key] + if q == "poste": + res_notation[q]['p_nb_prepositionne'] = Decision.objects.filter( + de_decision=constants.DECISION_PREPOSITIONNE).count() + res_notation[q]['p_nb_positionne'] = Decision.objects.filter( + de_decision=constants.DECISION_POSITIONNE).count() + res_notation[q]['p_nb_omi_active'] = Decision.objects.filter( + de_decision=constants.DECISION_OMI_ACTIVE).count() + res_notation[q]['p_nb_omi_en_cours'] = Decision.objects.filter( + de_decision=constants.DECISION_OMI_EN_COURS).count() + result.append(res_notation) + return Response(result) + + +class DecisionView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des decisions. + """ + serializer_class = DecisionSerializer + queryset = Decision.objects.all() + + def list(self, request, pk=None): + """Cette fonction envoie les informations du poste lié à l'administre dans une décision avec le score de l'administre et inversement. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre ou le poste. + + :return: - **res_decision** (*JsonResponse*): Json contenant le classement. + """ + decision = [] + q = 'poste' + administre_id = None + poste_id = None + if 'administre_id' in request.query_params: + administre_id = request.query_params['administre_id'] + administres_keys = ( + 'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere', + 'poste__p_eip','poste__p_avis', + 'poste__formation_emploi__fe_code', 'poste__competences', + 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', + 'poste__formation_emploi__fe_libelle', 'poste__formation_emploi__garnison__gar_lieu', + 'poste__p_dep', 'poste__p_code_fonction', 'poste__competences', + 'poste__p_fonction', + # 'poste__decision__de_decision', + 'poste__p_nb_p1', + 'poste__p_nb_p2', + 'poste__p_nb_p3', + 'poste__p_nb_p4', + 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', + 'de_decision', 'de_date_decision', 'de_notes_gestionnaire') + decision = Decision.objects.filter(administre_id=administre_id).select_related('poste') + decision = list(decision.values(*administres_keys)) + + if 'poste_id' in request.query_params: + q = "administre" + poste_id = request.query_params['poste_id'] + postes_keys = ( + 'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__a_statut_pam', + 'administre__a_fonction', 'administre__a_code_fonction', 'administre__a_liste_id_competences', + 'administre__grade_id', 'administre__a_liste_id_marques', 'administre__decision__de_decision', + 'administre__decision__de_date_decision', 'de_decision', 'de_date_decision', + 'de_notes_gestionnaire', 'administre__a_notes_gestionnaire', 'administre__a_liste_id_competences', + 'administre__decision__poste_id') + + decision = Decision.objects.filter(poste_id=poste_id).select_related('administre') + + decision = list(decision.values(*postes_keys)) + res_decision = [] + if len(decision) > 0: + for k in range(len(decision)): + decision_unit = decision[k] + try: + administre_id = decision_unit.administre_id + except: + administre_id = None + + try: + poste_id = decision_unit.poste_id + except: + poste_id = None + + try: + res_decision_unit = {q: {}, 'no_score_administre': Notation.objects.get( + administre_id=administre_id, + poste_id=poste_id).no_score_administre} + except: + res_decision_unit = {q: {}, 'no_score_administre': None} + for key in decision_unit: + if (q + "__") in key: + res_decision_unit[q][key.replace(q + '__', '')] = decision_unit[key] + else: + res_decision_unit[key] = decision_unit[key] + # Ajout du relevé des décisions sur le poste (cas q = "poste") + if q == "poste": + res_decision_unit[q]['p_nb_prepositionne'] = Decision.objects.filter( + de_decision=constants.DECISION_PREPOSITIONNE).count() + res_decision_unit[q]['p_nb_positionne'] = Decision.objects.filter( + de_decision=constants.DECISION_POSITIONNE).count() + res_decision_unit[q]['p_nb_omi_active'] = Decision.objects.filter( + de_decision=constants.DECISION_OMI_ACTIVE).count() + res_decision_unit[q]['p_nb_omi_en_cours'] = Decision.objects.filter( + de_decision=constants.DECISION_OMI_EN_COURS).count() + res_decision.append(res_decision_unit) + return Response(res_decision) + + def create(self, request): # équivalent post + """Cette fonction crée une decision. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre, le poste et la decision. + + :return: - **response** (*JsonResponse*): Json contenant le message "Décision changée". + """ + result = {} + # TODO : try except + administre_id = request.data['administre_id'] + poste_id = request.data['poste_id'] + de_decision = request.data['de_decision'] + decision = Decision(administre_id=administre_id, poste_id=poste_id) + administre_decision = Decision.objects.filter(administre_id=administre_id) + # Lorsqu'on crée une décision on supprime toute décision précédente sur l'administré + # poste_decision = Decision.objects.filter(poste_id=poste_id) + if administre_decision.exists(): + administre_decision.delete() + + decision.de_decision = de_decision + decision.de_date_decision = timezone.now() + decision.save() + + poste = Poste.objects.get(p_id=poste_id) + poste.p_nb_vacant = poste.p_nb_vacant - 1 + poste.save() + + response = JsonResponse({'status': 'success', 'message': 'Décision changée'}) + + return response + + +class FormationEmploiView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des FormationEmplois. + """ + serializer_class = FormationEmploiSerializer + + # important : mettre à jour quand le serializer change + def get_queryset(self): + Cols = FormationEmploi.Cols + return FormationEmploi.objects.select_related(Cols.REL_GARNISON, Cols.REL_MERE) + + +class ListesPreferencesView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des ListesPreferences. + """ + serializer_class = PreferencesListe + queryset = PreferencesListe.objects.all() + + +class MarqueView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des marques. + """ + serializer_class = MarqueSerializer + queryset = Marque.objects.all() + + +class MarquesGroupeView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des marquegroupes. + """ + serializer_class = MarquesGroupeSerializer + queryset = MarquesGroupe.objects.all() + + +class FmobView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des Fmob. + """ + serializer_class = FmobSerializer + queryset = FMOB.objects.all() + + +class FiliereView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des filieres. + """ + serializer_class = FiliereSerializer + queryset = Filiere.objects.all() + + +class DomaineView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des domaines. + """ + serializer_class = DomaineSerializer + queryset = Domaine.objects.all() + + +class SousVivierAssociationView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue du SousVivierAssociation. + """ + serializer_class = SousVivierAssociationSerializer + queryset = SousVivierAssociation.objects.all() + + +class PcpFeGroupeView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue du PcpFeGroupe. + """ + serializer_class = PcpFeGroupeSerializer + queryset = PcpFeGroupe.objects.all() diff --git a/backend-django/backend/views/__init__.py b/backend-django/backend/views/__init__.py new file mode 100644 index 0000000..4a1972c --- /dev/null +++ b/backend-django/backend/views/__init__.py @@ -0,0 +1,16 @@ +from .alimentation import * +from .alimentation_commentaires import * +from .alimentation_ref_droits import * +from .alimentation_zones_geo import * +from .chargement_competences import * +from .current_user import * +from .decision import * +from .exportation_fichiers import * +from .fiche_detaillee import * +from .formation_emploi import * +from .initial import * +from .notation import * +from .references import * +from .scoring import * +from .suppression_administres import * +from .chargement_pam import * diff --git a/backend-django/backend/views/alimentation.py b/backend-django/backend/views/alimentation.py new file mode 100644 index 0000000..c489026 --- /dev/null +++ b/backend-django/backend/views/alimentation.py @@ -0,0 +1,405 @@ +from datetime import datetime +import time +import datetime + +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +import datetime + +from ..models import Administre, PAM +from ..models import StatutPamChoices as StatutPam +from ..serializers import AlimentationSerializer +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils.extraction.administre import (to_table_administres_bo) +from ..utils.insertion.administre import (insert_administre_bo, + update_administre_fmob) +from ..utils_extraction import (DataFrameTypes, FileTypes, read_files_by_type, + to_table_administre_notation, + to_table_affectation, to_table_diplome, + to_table_domaines, to_table_fe, + to_table_filieres, to_table_fmob_femp, + to_table_fmob_fmob, + to_table_fonctions, to_table_fud, + to_table_garnisons, to_table_groupesMarques, + to_table_marques, to_table_pam, to_table_postes, + to_table_ref_gest, to_table_ref_org, + to_table_ref_sv_fil, to_table_reo_ocv, + to_table_sous_vivier, + to_table_zone_geographique) +from ..utils_insertion import (insert_PAM, insert_administre_notation, insert_Affectation, + insert_Diplome, insert_Filiere, + insert_FMOB_femp, insert_FMOB_fmob, + insert_Fonction, + insert_FormationEmploi, insert_Fud, + insert_Garnison, insert_Grade, insert_Marque, + insert_MarquesGroupe, insert_Poste, + insert_RefFeMere, insert_RefGest, insert_RefOrg, + insert_RefSvFil, insert_SousVivier, + insert_SousVivier_instances, + insert_ZoneGeographique, update_domaine, + update_m2m_links_gestionnaire, update_poste_ocv, insert_delta) + +import pandas as pd + + +class AlimentationView(APIView): + """ + Cette page est l'alimentation principale d'Ogure. + Elle permet à l'administrateur de charger un ensemble de fichiers pour alimenter ou mettre à jour la base de données. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = AlimentationSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + def get(self, request): + """La fonction get renvoie une reponse contenant Formulaire d'alimentation d'OGURE NG + + :type request: rest_framework.request.Request + :param request: Request + + + :return: - **return** (*json*): json contenant "Formulaire d'alimentation d'OGURE NG". + """ + return Response("Formulaire d'alimentation d'OGURE NG") + + + @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def post(self, request): + """La fonction post charge les fichiers. + + :type request: rest_framework.request.Request + :param request: Request contenant les fichiers + + :return: - **Response** (*Response*): Reponse contient les erreurs de chargement des données . + """ + try: + # TODO: préparer une variable qui renvoie les erreurs sous 3 axes : données référentielles, administrés, postes + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + self.logger.info('---------------------Upload begining---------------------------') + start_time = time.time() + + file_donnees_bo = serializer.validated_data.get('Donnees_BO_ADT') + file_reo = serializer.validated_data.get('REO') + file_reo_suivant = serializer.validated_data.get('REO_PAM_SUIVANT') + file_reo_ocv = serializer.validated_data.get('REO_OCV') + file_ref_gest = serializer.validated_data.get('referentiel_gestionnaire') + file_ref_org = serializer.validated_data.get('referentiel_organique') + file_ref_sv_fil = serializer.validated_data.get('refeferentiel_sous_vivier_filiere') + file_ref_fe = serializer.validated_data.get('referentiel_fe') + file_fmob = serializer.validated_data.get('FMOB') + file_fmob_suivant = serializer.validated_data.get('FMOB_PAM_SUIVANT') + file_filiere_domaine = serializer.validated_data.get('domaine_filiere') + file_insee = serializer.validated_data.get('insee_maping') + file_diplomes = serializer.validated_data.get('diplomes') + file_fud = serializer.validated_data.get('FUD') + file_ref_zones_geo = serializer.validated_data.get('ref_zones_geo') + + self.logger.info('---------------------Upload ending-----------------------------') + self.logger.debug("--------------upload time -- %d seconds ------------------------", time.time() - start_time) + + df_by_type = read_files_by_type({ + FileTypes.BO: file_donnees_bo, + FileTypes.DIPLOME: file_diplomes, + FileTypes.DOM_FIL: file_filiere_domaine, + FileTypes.FMOB_FEMP: file_fmob, + FileTypes.FMOB_FEMP_PAM_SUIVANT: file_fmob_suivant, + FileTypes.FUD: file_fud, + FileTypes.INSEE: file_insee, + FileTypes.REF_FE: file_ref_fe, + FileTypes.REF_GEO: file_ref_zones_geo, + FileTypes.REF_GEST: file_ref_gest, + FileTypes.REF_ORG: file_ref_org, + FileTypes.REF_SV_FIL: file_ref_sv_fil, + FileTypes.REO: file_reo, + FileTypes.REO_PAM_SUIVANT: file_reo_suivant, + FileTypes.REO_OCV: file_reo_ocv, + }) + + DF = DataFrameTypes + diplomes_df = df_by_type.get(DF.DIPLOME) + donnees_bo_df = df_by_type.get(DF.BO) + femp_df = df_by_type.get(DF.FEMP) + filiere_domaine_df = df_by_type.get(DF.DOM_FIL) + fmob_df = df_by_type.get(DF.FMOB) + fud_df = df_by_type.get(DF.FUD) + insee_df = df_by_type.get(DF.INSEE) + ref_fe_df = df_by_type.get(DF.REF_FE) + ref_gest_df = df_by_type.get(DF.REF_GEST) + ref_org_df = df_by_type.get(DF.REF_ORG) + ref_sv_fil_df = df_by_type.get(DF.REF_SV_FIL) + ref_zones_geo_df = df_by_type.get(DF.REF_GEO) + reo_df = df_by_type.get(DF.REO) + reo_ocv_df = df_by_type.get(DF.REO_OCV) + + #dataframe pam + 1 + femp_suivant_df = df_by_type.get(DF.FEMP_PAM_SUIVANT) + fmob_suivant_df = df_by_type.get(DF.FMOB_PAM_SUIVANT) + reo_suivant_df = df_by_type.get(DF.REO_PAM_SUIVANT) + + + self.logger.info('-------------------- Insert beginning ---------------------') + start_time_insert = time.time() + text_response = [] + + + + if ref_sv_fil_df is not None: + sv_cree, sv_modifie, sv_erreur, sv_supprime = insert_SousVivier(to_table_sous_vivier(ref_sv_fil_df)) + text_response.append([f"Référentiel de sous-viviers/filières : {sv_cree} SousVivier créés, {sv_modifie} SousVivier mis à jour, {sv_erreur} SousVivier en erreur et {sv_supprime} SousVivier supprimés."]) + self.logger.info('Sous-viviers ---------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : sous-viviers (nécessite %s)', DF.REF_SV_FIL.value[1]) + + if ref_org_df is not None: + ref_org_cree, ref_org_modifie, ref_org_erreur, ref_org_supprime = insert_RefOrg(to_table_ref_org(ref_org_df)) + text_response.append([f"Référentiel organique : {ref_org_cree} RefOrg créés, {ref_org_modifie} RefOrg mis à jour, {ref_org_erreur} RefOrg en erreur et {ref_org_supprime} RefOrg supprimés."]) + self.logger.info('Référentiel organique ------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : référentiel organique') + + if ref_gest_df is not None: + ref_gest_cree, ref_gest_modifie, ref_gest_erreur, ref_gest_supprime, user_cree, user_modifie, user_supprime, user_ignore = insert_RefGest(to_table_ref_gest(ref_gest_df)) + text_response.append([f"Référentiel de gestionnaires : {ref_gest_cree} RefGest créés, {ref_gest_modifie} RefGest mis à jour, {ref_gest_erreur} RefGest en erreur et {ref_gest_supprime} RefGest supprimés."]) + text_response.append([f"Référentiel de gestionnaires : {user_cree} User créés, {user_modifie} User mis à jour, {user_supprime} User supprimés et {user_ignore} User ignorés."]) + self.logger.info('Référentiel gestionnaire ---------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : référentiel de gestionnaires') + + if ref_sv_fil_df is not None: + ref_sv_cree, ref_sv_delete, ref_sv_erreur, ref_sv_ignore = insert_RefSvFil(to_table_ref_sv_fil(ref_sv_fil_df)) + text_response.append([f"Référentiel de sous-viviers/filières : {ref_sv_cree} RefSvFil créés, {ref_sv_delete} RefSvFil supprimés, {ref_sv_erreur} RefSvFil en erreur et {ref_sv_ignore} RefSvFil ignorés."]) + self.logger.info('Référentiel sous-vivier filière --------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : référentiel de sous-viviers/filières') + + if ref_gest_df is not None or ref_org_df is not None or ref_sv_fil_df is not None: + update_m2m_links_gestionnaire('SV') + self.logger.info('Liens gestionnaires/sous-viviers -------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : liens gestionnaires/sous-viviers (nécessite %s ou %s ou %s)', + DF.REF_GEST.value[1], DF.REF_ORG.value[1], DF.REF_SV_FIL.value[1]) + + if filiere_domaine_df is not None: + update_domaine(to_table_domaines(filiere_domaine_df)) + self.logger.info('Domaine --------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : domaines (nécessite %s)', DF.DOM_FIL.value[1]) + + if insee_df is not None and donnees_bo_df is not None: + gar_cree, gar_maj , error = insert_Garnison(to_table_garnisons(insee_df, donnees_bo_df)) + text_response.append([f"INSEE et Données BO : {gar_cree} garnisons crées, {gar_maj} garnisons mises à jour, {error} garnisons en erreur."]) + self.logger.info('Garnison -------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : garnisons (nécessite %s + %s)', DF.INSEE.value[1], DF.BO.value[1]) + + if reo_df is not None: + reo_cree = insert_Fonction(to_table_fonctions(reo_df)) + text_response.append([f"REO : {reo_cree} fonctions de postes créés."]) + self.logger.info('Fonction -------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : fonctions (nécessite %s)', DF.REO.value[1]) + + if reo_suivant_df is not None: + reo_suivant_cree = insert_Fonction(to_table_fonctions(reo_suivant_df)) + text_response.append([f"REO : {reo_suivant_cree} fonctions de postes A+1 créés."]) + self.logger.info('Fonction A+1 -------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : fonctions A + 1 (nécessite %s)', DF.REO_PAM_SUIVANT.value[1]) + + + # TODO if .... + insert_Grade() + self.logger.info('Grade ----------------------------------------------> Succès') + + if filiere_domaine_df is not None: + dom_fil_cree, dom_fil_modifie, dom_fil_erreur, dom_fil_supprime = insert_Filiere(to_table_filieres(filiere_domaine_df)) + text_response.append([f"Domaines - filières : {dom_fil_cree} couples domaine/filière créés, {dom_fil_modifie} couples domaine/filière mis à jour, {dom_fil_erreur} couples domaine/filière en erreur et {dom_fil_supprime} couples domaine/filière supprimés."]) + self.logger.info('Filières ------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : filières (nécessite %s)', DF.DOM_FIL.value[1]) + + # TODO if .... + insert_MarquesGroupe(to_table_groupesMarques()) + self.logger.info('MarquesGroupe -------------------------------------> Succès') + + # TODO if .... + insert_Marque(to_table_marques()) + self.logger.info('Marque --------------------------------------------> Succès') + + if ref_fe_df is not None: + df = to_table_fe(ref_fe_df) + self.logger.debug('Extraction des données du référentiel FE ----------> Succès') + fe_cree, fe_maj, error_count = insert_FormationEmploi(df) + text_response.append([f"Référentiel FE : {fe_cree} formation d'emplois créées, {fe_maj} formation d'emplois mises à jour, {error_count} formation d'emplois en erreur."]) + self.logger.info('FormationEmplois ----------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : formations-emplois (nécessite %s)', DF.REF_FE.value[1]) + + if ref_fe_df is not None: + insert_RefFeMere(ref_fe_df) + self.logger.info('Référentiel FE mère -------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : formations-emplois mères (nécessite %s)', DF.REF_FE.value[1]) + + if ref_gest_df is not None or ref_org_df is not None or ref_fe_df is not None: + update_m2m_links_gestionnaire('FE') + self.logger.info('Liens gestionnaires/formations-emplois ------------> Succès') + else: + self.logger.info('Mise à jour ignorée : liens gestionnaires/formations-emplois (nécessite %s ou %s ou %s)', + DF.REF_GEST.value[1], DF.REF_ORG.value[1], DF.REF_FE.value[1]) + + if donnees_bo_df is not None: + bo_adm_cree, bo_adm_modifie, bo_adm_deja_modifie, bo_adm_erreur, bo_adm_ignore, bo_dom_ignore, bo_fil_ignore = insert_administre_bo(to_table_administres_bo(donnees_bo_df)) + text_response.append([f"Données BO : {bo_adm_cree} administrés créés, {bo_adm_modifie} administrés mis à jour, {bo_adm_deja_modifie} adminsitrés déjà à jour, {bo_adm_erreur} administrés en erreur, {bo_adm_ignore} administrés ignorés, {bo_dom_ignore} domaines ignorés et {bo_fil_ignore} filières ignorées."]) + self.logger.info('Administre ----------------------------------------> Succès') + + else: + self.logger.info('Mise à jour ignorée : administrés (nécessite %s)', DF.BO.value[1]) + + if diplomes_df is not None: + diplome_cree, diplome_maj, diplome_sup = insert_Diplome(to_table_diplome(diplomes_df)) + text_response.append([f"Diplômes : {diplome_cree} diplômes créés, {diplome_maj} diplômes mis à jour, {diplome_sup} supprimés."]) + self.logger.info('Diplome -------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : diplômes (nécessite %s)', DF.DIPLOME.value[1]) + + if donnees_bo_df is not None: + donnee_bo_cree, donnee_bo_sup = insert_Affectation(to_table_affectation(donnees_bo_df)) + text_response.append([f"Données BO : {donnee_bo_cree} affectation créés, {donnee_bo_sup} affectation mis à jour."]) + self.logger.info('Affectations --------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : affectations (nécessite %s)', DF.BO.value[1]) + + if fud_df is not None: + fud_cree, fud_sup = insert_Fud(to_table_fud(fud_df)) + text_response.append([f"FUD : {fud_cree} FUD créés, {fud_sup} FUD supprimées."]) + self.logger.info('FUD -----------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : FUD (nécessite %s)', DF.FUD.value[1]) + + if donnees_bo_df is not None: + bo_cree, bo_maj, error_count = insert_administre_notation(to_table_administre_notation(donnees_bo_df)) + text_response.append([f"Données BO : {bo_cree} notations créées, {bo_maj} notations mises à jour, {error_count} notations en erreurs."]) + self.logger.info('Administre Notation -------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : administrés/notations (nécessite %s)', DF.BO.value[1]) + + if reo_df is not None: + annee_pam = PAM.objects.get(pam_statut='PAM en cours').pam_id + reo_cree, reo_suivant_cree, reo_modifie, reo_erreur, reo_ignore = insert_Poste(to_table_postes(reo_df),annee_pam) + text_response.append([f"REO : {reo_cree} postes créés, {reo_modifie} postes mis à jour, {reo_erreur} postes en erreur et {reo_ignore} postes ignorés."]) + self.logger.info('Poste ---------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : postes (nécessite %s)', DF.REO.value[1]) + + if reo_suivant_df is not None: + annee_pam = PAM.objects.get(pam_statut='PAM A+1').pam_id + reo_cree, reo_suivant_cree, reo_modifie, reo_erreur, reo_ignore = insert_Poste(to_table_postes(reo_suivant_df),annee_pam) + #info reo + text_response.append([f"REO A + 1 : {reo_suivant_cree} postes A+1 créés, {reo_erreur} postes A+1 en erreur et {reo_ignore} postes A+1 ignorés."]) + self.logger.info('Poste A+1 ---------------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : postes A + 1 (nécessite %s)', DF.REO_PAM_SUIVANT.value[1]) + + if reo_suivant_df is not None: + self.logger.info('Calcul du delta de la colonne Info REO, veuillez patienter ...') + insert_delta(to_table_postes(reo_suivant_df)) + text_response.append([f"Mise à jour de la colonne info REO"]) + self.logger.info('Calcul du delta Info Reo ---------------------------------------------> Succès') + else: + self.logger.info('Mise à jour de la colonne "info reo" ignorée') + + # TODO if .... + insert_SousVivier_instances() + self.logger.info('Sous-viviers instances ---------------------------> Succès') + + if reo_ocv_df is not None: + reo_ocv_modifie, reo_ocv_erreur, reo_ocv_ignore = update_poste_ocv(to_table_reo_ocv(reo_ocv_df)) + text_response.append([f"Requêtes OCV : {reo_ocv_modifie} postes-ocv mis à jour, {reo_ocv_erreur} postes-ocv en erreur et {reo_ocv_ignore} postes-ocv ignorés."]) + self.logger.info('Poste-ocv -----------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : postes/administrés (nécessite %s)', DF.REO_OCV.value[1]) + + if ref_zones_geo_df is not None: + ref_zones_geo_df_cree, ref_zones_geo_df_sup, error_count = insert_ZoneGeographique(to_table_zone_geographique(ref_zones_geo_df)) + text_response.append([f"Référentiels Zones Géographiques : {ref_zones_geo_df_cree} zones-geo créées, {ref_zones_geo_df_sup} zones-geo supprimés et {error_count} zones-geo en erreur."]) + self.logger.info('Référentiel zones géographiques -------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : zones géographiques (nécessite %s)', DF.REF_GEO.value[1]) + + if fmob_df is not None: + fmob_cree, fmob_maj, ignore_count = insert_FMOB_fmob(to_table_fmob_fmob(fmob_df)) + text_response.append([f"Formulaire de mobilité : {fmob_cree} fmob créés, {fmob_maj} fmob mis à jour et {ignore_count} fmob ignorés."]) + self.logger.debug('Fichier FMOB --------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : FMOB (nécessite %s)', DF.FMOB.value[1]) + + if femp_df is not None: + femp_cree, femp_maj, ignore_count = insert_FMOB_femp(to_table_fmob_femp(femp_df)) + text_response.append([f"Formulaire de mobilité : {femp_cree} femp créés, {femp_maj} femp mis à jour et {ignore_count} femp ignorés."]) + self.logger.debug('Fichier FEMP --------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : FEMP (nécessite %s)', DF.FEMP.value[1]) + + if fmob_suivant_df is not None: + fmob_cree, fmob_maj, ignore_count = insert_FMOB_fmob(to_table_fmob_fmob(fmob_suivant_df)) + text_response.append([f"Formulaire de mobilité A+1 : {fmob_cree} fmob A+1 créés, {fmob_maj} fmob A+1 mis à jour et {ignore_count} fmob A+1 ignorés."]) + self.logger.debug('Fichier FMOB A+1--------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : FMOB A + 1(nécessite %s)', DF.FMOB_PAM_SUIVANT.value[1]) + + if femp_suivant_df is not None: + femp_cree, femp_maj, ignore_count = insert_FMOB_femp(to_table_fmob_femp(femp_suivant_df)) + text_response.append([f"Formulaire de mobilité A+1 : {femp_cree} femp A+1 créés, {femp_maj} femp A+1 mis à jour et {ignore_count} femp A+1 ignorés."]) + self.logger.debug('Fichier FEMP A+1 --------------------------------------> Succès') + else: + self.logger.info('Mise à jour ignorée : FEMP A + 1(nécessite %s)', DF.FEMP_PAM_SUIVANT.value[1]) + + + if fmob_df is not None: + a_maintenir, a_traiter = update_administre_fmob(to_table_fmob_fmob(fmob_df)) + text_response.append([f"Formulaire de mobilité : {a_maintenir} administrés mis à jour en 'A maintenir' car FMOB annulé, {a_traiter} administrés mis à jour en 'A muter' car ils ont un FMOB."]) + self.logger.debug('Mise à jour du statut PAM si annulation FMOB et si FMOB existe------> Succès') + else: + self.logger.info('Mise à jour ignorée : statut PAM si annulation FMOB (nécessite %s ou %s)', DF.BO.value[1], DF.FMOB.value[1]) + + if fmob_suivant_df is not None: + a_maintenir, a_traiter = update_administre_fmob(to_table_fmob_fmob(fmob_suivant_df)) + text_response.append([f"Formulaire de mobilité A+1 : {a_maintenir} administrés A+1 mis à jour en 'A maintenir' car FMOB annulé, {a_traiter} administrés A+1 mis à jour en 'A muter' car ils ont un FMOB A+1."]) + self.logger.debug('Mise à jour du statut PAM A+1 si annulation FMOB et si FMOB existe ------> Succès') + else: + self.logger.info('Mise à jour ignorée : statut PAM A+1 si annulation FMOB (nécessite %s ou %s)', DF.BO.value[1], DF.FMOB_PAM_SUIVANT.value[1]) + + + + self.logger.debug('Administres Pams -------------------------> Success') + + self.logger.debug('--------------- Insert time : %d seconds -----------------', time.time() - start_time_insert) + self.logger.info('---------------------- Insert ending ----------------------') + + text_response_df = pd.DataFrame(text_response) + if text_response_df.empty: + return Response(text_response_df) + else: + return Response(text_response_df[0]) + except (Http404, APIException): + raise + except BaseException: + message = "Impossible d'alimenter le(s) référentiel(s)" + self.logger.exception(message) + raise APIException(message) + + + diff --git a/backend-django/backend/views/alimentation_commentaires.py b/backend-django/backend/views/alimentation_commentaires.py new file mode 100644 index 0000000..05cd738 --- /dev/null +++ b/backend-django/backend/views/alimentation_commentaires.py @@ -0,0 +1,70 @@ +import pandas as pd +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..serializers import AlimentationCommentairesSerializer +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils_extraction import (DataFrameTypes, FileTypes, read_files_by_type, + to_table_commentaires) +from ..utils_insertion import insert_Commentaires + + +class AlimentationCommentairesView(APIView): + """ Vue pour alimenter la base avec les commentaires """ + + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = AlimentationCommentairesSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + def get(self, request): + return Response("Formulaire d'alimentation d'OGURE NG pour les commentaires") + + + @atomic + @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def post(self, request): + """ + Charge le(s) fichier(s) et met à jour la base. + + :param request: requête, contient les fichiers + :type request: class:`rest_framework.request.Request` + + :raises: class:`rest_framework.exceptions.APIException` + + :return: réponse + :rtype: class:`rest_framework.response.Response` + """ + try: + validator = self.serializer_class(data=request.data) + validator.is_valid(raise_exception=True) + + df_comments = read_files_by_type({ + FileTypes.COMMENTS: validator.validated_data.get('commentaires') + }).get(DataFrameTypes.COMMENTS) + + if df_comments is not None: + df = to_table_commentaires(df_comments) + self.logger.info('Extraction des commentaires ------> Succès') + + insert_Commentaires(df) + self.logger.info('Insertion des commentaires ------> Succès') + else: + self.logger.info('Mise à jour ignorée : commentaires') + return Response({'Insertion réussie'}) + except (Http404, APIException): + raise + except BaseException: + message = "Impossible d'alimenter le(s) référentiel(s)" + self.logger.exception(message) + raise APIException(message) diff --git a/backend-django/backend/views/alimentation_ref_droits.py b/backend-django/backend/views/alimentation_ref_droits.py new file mode 100644 index 0000000..1f6e25a --- /dev/null +++ b/backend-django/backend/views/alimentation_ref_droits.py @@ -0,0 +1,153 @@ +import time + +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..serializers import AlimentationRefsDroitSerializer +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils_extraction import (DataFrameTypes, FileTypes, read_files_by_type, + to_table_fe, to_table_ref_gest, + to_table_ref_org, to_table_ref_sv_fil, + to_table_sous_vivier) +from ..utils_insertion import (insert_FormationEmploi, insert_RefFeMere, + insert_RefGest, insert_RefOrg, insert_RefSvFil, + insert_SousVivier, insert_SousVivier_instances, + update_m2m_links_gestionnaire) + + +class AlimentationReferentielsDroitView(APIView): + """ + Page d'alimentation des référentiels destinés à la gestion des droits. + - Chargement des sous-viviers via le référentiel sous-viviers/flières, + - Chargement des référentiels gestionnaires, organique, sous-viviers/filières et FE, + - Chargement des liens entre les formation-emplois et les gestionnaires suivant la gestion de droits mise en place, + - Chargement des liens entre les sous-viviers et les gestionnaires suivant la gestion de droits mise en place, + - Attribution des sous-viviers aux postes et aux administrés. + """ + + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = AlimentationRefsDroitSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + def get(self, request): + return Response("Formulaire d'alimentation d'OGURE NG pour les référentiels destinés à la gestion des droits") + + + @atomic + @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def post(self, request): + """ + Charge le(s) fichier(s) et met à jour la base. + + :param request: requête, contient les fichiers + :type request: class:`rest_framework.request.Request` + + :raises: class:`rest_framework.exceptions.APIException` + + :return: réponse + :rtype: class:`rest_framework.response.Response` + """ + try: + validator = self.serializer_class(data=request.data) + validator.is_valid(raise_exception=True) + + df_by_type = read_files_by_type({ + FileTypes.REF_FE: validator.validated_data.get('referentiel_fe'), + FileTypes.REF_GEST: validator.validated_data.get('referentiel_gestionnaire'), + FileTypes.REF_ORG: validator.validated_data.get('referentiel_organique'), + FileTypes.REF_SV_FIL: validator.validated_data.get('refeferentiel_sous_vivier_filiere'), + }) + DF = DataFrameTypes + ref_fe_df = df_by_type.get(DF.REF_FE) + ref_gest_df = df_by_type.get(DF.REF_GEST) + ref_org_df = df_by_type.get(DF.REF_ORG) + ref_sv_fil_df = df_by_type.get(DF.REF_SV_FIL) + + self.logger.info('-------------------- Insert beginning ---------------------') + start_time_insert = time.time() + + if ref_sv_fil_df is not None: + df = to_table_sous_vivier(ref_sv_fil_df) + self.logger.info('Extraction des données de sous-viviers ------> Succès') + insert_SousVivier(df) + self.logger.info('Insertion des sous-viviers ------> Succès') + else: + self.logger.info('Mise à jour ignorée : sous-viviers (nécessite %s)', DF.REF_SV_FIL.value[1]) + + if ref_org_df is not None: + df = to_table_ref_org(ref_org_df) + self.logger.info('Extraction des données du référentiel organique DRHAT ------> Succès') + insert_RefOrg(df) + self.logger.info('Insertion du référentiel organique ------> Succès') + else: + self.logger.info('Mise à jour ignorée : référentiel organique') + + if ref_gest_df is not None: + df = to_table_ref_gest(ref_gest_df) + self.logger.info('Extraction des données de gestionnaires DRHAT ------> Succès') + insert_RefGest(df) + self.logger.info('Insertion du référentiel gestionnaire / utilisateurs ------> Succès') + else: + self.logger.info('Mise à jour ignorée : référentiel de gestionnaires') + + if ref_sv_fil_df is not None: + df = to_table_ref_sv_fil(ref_sv_fil_df) + self.logger.info('Extraction des données du référentiel de sous-viviers/filières ------> Succès') + insert_RefSvFil(df) + self.logger.info('Insertion du référentiel de sous-viviers/filières ------> Succès') + else: + self.logger.info('Mise à jour ignorée : référentiel de sous-viviers/filières') + + if ref_gest_df is not None or ref_org_df is not None or ref_sv_fil_df is not None: + update_m2m_links_gestionnaire('SV') + self.logger.info('Insertion des liens M2M entre sous-viviers et gestionnaires ------> Succès') + else: + self.logger.info('Mise à jour ignorée : liens gestionnaires/sous-viviers (nécessite %s ou %s ou %s)', + DF.REF_GEST.value[1], DF.REF_ORG.value[1], DF.REF_SV_FIL.value[1]) + + if ref_fe_df is not None: + df = to_table_fe(ref_fe_df) + self.logger.info('Extraction des données du référentiel FE ------> Succès') + insert_FormationEmploi(df) + self.logger.info('Insertion du référentiel FE ------> Succès') + else: + self.logger.info('Mise à jour ignorée : formations-emplois (nécessite %s)', DF.REF_FE.value[1]) + + if ref_fe_df is not None: + insert_RefFeMere(ref_fe_df) + self.logger.info('Référentiel FE mère ------> Succès') + else: + self.logger.info('Mise à jour ignorée : formations-emplois mères (nécessite %s)', DF.REF_FE.value[1]) + + if ref_gest_df is not None or ref_org_df is not None or ref_fe_df is not None: + update_m2m_links_gestionnaire('FE') + self.logger.info('Insertion des liens M2M entre formations-emplois et gestionnaires ------> Succès') + else: + self.logger.info('Mise à jour ignorée : liens gestionnaires/formations-emplois (nécessite %s ou %s ou %s)', + DF.REF_GEST.value[1], DF.REF_ORG.value[1], DF.REF_FE.value[1]) + + # TODO if .... + insert_SousVivier_instances() + self.logger.info('Insertion des liens entre sous-vivivers et administrés/postes ------> Succès') + + self.logger.debug('--------------- Insert time : %d seconds -----------------', time.time() - start_time_insert) + self.logger.info('---------------------- Insert ending ----------------------') + + return Response({'Insertion réussie'}) + except (Http404, APIException): + raise + except BaseException: + message = "Impossible d'alimenter les référentiels" + self.logger.exception(message) + raise APIException(message) diff --git a/backend-django/backend/views/alimentation_zones_geo.py b/backend-django/backend/views/alimentation_zones_geo.py new file mode 100644 index 0000000..28c6934 --- /dev/null +++ b/backend-django/backend/views/alimentation_zones_geo.py @@ -0,0 +1,70 @@ +import pandas as pd +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..serializers.alimentation import AlimentationZoneGeographiqueSerializer +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils_extraction import (DataFrameTypes, FileTypes, read_files_by_type, + to_table_zone_geographique) +from ..utils_insertion import insert_ZoneGeographique + + +class AlimentationZoneGeographiqueView(APIView): + """ Vue pour alimenter la base à partir de référentiels """ + + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = AlimentationZoneGeographiqueSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + def get(self, request): + return Response("Formulaire d'alimentation d'OGURE NG pour les zones geographiques") + + + @atomic + @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def post(self, request): + """ + Charge le(s) fichier(s) et met à jour la base. + + :param request: requête, contient les fichiers + :type request: class:`rest_framework.request.Request` + + :raises: class:`rest_framework.exceptions.APIException` + + :return: réponse + :rtype: class:`rest_framework.response.Response` + """ + try: + validator = self.serializer_class(data=request.data) + validator.is_valid(raise_exception=True) + + ref_zones_geo_df = read_files_by_type({ + FileTypes.REF_GEO: validator.validated_data.get('ref_zones_geo') + }).get(DataFrameTypes.REF_GEO) + + if ref_zones_geo_df is not None: + df = to_table_zone_geographique(ref_zones_geo_df) + self.logger.info('Extraction des données du référentiel ------> Succès') + + insert_ZoneGeographique(df) + self.logger.info('Mise à jour du référentiel ------> Succès') + else: + self.logger.info('Mise à jour ignorée : zones géographiques (nécessite %s)', DataFrameTypes.REF_GEO.value[1]) + return Response({'Insertion réussie'}) + except (Http404, APIException): + raise + except BaseException: + message = "Impossible d'alimenter le(s) référentiel(s)" + self.logger.exception(message) + raise APIException(message) diff --git a/backend-django/backend/views/chargement_competences.py b/backend-django/backend/views/chargement_competences.py new file mode 100644 index 0000000..5086572 --- /dev/null +++ b/backend-django/backend/views/chargement_competences.py @@ -0,0 +1,340 @@ +from typing import List, Tuple, Union + +import numpy as np +import pandas as pd +from django.core.files.base import File +from django.db.models import Q +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework import status +from rest_framework.exceptions import APIException +from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import Administre, Poste +from ..models.competence import Competence +from ..models.domaine import Domaine +from ..models.filiere import Filiere +from ..serializers.alimentation import ChargementCompetencesSerializer +from ..utils import cleanString +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils_extraction import open_excel + + +class _SkillFiles(): + """ Regroupe les constantes de fichiers de compétences """ + + class Ref(): + """ Constantes pour les colonnes du fichier de référence """ + + PK = 'Macro compétence libellé court (15 caractères MAXI)' # E (tous onglets) + CATEGORIE = 'Catégorie' # A (tous onglets) + DOMAINE = 'Domaine' # B (tous onglets) + FILIERE = 'Filière' # C (tous onglets) + LIBELLE = 'Macro compétence libellé long' # D (tous onglets) + + class Specific(): + """ Constantes pour les colonnes du fichier de compétences particulières """ + + ADMINISTRE_PK = 'N SAP' # A + ADMINISTRE_COMPETENCE_1 = 'COMPETENCE 1' # Q + ADMINISTRE_COMPETENCE_2 = 'COMPETENCE 2' # R + ADMINISTRE_COMPETENCE_3 = 'COMPETENCE 2.1' # S la notation .1 est documentée dans 'read_excel' (pandas) + + POSTE_DOMAINE = 'DOM EIP' # N + POSTE_FILIERE = 'FIL EIP' # O + POSTE_FE = 'CODE FE' # C + POSTE_FONCTION = 'CODE FONCTION' # T + POSTE_NF = 'NR EIP' # P + POSTE_COMPETENCE_1 = 'COMPETENCE 1' # Q + POSTE_COMPETENCE_2 = 'COMPETENCE 2' # R + POSTE_COMPETENCE_3 = 'COMPETENCE 2.1' # S la notation .1 est documentée dans 'read_excel' (pandas) + + +class ChargementCompetenceView(APIView): + """ + Cette classe est dédiée au vue de chargement des competences. + - Charge et traite les fichiers de compétences. + - Attribue les compétences présentes aux administrés et postes correspondants + """ + + permission_classes = [IsAuthenticated, IsAdminUser] + parser_classes = [MultiPartParser] + serializer_class = ChargementCompetencesSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + def get(self, request): + return Response("Formulaire de chargement des référentiels de compétences") + + + def _read_ref_skills(self, file: File) -> pd.DataFrame: + """ + Extrait les données du référentiel de compétences. + + :param file: référentiel de compétences + :type file: class:`django.core.files.base.File` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + COLS = _SkillFiles.Ref + col_mapping = { + COLS.PK: Competence.Cols.PK, + COLS.LIBELLE: Competence.Cols.LIBELLE, + COLS.CATEGORIE: Competence.Cols.CATEGORIE, + COLS.DOMAINE: Competence.Cols.REL_DOMAINE + '_id', + COLS.FILIERE: Competence.Cols.REL_FILIERE + '_id' + } + df = (pd.concat([open_excel(file, sheetname=i, usecols=col_mapping.keys(), engine='openpyxl') for i in range(3)]) + .dropna(subset=[COLS.PK]) + .fillna(np.nan) + .astype(str) + .replace([np.nan, 'nan'], [None, None])) + df[COLS.PK] = df[COLS.PK].str.replace('[^a-zA-Z0-9]', '_', regex=True) + return (df.drop_duplicates(subset=[COLS.PK]) + .rename(columns=col_mapping)) + + + def _read_specific_skills(self, administre: bool, file: File) -> pd.DataFrame: + """ + Extrait les données du fichier de compétences particulières. + + :param administre: True pour les administré, False pour les postes + :type administre: bool + + :param file: fichier de compétences particulières + :type file: class:`django.core.files.base.File` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + COLS = _SkillFiles.Specific + if administre: + col_mapping = {COLS.ADMINISTRE_PK: Administre.Cols.PK} + col_types = {COLS.ADMINISTRE_PK: 'int32'} + cols_skill = [COLS.ADMINISTRE_COMPETENCE_1, COLS.ADMINISTRE_COMPETENCE_2, COLS.ADMINISTRE_COMPETENCE_3] + col_skill = COLS.ADMINISTRE_COMPETENCE_1 + sheetname = 0 + else: + col_mapping = { + COLS.POSTE_DOMAINE: Poste.Cols.REL_DOMAINE + '_id', + COLS.POSTE_FILIERE: Poste.Cols.REL_FILIERE + '_id', + COLS.POSTE_FE: Poste.Cols.REL_FORMATION_EMPLOI + '_id', + COLS.POSTE_FONCTION: Poste.Cols.FONCTION, + COLS.POSTE_NF: Poste.Cols.NIVEAU_FONCTIONNEL, + } + col_types = {c: 'str' for c in col_mapping.keys()} + cols_skill = [COLS.POSTE_COMPETENCE_1, COLS.POSTE_COMPETENCE_2, COLS.POSTE_COMPETENCE_3] + col_skill = COLS.POSTE_COMPETENCE_1 + sheetname = 1 + + dfs = [] + for temp_col_skill in cols_skill: + dfs.append(open_excel(file, sheetname=sheetname, usecols=[*col_mapping.keys(), temp_col_skill], engine='openpyxl') + .rename(columns={temp_col_skill: col_skill})) + df = (pd.concat(dfs) + .dropna(subset=[col_skill]) + .fillna(np.nan) + .astype({**col_types, COLS.ADMINISTRE_COMPETENCE_1: 'str'}) + .replace([np.nan, 'nan'], [None, None])) + df[col_skill] = df[col_skill].str.replace('[^a-zA-Z0-9]', '_', regex=True) + return (df.drop_duplicates() + .rename(columns=col_mapping)) + + + @atomic + def _update_ref(self, df: pd.DataFrame, domaines_in_db: Union[Tuple[str], List[str]], filieres_in_db: Union[Tuple[str], List[str]]) -> None: + """ + Met à jour la table des compétences à partir du DataFrame de données de référence. + + :param df: données de référence + :type df: class:`pandas.DataFrame` + """ + ModelType = Competence + Cols = ModelType.Cols + col_pk = Cols.PK + fields_to_update = (Cols.LIBELLE, Cols.CATEGORIE, Cols.REL_DOMAINE + '_id', Cols.REL_FILIERE + '_id') + models_in_db = {m.pk: m for m in ModelType.objects.only(col_pk, *fields_to_update)} + + batch_size = 100 + dict_create = {} + dict_update = {} + dict_up_to_date = {} + error_count = 0 + to_ignore = {} + for idx, rec in enumerate(df.to_dict('records')): + pk = rec.get(col_pk) + try: + domaine = rec.get(Cols.REL_DOMAINE + '_id') + if domaine is not None and domaine not in domaines_in_db: + to_ignore.setdefault(pk, {}).setdefault(Domaine, domaine) + continue + filiere = rec.get(Cols.REL_FILIERE + '_id') + if filiere is not None and filiere not in filieres_in_db: + to_ignore.setdefault(pk, {}).setdefault(Filiere, filiere) + continue + + in_db = models_in_db.get(pk) + model = ModelType(pk=pk, **{f: rec.get(f) for f in fields_to_update}) + if not in_db: + model.full_clean(validate_unique=False) + dict_create.setdefault(pk, model) + elif any(getattr(in_db, f) != getattr(model, f) for f in fields_to_update): + model.full_clean(validate_unique=False) + dict_update.setdefault(pk, model) + else: + dict_up_to_date.setdefault(pk, model) + except Exception: + error_count = error_count + 1 + self.logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', ModelType.__name__, idx, pk) + + if error_count: + self.logger.warning("%s(s) en erreur : %s", ModelType.__name__, error_count) + + if to_ignore: + self.logger.warning('%s(s) ignorée(s) : %s', ModelType.__name__, len(to_ignore)) + for _pk, _dict in to_ignore.items(): + self.logger.warning('- %s car :', _pk) + for _type, v in _dict.items(): + self.logger.warning(' - %s absent(e) du référentiel : %s', _type.__name__, v) + + if dict_create: + ModelType.objects.bulk_create(dict_create.values(), batch_size=batch_size) + self.logger.info('%s(s) créée(s) : %s', ModelType.__name__, len(dict_create)) + + if fields_to_update: + if dict_update: + ModelType.objects.bulk_update(dict_update.values(), batch_size=batch_size, fields=fields_to_update) + self.logger.info('%s(s) mise(s) à jour : %s', ModelType.__name__, len(dict_update)) + + if dict_up_to_date: + self.logger.info('%s(s) déjà à jour : %s', ModelType.__name__, len(dict_up_to_date)) + + deleted = ModelType.objects.filter(~Q(pk__in={*dict_create.keys(), *dict_update.keys(), *dict_up_to_date.keys()})).delete()[0] + if deleted: + self.logger.info('%s(s) supprimée(s) : %s', ModelType.__name__, deleted) + + + @atomic + def _update_specific(self, administre: bool, df: pd.DataFrame, skills_in_db: Union[Tuple[str], List[str]]) -> None: + """ + Met à jour les liens M2M entre le modèle et les compétences. + + :param administre: True pour les administré, False pour les postes + :type administre: bool + + :param df: données de référence + :type df: class:`pandas.DataFrame` + + :param skills_in_db: clés de toutes les compétences en base, les autres compétences sont ignorées + :type skills_in_db: Union[Tuple[str], List[str]] + """ + if administre: + ModelType = Administre + Cols = ModelType.Cols + LinkModelType = getattr(ModelType, Cols.M2M_COMPETENCES).through + fields_to_filter = (Cols.PK,) + col_skill = _SkillFiles.Specific.ADMINISTRE_COMPETENCE_1 + else: + ModelType = Poste + Cols = ModelType.Cols + LinkModelType = getattr(ModelType, Cols.M2M_COMPETENCES).through + fields_to_filter = (Cols.REL_DOMAINE + '_id', Cols.REL_FILIERE + '_id', Cols.REL_FORMATION_EMPLOI + '_id', Cols.FONCTION, Cols.NIVEAU_FONCTIONNEL) + col_skill = _SkillFiles.Specific.POSTE_COMPETENCE_1 + + link_dict = {} + to_ignore = set() + for rec in df.to_dict('records'): + skill = rec.get(col_skill) + if skill not in skills_in_db: + to_ignore.add(skill) + else: + key = tuple(rec.get(f) for f in fields_to_filter) + link_dict.setdefault(key, set()).add(skill) + if to_ignore: + self.logger.warning('%s(s) ignorée(s) car absente(s) du référentiel : %s (%s)', Competence.__name__, len(to_ignore), to_ignore) + + batch_size = 100 + error_count = 0 + to_create = [] + for in_db in ModelType.objects.only('pk', *fields_to_filter): + try: + links = link_dict.get(tuple(getattr(in_db, f) for f in fields_to_filter)) or () + for link in links: + to_create.append(LinkModelType(**{f'{ModelType.__name__.lower()}_id': in_db.pk, 'competence_id': link})) + except Exception: + error_count = error_count + 1 + self.logger.exception("une erreur est survenue lors de l'ajout de lien(s) %s[pk=%s]/%s", ModelType.__name__, in_db.pk, Competence.__name__) + + if error_count: + self.logger.warning("lien(s) %s/%s en erreur : %s", ModelType.__name__, Competence.__name__, error_count) + + deleted = LinkModelType.objects.all().delete()[0] + if deleted: + self.logger.info('lien(s) %s/%s supprimé(s) : %s', ModelType.__name__, Competence.__name__, deleted) + + if to_create: + LinkModelType.objects.bulk_create(to_create, batch_size=batch_size) + self.logger.info('lien(s) %s/%s créé(s) : %s', ModelType.__name__, Competence.__name__, len(to_create)) + + + @execution_time(warn_after=30000, logger_factory=data_perf_logger_factory) + @query_count(warn_after=50, logger_factory=data_perf_logger_factory) + def post(self, request: Request) -> Response: + """ + Charge les competences, met à jour la table de compétences et les liens M2M avec les administrés et les postes. + + :param request: Request contenant le fichier de competence + :type request: rest_framework.request.Request + + :return: un message + :rtype: class:`rest_framework.response.Response` + """ + try: + # validation et récupération des fichiers + ser = self.serializer_class(data=request.data) + ser.is_valid(raise_exception=True) + ref_skill_file = ser.validated_data.get('ref_skills') + specific_skill_file = ser.validated_data.get('specific_skills') + + try: + df_ref = self._read_ref_skills(ref_skill_file) + self.logger.info('Lecture du fichier de référentiel de compétences ------> Succès') + self._update_ref(df_ref, + domaines_in_db=list(Domaine.objects.values_list('pk', flat=True)), + filieres_in_db=list(Filiere.objects.values_list('pk', flat=True))) + self.logger.info('Mise à jour du référentiel de compétences ------> Succès') + except Exception as e : + self.logger.info('Lecture du fichier de référentiel de compétences ------> Ignoré') + self.logger.info(e) + + try: + df_specific_administre = self._read_specific_skills(True, specific_skill_file) + df_specific_poste = self._read_specific_skills(False, specific_skill_file) + self.logger.info("Lecture des compétences particulières d'administrés ------> Succès") + self.logger.info('Lecture des compétences particulières de postes ------> Succès') + ref_data = list(Competence.objects.values_list('pk', flat=True)) + self._update_specific(True, df_specific_administre, ref_data) + self.logger.info("Mise à jour des compétences particulières d'administrés ------> Succès") + self._update_specific(False, df_specific_poste, ref_data) + self.logger.info('Mise à jour des compétences particulières de postes ------> Success') + except Exception as e : + self.logger.info("Mise à jour des compétences particulières d'administrés et postes ------> Ignoré") + self.logger.info(e) + + return Response({'Insertion réussie'}) + except (Http404, APIException): + raise + except BaseException: + message = "Impossible d'alimenter le référentiel de compétences" + self.logger.exception(message) + raise APIException(message) diff --git a/backend-django/backend/views/chargement_pam.py b/backend-django/backend/views/chargement_pam.py new file mode 100644 index 0000000..b9c7420 --- /dev/null +++ b/backend-django/backend/views/chargement_pam.py @@ -0,0 +1,189 @@ +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from django.db.models import Q +import datetime +from typing import Any, List, Tuple, Union + +from ..utils_extraction import to_table_pam + +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils_insertion import insert_PAM +from ..models import Administres_Pams, Postes_Pams,Calcul, Poste, PAM, StatutPamChoices as StatutPam, AvisPosteChoices +from ..utils.insertion.commun import batch_iterator + +class AlimentationPamView(APIView): + """ Vue pour charger un nouveau PAM """ + + permission_classes = [IsAuthenticated, IsAdminUser] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + + + def get(self, request): + try: + self.logger.debug('Début de la mise à jour de la table PAM, veuillez patienter...') + # today = datetime.date(2023,1,1) + today = datetime.datetime.now() + insert_PAM(to_table_pam(today)) + + self.logger.debug('Insertion/Mise à jour de la table PAM --------------------------------------------> Success') + + 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 + + self.logger.debug('Mise à jour des données des administrés pour les PAM A et A+1, veuillez patienter ...') + dict_create_administres = {} + dict_update_administres = {} + + qs = Administres_Pams.objects.all().filter(pam_id=annee_pam).values('administre_id') + + batch_size = 50 + admin_suivant = dict((f"{o.administre_id}", o) for o in Administres_Pams.objects.all().filter(pam_id=annee_pam_suivant)) + + for a_id_sap in qs: + administre_id=a_id_sap['administre_id'] + pk = str(administre_id) + str(annee_pam_suivant) + + model_administre = Administres_Pams(**{ + 'id' :pk, + 'pam_id' :annee_pam_suivant, + 'administre_id' :administre_id, + 'a_statut_pam_annee' :StatutPam.NON_ETUDIE, + 'notes_pam' :None, + 'a_ciat_pam' :False, + 'a_specifique_pam' :None, + 'a_liste_depts_souhaites_pam' :None, + 'a_liste_zones_geographiques_shm_pam' :None, + 'a_situationfuture_notes_fe' :None, + }) + + if str(administre_id) not in admin_suivant : + dict_create_administres.setdefault(pk, model_administre) + + else: + dict_update_administres.setdefault(pk, model_administre) + + + if dict_create_administres: + self.logger.debug("Nombre d'administrés à créer dans l'année %s : %s'",annee_pam_suivant,len(dict_create_administres)) + for idx, data_batch in enumerate(batch_iterator(list(dict_create_administres.values()), batch_size)): + try: + Administres_Pams.objects.bulk_create(data_batch) + except Exception as e: + self.logger.exception("Cet administré existe déjà dans la base : %s", e) + + self.logger.debug('Mise à jour des liens Administres/Pams terminés.') + + self.logger.debug('Mise à jour des données des postes pour les PAM A et A+1, veuillez patienter ...') + dict_create_postes = {} + dict_update_postes = {} + + # def mise_a_jour_info_reo(info_reo, annee_pam): + # if "SUREFF" in info_reo: + # model.info_reo=info_reo + # else: + # model.info_reo=f'REO {annee_pam}' + # return model.info_reo + + qs = Postes_Pams.objects.all().filter(p_pam_id=annee_pam).values_list('poste_id','info_reo') + + batch_size = 50 + poste_suivant = dict((f"{o.poste_id}", o) for o in Postes_Pams.objects.all().filter(p_pam_id=annee_pam_suivant)) + + for poste in qs: + poste_id=poste[0] + info_reo= poste[1] + pk = str(poste_id) + str(annee_pam_suivant) + model_poste = Postes_Pams(**{ + 'id' :pk, + 'p_pam_id' :annee_pam_suivant, + 'poste_id' :poste_id, + 'p_avis_pam' :StatutPam.NON_ETUDIE, + 'p_avis_fe_pam' :StatutPam.NON_ETUDIE, + 'p_direct_commissionne_pam' :None, + 'p_notes_gestionnaire_pam' :None, + 'p_priorisation_pcp_pam' :None, + 'info_reo' :info_reo if "SUREFF" in info_reo else f'REO {annee_pam}', + }) + + if str(poste_id) not in poste_suivant : + dict_create_postes.setdefault(pk, model_poste) + + else: + dict_update_postes.setdefault(pk, model_poste) + + if dict_create_postes: + self.logger.debug("Nombre de postes à créer dans l'année %s : %s'",annee_pam_suivant,len(dict_create_postes)) + for idx, data_batch in enumerate(batch_iterator(list(dict_create_postes.values()), batch_size)): + try: + Postes_Pams.objects.bulk_create(data_batch) + except Exception as e: + self.logger.exception("Ce poste existe déjà dans la base : %s", e) + + self.logger.debug('Mise à jour des liens Postes/Pams terminés.') + + self.logger.debug(f'Début de la suppression des données du PAM {int(annee_pam)-1}, veuillez patienter ...') + PAM.objects.filter(pam_id=int(annee_pam)-1).delete() + self.logger.debug("Fin de l'initialisation/Mise à jour du PAM") + + + return Response({'Initialisation/Mise à jour du Pam réussie'}) + + except (Http404, APIException): + message = "Impossible de mettre à jour ce PAM" + self.logger.exception(message) + raise APIException(message) + +class NettoyagePamView(APIView): + """ Vue pour nettoyer les postes tagués en SUP REO """ + + permission_classes = [IsAuthenticated, IsAdminUser] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + def get(self, request): + try: + + self.logger.debug("Suppression des postes tagués en SUP REO de l'année A --------------------------------------------> Success") + + annee_pam = PAM.objects.filter(pam_statut='PAM en cours')[0].pam_id + + qs = Postes_Pams.objects.all().filter(p_pam_id=annee_pam).values('poste_id') + + for p_id in qs: + poste_id=p_id['poste_id'] + + if Postes_Pams.objects.filter(Q(info_reo='SUP REO') & Q(poste_id=poste_id)): + self.logger.debug(f'Suppression du poste {poste_id}') + Poste.objects.filter(p_id=poste_id).delete() + + #Mise à jour des postes tagues CREE REO A en REO A + if not Postes_Pams.objects.filter(Q(p_pam_id=annee_pam) & Q(poste_id=poste_id) & Q(info_reo__contains='SUREFF')): + Postes_Pams.objects.filter(Q(p_pam_id=annee_pam) & Q(poste_id=poste_id)).update(info_reo=f'REO {annee_pam}') + + self.logger.debug(f'Début de la suppression des administrés du PAM {int(annee_pam)-1}') + Administres_Pams.objects.filter(pam_id=int(annee_pam)-1).delete() + self.logger.debug(f'Début de la suppression des postes du PAM {int(annee_pam)-1}') + Postes_Pams.objects.filter(p_pam_id=int(annee_pam)-1).delete() + Calcul.objects.filter(pam_id=int(annee_pam)-1).delete() + self.logger.debug(f'Fin de la suppression des données du PAM {int(annee_pam)-1}') + + + return Response({'Mise à jour du Pam réussie'}) + + except (Http404, APIException): + message = "Impossible de mettre à jour ce PAM" + self.logger.exception(message) + raise APIException(message) \ No newline at end of file diff --git a/backend-django/backend/views/commun.py b/backend-django/backend/views/commun.py new file mode 100644 index 0000000..f72432e --- /dev/null +++ b/backend-django/backend/views/commun.py @@ -0,0 +1,292 @@ +# -------------------------------------------------------------------------------- +""" +Définitions communes à plusieurs vues +""" +# -------------------------------------------------------------------------------- + +from django.db.models import Q +from rest_framework.exceptions import APIException +from rest_framework.permissions import SAFE_METHODS, BasePermission + +from ..models import Administre, Decision, Poste, SpecifiqueChoices, Administres_Pams, Postes_Pams + +from ..utils.decorators import (class_logger, decorate_functions, + execution_time, query_count) +from ..utils.permissions import (Profiles, KEY_READ, KEY_WRITE, + get_profile_summary, + 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_queryset_org_by_user, is_truthy) +from ..utils.view_predicates import viewset_functions + +# décorateur pour tracer le temps d'exécution d'une vue +execution_time_viewset = decorate_functions( + lambda cls: execution_time(warn_after=5000, logger_name=cls), + viewset_functions, + factory=True +) + +# décorateur pour tracer le nombre de requêtes d'une vue +query_count_viewset = decorate_functions( + lambda cls: query_count(warn_after=20, logger_name=cls), + viewset_functions, + factory=True +) + +def api_exception(status_code: int, message: str = None) -> APIException: + """ + Crée une APIException avec un statut personnalisé. + note : inutile d'appeler cette fonction pour un code 500 car APIException(message) suffit + + :param status_code: code de statut HTTP + :type status_code: int + + :param message: message de l'exception + :type message: str, optional + + :return: exception + :rtype: class:`APIException` + """ + return type('MyException', (APIException,), {'status_code': status_code, 'default_detail': message}) + + +@class_logger +class GestionnairePermission(BasePermission): + """ + Classe de gestion des droits de lecture et d'écriture d'un gestionnaire + """ + message = "Le gestionnaire n'a pas les droits de lecture ou d'écriture" + + def is_in_list_ok(self, list, list_ok): # A modif en utilisant set + """ + Cette fonction vérifie que tous les objets de la liste list soient dans la liste list_ok + + :param list: liste 1 + :type list: list + + :param list: liste 2 + :type list: list + + :return: booléen indiquant si les objets de la liste 1 sont dans la liste 2 + :rtype: bool + """ + for obj in list: + if obj not in list_ok: + return False + return True + + + def verif_droits_adm(self, pk, codes_lvl4, is_fil, is_pcp, is_bvt): + """ + Cette fonction vérifie si le gestionnaire a bien le droit de modifier (mettre à jour) + les attributs du ou des administres d'identifiant pk. + + :param pk: id sap du ou des administrés édités + :type org_code: int ou list + + :param codes_lvl4: codes de niveau 4 des référentiels organiques liés à l'utilisateur + :type codes_lvl4: Tuple[str] + + :param is_fil: profil filiere + :type is_fil: bool + + :param is_pcp: profil pcp + :type is_pcp: bool + + :param is_bvt: profil bvt + :type is_bvt: bool + + :return: booléen pour indiqué si l'utilisateur a les droits ou non sur l'édition + du ou des administrés concernés + :rtype: bool + """ + # On récupère le ou les administrés dont l'id match avec pk (liste ou non) + qs = Administre.objects.filter(a_id_sap__in=pk) if isinstance(pk, list) else Administre.objects.filter(a_id_sap=pk) + list_a = list(qs.values_list('pk', flat=True)) + + # On récupère le ou les administrés autorisés + 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 + + list_a_ok = Administres_Pams.objects.filter(adm_filter).values_list('administre_id', flat=True) + perm = self.is_in_list_ok(list_a, list_a_ok) + + msg = 'Administre rights verification ok\n' if perm else 'Administre rights verification not ok\n' + self.logger.debug(msg) + + return perm + + + def verif_droits_poste(self, pk, codes_lvl4, is_fil, is_pcp, is_bvt, is_itd): + """ + Cette fonction vérifie si le gestionnaire a bien le droit de modifier (mettre à jour) + les attributs du ou des postes d'identifiant pk. + + :param pk: id sap du ou des administrés édités + :type org_code: int ou list + + :param codes_lvl4: codes de niveau 4 des référentiels organiques liés à l'utilisateur + :type codes_lvl4: Tuple[str] + + :param is_fil: profil filiere + :type is_fil: bool + + :param is_pcp: profil pcp + :type is_pcp: bool + + :param is_bvt: profil bvt + :type is_bvt: bool + + :param is_itd: profil itd + :type is_itd: bool + + :return: booléen pour indiqué si l'utilisateur a les droits ou non sur l'édition + du ou des postes concernés + :rtype: bool + """ + # On récupère le ou les postes dont l'id match avec pk (liste ou non) + qs = Poste.objects.filter(p_id__in=pk) if isinstance(pk, list) else Poste.objects.filter(p_id=pk) + list_p = list(qs.values_list('pk', flat=True)) + + # On créer le filtre avec le ou les postes autorisés + poste_filter = None + if is_fil: + poste_filter = get_poste_filter_by_lvl4_codes_fil(codes_lvl4) + if is_pcp: + pcp_filter = get_poste_filter_by_lvl4_codes_pcp(codes_lvl4) + poste_filter = poste_filter | pcp_filter if poste_filter else pcp_filter + if is_itd: + itd_filter = Q(**{f'{Postes_Pams.Cols.REL_POSTE}__p_specifique' : SpecifiqueChoices.ITD}) + 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 + + list_p_ok = Postes_Pams.objects.filter(poste_filter).values_list('poste_id', flat=True) + perm = self.is_in_list_ok(list_p, list_p_ok) + + msg = 'Poste rights verification ok\n' if perm else 'Poste rights verification not ok\n' + self.logger.debug(msg) + + return perm + + + def commun_verif(self, request, view, obj=None): + """ + Partie commune de la vérification des droits de lecture aux fonctions has_permission et has_object_permission + """ + P = Profiles + + user = request.user + if not user or not user.is_authenticated: + self.logger.debug('No user connected\n') + return False + + is_super = user.is_superuser + + if is_super and str(view) in ['ReportingView']: + self.logger.debug('Rights verification ok : the user is a superuser\n') + return True + + summary = get_profile_summary(user) + profiles = summary.profiles + r_profiles = profiles.get(KEY_READ, ()) + w_profiles = profiles.get(KEY_WRITE, ()) + + if not profiles: + self.logger.debug('The user as no profile\n') + return False + + if request.method in SAFE_METHODS: + resp = True if (r_profiles and r_profiles!=(P.SUPER,)) else False + if not resp: + msg = 'The user does not have reading rights\n' + self.logger.debug(msg) + return resp + + # La gestion de droits des administrateurs est faite via IsAdminUser pour les api d'alimentation + is_fil = P.FILIERE in w_profiles + is_pcp = P.PCP in w_profiles + is_bvt = P.BVT in w_profiles + is_itd = P.ITD in w_profiles + is_hme = P.HME in w_profiles + + codes_lvl4 = get_lvl4_org_codes_by_any_code(summary.org_code) + + if obj: # Modification d'une seule instance + if not w_profiles or w_profiles==(P.SUPER,): + self.logger.debug('The user does not have writing rights\n') + return False + + if isinstance(obj, Administre): + pk = obj.pk + return self.verif_droits_adm(pk, codes_lvl4, is_fil, is_pcp, is_bvt) + + elif isinstance(obj, Poste): + pk = obj.pk + return self.verif_droits_poste(pk, codes_lvl4, is_fil, is_pcp, is_bvt, is_itd) + + elif isinstance(obj, Decision): + a_pk = obj.administre.pk + p_pk = obj.poste.pk + return self.verif_droits_poste(p_pk, codes_lvl4, is_fil, is_pcp, is_bvt, is_itd) or \ + self.verif_droits_adm(a_pk, codes_lvl4, is_fil, is_pcp, is_bvt) + + else: # Vérification globale ou modification multiple + + self.logger.debug('---- Writing profiles ----') + self.logger.debug('Expert HME : %s', is_hme) + self.logger.debug('Pameur BMOB : %s', is_pcp) + self.logger.debug('Gestionnaire BVT : %s', is_bvt) + self.logger.debug('Gestionnaire ITD : %s', is_itd) + self.logger.debug('Gestionnaire BGCAT : %s', is_fil) + self.logger.debug('--------------------------') + + if isinstance(request.data, list): + if not w_profiles or w_profiles==(P.SUPER,): + self.logger.debug('The user does not have writing rights\n') + return False + + if 'a_id_sap' in request.data[0].keys(): + pk = [adm['a_id_sap'] for adm in request.data] + return self.verif_droits_adm(pk, codes_lvl4, is_fil, is_pcp, is_bvt) + + elif 'p_id' in request.data[0].keys(): + pk = [p['p_id'] for p in request.data] + return self.verif_droits_poste(pk, codes_lvl4, is_fil, is_pcp, is_bvt, is_itd) + + self.logger.debug('Global rights verification ok\n') + return True # On autorise une permission globale pour ensuite permettre des vérifications au niveau des instances avec has_object_permission + + self.logger.debug('User rights verification failed\n') + return False + + + def has_permission(self, request, view): + if request.method != 'GET': + self.logger.debug('------------------------- Permissions verification ---------------------') + self.logger.debug('Request method : %s', request.method) + self.logger.debug('Request data : %s', request.data) + self.logger.debug('View : %s', view) + + return self.commun_verif(request=request, view=view) + + def has_object_permission(self, request, view, obj): + self.logger.debug('--------------------- Object permissions verification ------------------') + self.logger.debug('Request method : %s', request.method) + self.logger.debug('Request data : %s', request.data) + self.logger.debug('View : %s', view) + self.logger.debug('Object : %s', obj) + + return self.commun_verif(request=request, view=view, obj=obj) diff --git a/backend-django/backend/views/current_user.py b/backend-django/backend/views/current_user.py new file mode 100644 index 0000000..92c8bd4 --- /dev/null +++ b/backend-django/backend/views/current_user.py @@ -0,0 +1,46 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import PcpFeGroupe, SousVivier, FormationEmploi +from ..serializers.current_user import (CTX_KEY_PCP_FE_GROUPE, + CTX_KEY_PROFILES, CTX_KEY_SOUS_VIVIERS, + CTX_KEY_FORMATION_EMPLOIS, CTX_KEY_SOUS_VIVIERS_SUPER, + UserInfoSerializer) +from ..utils.decorators import class_logger +from ..utils.permissions import KEY_READ, Profiles, get_profile_summary +from .commun import execution_time_viewset, query_count_viewset + + +@class_logger +@execution_time_viewset +@query_count_viewset +class CurrentUserView(APIView): + """ + Cette classe est dédiée au vue de l'utilisateur courant: + """ + + permission_classes = [IsAuthenticated] + + # TODO adapter au nouveau système de gestion de profils + def get(self, request: Request) -> Response: + """ + Renvoie les informations de l'utilisateur connecté. + + :param request: requête contenant l'utilisateur authentifié + :type request: class:`rest_framework.request.Request` + + :return: réponse contenant les informations de l'utilisateur dont la formation emploi group et les sous-viviers specifiques à cet utilisateur. + :rtype: class:`rest_framework.response.Response` + """ + user = request.user + summary = get_profile_summary(request.user) + profiles = summary.profiles.get(KEY_READ, ()) + return Response(UserInfoSerializer(user, context={ + # CTX_KEY_SOUS_VIVIERS_SUPER: SousVivier.objects.all() if Profiles.SUPER in profiles else None, + CTX_KEY_SOUS_VIVIERS: SousVivier.objects.filter(gestionnaires__id=request.user.id), + CTX_KEY_FORMATION_EMPLOIS: FormationEmploi.objects.filter(gestionnaires__id=request.user.id).order_by('fe_mere_credo').distinct('fe_mere_credo'), + CTX_KEY_PCP_FE_GROUPE: PcpFeGroupe.objects.filter(gestionnaire_id=user.pk).first(), + CTX_KEY_PROFILES: summary.profiles + }).data) diff --git a/backend-django/backend/views/decision.py b/backend-django/backend/views/decision.py new file mode 100644 index 0000000..998af99 --- /dev/null +++ b/backend-django/backend/views/decision.py @@ -0,0 +1,152 @@ +from django.db.transaction import atomic +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +import datetime +from ..models import Administre, Decision, DecisionChoices, Notation, Poste +from ..serializers.decision import CreateDecisionSerializer, DecisionSerializer +from ..utils.decorators import class_logger +from .commun import (GestionnairePermission, api_exception, + execution_time_viewset, query_count_viewset) + + +@class_logger +@execution_time_viewset +@query_count_viewset +class DecisionView(ModelViewSet): + """ + Cette classe est dédiée au vue des decisions. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = DecisionSerializer + queryset = Decision.objects.all() + + def get_serializer_class(self): + """ Renvoie un serializer particulier pour la création """ + if self.action == 'create': + return CreateDecisionSerializer + return self.serializer_class + + def list(self, request: Request, pk=None) -> Response: + """Cette fonction envoie les informations du poste lié à l'administre dans une décision avec le score de l'administre et inversement. + TODO: rajouter la possibilité d'envoyer les informations du poste lié à l'administré selon l'année du PAM dans lequel on se situe + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre ou le poste. + + :return: - **res_decision** (*Response*): Json contenant le classement. + """ + decision = [] + q = 'poste' + administre_id = None + poste_id = None + + if 'pam_annee' in request.query_params: + annee_pam = request.query_params['pam_annee'] + else : + annee_pam = None + + if 'administre_id' in request.query_params: + administre_id = request.query_params['administre_id'] + administre_pam_id = str(administre_id) + str(annee_pam) + administres_keys = ( + 'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere', + 'poste__p_eip', #'poste__p_avis', TODO : Change p_avis to postesPAM + 'poste__formation_emploi__fe_code', 'poste__competences', + 'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', + 'poste__formation_emploi__fe_libelle', + 'poste__formation_emploi__fe_garnison_lieu', + 'poste__p_dep', 'poste__p_code_fonction', + 'poste__p_fonction', + 'de_decision', 'de_date_decision', 'de_notes_gestionnaire') + decision = Decision.objects.filter(administre_pam_id = administre_pam_id).select_related('poste') + decision = list(decision.values(*administres_keys)) + + if 'poste_id' in request.query_params: + q = "administre" + poste_id = request.query_params['poste_id'] + poste_pam_id = poste_id + str(annee_pam) + postes_keys = ( + 'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__administre__a_statut_pam_annee', + 'administre__a_fonction', 'administre__a_code_fonction', 'administre__a_liste_id_competences', + 'administre__grade_id', 'administre__a_liste_id_marques', 'de_decision', 'de_date_decision', + 'de_notes_gestionnaire', 'administre__a_notes_gestionnaire', 'administre__a_liste_id_competences', + 'administre__decision__poste_id') + + decision = Decision.objects.filter(poste_pam_id = poste_pam_id).select_related('administre') + + decision = list(decision.values(*postes_keys)) + res_decision = [] + if len(decision) > 0: + for k in range(len(decision)): + decision_unit = decision[k] + try: + administre_id = decision_unit.administre_id + except: + administre_id = None + + try: + poste_id = decision_unit.poste_id + except: + poste_id = None + + try: + res_decision_unit = {q: {}, 'no_score_administre': Notation.objects.get( + administre_id=administre_id, + poste_id=poste_id, pam_id = annee_pam).no_score_administre} + except: + res_decision_unit = {q: {}, 'no_score_administre': None} + for key in decision_unit: + if (q + "__") in key: + res_decision_unit[q][key.replace(q + '__', '')] = decision_unit[key] + else: + res_decision_unit[key] = decision_unit[key] + # Ajout du relevé des décisions sur le poste (cas q = "poste") + if q == "poste": + res_decision_unit[q]['p_nb_prepositionne'] = Decision.objects.filter( + de_decision=DecisionChoices.PREPOSITIONNE).count() + res_decision_unit[q]['p_nb_positionne'] = Decision.objects.filter( + de_decision=DecisionChoices.POSITIONNE).count() + res_decision_unit[q]['p_nb_omi_active'] = Decision.objects.filter( + de_decision=DecisionChoices.OMI_ACTIVE).count() + res_decision_unit[q]['p_nb_omi_en_cours'] = Decision.objects.filter( + de_decision=DecisionChoices.OMI_EN_COURS).count() + res_decision.append(res_decision_unit) + return Response(res_decision) + + @atomic + def perform_create(self, serializer) -> None: + """Cette fonction crée une decision à partir de données validées """ + + data = serializer.validated_data + annee_pam = self.request.query_params['pam__in'] + administre_id = data.get('administre_id') + administre_id_pam = str(administre_id) + annee_pam + poste_id = data.get('poste_id') + poste_id_pam = poste_id + annee_pam + de_decision = data.get('de_decision') + delete_former = data.get('delete_former') + a = get_object_or_404(Administre, pk=administre_id) + p = get_object_or_404(Poste, pk=poste_id) + self.check_object_permissions(self.request, a) + self.check_object_permissions(self.request, p) + + qs_former = Decision.objects.filter(pk=administre_id) + if delete_former: + qs_former.delete() + elif qs_former.exists(): + raise api_exception(status.HTTP_400_BAD_REQUEST, "une décision existe déjà pour cet administré") + + Decision.objects.create(administre_id=administre_id, poste_id=poste_id, de_decision=de_decision, de_date_decision=timezone.now(), + administre_pam_id=administre_id_pam, poste_pam_id=poste_id_pam ) + + def perform_update(self, serializer) -> None: + """ Met à jour la décision à partir de données validées. La date est mise à jour en même temps que le statut """ + data = serializer.validated_data + if hasattr(data, Decision.Cols.STATUT): + setattr(data, Decision.Cols.DATE, timezone.now()) + super().perform_update(serializer) diff --git a/backend-django/backend/views/exportation_fichiers.py b/backend-django/backend/views/exportation_fichiers.py new file mode 100644 index 0000000..c770c79 --- /dev/null +++ b/backend-django/backend/views/exportation_fichiers.py @@ -0,0 +1,580 @@ +import time +import pandas as pd +import numpy as np +from django.utils import timezone +from django.db.transaction import atomic +from django.http import Http404, HttpResponse +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..utils_extraction import APP_NAN +from ..utils.alimentation import BOCols, ReoCols +from ..serializers import ExportationSerializer +from ..models import Administre, Affectation, Administre_Notation, Poste, FichiersExporte +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count + + +class ExportationFichiersView(APIView): + """ Vue pour exporter les données de la base dans des fichiers au format de ceux utilisés pour l'insertion""" + + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = ExportationSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + def get(self, request): + resp = [] + resp.append(["Formulaire d\'OGURE NG permettant d'exporter les données de la base au format des fichiers d'insertion."]) + resp.append(["Selectionnez le fichier que vous souhaitez exporter puis appuyez sur le bouton 'POST' pour lancer l'exportation."]) + resp = pd.DataFrame(resp) + return Response(resp[0]) + + def export_adm_bo_df(self): + self.logger.info("Lecture et traitement de la table des administrés... ") + start_time_adm = time.time() + + Cols = Administre.Cols + administres = Administre.objects.all().values_list( + Cols.PK, + Cols.REL_GRADE, + 'a_nom', + 'a_prenom', + 'a_id_def', + 'a_eip', + 'a_eis', + 'a_domaine_gestion', + Cols.REL_DOMAINE, + Cols.REL_FILIERE, + 'a_nf', + 'a_arme', + 'a_rg_origine_recrutement', + 'a_date_entree_service', + 'a_nombre_enfants', + 'a_origine_recrutement', + 'a_date_naissance', + 'a_fonction', + 'a_diplome_hl', + 'a_dernier_diplome', + 'a_date_rdc', + 'a_credo_fe', + f'{Cols.REL_FORMATION_EMPLOI}__fe_garnison_lieu', + 'a_date_arrivee_fe', + 'a_date_dernier_acr', + 'a_pos_statuaire', + 'a_date_pos_statuaire', + 'a_interruption_service', + 'a_grade_date_debut', + 'a_profession_conjoint', + 'a_sap_conjoint', + 'a_id_def_conjoint', + 'a_sexe_conjoint', + 'a_date_fonction1', + 'a_fonction1', + 'a_date_fonction2', + 'a_fonction2', + 'a_date_fonction3', + 'a_fonction3', + 'a_date_fonction4', + 'a_fonction4', + 'a_date_fonction5', + 'a_fonction5', + 'a_date_fonction6', + 'a_fonction6', + 'a_date_fonction7', + 'a_fonction7', + 'a_date_fonction8', + 'a_fonction8', + 'a_date_fonction9', + 'a_fonction9', + 'a_date_mariage', + 'a_situation_fam', + 'a_sexe', + 'a_date_fud', + 'a_fud', + Cols.DATE_STATUT_CONCERTO, + Cols.STATUT_CONCERTO, + Cols.DATE_STATUT_CONCERTO_FUTUR, + Cols.STATUT_CONCERTO_FUTUR, + 'a_domaine_poste', + 'a_filiere_poste', + 'a_nf_poste', + 'a_lien_service', + 'a_marqueur_pn', + 'a_pls_gb_max', + 'a_enfants', + ) + + administres_df = pd.DataFrame.from_records(administres) + + adm_bo_cols = BOCols.columns( + BOCols.ID_SAP, # A + BOCols.GRADE, # B + BOCols.NOM, # C + BOCols.PRENOM, # D + BOCols.ID_DEF, # E + BOCols.EIP, # F + BOCols.EIS, # G + BOCols.DOMAINE_GESTION, # H + BOCols.DOMAINE, # I + BOCols.FILIERE, # J + BOCols.NF, # K + BOCols.ARME, # L + BOCols.REGROUPEMENT_ORIGINE_RECRUTEMENT, # M + BOCols.DATE_ENTREE_SERVICE, # N + BOCols.NOMBRE_ENFANTS, # O + BOCols.ORIGINE_RECRUTEMENT, # Q + BOCols.DATE_NAISSANCE, # R + BOCols.FONCTION, # T + BOCols.DIPLOME_PLUS_HAUT_NIVEAU, # U + BOCols.DERNIER_DIPLOME, # V + BOCols.DATE_RDC, # W + BOCols.CREDO_FE, # X + BOCols.GARNISON, # Z (utilisé dans la table des garnisons) + BOCols.DATE_ARRIVEE_FE, # AA + BOCols.DATE_DERNIER_ACR, # AC + BOCols.POSITION_STATUTAIRE, # AE + BOCols.DATE_POSITION_STATUAIRE, # AF + BOCols.INTERRUPTION_SERVICE, # AG + BOCols.DATE_DEBUT_GRADE, # AH + BOCols.PROFESSION_CONJOINT, # AL + BOCols.ID_SAP_CONJOINT, # AM + BOCols.ID_DEF_CONJOINT, # AN + BOCols.SEXE_CONJOINT, # AT + BOCols.DATE_FONCTION_1, # BM + BOCols.FONCTION_1, # BN + BOCols.DATE_FONCTION_2, # BO + BOCols.FONCTION_2, # BP + BOCols.DATE_FONCTION_3, # BQ + BOCols.FONCTION_3, # BR + BOCols.DATE_FONCTION_4, # BS + BOCols.FONCTION_4, # BT + BOCols.DATE_FONCTION_5, # BU + BOCols.FONCTION_5, # BV + BOCols.DATE_FONCTION_6, # BW + BOCols.FONCTION_6, # BX + BOCols.DATE_FONCTION_7, # BY + BOCols.FONCTION_7, # BZ + BOCols.DATE_FONCTION_8, # CA + BOCols.FONCTION_8, # CB + BOCols.DATE_FONCTION_9, # CC + BOCols.FONCTION_9, # CD + BOCols.DATE_MARIAGE, # CH + BOCols.SITUATION_FAMILIALE, # CI + BOCols.SEXE, # CJ + BOCols.DATE_FUD, # DV + BOCols.FUD, # DW + BOCols.DATE_STATUT_CONCERTO, # DX + BOCols.STATUT_CONCERTO, # DY + BOCols.DATE_STATUT_CONCERTO_FUTUR, # DZ + BOCols.STATUT_CONCERTO_FUTUR, # EA + BOCols.DOMAINE_POSTE, # ED (non utilisé) + BOCols.FILIERE_POSTE, # EE (non utilisé) + BOCols.NF_POSTE, # EF (non utilisé) + BOCols.DATE_LIEN_SERVICE, # EG + BOCols.MARQUEUR_PN, # EH + BOCols.PLS_GB_MAX, # EI + BOCols.ENFANTS, # EK + ) + + administres_df.columns = adm_bo_cols + + # Changement des types des colonnes dates + administres_df[BOCols.DATE_ARRIVEE_FE] = pd.to_datetime(administres_df[BOCols.DATE_ARRIVEE_FE], errors='coerce').dt.strftime('%d/%m/%Y') # AA + administres_df[BOCols.DATE_DEBUT_GRADE] = pd.to_datetime(administres_df[BOCols.DATE_DEBUT_GRADE], errors='coerce').dt.strftime('%d/%m/%Y') # AH + administres_df[BOCols.DATE_DERNIER_ACR] = pd.to_datetime(administres_df[BOCols.DATE_DERNIER_ACR], errors='coerce').dt.strftime('%d/%m/%Y') # AC + administres_df[BOCols.DATE_ENTREE_SERVICE] = pd.to_datetime(administres_df[BOCols.DATE_ENTREE_SERVICE], errors='coerce').dt.strftime('%d/%m/%Y') # N + administres_df[BOCols.DATE_FUD] = pd.to_datetime(administres_df[BOCols.DATE_FUD], errors='coerce').dt.strftime('%d/%m/%Y') # DV + administres_df[BOCols.DATE_LIEN_SERVICE] = pd.to_datetime(administres_df[BOCols.DATE_LIEN_SERVICE], errors='coerce').dt.strftime('%d/%m/%Y') # EG + administres_df[BOCols.DATE_NAISSANCE] = pd.to_datetime(administres_df[BOCols.DATE_NAISSANCE], errors='coerce').dt.strftime('%d/%m/%Y') # R + administres_df[BOCols.DATE_POSITION_STATUAIRE] = pd.to_datetime(administres_df[BOCols.DATE_POSITION_STATUAIRE], errors='coerce').dt.strftime('%d/%m/%Y') # AF + administres_df[BOCols.DATE_RDC] = pd.to_datetime(administres_df[BOCols.DATE_RDC], errors='coerce').dt.strftime('%d/%m/%Y') # W + administres_df[BOCols.DATE_STATUT_CONCERTO] = pd.to_datetime(administres_df[BOCols.DATE_STATUT_CONCERTO], errors='coerce').dt.strftime('%d/%m/%Y') # DX + administres_df[BOCols.DATE_STATUT_CONCERTO_FUTUR] = pd.to_datetime(administres_df[BOCols.DATE_STATUT_CONCERTO_FUTUR], errors='coerce').dt.strftime('%d/%m/%Y') # DZ + administres_df[BOCols.DATE_MARIAGE] = pd.to_datetime(administres_df[BOCols.DATE_MARIAGE], errors='coerce').dt.strftime('%d/%m/%Y') # CH + for i in range(1, 10): + administres_df[f'Fonction -{i} DD'] = pd.to_datetime(administres_df[f'Fonction -{i} DD']).dt.strftime('%d/%m/%Y') + + # Changement des types des colonnes bool + administres_df[[BOCols.MARQUEUR_PN]] = administres_df[[BOCols.MARQUEUR_PN]].replace([True, False], ['X', None]) + + # CHangement des types des colonnes int + administres_df[BOCols.ID_SAP_CONJOINT] = administres_df[BOCols.ID_SAP_CONJOINT].fillna(0).astype(int).replace({0: None}) + administres_df[BOCols.PLS_GB_MAX] = administres_df[BOCols.PLS_GB_MAX].fillna(0).astype(int).replace({0: None}) + + administres_df = (administres_df.fillna(APP_NAN) + .replace({APP_NAN: None}) + .replace({np.nan: None}) + .replace({'nan': None})) + + self.logger.info("Table des administrés lue et traitée en %d secondes : %s lignes et %s colonnes extraites\n", time.time()-start_time_adm, administres_df.shape[0], administres_df.shape[1]) + return administres_df + + + def export_aff_bo_df(self): + self.logger.info("Lecture et traitement de la table des affectations... ") + start_time_aff = time.time() + + aff = pd.DataFrame.from_records(Affectation.objects.all().values()).sort_values('affect_date', ascending=False) + + aff_grouped = aff.groupby(['administre_id']).agg( + affect_libelle=('affect_libelle', ',,,'.join), + affect_date=('affect_date', ',,,'.join), + ) + + aff_date_cols = [f'Affectation -{i} DD' for i in range(1,10)] + aff_libelle_cols = [f'Affectation -{i} L' for i in range(1,10)] + + aff_date_df = aff_grouped['affect_date'].str.split(',,,', expand=True) + aff_libelle_df = aff_grouped['affect_libelle'].str.split(',,,', expand=True) + + nb_cols_date = aff_date_df.shape[1] + nb_cols_libelle = aff_date_df.shape[1] + + aff_date_cols_tronc = aff_date_cols[:nb_cols_date] + aff_libelle_cols_tronc = aff_libelle_cols[:nb_cols_libelle] + + aff_grouped[aff_date_cols_tronc] = aff_date_df + aff_grouped[aff_libelle_cols_tronc] = aff_libelle_df + + aff_grouped = (aff_grouped.drop(['affect_libelle', 'affect_date'], axis=1) + .reset_index()) + + # Changement des types des colonnes dates + for i in range(1, 10): + aff_grouped[f'Affectation -{i} DD'] = pd.to_datetime(aff_grouped[f'Affectation -{i} DD']).dt.strftime('%d/%m/%Y') + + aff_grouped = (aff_grouped.fillna(APP_NAN) + .replace({APP_NAN: None}) + .replace({np.nan: None}) + .replace({'nan': None})) + + self.logger.info("Table des affectations lue et traitée en %d secondes : %s lignes et %s colonnes extraites\n", time.time()-start_time_aff, aff_grouped.shape[0], aff_grouped.shape[1]) + return aff_grouped + + + def export_no_bo_df(self): + self.logger.info("Lecture et traitement de la table des notations... ") + start_time_no = time.time() + + no = pd.DataFrame.from_records(Administre_Notation.objects.all().values()).sort_values('no_age_annees', ascending=False) + + no_grouped = no.groupby(["administre_id"]).agg( + no_annne_de_notation=("no_annne_de_notation", ",,,".join), + no_nr_ou_iris=("no_nr_ou_iris", ",,,".join), + no_rac_ou_iris_cumule=("no_rac_ou_iris_cumule", ",,,".join), + no_rf_qsr=("no_rf_qsr", ",,,".join), + no_aptitude_emploie_sup=("no_aptitude_emploie_sup", ",,,".join), + no_potentiel_responsabilite_sup=("no_potentiel_responsabilite_sup", ",,,".join), + no_age_annees=("no_age_annees", ",,,".join), + ) + + no_age_annees_cols = ['Age en années (au 31/12)'] + no_annne_de_notation_cols = ['Année notation A'] + [f'Année notation A-{i}' for i in range(1,6)] + no_nr_ou_iris_cols = ['IRIS / RAC retenu A'] + [f'IRIS / RAC retenu A-{i}' for i in range(1,6)] + no_rac_ou_iris_cumule_cols = ['NR/NGC cumulé A'] + [f'NR/NGC cumulé A-{i}' for i in range(1,6)] + no_rf_qsr_cols = ['QSR A'] + [f'QSR A-{i}' for i in range(1,6)] + no_aptitude_emploie_sup_cols = ['Apt resp / Emp sup A'] + [f'Apt resp / Emp sup A-{i}' for i in range(1,6)] + no_potentiel_responsabilite_sup_cols = ['Potentiel responsabilités catégorie sup A'] + [f'Potentiel responsabilités catégorie sup A-{i}' for i in range(1,6)] + + no_grouped[no_age_annees_cols] = no_grouped['no_age_annees'].str.split(',,,', expand=True)[0] + no_grouped[no_annne_de_notation_cols] = no_grouped['no_annne_de_notation'].str.split(',,,', expand=True) + no_grouped[no_nr_ou_iris_cols] = no_grouped['no_nr_ou_iris'].str.split(',,,', expand=True) + no_grouped[no_rac_ou_iris_cumule_cols] = no_grouped['no_rac_ou_iris_cumule'].str.split(',,,', expand=True) + no_grouped[no_rf_qsr_cols] = no_grouped['no_rf_qsr'].str.split(',,,', expand=True) + no_grouped[no_aptitude_emploie_sup_cols] = no_grouped['no_aptitude_emploie_sup'].str.split(',,,', expand=True) + no_grouped[no_potentiel_responsabilite_sup_cols] = no_grouped['no_potentiel_responsabilite_sup'].str.split(',,,', expand=True) + + no_grouped = (no_grouped.drop(['no_annne_de_notation', + 'no_nr_ou_iris', + 'no_rac_ou_iris_cumule', + 'no_rf_qsr', + 'no_aptitude_emploie_sup', + 'no_potentiel_responsabilite_sup', + 'no_age_annees'], axis=1) + .reset_index()) + + # Changement des types des colonnes int + no_grouped[f'Année notation A'] = no_grouped[f'Année notation A'].astype(float).fillna(0).astype(int).replace({0: None}) + no_grouped[f'IRIS / RAC retenu A'] = no_grouped[f'IRIS / RAC retenu A'].astype(float).fillna(0).astype(int).replace({0: None}) + no_grouped[f'NR/NGC cumulé A'] = no_grouped[f'NR/NGC cumulé A'].astype(float).fillna(0).astype(int).replace({0: None}) + for i in range(1, 6): + no_grouped[f'Année notation A-{i}'] = no_grouped[f'Année notation A-{i}'].astype(float).fillna(0).astype(int).replace({0: None}) + no_grouped[f'IRIS / RAC retenu A-{i}'] = no_grouped[f'IRIS / RAC retenu A-{i}'].astype(float).fillna(0).astype(int).replace({0: None}) + no_grouped[f'NR/NGC cumulé A-{i}'] = no_grouped[f'NR/NGC cumulé A-{i}'].astype(float).fillna(0).astype(int).replace({0: None}) + + no_grouped = (no_grouped.fillna(APP_NAN) + .replace({APP_NAN: None}) + .replace({np.nan: None}) + .replace({'nan': None})) + + self.logger.info("Table des notations lue et traitée en %d secondes : %s lignes et %s colonnes extraites\n", time.time()-start_time_no, no_grouped.shape[0], no_grouped.shape[1]) + return no_grouped + + + def export_pos_reo_df(self): + self.logger.info("Lecture et traitement de la table des postes... ") + start_time_pos = time.time() + + Cols = Poste.Cols + postes = Poste.objects.all().values_list( + 'p_annee', + Cols.REL_FORMATION_EMPLOI, + 'p_dep', + Cols.CATEGORIE, + 'p_eip', + Cols.REL_DOMAINE, + Cols.REL_FILIERE, + Cols.NIVEAU_FONCTIONNEL, + Cols.PK, + f'{Cols.REL_FONCTION}__fon_id', + f'{Cols.REL_FONCTION}__fon_libelle', + 'p_nfs', + ) + + postes_df = pd.DataFrame.from_records(postes) + + pos_reo_cols = ReoCols.columns( + ReoCols.ANNEE_PROJET, # B + ReoCols.FORMATION_EMPLOI, # F + ReoCols.CODE_POSTAL, # J + ReoCols.CATEGORIE, # P + ReoCols.EIP, # T + ReoCols.DOMAINE, # U + ReoCols.FILIERE, # V + ReoCols.CODE_NF, # W + ReoCols.ID_POSTE, # X + ReoCols.FONCTION_ID, # Y + ReoCols.FONCTION_LIBELLE, # Z + ReoCols.DOMAINE_GESTION, # AA + ) + + postes_df.columns = pos_reo_cols + + postes_df = (postes_df.fillna(APP_NAN) + .replace({APP_NAN: None}) + .replace({np.nan: None}) + .replace({'nan': None})) + + self.logger.info("Table des postes lue et traitée en %d secondes : %s lignes et %s colonnes extraites\n", time.time()-start_time_pos, postes_df.shape[0], postes_df.shape[1]) + return postes_df + + + @atomic + # @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def export_bo(self, request): + self.logger.info("--------- Début de l'exportation du fichier 'Données BO' --------- ") + start_time_export = time.time() + + administres_df = self.export_adm_bo_df() + affectations_df = self.export_aff_bo_df() + notations_df = self.export_no_bo_df() + + bo_df = administres_df.merge(affectations_df, how='left', left_on=BOCols.ID_SAP, right_on='administre_id') + bo_df = bo_df.drop(['administre_id'], axis=1) + bo_df = bo_df.merge(notations_df, how='left', left_on=BOCols.ID_SAP, right_on='administre_id') + bo_df = bo_df.drop(['administre_id'], axis=1) + + bo_cols = BOCols.columns( + BOCols.ID_SAP, # A + BOCols.GRADE, # B + BOCols.NOM, # C + BOCols.PRENOM, # D + BOCols.ID_DEF, # E + BOCols.EIP, # F + BOCols.EIS, # G + BOCols.DOMAINE_GESTION, # H + BOCols.DOMAINE, # I + BOCols.FILIERE, # J + BOCols.NF, # K + BOCols.ARME, # L + BOCols.REGROUPEMENT_ORIGINE_RECRUTEMENT, # M + BOCols.DATE_ENTREE_SERVICE, # N + BOCols.NOMBRE_ENFANTS, # O + BOCols.ORIGINE_RECRUTEMENT, # Q + BOCols.DATE_NAISSANCE, # R + BOCols.FONCTION, # T + BOCols.DIPLOME_PLUS_HAUT_NIVEAU, # U + BOCols.DERNIER_DIPLOME, # V + BOCols.DATE_RDC, # W + BOCols.CREDO_FE, # X + BOCols.GARNISON, # Z (utilisé dans la table des garnisons) + BOCols.DATE_ARRIVEE_FE, # AA + BOCols.DATE_DERNIER_ACR, # AC + BOCols.POSITION_STATUTAIRE, # AE + BOCols.DATE_POSITION_STATUAIRE, # AF + BOCols.INTERRUPTION_SERVICE, # AG + BOCols.DATE_DEBUT_GRADE, # AH + BOCols.PROFESSION_CONJOINT, # AL + BOCols.ID_SAP_CONJOINT, # AM + BOCols.ID_DEF_CONJOINT, # AN + BOCols.SEXE_CONJOINT, # AT + BOCols.DATE_AFFECTATION_1, # AU + BOCols.AFFECTATION_1, # AV + BOCols.DATE_AFFECTATION_2, # AW + BOCols.AFFECTATION_2, # AX + BOCols.DATE_AFFECTATION_3, # AY + BOCols.AFFECTATION_3, # AZ + BOCols.DATE_AFFECTATION_4, # BA + BOCols.AFFECTATION_4, # BB + BOCols.DATE_AFFECTATION_5, # BC + BOCols.AFFECTATION_5, # BD + BOCols.DATE_AFFECTATION_6, # BE + BOCols.AFFECTATION_6, # BF + BOCols.DATE_AFFECTATION_7, # BG + BOCols.AFFECTATION_7, # BH + BOCols.DATE_AFFECTATION_8, # BI + BOCols.AFFECTATION_8, # BJ + BOCols.DATE_AFFECTATION_9, # BK + BOCols.AFFECTATION_9, # BL + BOCols.DATE_FONCTION_1, # BM + BOCols.FONCTION_1, # BN + BOCols.DATE_FONCTION_2, # BO + BOCols.FONCTION_2, # BP + BOCols.DATE_FONCTION_3, # BQ + BOCols.FONCTION_3, # BR + BOCols.DATE_FONCTION_4, # BS + BOCols.FONCTION_4, # BT + BOCols.DATE_FONCTION_5, # BU + BOCols.FONCTION_5, # BV + BOCols.DATE_FONCTION_6, # BW + BOCols.FONCTION_6, # BX + BOCols.DATE_FONCTION_7, # BY + BOCols.FONCTION_7, # BZ + BOCols.DATE_FONCTION_8, # CA + BOCols.FONCTION_8, # CB + BOCols.DATE_FONCTION_9, # CC + BOCols.FONCTION_9, # CD + BOCols.AGE_ANNEES, # CG + BOCols.DATE_MARIAGE, # CH + BOCols.SITUATION_FAMILIALE, # CI + BOCols.SEXE, # CJ + BOCols.ANNEE_NOTATION, # CL + BOCols.RAC_OU_IRIS_CUMULE, # CM + BOCols.NR_OU_IRIS, # CN + BOCols.RF_QSR, # CO + BOCols.APTITUDE_EMPLOI_SUP, # CP + BOCols.POTENTIEL_RESPONSABILITE_SUP, # CQ + BOCols.ANNEE_NOTATION_1, # CR + BOCols.RAC_OU_IRIS_CUMULE_1, # CS + BOCols.NR_OU_IRIS_1, # CT + BOCols.RF_QSR_1, # CU + BOCols.APTITUDE_EMPLOI_SUP_1, # CV + BOCols.POTENTIEL_RESPONSABILITE_SUP_1, # CW + BOCols.ANNEE_NOTATION_2, # CX + BOCols.RAC_OU_IRIS_CUMULE_2, # CY + BOCols.NR_OU_IRIS_2, # CZ + BOCols.RF_QSR_2, # DA + BOCols.APTITUDE_EMPLOI_SUP_2, # DB + BOCols.POTENTIEL_RESPONSABILITE_SUP_2, # DC + BOCols.ANNEE_NOTATION_3, # DD + BOCols.RAC_OU_IRIS_CUMULE_3, # DE + BOCols.NR_OU_IRIS_3, # DF + BOCols.RF_QSR_3, # DG + BOCols.APTITUDE_EMPLOI_SUP_3, # DH + BOCols.POTENTIEL_RESPONSABILITE_SUP_3, # DI + BOCols.ANNEE_NOTATION_4, # DJ + BOCols.RAC_OU_IRIS_CUMULE_4, # DK + BOCols.NR_OU_IRIS_4, # DL + BOCols.RF_QSR_4, # DM + BOCols.APTITUDE_EMPLOI_SUP_4, # DN + BOCols.POTENTIEL_RESPONSABILITE_SUP_4, # DO + BOCols.ANNEE_NOTATION_5, # DP + BOCols.RAC_OU_IRIS_CUMULE_5, # DQ + BOCols.NR_OU_IRIS_5, # DR + BOCols.RF_QSR_5, # DS + BOCols.APTITUDE_EMPLOI_SUP_5, # DT + BOCols.POTENTIEL_RESPONSABILITE_SUP_5, # DU + BOCols.DATE_FUD, # DV + BOCols.FUD, # DW + BOCols.DATE_STATUT_CONCERTO, # DX + BOCols.STATUT_CONCERTO, # DY + BOCols.DATE_STATUT_CONCERTO_FUTUR, # DZ + BOCols.STATUT_CONCERTO_FUTUR, # EA + BOCols.DOMAINE_POSTE, # ED (non utilisé) + BOCols.FILIERE_POSTE, # EE (non utilisé) + BOCols.NF_POSTE, # EF (non utilisé) + BOCols.DATE_LIEN_SERVICE, # EG + BOCols.MARQUEUR_PN, # EH + BOCols.PLS_GB_MAX, # EI + BOCols.ENFANTS, # EK + ) + + bo_df = bo_df.reindex(columns=bo_cols) + + if bo_df.empty: + return Response("Aucune donnée à extraire.") + else: + try: + self.logger.info("Exportation du fichier 'Données BO'...") + start_time_export_bo = time.time() + + now = timezone.now().date() + bo_df.to_excel(f'..\\fichiers_exportes\\{now}_donnees_BO_export.xlsx', index=False, header=True) + bo_obj = FichiersExporte(nom_fichier=f'{now}_donnees_BO_export.xlsx') + bo_obj.save() + + self.logger.info("Exportation du fichier 'Données BO' réalisée en %d secondes : %s lignes et %s colonnes exportées", time.time()-start_time_export_bo, bo_df.shape[0], bo_df.shape[1]) + self.logger.info("---- Exportation totale réalisée en : %d minutes et %d secondes ----\n", (time.time()-start_time_export)//60, (time.time()-start_time_export)%60) + return Response("Exportation terminée. Vous trouverez le fichier 'Données BO' exporté dans le dossier 'fichiers_exportes'.") + except (Http404, APIException): + raise + except BaseException: + message = "Imposible de réaliser l'exportation du fichier 'Données BO'. Vérifiez que les fichiers Excel d'exportation sont bien fermés." + self.logger.exception(message) + raise APIException(message) + + + @atomic + # @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def export_reo(self, request): + self.logger.info("--------- Début de l'exportation du fichier 'REO' --------- ") + start_time_export = time.time() + + postes_df = self.export_pos_reo_df() + reo_df = postes_df + + if reo_df.empty: + return Response("Aucune donnée à extraire.") + else: + try: + self.logger.info("Exportation du fichier 'REO'...") + start_time_export_reo = time.time() + + now = timezone.now().date() + reo_df.to_excel(f'..\\fichiers_exportes\\{now}_REO_export.xlsx', index=False, header=True) + reo_obj = FichiersExporte(nom_fichier=f'{now}_REO_export.xlsx') + reo_obj.save() + + self.logger.info("Exportation du fichier 'REO' réalisée en %d secondes : %s lignes et %s colonnes exportées", time.time()-start_time_export_reo, reo_df.shape[0], reo_df.shape[1]) + self.logger.info("------- Exportation totale réalisée en : %d secondes ------\n", time.time()-start_time_export) + return Response("Exportation terminée. Vous trouverez le fichier 'REO' exporté dans le dossier 'fichiers_exportes'.") + except (Http404, APIException): + raise + except BaseException: + message = "Imposible de réaliser l'exportation du fichier 'REO'. Vérifiez que les fichiers Excel d'exportation sont bien fermés." + self.logger.exception(message) + raise APIException(message) + + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + nom_fichier = serializer.validated_data.get('fichier_exporte') + + if nom_fichier == '1': + resp = self.export_bo(request) + return resp + if nom_fichier == '2': + resp = self.export_reo(request) + return resp + return Response("Imposible de réaliser l'exportation.") + + diff --git a/backend-django/backend/views/fiche_detaillee.py b/backend-django/backend/views/fiche_detaillee.py new file mode 100644 index 0000000..831b354 --- /dev/null +++ b/backend-django/backend/views/fiche_detaillee.py @@ -0,0 +1,110 @@ +from django.forms import model_to_dict +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import FMOB, Administre, FormationEmploi +from ..utils.decorators import class_logger +from .commun import (GestionnairePermission, execution_time_viewset, + query_count_viewset) + + +@class_logger +@execution_time_viewset +@query_count_viewset +class FicheDetailleeView(APIView): + """ + Cette classe est dédiée au vue de la fiche détaillée des administrés + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + + def get(self, request: Request) -> Response: + """La fonction get recupére les infos administrés + + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant de l'administré + + :return: - **Response** (*objet*): informations concernant l'administré, informations sur le formulaire de mobilité. + """ + try: + res = {"result": "error"} + if 'administre_id' in request.query_params: + administre_id = request.query_params['administre_id'] + annee_pam = request.query_params['pam__in'] + administre_id_pam = request.query_params['administre_id'] + annee_pam + administre = Administre.objects.get(a_id_sap=administre_id) + res = model_to_dict(administre, fields=[field.name for field in administre._meta.fields if field != 'a_liste_id_competences']) + res['a_liste_id_competences'] = ','.join(list(administre.a_liste_id_competences.values_list('comp_libelle', flat=True))) + fmobs = FMOB.objects.filter(administre_id=administre_id_pam) + res["fmob"] = None + res['pam'] = None + + if fmobs.exists(): + fmob_dict = model_to_dict(fmobs[0]) + res["fmob"] = fmob_dict + + res["fe"] = None + fe = list(FormationEmploi.objects.filter(fe_code=administre.formation_emploi_id).values( + *('fe_code', 'fe_code_postal', 'fe_garnison_lieu', 'fe_libelle'))) + # if fe.exists(): + # fe = fe[0] + # garnison_dict = model_to_dict() + # res["garnison"] = garnison_dict + if len(fe): + res["fe"] = fe[0] + res["conjoint"] = None + conjoint = Administre.objects.filter(a_id_sap=administre.a_sap_conjoint) + if conjoint.exists(): + self.logger.debug('has conjoint') + conjoint_dict = model_to_dict(conjoint[0]) + conjoint_dict['pam'] = None + conjoint_dict["fe"] = None + fe_conjoint = list(FormationEmploi.objects.filter(fe_code=conjoint_dict['formation_emploi']).values( + *('fe_code', 'fe_code_postal', 'fe_garnison_lieu', 'fe_libelle'))) + if len(fe): + conjoint_dict["fe"] = fe_conjoint[0] + + # Verifier si le conjoint est formobé + conjoint_dict["fmob_O_N"] = "Oui" if FMOB.objects.filter(administre_id=str(conjoint_dict['a_id_sap']) + annee_pam).exists() else "Non" + res["conjoint"] = conjoint_dict + + adm_affect = list(administre.adm_affec.values()) + adm_affect_sorted = sorted(adm_affect, key=lambda d: d['affect_date'], reverse=True) + for i in range(len(adm_affect_sorted)): + res['a_affectation{}'.format(i + 1)] = adm_affect_sorted[i]['affect_libelle'] + res['a_date_affectation{}'.format(i + 1)] = adm_affect_sorted[i]['affect_date'] + + adm_dip = list(administre.adm_dip.values()) + adm_dip_sorted = sorted(adm_dip, key=lambda d: d['diplome_date'], reverse=True) + for i in range(len(adm_dip)): + res['a_diplome_{}'.format(i + 1)] = adm_dip_sorted[i]['diplome_libelle'] + res['a_diplome_{}_date'.format(i + 1)] = adm_dip_sorted[i]['diplome_date'] + res['a_diplome_{}_note'.format(i + 1)] = adm_dip_sorted[i]['diplome_note'] + + adm_fud = list(administre.adm_fud.values()) + adm_fud_sorted = sorted(adm_fud, key=lambda d: d['fud_date_debut'], reverse=True) + for i in range(len(adm_fud)): + res['a_fud_{}_dd'.format(i + 1)] = adm_fud_sorted[i]['fud_date_debut'] + res['a_fud_{}_df'.format(i + 1)] = adm_fud_sorted[i]['fud_date_fin'] + res['a_fud_{}_l'.format(i + 1)] = adm_fud_sorted[i]['fud_libelle'] + + adm_not = list(administre.adm_not.values()) + adm_not_sorted = sorted(adm_not, key=lambda d: d['no_age_annees'], reverse=False) + for i in range(len(adm_not_sorted)): + res['no_annne_de_notation_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_annne_de_notation'] + res['no_nr_ou_iris_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_nr_ou_iris'] + res['no_rac_ou_iris_cumule_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_rac_ou_iris_cumule'] + res['no_rf_qsr_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_rf_qsr'] + res['no_aptitude_emploie_sup_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_aptitude_emploie_sup'] + res['no_potentiel_responsabilite_sup_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_potentiel_responsabilite_sup'] + res['no_age_annees_A_{}'.format(i + 1)] = adm_not_sorted[i]['no_age_annees'] + return Response(res) + except (Http404, APIException): + raise + except: + message = "impossible d'afficher la vue détaillée" + self.logger.exception(message) + raise APIException(message) diff --git a/backend-django/backend/views/formation_emploi.py b/backend-django/backend/views/formation_emploi.py new file mode 100644 index 0000000..931acbc --- /dev/null +++ b/backend-django/backend/views/formation_emploi.py @@ -0,0 +1,23 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ModelViewSet + +from ..models import FormationEmploi +from ..serializers.formation_emploi import FormationEmploiSerializer +from .commun import (GestionnairePermission, execution_time_viewset, + query_count_viewset) + + +@execution_time_viewset +@query_count_viewset +class FormationEmploiView(ModelViewSet): + """ + Cette classe est dédiée au vue des FormationEmplois. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = FormationEmploiSerializer + + # important : mettre à jour quand le serializer change + def get_queryset(self): + Cols = FormationEmploi.Cols + return FormationEmploi.objects.select_related(Cols.REL_MERE) + diff --git a/backend-django/backend/views/initial.py b/backend-django/backend/views/initial.py new file mode 100644 index 0000000..d46fd92 --- /dev/null +++ b/backend-django/backend/views/initial.py @@ -0,0 +1,1463 @@ +from ast import For +import time +from datetime import date +#from tkinter.tix import Form + +import numpy as np +import pandas as pd +from django.db.models import Case, IntegerField, Prefetch, Sum, When +from django.db.transaction import atomic +from django.forms import model_to_dict +from django.http import Http404, JsonResponse +from django.shortcuts import get_object_or_404 +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import status, viewsets +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from django.db.models import Q + + + +from .. import constants +from ..filters import AdministreFilter, AdministrePAMFilter, PosteFilter, PostePAMFilter, RelatedOrderingFilter +from ..models import (FMOB, Administre, Decision, DecisionChoices, Domaine, + Filiere, FormationEmploi, Marque, MarquesGroupe, + PcpFeGroupe, Poste, PreferencesListe, SousVivier, + SousVivierAssociation, SpecifiqueChoices, Administres_Pams, StatutPamChoices as StatutPam, + Postes_Pams,PAM) +from ..paginations import HeavyDataPagination +from ..reporting import (reporting_suivi_pam_admin, reporting_suivi_pam_poste, + reporting_taux_armement_gestionnaire, + reporting_taux_armement_pcp) +from ..serializers import (CTX_KEY_DECISIONS, CTX_KEY_PROFILES, + AdministreSerializer, + AdministresPamsSerializer, + AlimentationReferentielSerializer, + DomaineSerializer, FileSVSerializer, + FiliereSerializer, FmobSerializer, MarqueSerializer, + MarquesGroupeSerializer, PcpFeGroupeSerializer, + PosteSerializer, SousVivierAssociationSerializer, + PostesPamsSerializer) +from ..utils import sous_viviers_du_cellule, without_keys, generate_sv_id, nf2categorie +from ..utils.decisions import get_available_decisions +from ..utils.decorators import class_logger +from ..utils.permissions import get_profiles_by_adm +from ..utils_extraction import (DataFrameTypes, FileTypes, open_excel, + read_files_by_type) +from .commun import (GestionnairePermission, execution_time_viewset, + query_count_viewset) + + +# Vue de calcul des indicateurs +# Renvoie les indicateurs pour les vues : +# - taux armement FE pour les PCP +# - taux armement FE pour les gestionnaires +# - suivi pam des gestionnaires pour les administrés +# - suivi pam des gestionnaires pour les postes +@class_logger +@execution_time_viewset +@query_count_viewset +class ReportingView(APIView): + """ + Cette classe est dédiée au vue du calcul des indicateurs + - taux armement FE pour les PCP + - taux armement FE pour les gestionnaires + - suivi pam des gestionnaires pour les administrés + - suivi pam des gestionnaires pour les postes + + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + + def __str__(self): + return 'ReportingView' + + def get(self, request): + """La fonction get renvoie les indicateurs + + :type request: rest_framework.request.Request + :param request: Request contenant le type, sous_vivier id, niveau fonctinonel et l'id de la formation emploi. + + :return: - **return** (*json*): Si le type est SUIVI_PAM_GESTIONNAIRE le json contiendra l'information sur le suivi pam des + gestionnaires pour les administrés et les postes. Si le type est TAUX_ARMEMENT_FE le json contiendra les indicateurs sur les taux d'armement. + + + """ + type = request.query_params["type"] + + sv_id = [] + if 'sv_id' in request.query_params: + sv_id = request.query_params["sv_id"] + + if 'vue' in request.query_params: + vue = request.query_params["vue"] + + if "nf" in request.query_params: + nf = request.query_params.getlist("nf") + else: + nf = ['1A', '1B', '1C', '2.', '3A', '3B', '3B NFS', '4.', '5A', '5B', '5C', '6A', '6B'] + + if 'fe_id' in request.query_params: # a voir + fe_id = request.query_params.getlist('fe_id') + else: + fe_id = list(FormationEmploi.objects.values_list('fe_code', flat=True)) + + # Vues pour les indicateurs sur le suivi du pam + if (type == "SUIVI_PAM_GESTIONNAIRE"): + if "d_code" in request.query_params: + d_id = request.query_params.getlist("d_code") + else: + filieres = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id', + flat=True).distinct()) + d_id = list(Filiere.objects.filter(f_code__in=filieres).values_list('domaine_id', flat=True).distinct()) + if "f_code" in request.query_params: + f_id = request.query_params.getlist("f_code") + else: + f_id = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id', + flat=True).distinct()) + if "categorie" in request.query_params: + categorie = request.query_params.getlist("categorie") + else: + categorie = list( + SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('sva_categorie', + flat=True).distinct()) + # résultats pour la vue suivi pam des gestionnaires pour les administrés + nb_a_etudier, nb_a_muter, nb_a_maintenir, nb_non_etudie_administres, nb_a_partant, nb_a_non_dispo, nb_prepos_administres, nb_pos_administres, nb_omi_active_administres, nb_omi_en_cours_administres, reste_a_realiser_administres, reste_a_realiser_a_etudier, reste_a_realiser_a_muter = reporting_suivi_pam_admin( + sv_id, f_id, d_id, nf, categorie) + # résultats pour la vue suivi pam des gestionnaires pour les postes + nb_p1, nb_p2, nb_p3, nb_p4, nb_gele, nb_non_etudie_postes, reste_a_realiser_postes, reste_a_realiser_p1, reste_a_realiser_p2, reste_a_realiser_p3, reste_a_realiser_p4, nb_prepos_postes, nb_pos_postes, nb_omi_active_postes, nb_omi_en_cours_postes = reporting_suivi_pam_poste( + sv_id, f_id, d_id, nf, categorie) + return JsonResponse( + {"nb_a_etudier": nb_a_etudier, "nb_a_muter": nb_a_muter, "nb_a_maintenir": nb_a_maintenir, + "nb_a_partant": nb_a_partant, "nb_a_non_dispo": nb_a_non_dispo, + "nb_non_etudie_administres": nb_non_etudie_administres, + "nb_prepos_administres": nb_prepos_administres, "nb_pos_administres": nb_pos_administres, + "nb_omi_en_cours_administres": nb_omi_en_cours_administres, + "nb_omi_active_administres": nb_omi_active_administres, + "reste_a_realiser_administres": reste_a_realiser_administres, + "reste_a_realiser_a_etudier": reste_a_realiser_a_etudier, + "reste_a_realiser_a_muter": reste_a_realiser_a_muter, + "nb_p1": nb_p1, "nb_p2": nb_p2, "nb_p3": nb_p3, "nb_p4": nb_p4, "nb_gele": nb_gele, + "nb_non_etudie_postes": nb_non_etudie_postes, "reste_a_realiser_postes": reste_a_realiser_postes, + "reste_a_realiser_p1": reste_a_realiser_p1, "reste_a_realiser_p2": reste_a_realiser_p2, + "reste_a_realiser_p3": reste_a_realiser_p3, "reste_a_realiser_p4": reste_a_realiser_p4, + "nb_prepos_postes": nb_prepos_postes, "nb_pos_postes": nb_pos_postes, + "nb_omi_en_cours_postes": nb_omi_en_cours_postes, "nb_omi_active_postes": nb_omi_active_postes}, + safe=False) + # Vues pour les indicateurs sur les taux d'armement + elif (type == 'TAUX_ARMEMENT_FE'): + if "d_code" in request.query_params: + d_id = request.query_params.getlist("d_code") + else: + d_id = list(Domaine.objects.all().values_list('d_code', flat=True)) + if "f_code" in request.query_params: + f_id = request.query_params.getlist("f_code") + else: + f_id = list(Filiere.objects.all().values_list('f_code', flat=True)) + if "categorie" in request.query_params: + categorie = request.query_params.getlist("categorie") + else: + categorie = ['MDR', 'SOFF', 'OFF', 'OGX'] + + if len(sv_id) == 0: + # résultats pour la vue taux armement FE pour les PCP + nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_pcp( + fe_id, f_id, d_id, nf, categorie) + else: + # résultats pour la vue taux armement FE pour les gestionnaires + nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_gestionnaire( + fe_id, f_id, d_id, nf, categorie, sv_id) + return JsonResponse({'nb_militaires_actuel': nb_militaires_actuel, "nb_postes_actuel": nb_postes_actuel, + 'ecart_actuel': ecart_actuel, 'taux_armement_actuel': taux_armement_actuel, + 'nb_militaires_entrants': nb_militaires_entrants, + 'nb_militaires_sortants': nb_militaires_sortants, + 'nb_militaires_projete': nb_militaires_projete, + 'taux_armement_projete': taux_armement_projete, + 'taux_armement_cible': taux_armement_cible}, safe=False) + else: + return JsonResponse({"autre vue": "autres indicateurs demandes"}) + + +# TODO : Supprimer cette vue qui n'est plus utilisée +# Vue de chargement des sous-viviers : +# - Charge et traite le fichier de définition des sous-viviers +# - Attribue les sous-viviers présents aux administrés et postes correspondants +@class_logger +@execution_time_viewset +@query_count_viewset +class ChargementSVView(APIView): + """ + Cette classe est dédiée au vue de chargement des sous-viviers. Charge et traite le fichier de définition des sous-viviers et attribue les sous-viviers présents aux administrés et postes correspondants + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + + serializer_class = FileSVSerializer + + def get(self, request): + """La fonction get renvoie une reponse contenant ok + + :type request: rest_framework.request.Request + :param request: Requset + + + :return: - **return** (*json*): json contenant "ok". + """ + return Response({"get": "ok"}) + + def post(self, request): + """La fonction post charge les sous-viviers + + :type request: rest_framework.request.Request + :param request: Request contenant le fichier SV + + :return: - **Response** (*Response*): Reponse contient la liste des sous-viviers créés, ignorés et où il y a eu une erreur. + """ + serializer = FileSVSerializer(data=request.data) + + if not serializer.is_valid(): + return Response( + data=serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + sv_file = request.data['SV'] + + sv_df = open_excel(sv_file, sheetname="GESTIONNAIRES", engine='openpyxl') + + sv = pd.DataFrame(columns=['gestionnaire_id_sap', 'f_code', 'asso_sv_categorie', 'arme']) + + for i in range(len(sv_df)): + sv.loc[i] = [sv_df.iloc[i, 0], sv_df.iloc[i, 3], sv_df.iloc[i, 4], sv_df.iloc[i, 5]] + + sv.dropna(subset=['f_code'], inplace=True) + sv['arme'] = sv['arme'].replace({np.nan: None}) + sv.reset_index(drop=True, inplace=True) + sv['asso_sv_categorie'].replace({"SOUS-OFFICIER": "SOFF", "OFFICIER": "OFF", "MILITAIRE DU RANG": "MDR"}, + inplace=True) + sv['sv_id'] = sv.f_code + '_' + sv.asso_sv_categorie + sv.f_code = sv.f_code.apply(lambda x: x.split(',')) + sv = sv.explode(column='f_code') + sv.reset_index(drop=True, inplace=True) + errors = [] + created = [] + ignored = [] + sv.fillna(np.nan, inplace=True) + sv.replace([np.nan], [None], inplace=True) + self.logger.debug(sv.head()) + + for i in range(len(sv)): + try: + filiere = Filiere.objects.get(f_code=sv.at[i, "f_code"]) + categorie = sv.at[i, "asso_sv_categorie"] + asso_sv = SousVivierAssociation.objects.filter(sva_categorie=categorie, + filiere__f_code=sv.at[i, "f_code"]) + + # Vérifier que filiere et catégorie n'ont pas déjà de sous vivier + if asso_sv.count() == 0 and categorie is not None: + # Créer le sous-vivier correspondant dans la table SousVivier + sous_vivier = SousVivier(sv_id=sv.at[i, "sv_id"], + sv_libelle=str(sv.at[i, "sv_id"])) + self.logger.debug(model_to_dict(sous_vivier)) + sous_vivier.save() + + self.logger.debug(sv.at[i, "sv_id"]) + + # Insertion du nouveau sous-vivier dans la table d'association (sans le droit arme dans un premier temps) + sva = SousVivierAssociation(sva_id=i, sous_vivier_id=sous_vivier.sv_id, filiere_id=filiere.f_code, + sva_categorie=categorie, sva_arme=sv.at[i, "arme"]) + sva.save() + + # Mise à jour du sous_vivier_id pour les postes et administres ayant la filiere et la categorie en cours + Administre.objects.filter(a_categorie=categorie, a_filiere_id=filiere.f_code).update( + sous_vivier_id=sous_vivier.sv_id) + + # En fonction du fichier excel, changer la mise à jour du sous_vivier + Poste.objects.filter(p_categorie=categorie, p_filiere_id=filiere.f_code).sous_viviers.set([sous_vivier.sv_id]) + created.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"])) + + else: + ignored.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"])) + # Si l'association appartient déjà à un sous_vivier, on ne fait rien + except Filiere.DoesNotExist: + errors.append("Filiere " + sv.at[i, 'f_code'] + "non retrouvée") + + return Response({"created": created, "ignored": ignored, "errors": errors}) + + + +@class_logger +@execution_time_viewset +@query_count_viewset +class AlimentationReferentielView(APIView): + """ Vue pour alimenter la base à partir de référentiels """ + + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = AlimentationReferentielSerializer + + def get(self, request): + return Response("Formulaire d'alimentation d'OGURE NG (référentiel)") + + @atomic + def post(self, request): + """ + Charge le(s) fichier(s) et met à jour la base. + + :param request: requête, contient les fichiers + :type request: class:`rest_framework.request.Request` + + :raises: class:`rest_framework.exceptions.APIException` + :return: réponse + :rtype: class:`rest_framework.response.Response` + """ + + try: + validator = self.serializer_class(data=request.data) + validator.is_valid(raise_exception=True) + + # récupération des fichiers + referentiel_fe = validator.validated_data.get('referentiel_fe') + + if referentiel_fe: + Cols = FormationEmploi.Cols + col_pk = Cols.PK + col_pk_mere = Cols.REL_MERE + col_libelle_mere = Cols.LIBELLE + col_zone_def = Cols.ZONE_DEFENSE + + def process_referentiel_fe(referentiel) -> pd.DataFrame: + """ + Fonction de lecture du référentiel de FE. + + :param referentiel: Fichier du référentiel FE + :type referentiel: XLSX + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + return open_excel(referentiel, sheetname=0, engine='openpyxl') + + def convertir_fe(df: pd.DataFrame) -> pd.DataFrame: + """ + Fonction de conversion du DataFrame de FE. + + :param df: DataFrame du référentiel FE + :type df: class:`pandas.DataFrame` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + + col_pk_avant = 'FE CREDO' # B + col_pk_mere_avant = 'FE mère CREDO' # D + col_libelle_mere_avant = 'FE mère LA' # E + col_zone_def_avant = 'Zone de Défense' # O + + return ( + df[[col_pk_avant, col_pk_mere_avant, col_libelle_mere_avant, col_zone_def_avant]] + .drop_duplicates(subset=col_pk_avant, keep='first') + .rename(columns={ + col_pk_avant: col_pk, + col_pk_mere_avant: col_pk_mere, + col_libelle_mere_avant: col_libelle_mere, + col_zone_def_avant: col_zone_def + }) + .astype({col_pk: 'str', col_pk_mere: 'str'}) + ) + + def mettre_a_jour_fe(df: pd.DataFrame) -> None: + """ + Met à jour les FE base à partir du DataFrame de FE. + + :param df: DataFrame du référentiel FE + :type df: class:`pandas.DataFrame` + + :return: DataFrame + :rtype: class:`pandas.DataFrame` + """ + + TypeModele = FormationEmploi + Cols = TypeModele.Cols + champs_maj = (Cols.REL_MERE, Cols.ZONE_DEFENSE) + modeles_en_base = {m.pk: m for m in TypeModele.objects.select_related(Cols.REL_MERE).only('pk', *champs_maj)} + + taille_batch = 100 + dict_update = {} + dict_mere_create = {} + error_count = 0 + for idx, rec in enumerate(df.to_dict('records')): + pk = rec.get(col_pk) + try: + id_mere = rec.get(col_pk_mere) + zone_defense = rec.get(col_zone_def) + + en_base = modeles_en_base.get(pk) + + # TODO pas de création pour l'instant (ni de suppression) quand en_base est falsy + if en_base: + mere = None + if id_mere is not None: + mere = modeles_en_base.get(id_mere) or dict_mere_create.get(id_mere) + if not mere: + try: + # les FE mères manquantes seront créées + mere = TypeModele(pk=id_mere, fe_libelle=rec.get(col_libelle_mere)) + except Exception as e: + raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" (mère) a échoué') from e + dict_mere_create.setdefault(id_mere, mere) + + if mere != getattr(en_base, Cols.REL_MERE, None) or zone_defense != getattr(en_base, Cols.ZONE_DEFENSE, None): + try: + modele = TypeModele(pk=pk, mere=mere, zone_defense=zone_defense) + except Exception as e: + raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" a échoué') from e + dict_update.setdefault(pk, modele) + except Exception: + error_count = error_count + 1 + self.logger.exception('%s une erreur est survenue à la ligne : %s (pk=%s)', TypeModele.__name__, idx, pk) + + if error_count: + self.logger.warning("%s(s) en erreur : %s", TypeModele.__name__, error_count) + + if dict_mere_create: + TypeModele.objects.bulk_create(dict_mere_create.values(), batch_size=taille_batch) + self.logger.info('%s(s) mères créée(s) : %s', TypeModele.__name__, len(dict_mere_create)) + + if dict_update and champs_maj: + TypeModele.objects.bulk_update(dict_update.values(), batch_size=taille_batch, fields=champs_maj) + self.logger.info('%s(s) mise(s) à jour : %s', TypeModele.__name__, len(dict_update)) + + df_referentiel_fe = process_referentiel_fe(referentiel_fe) + self.logger.info('Lecture du référentiel FE ------> Succès') + + df_referentiel_fe = convertir_fe(df_referentiel_fe) + self.logger.info('Extraction des données du référentiel FE ------> Succès') + + mettre_a_jour_fe(df_referentiel_fe) + self.logger.info('Mise à jour du référentiel FE ------> Succès') + + return Response({'Insertion réussie'}) + except (Http404, APIException): + raise + except BaseException: + message = "Impossible d'alimenter le(s) référentiel(s)" + self.logger.exception(message) + raise APIException(message) + +# Vue API des administrés +# Pagination OK +# Ordering OK +# Search NOK +# Filtering NOK + +@class_logger +@execution_time_viewset +@query_count_viewset +class AdministreView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue de l'administre. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = AdministreSerializer + + filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] + # filterset_fields = ['category', 'in_stock'] + filterset_class = AdministreFilter + ordering_fields = Administre.Cols.PK + ordering = [Administre.Cols.PK] + pagination_class = HeavyDataPagination + + # important : mettre à jour quand le serializer change + def get_queryset(self): + Cols = Administre.Cols + ColsFE = FormationEmploi.Cols + return Administre.objects.select_related( + Cols.REL_FONCTION, Cols.REL_GRADE, Cols.REL_SOUS_VIVIER + ).prefetch_related( + Cols.M2M_COMPETENCES, + Prefetch(Cols.M2M_PAM, queryset=Administres_Pams.objects.select_related(Administres_Pams.Cols.O2M_FMOB)), + # Prefetch(Cols.REL_DECISION, queryset=Decision.objects.select_related( + # f'{Decision.Cols.REL_POSTE}__{Poste.Cols.REL_FORMATION_EMPLOI}__{ColsFE.REL_MERE}', + # )), + Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(ColsFE.REL_MERE)), + ) + + def get_serializer(self, *args, **kwargs): + ser = super().get_serializer(*args, **kwargs) + arg = args[0] if args else None + if arg and isinstance(arg, (list, tuple)) and self.request.method == 'GET': + # calcule directement, si besoin, les profils et les décisions disponibles pour tous les résultats + if CTX_KEY_PROFILES not in ser.context: + ser.context[CTX_KEY_PROFILES] = get_profiles_by_adm(self.request.user, *arg) + if CTX_KEY_DECISIONS not in ser.context: + ser.context[CTX_KEY_DECISIONS] = get_available_decisions(arg, profiles_by_adm=ser.context[CTX_KEY_PROFILES], + annee_pam = self.request.query_params['pam__in']) + return ser + + @atomic + def put(self, request): + """La fonction put met à jour une liste d'administres. + + :type request: rest_framework.request.Request + :param request: Request contenant la liste d'administres. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des administres . + """ + try: + req_data = request.data + is_list = isinstance(req_data, list) + validator = self.serializer_class(data=req_data, many=is_list, partial=True) + validator.is_valid(raise_exception=True) + + today = date.today() + Cols = Administre.Cols + + if 'annee_pam' in req_data: + annee_pam = req_data.pop('annee_pam') + else: + annee_pam = '' + + + def copy_data_item(input, pk): + """Copie et complète si besoin le dictionnaire en entrée""" + result = {**input} + if pk and Cols.PK not in result: + result[Cols.PK] = pk + return result + + def set_competences(administre_obj, a_comp): + """Set competences to administre object""" + a_comp_id = [competence.comp_id for competence in a_comp] + administre_obj.a_liste_id_competences.set(a_comp_id) + return administre_obj + + if is_list: # Update multiple elements + data = [copy_data_item(item, req_data[idx][Cols.PK]) for idx, item in enumerate(validator.validated_data)] + fields = [key for key in data[0].keys() if key != Cols.PK] + + # Direct assignment to the forward side of a many-to-many set is prohibited. Use a_liste_id_competences.set() instead. + if Cols.STATUT_PAM in fields: + fields_to_update_pam = ['a_statut_pam_annee'] + data_statut_pam = [{'id' :str(o['a_id_sap'])+str(annee_pam), 'a_statut_pam_annee' : o[Cols.STATUT_PAM]} for o in data] + objs = [Administres_Pams(**data_item) for data_item in data_statut_pam] + Administres_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + + elif 'a_liste_id_competences' not in fields: + objs = [Administre(**data_item) for data_item in data] + if Cols.STATUT_PAM in fields: + list_error = [] + for i in range(len(objs)): + old_administre = Administre.objects.get(a_id_sap=objs[i].a_id_sap) + administre = objs[i] + eip = administre.a_eip + fe_code = old_administre.formation_emploi_id + avis = administre.a_statut_pam + old_avis = old_administre.a_statut_pam + try: + categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] + except: + erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' + raise Exception(erreur) + + + + # liste_error = impact_decisions(old_administre, administre, old_avis, avis, eip, fe_code, categorie) + + Administre.objects.bulk_update(objs, fields=fields) + else: + objs_w_comp = [set_competences(Administre(**without_keys(i, ['a_liste_id_competences'])), i['a_liste_id_competences']) for i in data] + + else: # Update one element + data = copy_data_item(validator.validated_data, validator.validated_data[Cols.PK]) + # c'est un bug la récupération de a_id_sap en-dessous, non ? + a_id_sap = Administre.objects.update(data) + db_instance = Administre.objects.filter(a_id_sap=a_id_sap).first() + db_instance.tag.clear() + + return Response({'msg': 'updated'}) + except APIException: + raise + except Exception: + message = 'échec de la mise à jour de N administrés' + self.logger.exception(message) + raise APIException(message) + + def partial_update(self, request, pk=None): + """ La fonction put met à jour un administre. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre. + + :type pk: integer + :param pk: Primary Key de l'administre. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour de l'administre . + """ + try: + a = get_object_or_404(Administre.objects, a_id_sap=pk) # Permet de déclancher la vérification des droits au niveau de l'objet + self.check_object_permissions(self.request, a) + req_data = request.data + + if 'annee_pam' in req_data: + annee_pam = req_data.pop('annee_pam') + + + # TODO valider les données (il faut d'abord corriger la MAJ des compétences) + # validator = self.serializer_class(data=req_data, partial=True) + # validator.is_valid(raise_exception=True) + + Cols = Administre.Cols + + # Copie et complète si besoin le dictionnaire en entrée + # administre = {**validator.validated_data} + administre = {**req_data} + administre[Cols.PK] = pk + + fields = [key for key in administre.keys() if key != Cols.PK] # and key != 'a_liste_id_competences'] + + if 'a_domaine_futur' in fields: + administre['a_domaine_futur_id'] = administre['a_domaine_futur'] + del administre['a_domaine_futur'] + + if 'a_filiere_futur' in fields: + administre['a_filiere_futur_id'] = administre['a_filiere_futur'] + del administre['a_filiere_futur'] + + if Cols.STATUT_PAM in fields: + old_administre = Administre.objects.get(a_id_sap=pk) + eip = old_administre.a_eip + fe_code = old_administre.formation_emploi_id + avis = administre[Cols.STATUT_PAM] + old_avis = old_administre.a_statut_pam + try: + categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] + except: + erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' + raise APIException(erreur) + + + + + # liste_error = impact_decisions(old_administre, Administre(**administre), old_avis, avis, eip, fe_code, categorie) + + + if 'a_liste_id_competences' in fields: + adm=Administre(pk=pk) + #adm=request.data['a_liste_id_competences'] + if administre['a_liste_id_competences']: + adm.a_liste_id_competences.set(administre['a_liste_id_competences'].split(',')) + else: + adm.a_liste_id_competences.set("") + elif Cols.STATUT_PAM in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_statut_pam_annee = administre[Cols.STATUT_PAM] + adm_pam.save() + else: + copy = { k: v for k, v in administre.items() if k != 'a_liste_id_competences'} + adm = Administre(**copy) + + if fields: + Administre.objects.bulk_update([adm], fields=fields) + + return Response({'msg': 'updated'}) + + except (Http404, APIException): + raise + except Exception: + message = "échec de la mise à jour de l'administré" + self.logger.exception(message) + raise APIException(message) + + +# GET /api/administres => 0 administrés +# POST /api/administres => Crée 1 nouvel administres => id=1 +# GET /api/administres/1 => 1 administre précédemment créé +# POST /api/administres/1 => 1 administre précédemment créé + + +@class_logger +@execution_time_viewset +@query_count_viewset +class AdministrePAMView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue de l'administre. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = AdministresPamsSerializer + + filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] + # filterset_fields = ['category', 'in_stock'] + filterset_class = AdministrePAMFilter + ordering_fields = Administres_Pams.Cols.PK + ordering = [Administres_Pams.Cols.PK] + pagination_class = HeavyDataPagination + # important : mettre à jour quand le serializer change + def get_queryset(self): + Cols = Administres_Pams.Cols + Cols_Administre = Administre.Cols + ColsFE = FormationEmploi.Cols + + return Administres_Pams.objects.select_related( + Cols.REL_DECISION, Cols.REL_PAM, Cols.O2M_FMOB + ).prefetch_related( + Prefetch(Cols.REL_ADMINISTRE, queryset = Administre.objects.select_related(Cols_Administre.REL_FONCTION, Cols_Administre.REL_GRADE, Cols_Administre.REL_SOUS_VIVIER + ).prefetch_related( + Cols_Administre.M2M_COMPETENCES, + Prefetch(Cols_Administre.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(ColsFE.REL_MERE)), + )) + ) + + def get_serializer(self, *args, **kwargs): + ser = super().get_serializer(*args, **kwargs) + arg = args[0] if args else None + + if arg and isinstance(arg, (list, tuple)) and self.request.method == 'GET': + # calcule directement, si besoin, les profils et les décisions disponibles pour tous les résultats + if CTX_KEY_PROFILES not in ser.context: + ser.context[CTX_KEY_PROFILES] = get_profiles_by_adm(self.request.user, *arg) + if CTX_KEY_DECISIONS not in ser.context: + ser.context[CTX_KEY_DECISIONS] = get_available_decisions(arg, profiles_by_adm=ser.context[CTX_KEY_PROFILES]) + return ser + + @atomic + def put(self, request): + """La fonction put met à jour une liste d'administres. + + :type request: rest_framework.request.Request + :param request: Request contenant la liste d'administres. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des administres . + """ + try: + req_data = request.data + is_list = isinstance(req_data, list) + # validator = AdministreSerializer(data=req_data, many=is_list, partial=True) + # validator.is_valid(raise_exception=True) + + today = date.today() + Cols = Administre.Cols + + # Extraire l'annee + if 'pam_id' in req_data[0]: + annee_pam = req_data[0]['pam_id'] + + + def copy_data_item(input, pk): + """Copie et complète si besoin le dictionnaire en entrée""" + result = {**input} + if pk and Cols.PK not in result: + result[Cols.PK] = pk + return result + + def set_competences(administre_obj, a_comp): + """Set competences to administre object""" + a_comp_id = [competence.comp_id for competence in a_comp] + administre_obj.a_liste_id_competences.set(a_comp_id) + return administre_obj + + if is_list: + data = [copy_data_item(item, req_data[idx][Cols.PK]) for idx, item in enumerate(req_data)] + fields = [key for key in data[0].keys() if key != Cols.PK] + + # Direct assignment to the forward side of a many-to-many set is prohibited. Use a_liste_id_competences.set() instead. + + # Update Administres Pams dans les if et elif et le else et elif est utilisé pour la mise a jour des Administres + + if Cols.STATUT_PAM in fields: + fields_to_update_pam = ['a_statut_pam_annee'] + # Creation d'une liste qui contient l'id ( a_id_sap + annee_pam) avec le statut choisi + data_statut_pam = [{'id' :str(o['a_id_sap'])+str(annee_pam), 'a_statut_pam_annee' : o[Cols.STATUT_PAM]} for o in data] + # Creation des objets Administres Pams + objs = [Administres_Pams(**data_item) for data_item in data_statut_pam] + # Mise a jour des objets Administres Pams + Administres_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + elif 'a_liste_depts_souhaites_pam' in fields: + fields_to_update_pam = ['a_liste_depts_souhaites_pam'] + # Creation d'une liste qui contient l'id ( a_id_sap + annee_pam) avec le statut choisi + data_statut_pam = [{'id' :str(o['a_id_sap'])+str(annee_pam), 'a_liste_depts_souhaites_pam' : o['a_liste_depts_souhaites_pam']} for o in data] + # Creation des objets Administres Pams + objs = [Administres_Pams(**data_item) for data_item in data_statut_pam] + # Mise a jour des objets Administres Pams + Administres_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + elif 'a_liste_id_competences' in fields: + objs_w_comp = [set_competences(Administre(**without_keys(i, ['a_liste_id_competences'])), i['a_liste_id_competences']) for i in data] + + elif 'a_liste_zones_geographiques_shm_pam' in fields: + fields_to_update_pam = ['a_liste_zones_geographiques_shm_pam'] + data_statut_pam = [{'id' :str(o['a_id_sap'])+str(annee_pam), 'a_liste_zones_geographiques_shm_pam' : o['a_liste_zones_geographiques_shm_pam']} for o in data] + objs = [Administres_Pams(**data_item) for data_item in data_statut_pam] + # Mise a jour des objets Administres Pams + Administres_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + else: + objs = [Administre(**data_item) for data_item in data] + if Cols.STATUT_PAM in fields: + list_error = [] + for i in range(len(objs)): + old_administre = Administre.objects.get(a_id_sap=objs[i].a_id_sap) + administre = objs[i] + eip = administre.a_eip + fe_code = old_administre.formation_emploi_id + avis = administre.a_statut_pam + old_avis = old_administre.a_statut_pam + try: + categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] + except: + erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' + raise Exception(erreur) + + # liste_error = impact_decisions(old_administre, administre, old_avis, avis, eip, fe_code, categorie) + + elif 'a_domaine_futur_id' in fields or 'a_filiere_futur_id' in fields or 'a_nf_futur' in fields: + for i in range(len(objs)): + old_administre = Administre.objects.get(a_id_sap=objs[i].a_id_sap) + administre = objs[i] + + dom = administre.a_domaine_futur_id if 'a_domaine_futur_id' in fields else old_administre.a_domaine_futur_id + fil = administre.a_filiere_futur_id if 'a_filiere_futur_id' in fields else old_administre.a_filiere_futur_id + nf = administre.a_nf_futur if 'a_nf_futur' in fields else old_administre.a_nf_futur + cat = nf2categorie(nf) + + sv_id = generate_sv_id(dom, fil, cat) + sv = SousVivier.objects.filter(sv_id=sv_id) + + try: + sv.exists() + administre.sous_vivier_id = sv_id + if 'sous_vivier_id' not in fields: + fields += ['sous_vivier_id'] + except: + message = f"La mise à jour a échoué car la sous-vivier d'id : {sv_id} n'est pas en base" + self.logger.exception(message) + raise APIException(message) + + Administre.objects.bulk_update(objs, fields=fields) + + return Response({'msg': 'updated'}) + except APIException: + raise + except Exception: + message = 'échec de la mise à jour de N administrés' + self.logger.exception(message) + raise APIException(message) + + def partial_update(self, request, pk=None): + """ La fonction put met à jour un administre. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre. + + :type pk: integer + :param pk: Primary Key de l'administre. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour de l'administre . + """ + try: + a = get_object_or_404(Administre.objects, a_id_sap=pk) # Permet de déclancher la vérification des droits au niveau de l'objet + self.check_object_permissions(self.request, a) + req_data = request.data + + if 'annee_pam' in req_data: + annee_pam = req_data.pop('annee_pam') + + + # TODO valider les données (il faut d'abord corriger la MAJ des compétences) + # validator = self.serializer_class(data=req_data, partial=True) + # validator.is_valid(raise_exception=True) + + Cols = Administre.Cols + + # Copie et complète si besoin le dictionnaire en entrée + # administre = {**validator.validated_data} + administre = {**req_data} + administre[Cols.PK] = pk + + fields = [key for key in administre.keys() if key != Cols.PK] # and key != 'a_liste_id_competences'] + + if 'a_domaine_futur' in fields: + administre['a_domaine_futur_id'] = administre['a_domaine_futur'] + del administre['a_domaine_futur'] + + if 'a_filiere_futur' in fields: + administre['a_filiere_futur_id'] = administre['a_filiere_futur'] + del administre['a_filiere_futur'] + + if 'a_domaine_futur' in fields or 'a_filiere_futur' in fields or 'a_nf_futur' in fields: + old_administre = Administre.objects.get(a_id_sap=pk) + + dom = administre['a_domaine_futur_id'] if 'a_domaine_futur' in fields else old_administre.a_domaine_futur_id + fil = administre['a_filiere_futur_id'] if 'a_filiere_futur' in fields else old_administre.a_filiere_futur_id + nf = administre['a_nf_futur'] if 'a_nf_futur' in fields else old_administre.a_nf_futur + cat = nf2categorie(nf) + + sv_id = generate_sv_id(dom, fil, cat) + sv = SousVivier.objects.filter(sv_id=sv_id) + + try: + sv.exists() + administre['sous_vivier_id'] = sv_id + fields += ['sous_vivier_id'] + except: + message = f"La mise à jour a échoué car la sous-vivier d'id : {sv_id} n'est pas en base" + self.logger.exception(message) + raise APIException(message) + + if Cols.STATUT_PAM in fields: + old_administre = Administre.objects.get(a_id_sap=pk) + eip = old_administre.a_eip + fe_code = old_administre.formation_emploi_id + avis = administre[Cols.STATUT_PAM] + old_avis = old_administre.a_statut_pam + try: + categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()] + except: + erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.' + raise APIException(erreur) + + if 'a_liste_id_competences' in fields: + adm=Administre(pk=pk) + if administre['a_liste_id_competences']: + adm.a_liste_id_competences.set(administre['a_liste_id_competences'].split(',')) + else: + adm.a_liste_id_competences.set("") + + elif Cols.STATUT_PAM in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_statut_pam_annee = administre[Cols.STATUT_PAM] + adm_pam.save() + + elif 'notes_pam' in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.notes_pam = administre['notes_pam'] + adm_pam.save() + + elif 'a_ciat_pam' in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_ciat_pam = administre['a_ciat_pam'] + adm_pam.save() + + elif 'a_specifique_pam' in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_specifique_pam = administre['a_specifique_pam'] + adm_pam.save() + + elif 'a_liste_depts_souhaites_pam' in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_liste_depts_souhaites_pam = administre['a_liste_depts_souhaites_pam'] + adm_pam.save() + + elif 'a_liste_zones_geographiques_shm_pam' in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_liste_zones_geographiques_shm_pam = administre['a_liste_zones_geographiques_shm_pam'] + adm_pam.save() + + elif 'a_situationfuture_notes_fe' in fields: + adm_pam = Administres_Pams.objects.get(id = str(pk)+str(annee_pam)) + adm_pam.a_situationfuture_notes_fe = administre['a_situationfuture_notes_fe'] + adm_pam.save() + + else: + copy = {k: v for k, v in administre.items() if k != 'a_liste_id_competences'} + adm = Administre(**copy) + + if fields: + Administre.objects.bulk_update([adm], fields=fields) + + return Response({'msg': 'updated'}) + + except (Http404, APIException): + raise + except Exception: + message = "échec de la mise à jour de l'administré" + self.logger.exception(message) + raise APIException(message) + + +@execution_time_viewset +@query_count_viewset +@class_logger +class PostePAMView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des postes. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + lookup_value_regex = r"[\w.]+" + serializer_class = PostesPamsSerializer + pagination_class = HeavyDataPagination + filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] + filterset_class = PostePAMFilter + # ordering_fields = '__all_related__' + ordering = ['poste_id','-p_pam'] + + # important : mettre à jour quand le serializer change + def get_queryset(self): + """Cette fonction permet d'ajouter plus de logique à l'attribut queryset.""" + + Cols = Postes_Pams.Cols + Poste_Cols = Poste.Cols + + queryset = Postes_Pams.objects.select_related( + Cols.REL_PAM + ).prefetch_related( + Prefetch(Cols.O2M_DECISION, queryset=Decision.objects.select_related(f'{Decision.Cols.REL_ADMINISTRE}__{Administre.Cols.REL_GRADE}')), + Prefetch(Cols.REL_POSTE, queryset=Poste.objects.select_related(Poste_Cols.REL_FONCTION) + .prefetch_related( + Poste_Cols.M2M_COMPETENCES, + Poste_Cols.M2M_SOUS_VIVIERS, + Prefetch(Poste_Cols.REL_FORMATION_EMPLOI, + queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_MERE)))) + ) + + sous_viviers_name = self.request.query_params.get('sous_vivier') + if sous_viviers_name is not None: + queryset = queryset.filter(poste__sous_viviers__in=sous_viviers_name.split('-')) + + + return queryset + + def put(self, request): + """La fonction put met à jour une liste des postes. + + :type request: rest_framework.request.Request + :param request: Request contenant la liste des postes. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des postes . + """ + data = request.data + self.logger.debug('------------------------- Modification Multiple de Poste verification ---------------------') + self.logger.debug('Request data : %s', data) + + if 'pam_id' in data[0]: + annee_pam = data[0]['pam_id'] + + # serialized = self.serializer_class(data=data, many=isinstance(data, list), partial=True) + # serialized.is_valid(raise_exception=True) + + if isinstance(data, list): # Update multiple elements + try: + fields = [key for key in data[0].keys() if key != 'p_id' ] + + + + if 'p_avis_fe' in fields: + fields_to_update_pam = ['p_avis_fe_pam', 'p_avis_pam'] + # Creation d'une liste qui contient l'id ( p_id + annee_pam) avec l'avis choisi + data_statut_pam = [{'id' :str(o['p_id'])+str(annee_pam), 'p_avis_fe_pam' : o['p_avis_fe'], 'p_avis_pam' : o['p_avis_fe']} for o in data] + # Creation des objets Poste Pams + objs = [Postes_Pams(**data_item) for data_item in data_statut_pam] + # Mise a jour des objets Poste Pams + Postes_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + elif 'p_avis' in fields: + fields_to_update_pam = ['p_avis_pam'] + data_statut_pam = [{'id' :str(o['p_id'])+str(annee_pam), 'p_avis_pam' : o['p_avis']} for o in data] + objs = [Postes_Pams(**data_item) for data_item in data_statut_pam] + Postes_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + elif 'p_direct_commissionne' in fields: + fields_to_update_pam = ['p_direct_commissionne_pam'] + data_statut_pam = [{'id' :str(o['p_id'])+str(annee_pam), 'p_direct_commissionne_pam' : o['p_direct_commissionne']} for o in data] + objs = [Postes_Pams(**data_item) for data_item in data_statut_pam] + Postes_Pams.objects.bulk_update(objs, fields=fields_to_update_pam) + + elif 'p_itd_cellule' in fields: + sous_viviers = sous_viviers_du_cellule(data[0]['p_itd_cellule']) + self.logger.debug('Modification Multiple de p_itd_cellule, les sous_viviers de la cellule sont : %s', sous_viviers) + if sous_viviers: + objs = [Poste(**data[i]) for i in range(len(data))] + Poste.objects.bulk_update(objs, fields=fields) + objs_w_comp = [Poste(**without_keys(data[i], ['p_itd_cellule'])).sous_viviers.set(sous_viviers) for i in range(len(data))] + else: + return JsonResponse({'info': f"La mise à jour n'a pas réussi : la cellule '{data[0]['p_itd_cellule']}' n'a pas de sous viviers enregistrés dans l'application."}) + + elif 'competence_id' not in fields: + if 'supression' in fields: + elem = data[0] + Postes_Pams.objects.filter(p_pam_id=elem['pam_id'],poste_id=elem['p_id']).delete() + + if 'departement' and 'codeFe' and 'lieu' and 'zoneDeDefense' and 'codeFeMere' and 'etr' in fields: + elem = data[0] + #generate random id : create compteur de poste en sureff + no_sureff = str(Postes_Pams.objects.filter(p_pam_id=str(annee_pam),id__contains='_').count() + 1) + + pam_actuel = PAM.objects.get(pam_statut='PAM en cours').pam_id + + #on crée l'instance de formation d'emploi + fe = FormationEmploi.objects.get(fe_code=elem['codeFe']) + fe_poste_duplique = fe + fe_poste_duplique.zone_defense =elem['zoneDeDefense'] + fe_poste_duplique.fe_garnison_lieu = elem['lieu'] + fe_poste_duplique.save() + #créer une copie d'instance de poste qui existe déjà en base: + poste = Poste.objects.get(p_id=elem['p_id']) + poste_duplique = poste + poste_duplique.p_id = poste.p_id + '_'+ no_sureff + poste_duplique.p_dep= elem['departement'] + poste_duplique.p_fonction = elem['etr'] + poste_duplique.formation_emploi = fe_poste_duplique + poste_duplique.save() + + if elem['pam_id'] == pam_actuel: + poste_sureffA = Postes_Pams.objects.get(id=elem['p_id'] + elem['pam_id']) + poste_sureffA.id = elem['p_id']+ '_'+ no_sureff + elem['pam_id'] + poste_sureffA.poste_id = elem['p_id']+ '_'+ no_sureff + poste_sureffA.poste = poste_duplique + poste_sureffA.info_reo = f"SUREFF {str(annee_pam)}" + poste_sureffA.save() + + poste_sureffA1 = Postes_Pams.objects.get(id=elem['p_id'] + str(int(elem['pam_id'])+1)) + poste_sureffA1.id = elem['p_id']+ '_'+ no_sureff +str(int(elem['pam_id'])+1) + poste_sureffA1.poste_id = elem['p_id']+ '_'+ no_sureff + poste_sureffA1.poste = poste_duplique + poste_sureffA1.info_reo = f"SUREFF {str(annee_pam)}" + poste_sureffA1.save() + + else: + poste_sureffA1 = Postes_Pams.objects.get(id=elem['p_id'] + elem['pam_id']) + poste_sureffA1.id = elem['p_id']+ '_'+ no_sureff + elem['pam_id'] + poste_sureffA1.poste_id = elem['p_id']+ '_'+ no_sureff + poste_sureffA1.poste = poste_duplique + poste_sureffA1.info_reo = f"SUREFF {str(annee_pam)}" + poste_sureffA1.save() + + else: + objs = [Poste(**data[i]) for i in range(len(data))] + Poste.objects.bulk_update(objs, fields=fields) + else: + objs_w_comp = [Poste(**without_keys(data[i],['competence_id'])).competences.set(data[i]['competence_id'].split(',')) for i in range(len(data))] + + except: + raise Exception("bulk update error") + + + return Response({'msg': 'updated'}) + + def partial_update(self, request, pk=None): + """ La fonction put met à jour un poste. + + :type request: rest_framework.request.Request + :param request: Request contenant le poste. + + :type pk: integer + :param pk: Primary Key du poste. + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour du poste . + """ + p = get_object_or_404(Poste.objects, p_id=pk) # Permet de déclancher la vérification des droits au niveau de l'objet + self.check_object_permissions(self.request, p) + poste = request.data + self.logger.debug('------------------------- Modification Unitaire de Poste verification ---------------------') + self.logger.debug('Request data : %s', poste) + + if 'annee_pam' in poste: + annee_pam = poste.pop('annee_pam') + + fields_not_updated = ['p_id', 'p_poids_filiere', 'p_poids_nf', 'p_poids_competences'] + fields = [key for key in poste.keys() if key not in fields_not_updated] + poste['p_id'] = pk + + + + if 'competence' in fields: + pos=Poste(pk=pk) + if poste['competence']: + pos.competences.set(poste['competence'].split(',')) + else: + pos.competences.set("") + + elif 'sous_vivier_id' in fields: + pos=Poste(pk=pk) + pos.sous_viviers.add(poste['sous_vivier_id']) + + elif 'p_avis_fe_pam' in fields: + poste_pam = Postes_Pams.objects.get(id = str(pk)+str(annee_pam)) + poste_pam.p_avis_pam = poste['p_avis_fe_pam'] + poste_pam.p_avis_fe_pam = poste['p_avis_fe_pam'] + poste_pam.save() + + elif 'p_avis_pam' in fields: + poste_pam = Postes_Pams.objects.get(id = str(pk)+str(annee_pam)) + poste_pam.p_avis_pam = poste['p_avis_pam'] + poste_pam.save() + + elif 'p_direct_commissionne_pam' in fields: + poste_pam = Postes_Pams.objects.get(id = str(pk)+str(annee_pam)) + poste_pam.p_direct_commissionne_pam = poste['p_direct_commissionne_pam'] + poste_pam.save() + + elif 'p_priorisation_pcp_pam' in fields: + poste_pam = Postes_Pams.objects.get(id = str(pk)+str(annee_pam)) + poste_pam.p_priorisation_pcp_pam = poste['p_priorisation_pcp_pam'] + poste_pam.save() + + elif 'p_notes_gestionnaire_pam' in fields: + poste_pam = Postes_Pams.objects.get(id = str(pk)+str(annee_pam)) + poste_pam.p_notes_gestionnaire_pam = poste['p_notes_gestionnaire_pam'] + poste_pam.save() + + else: + copy = {k: v for k, v in poste.items() if k != 'competence'} + pos = Poste(**copy) + + # l'orde des if est tres important + if 'p_specifique' in fields and poste['p_specifique'] == SpecifiqueChoices.ITD: + pos.p_itd_affecte = False + fields += ['p_itd_affecte'] + + elif 'p_itd_cellule' in fields: + sous_viviers = sous_viviers_du_cellule(poste['p_itd_cellule']) + if sous_viviers: + pos.sous_viviers.set(sous_viviers) + pos.p_itd_affecte = True + fields += ['p_itd_affecte'] + else : + self.logger.debug(f"La mise à jour n'a pas réussi, La cellule '{poste['p_itd_cellule']}' n'a pas de sous viviers enregistrés dans l'application.") + return JsonResponse({'info': f"La mise à jour n'a pas réussi : la cellule '{poste['p_itd_cellule']}' n'a pas de sous viviers enregistrés dans l'application."}) + + if fields: + Poste.objects.bulk_update([pos], fields=fields) + + + + return Response({'msg': 'updated'}) + +@execution_time_viewset +@query_count_viewset +@class_logger +class PosteView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des postes. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + lookup_value_regex = r"[\w.]+" + serializer_class = PosteSerializer + pagination_class = HeavyDataPagination + filter_backends = [DjangoFilterBackend, RelatedOrderingFilter] + filterset_class = PosteFilter + ordering_fields = '__all_related__' + ordering = ['p_id'] + + # important : mettre à jour quand le serializer change + def get_queryset(self): + """Cette fonction permet d'ajouter plus de logique à l'attribut queryset.""" + + Cols = Poste.Cols + + queryset = Poste.objects.select_related( + Cols.REL_FONCTION + ).prefetch_related( + Cols.M2M_COMPETENCES, + Cols.M2M_SOUS_VIVIERS, + Prefetch(Cols.M2M_PAM, queryset=Postes_Pams.objects.select_related(Postes_Pams.Cols.O2M_DECISION)), + Prefetch(Cols.O2M_DECISION, queryset=Decision.objects.select_related(f'{Decision.Cols.REL_ADMINISTRE}__{Administre.Cols.REL_GRADE}')), + Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_MERE)), + ).annotate( + p_nb_prepositionne=Sum( + Case(When(decisions__de_decision=DecisionChoices.PREPOSITIONNE, then=1), default=0, + output_field=IntegerField())), + p_nb_positionne=Sum(Case(When(decisions__de_decision=DecisionChoices.POSITIONNE, then=1), default=0, + output_field=IntegerField())), + p_nb_omi_en_cours=Sum(Case(When(decisions__de_decision=DecisionChoices.OMI_EN_COURS, then=1), default=0, + output_field=IntegerField())), + p_nb_omi_active=Sum(Case(When(decisions__de_decision=DecisionChoices.OMI_ACTIVE, then=1), default=0, + output_field=IntegerField())), + ) + + sous_viviers_name = self.request.query_params.get('sous_vivier') + if sous_viviers_name is not None: + queryset = queryset.filter(sous_viviers=sous_viviers_name) + + + return queryset + + + def put(self, request): + """La fonction put met à jour une liste des postes. + + :type request: rest_framework.request.Request + :param request: Request contenant la liste des postes. + + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des postes . + """ + data = request.data + self.logger.debug('------------------------- Modification Multiple de Poste verification ---------------------') + self.logger.debug('Request data : %s', data) + serialized = self.serializer_class(data=data, many=isinstance(data, list), partial=True) + serialized.is_valid(raise_exception=True) + if isinstance(data, list): # Update multiple elements + try: + fields = [key for key in data[0].keys() if key != 'p_id' ] + + if 'p_itd_cellule' in fields: + sous_viviers = sous_viviers_du_cellule(data[0]['p_itd_cellule']) + self.logger.debug('Modification Multiple de p_itd_cellule, les sous_viviers de la cellule sont : %s', sous_viviers) + if sous_viviers: + objs = [Poste(**data[i]) for i in range(len(data))] + Poste.objects.bulk_update(objs, fields=fields) + objs_w_comp = [Poste(**without_keys(data[i],['p_itd_cellule'])).sous_viviers.set(sous_viviers) for i in range(len(data))] + else : + return JsonResponse({'info': f"La mise à jour n'a pas réussi, La cellule '{data[0]['p_itd_cellule']}' n'a pas de sous viviers enregistrés dans l'application."}) + + + elif 'competence_id' not in fields: + objs = [Poste(**data[i]) for i in range(len(data))] + Poste.objects.bulk_update(objs, fields=fields) + else: + objs_w_comp = [Poste(**without_keys(data[i],['competence_id'])).competences.set(data[i]['competence_id'].split(',')) for i in range(len(data))] + except: + raise Exception("bulk update error") + else: # Update one element + a_id_sap = Poste.objects.update(serialized.validated_data) + db_instance = Poste.objects.filter( + a_id_sap=a_id_sap).first() + db_instance.tag.clear() + + return Response({'msg': 'updated'}) + + def partial_update(self, request, pk=None): + """ La fonction put met à jour un poste. + + :type request: rest_framework.request.Request + :param request: Request contenant le poste. + + :type pk: integer + :param pk: Primary Key du poste. + :return: - **Response** (*Response*): Reponse contient un message de la réussite de met à jour du poste . + """ + p = get_object_or_404(Poste.objects, p_id=pk) # Permet de déclancher la vérification des droits au niveau de l'objet + self.check_object_permissions(self.request, p) + poste = request.data + self.logger.debug('------------------------- Modification Unitaire de Poste verification ---------------------') + self.logger.debug('Request data : %s', poste) + fields_not_updated = ['p_id', 'p_poids_filiere', 'p_poids_nf', 'p_poids_competences'] + fields = [key for key in poste.keys() if key not in fields_not_updated] + poste['p_id'] = pk + + if 'competence' in fields: + pos=Poste(pk=pk) + if poste['competence']: + pos.competences.set(poste['competence'].split(',')) + else: + pos.competences.set("") + elif 'sous_vivier_id' in fields: + pos=Poste(pk=pk) + pos.sous_viviers.add(poste['sous_vivier_id']) + else: + copy = { k: v for k, v in poste.items() if k != 'competence'} + pos = Poste(**copy) + + if 'p_avis_fe' in fields: + pos.p_avis = poste['p_avis_fe'] + fields += ['p_avis'] + + # l'orde des if est tres important + if 'p_specifique' in fields and poste['p_specifique'] == SpecifiqueChoices.ITD: + pos.p_itd_affecte = False + fields += ['p_itd_affecte'] + + + elif 'p_itd_cellule' in fields: + sous_viviers = sous_viviers_du_cellule(poste['p_itd_cellule']) + if sous_viviers: + pos.sous_viviers.set(sous_viviers) + pos.p_itd_affecte = True + fields += ['p_itd_affecte'] + else : + self.logger.debug(f"La mise à jour n'a pas réussi, La cellule '{poste['p_itd_cellule']}' n'a pas de sous viviers enregistrés dans l'application.") + return JsonResponse({'info': f"La mise à jour n'a pas réussi, La cellule '{poste['p_itd_cellule']}' n'a pas de sous viviers enregistrés dans l'application."}) + + if fields: + Poste.objects.bulk_update([pos], fields=fields) + + + return Response({'msg': 'updated'}) + + +@execution_time_viewset +@query_count_viewset +class ListesPreferencesView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des ListesPreferences. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = PreferencesListe + queryset = PreferencesListe.objects.all() + + +@execution_time_viewset +@query_count_viewset +class MarqueView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des marques. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = MarqueSerializer + queryset = Marque.objects.all() + + +@execution_time_viewset +@query_count_viewset +class MarquesGroupeView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des marquegroupes. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = MarquesGroupeSerializer + queryset = MarquesGroupe.objects.all() + + +@execution_time_viewset +@query_count_viewset +class FmobView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des Fmob. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = FmobSerializer + queryset = FMOB.objects.all() + + +@execution_time_viewset +@query_count_viewset +class FiliereView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des filieres. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = FiliereSerializer + queryset = Filiere.objects.all() + + +@execution_time_viewset +@query_count_viewset +class DomaineView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue des domaines. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = DomaineSerializer + queryset = Domaine.objects.all() + + +@execution_time_viewset +@query_count_viewset +class SousVivierAssociationView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue du SousVivierAssociation. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = SousVivierAssociationSerializer + queryset = SousVivierAssociation.objects.all() + + +@execution_time_viewset +@query_count_viewset +class PcpFeGroupeView(viewsets.ModelViewSet): + """ + Cette classe est dédiée au vue du PcpFeGroupe. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = PcpFeGroupeSerializer + queryset = PcpFeGroupe.objects.all() diff --git a/backend-django/backend/views/notation.py b/backend-django/backend/views/notation.py new file mode 100644 index 0000000..c21632b --- /dev/null +++ b/backend-django/backend/views/notation.py @@ -0,0 +1,146 @@ +from django.shortcuts import get_object_or_404 +from django.db.models import Q +from numpy import True_ +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from ..models import Decision, DecisionChoices, Notation, Poste +from ..paginations import HeavyDataPagination +from ..serializers.notation import NotationSerializer +from .commun import (GestionnairePermission, execution_time_viewset, + query_count_viewset) + + +@execution_time_viewset +@query_count_viewset +class NotationView(ModelViewSet): + """ + Cette classe est dédiée au vue des notations. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + serializer_class = NotationSerializer + queryset = Notation.objects.all() + pagination_class = HeavyDataPagination + + def list(self, request: Request, pk=None) -> Response: + """La fonction list envoie le classement des postes pour un administré particulier et inversement. + + :type request: rest_framework.request.Request + :param request: Request contenant l'administre ou le poste. + + :return: - **JsonResponse** (*JsonResponse*): Json contenant le classement. + """ + notations_list = [] + counter = 10 + if 'administre_id' in request.query_params: + q = 'poste_pam' + q1 = 'poste' + administre_id = request.query_params['administre_id'] + administres_keys = ( + 'poste_pam__id', + 'poste_pam__poste__p_id', + 'poste_pam__poste__p_nf', + 'poste_pam__poste__p_domaine', + 'poste_pam__poste__p_filiere', + 'poste_pam__poste__p_eip', + 'poste_pam__poste__formation_emploi__fe_code', + 'poste_pam__poste__p_notes_gestionnaire', + 'poste_pam__poste__p_liste_id_marques', + 'poste_pam__poste__p_code_fonction', + 'poste_pam__p_avis_pam', + 'poste_pam__poste__p_dep', + 'poste_pam__poste__formation_emploi__fe_libelle', + 'poste_pam__poste__p_fonction', + 'poste_pam__poste__formation_emploi__fe_garnison_lieu', + 'poste_pam__poste__formation_emploi__fe_code_postal', + 'no_score_administre', + 'no_flag_cple_ideal') + + notation_qs = (Notation.objects.filter(Q(administre_pam_id=administre_id) & Q(poste_pam__decisions__isnull=True)) + .order_by('-no_score_administre') + .select_related('poste_pams')) + + notation_qs_matching_parfait = (Notation.objects.filter(administre_pam_id=administre_id, + no_flag_cple_ideal=True, + poste_pam__decisions__isnull=True) + .select_related('poste_pams')) + + notation_qs.union(notation_qs_matching_parfait) + notation_qs = notation_qs | notation_qs_matching_parfait + notations_list = list(notation_qs.values(*administres_keys)) + + for notation in notations_list: + poste_id = notation['poste_pam__id'] + topCounter = 0 + allNotationsInvolved = Notation.objects.filter(poste_pam_id=poste_id) + for note in allNotationsInvolved: + topList = list( + Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre').values('poste_pam_id')) + topPostes = [poste['poste_pam_id'] for poste in topList] + if poste_id in topPostes: + topCounter += 1 + + notation['poste__nb_top'] = topCounter + + if 'poste_id' in request.query_params: + q = "administre_pam" + q1 = 'administre' + poste_id = request.query_params['poste_id'] + postes_keys = ( + 'administre_pam__id', + 'administre_pam__administre__a_id_sap', + 'administre_pam__administre__a_nom', + 'administre_pam__administre__a_prenom', + 'administre_pam__a_statut_pam_annee', + 'administre_pam__administre__grade_id', + 'administre_pam__administre__a_liste_id_marques', + 'administre_pam__decision__de_decision', + 'administre_pam__decision__de_date_decision', + 'no_score_administre', + 'no_flag_cple_ideal', + 'administre_pam__notes_pam', + 'administre_pam__administre__a_notes_gestionnaire', + 'administre_pam__administre__a_fonction', + 'administre_pam__administre__a_code_fonction', + 'administre_pam__administre__a_liste_id_marques', + 'administre_pam__decision__poste_id') + + notation_qs = (Notation.objects.filter(Q(poste_pam_id=poste_id) & Q(administre_pam__decision__isnull=True)) + .order_by('-no_score_administre') + .select_related('administre_pam')) + notation_qs_matching_parfait = (Notation.objects.filter(poste_pam_id=poste_id, + no_flag_cple_ideal=True, + administre_pam__decision__isnull=True) + .select_related('administre_pam')) + notation_qs.union(notation_qs_matching_parfait) + notations_list = list( + notation_qs.values(*postes_keys)) + + for notation in notations_list: + administre_id = notation['administre_pam__id'] + topCounter = 0 + allNotationsInvolved = Notation.objects.filter(administre_pam_id=administre_id) + for note in allNotationsInvolved: + topList = list(Notation.objects.filter(no_id=note.no_id) + .order_by('-no_score_administre') + .values('administre_pam_id')) + topAdministres = [administre['administre_pam_id'] for administre in topList] + if administre_id in topAdministres: + topCounter += 1 + + notation['administre__nb_top'] = topCounter + + result = [] + for notation in notations_list: + res_notation = {q1: {}} + for key in notation: + if (q + "__") in key: + res_notation[q1][key.replace(q + '__', '').replace(q1 + '__', '')] = notation[key] + else: + res_notation[key] = notation[key] + result.append(res_notation) + + + return Response(result) diff --git a/backend-django/backend/views/references.py b/backend-django/backend/views/references.py new file mode 100644 index 0000000..3ab1097 --- /dev/null +++ b/backend-django/backend/views/references.py @@ -0,0 +1,71 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from django.db.models import Q + +from ..models import AvisPosteChoices as AvisPoste +from ..models import (Competence, Domaine, Filiere, FormationEmploi, Grade, + SousVivier, PAM, + Marque, MarquesGroupe, RefSvFil, SpecifiqueChoices) +from ..models import StatutFuturChoices as StatutFutur +from ..models import StatutPamChoices as StatutPam +from ..models import ZoneGeographique +from ..models import Fonction +from ..serializers import (ChoicesSerializer, RefAvisPosteChoicesSerializer, + RefStatutPamChoicesSerializer) +from ..serializers.commun import AssignmentState +from ..utils.decisions import get_all_decisions +from .commun import execution_time_viewset, query_count_viewset + + +@execution_time_viewset +@query_count_viewset +class ReferencesView(APIView): + """ + Cette classe est dédiée au vue de la reference. + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + """ + La fonction get envoie les données de référence + + :return: réponse contenant les données de référence + """ + + groupesMarques = [obj.as_dict() for obj in MarquesGroupe.objects.all().order_by('gm_code')] + marques = [obj.as_dict() for obj in Marque.objects.all().order_by('mar_code')] + competences = [obj.as_dict() for obj in Competence.objects.all().order_by('comp_id')] + domaines = [obj.as_dict() for obj in Domaine.objects.all().order_by('d_code')] + filieres = [obj.as_dict() for obj in Filiere.objects.all().order_by('domaine__d_code', 'f_code')] + zones = [obj.as_dict() for obj in ZoneGeographique.objects.all()] + # TODO: Vérifier la présence de l'ordre sur les postes' + grades = [obj.as_dict() for obj in Grade.objects.all()] + FEs = [obj.as_dict() for obj in FormationEmploi.objects.all().order_by('fe_mere_credo')] + garnison_lieux = [obj.as_dict() for obj in FormationEmploi.objects.all().order_by('fe_mere_credo', 'fe_garnison_lieu')] + RefSvFils_init = {obj.as_dict()['ref_sv_fil_code'] for obj in RefSvFil.objects.all().order_by('ref_sv_fil_code')} + RefSvFils = [{'ref_sv_fil_code': obj} for obj in RefSvFils_init] + Specifique = [{'code': i.value} for i in SpecifiqueChoices] + SousViviers = [obj.as_dict() for obj in SousVivier.objects.filter(~Q(sv_id='BVT')).order_by('sv_libelle')] + Pams = [obj.as_dict() for obj in PAM.objects.filter(~Q(pam_id='SORG')& Q(pam_statut__in=['PAM en cours', 'PAM A+1'])).order_by('pam_id')] + Etr = [obj.as_dict() for obj in Fonction.objects.all()] + return Response({ + 'groupesMarques': groupesMarques, + 'marques': marques, + 'domaines': domaines, + 'filieres': filieres, + 'grades': grades, + 'competences': competences, + 'FEs': FEs, + 'statutFutur': ChoicesSerializer(StatutFutur, many=True).data, + 'Zones': zones, + 'decisions': ChoicesSerializer(get_all_decisions(), many=True).data, + 'avisAdministre': RefStatutPamChoicesSerializer(StatutPam, many=True).data, + 'avisPoste': RefAvisPosteChoicesSerializer(AvisPoste, many=True).data, + 'RefSvFils': RefSvFils, + 'Specifique': Specifique, + 'etatsFeBmob': tuple(e.value for e in AssignmentState), + 'sousViviers': SousViviers, + 'pams': Pams, + 'etr' : Etr, + }) diff --git a/backend-django/backend/views/scoring.py b/backend-django/backend/views/scoring.py new file mode 100644 index 0000000..f72f90f --- /dev/null +++ b/backend-django/backend/views/scoring.py @@ -0,0 +1,426 @@ +from datetime import datetime +from typing import Optional, Tuple + +import pandas as pd +import requests +from django.conf import settings +from django.db.models import Q +from django.db.transaction import atomic +from django.http import Http404, JsonResponse +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from rest_framework import status +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models import Administre, StatutPamChoices +from ..models import AvisPosteChoices as AvisPoste +from ..models import Poste, SousVivier +from ..models.calcul import Calcul +from ..models.calcul import StatutCalculChoices as StatutCalcul +from ..serializers import ScoringSelectifValidator, ScoringValidator +from ..utils.decorators import class_logger +from ..utils_calcul import lancer_calculs, lancer_calculSelectif +from .commun import (GestionnairePermission, api_exception, + execution_time_viewset, query_count_viewset) +import _json + + +# @execution_time_viewset +# @query_count_viewset +@class_logger +class ScoringView(APIView): + """ + Cette classe est dédiée au vue de lancement pour le scoring et au calcul des scores des administrés à muter avec les postes à pourvoir. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + + def __recuperer_chemin_requete(self, request: Request) -> str: + """ récupère et reconstruit le chemin de la requête (mais pas forcément tel que le client la connaît) + + :type request: rest_framework.request.Request + :param request: Request contenant le port et l'adrresse ip du serveur + + :return: - **return** (*string*): une chaine de caractère contenant le statut modifié du calcul. + + """ + + port = request.get_port() + return f"{request.scheme}://localhost{':' + port if port else ''}{request.path}" + + + def __appeler_api_depiler_calculs(self, request: Request) -> None: + """ appelle l'API pour lancer le prochain calcul en attente, ce qui permet d'attribuer l'opération à un worker indéfini + :type request: rest_framework.request.Request + :param request: Request contenant le chemin de la requète + """ + url = self.__recuperer_chemin_requete(request) + try: + requests.patch(url, headers=request.headers, cookies=request.COOKIES, timeout=0.001) + except requests.exceptions.ReadTimeout: + pass + except Exception as e: + self.logger.debug("échec de l'appel à %s: %s", url, e) + + @atomic + def __creer_calcul(self, pam_id: str, sv_id: str) -> Calcul: + """ crée un calcul en base sauf s'il existe déjà un calcul en cours ou en attente pour le même sous-vivier + Raises + ------ + ApiException: + un calcul est déjà en cours ou en attente pour le même sous-vivier + IntegrityError: + la création a échoué + + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :return: - **Object** (*Object*): retourne l'objet calcul crée + """ + + Statut = StatutCalcul + COL_ID = 'id' + COL_SV = 'sous_vivier_id' + COL_PAM = 'pam_id' + COL_STATUT = 'ca_statut' + COL_DEBUT = 'ca_date_debut' + + colonnes = (COL_ID, COL_SV, COL_PAM, COL_DEBUT, COL_STATUT) + df = pd.DataFrame.from_records(Calcul.objects.values_list(*colonnes), columns=colonnes) + + en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values + if sv_id in en_cours: + raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-vivier') + + en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_SV].values + if sv_id in en_attente: + raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en attente pour ce sous-vivier') + + if en_attente.size: + calcul_autorise = False + else: + nb_max = getattr(settings, 'MAX_CALCULS', None) + calcul_autorise = not isinstance(nb_max, int) or len(en_cours) < nb_max + + date_debut = timezone.now() + Calcul.objects.filter(Q(id=str(sv_id)+str(pam_id)) & Q(pam_id=pam_id) & Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete() + if calcul_autorise: + return Calcul.objects.create(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_COURS) + return Calcul.objects.create(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_ATTENTE) + + @atomic + def __creer_calculSelectif(self, sv_id: str, pam_id: str, l_a_id: list, l_p_id: list) -> Calcul: + """ crée un calcul en base sauf s'il existe déjà un calcul en cours ou en attente pour le même sous-vivier + Raises + ------ + ApiException: + un calcul est déjà en cours ou en attente pour le même sous-vivier + IntegrityError: + la création a échoué + + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :return: - **Object** (*Object*): retourne l'objet calcul crée + """ + + Statut = StatutCalcul + COL_ID = 'id' + COL_SV = 'sous_vivier_id' + COL_STATUT = 'ca_statut' + COL_DEBUT = 'ca_date_debut' + COL_PAM = 'pam_id' + + colonnes = (COL_ID, COL_SV, COL_DEBUT, COL_STATUT, COL_PAM) + df = pd.DataFrame.from_records(Calcul.objects.values_list(*colonnes), columns=colonnes) + + + en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values + + if l_a_id and l_p_id: + if sv_id in en_cours: + raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-ensemble') + + en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_SV].values + if sv_id in en_attente: + raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en attente pour ce sous-ensemble') + + if en_attente.size: + calcul_autorise = False + else: + nb_max = getattr(settings, 'MAX_CALCULS', None) + calcul_autorise = not isinstance(nb_max, int) or len(en_cours) < nb_max + + date_debut = timezone.now() + Calcul.objects.filter(Q(id=str(sv_id)+str(pam_id)+'selectif') & Q(pam_id=pam_id) & Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete() + if calcul_autorise: + return Calcul.objects.create(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_COURS) + return Calcul.objects.create(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_ATTENTE) + + @atomic + def __modifier_statut_premier_calcul_en_attente(self) -> Optional[Tuple[str, datetime]]: + """ lance le calcul en attente le plus ancien (si possible). Il ne s'agit que d'une modification en base. + (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :type ca_date_debut : date + :param ca_date_debut: date de début calcul en attente + :return: - **Object** (*Object*): retourne l'objet calcul crée + Raises + ------ + ApiException: + un calcul est déjà en cours ou en attente pour le même sous-vivier + IntegrityError: + la création a échoué + """ + + Statut = StatutCalcul + COL_ID = 'id' + COL_SV = 'sous_vivier_id' + COL_STATUT = 'ca_statut' + COL_DEBUT = 'ca_date_debut' + COL_PAM = 'pam_id' + + + colonnes = (COL_ID, COL_SV, COL_PAM, COL_DEBUT, COL_STATUT) + df = pd.DataFrame.from_records(Calcul.objects.order_by(COL_DEBUT).values_list(*colonnes), columns=colonnes) + + en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE] + if en_attente.empty: + return None + + en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values + nb_max = getattr(settings, 'MAX_CALCULS', None) + calcul_autorise = not isinstance(nb_max, int) or en_cours.size < nb_max + if not calcul_autorise: + return None + + a_lancer = en_attente.iloc[0] + sv_id = a_lancer[COL_ID] + date_attente = a_lancer[COL_DEBUT] + date_debut = timezone.now() + + # la date de début est mise à jour aussi pour éviter de compter le temps d'attente + mis_a_jour = ( + Calcul.objects + .filter(sous_vivier_id=sv_id, ca_date_debut=date_attente, ca_statut=Statut.EN_ATTENTE) + .update(ca_date_debut=date_debut, ca_statut=Statut.EN_COURS) + ) + return (sv_id, date_debut) if mis_a_jour else None + + + def __executer_calcul(self, pam_id: str,sv_id: str, date_debut: datetime) -> Calcul: + """ exécute un calcul (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :type ca_date_debut : date + :param ca_date_debut: date de début calcul en attente + :return: - **Object** (*Object*): retourne l'objet calcul ainsi que son statut + """ + + Statut = StatutCalcul + qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut) + qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS) + try: + lancer_calculs(pam_id,sv_id) + except SystemExit: + self.logger.info("OGURE M'A TUER") + qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE) + raise + except Exception as e: + qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut) + if str(e) == "Arret du calcul": + qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE) + raise + else: + qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ERREUR) + raise + qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE) + return qs_calcul.first() + + def __executer_calculSelectif(self, sv_id: str, pam_id: str, l_a_id: list, l_p_id: list, date_debut: datetime) -> Calcul: + """ exécute un calcul (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé + :type request: rest_framework.request.Request + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :type ca_date_debut : date + :param ca_date_debut: date de début calcul en attente + :return: - **Object** (*Object*): retourne l'objet calcul ainsi que son statut + """ + + Statut = StatutCalcul + qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id)+'selectif',sous_vivier_id=sv_id, pam_id=pam_id, ca_date_debut=date_debut) + qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS) + try: + lancer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id) + + except SystemExit: + qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE) + raise + except Exception as e: + qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id)+'selectif',sous_vivier_id=sv_id, pam_id=pam_id,ca_date_debut=date_debut) + if str(e) == "Arret du calcul": + qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE) + raise + else: + qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ERREUR) + raise + qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE) + return qs_calcul.first() + + + def get(self, request: Request) -> Response: + """La fonction get vérifie s'il y a un calcul en cours et renvoie des informations sur le calcul. + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier + :return: - réponse contenant les informations sur le calcul comme statut, date_debut, date_fin, administres et postes. + """ + debut = None + fin = None + administres = 0 + postes = 0 + try: + sv_id = request.query_params['sous_vivier_id'] + pam_id = request.query_params['pam_id'] + calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id) + debut = calcul.ca_date_debut + fin = calcul.ca_date_fin + administres_statuts = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER] + administres = Administre.objects.filter(pam__pam_id=pam_id,sous_vivier_id=sv_id,a_statut_pam__in=administres_statuts).count() + postes = Poste.objects.values_list('p_avis').filter(Q(p_pam__pam_id=pam_id) & Q(sous_viviers=sv_id) & Q(p_avis__in=[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4])).count() + statut = calcul.ca_statut + statut_pourcentage = calcul.ca_statut_pourcentage + + except: + statut = StatutCalcul.AUCUN + statut_pourcentage = 0 + + return Response( + {"statut": statut, "statut_pourcentage": statut_pourcentage, "date_debut": debut, "date_fin": fin, "administres": administres, "postes": postes}) + + def patch(self, request: Request, id=None) -> Response: + """La fonction patch est une méthode privée qui execute des méthodes privées qui exécute le calcul et le dépile. + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier et la date de dbut du calcul + """ + Statut = StatutCalcul + try: + id_et_debut = self.__modifier_statut_premier_calcul_en_attente() + if id_et_debut is None: + return Response({'message': "aucun calcul n'a été lancé"}) + + try: + calcul = self.__executer_calcul(id_et_debut[0], id_et_debut[1]) + return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN}) + finally: + self.__appeler_api_depiler_calculs(request) + except (Http404, APIException): + raise + except SystemExit: + raise APIException('lancement du prochain calcul arrêté') + except: + message = 'impossible de lancer le prochain calcul en attente' + self.logger.exception(message) + raise APIException(message) + + + def post(self, request: Request) -> Response: + """ lance le calcul sur un sous vivier + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier + :type sv_id: str + :param sv_id: clé primaire du sous vivier + :return: - **return** (*Response*): retourne le statut des postes + """ + try: + # Gestion des 2 cas, calculs sélectifs ou calculs sur l'ensemble du sous-vivier + if 'administre_id' in request.data.keys() and 'poste_id' in request.data.keys(): + Statut = StatutCalcul + validator = ScoringSelectifValidator(data=request.data) + validator.is_valid(raise_exception=True) + sv_id = validator.validated_data.get('sous_vivier_id') + pam_id = validator.validated_data.get('pam_id') + get_object_or_404(SousVivier.objects, sv_id=sv_id) + l_a_id = validator.validated_data.get('administre_id') + l_p_id = validator.validated_data.get('poste_id') + # calcul + calcul = self.__creer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id) + if calcul.ca_statut == Statut.EN_COURS: + try: + calcul = self.__executer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id, calcul.ca_date_debut) + except Exception as e: + Statut= Statut.ERREUR + calcul.ca_statut = Statut + calcul.save() + error = str(e).split(',') + return JsonResponse({'info': error[0]}) + finally: + self.__appeler_api_depiler_calculs(request) + + else: + Statut = StatutCalcul + validator = ScoringValidator(data=request.data) + # validation + validator.is_valid(raise_exception=True) + sv_id = validator.validated_data.get('sous_vivier_id') + pam_id = validator.validated_data.get('pam_id') + get_object_or_404(SousVivier.objects, sv_id=sv_id) + # calcul + calcul = self.__creer_calcul(pam_id,sv_id) + if calcul.ca_statut == Statut.EN_COURS: + try: + calcul = self.__executer_calcul(pam_id,sv_id, calcul.ca_date_debut) + except Exception as e: + + Statut= Statut.ERREUR + calcul.ca_statut = Statut + calcul.save() + error = str(e).split(',') + return JsonResponse({'info': error[0]}) + + finally: + self.__appeler_api_depiler_calculs(request) + return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN}) + except (Http404, APIException): + raise + except SystemExit: + raise APIException('calcul arrêté') + except: + message = 'impossible de lancer le calcul' + self.logger.exception(message) + raise APIException(message) + + +@class_logger +@execution_time_viewset +@query_count_viewset +class ArretCalcul(APIView): + """ + Cette classe est dédiée a l'arrêt du calcul. + """ + permission_classes = [IsAuthenticated, GestionnairePermission] + + def post(self, request): + """La fonction post change le statut d'un calcul à EN_ATTENTE_ARRET + + :type request: rest_framework.request.Request + :param request: Request contenant l'identifiant du sous vivier + + :return: - **return** (*json*): json contenant le statut modifié du calcul. + """ + Statut = StatutCalcul + sv_id = request.data['sous_vivier_id'] + pam_id = request.data['pam_id'] + calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id) + if Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id).exists() and calcul.ca_statut == Statut.EN_COURS: + calcul.ca_statut = Statut.EN_ATTENTE_ARRET + calcul.save() + elif Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id).exists() and calcul.ca_statut == Statut.EN_ATTENTE: + calcul.ca_statut = Statut.TERMINE + calcul.save() + return Response({"statut": calcul.ca_statut}) diff --git a/backend-django/backend/views/suppression_administres.py b/backend-django/backend/views/suppression_administres.py new file mode 100644 index 0000000..81cc0f2 --- /dev/null +++ b/backend-django/backend/views/suppression_administres.py @@ -0,0 +1,70 @@ +import pandas as pd +from django.db.transaction import atomic +from django.http import Http404 +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAdminUser, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..serializers import SuppressionAdministresSerializer +from ..utils.alimentation_decorators import (data_perf_logger_factory, + get_data_logger) +from ..utils.decorators import execution_time, query_count +from ..utils_extraction import (DataFrameTypes, FileTypes, read_files_by_type, + to_table_suppression_administres) +from ..utils_insertion import suppression_administres + + +class SuppressionAdministresView(APIView): + """ Vue pour supprimer les administrés de la base """ + + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = SuppressionAdministresSerializer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = get_data_logger(self) + + + def get(self, request): + return Response("Formulaire d\'OGURE NG permettant de supprimer des administés de la base de données") + + + @atomic + @execution_time(logger_factory=data_perf_logger_factory) + @query_count(logger_factory=data_perf_logger_factory) + def post(self, request): + """ + Charge le(s) fichier(s) et met à jour la base. + + :param request: requête, contient les fichiers + :type request: class:`rest_framework.request.Request` + + :raises: class:`rest_framework.exceptions.APIException` + + :return: réponse + :rtype: class:`rest_framework.response.Response` + """ + try: + validator = self.serializer_class(data=request.data) + validator.is_valid(raise_exception=True) + adm_suppr = 0 + + df_adm_suppr = read_files_by_type({ + FileTypes.ADM_SUPPR: validator.validated_data.get('administres') + }).get(DataFrameTypes.ADM_SUPPR) + + if df_adm_suppr is not None: + df = to_table_suppression_administres(df_adm_suppr) + self.logger.info('Extraction des administés à supprimer ------> Succès') + adm_suppr = suppression_administres(df) + self.logger.info('Suppression des administrés ------> Succès') + else: + self.logger.info('Mise à jour ignorée : suppression des administrés') + return Response({f'Suppression de {adm_suppr} administré(s) réussie'}) + except (Http404, APIException): + raise + except BaseException: + message = "Echec de la suppression" + self.logger.exception(message) + raise APIException(message) diff --git a/backend-django/entrypoint.sh b/backend-django/entrypoint.sh new file mode 100644 index 0000000..4a21a65 --- /dev/null +++ b/backend-django/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +if [ "$DATABASE" = "postgres" ] +then + echo "Waiting for postgres..." + + while ! nc -z $SQL_HOST $SQL_PORT; do + sleep 0.1 + done + + echo "PostgreSQL started" +fi + +python manage.py flush --no-input +python manage.py migrate + +exec "$@" \ No newline at end of file diff --git a/backend-django/main.py b/backend-django/main.py new file mode 100644 index 0000000..3450bc0 --- /dev/null +++ b/backend-django/main.py @@ -0,0 +1,24 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ogure.wsgi import application + +# App Engine by default looks for a main.py file at the root of the app +# directory with a WSGI-compatible object called app. +# This file imports the WSGI-compatible object of your Django app, +# application from ogure/wsgi.py and renames it app so it is discoverable by +# App Engine without additional configuration. +# Alternatively, you can add a custom entrypoint field in your app.yaml: +# entrypoint: gunicorn -b :$PORT ogure.wsgi +app = application diff --git a/backend-django/manage.py b/backend-django/manage.py new file mode 100644 index 0000000..082cc38 --- /dev/null +++ b/backend-django/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ogure.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend-django/nohup.out b/backend-django/nohup.out new file mode 100644 index 0000000..aab3687 --- /dev/null +++ b/backend-django/nohup.out @@ -0,0 +1,12 @@ +[2022-08-25 06:08:02 -0400] [106223] [INFO] Starting gunicorn 20.0.4 +[2022-08-25 06:08:02 -0400] [106223] [ERROR] Connection in use: ('0.0.0.0', 8000) +[2022-08-25 06:08:02 -0400] [106223] [ERROR] Retrying in 1 second. +[2022-08-25 06:08:03 -0400] [106223] [ERROR] Connection in use: ('0.0.0.0', 8000) +[2022-08-25 06:08:03 -0400] [106223] [ERROR] Retrying in 1 second. +[2022-08-25 06:08:04 -0400] [106223] [ERROR] Connection in use: ('0.0.0.0', 8000) +[2022-08-25 06:08:04 -0400] [106223] [ERROR] Retrying in 1 second. +[2022-08-25 06:08:05 -0400] [106223] [ERROR] Connection in use: ('0.0.0.0', 8000) +[2022-08-25 06:08:05 -0400] [106223] [ERROR] Retrying in 1 second. +[2022-08-25 06:08:06 -0400] [106223] [ERROR] Connection in use: ('0.0.0.0', 8000) +[2022-08-25 06:08:06 -0400] [106223] [ERROR] Retrying in 1 second. +[2022-08-25 06:08:07 -0400] [106223] [ERROR] Can't connect to ('0.0.0.0', 8000) diff --git a/backend-django/ogure/__init__.py b/backend-django/ogure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend-django/ogure/asgi.py b/backend-django/ogure/asgi.py new file mode 100644 index 0000000..79bb4c4 --- /dev/null +++ b/backend-django/ogure/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for ogure project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ogure.settings') + +application = get_asgi_application() diff --git a/backend-django/ogure/cors.py b/backend-django/ogure/cors.py new file mode 100644 index 0000000..fd52621 --- /dev/null +++ b/backend-django/ogure/cors.py @@ -0,0 +1,10 @@ + + +def cors_all_access_middleware(get_response): + def middleware(request): + response = get_response(request) + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Headers"] = "*" + return response + + return middleware \ No newline at end of file diff --git a/backend-django/ogure/middleware.py b/backend-django/ogure/middleware.py new file mode 100644 index 0000000..e384154 --- /dev/null +++ b/backend-django/ogure/middleware.py @@ -0,0 +1,17 @@ +class CustomCorsMiddleware: + def __init__(self, get_response): + self.get_response = get_response + # One-time configuration and initialization. + + def __call__(self, request): + # Code to be executed for each request before + # the view (and later middleware) are called. + + response = self.get_response(request) + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Headers"] = "*" + + # Code to be executed for each request/response after + # the view is called. + + return response \ No newline at end of file diff --git a/backend-django/ogure/settings.py b/backend-django/ogure/settings.py new file mode 100644 index 0000000..ae1863d --- /dev/null +++ b/backend-django/ogure/settings.py @@ -0,0 +1,256 @@ +""" +Django settings for ogure project. + +Generated by 'django-admin startproject' using Django 3.2. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get("SECRET_KEY", "foo") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = int(os.environ.get("DEBUG", default=1)) + +ALLOWED_HOSTS = ['*'] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'backend.apps.BackendConfig', + 'corsheaders', + 'rest_framework', + 'django_filters', + 'drf_api_logger', +] + +AUTH_USER_MODEL = "backend.CustomUser" +LOGIN_REDIRECT_URL = '/' + +CSRF_COOKIE_SAMESITE = os.environ.get("DJANGO_CSRF_COOKIE_SAMESITE", 'Strict') +SESSION_COOKIE_SAMESITE = os.environ.get("DJANGO_SESSION_COOKIE_SAMESITE", 'Strict') +# CSRF_COOKIE_HTTPONLY = os.environ.get("DJANGO_CSRF_COOKIE_HTTPONLY", False) +CSRF_COOKIE_HTTPONLY = False +# False since we will grab it via universal-cookies +SESSION_COOKIE_HTTPONLY = os.environ.get("DJANGO_SESSION_COOKIE_HTTPONLY", False) +# Complete with all autorized domains +CSRF_TRUSTED_ORIGINS = ['*'] +# PROD ONLY +# CSRF_COOKIE_SECURE = os.environ.get("DJANGO_# CSRF_COOKIE_SECURE", True) +# SESSION_COOKIE_SECURE = os.environ.get("DJANGO_# SESSION_COOKIE_SECURE", True) +CSRF_COOKIE_SECURE = False +SESSION_COOKIE_SECURE = False +# ajout manuel DRHAT +SESSION_COOKIE_AGE = 14400 +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +CACHE_MIDDLEWARE_SECONDS = 600 +#fin ajout +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication'], + 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'], + 'TEST_REQUEST_DEFAULT_FORMAT': 'json' +} + +MIDDLEWARE = [ + 'django.middleware.gzip.GZipMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'ogure.middleware.CustomCorsMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'drf_api_logger.middleware.api_logger_middleware.APILoggerMiddleware', + +] + +ROOT_URLCONF = 'ogure.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'ogure.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases +if os.getenv('GAE_APPLICATION', None): + # Running on production App Engine, so connect to Google Cloud SQL using + # the unix socket at /cloudsql/ + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'HOST': '/cloudsql/ogure-ng:europe-west1:ogure-db', + 'USER': 'ogure', + 'PASSWORD': 'ogure', + 'NAME': 'ogure', + } + } +else: + DATABASES = { + 'default': { + "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.postgresql"), + "NAME": os.environ.get("SQL_DATABASE", "Ogure-DB"), + "USER": os.environ.get("SQL_USER", "postgres"), + "PASSWORD": os.environ.get("SQL_PASSWORD", "S91g7xDDu9bYGA"), + "HOST": os.environ.get("SQL_HOST", "localhost"), + "PORT": os.environ.get("SQL_PORT", "5432"), + } + } + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'fr' + +TIME_ZONE = 'Europe/Paris' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/staticfiles/" +STATIC_ROOT = [os.path.join(BASE_DIR, "staticfiles")] + +STATICFILES_DIRS = [ + # Where Django should look for React's static files (css, js) + #os.path.join(BASE_DIR, "frontend/dist"), +] + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +FORCE_SCRIPT_NAME = os.environ.get('DJANGO_FORCE_SCRIPT_NAME', '') + +# Configuration des variables associ�es au log de requ�tes avec DRF-API-Logger +DRF_API_LOGGER_DATABASE = True # Default to False +DRF_API_LOGGER_SLOW_API_ABOVE = 2000 +# TODO : Sp�cifier ici l'ensemble des champs que l'on ne souhaite pas retrouver dans les logs de requ�te +DRF_API_LOGGER_EXCLUDE_KEYS = ['password', 'token', 'access', 'refresh', 'results'] +# TODO : d�finir pr�cis�ment le format des logs qu'on veut visualiser et se lib�rer du format de donn�es de DRF-API-Logger +# FIXME : bug de suppression des logs dans l'admin + +nb_max = os.getenv('MAX_CALCULS', None) +MAX_CALCULS = None if nb_max is None else int(nb_max) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '{asctime} {levelname} [{name}] {message}', + 'style': '{', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'standard' + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': './debug.log', + 'formatter': 'standard', + } + }, + 'loggers': { + '': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG', + }, + 'django': { + 'handlers': ['console', 'file'], + #'level': os.getenv('DJANGO_LOG_LEVEL', 'WARNING'), + 'level': 'DEBUG', + 'propagate': False, + }, + 'django.db.backends': { + 'handlers': ['console'], + #'level': os.getenv('DJANGO_LOG_LEVEL_DB', 'WARNING'), + 'level': 'DEBUG', + 'propagate': False, + }, + 'django.security.csrf': { + 'handlers': ['console'], + #'level': os.getenv('DJANGO_LOG_LEVEL', 'WARNING'), + 'level': 'DEBUG', + 'propagate': False, + }, + 'backend': { + 'handlers': ['console', 'file'], + #'level': os.getenv('APP_LOG_LEVEL', 'WARNING'), + 'level': 'DEBUG', + 'propagate': False, + }, + 'compute': { + 'handlers': ['console', 'file'], + #'level': os.getenv('APP_LOG_LEVEL', 'WARNING'), + 'level': 'DEBUG', + 'propagate': False, + }, + 'ogure': { + 'handlers': ['console', 'file'], + #'level': os.getenv('APP_LOG_LEVEL', 'WARNING'), + 'level': 'DEBUG', + 'propagate': False, + }, + } +} diff --git a/backend-django/ogure/urls.py b/backend-django/ogure/urls.py new file mode 100644 index 0000000..9c394be --- /dev/null +++ b/backend-django/ogure/urls.py @@ -0,0 +1,34 @@ +"""ogure URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.conf import settings +from django.conf.urls.static import static + +admin.site.site_header = "Administration OGURE NG" +admin.site.site_title = "Administration OGURE NG" +admin.site.index_title = "Bienvenue sur l'espace d'administration OGURE NG" + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('backend.urls')), + path('accounts/', include('django.contrib.auth.urls')), +] +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +urlpatterns += staticfiles_urlpatterns() +#urlpatterns.append(path('', include('frontend.urls'))) + diff --git a/backend-django/ogure/wsgi.py b/backend-django/ogure/wsgi.py new file mode 100644 index 0000000..0636d30 --- /dev/null +++ b/backend-django/ogure/wsgi.py @@ -0,0 +1,33 @@ +""" +WSGI config for ogure project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application +from django.conf import settings + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ogure.settings') + +_application = get_wsgi_application() + + +# Here is the important part +def application(environ, start_response): + script_name = getattr(settings, 'FORCE_SCRIPT_NAME', None) + if script_name: + environ['SCRIPT_NAME'] = script_name + path_info = environ['PATH_INFO'] + if path_info.startswith(script_name): + environ['PATH_INFO'] = path_info[len(script_name):] + + scheme = environ.get('HTTP_X_SCHEME', '') + if scheme: + environ['wsgi.url_scheme'] = scheme + + return _application(environ, start_response) diff --git a/backend-django/requirements.txt b/backend-django/requirements.txt new file mode 100644 index 0000000..49dac29 --- /dev/null +++ b/backend-django/requirements.txt @@ -0,0 +1,14 @@ +Django >=3.0,<4.0 +django-bulk-update-or-create==0.3.0 +django-cors-headers==3.7.0 +django-filter==2.4.0 +djangorestframework==3.12.4 +djangorestframework-simplejwt==4.7.1 +drf-api-logger==1.0.8 +gunicorn==20.0.4 +matching==1.4 +openpyxl==3.0.7 +pandas==1.2.5 +psycopg2-binary>=2.8 +requests==2.27.1 +xlrd==2.0.1 diff --git a/backend-django/templates/admin/base_site.html b/backend-django/templates/admin/base_site.html new file mode 100644 index 0000000..f8a0cad --- /dev/null +++ b/backend-django/templates/admin/base_site.html @@ -0,0 +1,14 @@ +{% extends "admin/base.html" %} + +{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} + +{% block branding %} + +

{{ site_header|default:_('OGURE NG') }}

+{% endblock %} + +{% block nav-global %}{% endblock %} diff --git a/backend-django/templates/charts_change_list.html b/backend-django/templates/charts_change_list.html new file mode 100644 index 0000000..8f85766 --- /dev/null +++ b/backend-django/templates/charts_change_list.html @@ -0,0 +1,118 @@ +{% extends "admin/change_list.html" %} +{% load static %} + + +{% block extrahead %} +{{ block.super }} + + + +{% endblock %} + +{% block content %} + +
+ +
+
+ +
+ +{{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/backend-django/templates/registration/base_generic.html b/backend-django/templates/registration/base_generic.html new file mode 100644 index 0000000..ac5fe58 --- /dev/null +++ b/backend-django/templates/registration/base_generic.html @@ -0,0 +1,20 @@ + + + + {% block title %}Authentification{% endblock %} + + + + + {% load static %} + + + +
+
+ {% block content %}{% endblock %} +
+
+ + \ No newline at end of file diff --git a/backend-django/templates/registration/logged_out.html b/backend-django/templates/registration/logged_out.html new file mode 100644 index 0000000..c07ec36 --- /dev/null +++ b/backend-django/templates/registration/logged_out.html @@ -0,0 +1,9 @@ +{% extends "registration/base_generic.html" %} + +{% block content %} + +

Déconnecté!

+ +
+ Cliquez ici pour vous reconnecter. +{% endblock %} diff --git a/backend-django/templates/registration/login.html b/backend-django/templates/registration/login.html new file mode 100644 index 0000000..3e0bd78 --- /dev/null +++ b/backend-django/templates/registration/login.html @@ -0,0 +1,34 @@ +{% extends "registration/base_generic.html" %} + +{% block content %} +

Bienvenue sur Ogure NG

+ + +{% if user.is_authenticated %} +

Vous n'avez pas accès à cette page.
+ Cliquez ici pour continuer.

+{% else %} + +
+
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+
+ {% if form.errors %} +

Your username and password didn't match. Please try again.

+ {% endif %} + + + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/backend-django/templates/registration/password_change_done.html b/backend-django/templates/registration/password_change_done.html new file mode 100644 index 0000000..9f1dbcb --- /dev/null +++ b/backend-django/templates/registration/password_change_done.html @@ -0,0 +1,13 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} +{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% translate 'Documentation' %} / {% endif %}{% translate 'Change password' %} / {% translate 'Log out' %}{% endblock %} +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% translate 'Your password was changed.' %}

+{% endblock %} diff --git a/backend-django/templates/registration/password_change_form.html b/backend-django/templates/registration/password_change_form.html new file mode 100644 index 0000000..9bcdfca --- /dev/null +++ b/backend-django/templates/registration/password_change_form.html @@ -0,0 +1,57 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} +{% block extrastyle %}{{ block.super }}{% endblock %} +{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% translate 'Documentation' %} / {% endif %} {% translate 'Change password' %} / {% translate 'Log out' %}{% endblock %} +{% block breadcrumbs %} + +{% endblock %} + +{% block content %}
+ +
{% csrf_token %} +
+{% if form.errors %} +

+ {% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} +

+{% endif %} + + +

{% translate 'Please enter your old password, for security’s sake, and then enter your new password twice so we can verify you typed it in correctly.' %}

+ +
+ +
+ {{ form.old_password.errors }} + {{ form.old_password.label_tag }} {{ form.old_password }} +
+ +
+ {{ form.new_password1.errors }} + {{ form.new_password1.label_tag }} {{ form.new_password1 }} + {% if form.new_password1.help_text %} +
{{ form.new_password1.help_text|safe }}
+ {% endif %} +
+ +
+{{ form.new_password2.errors }} + {{ form.new_password2.label_tag }} {{ form.new_password2 }} + {% if form.new_password2.help_text %} +
{{ form.new_password2.help_text|safe }}
+ {% endif %} +
+ +
+ +
+ +
+ +
+
+ +{% endblock %} diff --git a/docker-compose (copie).yml b/docker-compose (copie).yml new file mode 100644 index 0000000..faeb34f --- /dev/null +++ b/docker-compose (copie).yml @@ -0,0 +1,45 @@ +version: '3.7' + +services: + node: + build: ./node + + volumes: + - front_ogure:/static + + db: + image: postgres:13.3-alpine + container_name: ogure_db + expose: + - 5432 + volumes: + - db_ogure:/var/lib/postgresql/data/ + env_file: + - .env.db + + backend-django: + build: . + volumes: + - staticfiles:/staticfiles + env_file: + - .env + ports: + - "8001:8000" + depends_on: + - db + + nginx: + build: ./nginx + volumes: + - staticfiles:/staticfiles + - front_ogure:/node + ports: + - "80:80" + depends_on: + - backend-django + - node + +volumes: + staticfiles: + db_ogure: + front_ogure: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a1e61ab --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.7' + +services: + + + db: + build: ./postgresql + container_name: ogure_db + ports: + - 5432:5432 + volumes: + - db_ogure:/var/lib/postgresql/data/ + env_file: + - ./postgresql/.env.db + + backend-django: + build: . + container_name: ogure_backend + volumes: + - staticfiles:/staticfiles + env_file: + - .env + ports: + - 8001:8000 + depends_on: + - db + node: + container_name: ogure_node + build: ./node + + volumes: + - front_ogure:/home/node/app/static + + nginx: + build: ./nginx + container_name: ogure_front + volumes: + - staticfiles:/staticfiles + - front_ogure:/usr/share/nginx/html/static + ports: + - 80:80 + depends_on: + - backend-django + +volumes: + front_ogure: + db_ogure: + staticfiles: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..dff391b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,11 @@ +#!bin/sh + + +python manage.py makemigrations +python manage.py migrate +python manage.py collectstatic --no-input +DJANGO_SUPERUSER_PASSWORD=$SUPER_USER_PASSWORD python manage.py createsuperuser --id $SUPER_USER_ID--username $SUPER_USER_NAME --noinput + +gunicorn ogure.wsgi:application --timeout 7200 --bind 0.0.0.0:8000 --name ogure + + diff --git a/nginx/.gitkeep b/nginx/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..370e96c --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:1.23.2-alpine + +RUN mkdir -p /usr/share/nginx/html/static/frontend +##COPY index.html /usr/share/nginx/html/static/frontend/. +COPY ./default.conf /etc/nginx/conf.d/default.conf + + diff --git a/nginx/default (copie).conf b/nginx/default (copie).conf new file mode 100644 index 0000000..dc0e5ac --- /dev/null +++ b/nginx/default (copie).conf @@ -0,0 +1,39 @@ +upstream django { + server backend-django:8001; +} + + +server { + + listen 80; + #listen [::]:80; + server_name localhost; + + proxy_read_timeout 7200; + proxy_connect_timeout 7200; + proxy_send_timeout 7200; + + # backend-django #1 + location ^~ /staticfiles/ { + alias staticfiles/; + } + + + # backend-django #2 + location ~ ^/(api|accounts|admin)/? { + proxy_pass http://django; + proxy_set_header Host $host; + proxy_redirect off; + } + + # frontend-react + location / { + root /usr/share/nginx/html/static/frontend; + index index.html index.htm; + try_files $uri $uri/ /index.html; + gzip on; + gzip_comp_level 3; + gzip_vary on; + gzip_types application/javascript application/json application/xml image/bmp image/jpeg image/gif image/png image/svg+xml text/css text/html text/plain; + } +} diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..dc0e5ac --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,39 @@ +upstream django { + server backend-django:8001; +} + + +server { + + listen 80; + #listen [::]:80; + server_name localhost; + + proxy_read_timeout 7200; + proxy_connect_timeout 7200; + proxy_send_timeout 7200; + + # backend-django #1 + location ^~ /staticfiles/ { + alias staticfiles/; + } + + + # backend-django #2 + location ~ ^/(api|accounts|admin)/? { + proxy_pass http://django; + proxy_set_header Host $host; + proxy_redirect off; + } + + # frontend-react + location / { + root /usr/share/nginx/html/static/frontend; + index index.html index.htm; + try_files $uri $uri/ /index.html; + gzip on; + gzip_comp_level 3; + gzip_vary on; + gzip_types application/javascript application/json application/xml image/bmp image/jpeg image/gif image/png image/svg+xml text/css text/html text/plain; + } +} diff --git a/nginx/index.html b/nginx/index.html new file mode 100644 index 0000000..5122956 --- /dev/null +++ b/nginx/index.html @@ -0,0 +1,2 @@ + +ceci est une page de test diff --git a/postgresql/.env.db b/postgresql/.env.db new file mode 100644 index 0000000..2deda6c --- /dev/null +++ b/postgresql/.env.db @@ -0,0 +1,3 @@ +POSTGRES_DB=Ogure-DB +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres diff --git a/postgresql/.gitkeep b/postgresql/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/postgresql/Dockerfile b/postgresql/Dockerfile new file mode 100644 index 0000000..08abda1 --- /dev/null +++ b/postgresql/Dockerfile @@ -0,0 +1,2 @@ +FROM postgres:15.0-alpine + diff --git a/references.md b/references.md new file mode 100644 index 0000000..145ff47 --- /dev/null +++ b/references.md @@ -0,0 +1 @@ +https://www.youtube.com/watch?v=vJAfq6Ku4cI diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..253aabd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +pandas==1.2.5 +django >=3.0,<4.0 +django-bulk-update-or-create==0.3.0 +django-cors-headers==3.7.0 +django-filter==2.4.0 +djangorestframework==3.12.4 +djangorestframework-simplejwt==4.7.1 +drf-api-logger==1.0.8 +gunicorn==20.0.4 +matching==1.4 +openpyxl==3.0.7 +psycopg2-binary>=2.8 +requests==2.27.1 +xlrd==2.0.1 diff --git a/sources/docker-compose-linux-x86_64_ b/sources/docker-compose-linux-x86_64_ new file mode 100644 index 0000000..23e6a9b Binary files /dev/null and b/sources/docker-compose-linux-x86_64_ differ