=== modified file 'includes/form.inc'
--- includes/form.inc	2009-12-29 20:16:09 +0000
+++ includes/form.inc	2009-12-31 11:19:43 +0000
@@ -1005,18 +1005,77 @@ function form_execute_handlers($type, &$
  *   element where the #parents array starts with 'foo'.
  * @param $message
  *   The error message to present to the user.
+ * @param $validate_show_errors
+ *   The #validate_show_errors property of the clicked button if it exists.
+ *   Multistep forms not wanting to validate the whole form can set the
+ *   #validate_show_errors property on buttons to avoid validation errors of some
+ *   elements preventing the button's submit handlers from running. 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 button-level #submit
+ *   handlers will be executed even if there is invalid input, so extreme care
+ *   should be taken with respect to what is performed by those handlers. This is
+ *   typically not a problem with buttons like "Previous" or "Add more" that do
+ *   not invoke persistent storage of the submitted form values.
+ *
+ *   Do not use this property on buttons that trigger saving of form values to the
+ *   database.
+ *
+ *   The #validate_show_errors property is a list of $form_state['values'] keys. For
+ *   example:
+ *   @code
+ *     $form['actions']['previous']['#validate_show_errors'] = array(
+ *       array('foo', 'bar'),
+ *       array('step1'),
+ *     );
+ *   @endcode
+ *   This will require $form_state['values']['step1']['choice'] to be valid, since
+ *   the first key is 'step1'. Validation errors will be suppressed for
+ *   $form_state['values']['step2'] and everything under it.
+ *   Errors for $form_state['values']['foo'] will be suppressed, but errors will
+ *   not be suppressed for $form_state['values']['foo']['bar'] and everything
+ *   under it.
  * @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 = '') {
+function form_set_error($name = NULL, $message = '', $validate_show_errors = NULL) {
   $form = &drupal_static(__FUNCTION__, array());
+  $show_sections = &drupal_static(__FUNCTION__ . ':show_sections');
+  if (isset($validate_show_errors)) {
+    $show_sections = $validate_show_errors;
+  }
+  
   if (isset($name) && !isset($form[$name])) {
-    $form[$name] = $message;
-    if ($message) {
-      drupal_set_message($message, 'error');
+    $record = TRUE;
+    if (isset($show_sections)) {
+      // #validate_show_errors is an array of "sections" for which to not
+      // suppress errors. If the element is within any of the sections, no
+      // errors are recorded. #validate_show_errors can be an empty array, in
+      // which case all errors re suppressed. For example, a "Previous" button
+      // might want its submit action triggered even if none of the submitted
+      // values are valid.
+      $record = FALSE;
+      foreach ($show_sections as $section) {
+        // Exploding by '][' reconstructs the element's #parents. If this
+        // reconstructed #parents array begins with all the same entries as the
+        // specified section, then the element's values exist within the part
+        // of $form_state['values'] that the clicked button requires to be
+        // valid, so errors for this name must be recorded.
+        if (array_slice(explode('][', $name), 0, count($section)) === $section) {
+          $record = TRUE;
+        }
+      }
+    }
+    if ($record) {
+      $form[$name] = $message;
+      if ($message) {
+        drupal_set_message($message, 'error');
+      }
     }
   }
+
   return $form;
 }
 
@@ -1312,6 +1371,9 @@ function _form_builder_handle_input_elem
       // the global variables. In this special case, we want to make sure that
       // the value of this element is listed in $form_variables under 'op'.
       $form_state['values'][$element['#name']] = $element['#value'];
+      if (isset($element['#validate_show_errors'])) {
+        form_set_error(NULL, '', $element['#validate_show_errors']);
+      }
       $form_state['clicked_button'] = $element;
 
       if (isset($element['#validate'])) {
@@ -1378,6 +1440,9 @@ function _form_builder_ie_cleanup($form,
       $form_state['submit_handlers'] = empty($button['#submit']) ? NULL : $button['#submit'];
       $form_state['validate_handlers'] = empty($button['#validate']) ? NULL : $button['#validate'];
       $form_state['values'][$button['#name']] = $button['#value'];
+      if (isset($button['#validate_show_errors'])) {
+        form_set_error(NULL, '', $button['#validate_show_errors']);
+      }
       $form_state['clicked_button'] = $button;
     }
   }
@@ -3261,7 +3326,7 @@ function batch_process($redirect = NULL,
   $batch =& batch_get();
 
   drupal_theme_initialize();
-  
+
   if (isset($batch)) {
     // Add process information
     $process_info = array(
@@ -3276,7 +3341,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-31 10:32:59 +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_show_errors' => 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-31 10:32:59 +0000
@@ -273,6 +273,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_show_errors' => array(array('choice')),
     '#submit' => array('poll_more_choices_submit'), // If no javascript action.
     '#ajax' => array(
       'callback' => 'poll_choice_js',

=== 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-31 10:32:59 +0000
@@ -231,6 +231,27 @@ 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.'));
   }
+
+  /**
+   * Tests partial form validation through #validate_show_errors.
+   */
+  function testValidateValues() {
+    $edit = array('test' => 'invalid');
+    $path = 'form-test/validate-values';
+
+    // Submit the form by pressing the button with #validate_show_errors and ensure
+    // that the title field is not validated, but the #element_validate handler
+    // for the 'test' field is triggered.
+    $this->drupalPost($path, $edit, t('Partial validate'));
+    $this->assertNoText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Test element is invalid');
+
+    // Now test full form validation and ensure that the #element_validate
+    // handler is still triggered.
+    $this->drupalPost($path, $edit, t('Full validate'));
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Test element is invalid');
+  }
 }
 
 /**

=== 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-31 10:32:59 +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_show_errors_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -204,6 +211,41 @@ function form_test_validate_form_validat
 }
 
 /**
+ * Builds a simple form with a button triggering partial validation.
+ */
+function form_test_validate_show_errors_form($form, &$form_state) {
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+  );
+  $form['test'] = array(
+    '#type' => 'textfield',
+    '#element_validate' => array('form_test_validate_show_errors_element_validate_test'),
+  );
+  $form['actions']['partial'] = array(
+    '#type' => 'submit',
+    '#validate_show_errors' => array(array('test')),
+    '#submit' => array(),
+    '#value' => t('Partial validate'),
+  );
+  $form['actions']['full'] = array(
+    '#type' => 'submit',
+    '#value' => t('Full validate'),
+  );
+  return $form;
+}
+
+/**
+ * Form element validation handler for the 'test' element.
+ */
+function form_test_validate_show_errors_element_validate_test(&$element, &$form_state) {
+  if ($element['#value'] == 'invalid') {
+    form_error($element, 'Test element is invalid');
+  }
+}
+
+/**
  * Create a header and options array. Helper function for callbacks.
  */
 function _form_test_tableselect_get_data() {
@@ -895,7 +937,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 +947,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) {

