In this tutorial, we will cover data synchronization with iTop and see how to adapt it to a specific need thanks to data collectors.

We will use the open APIs available on https://api.gouv.fr/ in order to synchronise national education institutions with our iTop server.
You can later collect more data using other APIs of the website.

This site references public service APIs, made available to local authorities, government departments and companies to build IT services for everyone.

api.gouv.fr

⏱️Approximate duration of the tutorial: 4 hours

Key steps in this tutorial

1/ Data structuring
2/ Creation of an iTop extension
3/ Creation of an iTop data collector
4/ Industrialisation of the solution
5/ Exercise
6/ Quizz

Prerequisites

An iTop community server version 2.7 or higher installed locally or on a remote machine on your network with user rights to make changes on setup.
A PHP runtime environment compatible with the iTop version and some knowledge of PHP.
A text editor like Notepad++ or similar.

Principle of synchronisation with iTop

Scheme of operation

The role of the collector is to convert external data to CSV format, to update the replicas with this data and finally to synchronise them with iTop data.
An abstraction layer handles the import and synchronisation phase for us.


For our specific REST collector, we simply need to extract data from the API and convert it to CSV.

Tutorial steps

1/ Data structuring

We would like to synchronise the list of French national education institutions with iTop. The data will be read-only on the iTop side.
We will use the Education Directory API: https://api.gouv.fr/les-api/api-annuaire-education.

The analysis

The first step consists of analysing the API and identifying the data we want to upload to iTop.
By going to the API overview page, we can see the main data available.

Overview of data provided by the API

Selection of data to be synchronised

We select the following information from the institutions:

· Unique identifier (UAI)
· Name
· Mailing address
· Type of institution
· Public or private information
· Academy code
· Number of pupils
· Availability of a catering service
· Open date

Several data types (text, number, date, enumeration and Boolean) will be synchronised.

Data source

The API request we will use is as follows:

GET /catalog/datasets/fr-en-annuaire-education/records

We will use select, limit and offset parameters in order to select only the data we are interested in, so that we can go through all the records in several iterations. It is indeed impossible to select all the institutions, as the limit of the number of results set by the API is 100 institutions per query.

You can run some tests to get familiar with the API here : https://api.gouv.fr/documentation/api-annuaire-education.

API testing page

2/ Creation of an iTop extension

We will create an extension to allow us to customise the iTop data model and thus be able to collect data from educational institutions. We will extend the location class and add specific information for national education institutions. We will also be making some changes to the iTop side menu to accommodate the menus associated with the api.gouv.fr APIs.

Skeleton generation

Start by generating an extension skeleton by entering the following information in the creation form.
Click on the button Generate and then download the generated archive.
Unzip the downloaded archive into the extensions folder on the iTop server.

Company name: MyCompany
Extension name: gouv-api-extension
Extension label: api.gouv.fr data synchronisation
Version: 1.0.0
Dependencies: itop-structure/3.0.0

Skeleton generation form

To go further in the creation of extensions, visit the official documentation.

Creation of the data model class

We will create our class to store school information. We choose to derive the class Location to inherit the location data.

General information

Initializing the class Establishment which derives from the class Location.

Add the general class information to the data model file as follows:

datamodel.gouv-edu-extension.xml

<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.6">
  …
    <classes>
      <class id="Establishment" _delta="define">
        <parent>Location</parent>
        <properties>
          <category>bizmodel,searchable</category>
          <abstract>false</abstract>
          <key_type>autoincrement</key_type>
          <db_table>establishment</db_table>
          <db_key_field>id</db_key_field>
          <db_final_class_field/>
          <naming>
            <format>%%1$s</format>
            <attributes>
              <attribute id="name"/>
            </attributes>
          </naming>
          <display_template/>
          <icon>assets/img/school.png</icon>
        </properties>
      </class>
    </classes>
  …
</itop_design>

💡The tag <icon> allows you to associate an image with the class.

Adding attributes to the data model class

Declaration of the attributes of the Establishment class. For each of these attributes we specify its data type xsi:type="AttributeXXX".
All attributes identifier, type and private_public are set as mandatory.

Add the attributes of the class to the data model file as follows:

datamodel.gouv-edu-extension.xml

<classes>
  <class id="Establishment" _delta="define">
  …
    <fields>
      <!-- identifier-->
      <field id="identifier" xsi:type="AttributeString">
        <sql>identifier</sql>
        <default_value/>
        <is_null_allowed>false</is_null_allowed>
      </field>
      <!-- type -->
      <field id="type" xsi:type="AttributeString">
        <sql>type</sql>
        <default_value/>
        <is_null_allowed>false</is_null_allowed>
      </field>
      <!-- private_public -->
      <field id="private_public" xsi:type="AttributeEnum">
        <values>
          <value>unknown</value>
          <value>private</value>
          <value>public</value>
        </values>
        <sql>private_public</sql>
        <default_value/>
        <is_null_allowed>false</is_null_allowed>
        <display_style>radio_horizontal</display_style>
      </field>
      <!-- academy_code -->
      <field id="academy_code" xsi:type="AttributeString">
        <sql>code_academie</sql>
        <default_value/>
        <is_null_allowed>true</is_null_allowed>
      </field>
      <!-- catering_service -->
      <field id="catering_service" xsi:type="AttributeBoolean">
        <sql>catering_service</sql>
        <default_value/>
        <is_null_allowed>true</is_null_allowed>
      </field>
      <!-- students_count -->
      <field id="students_count" xsi:type="AttributeInteger">
        <sql>students_count</sql>
        <default_value/>
        <is_null_allowed>false</is_null_allowed>
      </field>
      <!-- opening_date -->
      <field id="opening_date" xsi:type="AttributeDate">
        <sql>opening_date</sql>
        <default_value/>
        <is_null_allowed>false</is_null_allowed>
      </field>
    </fields>
  </class>
</classes>

Add the attributes of the class to the data model file as follows:
By default, only the English translation file is created, add other languages if required.

en.dict.gouv-api-extension.php

Dict::Add('EN US', 'English', 'English', array(
  'Class:Establishment/Attribute:identifier' => 'Establishment code',
  'Class:Establishment/Attribute:identifier+' => 'Code UAI',
  'Class:Establishment/Attribute:type' => 'Establishment type',
  'Class:Establishment/Attribute:private_public' => 'Private / Public',
  'Class:Establishment/Attribute:private_public/Value:private' => 'Private',
  'Class:Establishment/Attribute:private_public/Value:public' => 'Public',
  'Class:Establishment/Attribute:private_public/Value:unknown' => 'Unknown',
  'Class:Establishment/Attribute:academy_code_academie' => 'Academy code',
  'Class:Establishment/Attribute:catering_service' => 'Catering service',
  'Class:Establishment/Attribute:students_count' => 'Students count',
  'Class:Establishment/Attribute:opening_date' => 'Opening date',
));

💡The + symbol allows you to attach a hovering tooltip to the form for a given attribute: ‘Class:Establishment/Attribute:identifier+

Adding presentation data

Declaration of the different display modes of our class.
Section détails sets the display of the class when viewing or editing an object.
Section list defines the attributes of the class used as columns in lists.
Section search defines the attributes used as criteria for searches.

Add the display modes as follows:

datamodel.gouv-edu-extension.xml

      <presentation>
        <details>
          <items>
            <item id="type">
              <rank>30</rank>
            </item>
            <item id="identifier">
              <rank>10</rank>
            </item>
            <item id="private_public">
              <rank>40</rank>
            </item>
            <item id="academy_code">
              <rank>50</rank>
            </item>
            <item id="catering_service">
              <rank>60</rank>
            </item>
            <item id="students_count">
              <rank>70</rank>
            </item>
            <item id="opening_date">
              <rank>80</rank>
            </item>
            <item id="org_id">
              <rank>90</rank>
            </item>
            <item id="address">
              <rank>100</rank>
            </item>
            <item id="postal_code">
              <rank>110</rank>
            </item>
            <item id="city">
              <rank>120</rank>
            </item>
            <item id="country">
              <rank>130</rank>
            </item>
          </items>
        </details>
        <search>
          <items>
            <item id="type">
              <rank>10</rank>
            </item>
            <item id="private_public">
              <rank>20</rank>
            </item>
            <item id="academy_code">
              <rank>30</rank>
            </item>
          </items>
        </search>
        <list>
          <items>
            <item id="identifier">
              <rank>10</rank>
            </item>
            <item id="private_public">
              <rank>30</rank>
            </item>
            <item id="city">
              <rank>40</rank>
            </item>
            <item id="opening_date">
              <rank>50</rank>
            </item>
            <item id="catering_service">
              <rank>60</rank>
            </item>
            <item id="students_count">
              <rank>70</rank>
            </item>
          </items>
        </list>
      </presentation>
Adding reconciliation data

Declaration of the reconciliation attributes of the class.
We use the school ID (UAI) for reconciliation because it is unique.

🎓Reconciliation defines one or more attributes that ensure the identification of an object.

Add the class reconciliation information to the data model file as follows:

datamodel.gouv-edu-extension.xml

<classes>
  <class id="Establishment" _delta="define">
    …
    <properties>
      …
      <reconciliation>
        <attributes>
          <attribute id="identifier"/>
        </attributes>
      </reconciliation>
    </properties>
  </class>
</classes>

iTop menu customisation

We will organise data retrieved from api.gouv.fr in a menu integrated in the iTop side menu.
You can then add as many entries to this menu as you implement APIs.

api.gouv.fr menu overview

Add the class menu information to the data model file as follows:

datamodel.gouv-edu-extension.xml

<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.6">
  …
  <menus>
    <menu id="GouvAPIMenu" xsi:type="MenuGroup" _delta="define">
      <rank>10</rank>
      <style>
        <decoration_classes>fas fa-dice-d20</decoration_classes>
      </style>
    </menu>
    <menu id="EstablishmentSearchMenu" xsi:type="SearchMenuNode" _delta="define">
      <rank>10</rank>
      <parent>GouvAPIMenu</parent>
      <class>Establishment</class>
    </menu>
  </menus>
  …
</itop_design>

💡Tag <decoration_classes> allows you to set an icon for the menu group.

Codes for the icons are available on Font Awesome website: https://fontawesome.com/.

Add the translations of your menus to the internationalisation file as follows:

en.dict.gouv-api-extension.php

Dict::Add('EN US', 'English', 'English', array(
  'Menu:GouvAPIMenu' => 'api.gouv.fr',
  'Menu:EstablishmentSearchMenu' => 'National Education Establishments',
));

Installing the extension

Copy the extension to the extensions folder on your iTop server.
Add write permissions to the iTop configuration file located in conf/production/config-itop.php then go to the following web address of your iTop: http://{adresse-serveur-itop}/setup in a web browser to start the installation.

Confirm the installation steps and make sure that our new extension is checked at the end of the installation wizard.

Extension selection screen

And that’s it! You can now host national education institutions in your iTop.
Try to create an institution to check that everything is up and running.

Navigate to the api.gouv.fr > National education establishments menu and click on create an establishment.

Form for the creation of a new establishment

In the next chapter, we will see how to synchronise data using a collector.

3/ Creation of an iTop data collector

Installation iTop data collector

Download the extension data collector base.
And unzip the contents into a folder of your choice. (Do not unzip into the folder www of your server to avoid exposing its contents)
It is perfectly possible to unzip on another machine on the network as long as it has a PHP 7.x runtime environment.

Read all about Data collector base in our official documentation.

Data source configuration

Data source configuration is described in JSON format.
The easiest way to do this is to set up the data source in iTop’s console user interface and transform it into JSON format via a script provided by the iTop data collector extension.

Create a new data source in the configuration menu> sources of synchronisation data.
Enter the following information and leave the other fields unchanged.

Name: Synchro national education establishments
Description: https://api.gouv.fr/documentation/api-annuaire-education
Target class: Establishment
Contact to notify: Your_email
Datatable: data_national_education_establishment

Data source configuration screen (Properties)

Choose the attribute identified for reconciliation.
Choose the reconciliation key Full name for the Owner organization attribute, which will allow us to indicate the organisation by name and thus be more readable.

Data source configuration screen (Attributes)

Then generate the JSON definition of this data source in the collectors directory.

php toolkit/dump_tasks.php --task_name="Synchro établissements éducation nationale" > collectors/DataEducationCollector.class.inc.json

For further information on this topic, please refer to our official documentation.

Collector settings

We need to prepare a settings file for the use of the collector.

Duplicate the configuration file params.distrib.xml and name it params.local.xml.
Update the configuration file with your settings as follows:

conf/params.local.xml

<?xml version="1.0" encoding="UTF-8"?>

<parameters>
  <itop_url>itop_server_url</itop_url>
  <itop_login>your_login</itop_login>
  <itop_password>your_password</itop_password>
  <itop_login_mode></itop_login_mode>

  <synchro_user>admin</synchro_user>
  <contact_to_notify>your_email</contact_to_notify>
  …

Collector class creation

Creation of the class DataEducationCollector derived from the class Collector.
The presence of curl has to be checked and you initialise it by deleting the old data collected.

DataEducationCollector.class.inc.php

class DataEducationCollector extends Collector
{
  /** @var array education establishment data */
  protected $aData = [];

  /** @var integer current process element */
  protected $iCurrent;

  /** @var integer fetched elements count */
  protected $idx;

  /**
  * Constructor.
  *
  * @throws Exception
  */
  public function __construct()
  {
    parent::__construct();

    // test curl presence
    if(!function_exists('curl_version')){
      throw new Exception('Curl is mandatory, check your PHP installation');
    }

    // initialization
    $this->init();
  }

  /**
  * Initialization.
  *
  * @return void
  */
  private function init() : void
  {
    // remove data files
    $this->RemoveDataFiles();
  }
}


Declare the main function for the API call.
This method returns a boolean to indicate that there is still data to collect.
You iterate by bunches of 100 elements (max defined by the API) and you limit yourself to 300 results for the moment.

DataEducationCollector.class.inc.php

class DataEducationCollector extends Collector
{
  …
  /**
  * Call rest api to acquire data.
  *
  * @return bool true if more data available
  */
  private function callRestApi() : bool
  {
    // open curl
    $oCurl = curl_init();

    // check limit
    $limit = 100;
    $max = 300;
    if($this->iCurrent + $limit > $max){
      $limit = $max - $this->iCurrent;
    }

    // construct API url
    $url = "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?limit=$limit&offset=$this->iCurrent";

    // curl options
    curl_setopt($oCurl, CURLOPT_URL, $url);
    curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, false);

    // execute
    $aResult = json_decode(curl_exec($oCurl), true);
    echo "curl_exec: $url\n";
    curl_close($oCurl);

    // retrieve records
    $this->aData = $aResult['records'];
    $this->iCurrent += count($aResult['records']);

    // processing flags
    $hasMoreData = $this->iCurrent < $aResult['total_count'];
    $hasReachMax = $this->iCurrent >= $max;

    return $hasMoreData && !$hasReachMax;
  }
}

The basic mechanism of collection.
The prepare function is called to collect a batch and then the records of this batch are unstacked by adding them in csv files respecting a maximum data size.
You will repeat the process as long as there is data to be collected.

DataEducationCollector.class.inc.php

class DataEducationCollector extends Collector
{
  …
  /**
  * Prepare data.
  *
  * @return bool true if more data available
  */
  public function Prepare() : bool
  {
    // call rest API for new block of data
    $this->idx = 0;
    return $this->callRestApi(); // return true if more data available
  }

  /**
  * Fetch next block of data.
  *
  * @return bool|array current element data or false if no more data to fetch
  */
  public function Fetch()
  {
    // has something to fetch ?
    if ($this->idx < count($this->aData))
    {
      // next value to fetch
      $aElement = $this->aData[$this->idx++]['record'];

      // table conversion establishment private public enumeration
      $tableConvPrivatePublicEnum = [
        'Privé' => 'private',
        'Public' => 'public',
        '' => 'unknown',
      ];

      return array(
        'primary_key' => $aElement['id'],
        'identifiant' => $aElement["fields"]['identifiant_de_l_etablissement'],
        'name' => $aElement["fields"]['nom_etablissement'],
        'org_id' => $this->aSynchroFieldsToDefaultValues['org_id'],
        'address' => $aElement['fields']['adresse_1'],
        'city' => $aElement['fields']['nom_commune'],
        'postal_code' => $aElement['fields']['code_postal'],
        'country' => 'France',
        'status' => 'active',
        'type' => $aElement['fields']['type_etablissement'],
        'private_public' => $tableConvPrivatePublicEnum[$aElement['fields']['statut_public_prive']],
        'code_academie' => $aElement['fields']['code_academie'],
        'restauration' => $aElement['fields']['restauration'] == '' ? '0' : $aElement['fields']['restauration'],
        'nombre_eleves' => $aElement['fields']['nombre_d_eleves'] == null ? 0 : $aElement['fields']['nombre_d_eleves'],
        'date_ouverture' => $aElement['fields']['date_ouverture']
      );
    }

    return false; // no more data to fetch
  }

  /**
  * Collect data.
  *
  * @return bool true if data has been collected
  */
  public function Collect($iMaxChunkSize = 0)
  {
    $idx = 0;

    do{
      // block preparation
      $continue = $this->Prepare();

      // fetch elements...
      while ($aRow = $this->Fetch()) {
        if (($idx == 0) || (($iMaxChunkSize > 0) && (($idx %% $iMaxChunkSize) == 0))) {
          $this->NextCSVFile();
          $this->AddHeader($this->aColumns);
        }
        $this->AddRow($aRow);
        $idx++;
      }
    }
    while($continue); // next block

    // cleanup
    $this->Cleanup();

    return $idx > 0;
  }
}


Collector registration

The next step is to declare the new collector to the orchestrator.

Create a file main.php to the root of the collectors folder and initialise it as follows:

collectors/main.php

Orchestrator::AddCollector(1, 'DataEducationCollector');

Launching the synchronisation

You can now trigger the synchronisation with the following command.

php .\exec.php

Browse the menu api.gouv.fr > National educational establishments.
You should get your 300 establishments synchronised.

4/ Industrialisation of the solution

We now want to finalise our solution by externalising parameters from our collector into the settings file.

Adding parameters

Add class members as follows:

DataEducationCollector.class.inc.php

class DataEducationCollector extends Collector
{
  const PRIMARY_KEY = 'primary_key';

  /** @var string rest API url */
  protected $sRestUrl;

  /** @var integer rest results amount limit */
  protected $iRestLimit;

  /** @var integer max elements to synchronize */
  protected $iMaxElements;

  /** @var array data columns */
  protected $aSynchroCols;

  /** @var array data translation */
  protected $aSynchroTranslation;

  /** @var array default values */
  protected $aSynchroDefaultValues;

  /** @var array class fields */
  protected $aSynchroClassFields;
  …

  /**
  * Initialization.
  *
  * @return void
  */
  private function init() : void
  {
    $this->iCurrent = 0;
    $this->iMaxElements = -1;

    // extract configuration values
    $this->extractConfigurationValues();

    // remove data files
    $this->RemoveDataFiles();
  }

  /**
  * Extract configuration values.
  *
  * @return void
  * @throws Exception
  */
  private function extractConfigurationValues() : void
  {
    // read configuration values
    $aConfigurationValues = Utils::GetConfigurationValue(strtolower(get_class($this)));

    // rest url
    if (array_key_exists('rest_url', $aConfigurationValues)) {
      $this->sRestUrl = $aConfigurationValues['rest_url'];
    }

    // rest limit
    if (array_key_exists('rest_limit', $aConfigurationValues)) {
      $this->iRestLimit = $aConfigurationValues['rest_limit'];
    }

    // max elements
    if (array_key_exists('max_elements', $aConfigurationValues)) {
      $this->iMaxElements = $aConfigurationValues['max_elements'];
    }

    // fields association
    if (array_key_exists('class_fields', $aConfigurationValues)) {
      $this->aSynchroClassFields = array(Self::PRIMARY_KEY => null);
      $this->aSynchroClassFields = array_merge($this->aSynchroClassFields, $aConfigurationValues['class_fields']);
      $this->aSynchroCols = array_keys($this->aSynchroClassFields);
    }

    // translation
    if (array_key_exists('translations', $aConfigurationValues)) {
      $this->aSynchroTranslation = $aConfigurationValues['translations'];
      foreach ($this->aSynchroTranslation as $key => $aTranslation) {
        $this->aSynchroTranslation[$key] = array_flip($this->aSynchroTranslation[$key]);
      }
    }

    // default values
    if (array_key_exists('defaults', $aConfigurationValues)) {
      $this->aSynchroDefaultValues = $aConfigurationValues['defaults'];
    }
  }

  /**
  * Call rest api to acquire data.
  *
  * @return void
  * @throws Exception
  */
  private function callRestApi() : bool
  {
    …
    // check limit
    $limit = $this->iRestLimit;
    if($this->iCurrent + $this->iRestLimit > $this->iMaxElements){
      $limit = $this->iMaxElements - $this->iCurrent;
    }

    // construct API url
    $url = "$this->sRestUrl?limit=$limit&offset=$this->iCurrent";
    …
  }

  /**
  * Fetch next data.
  *
  * @return bool|array current element data or false if no more data to tech
  */
  public function Fetch()
  {
    // has something to fetch ?
    if ($this->idx < count($this->aData))
    {
      // next value to fetch
      $aElement = $this->aData[$this->idx++]['record'];

      // fetched data
      $fetchedData = array();

      // fill values
      foreach ($this->aSynchroClassFields as $classField => $value) {
        if($classField == Self::PRIMARY_KEY){
          $fetchedData[$classField] = $aElement['id'];
        }
        else if(array_key_exists($value, $aElement["fields"]) && $aElement["fields"][$value] != ''){
          $fetchedData[$classField] = $this->getFieldValue($classField, $aElement["fields"][$value]);
        }
        else{
          $fetchedData[$classField] = $this->getFieldDefaultValue($classField);
        }
      }

      return $fetchedData;
    }

    return false; // no more data to fetch
  }

Versioning the collector

Create a module declaration file at the collectors folder root and initialise it as follows:

collectors/module.gouv-api-collectors-extension.php

__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'gouv-api-collectors-extension/1.0.0',
array(
  // Identification
  //
  'label' => 'api.gouv.fr API data synchronisation collectors',
  'category' => 'business',

  // Setup
  //
  'dependencies' => array(
    'gouv-api-extension/1.0.0'
  ),
  'mandatory' => false
  )
);

5/ Exercise on your own!

List of AFPA establishments throughout France
https://api.gouv.fr/documentation/api_etablissements_publics

Add a new entry in the sidebar menu.

6/ Quizz

– How to create an extension skeleton quickly
🔲 By creating it with a text editor
🔲 Thanks to the wiki pages
🔲 Based on another extension

– What is reconciliation?
🔲 The primary key of a class
🔲 Displaying the intelligible name of an object
🔲 One or a set of identifying attributes

– Where are translations of the text defined?
🔲 In the class tag of the data model
🔲 In the dictionaries tag of the data model
🔲 In the PHP function Dict::Add

– How to install an extension
🔲 With iTop hub
🔲 By running an installation script
🔲 By putting the extension in the extensions folder of the iTop server.

– How to release a version of your collector
🔲 By naming the folder with the version
🔲 By creating a module file
🔲 By pushing it to Git

Share This