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 10 Feb 2009 10:16:51 -0000 @@ -257,7 +257,12 @@ '#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. + // Submit handler to specifically add a new choice to the form. + '#submit' => array('poll_more_choices_submit'), + // No form-level validation needed for the more button. + '#validate' => array(), + // Enable element level validation only for the choice section. + '#validate_section' => array('choice_wrapper', 'choice'), '#ahah' => array( 'callback' => 'poll_choice_js', 'wrapper' => 'poll-choices', @@ -304,7 +309,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 10 Feb 2009 10:16:51 -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 10 Feb 2009 10:16:51 -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 10 Feb 2009 10:16:51 -0000 @@ -581,7 +581,24 @@ } } - _form_validate($form, $form_state, $form_id); + // Allow only a portion of the form to be validated, but only if this property + // is on a button AND a custom #submit property is defined. This prevents the + // form-level submit handlers from accidentally saving data 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); + + // Call user-defined form level validators. + form_execute_handlers('validate', $form, $form_state); + $validated_forms[$form_id] = TRUE; } @@ -645,8 +662,8 @@ * completed, #maxlength is not exceeded, and selected options were in the * list of options given to the user. Then calls user-defined validators. * - * @param $elements - * An associative array containing the structure of the form. + * @param $form + * An associative array containing the complete structure of the form. * @param $form_state * A keyed array containing the current state of the form. The current * user-submitted data is stored in $form_state['values'], though @@ -657,20 +674,34 @@ * 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 $form_id - * A unique string identifying the form for validation, submission, - * theming, and hook_form_alter functions. + * @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 $elements + * Internal use. The section of the form to be validated. */ -function _form_validate($elements, &$form_state, $form_id = NULL) { - static $complete_form; - +function _form_validate(&$form, &$form_state, $validate_section = NULL, &$elements = NULL) { // Also used in the installer, pre-database setup. $t = get_t(); + // If no part of the form was passed in, validate the whole thing. + if (!isset($elements)) { + $elements = &$form; + } + // 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($form, $form_state, NULL, $elements[$key]); + } + // Otherwise, check if this is the next sub-section that needs validation. + elseif (empty($validate_section) || $validate_section[0] == $key) { + array_shift($validate_section); + _form_validate($form, $form_state, $validate_section, $elements[$key]); + } } } // Validate the current input. @@ -712,19 +743,12 @@ } } - // Call user-defined form level validators and store a copy of the full - // form so that element-specific validators can examine the entire structure - // if necessary. - if (isset($form_id)) { - form_execute_handlers('validate', $elements, $form_state); - $complete_form = $elements; - } // Call any element-specific validators. These must act on the element // #value data. - elseif (isset($elements['#element_validate'])) { + if (isset($elements['#element_validate'])) { foreach ($elements['#element_validate'] as $function) { if (drupal_function_exists($function)) { - $function($elements, $form_state, $complete_form); + $function($elements, $form_state, $form); } } } @@ -746,9 +770,12 @@ * A keyed array containing the current state of the form. If the user * submitted the form by clicking a button with custom handler functions * defined, those handlers will be stored here. + * @return + * An array of the results of each submit handler, or simply TRUE for each + * submit handler executed if the handler does not return a value. */ function form_execute_handlers($type, &$form, &$form_state) { - $return = FALSE; + $return = array(); if (isset($form_state[$type . '_handlers'])) { $handlers = $form_state[$type . '_handlers']; } @@ -768,9 +795,9 @@ $batch['sets'][] = array('form_submit' => $function); } else { - $function($form, $form_state); + $return[$function] = $function($form, $form_state); } - $return = TRUE; + $return[$function] = isset($return[$function]) ? $return[$function] : TRUE; } } return $return;