I know that there are some modules which allows to create taxonomy terms based on node info; such as NAT - based on node title and Content Taxonomy - based on words entered to specific taxonomy fields. However, what i'm looking for is a system which will assign existing terms to existing nodes based on some IF-THEN logic.

Let's say i have a CCK content type C, with several integer fields F1, F2 and F3, each ranging from 0 to 100. I have also a vocabulary for content type C with terms T1, T2, and T3. And finally, there are already existing C type nodes N1, N2, N3. Now, what i want to make is to assign terms to existing nodes based on F1, F2 and F3 values. For example;

For all nodes
--- if F1>70, assign T1 to Node
--- if F2>70, assign T2 to Node
--- if F3>70, assign T3 to Node

In the real world, there may be some situations where this functionality will become very handy. Let's say you have a product db with thousands of products and each product has a price field. You want to group products into some price ranges such as "lower than 1euro, 1-100 euro, 100-1000 euro and 1000+". Assigning these terms manually would take a lot of time.

First, i want to know if this kind of functionality is possible using the existing modules. And if it is not, then how can this be accomplished?

Comments

drupaloSa’s picture

After a few hours of work, i was able to achieve this functionality. I'm neither a php/mysql expert nor an experienced Drupaller, so the code is possibly ugly and not very flexible. Nevertheless, I'll share the code, in case someone might want to work on it and develop a more flexible module. In fact, that would be a great module especially when coupled with the Faceted Search module.

First, i want to explain what this module does and where you can use it.

- You have a custom CCK content type with contains some integer CCK fields.
- You also have a vocabulary of terms for this content type, which you want to assign to nodes based on those CCK field values.
- You want to assign a term to a node if a specified CCK field has a value greater than a given threshold value for that term.

So basically you have:

Integer CCK fields: F1, F2, F3,
Terms: T1, T2, T3,
Threshold Values: V1, V2, V3,
Relations: (T1,F1, V1), (T2, F2, V2), (T3,F3,V3)

And you want to assign terms as follows:

FOR ALL NODES
--- IF Node's F1 Value >= V1 THEN ASSIGN T1 to this node
--- IF Node's F2 Value >= V2 THEN ASSIGN T2 to this node
--- IF Node's F3 Value >= V3 THEN ASSIGN T3 to this node

So this module takes all those information and assigns terms to nodes. Since the code isn't very flexible, you have to edit the module file for each vocabulary and content type. The user interface is very basic; there are two options in the module menu: one for deleting all the term assigments of the given vocabulary (don't forget to backup your db!), and the other for term assignment. You have to enter all the required data directly to the module file. And also don't forget to add a menu link for the autoterm page, since it doesn't add one automatically.

And here's my ugly code :)

<?php
/* $Id$ */

/**
* @file
* AutoTerm Module (not even beta!)
* Function: Automatically assigns a term to a node if the CCK field value is greater than a given threshold
*/

/**
* Implementation of hook_help().
*/
function autoterm_help($section) {
  switch ($section) {
    case 'admin/help#autoterm':
      $output = '<p>AutoTerm help...</p>';
      return $output;
    case 'admin/modules#description':
      return 'AutoTerm module description...';
  }
}

/**
 * Implementation of hook_perm().
 */
function autoterm_perm() {
  return array('Run autoterm script');
}

/**
* Implementation of hook_menu().
*/
function autoterm_menu($may_cache) {
  $items = array();

  if ($may_cache) {
  }
  else {
    $items[] = array(
      'path' => 'autoterm', // drupal path example.com/?q=autoterm
      'title' => 'AutoTerm page', // page title
      'callback' => 'autoterm_page', // callback function name
      'access' => user_access('Run autoterm script'),
      'type' => MENU_CALLBACK // define type of menu item as callback
    );
  }

  return $items;
}


/**
* Function which generate page (this generate any content - you need only your own code...)
*/
function autoterm_page() {
  return drupal_get_form('autoterm_form');
}

function autoterm_form (){

  $options = array('0' => t('Delete term-CCK field relations (all related records will be deleted from term_node table!)'),
   '1' => t('Assign terms based on CCK fields'));

  $form['autoterm_menu'] = array(
          '#type' => 'radios',
          '#title' => t('AutoTerm Menu Options'),
          '#default_value' => 0,
          '#options' => $options,
  );
  
  $form['submit'] = array('#type' => 'submit', '#value' => t('Process Nodes'));
  
  return $form;
}

function autoterm_form_submit($form_id, $form_values) {

  $vid = 1; //enter the id of the vocabulary which the terms belong to
  $ccktype = "mycckcontenttype"; //enter your custom CCK content type
  
  $fieldlist = array("field_f1_value", "field_f2_value","field_f3_value",
    "field_f4_value");
  /* $fieldlist contains the CCK fields that will be used to determine the term assignments.
      Enter your own CCK field names here.
  */ 
  $termlist = array(1,4,3,2);
  /* $termlist contains the term ids. Ids should be ordered with respect to $fieldlist. For example, if you want
     to assign TERM-19 according to the value of FIELD-A and TERM-7 according to the value of FIELD-B then you should
     write them as follows: $fieldlist = array("FIELD-A", "FIELD-B") and $termlist = array(19,7)
   
  $threshold = array(15,30,5,50);  
  /*$threshold contains the lowest possible values for each field for assigning the matching terms.
    Enter your threshold values here.
  */
  $listlength = count($fieldlist);
  
  switch ($form_values['autoterm_menu']) {

      //Delete all terms assignments for this vocabulary
      case '0':   
        $query = "DELETE FROM {term_node} WHERE tid in (SELECT tid FROM {term_data} WHERE vid=%d)";
        db_query($query,$vid);
        drupal_set_message(t('Term assignments are deleted!'));        
        break;
      //Assign terms  
      case '1':  
        $fields = "node.nid";
        foreach ($fieldlist as $value)
        {
          $fields .= ",".$value;
        }
        $query = "SELECT ".$fields." FROM {content_type_".$ccktype."} AS ctype, {node} ".
          "WHERE ctype.vid = node.vid ORDER BY node.nid, node.vid DESC";          
        $result = db_query($query);  
        $counter=0;
        while ($row = db_fetch_array($result)) {                
          for ($i=0; $i<$listlength; $i++)
          {            
            if ($row[$fieldlist[$i]]>=$threshold[$i]) {
              //assign term if the CCK field value is greater than the given threshold for this term
              db_query("INSERT INTO {term_node} VALUES (%d,%d)",$row['nid'],$termlist[$i]);
              $counter++;
            }
          }
        }
        if ($counter>0) drupal_set_message(t('Terms are assigned to nodes!'));  
        else drupal_set_message(t('No matches: Operation aborted!'));  
        break;
  }  
}

?>

Installation

1. Copy/paste this code as autoterm.module, and also add the following code as autoterm.info file.
2. Create a folder named "autoterm" and put both files in this directory and move it to the drupal modules directory.
3. Enable the module in admin->modules.
4. Go to Access Control and set permissions as you want.
5. Open autoterm.module file with a text-editor and enter your own data.
6. Go to "yoursite.com/autoterm" page, select "Assign terms based on CCK fields" and click "Process Nodes".

; $Id: autoterm.info,v 1.0 2008/02/09 18:55:34 oSa Exp $
name = AutoTerm
description = Add existing terms to existing nodes based on cck fields.
package = Other
version = "5.7"
summit’s picture

Would be great if this becomes a module on drupal.org! with the flexibility and UI. greetings, Martijn

rosenhauer’s picture

I'm working on something like this (but not for CCK) that will be fully configurable from within Drupal. Mine updates the taxonomy during the forms submission (add or update). One of the things I'm missing is what to do if they change the cutoffs. But I think using your code will get me so I can then update all of the nodes at once.

I'll get the code out as soon as I integrate your code.

Dave

rosenhauer’s picture

Here it is.

README.txt

Taxonomy terms by range

by Dave Rosenhauer, dave@dmrenterprises.com


This module allows you to gefine taxonomy categories and terms to be auto filled in based on numeric (or alpha?) ranges.

Setup:
1. Install the module.
2. Create a Category (administer->categories)
3. In the "Help Text:" field start the field with "AUTOFILL:" for example "AUTOFILL:Pricing Group" 
4. Add terms to the Category.
5. In the description field enter the range information in the following format:
  RANGE:<field_name>:<min_value>:<max_value> 
  <field_name> = the field name as it's refered to in the $node element.
  <min_value> = the lowest value to be in the range (uses field > min_value)
  <max_value> = the lowest value to be considered above the range (uses field < <max_value)
  for example 
    RANGE:price:0:50000
  This allows for adjacent ranges to use the same endpoints. you can setup several ranges (in seperate terms) as follows:
    RANGE:price:0:50000 >>> True if >= 0 and < 50000
    RANGE:price:50000:100000 >>> True if >= 50000 and < 100000
        
How it works:
Whenever a form is created the module checks if any Taxonomy categories are included. If one is included it then checks to see if the category has the "AUTOFILL:" in the help text.  If it finds it, it then removes the entry from the form.  Then during the form save (or update) it checks the values and assigns the proper term(s).

What if I need to change the ranges?  
In the Administration/Site Configuration menu select Taxonomy By Range.  If you check the "Force Update" box then submit it will scan for all nodes in the system that have AutoFill Taxonomy categories and update all of the terms (may take a while on large sites).  This is also helpful for adding new terms/categories.  Just Force an update and all current content is up to date.

taxbyrange.info

name = taxbyrange
dependencies  = taxonomy
description = "Set taxonomy terms by field value ranges"
package = "DMR Enterprises"

taxbyrange.module

<?php

/*
*******************************************************************************
** Update the terms based on the description field 
*******************************************************************************
*/
function taxbyrange_node_save($node) {  
  // Get the Taxonomy terms for this node
  $vocabularies = taxonomy_get_vocabularies($node->type);
  foreach ($vocabularies as $vocabulary) {
    $tree = taxonomy_get_tree($vocabulary->vid);
    foreach ($tree as $term) {

      if ($term->description) { // We have a description
        if (substr($term->description,0,6) == 'RANGE:') { //does the description start with 'RANGE'
          $params = explode(':',$term->description);  
          // $params[0] = 'RANGE' identifier
          // $params[1] = field name
          // $params[2] = min value (will use >=)
          // $params[3] = max value (will use <)
          $clearterm = false;
                    
          if ($node->$params[1]) {
            // The field in question has a value
            $fieldval = $node->$params[1];
             
            if (($fieldval >= $params[2]) && ($fieldval < $params[3])) {
              // Update the Taxonomy value
              // Check if it is already marked
              $query = "SELECT * FROM {term_node} WHERE nid = %d and tid = %d;"; 
              if (!db_result(db_query($query,$node->nid,$term->tid))) {          
                db_query("INSERT INTO {term_node} (nid,tid) VALUES (%d,%d)",$node->nid,$term->tid);
              }
            } else {
              // Have value and not in range so clear the term
              $clearterm = true;
            }
          } else {
            // Don't have value so clear term
            $clearterm = true;
           } 
          if ($clearterm == true) {
            // Delete the current term
            $query = "DELETE FROM {term_node} WHERE nid = %d and tid = %d;";  
            db_query($query,$node->nid,$term->tid);
          }
        }
      }
    }     
  }
}


/**
 * Implementation of hook_form_alter().
 * Generate a form for selecting terms to associate with a node.
 */
function taxbyrange_form_alter($form_id, &$form) {
  if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
    $node = $form['#node'];

    // Check if any taxonomy fields are present
    if (is_array($form['taxonomy']) && !empty($form['taxonomy'])) {
      foreach ($form['taxonomy'] as $vid => $term) {
        if (substr($term['#description'],0,9) == 'AUTOFILL:') { //does the description start with 'AUTOFILL'
          // Remove the item as it will be auto filled
          unset($form['taxonomy'][$vid]);
        }  
      }
    }
  }
}


function taxbyrange_nodeapi($node, $op, $arg = 0) {
  switch ($op) {
    case 'insert':
      taxbyrange_node_save($node);
      break;
    case 'update':
      taxbyrange_node_save($node);
      break;
  }
}

function taxbyrange_admin_settings() {
  $form['queries'] = array('#type' => 'fieldset', '#title' => t('Update all Autolilled taxonomy fields'));
  
  $form['queries']['taxbyrange_update'] = array('#type' => 'checkbox',
    '#title' => t('Force Update'),
    '#default_value' => 0,
    '#description' => t("You should do this after changing the ranges for auto fill fields."),
  );
  
  return system_settings_form($form);
}

function taxbyrange_admin_settings_submit($form_id, $form_values) {
  if ($form_values['taxbyrange_update']){
    // get all of the autofill categories
    $vocabularies = taxonomy_get_vocabularies();
    $nodetypes = array();
  
    foreach ($vocabularies as $id => $vocabulary) {
      if (substr($vocabulary->help,0,9) == 'AUTOFILL:') { //does the description start with 'AUTOFILL' 
        // Add each node type that uses this vocab to the array (without dupes)
        foreach ($vocabulary->nodes as $node) {
          $nodetypes[$node] = $node;
        }  
      }  
    }
    
    // Now we have a list of the node types to process
    foreach ($nodetypes as $nodetype) {
      // Get the nodes for that type
      $query = "SELECT * FROM {node} WHERE type = '%s';"; 
              
      $result = db_query($query,$nodetype);
      while ($row = db_fetch_object($result)) {
        $tempnode = node_load($row->nid); 
        taxbyrange_node_save($tempnode);   
      }
    }
    
    form_set_error('form_token', t('Finished Updating all of the AutoFill taxonomy terms.'));
  }
}

/**
 * Implementation of hook_menu.
 */
function taxbyrange_menu($may_cache) {
  $items = array();
  if ($may_cache) {
   $items[] = array(
      'path' => 'admin/settings/taxbyrange',
      'title' => t('Taxonomy By Range'),
      'description' => t('Update Autofilled Taxonomy fields.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('taxbyrange_admin_settings'),
      'access' => user_access('administer site configuration'),
      'type' => MENU_NORMAL_ITEM
    );
  }
  return $items;
}

?>

I actually didn't end up using your code.

Dave

drupaloSa’s picture

Hi Dave, thanks for the great work. Will you provide this as a module for Drupal (i mean in the modules directory)?

rosenhauer’s picture

Since there seems to be some interest I'll try and get it posted as a module.

Dave

drupaloSa’s picture

This is great, i'm looking forward to using it in my next project :)

momper’s picture

great + thanks

momper

notarealperson’s picture

Has anyone been able to get this working? I implemented as directed but it doesn't seem to be assigning the taxonomy terms.

notarealperson’s picture

sorry, double post.

rosenhauer’s picture

Change the part that has:
// The field in question has a value
$fieldval = $node->$params[1];

to be

// The field in question has a value
if (!is_array($node->$params[1])) {
// Non CCK the value is held directly in the node element
$fieldval = $node->$params[1];
} else {
// It's an array so assume CCK
// Get the field element
$fieldElement = $node->$params[1];
// Get the value
// CCK stores the value in an array along with a display version of it - we just want the value
$fieldval = $fieldElement[0]['value'];
}

You can all thank jimi089 for getting me to update it for his project.

Dave

dschneider’s picture

Hi,

I really like that functionality. Did you manage to get it in a Drupal module? Is it D6 compatible?

cheers, dom

farald’s picture

This looks very interesting, I would very much welcome such functionality in drupal 6.