Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.7 diff -u -p -r1.7 ajax.inc --- includes/ajax.inc 27 Aug 2009 04:40:12 -0000 1.7 +++ includes/ajax.inc 27 Aug 2009 17:17:09 -0000 @@ -186,6 +186,9 @@ function ajax_get_form() { // Since some of the submit handlers are run, redirects need to be disabled. $form['#redirect'] = FALSE; + // Use the validate section handling on the clicked button (if available). + $form['#validate_section_allowed'] = TRUE; + // The form needs to be processed; prepare for that by setting a few internal // variables. $form_state['input'] = $_POST; Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.367 diff -u -p -r1.367 form.inc --- includes/form.inc 27 Aug 2009 04:40:12 -0000 1.367 +++ includes/form.inc 27 Aug 2009 17:41:21 -0000 @@ -705,13 +705,26 @@ function drupal_validate_form($form_id, // If the session token was set by drupal_prepare_form(), ensure that it // matches the current user's session. if (isset($form['#token'])) { - if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) { + if (!drupal_valid_token($form['form_token']['#value'], $form['#token'])) { // Setting this error will cause the form to fail validation. form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.')); } } - _form_validate($form, $form_state, $form_id); + // Allow only a portion of the form to be validated. This is only allowed if + // the #validate_section property is on a button AND a custom #submit property + // is defined. This prevents the form-level submit handlers from firing + // accidentally when a portion of the form has not been validated. + $elements = $form; + $button = isset($form_state['clicked_button']) ? $form_state['clicked_button'] : FALSE; + if ($button && !empty($form['#validate_section_allowed']) && isset($button['#validate_section']) && isset($button['#submit'])) { + foreach ($button['#validate_section'] as $section) { + $elements = $elements[$section]; + } + } + + // Validate the section of the form. + _form_validate($elements, $form_state, $form_id); $validated_forms[$form_id] = TRUE; } @@ -768,7 +781,8 @@ function drupal_redirect_form($form, $re * 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. + * theming, and hook_form_alter functions. Indicates that we are starting a + * new validate operation. */ function _form_validate($elements, &$form_state, $form_id = NULL) { // Also used in the installer, pre-database setup. @@ -777,6 +791,10 @@ function _form_validate($elements, &$for // Recurse through all children. foreach (element_children($elements) as $key) { if (isset($elements[$key]) && $elements[$key]) { + // Only validated elements receive a value. + if (isset($elements[$key]['#value'])) { + form_set_value($elements[$key], $elements[$key]['#value'], $form_state); + } _form_validate($elements[$key], $form_state); } } @@ -827,7 +845,7 @@ function _form_validate($elements, &$for // #value data. elseif (isset($elements['#element_validate'])) { foreach ($elements['#element_validate'] as $function) { - if (function_exists($function)) { + if (function_exists($function) && $function != '_form_validate') { $function($elements, $form_state, $form_state['complete form']); } } @@ -1184,7 +1202,6 @@ function _form_builder_handle_input_elem } } } - form_set_value($element, $element['#value'], $form_state); } /** Index: modules/poll/poll.module =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v retrieving revision 1.310 diff -u -p -r1.310 poll.module --- modules/poll/poll.module 24 Aug 2009 00:14:21 -0000 1.310 +++ modules/poll/poll.module 27 Aug 2009 17:27:21 -0000 @@ -256,20 +256,17 @@ function poll_form($node, $form_state) { $delta = 0; $weight = 0; if (isset($node->choice)) { - $delta = count($node->choice); - $weight = -$delta; - foreach ($node->choice as $chid => $choice) { - $key = 'chid:' . $chid; - $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count); + foreach ($node->choice as $choice) { + $choice['chid'] = isset($choice['chid']) ? $choice['chid'] : NULL; + $form['choice_wrapper']['choice'][$delta] = _poll_choice_form($delta, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count); $weight = ($choice['weight'] > $weight) ? $choice['weight'] : $weight; + $delta++; } } // Add initial or additional choices. - $existing_delta = $delta; for ($delta; $delta < $choice_count; $delta++) { - $key = 'new:' . ($delta - $existing_delta); - $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, NULL, '', 0, $weight, $choice_count); + $form['choice_wrapper']['choice'][$delta] = _poll_choice_form($delta, NULL, '', 0, $weight, $choice_count); } // We name our button 'poll_more' to avoid conflicts with other modules using @@ -280,6 +277,8 @@ function poll_form($node, $form_state) { '#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(), // No form-level validation for this button. + '#validate_section' => array('choice_wrapper', 'choice'), '#ajax' => array( 'callback' => 'poll_choice_js', 'wrapper' => 'poll-choices', @@ -325,14 +324,25 @@ function poll_form($node, $form_state) { * entire form is rebuilt during the page reload. */ function poll_more_choices_submit($form, &$form_state) { - include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc'; - // Set the form to rebuild and run submit handlers. - node_form_submit_build_node($form, $form_state); + module_load_include('inc', 'node', 'node.pages'); + // Non-JavaScript behavior. Entire form is validated and 5 choices are added. + // The node must be rebuilt to populate the $node variable for hook_form(). + if (empty($form['#validate_section_allowed'])) { + node_form_submit_build_node($form, $form_state); + $new_choices = 5; + } + // JavaScript behavior. Only the choice section is validated, and 1 choice is + // added. We don't need to (and can't) rebuild the node because only the + // validated section of the form is given any values. + else { + $form_state['node']['choice'] = array_values($form_state['values']['choice']); + $form_state['rebuild'] = TRUE; + $new_choices = 1; + } // Make the changes we want to the form state. - if ($form_state['values']['poll_more']) { - $n = $_GET['q'] == 'system/ajax' ? 1 : 5; - $form_state['choice_count'] = count($form_state['values']['choice']) + $n; + if ($form_state['values']['op'] == $form['choice_wrapper']['poll_more']['#value']) { + $form_state['choice_count'] = count($form_state['values']['choice']) + $new_choices; } } Index: modules/poll/poll.test =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v retrieving revision 1.22 diff -u -p -r1.22 poll.test --- modules/poll/poll.test 17 Aug 2009 07:12:16 -0000 1.22 +++ modules/poll/poll.test 27 Aug 2009 17:08:23 -0000 @@ -60,11 +60,14 @@ class PollTestCase extends DrupalWebTest $edit = array( 'title' => $title ); - foreach ($already_submitted_choices as $k => $text) { - $edit['choice[chid:' . $k . '][chtext]'] = $text; + $delta = 0; + foreach ($already_submitted_choices as $text) { + $edit['choice[' . $delta . '][chtext]'] = $text; + $delta++; } - foreach ($new_choices as $k => $text) { - $edit['choice[new:' . $k . '][chtext]'] = $text; + foreach ($new_choices as $text) { + $edit['choice[' . $delta . '][chtext]'] = $text; + $delta++; } return array($edit, count($already_submitted_choices) + count($new_choices)); } @@ -331,11 +334,7 @@ class PollJSAddChoice extends DrupalWebT $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 @@ -361,8 +360,8 @@ class PollJSAddChoice extends DrupalWebT // 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[new:0][chtext]', '', t('Field !i found', array('!i' => 2))); + $this->assertFieldByName('choice[0][chtext]', '', t('Field !i found', array('!i' => 0))); + $this->assertFieldByName('choice[1][chtext]', '', t('Field !i found', array('!i' => 1))); + $this->assertFieldByName('choice[2][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.14 diff -u -p -r1.14 form.test --- modules/simpletest/tests/form.test 13 Jul 2009 21:51:41 -0000 1.14 +++ modules/simpletest/tests/form.test 27 Aug 2009 17:08:23 -0000 @@ -69,8 +69,111 @@ class FormsTestCase extends DrupalWebTes $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 passes. + */ + 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); + } } }