Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.411
diff -u -p -r1.411 form.inc
--- includes/form.inc	1 Dec 2009 16:28:57 -0000	1.411
+++ includes/form.inc	2 Dec 2009 12:09:46 -0000
@@ -870,7 +870,7 @@ function _form_validate(&$elements, &$fo
       // 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)) {
+      if ($elements['#required'] && !_form_validate_skip($elements, $form_state) && (!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'])));
       }
 
@@ -908,7 +908,7 @@ function _form_validate(&$elements, &$fo
     }
     // Call any element-specific validators. These must act on the element
     // #value data.
-    elseif (isset($elements['#element_validate'])) {
+    elseif (isset($elements['#element_validate']) && !_form_validate_skip($elements, $form_state)) {
       foreach ($elements['#element_validate'] as $function) {
         if (function_exists($function))  {
           $function($elements, $form_state, $form_state['complete form']);
@@ -920,6 +920,65 @@ function _form_validate(&$elements, &$fo
 }
 
 /**
+ * Check if an element's validation can be skipped.
+ *
+ * A form button may specify the #validate_parents property to limit form
+ * validation to form elements that share the same #parents with
+ * #validate_parents. Such buttons are required to also define custom #submit
+ * handlers, which will ONLY process form values for elements contained in
+ * #validate_parents.
+ *
+ * For example, this is used to skip validation of #required and
+ * #element_validate properties for other elements in the form (such as a
+ * required title) and process submitted form values to add another item
+ * belonging to the field of the pressed submit button.
+ *
+ * This will never skip properties that are enforced by HTML, such as #maxlength
+ * or #options.
+ *
+ * @param $element
+ *   The element whose parents will be checked to determine if this element's
+ *   validation should be skipped.
+ * @param $form_state
+ *   A keyed array containing the current state of the form. The
+ *   'clicked_button' property is checked to determine which elements should be
+ *   validated.
+ *
+ * @return
+ *   Boolean TRUE if the element should not be validated. FALSE if the element
+ *   should be validated.
+ *
+ * @todo D8: This is should live in _form_builder_handle_input_element(),
+ *   because when taking a step back, we want to skip input processing for
+ *   unrelated elements in reality. However, we currently cannot skip input
+ *   processing, because node/field validate/submit handlers cast
+ *   $form_state['values'] into an object and presume that doing so results in
+ *   an object that contains all properties of the entity.
+ */
+function _form_validate_skip($element, $form_state) {
+  // Check whether the pressed button defined form element parents to validate
+  // and a button-level submit handler to execute. If it didn't, then form
+  // submit handlers would potentially get unvalidated values, so we cannot
+  // allow this.
+  if (!(isset($form_state['clicked_button']['#validate_parents']) && isset($form_state['clicked_button']['#submit']))) {
+    return FALSE;
+  }
+  // Iterate over the parents of $element and compare them with the parents
+  // defined in the pressed button. If this element's parents are not contained
+  // in the defined parents, then validation for this element can be skipped.
+  $parents = $form_state['clicked_button']['#validate_parents'];
+  $skip = FALSE;
+  foreach ($parents as $key => $parent) {
+    if ($element['#parents'][$key] != $parent) {
+      $skip = TRUE;
+      break;
+    }
+  }
+
+  return $skip;
+}
+
+/**
  * 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.
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.36
diff -u -p -r1.36 field.form.inc
--- modules/field/field.form.inc	1 Dec 2009 03:07:33 -0000	1.36
+++ modules/field/field.form.inc	2 Dec 2009 12:09:46 -0000
@@ -214,7 +214,8 @@ function field_multiple_value_form($fiel
           '#name' => $field_name . '_add_more',
           '#value' => t('Add another item'),
           '#attributes' => array('class' => array('field-add-more-submit')),
-          // Submit callback for disabled JavaScript.
+          '#validate' => array(),
+          '#validate_parents' => array($field_name),
           '#submit' => array('field_add_more_submit'),
           '#ajax' => array(
             'callback' => 'field_add_more_js',
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.326
diff -u -p -r1.326 poll.module
--- modules/poll/poll.module	1 Dec 2009 13:14:42 -0000	1.326
+++ modules/poll/poll.module	2 Dec 2009 12:09:46 -0000
@@ -273,7 +273,9 @@ 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,
-    '#submit' => array('poll_more_choices_submit'), // If no javascript action.
+    '#validate' => array(),
+    '#validate_parents' => array('choice_wrapper'),
+    '#submit' => array('poll_more_choices_submit'),
     '#ajax' => array(
       'callback' => 'poll_choice_js',
       'wrapper' => 'poll-choices',
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.27
diff -u -p -r1.27 form.test
--- modules/simpletest/tests/form.test	1 Dec 2009 03:07:34 -0000	1.27
+++ modules/simpletest/tests/form.test	2 Dec 2009 12:10:18 -0000
@@ -204,6 +204,46 @@ 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 using #validate_parents.
+   */
+  function testValidateParents() {
+    $this->drupalGet('form-test/validate-parents');
+
+    // Add one 'field' value without a value for required 'title' element.
+    $edit = array(
+      'field[new]' => 'foo',
+    );
+    $this->drupalPost(NULL, $edit, 'Add');
+    $this->assertText('Form element validation handler for field triggered.');
+    $this->assertFieldByName('field[0]', $edit['field[new]'], t('First field element contains submitted value.'));
+    $this->assertNoText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertNoText('Form element validation handler for element triggered.');
+    $this->assertNoText('Form submit handler triggered.');
+
+    // Try to submit the form without posting a title, but also not altering the
+    // default value for 'element' to trigger the element validation handler.
+    $this->drupalPost(NULL, array(), 'Submit');
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertText('Form element validation handler for element triggered.');
+    $this->assertNoText('Form submit handler triggered.');
+
+    // Try again without posting a title.
+    $edit = array(
+      'element' => 'bar',
+    );
+    $this->drupalPost(NULL, $edit, 'Submit');
+    $this->assertText(t('!name field is required.', array('!name' => 'Title')));
+    $this->assertNoText('Form submit handler triggered.');
+
+    // Now do a valid post and verify invocation of submit handler.
+    $edit = array(
+      'title' => 'beer',
+    );
+    $this->drupalPost(NULL, $edit, 'Submit');
+    $this->assertText('Form submit handler triggered.');
+  }
 }
 
 /**
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.19
diff -u -p -r1.19 form_test.module
--- modules/simpletest/tests/form_test.module	1 Dec 2009 03:07:34 -0000	1.19
+++ modules/simpletest/tests/form_test.module	2 Dec 2009 12:09:46 -0000
@@ -17,6 +17,12 @@ function form_test_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  $items['form-test/validate-parents'] = array(
+    'title' => '#validate_parents test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_validate_parents_form'),
+    'access arguments' => array('access content'),
+  );
 
   $items['form_test/tableselect/multiple-true'] = array(
     'title' => 'Tableselect checkboxes test',
@@ -180,6 +186,93 @@ function form_test_validate_form_validat
 }
 
 /**
+ * Form builder for testing #validate_parents.
+ */
+function form_test_validate_parents_form($form, &$form_state) {
+  $form['#tree'] = TRUE;
+
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Title',
+    '#required' => TRUE,
+  );
+  $form['element'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Element',
+    '#default_value' => 'ELEMENT',
+    '#element_validate' => array('form_test_validate_parents_form_element_validate'),
+  );
+
+  $form['field'] = array(
+    '#type' => 'fieldset',
+    '#title' => 'Field',
+  );
+  if (empty($form_state['storage']['field'])) {
+    $form_state['storage']['field'] = array();
+  }
+  // Always add an empty field the user can submit.
+  $form_state['storage']['field']['new'] = '';
+  $weight = 0;
+  foreach ($form_state['storage']['field'] as $delta => $value) {
+    $form['field'][$delta] = array(
+      '#type' => 'textfield',
+      '#title' => 'Field ' . $delta,
+      '#default_value' => $value,
+      '#element_validate' => array('form_test_validate_parents_form_field_validate'),
+      '#weight' => $weight++,
+    );
+  }
+  $form['field']['new']['#weight'] = 999;
+  $form['field']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Add',
+    '#validate_parents' => array('field'),
+    '#submit' => array('form_test_validate_parents_form_field_submit'),
+    '#weight' => 1000,
+  );
+
+  $form['submit'] = array('#type' => 'submit', '#value' => 'Submit');
+
+  return $form;
+}
+
+/**
+ * Form element validation handler for 'element' in form_test_validate_parents_form().
+ */
+function form_test_validate_parents_form_element_validate($element, &$form_state) {
+  if (empty($form_state['values']['element']) || $form_state['values']['element'] == $element['#default_value']) {
+    form_error($element, 'Form element validation handler for element triggered.');
+  }
+}
+
+/**
+ * Form element validation handler for 'field' in form_test_validate_parents_form().
+ */
+function form_test_validate_parents_form_field_validate($element, &$form_state) {
+  drupal_set_message('Form element validation handler for field triggered.');
+}
+
+/**
+ * Form element submit handler for form_test_validate_parents_form().
+ */
+function form_test_validate_parents_form_field_submit($form, &$form_state) {
+  $form_state['storage']['field'][] = $form_state['values']['field']['new'];
+  // 
+  unset($form_state['input']['field']['new']);
+  // @todo The form constructor should be able to define this, but currently
+  //   cannot. @see http://drupal.org/node/648170
+  $form_state['rebuild'] = TRUE;
+  $form_state['cache'] = TRUE;
+}
+
+/**
+ * Form submit handler for form_test_validate_parents_form().
+ */
+function form_test_validate_parents_form_submit($form, &$form_state) {
+  drupal_set_message('Form submit handler triggered.');
+}
+
+/**
  * Create a header and options array. Helper function for callbacks.
  */
 function _form_test_tableselect_get_data() {
