You are here

nat.module in Node Auto Term [NAT] 6

Same filename and directory in other branches
  1. 5 nat.module
  2. 6.2 nat.module
  3. 7.2 nat.module
  4. 7 nat.module

NAT - node auto term - is a helper module that automatically creates a term using the same title as a node.

@author Karthik Kumar / Zen [ http://drupal.org/user/21209 ]. @internal There are a number of cases to be considered (dev notes): o Term adds/updates/deletes: i.e. should this be a 2-way module? o Vocabulary deletes. o Dissociation of node type and vocabulary in nat_config - how should this be handled? o Filter handling for body/description fields. o Duplicate handling?

Features to be added: o Node deletes: Optionally delete child nodes associated via NAT. o Maintain hierarchy on unassociated vocabularies (on a best effort basis?)

File

nat.module
View source
<?php

/**
 * @file
 * NAT - node auto term - is a helper module that automatically creates a
 * term using the same title as a node.
 *
 * @author Karthik Kumar / Zen [ http://drupal.org/user/21209 ].
 * @internal There are a number of cases to be considered (dev notes):
 *  o Term adds/updates/deletes: i.e. should this be a 2-way module?
 *  o Vocabulary deletes.
 *  o Dissociation of node type and vocabulary in nat_config - how should this
 *    be handled?
 *  o Filter handling for body/description fields.
 *  o Duplicate handling?
 *
 * Features to be added:
 *  o Node deletes: Optionally delete child nodes associated via NAT.
 *  o Maintain hierarchy on unassociated vocabularies (on a best effort basis?)
 */

/**
 * Implementation of hook_help().
 */
function nat_help($path, $arg) {
  switch ($path) {
    case 'admin/help#nat':
      return t('NAT - node auto term - is a helper module that automatically creates a term using the same title as a node.');
  }
}

/**
 * Implementation of hook_menu().
 */
function nat_menu() {
  $items = array();
  $items['admin/settings/nat'] = array(
    'title' => 'NAT',
    'description' => 'Establish node - node relationships via the taxonomy module.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'nat_settings_form',
    ),
    'access arguments' => array(
      'administer NAT configuration',
    ),
    'file' => 'nat.admin.inc',
  );
  $items['admin/settings/nat/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'nat_settings_form',
    ),
    'access arguments' => array(
      'administer NAT configuration',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file' => 'nat.admin.inc',
  );
  $items['admin/settings/nat/sync'] = array(
    'title' => 'Sync',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'nat_sync_form',
    ),
    'access arguments' => array(
      'administer NAT configuration',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'nat.admin.inc',
  );
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function nat_perm() {
  return array(
    'administer NAT configuration',
  );
}

/**
 * Implementation of hook_nodeapi().
 */
function nat_nodeapi(&$node, $op, $teaser, $page) {
  $nat_config = _nat_variable_get();
  if (!isset($nat_config['types'][$node->type]) || empty($nat_config['types'][$node->type])) {
    return;
  }
  switch ($op) {
    case 'load':
      $node->nat = nat_get_terms($node->nid);
      break;
    case 'insert':

      // Add term(s).
      $terms = _nat_add_terms($node);

      // Save node-term association in the NAT table.
      _nat_save_association($node->nid, $terms);
      break;
    case 'update':

      // Ensure that this is a node form submission and not a direct node_save
      // operation. @see http://drupal.org/node/197532 and
      // http://drupal.org/node/188377 .
      if (isset($node->form_id)) {
        _nat_update_terms($node);
      }
      break;
    case 'delete':

      // Deleting the associated term when a node is deleted is optional.
      if (isset($nat_config['delete'][$node->type])) {
        _nat_delete_terms($node->nid);
      }

      // Delete node-term association from the NAT table.
      _nat_delete_association($node->nid);
      break;
  }
}

/**
 * Implementation of hook_form_alter().
 */
function nat_form_alter(&$form, $form_state, $form_id) {
  if ($form['#id'] == 'node-form') {
    $config = _nat_variable_get();
    foreach ($config['types'] as $type => $associations) {
      if (count($associations) && $form_id == $type . '_node_form') {
        $nat_terms = array();

        // If this is a node update, remove this node's associated terms from
        // its associated vocabularies.
        // N.B. Free-tag vocabularies are unaffected by this.
        if (isset($form['#node']->nid)) {
          foreach ($form['#node']->nat as $tid => $term) {
            $nat_terms[$term->vid] = $tid;

            // Cull associated terms and their children from the taxonomy form.
            if (isset($form['taxonomy'])) {
              foreach ($form['taxonomy'] as $vid => $values) {
                if ($term->vid == $vid) {
                  $children = _nat_taxonomy_term_children($tid, $vid);
                  foreach ($values['#options'] as $id => $option) {

                    // Discount the -None- entry.
                    if (is_object($option)) {
                      $option_id = array_pop(array_keys($option->option));
                      if (in_array($option_id, $children)) {
                        unset($form['taxonomy'][$vid]['#options'][$id]);
                      }
                    }
                  }
                  break;
                }
              }
            }
          }
        }

        // Related terms.
        if (isset($config['related'][$type])) {
          $form['nat'] = array(
            '#type' => 'fieldset',
            '#title' => t('Term information'),
            '#tree' => TRUE,
            '#weight' => -2,
            '#collapsible' => TRUE,
            '#collapsed' => FALSE,
          );
          foreach ($associations as $vocabulary_id) {
            $vocabulary = taxonomy_vocabulary_load($vocabulary_id);
            if ($vocabulary->relations) {
              if (isset($nat_terms[$vocabulary_id])) {
                $default = array_keys(taxonomy_get_related($nat_terms[$vocabulary_id]));
                $exclude = array(
                  $nat_terms[$vocabulary_id],
                );
              }
              else {
                $default = $exclude = array();
              }
              $form['nat']['related'][$vocabulary_id] = _taxonomy_term_select(t('Related @terms', array(
                '@terms' => $vocabulary->name,
              )), 'relations', $default, $vocabulary_id, NULL, 1, t('<none>'), $exclude);
            }
          }

          // Synonyms: It is assumed that the synonyms for NAT terms in
          // different vocabularies are the same.
          if (!empty($nat_terms)) {
            $tid = array_pop($nat_terms);
            $default = implode("\n", taxonomy_get_synonyms($tid));
          }
          else {
            $default = '';
          }
          $form['nat']['synonyms'] = array(
            '#type' => 'textarea',
            '#title' => t('Synonyms'),
            '#default_value' => $default,
            '#description' => t('Synonyms of this term; one synonym per line.'),
          );
        }

        // This can only match once, so we just break the foreach.
        break;
      }
    }
  }
}

/**
 * Implementation of hook_link_alter().
 */
function nat_link_alter(&$links, $node) {
  $nat_config = _nat_variable_get();
  if (isset($nat_config['node_links'][$node->type])) {
    foreach ($links as $module => $link) {

      // Extract the term ID from the module indicator.
      $tid = str_replace('taxonomy_term_', '', $module, $count);

      // $link['title'] will be empty during node previews at which point
      // taxonomy links do not work.
      if ($count && $link['title']) {
        $nids = array_keys(nat_get_nids(array(
          $tid,
        ), FALSE));
        if (!empty($nids)) {

          // Link back to the NAT node and not the taxonomy term page.
          $links[$module]['href'] = "node/{$nids[0]}";
        }
      }
    }
  }
}

/**
 * Implementation of hook_views_api().
 *
 * This one is used as the base to reduce errors when updating.
 */
function nat_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'nat') . '/includes',
  );
}

/**
 * Gets terms associated with a node.
 *
 * @param $nid
 *   The nid of the node whose NAT terms are to be retrieved.
 * @return $return
 *   An associative array of NAT-associated term objects.
 */
function nat_get_terms($nid) {
  static $term_cache = NULL;
  if (isset($term_cache[$nid])) {
    return $term_cache[$nid];
  }
  $return = array();
  $result = db_query("SELECT td.* FROM {nat} n INNER JOIN {term_data} td USING (tid) WHERE n.nid = %d", $nid);
  while ($term = db_fetch_object($result)) {
    $return[$term->tid] = $term;
  }

  // Cache result.
  $term_cache[$nid] = $return;
  return $return;
}

/**
 * Retrieve the NAT terms associated with a node restricted by vocabulary.
 *
 * @param $nid
 *   The node ID of the node whose NAT-terms are to be retrieved.
 * @param $vocabularies
 *   An array of vocabulary IDs used to optionally retrict the retrieved terms
 * to a defined set of vocabularies.
 */
function nat_get_terms_by_vocabulary($nid, $vocabularies = array()) {
  $terms = nat_get_terms($nid);
  if (!empty($vocabularies)) {
    foreach ($terms as $tid => $term) {
      if (!in_array($term->vid, $vocabularies)) {
        unset($terms[$tid]);
      }
    }
  }
  return $terms;
}

/**
 * Retrieve the first / single NAT term associated with a node optionally
 * restricted by vocabulary.
 *
 * @param $nid
 *   The node ID of the node whose NAT-term is to be retrieved.
 * @param $vocabularies
 *   An optional array of vocabulary IDs used to optionally retrict the
 * retrieved term to a defined set of vocabularies.
 */
function nat_get_term($nid, $vocabularies = array()) {
  $terms = empty($vocabularies) ? nat_get_terms($nid) : nat_get_terms_by_vocabulary($nid, $vocabularies);
  return array_shift($terms);
}

/**
 * Retrieve all all the children of a node via its NAT association.
 * Note: taxonomy_select_nodes is a rather resource hungry function.
 *
 * @param $nid
 *   The node ID of the node whose child nodes are to be retrieved.
 * @param $types
 *   Unknown.
 * @param $vocabularies
 *   Retrict children to nodes categorised using provided vocabularies.
 * @return
 *   The resource identifier returned by taxonomy_select_nodes.
 */
function nat_get_children($nid, $types = array(), $vocabularies = array()) {
  $terms = nat_get_terms_by_vocabulary($nid, $vocabularies);
  $tids = array_keys($terms);
  return taxonomy_select_nodes($tids);
}

/**
 * Gets node IDs/nodes associated with a term.
 *
 * @param $tids
 *   An array of term IDs whose associated nodes are to be retrived.
 * @param $get_nodes
 *   A boolean indicating if node_load operations are to be performed on the
 * associated nodes.
 * @return $return
 *   An associative array of (nid => node) or (nid => title) depending on the
 * value of $get_nodes.
 */
function nat_get_nids($tids, $get_nodes = FALSE) {
  static $nid_cache = NULL;
  static $node_cache = NULL;
  $return = array();

  // Keep processing to a minimum for empty tid arrays.
  if (!empty($tids)) {

    // Sort tid array to ensure that the cache_string never suffers from order
    // issues.
    sort($tids);
    $cache_string = implode('+', $tids);
    if ($get_nodes) {
      if (isset($node_cache[$cache_string])) {
        return $node_cache[$cache_string];
      }
      elseif (isset($nid_cache[$cache_string])) {

        // If the nid cache stores the same string, node_load() each nid and
        // return them.
        $return = array();
        foreach (array_keys($nid_cache[$cache_string]) as $nid) {
          $return[$nid] = node_load($nid);
        }
        $node_cache[$cache_string] = $return;
        return $return;
      }
    }
    else {
      if (isset($nid_cache[$cache_string])) {
        return $nid_cache[$cache_string];
      }
      elseif (isset($node_cache[$cache_string])) {

        // If the node cache stores the same string, retrieve only the nids and
        // return them.
        foreach ($node_cache[$cache_string] as $nid => $node) {
          $return[$nid] = $node->name;
        }

        // Cache extracted results.
        $nid_cache[$cache_string] = $return;
        return $return;
      }
    }

    // Results have not been cached.
    $result = db_query("SELECT n.nid, t.name FROM {nat} n INNER JOIN {term_data} t USING (tid) WHERE n.tid IN (" . db_placeholders($tids) . ")", $tids);
    while ($node = db_fetch_object($result)) {
      if ($get_nodes) {
        $return[$node->nid] = node_load($node->nid);
      }
      else {
        $return[$node->nid] = $node->name;
      }
    }
    if ($get_nodes) {
      $node_cache[$cache_string] = $return;
    }
    else {
      $nid_cache[$cache_string] = $return;
    }
  }
  return $return;
}

/**
 * Update the NAT config to include node->vocabulary associations and related
 * settings. Commonly used in .install files to register associations and save
 * the admin some work.
 *
 * @param $type
 *   The node type.
 * @param $vids
 *   Array of vocabulary IDs that the above node type is to be associated with
 * via NAT.
 * @param $delete
 *   Boolean to indicate if associated term should be deleted when a node is
 * deleted.
 * @param $links
 *   Boolean to indicate if links to NAT terms should point to the associated
 * nodes instead.
 * @param $body
 *   Boolean to indicated if the node body should be in sync with the term
 * description field.
 * @param $related
 *   Boolean to indicate if related terms and synonyms can be set during node
 * creation.
 */
function nat_set_config($type, $vids, $delete = TRUE, $links = TRUE, $body = FALSE, $related = FALSE) {
  $nat_config = _nat_variable_get();
  if (!isset($nat_config['types'][$type])) {
    $nat_config['types'][$type] = array();
  }
  foreach ($vids as $vid) {
    $nat_config['types'][$type][$vid] = $vid;
  }
  if ($delete) {
    $nat_config['delete'][$type] = TRUE;
  }
  if ($links) {
    $nat_config['links'][$type] = TRUE;
  }
  if ($body) {
    $nat_config['body'][$type] = TRUE;
  }
  if ($related) {
    $nat_config['related'][$type] = TRUE;
  }
  variable_set('nat_config', $nat_config);
}

/**
 * Retrieve all vocabularies.
 *
 * @return $vocabularies
 *   An associative array of vocabulary IDs to vocabulary names.
 */
function _nat_get_vocabularies() {
  $vocabularies = taxonomy_get_vocabularies();
  foreach ($vocabularies as $id => $vocabulary) {
    $vocabularies[$id] = check_plain($vocabulary->name);
  }
  return $vocabularies;
}

/**
 * Add node titles as terms into the taxonomy system.
 * @todo Ideas are welcome to allow retaining the hierarchy for vocabularies not
 * present in the node form.
 *
 * @param Object $node
 *   The node object to associate and update.
 * @param Array $vids
 *   An array of vocabulary IDs to restrict associations to; useful for
 * operations such as NAT sync ...
 *
 * @return Array $tids
 *   An array of term objects.
 */
function _nat_add_terms($node, $vids = array()) {
  $nat_config = _nat_variable_get();
  $edit = array(
    'name' => $node->title,
    'description' => isset($nat_config['body'][$node->type]) ? $node->body : '',
    'weight' => 0,
  );
  $tids = array();
  $hierarchy = isset($node->taxonomy) ? $node->taxonomy : array();
  $vids = empty($vids) ? $nat_config['types'][$node->type] : $vids;
  foreach ($vids as $vid) {

    // $edit is passed by reference and 'tid' is set with the tid of the new
    // term.
    unset($edit['tid']);
    $edit['vid'] = $vid;

    // Save hierarchy for vocabularies also present in the node form.
    if (isset($hierarchy[$vid])) {
      $edit['parent'] = $hierarchy[$vid];
    }
    else {
      $edit['parent'] = array();
    }
    $edit['relations'] = isset($node->nat) && isset($node->nat['related'][$vid]) ? $node->nat['related'][$vid] : array();
    $edit['synonyms'] = isset($node->nat) ? $node->nat['synonyms'] : '';
    taxonomy_save_term($edit);
    $tids[] = $edit;
  }
  return $tids;
}

/**
 * Update saved node-terms.
 *
 * @param Object $node
 *   The node object to associate and update.
 */
function _nat_update_terms($node) {
  $nat_config = _nat_variable_get();
  $edit = array(
    'name' => $node->title,
  );
  $hierarchy = isset($node->taxonomy) ? $node->taxonomy : array();
  $terms = nat_get_terms($node->nid);
  foreach ($terms as $term) {
    $edit['tid'] = $term->tid;
    $edit['vid'] = $term->vid;
    $edit['description'] = isset($nat_config['body'][$node->type]) ? $node->body : $term->description;
    $edit['weight'] = $term->weight;
    $edit['parent'] = isset($hierarchy[$term->vid]) ? $hierarchy[$term->vid] : array();

    // If $node->nat is set, then so is $nat_config['related'][$node->type].
    if (isset($node->nat)) {
      $edit['relations'] = isset($node->nat['related'][$term->vid]) ? $node->nat['related'][$term->vid] : array();
      $edit['synonyms'] = $node->nat['synonyms'];
    }
    taxonomy_save_term($edit);
  }
}

/**
 * Delete associated terms from the taxonomy system.
 * @todo Options to delete child nodes as well etc.
 *
 * @param $nid
 *   Node ID of the node whose NAT-terms are to be deleted.
 */
function _nat_delete_terms($nid) {
  $terms = nat_get_terms($nid);
  foreach ($terms as $term) {
    taxonomy_del_term($term->tid);
  }
}

/**
 * Save node-term associations in the NAT table.
 *
 * @param Integer $nid
 *   Node ID of the node.
 * @param Array $terms
 *   NAT-term objects of the above node.
 */
function _nat_save_association($nid, $terms) {
  foreach ($terms as $term) {
    $term['nid'] = $nid;
    drupal_write_record('nat', $term);
  }
}

/**
 * Delete node-term associations from the NAT table.
 *
 * @param $nid
 *   Node ID of the node which is to be removed from the NAT table.
 */
function _nat_delete_association($nid) {
  db_query("DELETE FROM {nat} WHERE nid = %d", $nid);
}

/**
 * Return a NAT module variable.
 *
 * @param $name
 *   The name of the variable to retrieve.
 * @return
 *   The value of the variable requested.
 */
function _nat_variable_get($name = NULL) {
  static $variables = array();
  if (empty($variables)) {
    $defaults = array(
      'types' => array(),
      'body' => array(),
      'delete' => array(),
      'related' => array(),
      'node_links' => array(),
    );
    $variables = variable_get('nat_config', array());
    $variables = array_merge($defaults, $variables);
  }
  return $name ? $variables[$name] : $variables;
}

/**
 * Given a taxonomy term, recursively list all its children.
 *
 * @param $tid
 *   Term ID.
 * @param $vid
 *   Vocabulary ID.
 * @return
 *   An array of term IDs including the parent term.
 */
function _nat_taxonomy_term_children($tid, $vid) {
  $tids = array(
    $tid,
  );
  $terms = taxonomy_get_children($tid, $vid);
  foreach ($terms as $term) {
    $tids = array_merge($tids, _nat_taxonomy_term_children($term->tid, $vid));
  }
  return $tids;
}

Functions

Namesort descending Description
nat_form_alter Implementation of hook_form_alter().
nat_get_children Retrieve all all the children of a node via its NAT association. Note: taxonomy_select_nodes is a rather resource hungry function.
nat_get_nids Gets node IDs/nodes associated with a term.
nat_get_term Retrieve the first / single NAT term associated with a node optionally restricted by vocabulary.
nat_get_terms Gets terms associated with a node.
nat_get_terms_by_vocabulary Retrieve the NAT terms associated with a node restricted by vocabulary.
nat_help Implementation of hook_help().
nat_link_alter Implementation of hook_link_alter().
nat_menu Implementation of hook_menu().
nat_nodeapi Implementation of hook_nodeapi().
nat_perm Implementation of hook_perm().
nat_set_config Update the NAT config to include node->vocabulary associations and related settings. Commonly used in .install files to register associations and save the admin some work.
nat_views_api Implementation of hook_views_api().
_nat_add_terms Add node titles as terms into the taxonomy system. @todo Ideas are welcome to allow retaining the hierarchy for vocabularies not present in the node form.
_nat_delete_association Delete node-term associations from the NAT table.
_nat_delete_terms Delete associated terms from the taxonomy system. @todo Options to delete child nodes as well etc.
_nat_get_vocabularies Retrieve all vocabularies.
_nat_save_association Save node-term associations in the NAT table.
_nat_taxonomy_term_children Given a taxonomy term, recursively list all its children.
_nat_update_terms Update saved node-terms.
_nat_variable_get Return a NAT module variable.