Commit 6ae02cd9 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Module Substitutions : possibilité de lancer la création d'une substitution...

Module Substitutions : possibilité de lancer la création d'une substitution depuis la page des doublons.
parent 101754b0
Loading
Loading
Loading
Loading
+1 −0
Changes for CHANGELOG.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ Journal des modifications
- Stockage de fichier S3/Filesystem : plus de copie sur disque dans le cas d'une demande d'un fichier issu du Filesystem.
- Inclusion au script de purge des fichiers temporaires ceux générés par le FichierStorageService.
- Module Substitutions : amélioration de fonctions pgsql du moteur de substitutions.
- Module Substitutions : possibilité de lancer la création d'une substitution depuis la page des doublons.
- Structures : amélioration visuelle sur la page listant les structures (Etablissement, ED, UR).
- Listing/recherche d'individus : lien vers la substitution si individu substituant ; nouveau filtre 'Historisés inclus' décoché par défaut ; cosmétique.  
- [FIX] Module Substitutions : plantage de la page consacrée aux doublons (substitutions possibles). 
+285 −0
Changes for doc/release-notes/v7.1.0.md: 285 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -122,4 +122,289 @@ end
$$;



--
-- Fonction substit_update_substitution_if_exists() : 
--   - plus de transmission de l'argument NPD, la signature est plus explicite comme ça.
--
drop function substit_update_substitution_if_exists;
create function substit_update_substitution_if_exists(type character varying, p_substitue record) returns boolean
  language plpgsql
as $$declare
  v_npd varchar(256);
  v_count int;
  v_data record;
  v_substit_record record;
begin
  --
  -- Recherche et mise à jour de la substitution existante spécifiée par l'enregistrement substitué.
  --
  -- Retourne `true` s'il n'y a plus rien à faire (càd que le cas de l'enregistrement est traité) ;
  -- ou `false` dans le cas où il faudra rechercher si l'enregistrement est en doublon et doit faire l'objet
  -- d'une nouvelle substitution.
  --

  raise notice 'Recherche si l''enregistrement % est substitué...', p_substitue.id;

  --
  -- calcul du npd de l'enregistrement (sauf si le npd est forcé).
  --
  execute format('select coalesce($1, substit_npd_%s($2))', type) using p_substitue.npd_force, p_substitue into v_npd;

  execute format('select * from substit_%s where from_id = %s', type, p_substitue.id) into v_substit_record;
  if v_substit_record.to_id is null then
    raise notice '=> Aucune substitution trouvée.';
    return false;
  end if;

  if p_substitue.source_id = app_source_id() then
    raise notice '=> Oui mais l''enregistrement est dans la source application.';
    raise notice '=> Retrait de l''enregistrement et mise à jour du substituant...';
    perform substit_remove_from_substitution(type, v_substit_record.from_id, v_substit_record.to_id);
    return true;
  elseif v_substit_record.npd <> v_npd then
    raise notice '=> Oui mais le NPD de l''enregistrement (%) a changé par rapport à celui de la substitution (%).', v_npd, v_substit_record.npd;
    raise notice '=> Retrait de l''enregistrement et mise à jour du substituant...';
    perform substit_remove_from_substitution(type, v_substit_record.from_id, v_substit_record.to_id);
    return false;
  elseif v_substit_record.npd = v_npd then
    raise notice '=> Oui et le NPD de l''enregistrement égale celui de la substitution (%).', v_npd;
    raise notice '=> Mise à jour de l''enregistrement substituant...';
    execute format('select count(*) from substit_fetch_data_for_substituant_%s(%L) limit 1', type, v_npd) into v_count;
    if v_count = 0 then
      raise exception 'Impossible de mettre à jour le substituant car aucun doublon de type % trouvé avec le NPD %', type, v_npd;
    end if;
    execute format('select * from substit_fetch_data_for_substituant_%s(%L) limit 1', type, v_npd) into v_data;
    perform substit_update_substituant(type, v_substit_record.to_id, v_data);
    return true;
  end if;

  return false;
end
$$;


--
-- Modif de la fonction substit_create_substitution_if_required() :
--   - on renvoie désormais l'id du substituant éventuellement créé.
--
drop function substit_create_substitution_if_required(varchar, varchar);
create function substit_create_substitution_if_required(type character varying, p_npd character varying) returns bigint
  language plpgsql
as
$$declare
  data record;
  cursor_doublons refcursor;
  doublon_record record;
  substituant_record_id bigint;
begin
  --
  -- Crée si nécessaire une substitution pour un NPD donné.
  --

  raise notice 'Création si necessaire d''une substitution avec le NPD %...', p_npd;

  open cursor_doublons for
    execute format('select i.* from %s i join v_%s_doublon v on v.id = i.id where v.npd = %L', type, type, p_npd);
  fetch next from cursor_doublons into doublon_record;
  if found then
    execute format('select * from substit_fetch_data_for_substituant_%s(%L) limit 1', type, p_npd) into data;
    execute format('select substit_create_substituant_%s($1)', type) using data into substituant_record_id;
    perform substit_insert_log(type, 'SUBSTITUANT_CREATE', null, substituant_record_id, p_npd,
                               format('Nouvel enregistrement substituant : %s', substituant_record_id));

    while found loop
            raise notice '- Doublon %', doublon_record;
            perform substit_add_to_substitution(type, doublon_record.id, p_npd, substituant_record_id);
            fetch next from cursor_doublons into doublon_record;
      end loop;
  else
    raise notice '=> Aucun doublon trouvé avec le NPD "%"', p_npd;
  end if;
  close cursor_doublons;

  return substituant_record_id;
end
$$;


--
-- Nouvelle fonction substit_update_or_create_substitution_with_npd() factorisant des opérations réalisées à la fois en cas 
-- d'INSERT et d'UPDATE d'enregistrements, et utilisée pour lancer la création d'une substitution à partir d'un doublon depuis l'UI.
--
create or replace function substit_update_or_create_substitution_with_npd(type character varying,
                                                                          p_npd character varying,
                                                                          p_substitue_id bigint) returns bigint
  language plpgsql
as $$declare
  data record;
  substit_record record;
  v_substituant_id bigint;
begin
  --
  -- recherche d'une substitution existante avec ce npd.
  --
  raise notice 'Recherche d''une substitution existante avec le NPD % ...', p_npd;
  execute format('select * from substit_%s where npd = %L limit 1', type, p_npd) into substit_record;

  -- si une subsitution existe, ajout de l'enregistrement à celle-ci.
  if substit_record.to_id is not null then
    raise notice '=> Substitution trouvée : %', substit_record;
    perform substit_add_to_substitution(type, p_substitue_id, p_npd, substit_record.to_id);
    -- mise à jour de l'enregistrement substituant
    execute format('select * from substit_fetch_data_for_substituant_%s(%L) limit 1', type, p_npd) into data;
    perform substit_update_substituant(type, substit_record.to_id, data);

    return substit_record.to_id;
  end if;

  raise notice '=> Aucune substitution trouvée.';

  ----> à ce stade, aucune substitution n'existe avec ce npd. <----

  --
  -- création de la substitution si le nouvel enregistrement est un doublon d'un enregistrement existant.
  --
  select substit_create_substitution_if_required(type, p_npd) into v_substituant_id;

  return v_substituant_id;
end
$$;


--
-- Fonction trigger principale du moteur :
--   - utilisation de la nouvelle fonction substit_update_or_create_substitution_with_npd().
--   - la fonction substit_update_substitution_if_exists() ne prend plus de NPD.
--
create or replace function substit_trigger_fct() returns trigger
  language plpgsql
as $$declare
  type varchar = tg_argv[0]; -- 'individu', 'doctorant', 'structure', etc.
  APP_SOURCE_ID bigint = app_source_id(); -- source correspondant à l'application
  operation varchar(32);
  operation_desc text;
  v_npd varchar(256);
  substit_record record;
begin
  --
  -- Fonction du trigger permettant de réagir aux update et insert d'un enregistrement afin de
  -- tenir à jour la liste des substituions des enregistrements en doublons par un enregistrement supplémentaire (le substituant).
  --

  -- Rappel : la source correspondant à l'application est celle dans laquelle sont créés les enregistrements substituants.
  -- Et les enregistrements doublons (i.e. substitués) ne peuvent pas être dans cette source.

  -- opération en cours
  if TG_OP = 'INSERT' and new.source_id <> APP_SOURCE_ID then
    operation = 'INSERT';
    operation_desc = format('%s - Ajout d''un nouvel enregistrement : %s', upper(type), new);
  elseif TG_OP = 'UPDATE' and new.source_id <> APP_SOURCE_ID and old.histo_destruction is not null and new.histo_destruction is null then
    operation = 'INSERT';
    operation_desc = format('%s - Restauration d''un enregistrement : %s', upper(type), new);
  elseif TG_OP = 'UPDATE' and old.source_id = APP_SOURCE_ID and new.source_id <> APP_SOURCE_ID then
    operation = 'INSERT';
    operation_desc = format('%s - Arrivée d''un enregistrement dans la source %s : %s', upper(type), new.source_id, new);
    ---
  elseif TG_OP = 'DELETE' and old.source_id <> APP_SOURCE_ID then
    operation = 'DELETE';
    operation_desc = format('%s - Suppression d''un enregistrement : %s', upper(type), old);
    ---
  elseif TG_OP = 'UPDATE' and old.source_id <> APP_SOURCE_ID and old.histo_destruction is null and new.histo_destruction is not null then
    operation = 'HISTORISATION';
    operation_desc = format('%s - Historisation d''un enregistrement : %s', upper(type), new);
    ---
  elseif TG_OP = 'UPDATE' and old.source_id <> APP_SOURCE_ID and new.source_id = APP_SOURCE_ID then
    operation = 'UPDATE';
    operation_desc = format('%s - Arrivée d''un enregistrement dans la source application %s : %s', upper(type), new.source_id, new);
  elseif TG_OP = 'UPDATE' and old.source_id <> APP_SOURCE_ID then
    operation = 'UPDATE';
    operation_desc = format('%s - Modification d''un enregistrement : %s', upper(type), new);

  else return coalesce(new, old);
  end if;

  ----------------------------------------- historisation ------------------------------------
  if operation = 'HISTORISATION' then
    raise notice '[HISTORISATION] %', operation_desc;
    raise notice 'Enregistrement : %', new;

    --
    -- recherche et mise à jour éventuelle d'une substitution existante de cet enregistrement.
    --
    perform substit_update_substitution_if_exists(type, new);

    return new;

    ----------------------------------------- suppression ------------------------------------
  elseif operation = 'DELETE' then
    raise notice '[DELETE] %', operation_desc;
    raise notice 'Enregistrement : %', old;

    --
    -- recherche et mise à jour éventuelle d'une substitution existante de cet enregistrement.
    --
    perform substit_update_substitution_if_exists(type, old);

    return old;

    ----------------------------------------- ajout ou restauration ------------------------------------
  elseif operation = 'INSERT' then
    raise notice '[INSERT] %', operation_desc;
    raise notice 'Enregistrement : %', new;

    --
    -- Gestion particulière du cas où l'ajout est fait avec un INSERT...SELECT : le trigger se déclenche bien N fois
    -- pour chacune des N lignes insérées mais à chaque exécution c'est comme si la table contenait les N
    -- enregistrements en cours d'insertion ! Ce qui veut dire qu'un enregistrement en cours d'insertion peut avoir
    -- déjà été substitué lors d'une précédente exécution du trigger.
    -- Si c'est le cas, une erreur d'unicité dans SUBSTIT_XXXX est levée puisque l'on tente d'ajouter un substitué
    -- à une substitution où il est déjà présent.
    -- On doit donc tester systématiquement que l'enregistrement inséré n'est pas déjà substitué !
    --
    execute format('select to_id from substit_%s where from_id = %s', type, new.id) into substit_record;
    if substit_record.to_id is not null then
      return new;
    end if;

    --
    -- calcul du npd de l'enregistrement (sauf si le npd est forcé).
    --
    execute format('select coalesce($1, substit_npd_%s($2))', type) using new.npd_force, new into v_npd;

    --
    -- mise à jour ou création si nécessaire d'une substitution pour ce NPD.
    --
    perform substit_update_or_create_substitution_with_npd(type, v_npd, new.id);

    return new;

    ----------------------------------------- modification ------------------------------------
  elseif operation = 'UPDATE' then
    raise notice '[UPDATE] %', operation_desc;
    raise notice 'Enregistrement avant : %', old;
    raise notice 'Enregistrement après : %', new;

    --
    -- mise à jour de la substitution éventuelle de cet enregistrement.
    --
    if substit_update_substitution_if_exists(type, new) = true then
      return new;
    end if;

    --
    -- calcul du npd de l'enregistrement (sauf si le npd est forcé).
    --
    execute format('select coalesce($1, substit_npd_%s($2))', type) using new.npd_force, new into v_npd;

    --
    -- mise à jour ou création si nécessaire d'une substitution pour ce NPD.
    --
    perform substit_update_or_create_substitution_with_npd(type, v_npd, new.id);

    return new;
  end if;
end
$$;

```
+15 −0
Changes for module/Substitution/config/module.config.php: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -74,6 +74,7 @@ return [
                        'voir',
                        'voirSubstitue',
                        'voirSubstituant',
                        'creer'
                    ],
                    'privileges' => [
                        SubstitutionPrivileges::SUBSTITUTION_CONSULTER,
@@ -255,6 +256,20 @@ return [
                                    ],
                                ],
                            ],
                            'creer' => [
                                'type' => Segment::class,
                                'options' => [
                                    'route' => '/:type/creer/substituable/:substituableId/npd/:npd',
                                    'constraints' => [
                                        'type' => Constants::TYPES_REGEXP_CONSTRAINT,
                                        'substituableId' => '\d+',
                                    ],
                                    'defaults' => [
                                        /** @see SubstitutionController::creerAction() */
                                        'action' => 'creer',
                                    ],
                                ],
                            ],
                        ],
                    ],
                    'doublon' => [
+52 −12
Changes for module/Substitution/doc/principes.md: 52 added lines, 12 removed lines.
Original line number Diff line number Diff line
Principes du moteur de substitutions
====================================

## Trigger sur la table xxxx (on insert, update, delete)

### Si modification

- calcul du NPD de l'enregistrement (sauf si le NPD est forcé)
- recherche et mise à jour éventuelle d'une substitution existante de cet enregistrement avec ce NPD
  - si retour true, ok, STOP.
- recherche d'une substitution existante pour le NPD de l'enregistrement.
  - si une subsitution existe, ajout de l'enregistrement à celle-ci
  - mise à jour de l'enregistrement substituant
- (à ce stade, aucune substitution n'existe avec ce NPD)
- création de la substitution si l'enregistrement en cours de modif est un doublon d'un enregistrement existant.
## Tables mises en jeu

- Tables "x" :
  - structure
  - etablissement
  - ecole_doctorale
  - unite_recherche
  - individu
  - doctorant

- Tables "substit_x"

## Triggers

- Triggers sur les tables "x" : "substit_trigger_x" (after insert or delete or update)

- Triggers sur les tables "substit_x" : "substit_trigger_on_substit_x" (after insert or delete)


## Événements scrutés sur la table "x"

### Après historisation

- Calcul du NPD de l'enregistrement (sauf si le NPD est forcé)
- Recherche et mise à jour éventuelle d'une substitution existante de cet enregistrement.

### Après suppression

- Calcul du NPD de l'enregistrement (sauf si le NPD est forcé)
- Recherche et mise à jour éventuelle d'une substitution existante de cet enregistrement.

### Après insertion

- Calcul du npd de l'enregistrement (sauf si le npd est forcé).
- Recherche d'une substitution existante pour le npd de l'enregistrement.
  - Si une subsitution existe, ajout de l'enregistrement à celle-ci.
  - Mise à jour de l'enregistrement substituant <<<<<<<<<<<<<<<<<<<<
  - Stop
- (À ce stade, aucune substitution n'existe avec ce NPD)
- Création de la substitution si le nouvel enregistrement est un doublon d'un enregistrement existant.

### Après mise à jour

- Calcul du NPD de l'enregistrement (sauf si le NPD est forcé)
- Recherche et mise à jour éventuelle d'une substitution existante de cet enregistrement avec ce NPD
  - Si retour true, ok, STOP.
- Recherche d'une substitution existante pour le NPD de l'enregistrement.
  - Si une subsitution existe, ajout de l'enregistrement à celle-ci
  - Mise à jour de l'enregistrement substituant <<<<<<<<<<<<<<<
  - Stop
- (À ce stade, aucune substitution n'existe avec ce NPD)
- Création de la substitution si l'enregistrement en cours de modif est un doublon d'un enregistrement existant.
- 
 No newline at end of file
+18 −0
Changes for module/Substitution/src/Substitution/Controller/SubstitutionController.php: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -61,6 +61,24 @@ class SubstitutionController extends AbstractActionController
        return $vm;
    }

    /**
     * @throws \Doctrine\DBAL\Exception
     */
    public function creerAction(): Response
    {
        $type = $this->getRequestedType();
        $substituableId = $this->params()->fromRoute('substituableId');
        $npd = $this->params()->fromRoute('npd');

        $result = $this->substitutionService->createSubstitutionForTypeAndSubstituable($type, $substituableId, $npd);
        $substitution = $result->fetchAssociative();
        $substituantId = $substitution['to_id'];

        $this->flashMessenger()->addSuccessMessage("Le substitué $substituableId a été ajouté avec succès à la substitution par $substituantId.");

        return $this->redirect()->toRoute('substitution/substitution/voir', ['type' => $type, 'id' => $substituantId], [], true);
    }

    /**
     * @throws \Doctrine\DBAL\Exception
     */
Loading