From 4f76fb5cd1fafb31404e0c39ac2130f306edfbba Mon Sep 17 00:00:00 2001
From: Paul Moers <info@i-freelance.be>
Date: Wed, 20 Feb 2013 18:11:07 +0000
Subject: [PATCH] Issue #1780576 by syngi: Preserve order during
 insert/update/delete

---
 nodeorder.admin.inc |    2 +-
 nodeorder.install   |    3 +-
 nodeorder.module    |  270 ++++++++++++++++++++++++++++++++++-----------------
 3 files changed, 184 insertions(+), 91 deletions(-)

diff --git a/nodeorder.admin.inc b/nodeorder.admin.inc
index 967503a..23b0d80 100644
--- a/nodeorder.admin.inc
+++ b/nodeorder.admin.inc
@@ -44,7 +44,7 @@ function nodeorder_admin_display_form($form, &$form_state, $tid) {
       '#type' => 'weight',
       '#title' => t('Weight for @title', array('@title' => $node->title)),
       '#title_display' => 'invisible',
-      '#delta' => 10,
+      '#delta' => $weight_delta,
       '#default_value' => $node->weight,
     );
   }
diff --git a/nodeorder.install b/nodeorder.install
index 90e8516..6189594 100644
--- a/nodeorder.install
+++ b/nodeorder.install
@@ -29,7 +29,6 @@ function nodeorder_install() {
   // Add the column to the table
   $ret = array();
   db_add_field('taxonomy_index', 'weight', $spec, $keys);
-  $installation_failed = FALSE;
 
   // Check for query errors
   for ($i = 0; $i < count($ret); $i++) {
@@ -105,4 +104,4 @@ function nodeorder_schema_alter(&$schema) {
     'initial' => 0,
     'description' => t('A user-defined weight for each node in its respective category.'),
   );
-}
+}
\ No newline at end of file
diff --git a/nodeorder.module b/nodeorder.module
index cbceeb6..1407f3c 100644
--- a/nodeorder.module
+++ b/nodeorder.module
@@ -90,7 +90,6 @@ function nodeorder_taxonomy_form_vocabulary_submit($form, &$form_state) {
         $query_max = db_select('taxonomy_index', 'ti')
           ->condition('tid', $tid);
         $query_max->addExpression('MAX(weight)', 'mweight');
-        $sql_update = "UPDATE {taxonomy_index} SET weight = %d WHERE tid = %d AND nid = %d";
 
         foreach ($tids as $i => $tid) {
           //select *current* nodes for the current term
@@ -108,9 +107,8 @@ function nodeorder_taxonomy_form_vocabulary_submit($form, &$form_state) {
         }
       }
 
-      // TODO Please review the conversion of this statement to the D7 database API syntax.
-      /* db_query("UPDATE {taxonomy_vocabulary} SET module = '%s' WHERE vid = %d", 'nodeorder', $vid) */
-      db_update('taxonomy_vocabulary')->fields(array('module' => 'nodeorder',))
+      db_update('taxonomy_vocabulary')
+        ->fields(array('module' => 'nodeorder'))
         ->condition('vid', $vid)
         ->execute();
 
@@ -122,9 +120,8 @@ function nodeorder_taxonomy_form_vocabulary_submit($form, &$form_state) {
       // Switching from orderable to non-orderable...
       cache_clear_all('nodeorder:', 'cache', TRUE);
 
-      // TODO Please review the conversion of this statement to the D7 database API syntax.
-      /* db_query("UPDATE {taxonomy_vocabulary} SET module = '%s' WHERE vid = %d", 'taxonomy', $vid) */
-      db_update('taxonomy_vocabulary')->fields(array('module' => 'taxonomy',))
+      db_update('taxonomy_vocabulary')
+        ->fields(array('module' => 'taxonomy'))
         ->condition('vid', $vid)
         ->execute();
 
@@ -138,8 +135,9 @@ function nodeorder_taxonomy_form_vocabulary_submit($form, &$form_state) {
         $tids[] = $term->tid;
       }
 
-      if (count($tids) > 0) {
-        db_update('taxonomy_index')->fields(array('weight' => 0))
+      if (count($tids)) {
+        db_update('taxonomy_index')
+          ->fields(array('weight' => 0))
           ->condition('tid', $tids, 'IN')
           ->execute();
       }
@@ -188,7 +186,7 @@ function nodeorder_add_link(&$links, $vocabularies, $node, $term) {
   $vocabulary = $vocabularies[$term->vid];
 
   if ($vocabulary->module == 'nodeorder') {
-    $weights = nodeorder_get_term_min_max($term->tid, FALSE);
+    $weights = nodeorder_get_term_min_max($term->tid);
     $weight = db_query("SELECT weight FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid", array(':nid' => $node->nid, ':tid' => $term->tid))->fetchField();
 
     if ($weight > $weights["min"]) {
@@ -215,26 +213,31 @@ function nodeorder_add_link(&$links, $vocabularies, $node, $term) {
  * @todo Please document this function.
  * @see http://drupal.org/node/1354
  */
-function nodeorder_get_term_min_max($tid, $reset) {
+function nodeorder_get_term_min_max($tid, $reset=FALSE) {
   static $min_weights = array();
   static $max_weights = array();
 
   if ($reset) {
-    unset($min_weights[$tid]);
-    unset($max_weights[$tid]);
+    $min_weights = array();
+    $max_weights = array();
   }
 
-  if (!$min_weights[$tid] || !$max_weights[$tid]) {
-    $result = db_query("SELECT tid, max(weight) as max_weight, min(weight) as min_weight FROM {taxonomy_index} WHERE tid = :tid GROUP BY tid", array(':tid' => $tid));
-
-    while ($row = db_fetch_object($result)) {
-      $min_weights[$row->tid] = $row->min_weight;
-      $max_weights[$row->tid] = $row->max_weight;
-    }
+  if (!isset($min_weights[$tid]) || !isset($max_weights[$tid])) {
+    $query = db_select('taxonomy_index', 'i')
+      ->fields('i', array('tid'))
+      ->condition('tid', $tid)
+      ->groupBy('tid');
+    $query->addExpression('MAX(weight)', 'max_weight');
+    $query->addExpression('MIN(weight)', 'min_weight');
+    $record = $query->execute()->fetch();
+
+    $min_weights[$tid] = $record->min_weight;
+    $max_weights[$tid] = $record->max_weight;
   }
 
   $weights["min"] = $min_weights[$tid];
   $weights["max"] = $max_weights[$tid];
+
   return $weights;
 }
 
@@ -495,7 +498,7 @@ function nodeorder_move_in_category($direction, $nid, $tid) {
   $weight = db_query("SELECT weight FROM {taxonomy_index} WHERE nid = :nid AND tid = :tid", array(':nid' => $node->nid, ':tid' => $tid))->fetchField();
 
   if ($up) {
-    $weights = nodeorder_get_term_min_max($tid, FALSE);
+    $weights = nodeorder_get_term_min_max($tid);
     if ($weight == $weights["min"]) {
       drupal_set_message(t('%title cannot be moved up as it already is at the top.', array('%title' => $node->title)), 'error');
       drupal_goto($destination);
@@ -506,7 +509,7 @@ function nodeorder_move_in_category($direction, $nid, $tid) {
     $direction = 'up';
   }
   else {
-    $weights = nodeorder_get_term_min_max($tid, FALSE);
+    $weights = nodeorder_get_term_min_max($tid);
     if ($weight == $weights["max"]) {
       drupal_set_message(t('%title cannot be moved down as it already is at the bottom.', array('%title' => $node->title)), 'error');
       drupal_goto($destination);
@@ -586,7 +589,7 @@ function nodeorder_can_be_ordered($node) {
     if (!empty($nodeorder_vocabularies)) {
       $result = db_select('taxonomy_vocabulary', 'v')
         ->condition('v.module', 'nodeorder')
-        ->condition('v.vid', $nodeorder_vocabularies, 'IN')
+        ->condition('v.machine_name', $nodeorder_vocabularies, 'IN')
         ->fields('v', array('vid'))
         ->execute()
         ->fetchColumn();
@@ -606,34 +609,60 @@ function nodeorder_can_be_ordered($node) {
 /**
  * Returns an array of the node's tids that are in orderable vocabularies...
  */
-function nodeorder_orderable_tids($node) {
+function nodeorder_orderable_tids($node, $reset=FALSE) {
   $tids = array();
   $orderable_tids = array();
   $cid = 'nodeorder:orderable_tids:' . $node->type;
 
-  if (($cache = cache_get($cid)) && !empty($cache->data)) {
+  if (!$reset && ($cache = cache_get($cid)) && !empty($cache->data)) {
     $orderable_tids = $cache->data;
   }
   else {
-    $sql = "SELECT v.vid AS vid FROM {taxonomy_vocabulary_node_type} vnt JOIN {taxonomy_vocabulary} v ON vnt.vid = v.vid WHERE vnt.type = '%s' AND v.module = 'nodeorder'";
-    $result = db_query("SELECT v.vid AS vid FROM {taxonomy_vocabulary_node_type} vnt JOIN {taxonomy_vocabulary} v ON vnt.vid = v.vid WHERE vnt.type = :vnt.type AND v.module = :v.module", array(':vnt.type' => $node->type, ':v.module' => 'nodeorder'));
-
-    while ($row = db_fetch_object($result)) {
-      $tree = taxonomy_get_tree($row->vid);
-      foreach ($tree as $term) {
-        $orderable_tids[] = $term->tid;
-      }
-    }
+    $query = db_select('taxonomy_index', 'i');
+    $query->join('taxonomy_term_data', 'd', 'd.tid = i.tid');
+    $query->join('taxonomy_vocabulary', 'v', 'v.vid = d.vid');
+    $query->condition('i.nid', $node->nid)
+      ->condition('v.module', 'nodeorder')
+      ->fields('i', array('tid'));
+    $tids = $query->execute()->fetchCol('tid');
 
     //permanently cache the value for easy reuse
-    cache_set($cid, $orderable_tids, 'cache');
+    cache_set($cid, $tids, 'cache');
   }
 
-  // Now select only those tids which are actually assigned to this term
-  foreach ($node->taxonomy as $key => $value) {
-    $list_of_tids = nodeorder_get_tids($key, $value);
+  return $tids;
+}
 
-    $tids = array_merge($tids, array_intersect($list_of_tids, $orderable_tids));
+/**
+ * Returns an array of the node's tids that are in orderable vocabularies...
+ * Slower than nodeorder_orderable_tids but needed when tids are flushed in cache and database
+ * Adopted form API function taxonomy_build_node_index
+ */
+function nodeorder_orderable_tids_by_node($node) {
+  $tids = array();
+  foreach (field_info_instances('node', $node->type) as $instance) {
+    $field_name = $instance['field_name'];
+    $field = field_info_field($field_name);
+    if ($field['module'] == 'taxonomy' && $field['storage']['type'] == 'field_sql_storage') {
+      // If a field value is not set in the node object when node_save() is
+      // called, the old value from $node->original is used.
+      if (isset($node->{$field_name})) {
+        $items = $node->{$field_name};
+      }
+      elseif (isset($node->original->{$field_name})) {
+        $items = $node->original->{$field_name};
+      }
+      else {
+        continue;
+      }
+      foreach (field_available_languages('node', $field) as $langcode) {
+        if (!empty($items[$langcode])) {
+          foreach ($items[$langcode] as $item) {
+            $tids[$item['tid']] = $item['tid'];
+          }
+        }
+      }
+    }
   }
 
   return $tids;
@@ -677,7 +706,6 @@ function nodeorder_get_tids($key, $value) {
  * Returns TRUE if the vocabulary is orderable...
  */
 function nodeorder_vocabulary_can_be_ordered($vid) {
-  $sql = "SELECT * FROM {taxonomy_vocabulary} WHERE module = 'nodeorder' AND vid = %d";
   $result = db_query("SELECT * FROM {taxonomy_vocabulary} WHERE module = :module AND vid = :vid", array(':module' => 'nodeorder', ':vid' => $vid));
 
   if ($result->fetchAssoc()) {
@@ -697,10 +725,8 @@ function nodeorder_term_can_be_ordered($tid) {
     return $cache->data;
   }
   else {
-    $sql = "SELECT vid FROM {taxonomy_term_data} WHERE tid = %d";
     $vid = db_query("SELECT vid FROM {taxonomy_term_data} WHERE tid = :tid", array(':tid' => $tid))->fetchField();
 
-    $sql = "SELECT * FROM {taxonomy_vocabulary} WHERE module = 'nodeorder' AND vid = %d";
     $result = db_query("SELECT * FROM {taxonomy_vocabulary} WHERE module = :module AND vid = :vid", array(':module' => 'nodeorder', ':vid' => $vid));
 
     $term_can_be_ordered = FALSE;
@@ -735,36 +761,40 @@ function nodeorder_node_presave($node) {
 
 /**
  * Implements hook_node_delete().
+ * Handle lists in which the node is removed
  */
 function nodeorder_node_delete($node) {
-  return;
-  // make sure the weight cache is invalidated
-  if (nodeorder_can_be_ordered($node)) {
-    $tids = nodeorder_orderable_tids($node);
-
-    if (count($tids) > 0) {
-      foreach ($tids as $i => $tid) {
-        nodeorder_get_term_min_max($tid, TRUE); // reinitialize the cache
-      }
-    }
+  // get tids from node var; in the database they're already removed
+  $tids = nodeorder_orderable_tids_by_node($node);
+  foreach ($tids as $tid) {
+    nodeorder_handle_node_lists_decrease($tid);
   }
 }
 
 /**
  * Implements hook_node_insert().
+ * Handle the weights of the node in the taxonomy orderable lists it id added
  */
 function nodeorder_node_insert($node) {
-  // Set the initial weight to max+1... This makes sure that the weight
-  // will be unique for each nid/tid combination
-  //
-  // NOTE - fall through to 'update' since we do mostly the same thing there.
+  $tids = nodeorder_orderable_tids($node, TRUE);
+  foreach ($tids as $tid) {
+    nodeorder_add_node_to_list($node, $tid);
+  }
 }
 
 /**
  * Implements hook_node_load().
  */
 function nodeorder_node_load($nodes, $types) {
-  $result = db_query('SELECT weight, nid FROM {taxonomy_index} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)));
+  if (arg(0) == 'taxonomy' && arg(1) == 'term' && arg(2)) $tid = arg(2);
+  else $tid = '';
+  $query = db_select('taxonomy_index', 't')
+    ->condition('nid', array_keys($nodes), 'IN');
+  if ($tid) $query->condition('tid', $tid);
+  $result = $query
+    ->fields('t', array('weight', 'nid'))
+    ->execute();
+
   foreach ($result as $record) {
     $nodes[$record->nid]->weight = $record->weight;
   }
@@ -772,47 +802,111 @@ function nodeorder_node_load($nodes, $types) {
 
 /**
  * Implements hook_node_update().
+ * Handle the weights, which were reset on rebuild of the taxonomy
  */
 function nodeorder_node_update($node) {
-  // Set the weight -- taxonomy probably stomped it because
-  // we added the weight column to term_node, and taxonomy
-  // just wants to delete and re-insert rows when things change...
 
-  // Note that we only want to set the weight for tids that
-  // are in orderable vocabularies...
-  if (nodeorder_can_be_ordered($node)) {
-    // @todo: switch on orderability check
-    // $tids = nodeorder_orderable_tids($node);
-    // if (count($tids) > 0) {
-    
-    if (TRUE) {
-      $sql = "UPDATE {taxonomy_index} SET weight = %d WHERE tid = %d AND nid = %d";
+  $tids = nodeorder_orderable_tids($node, TRUE);
+  $old_tids = $node->nodeorder;
+  foreach ($tids as $tid) {
+    // restore weight of unchanged terms, or leave as is if zero
+    if (isset($old_tids[$tid])) {
+      $old_weight = $old_tids[$tid];
+      unset($old_tids[$tid]);
+
+      if (!$old_weight) continue;
+      $query = db_update('taxonomy_index')
+        ->fields(array('weight' => $old_weight))
+        ->condition('nid', $node->nid)
+        ->condition('tid', $tid)
+        ->execute();
+    }
+    // push new or newly orderable node to top of ordered list
+    else {
+      nodeorder_add_node_to_list($node, $tid);
+    }
+  }
 
-      foreach ($tids as $i => $tid) {
-        db_lock_table('taxonomy_term_node');
-        $weights = nodeorder_get_term_min_max($tid, FALSE); // get the cached weights
-        // TODO Please convert this statement to the D7 database API syntax.
-        db_query($sql, $weights["max"] + 1, $tid, $node->nid);
-        nodeorder_get_term_min_max($tid, TRUE); // reinitialize the cache
-        db_unlock_tables();
-      }
+  // Handle lists in which the node is removed
+  // Note that the old tids are at this point only the ones that were not updated, the others were dropped when restoring above
+  foreach ($old_tids as $tid => $weight) {
+    nodeorder_handle_node_lists_decrease($tid);
+  }
+
+}
+
+/**
+ * Push new or newly orderable node to top of ordered list
+ */
+function nodeorder_add_node_to_list($node, $tid) {
+  // append new orderable node
+  $weights = nodeorder_get_term_min_max($tid); // get the cached weights
+  $query = db_update('taxonomy_index')
+    ->fields(array('weight' => $weights['min'] - 1))
+    ->condition('nid', $node->nid)
+    ->condition('tid', $tid)
+    ->execute();
+  // if new node out of range, push top nodes down filling the order gap
+  // this is when old list's min weight is top range
+  // except when new orderable node increases range (new list is not even)
+  $taxonomy_nids = taxonomy_select_nodes($tid, FALSE, FALSE, array('t.weight' => 'ASC'));
+
+  $new_node_out_of_range = (count($taxonomy_nids) % 2 == 0 && $weights['min'] == -ceil(count($taxonomy_nids) / 2));
+  if ($new_node_out_of_range) {
+    // collect top nodes
+    // note that while the node data is not yet updated in the database, the taxonomy is
+    $top_range_nids = array();
+    $previous_weight = $weights['min'] - 2;
+    foreach ($taxonomy_nids as $taxonomy_nid) {
+      $taxonomy_node_weight = db_select('taxonomy_index', 'i')
+        ->fields('i', array('weight'))
+        ->condition('tid', $tid)
+        ->condition('nid', $taxonomy_nid)
+        ->execute()
+        ->fetchField();
+
+      if ($taxonomy_node_weight > $previous_weight + 1)  break;
+      $previous_weight = $taxonomy_node_weight;
+      $top_range_nids[] = $taxonomy_nid;
     }
 
-    // New nodes won't have any saved weight values so this array will be empty...
-    if ($node->nodeorder) {
-      // Restore any saved weight values...
-      $sql = "UPDATE {taxonomy_index} SET weight = %d WHERE nid = %d AND tid = %d";
-      foreach ($node->nodeorder as $tid => $weight) {
-        // weight cannot be 0
-        if ($weight != 0) {
-          // TODO Please convert this statement to the D7 database API syntax.
-          db_query($sql, $weight, $node->nid, $tid);
-        }
-      }
+    // move top nodes down
+    $query = db_update('taxonomy_index');
+    $query->expression('weight', 'weight + 1');
+    $query->condition('nid', $top_range_nids, 'IN')
+      ->condition('tid', $tid)
+      ->execute();
+  }
+  // Make sure the weight cache is invalidated
+  nodeorder_get_term_min_max($tid, TRUE);
+}
+
+/**
+ * Reorder list in which the node is dropped and where the borders became out of range
+ */
+function nodeorder_handle_node_lists_decrease($tid) {
+  $taxonomy_nids = taxonomy_select_nodes($tid, FALSE, FALSE, array('t.weight' => 'ASC'));
+  if (!count($taxonomy_nids)) return;
+  $weights = nodeorder_get_term_min_max($tid, TRUE);
+  $range_border = ceil(count($taxonomy_nids) / 2);
+  // Out of range when one of both new list's border weights is corresponding range border
+  $border_out_of_range = ($weights['min'] < -$range_border || $weights['max'] > $range_border);
+  if ($border_out_of_range) {
+    $weight = -$range_border;
+    foreach ($taxonomy_nids as $nid) {
+      $query = db_update('taxonomy_index')
+        ->fields(array('weight' => $weight))
+        ->condition('nid', $nid)
+        ->condition('tid', $tid)
+        ->execute();
+      $weight++;
     }
+    // Make sure the weight cache is invalidated
+    nodeorder_get_term_min_max($tid, TRUE);
   }
 }
 
+
 /**
  * Form for Admin Settings
  */
-- 
1.7.9.5

