Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.288
diff -u -r1.288 poll.module
--- modules/poll/poll.module	3 Feb 2009 18:55:31 -0000	1.288
+++ modules/poll/poll.module	11 Feb 2009 05:43:52 -0000
@@ -258,6 +258,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 more button.
+    '#validate_section' => array('choice_wrapper', 'choice'),
     '#ahah' => array(
       'callback' => 'poll_choice_js',
       'wrapper' => 'poll-choices',
@@ -304,7 +306,9 @@
  */
 function poll_more_choices_submit($form, &$form_state) {
   // Set the form to rebuild and run submit handlers.
-  node_form_submit_build_node($form, $form_state);
+  if (drupal_function_exists('node_form_submit_build_node')) {
+    node_form_submit_build_node($form, $form_state);
+  }
 
   // Make the changes we want to the form state.
   if ($form_state['values']['poll_more']) {
Index: modules/poll/poll.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v
retrieving revision 1.15
diff -u -r1.15 poll.test
--- modules/poll/poll.test	1 Feb 2009 06:48:15 -0000	1.15
+++ modules/poll/poll.test	11 Feb 2009 05:43:52 -0000
@@ -178,11 +178,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
@@ -201,8 +197,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[chid:0][chtext]', '', t('Field !i found', array('!i' => 0)));
+    $this->assertFieldByName('choice[chid:1][chtext]', '', t('Field !i found', array('!i' => 1)));
     $this->assertFieldByName('choice[new:0][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.3
diff -u -r1.3 form.test
--- modules/simpletest/tests/form.test	28 Jan 2009 07:43:26 -0000	1.3
+++ modules/simpletest/tests/form.test	11 Feb 2009 05:43:52 -0000
@@ -68,8 +68,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 pass.
+   */
+  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);
+    }
   }
 }
 
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.320
diff -u -r1.320 form.inc
--- includes/form.inc	3 Feb 2009 18:55:29 -0000	1.320
+++ includes/form.inc	11 Feb 2009 05:43:52 -0000
@@ -581,7 +581,21 @@
     }
   }
 
-  _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.
+  $validate_section = NULL;
+  if (isset($form_state['clicked_button'])) {
+    $button = $form_state['clicked_button'];
+    if (isset($button['#validate_section']) && isset($button['#submit'])) {
+      $validate_section = $button['#validate_section'];
+    }
+  }
+
+  // Validate individual #element_validate, #required, and #options properties.
+  _form_validate($form, $form_state, $validate_section, $form_id);
+
   $validated_forms[$form_id] = TRUE;
 }
 
@@ -657,11 +671,15 @@
  *   This technique is useful when validation requires file parsing,
  *   web service requests, or other expensive requests that should
  *   not be repeated in the submission step.
+ * @param $validate_section
+ *   An array of parents indicating the part of the form that should be
+ *   validated. For example, if needing to validate only the $form['foo']['bar']
+ *   portion of the form, pass in array('foo', 'bar').
  * @param $form_id
  *   A unique string identifying the form for validation, submission,
  *   theming, and hook_form_alter functions.
  */
-function _form_validate($elements, &$form_state, $form_id = NULL) {
+function _form_validate($elements, &$form_state, $validate_section = NULL, $form_id = NULL) {
   static $complete_form;
 
   // Also used in the installer, pre-database setup.
@@ -670,7 +688,18 @@
   // Recurse through all children.
   foreach (element_children($elements) as $key) {
     if (isset($elements[$key]) && $elements[$key]) {
-      _form_validate($elements[$key], $form_state);
+      // If not using partial validation, validate all elements.
+      if (!isset($validate_section)) {
+        _form_validate($elements[$key], $form_state);
+      }
+      // Check if this is the sub-section of the form that needs validation.
+      // As we go deeper into the form, the first item of $validate_section
+      // contains the next key we need to validate. If the $validate_section
+      // array is empty, we are already inside the part that needs validating.
+      elseif (empty($validate_section) || $validate_section[0] == $key) {
+        array_shift($validate_section);
+        _form_validate($elements[$key], $form_state, $validate_section);
+      }
     }
   }
   // Validate the current input.
