=== modified file 'includes/form.inc'
--- includes/form.inc	2009-12-17 21:59:31 +0000
+++ includes/form.inc	2009-12-29 19:17:35 +0000
@@ -879,15 +879,26 @@ function drupal_redirect_form($form_stat
  *   theming, and hook_form_alter functions.
  */
 function _form_validate(&$elements, &$form_state, $form_id = NULL) {
-  // Also used in the installer, pre-database setup.
-  $t = get_t();
-
   // Recurse through all children.
   foreach (element_children($elements) as $key) {
-    if (isset($elements[$key]) && $elements[$key]) {
-      _form_validate($elements[$key], $form_state);
-    }
+    _form_validate($elements[$key], $form_state);
+  }
+  // Check whether validation needs to be skipped according to the
+  // #validate_values of the clicked button. This can not be moved above
+  // as we must recurse to all form elements even if themselves are not
+  // validated because #validate_values can contain something like
+  // array('foo', 'bar') and then $form_state['values']['foo'] itself won't be
+  // validated but if recursion skips it, then
+  // $form_state['values']['foo']['bar] would be skipped too.
+  // After the complete form has been validated the button-level #validate
+  // handlers are executed via form_execute_handlers() so it must not be
+  // skipped.
+  if (!isset($form_id) && _form_validate_skip($elements, $form_state)) {
+    return;
   }
+  // Also used in the installer, pre-database setup.
+  $t = get_t();
+
   // Validate the current input.
   if (!isset($elements['#validated']) || !$elements['#validated']) {
     if (isset($elements['#needs_validation'])) {
@@ -947,6 +958,50 @@ function _form_validate(&$elements, &$fo
 }
 
 /**
+ * Check if the validation of an element needs to be skipped.
+ *
+ * Multistep forms not wanting to validate the whole form can set the
+ * #validate_values property on buttons to avoid the validation of some
+ * elements. For example, pressing the "Previous" button should not fire
+ * validation errors just because the current step has invalid values. AJAX is
+ * another typical example.
+ *
+ * If this property is set on the clicked button the form-level #validate and
+ * #submit handlers won't be executed. Only button-level #validate and #submit
+ * handlers will be executed and there should be extreme care taken because
+ * $form_state['values'] will contain values not validated. However, this
+ * typically is not a problem as these buttons (like Previous or Add more) do
+ * not persist anything. Do not use this property if the button triggers saving
+ * to the database.
+ *
+ * This property is a list of $form_state['values'] keys. For example:
+ * @code
+ *   $form['submit]['#validate_values'] = array(
+ *     array('foo', 'bar'),
+ *     array('step1'),
+ *   );
+ * @endcode
+ * This will validate $form_state['values']['step1']['choice'] as the first key
+ * is step1. Anything under $form_values['step2'] won't be validated.
+ * $form_state['values']['foo'] won't be validated but everything under
+ * $form_state['values']['foo']['bar'] will be.
+ */
+function _form_validate_skip($elements, $form_state) {
+  if (isset($form_state['clicked_button']['#validate_values'])) {
+    // The Previous button mentioned above needs nothing to be validated.
+    if (!$form_state['clicked_button']['#validate_values']) {
+      return TRUE;
+    }
+    foreach ($form_state['clicked_button']['#validate_values'] as $section) {
+      if (array_slice($elements['#parents'], 0, count($section)) === $section) {
+        return FALSE;
+      }
+    }
+  }
+  return TRUE;
+}
+
+/**
  * A helper function used to execute custom validation and submission
  * handlers for a given form. Button-specific handlers are checked
  * first. If none exist, the function falls back to form-level handlers.
@@ -967,8 +1022,9 @@ function form_execute_handlers($type, &$
   if (isset($form_state[$type . '_handlers'])) {
     $handlers = $form_state[$type . '_handlers'];
   }
-  // Otherwise, check for a form-level handler.
-  elseif (isset($form['#' . $type])) {
+  // Otherwise, check for a form-level handler, but only if the complete form
+  // was validated.
+  elseif (isset($form['#' . $type]) && !isset($form_state['clicked_button']['#validate_values'])) {
     $handlers = $form['#' . $type];
   }
   else {
@@ -1006,7 +1062,7 @@ function form_execute_handlers($type, &$
  * @param $message
  *   The error message to present to the user.
  * @return
- *   Return value is for internal use only. To get a list of errors, use 
+ *   Return value is for internal use only. To get a list of errors, use
  *   form_get_errors() or form_get_error().
  */
 function form_set_error($name = NULL, $message = '') {
@@ -3261,7 +3317,7 @@ function batch_process($redirect = NULL,
   $batch =& batch_get();
 
   drupal_theme_initialize();
-  
+
   if (isset($batch)) {
     // Add process information
     $process_info = array(
@@ -3276,7 +3332,7 @@ function batch_process($redirect = NULL,
     );
     $batch += $process_info;
 
-    // The batch is now completely built. Allow other modules to make changes to the 
+    // The batch is now completely built. Allow other modules to make changes to the
     // batch so that it is easier to reuse batch processes in other enviroments.
     drupal_alter('batch', $batch);
 

=== modified file 'modules/field/field.form.inc'
--- modules/field/field.form.inc	2009-12-21 13:47:31 +0000
+++ modules/field/field.form.inc	2009-12-29 16:33:57 +0000
@@ -214,6 +214,7 @@ function field_multiple_value_form($fiel
           '#name' => $field_name . '_add_more',
           '#value' => t('Add another item'),
           '#attributes' => array('class' => array('field-add-more-submit')),
+          '#validate_values' => array(array($field_name, $langcode)),
           // Submit callback for disabled JavaScript.
           '#submit' => array('field_add_more_submit'),
           '#ajax' => array(

=== modified file 'modules/poll/poll.module'
--- modules/poll/poll.module	2009-12-26 16:50:08 +0000
+++ modules/poll/poll.module	2009-12-29 19:09:47 +0000
@@ -250,6 +250,12 @@ function poll_form($node, &$form_state) 
   $delta = 0;
   $weight = 0;
   if (isset($node->choice)) {
+    // Reset chid when "more choices" has been pressed. Normally validate does
+    // this but when "more choices" is pressed then the form level validate is
+    // executed.
+    if (isset($form_state['node'])) {
+      $node->choice = array_values($node->choice);
+    }
     $delta = count($node->choice);
     $weight = -$delta;
     foreach ($node->choice as $chid => $choice) {
@@ -273,6 +279,7 @@ function poll_form($node, &$form_state) 
     '#value' => t('More choices'),
     '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
     '#weight' => 1,
+    '#validate_values' => array(array('choice')),
     '#submit' => array('poll_more_choices_submit'), // If no javascript action.
     '#ajax' => array(
       'callback' => 'poll_choice_js',
@@ -901,4 +908,3 @@ function poll_user_cancel($edit, $accoun
       break;
   }
 }
-

=== modified file 'modules/simpletest/tests/form.test'
--- modules/simpletest/tests/form.test	2009-12-17 17:18:02 +0000
+++ modules/simpletest/tests/form.test	2009-12-29 15:16:10 +0000
@@ -231,6 +231,26 @@ class FormValidationTestCase extends Dru
     $this->assertNoFieldByName('name', t('Form element was hidden.'));
     $this->assertText('Name value: element_validate_access', t('Value for inaccessible form element exists.'));
   }
+
+  /**
+   * Test button press validates only certain values.
+   */
+  function testValidateValues() {
+    $edit = array('test' => 'pass');
+    $path = 'form-test/validate-values';
+    // Test partial form validation.
+    $this->drupalPost($path, $edit, t('Partial validate'));
+    // Make sure the title field is not validated.
+    $this->assertNoText(t('!name field is required.', array('!name' => 'Title')));
+    // Make sure the #element_validate handler for the test field is executed.
+    $this->assertText('Test is validated');
+    // Now test full form validation.
+    $this->drupalPost($path, $edit, t('Full validate'));
+    // Make sure the title field is validated.
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    // Make sure the #element_validate handler for the test field is executed.
+    $this->assertText('Test is validated');
+  }
 }
 
 /**

=== modified file 'modules/simpletest/tests/form_test.module'
--- modules/simpletest/tests/form_test.module	2009-12-17 17:18:02 +0000
+++ modules/simpletest/tests/form_test.module	2009-12-29 16:33:20 +0000
@@ -17,6 +17,13 @@ function form_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/validate-values'] = array(
+    'title' => 'Form validation values test_form',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_validate_values_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -204,6 +211,40 @@ function form_test_validate_form_validat
 }
 
 /**
+ * Builds a simple form with a button triggerring partial validation.
+ */
+function form_test_validate_values_form($form) {
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+  );
+  $form['test'] = array(
+    '#type' => 'textfield',
+    '#element_validate' => array('form_test_validate_values_form_validate_test'),
+  );
+  $form['buttons']['partial'] = array(
+    '#type' => 'submit',
+    '#validate_values' => array(array('test')),
+    '#value' => t('Partial validate'),
+  );
+  $form['buttons']['full'] = array(
+    '#type' => 'submit',
+    '#value' => t('Full validate'),
+  );
+  return $form;
+}
+
+/**
+ * Element handler for the test element.
+ */
+function form_test_validate_values_form_validate_test($form, $form_state) {
+  if ($form_state['values']['test'] == 'pass') {
+    drupal_set_message('Test is validated');
+  }
+}
+
+/**
  * Create a header and options array. Helper function for callbacks.
  */
 function _form_test_tableselect_get_data() {
@@ -895,7 +936,7 @@ function form_test_state_persist($form, 
 
 /**
  * Submit handler.
- * 
+ *
  * @see form_test_state_persist()
  */
 function form_test_state_persist_submit($form, &$form_state) {
@@ -905,7 +946,7 @@ function form_test_state_persist_submit(
 
 /**
  * Implements hook_form_FORM_ID_alter().
- * 
+ *
  * @see form_test_state_persist()
  */
 function form_test_form_form_test_state_persist_alter(&$form, &$form_state) {

