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 27 Dec 2009 01:43:41 -0000 @@ -262,30 +262,13 @@ function ajax_form_callback() { // drupal_process_form() set up. $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 - // elements we need to recurse into the form structure to find the triggering - // element, so we can retrieve the #ajax['callback'] from it. - if (!empty($triggering_element_path)) { - if (!isset($form['#access']) || $form['#access']) { - $triggering_element = $form; - foreach (explode('/', $triggering_element_path) as $key) { - if (!empty($triggering_element[$key]) && (!isset($triggering_element[$key]['#access']) || $triggering_element[$key]['#access'])) { - $triggering_element = $triggering_element[$key]; - } - else { - // We did not find the $triggering_element or do not have #access, - // so break out and do not provide it. - $triggering_element = NULL; - break; - } - } - } - } + // Retrieve the triggering element from $form. If not found, default to + // $form_state['clicked_button']. + $triggering_element = drupal_subtree($form, $triggering_element_path, TRUE); if (empty($triggering_element)) { $triggering_element = $form_state['clicked_button']; } - // Now that we have the element, get a callback if there is one. + // Invoke the callback function specified by the triggering element. if (!empty($triggering_element)) { $callback = $triggering_element['#ajax']['callback']; } @@ -418,7 +401,7 @@ function ajax_process_form($element, &$f 'method' => empty($element['#ajax']['method']) ? 'replace' : $element['#ajax']['method'], 'progress' => empty($element['#ajax']['progress']) ? array('type' => 'throbber') : $element['#ajax']['progress'], 'button' => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE, - 'formPath' => implode('/', $element['#array_parents']), + 'formPath' => drupal_tree_path($element['#array_parents'], DRUPAL_TREE_PATH_SLASH_DELIMITED), ); // Convert a simple #ajax['progress'] type string into an array. Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1070 diff -u -p -r1.1070 common.inc --- includes/common.inc 26 Dec 2009 16:50:08 -0000 1.1070 +++ includes/common.inc 27 Dec 2009 01:43:41 -0000 @@ -167,6 +167,29 @@ define('DRUPAL_CACHE_PER_PAGE', 0x0004); define('DRUPAL_CACHE_GLOBAL', 0x0008); /** + * Constants defining the representation format of a tree path. + * + * A tree is a nested array used for structuring hierarchical data. A tree path + * identifies a section of the hierarchy. Given a tree path, a subtree of the + * original tree can be retrieved. For example, given $tree, the subtree + * $tree['a']['b']['c'] can be identified using one of the following: + * - array('a', 'b', 'c'): A tree path in array format, convenient for tree + * traversal, because the array can be iterated. The #parents property of form + * elements uses this format. + * - 'a/b/c': A tree path in slash delimited format, commonly used when needing + * a string representation of a path (for example, for URLs). + * - 'a[b][c]': A tree path in brackets delimited format. This format is used + * for constructing the 'name' attribute for input elements of a form, because + * PHP automatically builds the $_POST tree from input element names that use + * this format. + * + * @see drupal_tree_path() + */ +define('DRUPAL_TREE_PATH_ARRAY', 0); +define('DRUPAL_TREE_PATH_SLASH_DELIMITED', 1); +define('DRUPAL_TREE_PATH_BRACKETS_DELIMITED', 2); + +/** * Add content to a specified region. * * @param $region @@ -6543,3 +6566,153 @@ function drupal_get_updaters() { } return $updaters; } + +/** + * Converts a tree path to the desired format. + * + * @param $path + * A tree path in any format. + * @param $format + * The desired format. One of: + * - DRUPAL_TREE_PATH_ARRAY: (default) Return the path in array format. For + * example: array('a', 'b', 'c'). + * - DRUPAL_TREE_PATH_SLASH_DELIMITED: Return the path in slash delimited + * format. For example: 'a/b/c'. + * - DRUPAL_TREE_PATH_BRACKETS_DELIMITED: Return the path in brackets + * delimited format. For example: 'a[b][c]'. + * @return + * The tree path in the desired format. + * + * @todo What's the best way to cross-link this to the PHPDoc above where these + * constants are defined? + */ +function drupal_tree_path($path, $format = DRUPAL_TREE_PATH_ARRAY) { + // Normalize the path to array format. If passed an array, just ensure numeric + // and ordered keys. Otherwise, explode on '/' and '[' delimiters, and remove + // ']' pseudo-delimiters. + $path = is_array($path) ? array_values($path) : explode('/', strtr($path, array('[' => '/', ']' => ''))); + + // Generate the return value based on the desired format. + switch ($format) { + case DRUPAL_TREE_PATH_ARRAY: + $return = $path; + break; + case DRUPAL_TREE_PATH_SLASH_DELIMITED: + $return = implode('/', $path); + break; + case DRUPAL_TREE_PATH_BRACKETS_DELIMITED: + $return = !empty($path) ? $path[0] : ''; + if (count($path) > 1) { + $return .= '[' . implode('][', array_slice($path, 1)) . ']'; + } + break; + } + + return $return; +} + +/** + * Returns if two tree paths refer to the same path. + */ +function drupal_tree_paths_are_equal($path1, $path2) { + return (drupal_tree_path($path1) === drupal_tree_path($path2)); +} + +/** + * Returns if the first path is a descendant of the second path. + */ +function drupal_tree_path_is_descendant($descendant_candidate, $ancestor_candidate) { + $descendant_candidate = drupal_tree_path($descendant_candidate); + $ancestor_candidate = drupal_tree_path($ancestor_candidate); + return (count($descendant_candidate) > count($ancestor_candidate) && array_slice($descendant_candidate, 0, count($ancestor_candidate)) === $ancestor_candidate); +} + +/** + * Returns the subtree of $tree identified by $path. + * + * If the code calling this function wants to retrieve a subtree in order to + * modify it, and have those modifications be reflected in the original tree, + * it needs to assign the result of this function by reference. Otherwise, if + * the code calling this function just wants a subtree for internal purposes, it + * does not need to assign the result by reference. Examples: + * + * @code + * function form_set_value($element, $value, &$form_state) { + * // Assign the result by reference, so that setting its value updates + * // the data within $form_state['values']. + * $subtree = &drupal_subtree($form_state['values'], $element['#parents'], FALSE, TRUE); + * $subtree = $value; + * } + * function field_add_more_js($form, $form_state) { + * ... + * // No need to assign result by reference, because we don't need to and + * // don't want to modify anything within $form. + * $field_form = drupal_subtree($form, $form['#fields'][$field_name]['form_path']); + * ... + * return drupal_render($field_form); + * } + * @endcode + * + * @param $tree + * The root tree from which a subtree is desired. + * @param $path + * The path identifying which subtree is desired. + * @param $check_access + * (optional) Boolean whether a #access of FALSE within any of the subarrays + * along the path should result in NULL being returned. This should be set to + * TRUE when called for the purpose of retrieving an element from within a + * Form API array or render array if it's desired to only retrieve an element + * that the user has access to. + * @param $create + * (optional) Boolean whether keys specified by $path should be added to the + * tree if they don't already exist. This is used when wanting to add to the + * tree (for an example, @see form_set_value()). + * @return + * A reference to the subtree (which could have a NULL value but still be a + * valid subtree), or a NULL that is not a reference to anything if the + * subtree doesn't exist or access to it is denied. + * + * @see drupal_tree_path() + */ +function &drupal_subtree(&$tree, $path, $check_access = FALSE, $create = FALSE) { + // If checking access, then it must be checked for the tree as well as each + // step along the path. + if ($check_access && isset($tree['#access']) && !$tree['#access']) { + $subtree = NULL; + } + else { + $subtree = &$tree; + foreach (drupal_tree_path($path) as $step) { + // If this step doesn't exist, then either create it, or break out of the + // loop and return NULL. + if (!is_array($subtree) || !array_key_exists($step, $subtree)) { + if ($create) { + if (!is_array($subtree)) { + $subtree = array(); + } + // Create new entries as NULL. If this is the last step, NULL is the + // desired value for the new entry. If this is not the last step, then + // it will be modified to an array in the next step. + $subtree[$step] = NULL; + } + else { + // Break the reference before setting it to NULL. + unset($subtree); + $subtree = NULL; + break; + } + } + // Traverse the step. + $subtree = &$subtree[$step]; + // If checking access, and #access is set and is FALSE, break out of the + // loop and return NULL. + if ($check_access && isset($subtree['#access']) && !$subtree['#access']) { + // Break the reference before setting it to NULL. + unset($subtree); + $subtree = NULL; + break; + } + } + } + return $subtree; +} Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.421 diff -u -p -r1.421 form.inc --- includes/form.inc 17 Dec 2009 21:59:31 -0000 1.421 +++ includes/form.inc 27 Dec 2009 01:43:41 -0000 @@ -897,7 +897,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']))); } @@ -935,7 +935,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']); @@ -947,6 +947,85 @@ function _form_validate(&$elements, &$fo } /** + * Check if an element's validation may be skipped. + * + * A form button may specify the #validate_partial property to limit form + * validation to sections of $form_state['values']. Such buttons are required to + * also define custom #submit handlers, which ONLY process form values within + * those sections. + * + * For example, when an "add another item" button for a multiple value field is + * clicked, we don't want a not yet filled out but required title to cause a + * validation error, preventing a new item from being added to the form. By + * setting that button's #validate_partial property, and only including + * the field's $field_name within #validate_partial['sections'], only elements + * within the field will be validated when the button is clicked, and the button + * submit handler can assume that data within $form_state['values'][$field_name] + * is valid, but must not assume that any other part of $form_state['values'] is + * valid. + * + * 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 should live in _form_builder_handle_input_element(), because + * ideally, we want to skip not just validation, but all input processing for + * unrelated elements. However, we currently cannot skip input processing, + * because node and field validate/submit handlers cast $form_state['values'] + * into an object and presume that doing so results in an object containing + * most properties of an entity. + * + * @todo The #validate_partial property needs to be documented somewhere other + * than with this internal function. The documentation should include examples + * of different use-cases for 'sections' and 'skip_ancestors'. It would make + * sense for this to live with wherever #validate and #element_validate are + * documented. Where is that? + */ +function _form_validate_skip($element, $form_state) { + // Check whether the pressed button defines #validate_partial to limit + // validation. We prevent insecure execution of form-level submit handlers by + // not supporting partial validation unless a button-level submit handler is + // defined. + if (!(isset($form_state['clicked_button']['#validate_partial']) && isset($form_state['clicked_button']['#submit']))) { + return FALSE; + } + // Iterate over #validate_partial['sections'], and if $element is responsible + // for data within that section, it needs validation. + $validate = FALSE; + foreach ($form_state['clicked_button']['#validate_partial']['sections'] as $section) { + // Check if the element's #parents is such that the element's values are + // within the section. + if (drupal_tree_paths_are_equal($element['#parents'], $section) || drupal_tree_path_is_descendant($element['#parents'], $section)) { + $validate = TRUE; + break; + } + // If an element's values occupy a part of $form_state['values'] that is + // an ancestor of the section, then its validation handlers may perform + // additional validation on data within the section, and should run by + // default. This includes the top-level form which is an ancestor to + // everything. #validate_partial['skip_ancestors'] may be set to skip + // ancestor element and form-level validation handlers. + if (empty($form_state['clicked_button']['#validate_partial']['skip_ancestors']) && drupal_tree_path_is_descendant($section, $element['#parents'])) { + $validate = TRUE; + break; + } + } + $skip = !$validate; + 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. @@ -969,7 +1048,13 @@ function form_execute_handlers($type, &$ } // Otherwise, check for a form-level handler. elseif (isset($form['#' . $type])) { - $handlers = $form['#' . $type]; + // Check if form-level validation handlers should be skipped. + if ($type == 'validate' && _form_validate_skip($form, $form_state)) { + $handlers = array(); + } + else { + $handlers = $form['#' . $type]; + } } else { $handlers = array(); @@ -1056,6 +1141,11 @@ function form_get_error($element) { * Flag an element as having an error. */ function form_error(&$element, $message = '') { + // @todo If a form uses #validate_partial without setting 'skip_ancestors', + // it's possible that an ancestor validation handler calls form_error() for + // an element that isn't within 'sections'. It may be desirable to add a + // _form_validate_skip() check here to ignore those errors, but this + // requires access to the $form_state variable. form_set_error(implode('][', $element['#parents']), $message); } @@ -1211,18 +1301,15 @@ function form_builder($form_id, $element */ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { if (!isset($element['#name'])) { - $name = array_shift($element['#parents']); - $element['#name'] = $name; if ($element['#type'] == 'file') { // To make it easier to handle $_FILES in file.inc, we place all // file fields in the 'files' array. Also, we do not support // nested file names. - $element['#name'] = 'files[' . $element['#name'] . ']'; + $element['#name'] = 'files[' . $element['#parents'][0] . ']'; } - elseif (count($element['#parents'])) { - $element['#name'] .= '[' . implode('][', $element['#parents']) . ']'; + else { + $element['#name'] = drupal_tree_path($element['#parents'], DRUPAL_TREE_PATH_BRACKETS_DELIMITED); } - array_unshift($element['#parents'], $name); } if (!empty($element['#disabled'])) { @@ -1236,17 +1323,16 @@ function _form_builder_handle_input_elem if ($form_state['programmed'] || ($form_state['process_input'] && (!isset($element['#access']) || $element['#access']))) { // Get the input for the current element. NULL values in the input need to // be explicitly distinguished from missing input. (see below) - $input = $form_state['input']; - $input_exists = TRUE; - foreach ($element['#parents'] as $parent) { - if (is_array($input) && array_key_exists($parent, $input)) { - $input = $input[$parent]; - } - else { - $input = NULL; - $input_exists = FALSE; - break; - } + $parents = $element['#parents']; + $last_parent = array_pop($parents); + $input = drupal_subtree($form_state['input'], $parents); + if (is_array($input) && array_key_exists($last_parent, $input)) { + $input = $input[$last_parent]; + $input_exists = TRUE; + } + else { + $input = NULL; + $input_exists = FALSE; } // For browser-submitted forms, the submitted values do not contain values // for certain elements (empty multiple select, unchecked checkbox). @@ -1258,11 +1344,8 @@ function _form_builder_handle_input_elem // submit explicit NULL values when calling drupal_form_submit(), so we do // not modify $form_state['input'] for them. if (!$input_exists && !$form_state['rebuild'] && !$form_state['programmed']) { - // We leverage the internal logic of form_set_value() to change the - // input values by passing $form_state['input'] instead of the usual - // $form_state['values']. In effect, this adds the necessary parent keys - // to $form_state['input'] and sets the element's input value to NULL. - _form_set_value($form_state['input'], $element, $element['#parents'], NULL); + // Add the necessary parent keys to $form_state['input']. + drupal_subtree($form_state['input'], $element['#parents'], FALSE, TRUE); $input_exists = TRUE; } // If we have input for the current element, assign it to the #value @@ -1410,31 +1493,10 @@ function form_state_values_clean(&$form_ // type. We remove the button value separately for each button element. foreach ($form_state['buttons'] as $button_type => $buttons) { foreach ($buttons as $button) { - // Remove this button's value from the submitted form values by finding - // the value corresponding to this button. - // We iterate over the #parents of this button and move a reference to - // each parent in $form_state['values']. For example, if #parents is: - // array('foo', 'bar', 'baz') - // then the corresponding $form_state['values'] part will look like this: - // array( - // 'foo' => array( - // 'bar' => array( - // 'baz' => 'button_value', - // ), - // ), - // ) - // We start by (re)moving 'baz' to $last_parent, so we are able unset it - // at the end of the iteration. Initially, $values will contain a - // reference to $form_state['values'], but in the iteration we move the - // reference to $form_state['values']['foo'], and finally to - // $form_state['values']['foo']['bar'], which is the level where we can - // unset 'baz' (that is stored in $last_parent). + // Remove this button's value from the submitted form values. $parents = $button['#parents']; - $values = &$form_state['values']; $last_parent = array_pop($parents); - foreach ($parents as $parent) { - $values = &$values[$parent]; - } + $values = &drupal_subtree($form_state['values'], $parents); unset($values[$last_parent]); } } @@ -1651,26 +1713,8 @@ function form_type_token_value($element, * Form state array where the value change should be recorded. */ function form_set_value($element, $value, &$form_state) { - _form_set_value($form_state['values'], $element, $element['#parents'], $value); -} - -/** - * Helper function for form_set_value() and _form_builder_handle_input_element(). - * - * We iterate over $parents and create nested arrays for them in $form_values if - * needed. Then we insert the value into the last parent key. - */ -function _form_set_value(&$form_values, $element, $parents, $value) { - $parent = array_shift($parents); - if (empty($parents)) { - $form_values[$parent] = $value; - } - else { - if (!isset($form_values[$parent])) { - $form_values[$parent] = array(); - } - _form_set_value($form_values[$parent], $element, $parents, $value); - } + $subtree = &drupal_subtree($form_state['values'], $element['#parents'], FALSE, TRUE); + $subtree = $value; } function form_options_flatten($array, $reset = TRUE) { Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.38 diff -u -p -r1.38 field.form.inc --- modules/field/field.form.inc 21 Dec 2009 13:47:32 -0000 1.38 +++ modules/field/field.form.inc 27 Dec 2009 01:43:42 -0000 @@ -214,7 +214,10 @@ 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_partial' => array( + 'sections' => array($field_name), + 'skip_ancestors' => TRUE, + ), '#submit' => array('field_add_more_submit'), '#ajax' => array( 'callback' => 'field_add_more_js', @@ -369,13 +372,8 @@ function field_add_more_js($form, $form_ if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { ajax_render(array()); } - // Navigate to the right part of the form. - $form_path = $form['#fields'][$field_name]['form_path']; - $field_form = $form; - foreach ($form_path as $key) { - $field_form = $field_form[$key]; - } - + // Retrieve just the field's part of the form. + $field_form = drupal_subtree($form, $form['#fields'][$field_name]['form_path']); // Add a DIV around the new field to receive the AJAX effect. $langcode = $field_form['#language']; $delta = $field_form[$langcode]['#max_delta']; Index: modules/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.15 diff -u -p -r1.15 file.module --- modules/file/file.module 12 Dec 2009 23:04:57 -0000 1.15 +++ modules/file/file.module 27 Dec 2009 01:43:42 -0000 @@ -468,9 +468,9 @@ function file_managed_file_value(&$eleme $fid = 0; // Find the current value of this field from the form state. - $form_state_fid = $form_state['values']; - foreach ($element['#parents'] as $parent) { - $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0; + $form_state_fid = drupal_subtree($form_state['values'], $element['#parents']); + if (!isset($form_state_fid)) { + $form_state_fid = 0; } if ($element['#extended'] && isset($form_state_fid['fid'])) { Index: modules/poll/poll.module =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v retrieving revision 1.330 diff -u -p -r1.330 poll.module --- modules/poll/poll.module 26 Dec 2009 16:50:09 -0000 1.330 +++ modules/poll/poll.module 27 Dec 2009 01:43:42 -0000 @@ -273,7 +273,13 @@ 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. + // @todo choice_wrapper has #tree set to FALSE, so this doesn't work. + // @see _form_validate_skip(). + '#validate_partial' => array( + 'sections' => array('choice_wrapper'), + 'skip_ancestors' => TRUE, + ), + '#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.31 diff -u -p -r1.31 form.test --- modules/simpletest/tests/form.test 17 Dec 2009 17:18:03 -0000 1.31 +++ modules/simpletest/tests/form.test 27 Dec 2009 01:43:42 -0000 @@ -231,6 +231,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_partial. + */ + function testValidatePartial() { + $this->drupalGet('form-test/validate-partial'); + + // 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.24 diff -u -p -r1.24 form_test.module --- modules/simpletest/tests/form_test.module 17 Dec 2009 17:18:03 -0000 1.24 +++ modules/simpletest/tests/form_test.module 27 Dec 2009 01:43:42 -0000 @@ -17,6 +17,12 @@ function form_test_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + $items['form-test/validate-partial'] = array( + 'title' => '#validate_partial test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_validate_partial_form'), + 'access arguments' => array('access content'), + ); $items['form_test/tableselect/multiple-true'] = array( 'title' => 'Tableselect checkboxes test', @@ -204,6 +210,96 @@ function form_test_validate_form_validat } /** + * Form builder for testing #validate_partial. + */ +function form_test_validate_partial_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_partial_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_partial_form_field_validate'), + '#weight' => $weight++, + ); + } + $form['field']['new']['#weight'] = 999; + $form['field']['submit'] = array( + '#type' => 'submit', + '#value' => 'Add', + '#validate_partial' => array( + 'sections' => array('field'), + 'skip_ancestors' => TRUE, + ), + '#submit' => array('form_test_validate_partial_form_field_submit'), + '#weight' => 1000, + ); + + $form['submit'] = array('#type' => 'submit', '#value' => 'Submit'); + + return $form; +} + +/** + * Form element validation handler for 'element' in form_test_validate_partial_form(). + */ +function form_test_validate_partial_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_partial_form(). + */ +function form_test_validate_partial_form_field_validate($element, &$form_state) { + drupal_set_message('Form element validation handler for field triggered.'); +} + +/** + * Form element submit handler for form_test_validate_partial_form(). + */ +function form_test_validate_partial_form_field_submit($form, &$form_state) { + // Store the new value and clear out the 'new' field. + $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_partial_form(). + */ +function form_test_validate_partial_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() {