Commit 9743a334 authored by Stephane Bouvry's avatar Stephane Bouvry
Browse files

Merge branch 'master' of git.unicaen.fr:open-source/oscar

parents bc89e8b9 c6192365
Loading
Loading
Loading
Loading
Loading
+12 −0
Changes for config/connectors/organization_db.yml.dist: 12 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -7,6 +7,18 @@ db_port:
db_user:
db_password:
db_name:
db_charset: AL32UTF8

db_query_single: >
  SELECT
    ID, CODE, PARENT, LIBELLE_COURT,
    TO_CHAR(
          CAST (DATE_MODIFICATION AS timestamp) AT TIME ZONE 'UTC',
          'yyyy-mm-dd"T"hh24:mi:ss.ff3"Z"'
      ) UPDATED_AT,
      TYPE_RECHERCHE, CODE_RECHERCHE, LIBELLE_LONG, TELEPHONE, SITE_URL, TYPE,
      RNSR, ADRESSE_POSTALE
  FROM organizations WHERE ID = :p1 ORDER BY UPDATED_AT DESC FETCH FIRST ROW ONLY

db_query_all: >
  SELECT
+1 −0
Changes for config/connectors/person_db.yml.dist: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ db_port:
db_user:
db_password:
db_name:
db_charset: AL32UTF8

db_query_single: >
  SELECT REMOTE_ID, PRENOM, NOM, CIVILITE, LOGIN, EMAIL, LANGAGE, STATUT,

doc/connectors-db.md

0 → 100644
+131 −0
Changes for doc/connectors-db.md: 131 added lines, 0 removed lines.
Original line number Diff line number Diff line
# Connectors OSCAR DB

*Oscar* permet d'importer et de synchroniser des données concernant les organisations et les personnes à partir de bases de données Oracle.

## Configuration du connecteur

La première étape pour faire fonctionner le connecteur base de données consiste à définir sa configuration.

 - Copier-coller le fichier [config/connectors/organization_db.yml.dist](../config/connectors/organization_db.yml.dist) en le renommant `config/connectors/organization_db.yml`

Ce document prend pour exemple le connecteur des organisations mais le principe est le même pour configurer le connecteur des personnes. Il suffit de remplacer `organization` par `person` à chacune des étapes.

Il faut ensuite définir les différents paramètres permettant de se connecter à la base de données et de récupérer les informations concernant les :

|     Variable      |                 Exemple                        | Description |
| ----------------- | ---------------------------------------------- | ----------- |
| url_organizations |                                                | Non utilisée pour le connecteur BDD |
| url_organization  |                                                | Non utilisée pour le connecteur BDD |
| access_strategy   | Oscar\Connector\Access\ConnectorAccessOracleDB | Indique la classe PHP chargée de se connecter à la BDD et d'exécuter les requêtes. Laisser la valeur par défaut. |
| db_host           | bdd.unicaen.fr                                 | Adresse du serveur de BDD Oracle |
| db_port           | 1521                                           | Port TCP sur lequel le serveur BDD écoute les connexions |
| db_user           | my_user                                        | Nom d'utilisateur pour la connexion à la BDD. Un utilisateur ayant un accès en lecteur seule uniquement à la table des organisations devrait la plupart du temps suffire. |
| db_password       | my_password                                    | Mot de passe pour la connexion à la BDD |
| db_name           | my_dbname                                      | Nom de la base de données |
| db_charset        | AL32UTF8                                       | Codage de caractères de la BDD |
| db_query_single   | SELECT * FROM organization WHERE ID = :p1      | Requête permettant de récupérer une organisation par son identifiant |
| db_query_all      | SELECT * FROM organization                     | Requête permettant de récupérer toutes les organisation |

Pour activer ce nouveau connecteur, il faut ajouter sa déclaration dans le fichier [config/autoload/local.php](../config/autoload/local.php)

```php
<?php
// /config/autoload/local.php
return array(
    'oscar' => [
        'connectors' => [
            // -------------------------------- Synchronisation des structures
            'organization' => [
                'db' => [
                  'class'     => \Oscar\Connector\ConnectorOrganizationDB::class,
                  'params'    => realpath(__DIR__) . '/../connectors/organization_db.yml'
                ]
            ],
            
            // -------------------------------- Synchronisation des personnes
            'person' => [
                'db' => [
                  'class'     => \Oscar\Connector\ConnectorPersonDB::class,
                  'params'    => realpath(__DIR__) . '/../connectors/person_db.yml'
                ]
            ]
        ],
    ]
);
```

## Vérification de la configuration

On peut alors lancer une vérification via la commande `php bin/oscar.php check:config`

Si tout est OK, la commande indique combien d'organisations et de personnes sont trouvées dans la base de données et seront synchronisées lors du lancement de commande `sync`.


## Données attendues pour les organisations

Les requêtes `db_query_single` et `db_query_all` du fichier `organization_db.yml` permettant de récupérer les informations des organisations doivent retourner les colonnes suivantes :

|  Nom colonne    |               Exemple                       | Type                 | Obligatoire |                 Description                                  |
| --------------- | ------------------------------------------- | -------------------- | ----------- | ------------------------------------------------------------ |
| ID              | 1                                           | Nombre entier        | Oui         | Identifiant unique de l'organisation dans la BDD             |
| CODE            | S23                                         | Chaîne de caractères | Oui         | Code unique de l'organisation dans la BDD                    |
| PARENT          | UNIV                                        | Chaîne de caractères |             | Code de l'organisation parente. Peut être `NULL`             |
| LIBELLE_COURT   | MRSH                                        | Chaîne de caractères | Oui         | Description courte de l'organisation                         |
| LIBELLE_LONG    | Maison de la Recherche en Sciences Humaines | Chaîne de caractères | Oui         | Description longue de l'organisation                         |
| UPDATED_AT      | 2022-09-28T12:29:23.000Z                    | Chaîne de caractères |             | Date de dernière mise à jour de la donnée au format ISO 8601 |
| TYPE_RECHERCHE  | USR                                         | Chaîne de caractères |             | Sigle / acronyme du type de structure (par ex. UMR)          |
| CODE_RECHERCHE  | 3486                                        | Chaîne de caractères |             | Identifiant associé au type de structure. TYPE_RECHERCHE + CODE_RECHERCHE formeront le `labintel` (USR3486) |
| TELEPHONE       | +33 2 31 56 62 00                           | Chaîne de caractères |             | Numéro de téléphone                                          |
| SITE_URL        | https://www.unicaen.fr/recherche/mrsh/      | Chaîne de caractères |             | Adresse du site web                                          |
| TYPE            | Service commun                              | Chaîne de caractères |             | Type de la structure                                         |
| RNSR            | 201221521V                                  | Chaîne de caractères |             | Identifiant répertoire national des structures de recherche (RNSR) |
| ADRESSE_POSTALE | {"address1":"Caen Campus 1 - MRSH - F - 1°Etage - SH  154","address2":"Esplanade de la paix","address3":"CS 14032","zipcode":"14032","city":"Caen Cedex 5","country":"France"} | Chaîne de caractères |             | Adresse postale au format JSON. Voir l'exemple pour les champs à indiquer. |

La requête `db_query_single` ne doit retourner qu'une seule ligne.

La requête `db_query_all` ne doit pas avoir de doublon (la valeur de la colonne ID doit être unique).

## Données attendues pour les personnes

Les requêtes `db_query_single` et `db_query_all` du fichier `person_db.yml` permettant de récupérer les informations des personnes doivent retourner les colonnes suivantes :

|       Nom colonne       |               Exemple                       | Type                 | Obligatoire |                 Description                                  |
| ----------------------- | ------------------------------------------- | -------------------- | ----------- | ------------------------------------------------------------ |
| REMOTE_ID               | 1                                           | Nombre entier        | Oui         | Identifiant unique de la personne dans la BDD                |
| LOGIN                   | mdupont                                     | Chaîne de caractères | Oui         | Login unique                                                 |
| PRENOM                  | Martin                                      | Chaîne de caractères | Oui         | Prénom                                                       |
| NOM                     | DUPONT                                      | Chaîne de caractères | Oui         | Nom de famille                                               |
| EMAIL                   | mdupont@unicaen.fr                          | Chaîne de caractères | Oui         | Adresse email                                                |
| CIVILITE                | M.                                          | Chaîne de caractères |             | Titre de civilité                                            |
| LANGAGE                 | fr                                          | Chaîne de caractères |             | Code ISO 639-1 de la langue privilégiée                      |
| STATUT                  | TITULAIRE                                   | Chaîne de caractères |             | Statut (CDI...)                                              |
| AFFECTATION             | Maison de la Recherche en Sciences Humaines | Chaîne de caractères |             | Composante à laquelle la personne est affectée               |
| INM                     | 334                                         | Nombre entier        |             | Indice majoré pour traitement indiciaire                     |
| TELEPHONE               | +33 2 01 02 03 04                           | Chaîne de caractères |             | Numéro de téléphone                                          |
| DATE_EXPIRATION         | 2025-09-30 23:59:59.000                     | DATE Oracle          |             | Date d'expiration du compte utilisateur                      |
| ROLES                   | {"S23":["Gestionnaire de laboratoire"]}     | Chaîne de caractères |             | Rôles au format JSON. Objet JSON dont les clés sont le code des structures et les valeurs sont un tableau des rôles de la personne au sein de cette structure. |
| ADRESSE_PROFESSIONNELLE | {"address1":"Caen Campus 1 - MRSH - F - 1°Etage - SH  149","address2":"Esplanade de la paix","address3":"CS 14032","zipcode":"14032","city":"Caen Cedex 5","country":"France"} | Chaîne de caractères |             | Adresse postale au format JSON. Voir l'exemple pour les champs à indiquer. |

La requête `db_query_single` ne doit retourner qu'une seule ligne.

La requête `db_query_all` ne doit pas avoir de doublon (la valeur de la colonne ID doit être unique).


## Notes pour les développeurs

### Que faire si la requête ne retourne pas exactement les champs attendus, au format attendu ?

Le [ConnectorAccessOracleDB](../module/Oscar/src/Oscar/Connector/Access/ConnectorAccessOracleDB.php) se contente d'exécuter la requête et de retourner un tableau associatif clé => valeur contenant les résultats. Un seul tableau est retourné dans le cas de la requête `db_query_single`, tandis qu'une liste de tableaux (un pour chaque ligne en base de données) est retournée pour la requête `db_query_all`.

C'est dans la méthode `objectFromDBRow` de chaque connecteur ([ConnectorOrganizationDB](../module/Oscar/src/Oscar/Connector/ConnectorOrganizationDB.php) et [ConnectorPersonDB](../module/Oscar/src/Oscar/Connector/ConnectorPersonDB.php)) que va se faire la correspondance entre les valeurs retournées par la requête SQL et l'objet métier à remplir.

S'il est nécessaire d'effectuer un traitement spécifique, il est donc possible de surcharger/étendre ou remplacer la classe `Connector*DB` voulue pour redéfinir une métode `objectFromDBRow` adaptée. Il faut ensuite bien penser à modifier la configuration dans le fichier [config/autoload/local.php](../config/autoload/local.php) pour indiquer la nouvelle classe `Connector*DB` à utiliser.

### Comment utiliser un autre type de base de données que Oracle ?

Le [ConnectorAccessOracleDB](../module/Oscar/src/Oscar/Connector/Access/ConnectorAccessOracleDB.php) ne permet aujourd'hui d'interroger que des bases de données Oracle. Si on souhaite interroger d'autre types de SGBD, il est possible de copier ce fichier et de l'adapter à un autre type de driver de bases de données, par exemple `ConnectorAccessPostgresDB`. Il faudra alors penser à l'indiquer dans les fichiers de configuration des connecteurs (par exemple `config/connectors/organization_db.yml`) :

```yml
# Accès spécifique
access_strategy:  Oscar\Connector\Access\ConnectorAccessPostgresDB
```
+1 −1
Changes for doc/connectors.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@ Il propose des utilitaires en ligne de commande pour **importer des données** d

Les connectors permettent de *brancher* Oscar sur des sources de données et d'automatiser la maintenance de ces données.

Les connectors dans version 2.0 d'Oscar s'appuient sur un service REST distant qui va livrer les données à Oscar sous un format standardisé.
Les connectors dans la version 2.0 d'Oscar s'appuient sur un service REST distant qui va livrer les données à Oscar sous un format standardisé. Pour importer des données directement depuis une base de données Oracle sans passer par un service REST, voir [connectors-db.md](connectors-db.md).

Oscar possède la possibilité de se connecter via une connexion ssl en relation avec un certificat .p12 avec mdp, il faudra cependant le préparer en le scindant en deux fichiers (certificat et clef)

+127 −0
Changes for module/Oscar/src/Oscar/Command/OscarOrganizationSyncOneCommand.php: 127 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Oscar\Command;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;

use Oscar\Entity\Organization;
use Oscar\Entity\OrganizationRepository;
use Oscar\Service\ConnectorService;
use Oscar\Service\OrganizationService;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class OscarOrganizationSyncOneCommand extends OscarCommandAbstract
{
    protected static $defaultName = 'organization:syncone';

    protected function configure()
    {
        $this
            ->setDescription("Execute la synchronisation d'une organisation")
            ->addArgument("connectorname", InputArgument::REQUIRED, "Connector (rest)")
            ->addArgument('value', InputArgument::REQUIRED, 'Identifiant de l\'organisation dans la source de données distante (remote id)')
            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forcer la mise à jour')
            ->addOption(
                'no-rebuild',
                'b',
                InputOption::VALUE_NONE,
                'Ignore la reconstruction de l\'index de recherche après la mise à jour'
            )->addOption(
                'purge',
                'p',
                InputOption::VALUE_NONE,
                'Déclenche la suppression des organisations qui sont retirées de la source'
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {

        try {

            $this->addOutputStyle($output);

            $io = new SymfonyStyle($input, $output);
            $io->title("Synchronisation d'une organisation");

            $connectorName        = $input->getArgument("connectorname");
            $organizationRemoteID = $input->getArgument("value");
            $force                = $input->getOption('force');
            $noRebuild            = $input->getOption('no-rebuild');

            $io->section("Connector infos : ");
            $io->writeln("Connecteur : <bold>$connectorName</bold>, remote id : $organizationRemoteID");

            /** @var ConnectorService $connectorService */
            $connectorService = $this->getServicemanager()->get(ConnectorService::class);

            $connector = $connectorService->getConnector("organization." . $connectorName);
            $connector->setOptionPurge($input->getOption('purge'));

            /** @var OrganizationRepository $organizationRepository */
            $organizationRepository = $this->getServicemanager()->get(EntityManager::class)->getRepository(Organization::class);
            
            $organization = NULL;
            $organizationDateUpdated = NULL;
            try {
                $organization = $organizationRepository->getObjectByConnectorID($connectorName, $organizationRemoteID);
                $organizationDateUpdated = $organization->getDateUpdated();
            } catch (NonUniqueResultException $e) {
                $io->error("Plusieurs organisations dans la base de données organization d'Oscar ont ce remote id (" . $organizationRemoteID . ") pour ce connecteur (" . $connectorName . ")");

                return self::FAILURE;

            } catch (NoResultException $e) {
                $io->writeln("Cette organisation n'existe pas encore dans Oscar. Si elle est trouvée dans la source de données distante via le connecteur alors elle sera ajoutée.");
            }

            $organizationRemote = $connector->syncOrganization($organization, $organizationRemoteID, $force);

            if ($organization == NULL && $organizationRemote == NULL) {
                $io->warning(sprintf("L'organisation d'id '%s' n'est présente ni dans Oscar, ni dans les données du connecteur distant et n'a donc pas été synchronisée.", $organizationRemoteID));
            } else if ($organization != NULL && $organizationRemote == NULL && !$input->getOption('purge')) {
                $io->warning(sprintf("L'organisation d'id '%s' n'est pas présente dans les données du connecteur distant et n'a donc pas été synchronisée. Pour supprimer l'organisation dans la base de données locale d'Oscar, relancez la commande avec l'option --purge", $organizationRemoteID));
            } else if ($organization != NULL && $organizationRemote == NULL && $input->getOption('purge')) {
                $io->warning(sprintf("L'organisation d'id '%s' n'est pas présente dans les données du connecteur distant et a donc été supprimée de la base de données locale d'Oscar.", $organizationRemoteID));
            } else if ($organization == NULL && $organizationRemote != NULL) {
                $io->success(sprintf("L'organisation '%s' a été ajoutée.", $organizationRemote));
            } else if ($organizationDateUpdated >= $organizationRemote->getDateUpdated() && !$force) {
                $io->info(sprintf("L'organisation '%s' est déjà à jour : date de dernière modification (dateupdated) plus récent ou égale à celle de la donnée distante du connecteur. Pour forcer tout de même la synchronisation, relancez la commande avec l'option --force", $organization));
            } else {
                $io->success(sprintf("L'organisation '%s' a été synchronisée.", $organization));
            }

        } catch ( \Exception $e ){
            $io->error($e->getMessage());
            return self::FAILURE;
        }
        
        $io->section("Reconstruction de l'index de recherche : ");
        if (!$noRebuild) {
            /** @var OrganizationService $organizationService */
            $organizationService = $this->getServicemanager()->get(OrganizationService::class);

            try {
                $organizations = $organizationService->getOrganizations();
                $organizationService->getSearchEngineStrategy()->rebuildIndex($organizations);
                $io->success(
                    sprintf('Index de recherche mis à jour avec %s organisations indexées', count($organizations))
                );
            } catch (\Exception $e) {
                $io->error($e->getMessage());
                return self::FAILURE;
            }
        } else {
            $io->warning("Pas de reconstruction d'index");
        }

        return self::SUCCESS;
    }
}
Loading