=== modified file 'includes/form.inc'
--- includes/form.inc	2009-12-29 20:16:09 +0000
+++ includes/form.inc	2010-01-01 14:29:15 +0000
@@ -888,19 +888,11 @@ function _form_validate(&$elements, &$fo
       _form_validate($elements[$key], $form_state);
     }
   }
+
   // Validate the current input.
   if (!isset($elements['#validated']) || !$elements['#validated']) {
+    // The following errors are always showed.
     if (isset($elements['#needs_validation'])) {
-      // Make sure a value is passed when the field is required.
-      // A simple call to empty() will not cut it here as some fields, like
-      // checkboxes, can return a valid value of '0'. Instead, check the
-      // length if it's a string, and the item count if it's an array.
-      // An unchecked checkbox has a #value of numeric 0, different than string
-      // '0', which could be a valid value.
-      if ($elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
-        form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
-      }
-
       // Verify that the value is not longer than #maxlength.
       if (isset($elements['#maxlength']) && drupal_strlen($elements['#value']) > $elements['#maxlength']) {
         form_error($elements, $t('!name cannot be longer than %max characters but is currently %length characters long.', array('!name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title'], '%max' => $elements['#maxlength'], '%length' => drupal_strlen($elements['#value']))));
@@ -929,6 +921,36 @@ function _form_validate(&$elements, &$fo
       }
     }
 
+    // While this element is being validated, it may be desired that some calls
+    // to form_set_error() be suppressed and not result in a form error, so
+    // that a button that implements low-risk functionality (such as "Previous"
+    // or "Add more") that doesn't require all user input to be valid can still
+    // have its submit handlers triggered. The clicked button's #show_errors
+    // property contains the information for which errors are needed, and all
+    // other errors are to be suppressed. The #show_errors property is ignored
+    // if the button doesn't define its own submit handlers, because it's too
+    // large a security risk to allow any error suppression if form-level
+    // submit handlers will be executed.
+    if (isset($form_state['clicked_button']['#show_errors']) && isset($form_state['clicked_button']['#submit'])) {
+      form_set_error(NULL, '', $form_state['clicked_button']['#show_errors']);
+    }
+    else {
+      // As an extra security measure, explicitly turn off error suppression.
+      // Since we also do this at the end of this function, this only matters
+      // in a rare edge case where a validate handler invokes form processing
+      // of another form.
+      drupal_static_reset('form_set_error:show_errors');
+    }
+    // Make sure a value is passed when the field is required.
+    // A simple call to empty() will not cut it here as some fields, like
+    // checkboxes, can return a valid value of '0'. Instead, check the
+    // length if it's a string, and the item count if it's an array.
+    // An unchecked checkbox has a #value of numeric 0, different than string
+    // '0', which could be a valid value.
+    if (isset($elements['#needs_validation']) && $elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
+      form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
+    }
+
     // Call user-defined form level validators.
     if (isset($form_id)) {
       form_execute_handlers('validate', $elements, $form_state);
@@ -944,6 +966,11 @@ function _form_validate(&$elements, &$fo
     }
     $elements['#validated'] = TRUE;
   }
+
+  // Done validating this element, so turn off error suppression.
+  // _form_validate() turns it on again when starting on the next element, if
+  // it's still appropriate to do so.
+  drupal_static_reset('form_set_error:show_errors');
 }
 
 /**
@@ -1005,18 +1032,80 @@ function form_execute_handlers($type, &$
  *   element where the #parents array starts with 'foo'.
  * @param $message
  *   The error message to present to the user.
+ * @param $show_errors
+ *   Internal use only. The #show_errors property of the clicked button if it
+ *   exists. Multistep forms not wanting to validate the whole form can set the
+ *   #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 #show_errors property is a list of $form_state['values'] keys. For
+ *   example:
+ *   @code
+ *     $form['actions']['previous']['#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 = '', $show_errors = NULL) {
   $form = &drupal_static(__FUNCTION__, array());
+  $show_sections = &drupal_static(__FUNCTION__ . ':show_errors');
+  if (isset($show_errors)) {
+    $show_sections = $show_errors;
+  }
+  
   if (isset($name) && !isset($form[$name])) {
-    $form[$name] = $message;
-    if ($message) {
-      drupal_set_message($message, 'error');
+    $record = TRUE;
+    if (isset($show_sections)) {
+      // #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. #show_errors can be an empty array, in
+      // which case all errors are 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;
+          break;
+        }
+      }
+    }
+    if ($record) {
+      $form[$name] = $message;
+      if ($message) {
+        drupal_set_message($message, 'error');
+      }
     }
   }
+
   return $form;
 }
 
@@ -3261,7 +3350,7 @@ function batch_process($redirect = NULL,
   $batch =& batch_get();
 
   drupal_theme_initialize();
-  
+
   if (isset($batch)) {
     // Add process information
     $process_info = array(
@@ -3276,7 +3365,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	2010-01-01 14:21:26 +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')),
+          '#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	2010-01-01 14:21:26 +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,
+    '#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	2010-01-01 14:21:26 +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 #show_errors.
+   */
+  function testValidateValues() {
+    $edit = array('test' => 'invalid');
+    $path = 'form-test/validate-values';
+
+    // Submit the form by pressing the button with #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	2010-01-01 14:21:26 +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_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_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_show_errors_element_validate_test'),
+  );
+  $form['actions']['partial'] = array(
+    '#type' => 'submit',
+    '#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_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) {

