? patches
? pathauto.api.php
Index: pathauto.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto.admin.inc,v
retrieving revision 1.20.2.12
diff -u -p -r1.20.2.12 pathauto.admin.inc
--- pathauto.admin.inc	27 Jul 2010 04:35:43 -0000	1.20.2.12
+++ pathauto.admin.inc	28 Jul 2010 17:27:53 -0000
@@ -19,11 +19,8 @@ function pathauto_patterns_form($form_st
 
   // Call the hook on all modules - an array of 'settings' objects is returned
   $all_settings = module_invoke_all('pathauto', 'settings');
-  $modulelist = array();
-  $indexcount = 0;
   foreach ($all_settings as $settings) {
     $module = $settings->module;
-    $modulelist[] = $module;
     $patterndescr = $settings->patterndescr;
     $patterndefault = $settings->patterndefault;
     $groupheader = $settings->groupheader;
@@ -84,22 +81,6 @@ function pathauto_patterns_form($form_st
       '#value' => theme('token_tree', array($settings->token_type), FALSE),
     );
 
-    // If the module supports bulk updates, offer the update action here
-    if ($settings->bulkname) {
-      $variable = 'pathauto_' . $module . '_bulkupdate';
-      if (variable_get($variable, FALSE)) {
-        variable_set($variable, FALSE);
-        $function = $module . '_pathauto_bulkupdate';
-        call_user_func($function);
-      }
-      $form[$module][$variable] = array(
-        '#type' => 'checkbox',
-        '#title' => $settings->bulkname,
-        '#default_value' => FALSE,
-        '#description' => $settings->bulkdescr,
-      );
-    }
-
     // If the module supports feeds, offer to generate aliases for them
     if ($supportsfeeds) {
       $variable = 'pathauto_' . $module . '_applytofeeds';
@@ -112,18 +93,8 @@ function pathauto_patterns_form($form_st
         '#description' => t('The text to use for aliases for RSS feeds. Examples are "0/feed" (used throughout Drupal core) and "feed" (used by some contributed Drupal modules, like Views).'),
       );
     }
-
-  }
-
-  if (isset($do_index_bulkupdate) && $do_index_bulkupdate) {
-    drupal_set_message(format_plural($indexcount,
-      'Bulk generation of index aliases completed, one alias generated.',
-      'Bulk generation of index aliases completed, @count aliases generated.'));
   }
 
-  // Keep track of which modules currently support pathauto
-  variable_set('pathauto_modulelist', $modulelist);
-
   return system_settings_form($form);
 }
 
@@ -187,17 +158,6 @@ function pathauto_settings_form() {
     '#element_validate' => array('_pathauto_validate_numeric_element'),
   );
 
-  $form['pathauto_max_bulk_update'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Maximum number of objects to alias in a bulk update'),
-    '#size' => 4,
-    '#maxlength' => 4,
-    '#default_value' => variable_get('pathauto_max_bulk_update', 50),
-    '#min_value' => 1,
-    '#description' => t('Maximum number of objects of a given type which should be aliased during a bulk update. The default is 50 and the recommended number depends on the speed of your server. If bulk updates "time out" or result in a "white screen" then reduce the number.'),
-    '#element_validate' => array('_pathauto_validate_numeric_element'),
-  );
-
   $form['pathauto_update_action'] = array(
     '#type' => 'radios',
     '#title' => t('Update action'),
@@ -345,6 +305,113 @@ function pathauto_settings_form_validate
 }
 
 /**
+ * Form contructor for path alias bulk update form.
+ *
+ * @see pathauto_bulk_update_form_submit()
+ * @ingroup forms
+ */
+function pathauto_bulk_update_form() {
+  _pathauto_include();
+
+  $form['#update_callbacks'] = array();
+
+  $form['update'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Select the types of un-aliased paths for which to generate URL aliases'),
+    '#options' => array(),
+    '#default_value' => array(),
+  );
+
+  $pathauto_settings = module_invoke_all('pathauto', 'settings');
+  foreach ($pathauto_settings as $settings) {
+    if (!empty($settings->batch_update_callback)) {
+      $form['#update_callbacks'][$settings->batch_update_callback] = $settings;
+      $form['update']['#options'][$settings->batch_update_callback] = $settings->groupheader;
+    }
+  }
+
+  $form['actions']['#weight'] = 100;
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submit handler for path alias bulk update form.
+ *
+ * @see pathauto_batch_update_form()
+ * @see pathauto_bulk_update_batch_finished()
+ */
+function pathauto_bulk_update_form_submit($form, &$form_state) {
+  $batch = array(
+    'title' => t('Bulk updating URL aliases'),
+    'operations' => array(
+      array('pathauto_bulk_update_batch_start', array()),
+    ),
+    'finished' => 'pathauto_bulk_update_batch_finished',
+    'file' => drupal_get_path('module', 'pathauto') . '/pathauto.admin.inc',
+  );
+
+  foreach ($form_state['values']['update'] as $callback) {
+    if (!empty($callback)) {
+      $settings = $form['#update_callbacks'][$callback];
+      if (!empty($settings->batch_file)) {
+        $batch['operations'][] = array('pathauto_bulk_update_batch_process', array($callback, $settings));
+      }
+      else {
+        $batch['operations'][] = array($callback, array());
+      }
+    }
+  }
+
+  batch_set($batch);
+}
+
+/**
+ * Batch callback; count the current number of URL aliases for comparison later.
+ */
+function pathauto_bulk_update_batch_start(&$context) {
+  $context['results']['count_before'] = db_result(db_query("SELECT COUNT(*) FROM {url_alias}"));
+}
+
+/**
+ * Common batch processing callback for all operations.
+ *
+ * Required to load our include the proper batch file.
+ */
+function pathauto_bulk_update_batch_process($callback, $settings, &$context) {
+  if (!empty($settings->batch_file)) {
+    require_once './' . $settings->batch_file;
+  }
+  return $callback($context);
+}
+
+/**
+ * Batch finished callback.
+ */
+function pathauto_bulk_update_batch_finished($success, $results, $operations) {
+  if ($success) {
+    // Count the current number of URL aliases after the batch is completed
+    // and compare to the count before the batch started.
+    $results['count_after'] = db_result(db_query("SELECT COUNT(*) FROM {url_alias}"));
+    $results['count_changed'] = max($results['count_after'] - $results['count_before'], 0);
+    if ($results['count_changed']) {
+      drupal_set_message(format_plural($results['count_changed'], 'Generated 1 URL alias.', 'Generated @count URL aliases.'));
+    }
+    else {
+      drupal_set_message('No new URL aliases to generate.');
+    }
+  }
+  else {
+    $error_operation = reset($operations);
+    drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
+  }
+}
+
+/**
  * Menu callback; select certain alias types to delete.
  */
 function pathauto_admin_delete() {
Index: pathauto.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto.install,v
retrieving revision 1.14.2.3
diff -u -p -r1.14.2.3 pathauto.install
--- pathauto.install	27 Jul 2010 04:35:43 -0000	1.14.2.3
+++ pathauto.install	28 Jul 2010 17:27:53 -0000
@@ -14,24 +14,17 @@
 function pathauto_install() {
   // Check to see if taxonomy module is enabled before we set those variables
   if (module_exists('taxonomy')) {
-    variable_set('pathauto_modulelist', array('node', 'user', 'taxonomy'));
     variable_set('pathauto_taxonomy_supportsfeeds', '0/feed');
     variable_set('pathauto_taxonomy_pattern', 'category/[vocab-raw]/[catpath-raw]');
-    variable_set('pathauto_taxonomy_bulkupdate', FALSE);
-    variable_set('pathauto_taxonomy_applytofeeds', FALSE);
     variable_set('pathauto_taxonomy_2_pattern', '');
     variable_set('pathauto_taxonomy_1_pattern', '');
   }
-  else {
-    // Node and user are required so we don't have to check
-    variable_set('pathauto_modulelist', array('node', 'user'));
-  }
+
   // Set the rest of the pathauto default variables
   variable_set('pathauto_indexaliases', FALSE);
   variable_set('pathauto_indexaliases_bulkupdate', FALSE);
   variable_set('pathauto_max_component_length', '100');
   variable_set('pathauto_max_length', '100');
-  variable_set('pathauto_node_bulkupdate', FALSE);
   variable_set('pathauto_node_forum_pattern', '');
   variable_set('pathauto_node_image_pattern', '');
   variable_set('pathauto_node_page_pattern', '');
@@ -40,7 +33,6 @@ function pathauto_install() {
   variable_set('pathauto_punctuation_quotes', 0);
   variable_set('pathauto_separator', '-');
   variable_set('pathauto_update_action', '2');
-  variable_set('pathauto_user_bulkupdate', FALSE);
   variable_set('pathauto_user_pattern', 'users/[user-raw]');
   variable_set('pathauto_user_supportsfeeds', NULL);
   variable_set('pathauto_verbose', FALSE);
@@ -122,3 +114,16 @@ function pathauto_update_6200() {
   variable_del('pathauto_tracker_applytofeeds');
   return array();
 }
+
+/**
+ * Remove obsolete variable since batch API is now used.
+ */
+function pathauto_update_6201() {
+  variable_del('pathauto_max_bulk_update');
+  variable_del('pathauto_node_bulkupdate');
+  variable_del('pathauto_taxonomy_bulkupdate');
+  variable_del('pathauto_forum_bulkupdate');
+  variable_del('pathauto_user_bulkupdate');
+  variable_del('pathauto_blog_bulkupdate');
+  variable_del('pathauto_modulelist');
+}
Index: pathauto.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto.module,v
retrieving revision 1.126.2.29
diff -u -p -r1.126.2.29 pathauto.module
--- pathauto.module	25 Jul 2010 05:16:12 -0000	1.126.2.29
+++ pathauto.module	28 Jul 2010 17:27:53 -0000
@@ -75,13 +75,22 @@ function pathauto_menu() {
     'weight' => 20,
     'file' => 'pathauto.admin.inc',
   );
+  $items['admin/build/path/update_bulk'] = array(
+    'title' => 'Bulk update',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('pathauto_bulk_update_form'),
+    'access arguments' => array('administer url aliases'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 30,
+    'file' => 'pathauto.admin.inc',
+  );
   $items['admin/build/path/delete_bulk'] = array(
     'title' => 'Delete aliases',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('pathauto_admin_delete'),
     'access arguments' => array('administer url aliases'),
     'type' => MENU_LOCAL_TASK,
-    'weight' => 30,
+    'weight' => 40,
     'file' => 'pathauto.admin.inc',
   );
 
@@ -213,23 +222,13 @@ function pathauto_nodeapi(&$node, $op, $
       break;
     case 'insert':
     case 'update':
-      // Only do work if there's a pattern.
-      $language = variable_get('language_content_type_' . $node->type, 0) ? $node->language : '';
-      if ($pattern = pathauto_pattern_load_by_entity('node', $node->type, $language)) {
-        _pathauto_include();
-        // Only create an alias if the checkbox was not provided or if the checkbox was provided and is checked
-        if (!isset($node->pathauto_perform_alias) || $node->pathauto_perform_alias) {
-          $placeholders = pathauto_get_placeholders('node', $node);
-          $src = "node/$node->nid";
-          $node->path = pathauto_create_alias('node', $op, $placeholders, $src, $node->nid, $node->type, $node->language);
-        }
+      if (!isset($node->pathauto_perform_alias) || !empty($node->pathauto_perform_alias)) {
+        pathauto_node_update_alias($node, 'insert');
       }
       break;
     case 'delete':
       pathauto_path_delete_all("node/{$node->nid}");
       break;
-    default:
-      break;
   }
 }
 
@@ -256,7 +255,7 @@ function pathauto_form_alter(&$form, $fo
           // If this is not a new node, compare it's current alias to the
           // alias that would be genereted by pathauto. If they are the same,
           // then keep the automatic alias enabled.
-          _pathauto_include();
+          module_load_include('inc', 'pathauto');
           $placeholders = pathauto_get_placeholders('node', $node);
           $pathauto_alias = pathauto_create_alias('node', 'return', $placeholders, "node/{$node->nid}", $node->nid, $node->type, $node->language);
           $node->pathauto_perform_alias = isset($node->path) && $node->path == $pathauto_alias;
@@ -309,7 +308,7 @@ function pathauto_node_operations() {
   $operations['pathauto_update_alias'] = array(
     'label' => t('Update URL alias'),
     'callback' => 'pathauto_node_update_alias_multiple',
-    'callback arguments' => array('bulkupdate', TRUE),
+    'callback arguments' => array('bulkupdate', array('message' => TRUE)),
   );
   return $operations;
 }
@@ -321,11 +320,19 @@ function pathauto_node_operations() {
  *   A node object.
  * @param $op
  *   Operation being performed on the node ('insert', 'update' or 'bulkupdate').
+ * @param $options
+ *   An optional array of additional options.
  */
-function pathauto_node_update_alias($node, $op) {
+function pathauto_node_update_alias($node, $op, $options = array()) {
+  // Skip processing if the term has no pattern.
+  $language = isset($node->language) ? $node->language : '';
+  if (!pathauto_pattern_load_by_entity('node', $node->type, $language)) {
+    return;
+  }
+
   module_load_include('inc', 'pathauto');
   $placeholders = pathauto_get_placeholders('node', $node);
-  pathauto_create_alias('node', $op, $placeholders, "node/{$node->nid}", $node->nid, $node->type, $node->language);
+  pathauto_create_alias('node', $op, $placeholders, "node/{$node->nid}", $node->nid, $node->type, $language);
 }
 
 /**
@@ -336,17 +343,19 @@ function pathauto_node_update_alias($nod
  * @param $op
  *   Operation being performed on the nodes ('insert', 'update' or
  *   'bulkupdate').
- * @param $message
- *   A boolean if TRUE will display a message about how many nodes were
- *   updated.
+ * @param $options
+ *   An optional array of additional options.
  */
-function pathauto_node_update_alias_multiple($nids, $op, $message = FALSE) {
+function pathauto_node_update_alias_multiple($nids, $op, $options = array()) {
+  $options += array('message' => FALSE);
+
   foreach ($nids as $nid) {
     if ($node = node_load($nid, NULL, TRUE)) {
-      pathauto_node_update_alias($node, $op);
+      pathauto_node_update_alias($node, $op, $options);
     }
   }
-  if ($message) {
+
+  if (!empty($options['message'])) {
     drupal_set_message(format_plural(count($nids), 'Updated URL alias for 1 node.', 'Updated URL aliases for @count nodes.'));
   }
 }
@@ -363,28 +372,12 @@ function pathauto_taxonomy($op, $type, $
       switch ($op) {
         case 'insert':
         case 'update':
-          $category = (object) $object;
-
-          // Only do work if there's a pattern.
-          if ($pattern = pathauto_pattern_load_by_entity('taxonomy', $category->vid)) {
-            _pathauto_include();
-
-            // Clear the taxonomy term's static cache.
-            if ($op == 'update') {
-              taxonomy_get_term($category->tid, TRUE);
-            }
-
-            // Use the category info to automatically create an alias
-            if ($category->name) {
-              $count = _taxonomy_pathauto_alias($category, $op);
-            }
-
-            // For all children generate new alias (important if [catpath] used)
-            foreach (taxonomy_get_tree($category->vid, $category->tid) as $subcategory) {
-              $count = _taxonomy_pathauto_alias($subcategory, $op);
-            }
+          $term = (object) $object;
+          // Clear the taxonomy term's static cache.
+          if ($op == 'update') {
+            taxonomy_get_term($term->tid, TRUE);
           }
-
+          pathauto_taxonomy_term_update_alias($term, $op);
           break;
         case 'delete':
           // If the category is deleted, remove the path aliases
@@ -395,12 +388,70 @@ function pathauto_taxonomy($op, $type, $
             pathauto_path_delete_all("taxonomy/term/{$category->tid}");
           }
           break;
-        default:
-          break;
       }
       break;
-    default:
-      break;
+  }
+}
+
+/**
+ * Update the URL aliases for an individual taxonomy term.
+ *
+ * @param $term
+ *   A taxonomy term object.
+ * @param $op
+ *   Operation being performed on the term ('insert', 'update' or 'bulkupdate').
+ * @param $options
+ *   An optional array of additional options.
+ */
+function pathauto_taxonomy_term_update_alias($term, $op, $options = array()) {
+  $options += array('alias children' => FALSE);
+
+  $module = 'taxonomy';
+  if (module_exists('forum') && $term->vid == variable_get('forum_nav_vocabulary', '')) {
+    $module = 'forum';
+  }
+
+  // Skip processing if the term has no pattern.
+  if (!pathauto_pattern_load_by_entity($module, $term->vid)) {
+    return;
+  }
+
+  module_load_include('inc', 'pathauto');
+  $source = taxonomy_term_path($term);
+  $placeholders = pathauto_get_placeholders('taxonomy', $term);
+  pathauto_create_alias($module, $op, $placeholders, $source, $term->tid, $term->vid);
+
+  if (!empty($options['alias children'])) {
+    // For all children generate new alias.
+    $options['alias children'] = FALSE;
+    foreach (taxonomy_get_tree($term->vid, $term->tid) as $subterm) {
+      pathauto_taxonomy_term_update_alias($subterm, $op, $options);
+    }
+  }
+}
+
+/**
+ * Update the URL aliases for multiple taxonomy terms.
+ *
+ * @param $tids
+ *   An array of term IDs.
+ * @param $op
+ *   Operation being performed on the nodes ('insert', 'update' or
+ *   'bulkupdate').
+ * @param $options
+ *   An optional array of additional options.
+ */
+function pathauto_taxonomy_term_update_alias_multiple(array $tids, $op, array $options = array()) {
+  $options += array('message' => FALSE);
+
+  foreach ($tids as $tid) {
+    if ($term = taxonomy_get_term($tid, TRUE)) {
+      pathauto_taxonomy_term_update_alias($term, $op, $options);
+    }
+  }
+
+  if (!empty($options['message'])) {
+    drupal_set_message(format_plural(count($tids), 'Updated URL alias for 1 term.', 'Updated URL aliases for @count terms.'));
   }
 }
 
@@ -414,10 +465,10 @@ function pathauto_user($op, &$edit, &$us
   switch ($op) {
     case 'insert':
     case 'update':
-      _pathauto_include();
       // Use the username to automatically create an alias
       $pathauto_user = (object) array_merge((array) $user, $edit);
       if ($user->name) {
+        module_load_include('inc', 'pathauto');
         $placeholders = pathauto_get_placeholders('user', $pathauto_user);
         $src = 'user/'. $user->uid;
         $alias = pathauto_create_alias('user', $op, $placeholders, $src, $user->uid);
@@ -457,7 +508,7 @@ function pathauto_user_operations() {
   $operations['pathauto_update_alias'] = array(
     'label' => t('Update URL alias'),
     'callback' => 'pathauto_user_update_alias_multiple',
-    'callback arguments' => array('bulkupdate', TRUE),
+    'callback arguments' => array('bulkupdate', array('message' => TRUE)),
   );
   return $operations;
 }
@@ -470,21 +521,26 @@ function pathauto_user_operations() {
  * @param $op
  *   Operation being performed on the account ('insert', 'update' or
  *   'bulkupdate').
- *
- * @todo Remove support for any sub-path aliases.
+ * @param $options
+ *   An optional array of additional options.
  */
-function pathauto_user_update_alias($account, $op) {
+function pathauto_user_update_alias($account, $op, $options = array()) {
+  $options += array('alias blog' => module_exists('blog'));
+
+  // Skip processing if the account has no pattern.
+  if (!pathauto_pattern_load_by_entity('user')) {
+    return;
+  }
+
   module_load_include('inc', 'pathauto');
   $placeholders = pathauto_get_placeholders('user', $account);
   pathauto_create_alias('user', $op, $placeholders, "user/{$account->uid}", $account->uid);
 
-  if (module_exists('blog')) {
-    if (node_access('create', 'blog', $account)) {
-      pathauto_create_alias('blog', $op, $placeholders, "blog/{$account->uid}", $account->uid);
-    }
-    else {
-      pathauto_path_delete_all("blog/{$user->uid}");
-    }
+  // Because blogs are also associated with users, also generate the blog paths.
+  if (!empty($options['alias blog'])) {
+    // Allow placeholders to be re-used.
+    $options['placeholders'] = $placeholders;
+    pathauto_blog_update_alias($account, $op);
   }
 }
 
@@ -496,17 +552,46 @@ function pathauto_user_update_alias($acc
  * @param $op
  *   Operation being performed on the accounts ('insert', 'update' or
  *   'bulkupdate').
- * @param $message
- *   A boolean if TRUE will display a message about how many accounts were
- *   updated.
+ * @param $options
+ *   An optional array of additional options.
  */
-function pathauto_user_update_alias_multiple($uids, $op, $message = FALSE) {
+function pathauto_user_update_alias_multiple($uids, $op, $options = array()) {
+  $options += array('message' => FALSE);
+
   foreach ($uids as $uid) {
     if ($account = user_load($uid)) {
-      pathauto_user_update_alias($account, $op);
+      pathauto_user_update_alias($account, $op, $options);
     }
   }
-  if ($message) {
+
+  if (!empty($options['message'])) {
     drupal_set_message(format_plural(count($uids), 'Updated URL alias for 1 user account.', 'Updated URL aliases for @count user accounts.'));
   }
 }
+
+/**
+ * Update the blog URL aliases for an individual user account.
+ *
+ * @param $account
+ *   A user account object.
+ * @param $op
+ *   Operation being performed on the blog ('insert', 'update' or
+ *   'bulkupdate').
+ * @param $options
+ *   An optional array of additional options.
+ */
+function pathauto_blog_update_alias($account, $op, $options = array()) {
+  // Skip processing if the blog has no pattern.
+  if (!pathauto_pattern_load_by_entity('blog')) {
+    return;
+  }
+
+  module_load_include('inc', 'pathauto');
+  $placeholders = isset($options['placeholders']) ? $options['placeholders'] : pathauto_get_placeholders('user', $account);
+  if (node_access('create', 'blog', $account)) {
+    pathauto_create_alias('blog', $op, $placeholders, "blog/{$account->uid}", $account->uid);
+  }
+  else {
+    pathauto_path_delete_all("blog/{$user->uid}");
+  }
+}
Index: pathauto.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto.test,v
retrieving revision 1.1.4.21
diff -u -p -r1.1.4.21 pathauto.test
--- pathauto.test	28 Jul 2010 15:45:34 -0000	1.1.4.21
+++ pathauto.test	28 Jul 2010 17:27:53 -0000
@@ -53,11 +53,21 @@ class PathautoTestHelper extends DrupalW
     $this->assertAlias($uri['path'], $expected_alias, $language);
   }
 
+  function assertEntityAliasExists($entity_type, $entity) {
+    $uri = $this->entity_uri($entity_type, $entity);
+    $this->assertAliasExists(array('source' => $uri['path']));
+  }
+
   function assertNoEntityAlias($entity_type, $entity, $expected_alias, $language = '') {
     $uri = $this->entity_uri($entity_type, $entity);
     $this->assertEntityAlias($entity_type, $entity, $uri['path'], $language);
   }
 
+  function assertNoEntityAliasExists($entity_type, $entity) {
+    $uri = $this->entity_uri($entity_type, $entity);
+    $this->assertNoAliasExists(array('source' => $uri['path']));
+  }
+
   function assertAlias($source, $expected_alias, $language = '') {
     drupal_clear_path_cache();
     $alias = drupal_get_path_alias($source, $language);
@@ -138,6 +148,11 @@ class PathautoTestHelper extends DrupalW
     $sql = "SELECT * FROM {url_alias} WHERE " . implode(' AND ', $conditions);
     return db_fetch_array(db_query_range($sql, $args, 0, 1));
   }
+
+  function deleteAllAliases() {
+    db_query("DELETE FROM {url_alias}");
+    drupal_clear_path_cache();
+  }
 }
 
 /**
@@ -611,3 +626,53 @@ class PathautoLocaleTestCase extends Pat
     $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => 'content/english-node-0'));
   }
 }
+
+/**
+ * Bulk update functionality tests.
+ */
+class PathautoBulkUpdateTestCase extends PathautoFunctionalTestHelper {
+  private $nodes;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Pathauto bulk updating',
+      'description' => 'Tests bulk updating of URL aliases.',
+      'group' => 'Pathauto',
+    );
+  }
+
+  function testBulkUpdate() {
+    // Create some nodes.
+    $this->nodes = array();
+    for ($i = 1; $i <= 5; $i++) {
+      $node = $this->drupalCreateNode();
+      $this->nodes[$node->nid] = $node;
+    }
+
+    // Clear out all aliases.
+    $this->deleteAllAliases();
+
+    // Bulk create aliases.
+    $edit = array(
+      'update[node_pathauto_bulk_update_batch_process]' => TRUE,
+      'update[user_pathauto_bulk_update_batch_process]' => TRUE,
+    );
+    $this->drupalPost('admin/build/path/update_bulk', $edit, t('Update'));
+    $this->assertText('Generated 7 URL aliases.'); // 5 nodes + 2 users
+
+    // Check that aliases have actually been created.
+    foreach ($this->nodes as $node) {
+      $this->assertEntityAliasExists('node', $node);
+    }
+    $this->assertEntityAliasExists('user', $this->admin_user);
+
+    // Add a new node.
+    $new_node = $this->drupalCreateNode(array('alias' => '', 'pathauto_perform_alias' => FALSE));
+
+    // Run the update again which should only run against the new node.
+    $this->drupalPost('admin/build/path/update_bulk', $edit, t('Update'));
+    $this->assertText('Generated 1 URL alias.'); // 1 node + 0 users
+
+    $this->assertEntityAliasExists('node', $new_node);
+  }
+}
Index: pathauto_node.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto_node.inc,v
retrieving revision 1.48.2.2
diff -u -p -r1.48.2.2 pathauto_node.inc
--- pathauto_node.inc	9 Jun 2010 02:18:17 -0000	1.48.2.2
+++ pathauto_node.inc	28 Jul 2010 17:27:53 -0000
@@ -20,26 +20,20 @@ function node_pathauto($op) {
       $settings['groupheader'] = t('Node paths');
       $settings['patterndescr'] = t('Default path pattern (applies to all node types with blank patterns below)');
       $settings['patterndefault'] = t('content/[title-raw]');
-      $settings['bulkname'] = t('Bulk generate aliases for nodes that are not aliased');
-      $settings['bulkdescr'] = t('Generate aliases for all existing nodes which do not already have aliases.');
+      $settings['batch_update_callback'] = 'node_pathauto_bulk_update_batch_process';
+      $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto_node.inc';
       $settings['supportsfeeds'] = 'feed';
 
+      $languages = array();
       if (module_exists('locale')) {
-        $languages = array('' => t('Language neutral')) + locale_language_list('name');
-      }
-      else {
-        $languages = array();
+        $languages = array('' => t('language neutral')) + locale_language_list('name');
       }
+
       foreach (node_get_types('names') as $node_type => $node_name) {
-        if (variable_get('language_content_type_'. $node_type, 0) && count($languages)) {
+        if (count($languages) && variable_get('language_content_type_'. $node_type, 0)) {
           $settings['patternitems'][$node_type] = t('Default path pattern for @node_type (applies to all @node_type node types with blank patterns below)', array('@node_type' => $node_name));
           foreach ($languages as $lang_code => $lang_name) {
-            if (!empty($lang_code)) {
-              $settings['patternitems'][$node_type .'_'. $lang_code] = t('Pattern for all @node_type paths in @language', array('@node_type' => $node_name, '@language' => $lang_name));
-            }
-            else {
-              $settings['patternitems'][$node_type .'_'. $lang_code] = t('Pattern for all language neutral @node_type paths', array('@node_type' => $node_name));
-            }
+            $settings['patternitems'][$node_type . '_' . $lang_code] = t('Pattern for all @language @node_type paths', array('@node_type' => $node_name, '@language' => $lang_name));
           }
         }
         else {
@@ -53,63 +47,41 @@ function node_pathauto($op) {
 }
 
 /**
- * Generate aliases for all nodes without aliases.
+ * Batch processing callback; Generate aliases for nodes.
  */
-function node_pathauto_bulkupdate() {
-  // From all node types, only attempt to update those with patterns
-  $pattern_types = array();
-
-  // If there's a default pattern we assume all types might be updated.
-  if (trim(variable_get('pathauto_node_pattern', ''))) {
-    $pattern_types = array_keys(node_get_types('names'));
+function node_pathauto_bulk_update_batch_process(&$context) {
+  if (!isset($context['sandbox']['current'])) {
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['current'] = 0;
   }
-  else {
-    // Check first for a node specific pattern...
-    $languages = array();
-    if (module_exists('locale')) {
-      $languages = array('' => t('Language neutral')) + locale_language_list('name');
-    }
-    foreach (array_keys(node_get_types('names')) as $type) {
-      if (trim(variable_get('pathauto_node_'. $type .'_pattern', ''))) {
-        $pattern_types[$type] = $type;
-        continue;
-      }
-      // ...then for a node-language pattern.
-      if (variable_get('language_content_type_'. $type, 0) && $languages) {
-        foreach ($languages as $lang_code => $lang_name) {
-          if (trim(variable_get('pathauto_node_'. $type .'_'. $lang_code .'_pattern', ''))) {
-            $pattern_types[$type] = $type;
-            continue 2;
-          }
-        }
-      }
+
+  $sql = "SELECT n.nid FROM {node} n LEFT JOIN {url_alias} ua ON CONCAT('node/', CAST(n.nid AS CHAR)) = ua.src WHERE ua.src IS NULL AND n.nid > %d ORDER BY n.nid";
+  $args = array($context['sandbox']['current']);
+
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $count_sql = str_replace('SELECT n.nid', 'SELECT COUNT(n.nid)', $sql);
+    $context['sandbox']['total'] = db_result(db_query($count_sql, $args));
+
+    // If there are no nodes to update, the stop immediately.
+    if (!$context['sandbox']['total']) {
+      $context['finished'] = 1;
+      return;
     }
   }
 
-  $count = 0;
-  if (count($pattern_types)) {
-    $query = "SELECT n.nid, n.vid, n.type, n.title, n.uid, n.created, n.language, alias.src, alias.dst FROM {node} n LEFT JOIN {url_alias} alias ON CONCAT('node/', CAST(n.nid AS CHAR)) = alias.src WHERE alias.src IS NULL AND n.type IN (". db_placeholders($pattern_types, 'varchar') .')';
-    $result = db_query_range($query, $pattern_types, 0, variable_get('pathauto_max_bulk_update', 50));
-
-    $placeholders = array();
-    while ($node_ref = db_fetch_object($result)) {
-      $node = node_load($node_ref->nid, NULL, TRUE);
-      $node->src = $node_ref->src;
-      $node->dst = $node_ref->dst;
-      if (module_exists('taxonomy')) {
-        // Must populate the terms for the node here for the category
-        // placeholders to work
-        $node->taxonomy = array_keys(taxonomy_node_get_terms($node));
-      }
-      $placeholders = pathauto_get_placeholders('node', $node);
-      $source = "node/$node->nid";
-      if (pathauto_create_alias('node', 'bulkupdate', $placeholders, $source, $node->nid, $node->type, $node->language)) {
-        $count++;
-      }
-    }
+  $query = db_query_range($sql, $args, 0, 5);
+  $nids = array();
+  while ($nid = db_result($query)) {
+    $nids[] = $nid;
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of nodes completed, one alias generated.',
-    'Bulk generation of nodes completed, @count aliases generated.'));
+  pathauto_node_update_alias_multiple($nids, 'bulkupdate');
+  $context['sandbox']['count'] += count($nids);
+  $context['sandbox']['current'] = max($nids);
+  $context['message'] = t('Updated alias for node @nid.', array('@nid' => end($nids)));
+
+  if ($context['sandbox']['count'] != $context['sandbox']['total']) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
Index: pathauto_taxonomy.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto_taxonomy.inc,v
retrieving revision 1.41.2.4
diff -u -p -r1.41.2.4 pathauto_taxonomy.inc
--- pathauto_taxonomy.inc	9 Jun 2010 02:18:17 -0000	1.41.2.4
+++ pathauto_taxonomy.inc	28 Jul 2010 17:27:53 -0000
@@ -21,8 +21,8 @@ function taxonomy_pathauto($op) {
       $settings['patterndescr'] = t('Default path pattern (applies to all vocabularies with blank patterns below)');
       $settings['patterndefault'] = t('category/[vocab-raw]/[catpath-raw]');
       $settings['supportsfeeds'] = '0/feed';
-      $settings['bulkname'] = t('Bulk generate aliases for terms that are not aliased');
-      $settings['bulkdescr'] = t('Generate aliases for all existing terms which do not already have aliases.');
+      $settings['batch_update_callback'] = 'taxonomy_pathauto_bulk_update_batch_process';
+      $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto_taxonomy.inc';
 
       $vocabularies = taxonomy_get_vocabularies();
       if (sizeof($vocabularies) > 0) {
@@ -43,69 +43,44 @@ function taxonomy_pathauto($op) {
 }
 
 /**
- * Generate aliases for all categories without aliases.
+ * Batch processing callback; Generate aliases for taxonomy terms.
  */
-function taxonomy_pathauto_bulkupdate() {
-  // From all node types, only attempt to update those with patterns
-  $pattern_vids = array();
-  foreach (taxonomy_get_vocabularies() as $vid => $info) {
-    // TODO - If there's a default we shouldn't do this crazy where statement because all vocabularies get aliases.
-    // TODO - Special casing to exclude the forum vid (and the images vid and...?).
-    if (pathauto_pattern_load_by_entity('taxonomy', $vid)) {
-      $pattern_vids[] = $vid;
-      if (empty($vid_where)) {
-        $vid_where = " AND (vid = '%s' ";
-      }
-      else {
-        $vid_where .= " OR vid = '%s'";
-      }
-    }
+function taxonomy_pathauto_bulk_update_batch_process(&$context) {
+  if (!isset($context['sandbox']['current'])) {
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['current'] = 0;
   }
-  $vid_where .= ')';
-
-  // Exclude the forums and join all the args into one array so they can be passed to db_query
-  $forum_vid[] = variable_get('forum_nav_vocabulary', '');
-  $query_args = array_merge($forum_vid, $pattern_vids);
-  $query = "SELECT tid, vid, name, description, src, dst FROM {term_data} LEFT JOIN {url_alias} ON CONCAT('taxonomy/term/', CAST(tid AS CHAR)) = src WHERE src IS NULL AND vid <> %d ". $vid_where;
-  $result = db_query_range($query, $query_args, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($category = db_fetch_object($result)) {
-    $count += _taxonomy_pathauto_alias($category, 'bulkupdate');
-  }
-
-  drupal_set_message(format_plural($count,
-    'Bulk generation of terms completed, one alias generated.',
-    'Bulk generation of terms completed, @count aliases generated.'));
-}
-
-/**
- * Create aliases for taxonomy objects.
- *
- * @param $category
- *   A taxonomy object.
- */
-function _taxonomy_pathauto_alias($category, $op) {
-  $count = 0;
-
-  $placeholders = pathauto_get_placeholders('taxonomy', $category);
 
   $forum_vid = variable_get('forum_nav_vocabulary', '');
-  // If we're in a forum vocabulary, also create a forum container, forum, or forum topic alias.
-  if (module_exists('forum') && $forum_vid == (int)$category->vid) {
-    $source = 'forum/'. $category->tid;
-    if (pathauto_create_alias('forum', $op, $placeholders, $source, $category->tid, $category->vid)) {
-      $count++;
+  $sql = "SELECT t.tid FROM {term_data} t LEFT JOIN {url_alias} ua ON CONCAT('taxonomy/term/', CAST(t.tid AS CHAR)) = ua.src WHERE ua.src IS NULL AND t.tid > %d AND t.vid <> %d ORDER BY t.tid";
+  $args = array($context['sandbox']['current'], $forum_vid);
+
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $count_sql = str_replace('SELECT t.tid', 'SELECT COUNT(t.tid)', $sql);
+    $context['sandbox']['total'] = db_result(db_query($count_sql, $args));
+
+    // If there are no nodes to update, the stop immediately.
+    if (!$context['sandbox']['total']) {
+      $context['finished'] = 1;
+      return;
     }
   }
-  else {
-    $source = taxonomy_term_path($category);
-    if (pathauto_create_alias('taxonomy', $op, $placeholders, $source, $category->tid, $category->vid)) {
-      $count++;
-    }
+
+  $query = db_query_range($sql, $args, 0, 25);
+  $tids = array();
+  while ($tid = db_result($query)) {
+    $tids[] = $tid;
+  }
+
+  pathauto_taxonomy_term_update_alias_multiple($tids, 'bulkupdate');
+  $context['sandbox']['count'] += count($tids);
+  $context['sandbox']['current'] = max($tids);
+  $context['message'] = t('Updated alias for term @tid.', array('@tid' => end($tids)));
+
+  if ($context['sandbox']['count'] != $context['sandbox']['total']) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
   }
-  return $count;
 }
 
 /**
@@ -121,8 +96,8 @@ function forum_pathauto($op) {
       $settings['patterndescr'] = t('Pattern for forums and forum containers');
       $settings['patterndefault'] = t('[vocab-raw]/[catpath-raw]');
       $settings['supportsfeeds'] = '0/feed';
-      $settings['bulkname'] = t('Bulk generate aliases for forum paths that are not aliased');
-      $settings['bulkdescr'] = t('Generate aliases for all existing forums and forum containers which do not already have aliases.');
+      $settings['batch_update_callback'] = 'forum_pathauto_bulk_update_batch_process';
+      $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto_taxonomy.inc';
       return (object) $settings;
     default:
       break;
@@ -130,20 +105,42 @@ function forum_pathauto($op) {
 }
 
 /**
- * Generate aliases for all forums and forum containers without aliases.
+ * Batch processing callback; Generate aliases for forums.
  */
-function forum_pathauto_bulkupdate() {
+function forum_pathauto_bulk_update_batch_process(&$context) {
+  if (!isset($context['sandbox']['current'])) {
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['current'] = 0;
+  }
+
   $forum_vid = variable_get('forum_nav_vocabulary', '');
-  $query = "SELECT tid, vid, name, description, src, dst FROM {term_data} LEFT JOIN {url_alias} ON CONCAT('forum/', CAST(tid AS CHAR)) = src WHERE vid = %d AND src IS NULL";
-  $result = db_query_range($query, $forum_vid, 0, variable_get('pathauto_max_bulk_update', 50));
+  $sql = "SELECT t.tid FROM {term_data} t LEFT JOIN {url_alias} ua ON CONCAT('forum/', CAST(t.tid AS CHAR)) = ua.src WHERE ua.src IS NULL AND t.tid > %d AND t.vid = %d ORDER BY t.tid";
+  $args = array($context['sandbox']['current'], $forum_vid);
 
-  $count = 0;
-  $placeholders = array();
-  while ($category = db_fetch_object($result)) {
-    $count = _taxonomy_pathauto_alias($category, 'bulkupdate') + $count;
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $count_sql = str_replace('SELECT t.tid', 'SELECT COUNT(t.tid)', $sql);
+    $context['sandbox']['total'] = db_result(db_query($count_sql, $args));
+
+    // If there are no nodes to update, the stop immediately.
+    if (!$context['sandbox']['total']) {
+      $context['finished'] = 1;
+      return;
+    }
+  }
+
+  $query = db_query_range($sql, $args, 0, 25);
+  $tids = array();
+  while ($tid = db_result($query)) {
+    $tids[] = $tid;
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk update of forums and forum containers completed, one alias generated.',
-    'Bulk update of forums and forum containers completed, @count aliases generated.'));
+  pathauto_taxonomy_term_update_alias_multiple($tids, 'bulkupdate');
+  $context['sandbox']['count'] += count($tids);
+  $context['sandbox']['current'] = max($tids);
+  $context['message'] = t('Updated alias for forum @tid.', array('@tid' => end($tids)));
+
+  if ($context['sandbox']['count'] != $context['sandbox']['total']) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
Index: pathauto_user.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/pathauto/pathauto_user.inc,v
retrieving revision 1.31.2.3
diff -u -p -r1.31.2.3 pathauto_user.inc
--- pathauto_user.inc	22 Jul 2010 00:13:43 -0000	1.31.2.3
+++ pathauto_user.inc	28 Jul 2010 17:27:53 -0000
@@ -20,8 +20,8 @@ function user_pathauto($op) {
       $settings['groupheader'] = t('User paths');
       $settings['patterndescr'] = t('Pattern for user account page paths');
       $settings['patterndefault'] = t('users/[user-raw]');
-      $settings['bulkname'] = t('Bulk generate aliases for users that are not aliased');
-      $settings['bulkdescr'] = t('Generate aliases for all existing user account pages which do not already have aliases.');
+      $settings['batch_update_callback'] = 'user_pathauto_bulk_update_batch_process';
+      $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto_user.inc';
       return (object) $settings;
     default:
       break;
@@ -29,6 +29,46 @@ function user_pathauto($op) {
 }
 
 /**
+ * Batch processing callback; Generate aliases for users.
+ */
+function user_pathauto_bulk_update_batch_process(&$context) {
+  if (!isset($context['sandbox']['current'])) {
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['current'] = 0;
+  }
+
+  $sql = "SELECT u.uid FROM {users} u LEFT JOIN {url_alias} ua ON CONCAT('user/', CAST(u.uid AS CHAR)) = ua.src WHERE ua.src IS NULL AND u.uid > %d ORDER BY u.uid";
+  $args = array($context['sandbox']['current']);
+
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $count_sql = str_replace('SELECT u.uid', 'SELECT COUNT(u.uid)', $sql);
+    $context['sandbox']['total'] = db_result(db_query($count_sql, $args));
+
+    // If there are no nodes to update, the stop immediately.
+    if (!$context['sandbox']['total']) {
+      $context['finished'] = 1;
+      return;
+    }
+  }
+
+  $query = db_query_range($sql, $args, 0, 25);
+  $uids = array();
+  while ($uid = db_result($query)) {
+    $uids[] = $uid;
+  }
+
+  pathauto_user_update_alias_multiple($uids, 'bulkupdate', array('alias blog' => FALSE));
+  $context['sandbox']['count'] += count($uids);
+  $context['sandbox']['current'] = max($uids);
+  $context['message'] = t('Updated alias for user @uid.', array('@uid' => end($uids)));
+
+  if ($context['sandbox']['count'] != $context['sandbox']['total']) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
+}
+
+/**
  * Implements hook_pathauto().
  */
 function blog_pathauto($op) {
@@ -41,8 +81,8 @@ function blog_pathauto($op) {
       $settings['patterndescr'] = t('Pattern for blog page paths');
       $settings['patterndefault'] = t('blogs/[user-raw]');
       $settings['supportsfeeds'] = 'feed';
-      $settings['bulkname'] = t('Bulk generate aliases for blogs that are not aliased');
-      $settings['bulkdescr'] = t('Generate aliases for all existing blog pages which do not already have aliases.');
+      $settings['batch_update_callback'] = 'blog_pathauto_bulk_update_batch_process';
+      $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto_user.inc';
       return (object) $settings;
     default:
       break;
@@ -50,45 +90,42 @@ function blog_pathauto($op) {
 }
 
 /**
- * Bulk generate aliases for all users without aliases.
+ * Batch processing callback; Generate aliases for blogs.
  */
-function user_pathauto_bulkupdate() {
-  $query = "SELECT uid, name, src, dst FROM {users} LEFT JOIN {url_alias} ON CONCAT('user/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL";
-  $result = db_query_range($query, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($user = db_fetch_object($result)) {
-    $placeholders = pathauto_get_placeholders('user', $user);
-    $source = 'user/'. $user->uid;
-    if (pathauto_create_alias('user', 'bulkupdate', $placeholders, $source, $user->uid)) {
-      $count++;
-    }
+function blog_pathauto_bulk_update_batch_process(&$context) {
+  if (!isset($context['sandbox']['current'])) {
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['current'] = 0;
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of users completed, one alias generated.',
-    'Bulk generation of users completed, @count aliases generated.'));
-}
+  $sql = "SELECT u.uid FROM {users} u LEFT JOIN {url_alias} ua ON CONCAT('blog/', CAST(u.uid AS CHAR)) = ua.src WHERE ua.src IS NULL AND u.uid > %d ORDER BY u.uid";
+  $args = array($context['sandbox']['current']);
 
-/**
- * Bulk generate aliases for all blogs without aliases.
- */
-function blog_pathauto_bulkupdate() {
-  $query = "SELECT uid, name, src, dst FROM {users} LEFT JOIN {url_alias} ON CONCAT('blog/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL";
-  $result = db_query_range($query, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($user = db_fetch_object($result)) {
-    $placeholders = pathauto_get_placeholders('user', $user);
-    $source = 'blog/'. $user->uid;
-    if (pathauto_create_alias('blog', 'bulkupdate', $placeholders, $source, $user->uid)) {
-      $count++;
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $count_sql = str_replace('SELECT u.uid', 'SELECT COUNT(u.uid)', $sql);
+    $context['sandbox']['total'] = db_result(db_query($count_sql, $args));
+
+    // If there are no nodes to update, the stop immediately.
+    if (!$context['sandbox']['total']) {
+      $context['finished'] = 1;
+      return;
     }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of user blogs completed, one alias generated.',
-    'Bulk generation of user blogs completed, @count aliases generated.'));
+  $query = db_query_range($sql, $args, 0, 25);
+  $uids = array();
+  while ($uid = db_result($query)) {
+    $uids[] = $uid;
+    $account = user_load($uid);
+    pathauto_blog_update_alias($account, 'bulkupdate');
+  }
+
+  $context['sandbox']['count'] += count($uids);
+  $context['sandbox']['current'] = max($uids);
+  $context['message'] = t('Updated alias for blog user @uid.', array('@uid' => end($uids)));
+
+  if ($context['sandbox']['count'] != $context['sandbox']['total']) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
