Index: modules/poll/poll.module =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v retrieving revision 1.292 diff -u -r1.292 poll.module --- modules/poll/poll.module 26 Apr 2009 16:31:23 -0000 1.292 +++ modules/poll/poll.module 27 Apr 2009 02:30:41 -0000 @@ -246,20 +246,17 @@ $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 @@ -270,6 +267,8 @@ '#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'), '#ahah' => array( 'callback' => 'poll_choice_js', 'wrapper' => 'poll-choices', @@ -315,13 +314,24 @@ * entire form is rebuilt during the page reload. */ 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); + // 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/ahah' ? 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.17 diff -u -r1.17 poll.test --- modules/poll/poll.test 26 Apr 2009 16:31:23 -0000 1.17 +++ modules/poll/poll.test 27 Apr 2009 02:30:41 -0000 @@ -60,11 +60,14 @@ $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)); } @@ -183,11 +186,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 @@ -206,8 +205,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[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.8 diff -u -r1.8 form.test --- modules/simpletest/tests/form.test 22 Apr 2009 09:12:44 -0000 1.8 +++ modules/simpletest/tests/form.test 27 Apr 2009 02:30:42 -0000 @@ -69,8 +69,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 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); + } } } Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.328 diff -u -r1.328 form.inc --- includes/form.inc 22 Apr 2009 09:12:44 -0000 1.328 +++ includes/form.inc 27 Apr 2009 02:30:41 -0000 @@ -668,13 +668,26 @@ // 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, TRUE); $validated_forms[$form_id] = TRUE; } @@ -745,7 +758,7 @@ * list of options given to the user. Then calls user-defined validators. * * @param $elements - * An associative array containing the structure of the form. + * An associative array containing the structure of a form section. * @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 @@ -756,20 +769,24 @@ * 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 $complete_form + * An associative array containing the structure of the entire form. + * @param $init + * Boolean value to indicate if we're starting a new validate operation. */ -function _form_validate($elements, &$form_state, $form_id = NULL) { - static $complete_form; - +function _form_validate($elements, &$form_state, $complete_form, $init = FALSE) { // Also used in the installer, pre-database setup. $t = get_t(); // Recurse through all children. foreach (element_children($elements) as $key) { if (isset($elements[$key]) && $elements[$key]) { - _form_validate($elements[$key], $form_state); + // 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, $complete_form); } } // Validate the current input. @@ -814,9 +831,8 @@ // 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; + if ($init) { + form_execute_handlers('validate', $complete_form, $form_state); } // Call any element-specific validators. These must act on the element // #value data. @@ -1165,7 +1181,6 @@ } } } - form_set_value($form, $form['#value'], $form_state); } /** @@ -1825,6 +1840,9 @@ // We will run some of the submit handlers so we need to disable redirecting. $form['#redirect'] = FALSE; + // Use the validate section handling on the clicked button (if available). + $form['#validate_section_allowed'] = TRUE; + // We need to process the form, prepare for that by setting a few internals // variables. $form_state['input'] = $_POST;