Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.7
diff -u -r1.7 ajax.inc
--- includes/ajax.inc	27 Aug 2009 04:40:12 -0000	1.7
+++ includes/ajax.inc	27 Aug 2009 20:04:56 -0000
@@ -186,6 +186,9 @@
   // Since some of the submit handlers are run, redirects need to be disabled.
   $form['#redirect'] = FALSE;
 
+  // Use the validate section handling on the clicked button (if available).
+  $form['#validate_section_allowed'] = TRUE;
+
   // The form needs to be processed; prepare for that by setting a few internal
   // variables.
   $form_state['input'] = $_POST;
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.367
diff -u -r1.367 form.inc
--- includes/form.inc	27 Aug 2009 04:40:12 -0000	1.367
+++ includes/form.inc	27 Aug 2009 20:04:57 -0000
@@ -705,13 +705,26 @@
   // If the session token was set by drupal_prepare_form(), ensure that it
   // matches the current user's session.
   if (isset($form['#token'])) {
-    if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) {
+    if (!drupal_valid_token($form['form_token']['#value'], $form['#token'])) {
       // Setting this error will cause the form to fail validation.
       form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.'));
     }
   }
 
-  _form_validate($form, $form_state, $form_id);
+  // Allow only a portion of the form to be validated. This is only allowed if
+  // the #validate_section property is on a button AND a custom #submit property
+  // is defined. This prevents the form-level submit handlers from firing
+  // accidentally when a portion of the form has not been validated.
+  $elements = $form;
+  $button = isset($form_state['clicked_button']) ? $form_state['clicked_button'] : FALSE;
+  if ($button && !empty($form['#validate_section_allowed']) && isset($button['#validate_section']) && isset($button['#submit'])) {
+    foreach ($button['#validate_section'] as $section) {
+      $elements = $elements[$section];
+    }
+  }
+
+  // Validate the section of the form.
+  _form_validate($elements, $form_state, $form_id);
   $validated_forms[$form_id] = TRUE;
 }
 
@@ -768,7 +781,8 @@
  *   not be repeated in the submission step.
  * @param $form_id
  *   A unique string identifying the form for validation, submission,
- *   theming, and hook_form_alter functions.
+ *   theming, and hook_form_alter functions. Indicates that we are starting a
+ *   new validate operation.
  */
 function _form_validate($elements, &$form_state, $form_id = NULL) {
   // Also used in the installer, pre-database setup.
@@ -777,6 +791,11 @@
   // Recurse through all children.
   foreach (element_children($elements) as $key) {
     if (isset($elements[$key]) && $elements[$key]) {
+      // Only validated elements receive a value.
+      if (array_key_exists('#value', $elements[$key])) {
+        dsm($elements[$key]['#value']);
+        form_set_value($elements[$key], $elements[$key]['#value'], $form_state);
+      }
       _form_validate($elements[$key], $form_state);
     }
   }
@@ -827,7 +846,7 @@
     // #value data.
     elseif (isset($elements['#element_validate'])) {
       foreach ($elements['#element_validate'] as $function) {
-        if (function_exists($function))  {
+        if (function_exists($function) && $function != '_form_validate')  {
           $function($elements, $form_state, $form_state['complete form']);
         }
       }
@@ -1184,7 +1203,6 @@
       }
     }
   }
-  form_set_value($element, $element['#value'], $form_state);
 }
 
 /**
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.310
diff -u -r1.310 poll.module
--- modules/poll/poll.module	24 Aug 2009 00:14:21 -0000	1.310
+++ modules/poll/poll.module	27 Aug 2009 20:04:57 -0000
@@ -256,20 +256,17 @@
   $delta = 0;
   $weight = 0;
   if (isset($node->choice)) {
-    $delta = count($node->choice);
-    $weight = -$delta;
-    foreach ($node->choice as $chid => $choice) {
-      $key = 'chid:' . $chid;
-      $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count);
+    foreach ($node->choice as $choice) {
+      $choice['chid'] = isset($choice['chid']) ? $choice['chid'] : NULL;
+      $form['choice_wrapper']['choice'][$delta] = _poll_choice_form($delta, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count);
       $weight = ($choice['weight'] > $weight) ? $choice['weight'] : $weight;
+      $delta++;
     }
   }
 
   // Add initial or additional choices.
-  $existing_delta = $delta;
   for ($delta; $delta < $choice_count; $delta++) {
-    $key = 'new:' . ($delta - $existing_delta);
-    $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, NULL, '', 0, $weight, $choice_count);
+    $form['choice_wrapper']['choice'][$delta] = _poll_choice_form($delta, NULL, '', 0, $weight, $choice_count);
   }
 
   // We name our button 'poll_more' to avoid conflicts with other modules using
@@ -280,6 +277,8 @@
     '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
     '#weight' => 1,
     '#submit' => array('poll_more_choices_submit'), // If no javascript action.
+    '#validate' => array(), // No form-level validation for this button.
+    '#validate_section' => array('choice_wrapper', 'choice'),
     '#ajax' => array(
       'callback' => 'poll_choice_js',
       'wrapper' => 'poll-choices',
@@ -325,14 +324,25 @@
  * entire form is rebuilt during the page reload.
  */
 function poll_more_choices_submit($form, &$form_state) {
-  include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc';
-  // Set the form to rebuild and run submit handlers.
-  node_form_submit_build_node($form, $form_state);
+  module_load_include('inc', 'node', 'node.pages');
+  // Non-JavaScript behavior. Entire form is validated and 5 choices are added.
+  // The node must be rebuilt to populate the $node variable for hook_form().
+  if (empty($form['#validate_section_allowed'])) {
+    node_form_submit_build_node($form, $form_state);
+    $new_choices = 5;
+  }
+  // JavaScript behavior. Only the choice section is validated, and 1 choice is
+  // added. We don't need to (and can't) rebuild the node because only the
+  // validated section of the form is given any values.
+  else {
+    $form_state['node']['choice'] = array_values($form_state['values']['choice']);
+    $form_state['rebuild'] = TRUE;
+    $new_choices = 1;
+  }
 
   // Make the changes we want to the form state.
-  if ($form_state['values']['poll_more']) {
-    $n = $_GET['q'] == 'system/ajax' ? 1 : 5;
-    $form_state['choice_count'] = count($form_state['values']['choice']) + $n;
+  if ($form_state['values']['op'] == $form['choice_wrapper']['poll_more']['#value']) {
+    $form_state['choice_count'] = count($form_state['values']['choice']) + $new_choices;
   }
 }
 
Index: modules/poll/poll.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v
retrieving revision 1.22
diff -u -r1.22 poll.test
--- modules/poll/poll.test	17 Aug 2009 07:12:16 -0000	1.22
+++ modules/poll/poll.test	27 Aug 2009 20:04:57 -0000
@@ -60,11 +60,14 @@
     $edit = array(
       'title' => $title
     );
-    foreach ($already_submitted_choices as $k => $text) {
-      $edit['choice[chid:' . $k . '][chtext]'] = $text;
+    $delta = 0;
+    foreach ($already_submitted_choices as $text) {
+      $edit['choice[' . $delta . '][chtext]'] = $text;
+      $delta++;
     }
-    foreach ($new_choices as $k => $text) {
-      $edit['choice[new:' . $k . '][chtext]'] = $text;
+    foreach ($new_choices as $text) {
+      $edit['choice[' . $delta . '][chtext]'] = $text;
+      $delta++;
     }
     return array($edit, count($already_submitted_choices) + count($new_choices));
   }
@@ -331,11 +334,7 @@
     $web_user = $this->drupalCreateUser(array('create poll content', 'access content'));
     $this->drupalLogin($web_user);
     $this->drupalGet('node/add/poll');
-    $edit = array(
-      'title' => $this->randomName(),
-      'choice[new:0][chtext]' => $this->randomName(),
-      'choice[new:1][chtext]' => $this->randomName(),
-    );
+    $edit = array();
 
     // @TODO: the framework should make it possible to submit a form to a
     // different URL than its action or the current. For now, we can just force
@@ -361,8 +360,8 @@
 
     // Needs to be emptied out so the new content will be parsed.
     $this->elements = '';
-    $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0)));
-    $this->assertFieldByName('choice[chid:1][chtext]', $edit['choice[new:1][chtext]'], t('Field !i found', array('!i' => 1)));
-    $this->assertFieldByName('choice[new:0][chtext]', '', t('Field !i found', array('!i' => 2)));
+    $this->assertFieldByName('choice[0][chtext]', '', t('Field !i found', array('!i' => 0)));
+    $this->assertFieldByName('choice[1][chtext]', '', t('Field !i found', array('!i' => 1)));
+    $this->assertFieldByName('choice[2][chtext]', '', t('Field !i found', array('!i' => 2)));
   }
 }
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.14
diff -u -r1.14 form.test
--- modules/simpletest/tests/form.test	13 Jul 2009 21:51:41 -0000	1.14
+++ modules/simpletest/tests/form.test	27 Aug 2009 20:04:58 -0000
@@ -69,8 +69,111 @@
         $this->assertTrue(isset($errors[$element]), "Check empty($key) '$type' field '$element'");
       }
     }
-    // Clear the expected form error messages so they don't appear as exceptions.
+    // Clear errors and messages.
     drupal_get_messages();
+    form_set_error(NULL, '', TRUE);
+  }
+
+  /**
+   * Validate a section of a form but not others.
+   *
+   * If the validated form fields are found in form_get_errors() and the
+   * non-validated field is not found then the test passes.
+   */
+  function testValidateSection() {
+    // A list of elements that will intentionally fail validation.
+    $validated_elements = array(
+      $this->randomName(),
+      $this->randomName(),
+    );
+
+    // A list of elements that will intentionally be skipped in validation.
+    $skipped_elements = array(
+      $this->randomName(),
+    );
+
+    // A fieldset to test validation within a nested field.
+    $fieldset = $this->randomName();
+
+    // Test both with a #submit handler and without one.
+    // The #submit property is required when using #validate_section.
+    $ops = array(
+      'with_submit' => array(
+        '#type' => 'submit',
+        '#value' => t('Submit'),
+        '#validate_section' => array($fieldset),
+        '#test_skipped_elements' => drupal_map_assoc($skipped_elements),
+        '#test_validate_elements' => drupal_map_assoc($validated_elements),
+        '#submit' => array(), // Required!
+      ),
+      'without_submit' => array(
+        '#type' => 'submit',
+        '#value' => t('Submit'),
+        '#validate_section' => array($fieldset),
+        '#test_skipped_elements' => array(),
+        '#test_validate_elements' => drupal_map_assoc(array_merge($skipped_elements, $validated_elements)),
+      ),
+    );
+
+    foreach ($ops as $test => $op) {
+      $form = array();
+      $form[$skipped_elements[0]] = array(
+        '#type' => 'textfield',
+        '#required' => TRUE,
+      );
+      $form[$fieldset] = array(
+        '#type' => 'fieldset',
+        '#tree' => TRUE,
+      );
+      $form[$fieldset][$validated_elements[0]] = array(
+        '#type' => 'textfield',
+        '#required' => TRUE,
+      );
+      $form[$fieldset][$this->randomName()][$validated_elements[1]] = array(
+        '#type' => 'textfield',
+        '#required' => TRUE,
+      );
+      $form['op'] = $op;
+
+      $form_id = $this->randomName();
+      $form_state = array();
+      $form_state['values'] = array(
+        'op' => t('Submit'),
+      );
+
+      $form['#post'] = $form_state['values'];
+      $form['#post']['form_id'] = $form_id;
+      drupal_prepare_form($form_id, $form, $form_state);
+      drupal_process_form($form_id, $form, $form_state);
+      $errors = form_get_errors();
+
+      $skipped = $op['#test_skipped_elements'];
+      $validated = array();
+      foreach ($errors as $element_tree => $error) {
+        foreach ($op['#test_validate_elements'] as $element) {
+          if (strpos($element_tree, $element) !== FALSE) {
+            $validated[$element] = $element;
+          }
+        }
+        foreach ($op['#test_skipped_elements'] as $element) {
+          if (strpos($element_tree, $element) !== FALSE) {
+            unset($skipped[$element]);
+          }
+        }
+      }
+      if ($test == 'with_submit') {
+        $this->assertTrue($skipped == $op['#test_skipped_elements'], "Form element validation intentionally skipped on a portion of the form.");
+        $this->assertTrue($validated == $op['#test_validate_elements'], "All elements within the validated section successfully validated.");
+      }
+      elseif ($test == 'without_submit') {
+        $this->assertTrue($skipped == $op['#test_skipped_elements'], "No elements were skipped because the button #submit property is not set.");
+        $this->assertTrue($validated == $op['#test_validate_elements'], "The entire form was validated because the button #submit property is not set.");
+      }
+
+      // Clear errors and messages.
+      drupal_get_messages();
+      form_set_error(NULL, '', TRUE);
+    }
   }
 }
 
