Skip to content

Commit

Permalink
chore: ajout d'un écran de visualisation & suppression de duplicats d…
Browse files Browse the repository at this point in the history
…'effectifs (#3116)

Co-authored-by: Maxime Dréau <[email protected]>
  • Loading branch information
sbenfares and totakoko authored Aug 31, 2023
1 parent cf6179d commit a6ea915
Show file tree
Hide file tree
Showing 19 changed files with 1,165 additions and 17 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"axiosist",
"coordonnees",
"DECA",
"duplicats",
"etablissement",
"gesti",
"groupby",
Expand Down
66 changes: 66 additions & 0 deletions server/src/common/actions/effectifs.duplicates.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ObjectId } from "mongodb";

import { effectifsDb } from "@/common/model/collections";

import { getAnneesScolaireListFromDate } from "../utils/anneeScolaireUtils";

/**
* Construction du pipeline d'aggregation du clean des noms / prenom pour identification des doublons
* @returns
*/
const getSanitizedNomPrenomPipeline = (
nomApprenantField = "$apprenant.nom",
prenomApprenantField = "$apprenant.prenom"
) => [
{
$addFields: {
sanitizedNom: { $regexFindAll: { input: { $toLower: nomApprenantField }, regex: /[A-Za-zÀ-ÖØ-öø-ÿ]/ } },
},
},
{
$addFields: {
sanitizedPrenom: { $regexFindAll: { input: { $toLower: prenomApprenantField }, regex: /[A-Za-zÀ-ÖØ-öø-ÿ]/ } },
},
},
{
$addFields: {
sanitizedNom: {
$reduce: { input: "$sanitizedNom.match", initialValue: "", in: { $concat: ["$$value", "$$this"] } },
},
},
},
{
$addFields: {
sanitizedPrenom: {
$reduce: { input: "$sanitizedPrenom.match", initialValue: "", in: { $concat: ["$$value", "$$this"] } },
},
},
},
];

/**
* Méthode de récupération de la liste des doublons au sein d'un organisme
* @param organisme_id
*/
export const getDuplicatesEffectifsForOrganismeId = async (organisme_id: ObjectId) => {
return await effectifsDb()
.aggregate([
{ $match: { organisme_id, annee_scolaire: { $in: getAnneesScolaireListFromDate(new Date()) } } },
...getSanitizedNomPrenomPipeline(),
{
$group: {
_id: {
nom_apprenant: "$sanitizedNom",
prenom_apprenant: "$sanitizedPrenom",
date_de_naissance_apprenant: "$apprenant.date_de_naissance",
annee_scolaire: "$annee_scolaire",
formation_cfd: "$formation.cfd",
},
count: { $sum: 1 },
duplicates: { $addToSet: { id: "$_id", created_at: "$created_at", source: "$source" } },
},
},
{ $match: { count: { $gt: 1 } } },
])
.toArray();
};
23 changes: 23 additions & 0 deletions server/src/http/routes/specific.routes/effectif.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ export default () => {
return res.json(buildEffectifResult(effectif));
});

router.get("/detail/:id", async ({ params }, res) => {
let { id } = await Joi.object({
id: Joi.string().required(),
})
.unknown()
.validateAsync(params, { abortEarly: false });

const effectif = await effectifsDb().findOne({ _id: new ObjectId(id) });
return res.json(effectif);
});

router.get("/:id/snapshot", async ({ params, query }, res) => {
const { id, organisme_id } = await Joi.object({
id: Joi.string().required(),
Expand Down Expand Up @@ -257,6 +268,18 @@ export default () => {
return res.json(buildEffectifResult(effectifUpdated));
});

router.delete("/:id", async ({ params }, res) => {
let { id } = await Joi.object({
id: Joi.string().required(),
})
.unknown()
.validateAsync(params, { abortEarly: false });

await effectifsDb().deleteOne({ _id: new ObjectId(id) });

return res.json({ status: "OK" });
});

router.post("/recherche-siret", async ({ body }, res) => {
// TODO organismeFormation
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
8 changes: 8 additions & 0 deletions server/src/http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import "express-async-errors";

import { activateUser, register, sendForgotPasswordRequest } from "@/common/actions/account.actions";
import { exportAnonymizedEffectifsAsCSV } from "@/common/actions/effectifs/effectifs-export.actions";
import { getDuplicatesEffectifsForOrganismeId } from "@/common/actions/effectifs.duplicates.actions";
import {
effectifsFiltersSchema,
fullEffectifsFiltersSchema,
Expand Down Expand Up @@ -447,6 +448,13 @@ function setupRoutes(app: Application) {
);
})
)
.get(
"/duplicates",
requireOrganismePermission("manageEffectifs"),
returnResult(async (req, res) => {
return await getDuplicatesEffectifsForOrganismeId(res.locals.organismeId);
})
)
.get(
"/sifa-export",
requireOrganismePermission("manageEffectifs"),
Expand Down
18 changes: 18 additions & 0 deletions server/tests/data/sampleEtablissements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ export default {
},
reseaux: ["AGRI"],
},
19040492100017: {
uai: "0040533H",
siret: "1904049210001",
nom: "LYCEE POLYVALENT TEST",
nature: NATURE_ORGANISME_DE_FORMATION.FORMATEUR,
adresse: {
academie: "2",
code_insee: "04112",
code_postal: "04100",
commune: "MANOSQUE",
complete: "LYCEE POLYVALENT DE TEST\r\n" + "117 AV REGIS RYCKEBUSH\r\n" + "04100 MANOSQUE\r\n" + "FRANCE",
departement: "04",
numero: 116,
region: "93",
voie: "AVREGIS RYCKEBUSH",
},
reseaux: ["AGRI"],
},
41461021200014: {
uai: "0611175W",
siret: "41461021200014",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { strict as assert } from "assert";

import { ObjectId } from "mongodb";

import { getDuplicatesEffectifsForOrganismeId } from "@/common/actions/effectifs.duplicates.actions";
import { Organisme } from "@/common/model/@types";
import { effectifsDb, organismesDb } from "@/common/model/collections";
import { createSampleEffectif, createRandomOrganisme } from "@tests/data/randomizedSample";
import { id } from "@tests/utils/testUtils";

const TEST_SIREN = "190404921";

const sampleOrganismeId = new ObjectId(id(1));
const sampleOrganisme: Organisme = {
_id: sampleOrganismeId,
...createRandomOrganisme({ siret: `${TEST_SIREN}00016` }),
};

/**
* Fonction utilitaire d'ajout en doublon d'effectif
* @param sampleEffectif
* @param nbDuplicates
*/
const insertDuplicateEffectifs = async (sampleEffectif, nbDuplicates = 2) => {
const insertedIdList: ObjectId[] = [];
for (let index = 0; index < nbDuplicates; index++) {
const { insertedId } = await effectifsDb().insertOne({
...sampleEffectif,
id_erp_apprenant: `ID_ERP_${index}`,
annee_scolaire: "2023-2024",
});
insertedIdList.push(insertedId);
}

return insertedIdList;
};

const sanitizeString = (string) => string.replace(/\s/g, "").toLowerCase();

describe("Test des actions Effectifs Duplicates", () => {
describe("getDuplicatesEffectifsForOrganismeId", () => {
beforeEach(async () => {
// Création d'un organisme de test
await organismesDb().insertOne(sampleOrganisme);
});

it("Permet de vérifier la récupération de doublons d'effectifs", async () => {
// Ajout de 2 doublons d'effectifs
const sampleEffectif = createSampleEffectif({ organisme: sampleOrganisme, annee_scolaire: "2023-2024" });
await insertDuplicateEffectifs(sampleEffectif);

const duplicates = await getDuplicatesEffectifsForOrganismeId(sampleOrganismeId);

// Vérification de la récupération d'une liste avec un doublon identifié 2 fois sur les champs de la clé d'unicité
assert.equal(duplicates.length, 1);
assert.equal(duplicates[0].count, 2);
assert.equal(duplicates[0].duplicates.length, 2);

assert.equal(sanitizeString(duplicates[0]._id.nom_apprenant), sanitizeString(sampleEffectif.apprenant.nom));
assert.equal(sanitizeString(duplicates[0]._id.prenom_apprenant), sanitizeString(sampleEffectif.apprenant.prenom));
assert.deepEqual(duplicates[0]._id.date_de_naissance_apprenant, sampleEffectif.apprenant.date_de_naissance);
assert.equal(duplicates[0]._id.annee_scolaire, sampleEffectif.annee_scolaire);
assert.equal(duplicates[0]._id.formation_cfd, sampleEffectif.formation?.cfd);
});

it("Permet de vérifier la non récupération de doublons d'effectifs", async () => {
// Ajout d'effectif
await insertDuplicateEffectifs(
createSampleEffectif({ organisme: sampleOrganisme, annee_scolaire: "2023-2024" }),
1
);
const duplicates = await getDuplicatesEffectifsForOrganismeId(sampleOrganismeId);

// Vérification de la récupération des doublons
assert.equal(duplicates.length, 0);
});

it("Permet de vérifier la récupération de doublons d'effectifs avec un prénom multi-casse", async () => {
// Ajout de 2 doublons d'effectifs
const sampleEffectif = createSampleEffectif({
organisme: sampleOrganisme,
apprenant: { prenom: "SYlvAiN" },
annee_scolaire: "2023-2024",
});
await insertDuplicateEffectifs(sampleEffectif, 5);

const duplicates = await getDuplicatesEffectifsForOrganismeId(sampleOrganismeId);

// Vérification de la récupération d'une liste avec un doublon identifié 5 fois sur les champs de la clé d'unicité
assert.equal(duplicates.length, 1);
assert.equal(duplicates[0].count, 5);
assert.equal(duplicates[0].duplicates.length, 5);

assert.equal(sanitizeString(duplicates[0]._id.nom_apprenant), sanitizeString(sampleEffectif.apprenant.nom));
assert.equal(sanitizeString(duplicates[0]._id.prenom_apprenant), sanitizeString(sampleEffectif.apprenant.prenom));
assert.deepEqual(duplicates[0]._id.date_de_naissance_apprenant, sampleEffectif.apprenant.date_de_naissance);
assert.equal(duplicates[0]._id.annee_scolaire, sampleEffectif.annee_scolaire);
assert.equal(duplicates[0]._id.formation_cfd, sampleEffectif.formation?.cfd);
});

it("Permet de vérifier la récupération de doublons d'effectifs avec un nom multi-casse", async () => {
// Ajout de 2 doublons d'effectifs
const sampleEffectif = createSampleEffectif({
organisme: sampleOrganisme,
apprenant: { nom: "mBaPpe" },
annee_scolaire: "2023-2024",
});
await insertDuplicateEffectifs(sampleEffectif);

const duplicates = await getDuplicatesEffectifsForOrganismeId(sampleOrganismeId);

// Vérification de la récupération d'une liste avec un doublon identifié 2 fois sur les champs de la clé d'unicité
assert.equal(duplicates.length, 1);
assert.equal(duplicates[0].count, 2);
assert.equal(duplicates[0].duplicates.length, 2);

assert.equal(sanitizeString(duplicates[0]._id.nom_apprenant), sanitizeString(sampleEffectif.apprenant.nom));
assert.equal(sanitizeString(duplicates[0]._id.prenom_apprenant), sanitizeString(sampleEffectif.apprenant.prenom));
assert.deepEqual(duplicates[0]._id.date_de_naissance_apprenant, sampleEffectif.apprenant.date_de_naissance);
assert.equal(duplicates[0]._id.annee_scolaire, sampleEffectif.annee_scolaire);
assert.equal(duplicates[0]._id.formation_cfd, sampleEffectif.formation?.cfd);
});

it("Permet de vérifier la récupération de doublons d'effectifs avec un prénom avec caractères spéciaux, accents et espace", async () => {
// Ajout de 2 doublons d'effectifs
const sampleEffectif = createSampleEffectif({
organisme: sampleOrganisme,
apprenant: { prenom: "JeAn- éDouArd" },
annee_scolaire: "2023-2024",
});
await insertDuplicateEffectifs(sampleEffectif, 5);

const duplicates = await getDuplicatesEffectifsForOrganismeId(sampleOrganismeId);

// Vérification de la récupération d'une liste avec un doublon identifié 5 fois sur les champs de la clé d'unicité
assert.equal(duplicates.length, 1);
assert.equal(duplicates[0].count, 5);
assert.equal(duplicates[0].duplicates.length, 5);

assert.equal(sanitizeString(duplicates[0]._id.nom_apprenant), sanitizeString(sampleEffectif.apprenant.nom));
assert.equal(sanitizeString(duplicates[0]._id.prenom_apprenant), "jeanédouard"); // Transformation du prenom_apprenant en champ normalisé
assert.deepEqual(duplicates[0]._id.date_de_naissance_apprenant, sampleEffectif.apprenant.date_de_naissance);
assert.equal(duplicates[0]._id.annee_scolaire, sampleEffectif.annee_scolaire);
assert.equal(duplicates[0]._id.formation_cfd, sampleEffectif.formation?.cfd);
});

it("Permet de vérifier la récupération de doublons d'effectifs avec un nom avec caractères spéciaux, accents et espace", async () => {
// Ajout de 2 doublons d'effectifs
const sampleEffectif = createSampleEffectif({
organisme: sampleOrganisme,
apprenant: { nom: "M' BaPpé" },
annee_scolaire: "2023-2024",
});
await insertDuplicateEffectifs(sampleEffectif, 5);

const duplicates = await getDuplicatesEffectifsForOrganismeId(sampleOrganismeId);

// Vérification de la récupération d'une liste avec un doublon identifié 5 fois sur les champs de la clé d'unicité
assert.equal(duplicates.length, 1);
assert.equal(duplicates[0].count, 5);
assert.equal(duplicates[0].duplicates.length, 5);

assert.equal(sanitizeString(duplicates[0]._id.nom_apprenant), "mbappé"); // Transformation du nom en champ normalisé
assert.equal(sanitizeString(duplicates[0]._id.prenom_apprenant), sanitizeString(sampleEffectif.apprenant.prenom));
assert.deepEqual(duplicates[0]._id.date_de_naissance_apprenant, sampleEffectif.apprenant.date_de_naissance);
assert.equal(duplicates[0]._id.annee_scolaire, sampleEffectif.annee_scolaire);
assert.equal(duplicates[0]._id.formation_cfd, sampleEffectif.formation?.cfd);
});
});
});
2 changes: 1 addition & 1 deletion ui/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_BASE_HOST=$NEXT_PUBLIC_BASE_HOST
ENV NEXT_PUBLIC_METABASE_URL=$NEXT_PUBLIC_METABASE_URL
ENV NEXT_PUBLIC_METABASE_SECRET_KEY=$NEXT_PUBLIC_METABASE_SECRET_KEY

ENV WATCHPACK_POLLING true

EXPOSE 3000
CMD yarn docker-dev
47 changes: 47 additions & 0 deletions ui/common/constants/dossierApprenant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Codes des statuts des apprenants
*/
export const CODES_STATUT_APPRENANT = {
inscrit: 2,
apprenti: 3,
abandon: 0,
} as const;

export const CODES_STATUT_APPRENANT_ENUM = [
CODES_STATUT_APPRENANT.abandon,
CODES_STATUT_APPRENANT.inscrit,
CODES_STATUT_APPRENANT.apprenti,
];

/**
* Sexe des apprenants (M=Homme, F=Femme)
*/
export const SEXE_APPRENANT_ENUM = ["M", "F"];

export const NATIONALITE_APPRENANT_ENUM = [1, 2, 3];
/**
* Nom des statuts
*/
const LABELS_STATUT_APPRENANT = [
{ code: CODES_STATUT_APPRENANT.abandon, name: "abandon" },
{ code: CODES_STATUT_APPRENANT.inscrit, name: "inscrit" },
{ code: CODES_STATUT_APPRENANT.apprenti, name: "apprenti" },
];

/**
* Fonction de récupération d'un nom de statut depuis son code
* @param {*} statutCode
* @returns
*/
export const getStatutApprenantNameFromCode = (statutCode) =>
LABELS_STATUT_APPRENANT.find((item) => item.code === statutCode)?.name ?? "NC";

/**
* Liste des nom des indicateurs
*/
export const EFFECTIF_INDICATOR_NAMES = {
apprentis: "apprenti",
inscritsSansContrats: "inscrit sans contrat",
rupturants: "rupturant",
abandons: "abandon",
};
10 changes: 10 additions & 0 deletions ui/common/types/duplicatesEffectifs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type DuplicateEffectif = {
_id: {
nom_apprenant: string;
prenom_apprenant: string;
date_de_naissance_apprenant: string;
annee_scolaire: string;
formation_cfd: string;
};
duplicates: [{ id: string; created_at: Date; source: string }];
};
Loading

0 comments on commit a6ea915

Please sign in to comment.