Prechádzať zdrojové kódy

Added documentation to describe creation offromatters and pagers to address issue #165

Stephen Ficklin 6 rokov pred
rodič
commit
69bd91bbae

+ 6 - 1
.gitignore

@@ -5,4 +5,9 @@ tests/.env
 .buildpath
 .project
 .settings/
-docs/_build/
+docs/_build
+docs/docs
+docs/html
+docs/latex
+docs/xml
+

+ 1 - 0
docs/dev_guide/custom_field.rst

@@ -44,4 +44,5 @@ The rest of this section will walk you through these steps.
    custom_field/manual_field_creation
    custom_field/custom_widget
    custom_field/custom_formatter
+   custom_field/create_instance
    custom_field/tripal_field_generator

+ 219 - 0
docs/dev_guide/custom_field/create_instance.rst

@@ -0,0 +1,219 @@
+Attach Fields to Content Types
+==============================
+In summary, creation of a new field requires creation of three classes that inherit from the ``TripalField``, ``TripalFieldWidget`` and ``TripalFieldFormatter`` base classes.  If the fields are created correctly and placed in the ``includes/TripalFields`` directory of your module then Tripal will automatically find them.  However, the field is not yet attached to any content type. They must be attached.  Fields can be attached programmatically or via the online Drupal interface by a site admin. 
+
+The hook_bundle_field_info() function
+-------------------------------------
+ The three TripalField classes simply define how the field will function, but Drupal does not yet know about the field.  The ``hook_bundle_field_info`` function tells Drupal about your field. It must be implemented in a custom Drupal module,and provides an array that tells Drupal about the fields and the classes to use for the field.  Suppose we were creating a field named ``obi__genus`` which displays the Genus for a species and we have a custom module named ``tripal_org2``.  The hook function would be named ``tripal_org2_bundle_field_info()``:
+
+.. code-block:: php
+  :linenos:
+
+  function tripal_org2_bundle_field_info($entity_type, $bundle) {
+    $info = [];
+    
+    // Make sure this bundle is an organism (OBI:0100026) then we'll attach our 
+    // field to display the genus.
+    $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
+    $term_accession = $term->vocab->vocabulary . '__' . $term->accession;
+    if ($term_accession == 'OBI:0100026') {
+      $field_name = 'obi__genus';
+      $field_type = 'obi__genus';
+      $info[$field_name] = [
+        'field_name' => $field_name,
+        'type' => $field_type,
+        'cardinality' => 1,
+        'locked' => FALSE,
+        'storage' => [
+          'type' => 'field_chado_storage',
+        ],
+        'settings' => [],
+      ];
+   }
+    
+    return $info
+  }
+  
+This function receives as it's second argument the ``$bundle``. This is the bundle that Drupal is requesting new fields for.  For this example we only want to attach the field if the content type is the organism content type.  The format of the returned ``$info`` array should have the field name as the key and an array that follows the instructions provided by Drupal's `field_create_field() <https://api.drupal.org/api/drupal/modules%21field%21field.crud.inc/function/field_create_field/7.x>`_ function. 
+
+The settings indicate the field name, the field type, the cardinality (how many values are allowed), any default settings and the storage type.  Because we expect our data to come from Chado we set the ``field_chado_storage`` as the type.  The ``locked`` setting is set to FALSE indicating that Drupal will allow the field to be deleted if the site developer desires.
+
+When the site administrator navigates to **Administer > Structure > Tripal Content Types**, clicks on a content type, and then the **manage fields** tab, a link appears at the top titled **Checkfor new fields**.  When that link is clicked, this hook function is called.
+
+Programmatically Attaching Fields
+---------------------------------
+You probably want to programmatically attach fields to content types if your have existing data that you know should be made available. For example, an organism always has a genus and only one genus.  If we have a field that displays the genus for an organism then we will want it automatically attached on installation of our module.  We can do this programmatically using two hook functions: ``hook_bundle_field_info()`` and ``hook_bundle_instance_info()``.  Both functions are required to attach a field to a content type. 
+
+The hook_bundle_instance_info() function.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The previous hook tells Drupal that our field exists and is allowed to be connected to the organism bundle.  Next we need to create an actual instance of this field for the bundle.  We do this with the ``hook_bundle_instance_info()`` function.  The format is the same as the previous hook but the info array is different.  For example:
+
+.. code-block:: php
+  :linenos:
+
+  function tripal_org2_bundle_instances_info($entity_type, $bundle) {
+    $info = []
+    
+    // Make sure this bundle is an organism (OBI:0100026) then we'll attach our 
+    // field to display the genus.
+    $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
+    $term_accession = $term->vocab->vocabulary . '__' . $term->accession;
+    if ($term_accession == 'OBI:0100026') {
+    
+      $field_name = 'obi__genus';
+      $is_required = FALSE;
+      $info[$field_name] =  [
+        'field_name' => $field_name,
+        'entity_type' => $entity_type,
+        'bundle' => $bundle->name,
+        'label' => 'Genus',
+        'description' => 'The genus for the organism',
+        'required' => TRUE,
+        'settings' => [
+          'auto_attach' => TRUE,
+          'chado_table' => 'organism',
+          'chado_column' => 'genus',
+          'base_table' => 'organism',
+          'term_accession' => '0000005',
+          'term_vocabulary' => 'TAXRANK',
+          'term_name' => 'Genus',
+        ],
+        'widget' => [
+          'type' => 'obi__genus_widget',
+          'settings' => [
+            'display_label' => 1,
+          ),
+        ],
+        'display' => [
+          'default' => [
+            'label' => 'inline',
+            'type' => 'obi__genus_formatter',
+            'settings' => [],
+          ],
+        ],
+      ];
+    }
+    return $info;
+  }
+  
+The format of the returned ``$info`` array should have the field name as the key and an array that follows the instructions provided by Drupal's `field_create_instance() <https://api.drupal.org/api/drupal/modules%21field%21field.crud.inc/function/field_create_instance/7.x>`_ function. 
+
+Unique to this info array are the settings related to Chado.  Because we expect our data to be loaded from Chado we must specify these settings:
+
+ - ``base_table``: the name of the base table to which the record will be associated. In our case the ``organism`` table of Chado is the base table.
+ - ``chado_table``: the name of the actual table form which the value of the field will be loaded or saved to.  In our case the ``organism`` table is also the ``chado_table``.  
+ - ``chado_column``: the name of the column in the ``chado_table`` where the data is loaded from. if the ``base_table`` and ``chado_table`` are the same then this is the name of the column. In our case the ``genus`` columns.  If the base and chado tables are different then it is the name o the primary key column in the ``chado_table``
+ - ``auto_attach``:  set this to TRUE if you want the field to automatically be added to an entity when it is generated for viewing.  Set it to FALSE to allow the field to be added via AJAX. For fields that require time to load setting to FALSE is preferred. 
+ 
+Notice as well that the ``display`` and ``widget`` sections list the name of our TripalEntityWidget and TripalEntityFormatter calsses respectively.  This tells drupal to use our widget and formatter classes by default.
+
+When the site administrator navigates to **Administer > Structure > Tripal Content Types**, clicks on a content type, and then the **manage fields** tab, a link appears at the top titled **Checkfor new fields**.  When that link is clicked, this hook function is called.  
+
+.. note::
+
+  Both hook functions must be properly constructed for the field to be automatically attached to the content type.
+  
+Allowing Manual Attachment of Fields
+------------------------------------
+Not all fields are created equal.  Some field can be added by the site developer to a bundle and some cannot.  When the ``TripalField`` class is implemented for a class the ``$no_ui`` parameter is set to indicate if a field can be added via the web interface or not.  See the :doc:`manual_field_creation` page for more details. But in short the following setting does not allow a field to be added using the web interface
+
+.. code-block::  php
+
+ public static $no_ui = FALSE;
+ 
+The following setting will allow the field to be added:
+
+.. code-block::  php
+
+ public static $no_ui = TRUE;
+
+Next, we must let Drupal know that our field exists.  We do this by adding an entry to the ``$info`` array of in the ``hook_bundle_field_info()`` function described above.  This let's Drupal know about our field. However, because we are not programmatically creating an instance of the field on a content type, but allowing the user to create them we do not need to implement the ``hook_bundle_instance_info()`` function. Instead, we must implement the ``hook_bundle_create_user_field()``.  This function is called when the user attempts to add our new field to a bundle.  One field that comes with Tripal is the ``chado_linker__prop`` field.  Most Chado base tables have an associated property table (e.g. ``organismprop``, ``featureprop``, ``stockprop``, etc). By default, the ``tripal_chado`` module automatically adds this field to all bundles that have existing properties. It adds a new instance for every propert type.  However, new properties can be added to bundle, and the site admin may want to add those properties via the user interface rather. Therefore, this field has the `$no_ui` set to TRUE and uses the  ``hook_bundle_create_user_field()`` to create the new field instnace for the user.
+
+The following code is a snippet from the ``tripal_chado_bundle_create_user_field`` function of the ``tripal_chado`` module. Note that it uses the ``field_create_field`` function and the ``field_create_instance`` functions directly.  The arrays passed to these functions are identical to the ``$info`` arrays of both the ``hook_bundle_field_info`` and ``hook_bundle_instance_info`` functions described above.
+
+.. code-block:: php
+  :linenos:
+  
+  function tripal_chado_bundle_create_user_field($new_field, $bundle) {
+
+    // Get the table this bundle is mapped to.
+    $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
+    $vocab = $term->vocab;
+    $params = array(
+      'vocabulary' => $vocab->vocabulary,
+      'accession' => $term->accession,
+    );
+    $chado_table = $bundle->data_table;
+    $chado_type_table = $bundle->type_linker_table;
+    $chado_type_column = $bundle->type_column;
+    $chado_type_id = $bundle->type_id;
+    $chado_type_value = $bundle->type_value;
+  
+    // We allow site admins to add new chado_linker__prop fields to an entity.
+    // This function will allow us to properly add them.  But at this point we
+    // don't know the controlled vocabulary term.  We'll have to use the
+    // defaults and let the user set it using the interface.
+    if ($new_field['type'] == 'chado_linker__prop') {
+      $table_name = $chado_table . 'prop';
+  
+      if (chado_table_exists($table_name)) {
+        $schema = chado_get_schema($table_name);
+        $pkey = $schema['primary key'][0];
+        $field_name = $new_field['field_name'];
+        $field_type = 'chado_linker__prop';
+  
+        // First add the field.
+        field_create_field(array(
+          'field_name' => $field_name,
+          'type' => $field_type,
+          'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+          'locked' => FALSE,
+          'storage' => array(
+            'type' => 'field_chado_storage',
+          ),
+        ));
+  
+        // Now add the instance
+        field_create_instance(array(
+          'field_name' => $field_name,
+          'entity_type' => 'TripalEntity',
+          'bundle' => $bundle->name,
+          'label' => $new_field['label'],
+          'description' => '',
+          'required' => FALSE,
+          'settings' => array(
+            'auto_attach' => TRUE,
+            'base_table' => $chado_table,
+            'chado_table' => $table_name,
+            'chado_column' => $pkey,
+            'term_vocabulary' => '',
+            'term_accession' => '',
+            'term_name' => ''
+          ),
+          'widget' => array(
+            'type' => 'chado_linker__prop_widget',
+            'settings' => array(
+              'display_label' => 1,
+            ),
+          ),
+          'display' => array(
+            'default' => array(
+              'label' => 'inline',
+              'type' => 'chado_linker__prop_formatter',
+              'settings' => array(),
+            ),
+          ),
+        ));
+      }
+      else {
+        drupal_set_message('Cannot add a property field to this entity. Chado does not support properties for this data type.', 'error');
+      }
+    }
+  }
+
+
+.. note::
+  
+  It is possible to have a field that is both programmtically attached to some content types but is also allowed to be attached to another content type by the site admin using the web interface. To do this, programmatically add the field to the content types using the ``hook_bundle_instance_info`` function and also implement the ``hook_bundle_create_user_field`` function to support manual adding.
+  
+ 

BIN
docs/dev_guide/custom_field/custom_formatter.pager.1.png


+ 203 - 0
docs/dev_guide/custom_field/custom_formatter.rst

@@ -0,0 +1,203 @@
+Creating a Custom Formatter
+===========================
+The third component of a field is the formatter.  Thus far we have introduced how to create a field class and a widget class for a field.  The field class is responsible for describing the field, loading data into it, and providing search support.  The widget class provided a Drupal form for online editing of the field.  Finally, the formatter is responsible for display of the field on a Tripal site.  
+ 
+.. note::
+  This guide assumes you already have your formatter class file created. For more information, see :doc:`manual_field_creation` or, :doc:`tripal_field_generator`. 
+  
+The formatter class is the most simple of all the Tripal field classes.  Here we will again use the **obi__organism** field that come swith the ``tripal_chado`` module.  
+
+The view() function.
+~~~~~~~~~~~~~~~~~~~~
+In most cases the only function you need to implement is the ``view()`` function. This function is called whenever your field needs to be displayed on a page. The following code is from the ``obi__organism_formatter.inc`` class file.  
+
+.. code-block:: php
+  :linenos:
+
+  public function view(&$element, $entity_type, $entity, $langcode, $items, $display) {
+  
+    if ($items[0]['value']) {
+      $content = $items[0]['value']['rdfs:label'];
+      if (array_key_exists('entity', $items[0]['value'])) {
+        list($entity_type, $entity_id) = explode(':', $items[0]['value']['entity']);
+        $content = l(strip_tags($items[0]['value']['rdfs:label']), 'bio_data/' . $entity_id);
+      }
+
+      // The cardinality of this field is 1 so we don't have to
+      // iterate through the items array, as there will never be more than 1.
+      $element[0] = array(
+        '#type' => 'markup',
+        '#markup' => $content,
+      );
+    }
+  }
+  
+In the code above the input arguments have the following meaning:
+  - ``$element`` is the first argument. It is an array into which you should set the contents to be displayed.  
+  - ``$entity_type`` will always have the value ``Tripal Entity``.
+  - ``$entity`` is the entity object which contains all information about the entity including the loaded data values.
+  -  ``$langcode`` is the language. This is used by Drupal to provide translations of data into other spoken languages. By default, Tripal does not use a language.
+  - ``$items`` is an array containing all of the loaded data for this field.  
+  - ``$display`` is the name of the display such as full page, a teaser, etc. Currently, Tripal does not distinguish between displays.
+  
+The purpose of the ``view()`` function is to iterate through the values in the ``$items`` array, and format them into an appropriate display for viewing.  Here you must remember the structure of the data in the ``$items`` array.  
+ 
+To demonstrate this function, let's look at what we expect in our ``$items`` array. Using the `Citrus sinesis` organism from the User's Guide. We would expect an items array to look like the following:
+ 
+.. code::
+
+  $items = [
+    0 => [
+      "value" => [
+        "rdfs:label" =>  "Citrus sinensis",
+        "rdfs:type" =>  "Organism",
+        "local:abbreviation" =>  "C. sinensis",
+        "TAXRANK:0000005" => "Citrus",
+        "TAXRANK:0000006" => "sinensis",
+        "entity" => "TripalEntity:3",
+      ],
+      "chado-feature__organism_id" => 12,
+    ],    
+  ];
+  
+You may recall that the ``$items`` array structure is the same as that created by the ``load()`` function described in the :doc:`manual_field_creation` page. Note that each key in the ``value`` array is an accession for a controlled vocabulary term.  These accessions are used to unambiguously describe the value. To display the organism on a page we need the element named ``rdfs:label``.  Therefore, on line 4 of the code above we set the ``$content`` variable to contain this value.
+
+Because our organisms are also published entities we want to link to their respective pages each time an organism is displayed.  Because the ``value`` array has an element named ``entity`` we know that this item is published.  Lines 5-6 above use this information to create a clickable link to the organism page.   Finally, the ``$element`` argument is set to provide content of type ``markup``.  This ``$element`` array is a `Drupal renderable array <https://www.drupal.org/docs/7/api/render-arrays/render-arrays-overview>`_.
+
+Lastly, notice the element named ``chado-feature__organsim_id``.  This element is at the same level as the ``value`` element.  This data is meant to be used internally by the field. It maps this fields values to the appropriate table in Chado where the data is stored.  
+
+.. warning:: 
+
+  You should never show to the user any data that is outside of ``value`` element.  
+
+In summary, the following should be observed when processing the ``$items`` array for viewing:
+
+  - A field with only one value (a cardinality of 1) will always have only one element in the ``$items`` array and can use the index 0. This is what has been done in this example code. 
+  - A field with more than one value can have any number of elements in the ``$items`` array.  You should therefore iterate through all of them.
+  - For each element in the ``$items`` array there is a ``value`` key.  Only the data in the ``value`` key should be shown to the user.
+  - Each element in the ``$items`` array may have more than a ``value`` key.  These values are meant to help manage the data. 
+
+.. warning::
+
+  You should never have SQL statments or any API calls that retreive data in the foramter ``view()`` function. The formatter should strictly format data for viewing.
+  
+Creating Pagers
+~~~~~~~~~~~~~~~
+The example shown in the previous section was for a field that will always only contain a single element.  However some fields may contain a large number of elements.  Consider an mRNA and it's relationships to subfeatures: exons, 5' UTRs, 3'UTRs, CDS, etc.).  A large mRNA can have many relationships.  Alternatively, consider the case where a genentic map content type may have a field that lists all of the markers on the map.  Such a list could become extremely long on the page.  In these cases it may be best to only list a few items at a time and to provide a pager to let the user cycle through the items.  An example of a pager added to the bottom of relationships is shown in the example below.
+
+.. image:: custom_formatter.pager.1.png
+
+To create a pager we first need to calculate the number of items we want to display per page and the total number of pages required to display all of the data.  
+
+.. code-block:: php
+  
+  $items_per_page = 10;
+  $total_records = count($items);
+  $total_pages = (int) ($total_records / $items_per_page) + 1;
+  
+Next, we must initialize the pager by calling the ``pager_default_initialize`` function.  We pass it the total number of records, the number of items per page and the index (i.e. ``$pelement``) for this pager on the page.  
+
+.. code-block:: php
+
+  $pelement = 0; 
+  $current_page = pager_default_initialize($total_records, $items_per_page, $pelement);
+  
+The call to ``pager_default_initialize`` will return the current page. Each time the user navigates to other pages the ``view()`` function is called and the current page is always provided via this function call. Next, we must theme the pager so that it follows the look-and-feel prescribed for the site. For this we use the Drupal ``theme()`` function.
+
+.. code-block:: php
+
+  $pager = theme('pager', array(
+    'tags' => array(),
+    'element' => $pelement,
+    'parameters' => array(),
+    'quantity' => $total_pages,
+  ));
+  
+By default, all links in the pager cause the page to reload.  We do not want the page to reload, rather we only want to update the contents of the field.  The TripalFieldFormatter class provides a function named ``axaifyPager`` to convert a pager into an AJAX pager:
+
+.. code-block:: php
+
+  $pager = $this->ajaxifyPager($pager, $entity);
+  
+Now that we have a pager, it has been setup for AJAX and we know the current page that the user is viewing we can now display only the items from the ``$items`` array that are appropriate for the page being viewed. A common way to provide multiple items on a page is within a table. When we set the ``$element`` array we need to be sure to provide both the content and the pager:
+
+.. code-block:: php
+
+    $element[0] = array(
+      '#type' => 'markup',
+      '#markup' => $content . $pager,
+    );
+    
+The settingsForm() Funtion.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Sometimes you may want to provide some control to the site developer for the formatter.  For example, the ``sbo__relationship_formater`` allows the site developer to customize the title that appears above the table that houses relationships and the text the appears if there are no relationships.  By default the title is "Relationships" and the empty text indicates there are no relationships. Both are a bit too generic.  The ``settingsForm()`` function allows you to provide a Drupal form for the field that appears on the **Administer > Strucutre > Tripal Content Types** on any content type's **manage display** page:
+
+.. image:: custom_formatter.settings.1.png
+
+The form shown in the screenshot above is provided by the ``settingsForm()`` function.  The following code generates this form:
+
+.. code-block:: php
+  :linenos:
+  
+  public function settingsForm($view_mode, $form, &$form_state) {
+
+    $display = $this->instance['display'][$view_mode];
+    $settings = $display['settings'];
+    $element = array();
+    $element['title'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Table Header',
+      '#default_value' => array_key_exists('title', $settings) ? $settings['title'] : 'Relationship',
+    );
+    $element['empty'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Empty text',
+      '#default_value' => array_key_exists('empty', $settings) ? $settings['empty'] : 'There are no relationships',
+    );
+  
+    return $element;
+  }
+  
+The form is typical of any form.  Note, however that the ``#default_value`` is set using the current settings values.
+
+A settings form is useful but it only works when Drupal knows what settings you want for your field.  You must provide the settings names (e.g. "title" and "empty" in this case) when you  attach your field to a given content type (i.e. bundle).  You tell Drupal to attach this field to a content type using the ``hook_bundle_instance_info`` function.  See 
+the :doc:`create_instance` to learn more about this function.  Briefly, the ``display`` section of the info array for the ``sbo__relationship`` field contains the following settings for the ``display``:
+
+.. code-block:: php
+
+    'display' => array(
+      'default' => array(
+        'label' => 'hidden',
+        'type' => 'sbo__relationship_formatter',
+        'settings' => array(
+          'title' => 'Relationships',
+          'empty' => 'There are no relationships'
+        ),
+      ),
+    ),
+
+The settingsSummary() Function.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The ``settingsSummary()`` function provides a summary of the current settings values for a field on the **manage display** page.  The following shows the same relationship field from the previous section, but with the settings form closed, and a summary of the current values shown:
+
+.. image:: custom_formatter.settings.2.png
+
+An example of the ``sesttingsSummary()`` function that generates the summary in the image above is as follows:
+
+.. code-block:: php
+  :linenos:
+  
+  public function settingsSummary($view_mode) {
+    $display = $this->instance['display'][$view_mode];
+    $settings = $display['settings'];
+
+    $summary = t('Title: @title<br>Empty: @empty',
+        array(
+          '@title' => $settings['title'],
+          '@empty' => $settings['empty'])
+        );
+
+    return $summary;
+  }
+
+  

BIN
docs/dev_guide/custom_field/custom_formatter.settings.1.png


BIN
docs/dev_guide/custom_field/custom_formatter.settings.2.png


+ 4 - 1
tripal_chado/includes/tripal_chado.fields.inc

@@ -2700,7 +2700,10 @@ function tripal_chado_bundle_instances_info_linker(&$info, $entity_type, $bundle
         'default' => array(
           'label' => 'hidden',
           'type' => 'sbo__relationship_formatter',
-          'settings' => array(),
+          'settings' => array(
+            'title' => 'Relationships',
+            'empty' => 'There are no relationships'
+          ),
         ),
       ),
     );