Index: issue.inc
===================================================================
RCS file: /cvs/drupal/contributions/modules/project_issue/issue.inc,v
retrieving revision 1.334
diff -u -r1.334 issue.inc
--- issue.inc	28 Jan 2009 23:43:10 -0000	1.334
+++ issue.inc	29 Jan 2009 08:21:03 -0000
@@ -14,11 +14,11 @@
   $form_id = array_shift($args);
   $form_state['post'] = $form['#post'] = $_POST;
   $form['#programmed'] = $form['#redirect'] = FALSE;
-  
+
   // Skip validation in project issue.
   $form_state['project_issue_ahah'] = TRUE;
   drupal_process_form($form_id, $form, $form_state);
-  
+
   // Rebuild the form and cache it again.
   $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
 
@@ -33,7 +33,7 @@
 
   // Only generate release stuff if the project_release module is enabled.
   if (module_exists('project_release')) {
-    
+
     $project->nid = $pid;
     if ($releases = project_release_get_releases($project, 0)) {
       $old_version = db_result(db_query("SELECT version FROM {project_release_nodes} WHERE nid = %d", $rid));
@@ -1248,6 +1248,159 @@
   return $form;
 }
 
+function project_issue_admin_component_rename_form($form_state, $pid, $component) {
+  $form['new_name'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $component,
+    '#title' => t('New component name'),
+  );
+  $form['old_name'] = array(
+    '#type' => 'value',
+    '#value'=> $component,
+  );
+  $form['pid'] = array(
+    '#type' => 'value',
+    '#value'=> $pid,
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+function project_issue_admin_component_delete_form($form_state, $pid, $component) {
+  $project = node_load($pid);
+
+  // Build the array of components that can replace the "soon-to-be-deleted" component.
+  $components = $project->project_issue['components'];
+  $options = array();
+  foreach ($project->project_issue['components'] as $comp) {
+    if ($comp != $component) {
+      $options[$comp] = $comp;
+    }
+  }
+
+  $form['new_name'] = array(
+    '#type' => 'select',
+    '#options' => $options,
+    '#default_value' => '',
+    '#title' => t('Replacement component'),
+  );
+  $form['old_name'] = array(
+    '#type' => 'value',
+    '#value'=> $component,
+  );
+  $form['pid'] = array(
+    '#type' => 'value',
+    '#value'=> $pid,
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+function project_issue_admin_component_add_form($form_state, $pid) {
+  $form['new_name'] = array(
+    '#type' => 'textfield',
+    '#default_value' => '',
+    '#title' => t('New component name'),
+  );
+  $form['pid'] = array(
+    '#type' => 'value',
+    '#value'=> $pid,
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+  $form['#redirect'] = "node/$pid/edit/issues";
+
+  return $form;
+}
+
+function project_issue_admin_component_rename_form_submit($form, &$form_state) {
+  require_once(drupal_get_path('module', 'project_issue') . '/includes/batch.inc');
+
+  $old_name = $form_state['values']['old_name'];
+  $new_name = $form_state['values']['new_name'];
+  $pid = $form_state['values']['pid'];
+  $project = node_load($pid);
+
+  project_issue_component_change_start_batch('rename', $project, $old_name, $new_name);
+}
+
+function project_issue_admin_component_delete_form_submit($form, &$form_state) {
+  require_once(drupal_get_path('module', 'project_issue') . '/includes/batch.inc');
+
+  $old_name = $form_state['values']['old_name'];
+  $new_name = $form_state['values']['new_name'];
+  $pid = $form_state['values']['pid'];
+  $project = node_load($pid);
+
+  project_issue_component_change_start_batch('delete', $project, $old_name, $new_name);
+}
+
+function project_issue_admin_component_add_form_submit($form, &$form_state) {
+  $new_name = $form_state['values']['new_name'];
+  $pid = $form_state['values']['pid'];
+
+  $project = node_load($pid);
+  $components = $project->project_issue['components'];
+  $components[] = $new_name;
+  db_query("UPDATE {project_issue_projects} SET components = '%s' WHERE nid = %d", serialize($components), $project->nid);
+}
+
+/*
+ * Set up and launch batch processing via the Batch API. The same batch is
+ * used by the "delete"- and "rename"-operations.
+ * TODO: doc
+ * @param $project
+ * @param $old_name
+ * @param $new_name
+ */
+function project_issue_component_change_start_batch($op, $project, $old_name, $new_name) {
+  require_once(drupal_get_path('module', 'project_issue') . '/includes/batch.inc');
+
+  // Set up the batch operation which will update the component for each
+  // issues & comments.
+  $batch = array(
+    'finished' => 'project_issue_batch_change_component_finished',
+    'title' => t('Updating issues & follow-ups.'),
+    'error_message' => t('An error occured while updating issues.'),
+    'file' => drupal_get_path('module', 'project_issue') . "/includes/batch.inc"
+  );
+
+  switch ($op) {
+    case 'rename':
+      $batch['operations'] = array(
+        array('project_issue_batch_change_component_current_values', array($project, $old_name, $new_name)),
+        array('project_issue_batch_change_component_original_values', array($project, $old_name, $new_name)),
+      );
+      break;
+    case 'delete':
+      $batch['operations'] = array(
+        array('project_issue_batch_delete_component_current_values', array($project, $old_name, $new_name)),
+        array('project_issue_batch_change_component_original_values', array($project, $old_name, $new_name)),
+      );
+      break;
+  }
+
+  batch_set($batch);
+}
+
+function project_issue_batch_change_component_finished($success, $results, $operations) {
+  if ($success) {
+    $pid = $results['pid'];
+    drupal_set_message(t('Successfully processed !processed issues. <ul><li>Updated the current component for !issue_updated issues.</li><li>Updated the component for !comments_updated comments.</li><li>Updated the original component for !orig_updated issues.</li></ul>', array('!processed' => $results['processed'], '!issue_updated' => $results['issues_updated'], '!comments_updated' => $results['comments_updated'], '!orig_updated' => $results['orig_updated'])));
+    drupal_goto("node/$pid/edit/issues");
+  }
+}
+
 /**
  * Submit handler for project_issue_admin_states_form.
  */
Index: project_issue.module
===================================================================
RCS file: /cvs/drupal/contributions/modules/project_issue/project_issue.module,v
retrieving revision 1.116
diff -u -r1.116 project_issue.module
--- project_issue.module	28 Jan 2009 23:43:10 -0000	1.116
+++ project_issue.module	29 Jan 2009 08:21:05 -0000
@@ -102,6 +102,32 @@
     'type' => MENU_CALLBACK,
   );
 
+  // Administer component settings
+  $items['node/%/edit/components/rename/%'] = array(
+    'title' => 'Rename component',
+    'description' => 'Rename component',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_issue_admin_component_rename_form', 1, 5),
+    'access arguments' => array('administer projects'),
+    'type' => MENU_CALLBACK,
+  );
+  $items['node/%/edit/components/delete/%'] = array(
+    'title' => 'Delete component',
+    'description' => 'Delete component',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_issue_admin_component_delete_form', 1, 5),
+    'access arguments' => array('administer projects'),
+    'type' => MENU_CALLBACK,
+  );
+  $items['node/%/edit/components/add'] = array(
+    'title' => 'Add component',
+    'description' => 'Add component',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_issue_admin_component_add_form', 1),
+    'access arguments' => array('administer projects'),
+    'type' => MENU_CALLBACK,
+  );
+
   // Issues subtab on project node edit tab.
   $items['node/%project_node/edit/issues'] = array(
     'title' => 'Issues',
@@ -678,13 +704,24 @@
     '#default_value' => isset($node->project_issue['issues']) ? $node->project_issue['issues'] : 1,
     '#description' => t('Let users submit bug requests, patches, feature requests, support requests, etc.'),
   );
+  $header = array(t('Component name'), t('Operations'), '');
+  $can_rename = $can_delete = user_access('administer projects');
+  foreach (explode("\n", str_replace("\r", '', $node->project_issue['components'])) as $component) {
+    $rows[] = array(
+      $component,
+      $can_rename ? l(t('rename'), "node/$node->nid/edit/components/rename/$component"): "",
+      $can_delete ? l('delete', "node/$node->nid/edit/components/delete/$component") : ""
+    );
+  }
   $form['issue']['components'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Components'),
-    '#default_value' => isset($node->project_issue['components']) ? $node->project_issue['components'] : NULL,
-    '#cols' => 20,
-    '#rows' => 5,
+    '#type' => 'markup',
+    '#value' => theme('table', $header, $rows, array()),
+  );
+  $form['issue']['components_link'] = array(
+    '#type' => 'markup',
+    '#value' => l(t('Add a component'), "node/$node->nid/edit/components/add"),
   );
+
   $form['issue']['help'] = array(
     '#type' => 'textarea',
     '#title' => t('Submission guidelines'),
@@ -789,11 +826,10 @@
  * @see project_issue_project_edit_issues
  */
 function project_issue_project_edit_form_submit($form, &$form_state) {
-  $components = serialize(explode("\n", str_replace("\r", '', $form_state['values']['components'])));
   $mail_copy_filter = serialize($form_state['values']['mail_copy_filter']);
   $mail_copy_filter_state = serialize($form_state['values']['mail_copy_filter_state']);
 
-  db_query("UPDATE {project_issue_projects} SET issues = %d, components = '%s', mail_digest = '%s', mail_reminder = %d, mail_copy = '%s', mail_copy_filter = '%s', mail_copy_filter_state = '%s', help = '%s' WHERE nid = %d", $form_state['values']['issues'], $components, $form_state['values']['mail_digest'], $form_state['values']['mail_reminder'], $form_state['values']['mail_copy'], $mail_copy_filter, $mail_copy_filter_state, $form_state['values']['help'], $form_state['values']['nid']);
+  db_query("UPDATE {project_issue_projects} SET issues = %d, mail_digest = '%s', mail_reminder = %d, mail_copy = '%s', mail_copy_filter = '%s', mail_copy_filter_state = '%s', help = '%s' WHERE nid = %d", $form_state['values']['issues'], $form_state['values']['mail_digest'], $form_state['values']['mail_reminder'], $form_state['values']['mail_copy'], $mail_copy_filter, $mail_copy_filter_state, $form_state['values']['help'], $form_state['values']['nid']);
   db_query("UPDATE {node} SET changed = %d WHERE nid = %d", time(), $form_state['values']['nid']);
   drupal_set_message(t('Issue settings have been saved.'));
 }
Index: includes/batch.inc
===================================================================
RCS file: includes/batch.inc
diff -N includes/batch.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/batch.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,83 @@
+<?php
+function project_issue_batch_delete_component_current_values($project, $old_name, $new_name, &$context) {
+  $components = array();
+  foreach ($project->project_issue['components'] as $component) {
+    if ($component != $old_name) {
+      $components[] = $component;
+    }
+  }
+
+  db_query("UPDATE {project_issue_projects} SET components = '%s' WHERE nid = %d", serialize($components), $project->nid);
+
+  // Update the *current* component for all the issues in one go.
+  db_query("UPDATE {project_issues} SET component = '%s' WHERE pid = %d AND component = '%s'", $new_name, $project->nid, $old_name);
+  $context['results']['issues_updated']  = db_affected_rows();
+
+  // Update all the comments in one go
+  db_query("UPDATE {project_issue_comments} SET component = '%s' WHERE pid = %d AND component = '%s'", $new_name, $project->nid, $old_name);
+  $context['results']['comments_updated']  = db_affected_rows();
+
+  $context['finished'] = 1;
+}
+
+function project_issue_batch_change_component_current_values($project, $old_name, $new_name, &$context) {
+  $components = $project->project_issue['components'];
+  $components[array_search($old_name, $components, TRUE)] = $new_name;
+  db_query("UPDATE {project_issue_projects} SET components = '%s' WHERE nid = %d", serialize($components), $project->nid);
+
+  // Update the *current* component for all the issues in one go.
+  db_query("UPDATE {project_issues} SET component = '%s' WHERE pid = %d AND component = '%s'", $new_name, $project->nid, $old_name);
+  $context['results']['issues_updated']  = db_affected_rows();
+
+  // Update all the comments in one go
+  db_query("UPDATE {project_issue_comments} SET component = '%s' WHERE pid = %d AND component = '%s'", $new_name, $project->nid, $old_name);
+  $context['results']['comments_updated']  = db_affected_rows();
+
+  $context['finished'] = 1;
+}
+
+function project_issue_batch_change_component_original_values($project, $old_name, $new_name, &$context) {
+  // Get the list of issues to update
+  if (!isset($context['sandbox']['issues'])) {
+    // We need to get every single issue from the database, since issues might have originally
+    // belonged to the project for which we're changing the component
+    $result = db_query('SELECT DISTINCT nid FROM {project_issues}');
+    while($nid = db_fetch_array($result)) {
+      $context['sandbox']['issues'][] = $nid['nid'];
+    }
+
+    $context['finished']                  = 0;
+    $context['sandbox']['max']            = count($context['sandbox']['issues']);
+    $context['sandbox']['progress']       = 0;
+    $conetxt['message']                   = 'Updated @current out of @total issues.';
+    $context['results']['orig_updated']   = 0;
+    $context['results']['processed']      = 0;
+    $context['results']['pid']            = $project->nid;
+
+    // Make sure there is at least one issue to update, otherwise set finished = 1 and
+    // stop processing immediately.
+    if (!$context['sandbox']['max']) {
+      $context['finished'] = 1;
+      return;
+    }
+  }
+
+  if ($context['sandbox']['issues']) {
+    $nid = array_pop($context['sandbox']['issues']);
+    // The original component for an issue is stored in a serialized array, so we have to
+    // load each of the issues, change the value, and save back to the database.
+    $original_values = unserialize(db_result(db_query('SELECT original_issue_data FROM {project_issues} WHERE nid = %d', $nid)));
+    if ($original_values->pid == $project->nid) {
+      if ($original_values->component == $old_name) {
+        $original_values->component = $new_name;
+        db_query("UPDATE {project_issues} SET original_issue_data = '%s' WHERE nid = %d", serialize($original_values), $nid);
+        $context['results']['orig_updated']++;
+      }
+    }
+  }
+  $context['sandbox']['progress']++;
+  $context['results']['processed']++;
+
+  // Update the completion level.
+  $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+}
