Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.19 diff -u -p -r1.19 ajax.inc --- includes/ajax.inc 22 Nov 2009 02:48:37 -0000 1.19 +++ includes/ajax.inc 26 Nov 2009 20:51:44 -0000 @@ -258,9 +258,10 @@ function ajax_form_callback() { // Build, validate and if possible, submit the form. drupal_process_form($form_id, $form, $form_state); - // This call recreates the form relying solely on the $form_state that - // drupal_process_form() set up. - $form = drupal_rebuild_form($form_id, $form_state, $form_build_id); + // AJAX forms are multi-step forms regardless of $form_state['rebuild']. + if (!form_get_errors()) { + $form = drupal_rebuild_form($form_id, $form_state, $form_build_id); + } // $triggering_element_path in a simple form might just be 'myselect', which // would mean we should use the element $form['myselect']. For nested form Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.406 diff -u -p -r1.406 form.inc --- includes/form.inc 24 Nov 2009 21:56:15 -0000 1.406 +++ includes/form.inc 26 Nov 2009 20:51:44 -0000 @@ -265,16 +265,16 @@ function drupal_build_form($form_id, &$f */ function form_state_defaults() { return array( - 'args' => array(), 'rebuild' => FALSE, 'redirect' => NULL, - 'build_info' => array(), + 'build_info' => array('args' => array()), 'storage' => array(), 'submitted' => FALSE, 'programmed' => FALSE, 'cache'=> FALSE, 'method' => 'post', 'groups' => array(), + 'input_processed' => FALSE, ); } @@ -310,6 +310,17 @@ function form_state_defaults() { * The newly built form. */ function drupal_rebuild_form($form_id, &$form_state, $form_build_id = NULL) { + // The values from the previous step is the input to the next step. Groups + // and values for the new step need to be rebuilt. + $form_state['input'] = $form_state['values']; + $form_state['groups'] = array(); + $form_state['values'] = array(); + + // The input is data that's already been processed in the previous step by the + // element value callbacks, so it is not a raw submission from the browser, + // and should not be reprocessed by value callbacks. + $form_state['input_processed'] = TRUE; + $form = drupal_retrieve_form($form_id, $form_state); if (!isset($form_build_id)) { @@ -326,15 +337,6 @@ function drupal_rebuild_form($form_id, & form_set_cache($form_build_id, $form, $form_state); } - // Clear out all post data, as we don't want the previous step's - // data to pollute this one and trigger validate/submit handling, - // then process the form for rendering. - $form_state['input'] = array(); - - // Also clear out all group associations as these might be different - // when rerendering the form. - $form_state['groups'] = array(); - // Do not call drupal_process_form(), since it would prevent the rebuilt form // to submit. $form = form_builder($form_id, $form, $form_state); @@ -1188,6 +1190,7 @@ function _form_builder_handle_input_elem if (!isset($element['#value']) && !array_key_exists('#value', $element)) { $value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value'; + // Set #value based on submitted input. if ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))) { $input = $form_state['input']; foreach ($element['#parents'] as $parent) { @@ -1195,10 +1198,13 @@ function _form_builder_handle_input_elem } // If we have input for the current element, assign it to the #value property. if (!$form_state['programmed'] || isset($input)) { - // Call #type_value to set the form value; - if (function_exists($value_callback)) { + // Call the value callback to set the form element value. + if (!$form_state['input_processed'] && function_exists($value_callback)) { $element['#value'] = $value_callback($element, $input, $form_state); } + // If there's no value callback or it didn't return a value, then set + // the #value to $input. Validation functions will still have a chance + // to run. if (!isset($element['#value']) && isset($input)) { $element['#value'] = $input; } Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.35 diff -u -p -r1.35 field.form.inc --- modules/field/field.form.inc 20 Nov 2009 04:51:27 -0000 1.35 +++ modules/field/field.form.inc 26 Nov 2009 20:51:45 -0000 @@ -351,6 +351,9 @@ function field_add_more_submit($form, &$ if ($form_state['values'][$field_name . '_add_more']) { $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$langcode]); } + + // @todo: Fix Field API form building to not require this. + $form_state['values'] = array(); } } Index: modules/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.11 diff -u -p -r1.11 file.module --- modules/file/file.module 31 Oct 2009 16:06:36 -0000 1.11 +++ modules/file/file.module 26 Nov 2009 20:51:45 -0000 @@ -212,9 +212,10 @@ function file_ajax_upload() { // Build, validate and if possible, submit the form. drupal_process_form($form_id, $form, $form_state); - // This call recreates the form relying solely on the form_state that the - // drupal_process_form() set up. - $form = drupal_rebuild_form($form_id, $form_state, $form_build_id); + // AJAX forms are multi-step forms regardless of $form_state['rebuild']. + if (!form_get_errors()) { + $form = drupal_rebuild_form($form_id, $form_state, $form_build_id); + } // Retrieve the element to be rendered. foreach ($form_parents as $parent) { Index: modules/simpletest/tests/ajax.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v retrieving revision 1.3 diff -u -p -r1.3 ajax.test --- modules/simpletest/tests/ajax.test 22 Nov 2009 02:48:37 -0000 1.3 +++ modules/simpletest/tests/ajax.test 26 Nov 2009 20:51:45 -0000 @@ -141,3 +141,46 @@ class AJAXCommandsTestCase extends AJAXT $this->assertTrue($command['command'] == 'restripe' && $command['selector'] == '#restripe_table', "'restripe' AJAX command issued with correct selector"); } } + +/** + * Test that $form_state['values'] is properly delivered to $ajax['callback']. + */ +class AJAXFormValuesTestCase extends AJAXTestCase { + public static function getInfo() { + return array( + 'name' => 'AJAX Form Values in command', + 'description' => 'Creates a form, then POSTS to system/ajax to change the form via pseudo-ajax.', + 'group' => 'AJAX', + ); + } + + /** + * Create a simple form with a select, then POST a change to it. + */ + function testSimpleAJAXFormValue() { + $web_user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($web_user); + + $form_path = 'ajax_forms_test_get_form'; + + $selects_to_test = array('red', 'green', 'blue'); + foreach($selects_to_test as $item) { + $edit = array(); + $edit['select'] = $item; + $commands = $this->drupalPostAJAX($form_path, $edit, 'select'); + $data_command = $commands[2]; + $this->assertEqual($data_command['value'], $edit['select']); + } + + $values_to_test = array(FALSE, TRUE); + foreach($values_to_test as $item) { + $edit = array(); + $edit['checkbox'] = $item; + $commands = $this->drupalPostAJAX($form_path, $edit, 'checkbox'); + $data_command = $commands[2]; + $value_expected = (int) $item; + $value_received = (int) $data_command['value']; + $this->assertEqual($value_received, $value_expected); + } + } +} Index: modules/simpletest/tests/form.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v retrieving revision 1.25 diff -u -p -r1.25 form.test --- modules/simpletest/tests/form.test 18 Nov 2009 18:51:11 -0000 1.25 +++ modules/simpletest/tests/form.test 26 Nov 2009 20:51:45 -0000 @@ -71,6 +71,7 @@ class FormsTestCase extends DrupalWebTes foreach (array(TRUE, FALSE) as $required) { $form_id = $this->randomName(); $form = $form_state = array(); + $form_state += form_state_defaults(); form_clear_error(); $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); $element = $data['element']['#title']; @@ -533,3 +534,55 @@ class FormStateValuesCleanTestCase exten } } +/** + * Test that form values are kept or not kept after a rebuild, as appropriate. + */ +class FormsFormRebuildTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Form rebuilding', + 'description' => 'Tests forms using different rebuild options to make sure values from one step are or are not carried over into the next step, as appropriate.', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Tests form rebuilding for a multistep form. + */ + function testStorage() { + $user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($user); + + $this->drupalPost('form_test/form-multistep', array('name1' => 'foo'), 'Continue'); + $this->assertText('Friend\'s name', t('The correct step is shown.')); + $this->assertFieldByName('name2', 'DEFAULT2', t('The name field for the next step was correctly set to its default value.')); + } + + /** + * Tests form rebuilding for a confirmation form. + */ + function testRebuild() { + $user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($user); + + $this->drupalPost('form_test/form-rebuild', array('name' => 'foo'), 'Delete'); + $this->assertFieldByName('name', 'foo', t('The name field for the rebuilt form was correctly set to its default value.')); + } + + /** + * Tests form rebuilding during an "add another item" step. + */ + function testRebuildProgressive() { + $user = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($user); + + $this->drupalPost('form_test/form-progressive-rebuild', array('groceries[0]' => 'apples'), 'Add grocery item'); + $this->assertFieldByName('groceries[0]', 'apples', t('A submitted field for the progressively rebuilt form was correctly set to its previously submitted value.')); + $this->assertFieldByName('groceries[1]', 'DEFAULT', t('A new field for the progressively rebuilt form was correctly set to its default value.')); + } +} Index: modules/simpletest/tests/form_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v retrieving revision 1.17 diff -u -p -r1.17 form_test.module --- modules/simpletest/tests/form_test.module 21 Nov 2009 17:01:31 -0000 1.17 +++ modules/simpletest/tests/form_test.module 26 Nov 2009 20:51:45 -0000 @@ -78,6 +78,30 @@ function form_test_menu() { 'type' => MENU_CALLBACK, ); + $items['form_test/form-multistep'] = array( + 'title' => 'Form multistep test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_multistep_test_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + $items['form_test/form-rebuild'] = array( + 'title' => 'Form rebuild test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_rebuild_test_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + $items['form_test/form-progressive-rebuild'] = array( + 'title' => 'Form progressive rebuild test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_progressive_rebuild_test_form'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -477,3 +501,189 @@ function _form_test_checkbox_submit($for drupal_json_output($form_state['values']); exit(); } + +/** + * A wizard style multistep form. + * + * Used by form.test to ensure that the 'name' field on step 2 isn't polluted + * with data from the different 'name' field from step 1. + * + * @see form_multistep_test_form_submit(). + */ +function form_multistep_test_form($form, &$form_state) { + if (!isset($form_state['storage']['step'])) { + $form_state['storage']['step'] = 1; + } + if ($form_state['storage']['step'] == 1) { + $form['name1'] = array( + '#type' => 'textfield', + '#title' => 'Your name', + '#default_value' => 'DEFAULT1', + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Continue', + ); + } + else { + $form['name2'] = array( + '#type' => 'textfield', + '#title' => 'Friend\'s name', + '#default_value' => 'DEFAULT2', + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Done', + ); + } + return $form; +} + +/** + * Submit callback for a wizard style multistep form. + */ +function form_multistep_test_form_submit($form, &$form_state) { + if ($form_state['storage']['step'] == 1) { + $form_state['storage']['name1'] = $form_state['values']['name1']; + $form_state['storage']['step']++; + $form_state['rebuild'] = TRUE; + } + else { + $form_state['storage']['name2'] = $form_state['values']['name2']; + drupal_set_message(t("Your name: %name1, Friend's name: %name2", array('%name1' => $form_state['storage']['name1'], '%name2' => $form_state['storage']['name2']))); + } +} + +/** + * A non wizard-style form requiring a rebuild. + * + * This example is pretty contrived, but the goal is to test something similar + * to what is tested by form_multistep_test_form(), but for a form that uses + * 'rebuild' without 'storage'. This pattern is used for delete confirmation + * forms, so we want to make sure to test it. + * + * @see form_rebuild_test_form_submit(). + */ +function form_rebuild_test_form($form, &$form_state) { + if (!isset($form_state['confirm_delete'])) { + $form['name'] = array( + '#type' => 'textfield', + '#title' => 'Your name', + '#default_value' => 'DEFAULT', + ); + $form['color'] = array( + '#type' => 'textfield', + '#title' => 'Your favorite color', + '#default_value' => 'DEFAULT', + ); + $form['delete'] = array( + '#type' => 'submit', + '#value' => 'Delete', + '#submit' => array('form_rebuild_test_form_submit_delete'), + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + } + else { + $form['name'] = array( + '#type' => 'textfield', + '#title' => 'Your name', + '#default_value' => 'DEFAULT FOR DELETE', + '#description' => 'To delete yourself from the system, enter your name.', + '#required' => TRUE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Yes, Really Delete', + '#submit' => array('form_rebuild_test_form_submit_delete_confirmed'), + ); + } + return $form; +} + +/** + * Submit callbacks for form_rebuild_test_form(). + */ +function form_rebuild_test_form_submit($form, &$form_state) { + drupal_set_message(t("Your name: %name, Your favorite color: %color", array('%name' => $form_state['values']['name'], '%color' => $form_state['values']['color']))); +} + +function form_rebuild_test_form_submit_delete($form, &$form_state) { + $form_state['rebuild'] = TRUE; + $form_state['confirm_delete'] = TRUE; +} + +function form_rebuild_test_form_submit_delete_confirmed($form, &$form_state) { + drupal_set_message(t("Ok, %name has been deleted.", array('%name' => $form_state['values']['name']))); +} + +/** + * A form requiring a rebuild with values preserved. + * + * Tests that a submit handler can set $form_state['rebuild_options']['values'] + * in order to preserve form values during a rebuild. + * + * @see form_progressive_rebuild_test_form_submit(). + */ +function form_progressive_rebuild_test_form($form, &$form_state) { + $num_grocery_items = isset($form_state['num_grocery_items']) ? $form_state['num_grocery_items'] : 1; + $num_movies = isset($form_state['num_movies']) ? $form_state['num_movies'] : 1; + $form['#tree'] = TRUE; + + $form['groceries'] = array(); + for ($i=0; $i < $num_grocery_items; $i++) { + $form['groceries'][$i] = array( + '#type' => 'textfield', + '#title' => 'Grocery item ' . ($i + 1), + '#default_value' => 'DEFAULT', + ); + } + $form['add_grocery_item'] = array( + '#type' => 'submit', + '#value' => 'Add grocery item', + '#submit' => array('form_progressive_rebuild_test_form_submit_add_item'), + '#_which_counter' => 'num_grocery_items', + ); + $form['movies'] = array(); + for ($i=0; $i < $num_movies; $i++) { + $form['movies'][$i] = array( + '#type' => 'textfield', + '#title' => 'Movie to rent ' . ($i + 1), + '#default_value' => 'DEFAULT', + ); + } + $form['add_movie'] = array( + '#type' => 'submit', + '#value' => 'Add movie', + '#submit' => array('form_progressive_rebuild_test_form_submit_add_item'), + '#_which_counter' => 'num_movies', + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + return $form; +} + +/** + * Form submit callback for form_progressive_rebuild_test_form(). + */ +function form_progressive_rebuild_test_form_submit_add_item($form, &$form_state) { + $form_state['rebuild'] = TRUE; + + $form_state['num_grocery_items'] = count($form_state['values']['groceries']); + $form_state['num_movies'] = count($form_state['values']['movies']); + $form_state[$form_state['clicked_button']['#_which_counter']]++; +} + +/** + * Form submit callback for form_progressive_rebuild_test_form(). + */ +function form_progressive_rebuild_test_form_submit($form, &$form_state) { + drupal_set_message(t("Your grocery items: %groceries, Your movies: %movies", array('%groceries' => implode(', ', $form_state['values']['groceries']), '%movies' => implode(', ', $form_state['values']['movies'])))); +} + Index: modules/taxonomy/taxonomy.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.admin.inc,v retrieving revision 1.83 diff -u -p -r1.83 taxonomy.admin.inc --- modules/taxonomy/taxonomy.admin.inc 8 Nov 2009 11:19:02 -0000 1.83 +++ modules/taxonomy/taxonomy.admin.inc 26 Nov 2009 20:51:45 -0000 @@ -110,8 +110,8 @@ function taxonomy_form_vocabulary($form, ); $form['#vocabulary'] = (object) $edit; // Check whether we need a deletion confirmation form. - if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) { - return taxonomy_vocabulary_confirm_delete($form, $form_state, $form_state['values']['vid']); + if (isset($form_state['confirm_delete']) && isset($form_state['confirm_delete_vid'])) { + return taxonomy_vocabulary_confirm_delete($form, $form_state, $form_state['confirm_delete_vid']); } $form['name'] = array( '#type' => 'textfield', @@ -203,6 +203,7 @@ function taxonomy_form_vocabulary_submit // Rebuild the form to confirm vocabulary deletion. $form_state['rebuild'] = TRUE; $form_state['confirm_delete'] = TRUE; + $form_state['confirm_delete_vid'] = isset($form_state['values']['vid']) ? $form_state['values']['vid'] : NULL; return; } $vocabulary = (object) $form_state['values'];