You are here

lineage.module in Taxonomy Lineage 7

Same filename and directory in other branches
  1. 5 lineage.module
  2. 6 lineage.module

lineage.module Module code for taxonomy term hierarchy lineage.

File

lineage.module
View source
<?php

/**
 * @file lineage.module
 * Module code for taxonomy term hierarchy lineage.
 */

/**
 * Define a base starting depth for nesting headers.
 * We use 3 (for <h3> tags) because <h1> and <h2> are generally page titles.
 */
define('LINEAGE_START_DEPTH', 3);

/**
 * Implements hook_taxonomy_term_delete().
 */
function lineage_taxonomy_term_delete($term) {
  db_delete('taxonomy_term_lineage')
    ->condition('tid', $term->tid)
    ->execute();
}

/**
 * Implements hook_taxonomy_term_update().
 */
function lineage_taxonomy_term_update($term) {
  if (arg() != array(
    'admin',
    'structure',
    'taxonomy',
    $term->vocabulary_machine_name,
  )) {
    lineage_update_term($term);
  }
}

/**
 * Implements hook_taxonomy_term_insert().
 */
function lineage_taxonomy_term_insert($term) {
  if (arg() != array(
    'admin',
    'structure',
    'taxonomy',
    $term->vocabulary_machine_name,
  )) {
    lineage_update_term($term);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function lineage_form_taxonomy_overview_terms_alter(&$form, &$form_state) {

  // Override submit handler.
  $form['#submit'] = array(
    'lineage_taxonomy_overview_terms_submit',
  );
}

/**
 * Submit handler for taxonomy_overview_terms (the admin term reordering form).
 * Overrides handling so that we can update lineages after the new term
 * weights are processed.
 * This function is mostly a clone of taxonomy_overview_terms_submit, with
 * the addition of lineage_update_all calls in the appropriate places.
 */
function lineage_taxonomy_overview_terms_submit(&$form, &$form_state) {
  if ($form_state['triggering_element']['#value'] == t('Reset to alphabetical')) {

    // Execute the reset action.
    if ($form_state['values']['reset_alphabetical'] === TRUE) {
      taxonomy_vocabulary_confirm_reset_alphabetical_submit($form, $form_state);

      // Update lineages.
      lineage_update_all($form_state['values']['vid']);
      return;
    }

    // Rebuild the form to confirm the reset action.
    $form_state['rebuild'] = TRUE;
    $form_state['confirm_reset_alphabetical'] = TRUE;
    return;
  }

  // Sort term order based on weight.
  uasort($form_state['values'], 'drupal_sort_weight');
  $vocabulary = $form['#vocabulary'];
  $hierarchy = 0;

  // Update the current hierarchy type as we go.
  $changed_terms = array();
  $tree = taxonomy_get_tree($vocabulary->vid);
  if (empty($tree)) {
    return;
  }

  // Build a list of all terms that need to be updated on previous pages.
  $weight = 0;
  $term = (array) $tree[0];
  while ($term['tid'] != $form['#first_tid']) {
    if ($term['parents'][0] == 0 && $term['weight'] != $weight) {
      $term['parent'] = $term['parents'][0];
      $term['weight'] = $weight;
      $changed_terms[$term['tid']] = $term;
    }
    $weight++;
    $hierarchy = $term['parents'][0] != 0 ? 1 : $hierarchy;
    $term = (array) $tree[$weight];
  }

  // Renumber the current page weights and assign any new parents.
  $level_weights = array();
  foreach ($form_state['values'] as $tid => $values) {
    if (isset($form[$tid]['#term'])) {
      $term = $form[$tid]['#term'];

      // Give terms at the root level a weight in sequence with terms on previous pages.
      if ($values['parent'] == 0 && $term['weight'] != $weight) {
        $term['weight'] = $weight;
        $changed_terms[$term['tid']] = $term;
      }
      elseif ($values['parent'] > 0) {
        $level_weights[$values['parent']] = isset($level_weights[$values['parent']]) ? $level_weights[$values['parent']] + 1 : 0;
        if ($level_weights[$values['parent']] != $term['weight']) {
          $term['weight'] = $level_weights[$values['parent']];
          $changed_terms[$term['tid']] = $term;
        }
      }

      // Update any changed parents.
      if ($values['parent'] != $term['parent']) {
        $term['parent'] = $values['parent'];
        $changed_terms[$term['tid']] = $term;
      }
      $hierarchy = $term['parent'] != 0 ? 1 : $hierarchy;
      $weight++;
    }
  }

  // Build a list of all terms that need to be updated on following pages.
  for ($weight; $weight < count($tree); $weight++) {
    $term = (array) $tree[$weight];
    if ($term['parents'][0] == 0 && $term['weight'] != $weight) {
      $term['parent'] = $term['parents'][0];
      $term['weight'] = $weight;
      $changed_terms[$term['tid']] = $term;
    }
    $hierarchy = $term['parents'][0] != 0 ? 1 : $hierarchy;
  }

  // Save all updated terms.
  foreach ($changed_terms as $changed) {
    $term = (object) $changed;

    // Update term_hierachy and term_data directly since we don't have a
    // fully populated term object to save.
    db_update('taxonomy_term_hierarchy')
      ->fields(array(
      'parent' => $term->parent,
    ))
      ->condition('tid', $term->tid, '=')
      ->execute();
    db_update('taxonomy_term_data')
      ->fields(array(
      'weight' => $term->weight,
    ))
      ->condition('tid', $term->tid, '=')
      ->execute();
  }

  // Update the vocabulary hierarchy to flat or single hierarchy.
  if ($vocabulary->hierarchy != $hierarchy) {
    $vocabulary->hierarchy = $hierarchy;
    taxonomy_vocabulary_save($vocabulary);
  }
  drupal_set_message(t('The configuration options have been saved.'));

  // Rebuild lineages.
  lineage_update_all($vocabulary->vid);
}
function lineage_enable() {
  drupal_set_message(t("Updated @number taxonomy lineages.", array(
    '@number' => lineage_update_all(),
  )));
}

/**
 * Updates all lineage records.
 * @param $vid=false If passed, limit to records for only one vocabulary.
 * @return $count The number of records updated.
 */
function lineage_update_all($vid = FALSE) {
  $query = db_select('taxonomy_term_data', 'td');
  $query
    ->join('taxonomy_term_hierarchy', 'th', 'th.tid = td.tid');
  $query
    ->fields('td', array(
    'tid',
    'name',
    'weight',
    'vid',
  ));
  $query
    ->fields('th', array(
    'parent',
  ));
  $query
    ->condition('th.parent', 0);

  // If we are only updating one vocabulary, select only those terms.
  if ($vid) {
    $query
      ->condition('td.vid', $vid);
  }
  $result = $query
    ->execute();
  $count = 0;
  foreach ($result as $term) {
    $count += lineage_update_term($term, TRUE);
  }
  return $count;
}
function lineage_update_term($term, $all = FALSE) {

  // If we are in the middle of updating all lineages, skip this.
  if (!$all) {

    // Select lineage for a different term in this vocab arbitrarily.
    $query = db_select('taxonomy_term_data', 'td');
    $query
      ->join('taxonomy_term_lineage', 'tl', 'tl.tid = td.tid');
    $query
      ->join('taxonomy_term_hierarchy', 'th', 'td.tid = th.tid');
    $query
      ->fields('tl', array(
      'lineage',
    ));
    $query
      ->fields('td', array(
      'tid',
      'name',
      'weight',
      'vid',
    ));
    $query
      ->fields('th', array(
      'parent',
    ));
    $query
      ->condition('td.vid', $term->vid, '=');
    $query
      ->condition('td.tid', $term->tid, '<>');
    $query
      ->range(0, 1);
    $other_term = $query
      ->execute()
      ->fetchObject();

    // If the lineage string doesn't match what we think it should be,
    // an update is required.
    if ($other_term->lineage != lineage_string($other_term)) {
      $count = lineage_update_all($term->vid);
      return $count;
    }
  }

  // Else, just update this term.
  $base = _lineage_get_parent_lineage($term->parent);
  return count(lineage_update_term_r($term, $base));
}
function lineage_update_term_r($term, $base, $tids = array()) {

  // Extend the base.
  $base['base'] .= lineage_string($term);

  // Table wants NO NULL, so just make sure.
  $base['depth'] = intval($base['depth']);

  // Update the hierarchy for the current tid.
  db_delete('taxonomy_term_lineage')
    ->condition('tid', $term->tid)
    ->execute();
  db_insert('taxonomy_term_lineage')
    ->fields(array(
    'tid' => $term->tid,
    'lineage' => trim($base['base']),
    'depth' => $base['depth'],
  ))
    ->execute();
  $base['depth']++;

  // Mark that we've done this one to prevent looping.
  $tids[$term->tid] = TRUE;

  // Update all the children.
  $query = db_select('taxonomy_term_hierarchy', 'th');
  $query
    ->join('taxonomy_term_data', 'td', 'td.tid = th.tid');
  $query
    ->fields('td', array(
    'tid',
    'name',
    'weight',
    'vid',
  ));
  $query
    ->condition('th.parent', $term->tid, '=');
  $result = $query
    ->execute();
  foreach ($result as $child) {

    // loop protection, just in case.
    if (!isset($tids[$child->tid])) {
      $tids = lineage_update_term_r($child, $base, $tids);
    }
  }
  return $tids;
}

/**
 * Ensure positive weights with enough digits so that all sort properly.
 *
 * @param $term
 *   A taxonomy term
 *
 * @return
 *   A term lineage string with weight.
 */
function lineage_string($term) {
  $w = _lineage_weights($term->vid);
  return sprintf("%0" . $w['digits'] . "d", $term->weight + $w['offset']) . "-" . $term->name . "\n";
}

/**
 * Strip the weight from the beginning of a lineage term string.
 *
 * @param $term_string
 *   One line of a lineage, with weight
 *
 * @return
 *   One line of a lineage, with the weight removed, or false on error.
 */
function lineage_strip_weight($term_string) {

  // Remove numbers 0-9 followed by a hyphen, at most once.
  $term_name = preg_replace("([0-9]+-)", "", $term_string, 1);
  if ($term_name != $term_string) {
    return $term_name;
  }
  else {

    // Passed string did not include a proper weight; set a warning.
    _lineage_format_warning();
    return FALSE;
  }
}

/**
 * Get the weight string from the beginning of a lineage term string.
 *
 * @param $term_string
 *   One line of a lineage, with weight.
 *
 * @return
 *   The weight from the term_string, or false on failure.
 */
function lineage_get_weight($term_string) {
  $matches = array();
  if (preg_match("([0-9]+-)", $term_string, $matches)) {

    // Return the matched weight pattern, minus the hyphen at the end.
    return substr($matches[0], 0, -1);
  }
  else {

    // Passed string did not include a proper weight; set a warning.
    _lineage_format_warning();
    return FALSE;
  }
}

/**
 * Recursive helper to assemble lineage.
 *
 * @param $id
 *   A taxonomy term id.
 *
 * @return
 *   An keyed array containg the following keys:
 *   - base
 *   - depth
 */
function _lineage_get_parent_lineage($tid) {
  $query = db_select('taxonomy_term_hierarchy', 'th');
  $query
    ->join('taxonomy_term_data', 'td', 'td.tid = th.tid');
  $query
    ->fields('td', array(
    'tid',
    'name',
    'weight',
    'vid',
  ));
  $query
    ->fields('th', array(
    'parent',
  ));
  $query
    ->condition('td.tid', $tid, '=');
  $result = $query
    ->execute();
  foreach ($result as $term) {
    $ret = _lineage_get_parent_lineage($term->parent);
    $ret['base'] .= lineage_string($term);
    $ret['depth'] += 1;
    return $ret;
  }

  // Return an empty set to suppress warnings in lineage_update_term_r().
  return array(
    'base' => '',
    'depth' => 0,
  );
}

/**
 * Implementation of hook_views_api().
 */
function lineage_views_api() {
  return array(
    'api' => 3,
  );
}

/**
 * Determine how many digits to use in weights.
 *
 * Use db_query() and not db_select() to keep the code small. Apparently
 * you can't chain addExpression() into a SelectQuery :-/
 *
 * @param vid
 *   The vocabulary vid.
 *
 * @return
 *   Array of min weight, max weight, offset to use, and # digits.
 */
function _lineage_weights($vid) {

  // Keep static array with weight info to prevent unneeded queries.
  static $weights = array();
  if (!isset($weights[$vid])) {

    // $weights[$vid]['min'] = db_select('taxonomy_term_data')->addExpression('MIN(weight)')->condition('vid', $vid, '=')->execute()->fetchField();
    $weights[$vid]['min'] = db_query('SELECT MIN(weight) FROM {taxonomy_term_data} WHERE vid = :vid', array(
      ':vid' => $vid,
    ))
      ->fetchField();
    $weights[$vid]['offset'] = $weights[$vid]['min'] < 0 ? abs($weights[$vid]['min']) : 0;

    // $weights[$vid]['max'] = db_select('taxonomy_term_data')->addExpression('MAX(weight)')->condition('vid', $vid, '=')->execute()->fetchField();
    $weights[$vid]['max'] = db_query('SELECT MAX(weight) FROM {taxonomy_term_data} WHERE vid = :vid', array(
      ':vid' => $vid,
    ))
      ->fetchField();
    $weights[$vid]['digits'] = floor(log($weights[$vid]['max'] + $weights[$vid]['offset'] + 1));
    if ($weights[$vid]['digits'] == 0) {
      $weights[$vid]['digits']++;
    }
  }
  return $weights[$vid];
}

/**
 * Displays a warning if appears that lineage strings are in the wrong format.
 */
function _lineage_format_warning() {
  drupal_set_message(t('Warning: your lineage data appears to be in an old format. Try disabling and re-enabling the module, or run update.php.'), 'warning', FALSE);
}

Functions

Namesort descending Description
lineage_enable
lineage_form_taxonomy_overview_terms_alter Implements hook_form_FORM_ID_alter().
lineage_get_weight Get the weight string from the beginning of a lineage term string.
lineage_string Ensure positive weights with enough digits so that all sort properly.
lineage_strip_weight Strip the weight from the beginning of a lineage term string.
lineage_taxonomy_overview_terms_submit Submit handler for taxonomy_overview_terms (the admin term reordering form). Overrides handling so that we can update lineages after the new term weights are processed. This function is mostly a clone of taxonomy_overview_terms_submit, with the…
lineage_taxonomy_term_delete Implements hook_taxonomy_term_delete().
lineage_taxonomy_term_insert Implements hook_taxonomy_term_insert().
lineage_taxonomy_term_update Implements hook_taxonomy_term_update().
lineage_update_all Updates all lineage records.
lineage_update_term
lineage_update_term_r
lineage_views_api Implementation of hook_views_api().
_lineage_format_warning Displays a warning if appears that lineage strings are in the wrong format.
_lineage_get_parent_lineage Recursive helper to assemble lineage.
_lineage_weights Determine how many digits to use in weights.

Constants

Namesort descending Description
LINEAGE_START_DEPTH Define a base starting depth for nesting headers. We use 3 (for <h3> tags) because <h1> and <h2> are generally page titles.