diff --git a/cck_select_other.install b/cck_select_other.install index b234b14..6f7ee5c 100644 --- a/cck_select_other.install +++ b/cck_select_other.install @@ -1,12 +1,19 @@ array( 'label' => t('Select other list'), 'description' => t('Provides an "other" option, which allows the user to provide an alternate value.'), - 'field types' => array('list', 'list_number', 'list_text'), + 'field types' => array('list_integer', 'list_float', 'list_text'), 'settings' => array( 'select_list_options' => '', 'select_list_options_fieldset' => array( @@ -30,20 +30,6 @@ function cck_select_other_field_widget_info() { } /** - * Implementation of hook_element_info(). - */ -function cck_select_other_element_info() { - return array( - 'cck_select_other' => array( - '#input' => TRUE, - '#process' => array('cck_select_other_process'), - '#post_render' => array('cck_select_other_post_render'), - '#pre_render' => array('cck_select_other_pre_render'), - ), - ); -} - -/** * Implementation of hook_field_formatter_info(). */ function cck_select_other_field_formatter_info() { @@ -51,7 +37,7 @@ function cck_select_other_field_formatter_info() { 'cck_select_other' => array( 'label' => t('Select other'), 'description' => t('The default list module formatters do not take into account select other list widgets.'), - 'field types' => array('list_integer', 'list_float', 'list_text', 'list_boolean'), + 'field types' => array('list_integer', 'list_float', 'list_text'), ), ); } @@ -65,9 +51,9 @@ function cck_select_other_field_formatter_view($entity_type, $entity, $field, $i $settings = $instance['widget']['settings']; $options = cck_select_other_options($instance); + $element = array(); foreach ($items as $delta => $item) { - $value = isset($options[$item['value']]) ? field_filter_xss($options[$item['value']]) : field_filter_xss($item['value']); $element[$delta] = array( @@ -122,63 +108,70 @@ function cck_select_other_field_widget_settings_form($field, $instance) { */ function cck_select_other_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { $options = cck_select_other_options($instance); - $def = $instance['required'] ? '' : '_none'; - $otherdef = ''; - - if (empty($items)) { - $items[] = array('value' => ''); - } - - if (!isset($items[$delta])) { - $items[$delta] = array('value' => ''); - } - - // Set default value if we have instance data for delta, a default - // value setting, or something basic if none at all. - if (!empty($items[$delta]['value'])) { - $def = (in_array($items[$delta]['value'], array_keys($options))) ? $items[$delta]['value'] : 'other'; - $otherdef = ($def == 'other') ? $items[$delta]['value'] : ''; - } - else if (isset($instance['default_value'])) { - $def = isset($instance['default_value'][0]['select_other_list']) ? $instance['default_value'][0]['select_other_list'] : 'other'; - $otherdef = ($def == 'other') ? $instance['default_value'][0]['value'] : ''; - } - - // This needs to be pulled out of our original element. - $description = $element['#description']; - $element = array( - '#type' => $instance['widget']['type'], - '#default_value' => '', - '#prefix' => '
', - '#suffix' => '
', - '#field_name' => $field['field_name'], // Required fields for field_conditional_state. - '#field_parents' => isset($form['#parents']) ? $form['#parents'] : array(), + // Setup select other wrapper. + $element += array( '#bundle' => $instance['bundle'], + '#field_name' => $field['field_name'], + '#langcode' => $langcode, + '#element_validate' => array('cck_select_other_widget_validate'), + '#pre_render' => array('cck_select_other_widget_pre_render'), + '#attributes' => array( + 'class' => array('form-select-other-wrapper', 'cck-select-other-wrapper'), + ), ); + // Setup select list. $element['select_other_list'] = array( - '#title' => check_plain($instance['label']), - '#description' => !empty($description) ? $description : t('You may specify your own option.'), + '#title' => $element['#title'], + '#description' => $element['#description'], '#type' => 'select', '#options' => $options, - '#default_value' => $def, + '#required' => $instance['required'], '#attributes' => array( - 'class' => array('form-text form-select select_other_list'), + 'class' => array('form-text form-select form-select-other-list'), ), - '#required' => $instance['required'], ); - // @fixme - #states is REALLY slow here so I'm using my own shit again. + // Setup text input. $element['select_other_text_input'] = array( '#type' => 'textfield', - '#default_value' => $otherdef, + '#title' => t('Provide other option'), + '#title_display' => 'invisible', + '#size' => 60, '#attributes' => array( - 'class' => array('form-text select_other_text_input'), + 'class' => array('form-text form-select-other-text-input'), ), - '#element_validate' => array('cck_select_other_widget_validate'), // Always send through this validate function! ); + // Default empty values. + $list_default = $instance['required'] ? '' : '_none'; + $text_default = ''; + $value = ''; + + if (isset($items[$delta]['value'])) { + // Use the value provided in items. + $value = $items[$delta]['value']; + } + elseif (isset($instance['default_value'])) { + // Use the default value of the field if it is set. + $value = $instance['default_value'][0]['value']; + } + + if ($value && in_array($value, array_keys($options))) { + // Value is not empty and value is in the list. + $list_default = $value; + } + elseif ($value) { + // Set the list default to other. + $list_default = 'other'; + $text_default = $value; + } + + // Set default values. + $element['select_other_list']['#default_value'] = $list_default; + $element['select_other_text_input']['#default_value'] = $text_default; + return $element; } @@ -209,43 +202,89 @@ function cck_select_other_form_alter(&$form, &$form_state, $form_id) { * Validate empty text input for other selection. */ function cck_select_other_widget_validate($element, &$form_state) { - // Reverse element parents because of element containers, notably profile2. - $reversed = array_reverse($element['#parents']); - - $element_name = array_shift($reversed); - $delta = array_shift($reversed); - $langcode = array_shift($reversed); - $field_name = array_shift($reversed); - - if (isset($form_state['field'][$field_name])) { - // Retrieve stored field & instance info and form state values. - $field = $form_state['field'][$field_name]; - $values = &$form_state['values']; + $values = drupal_array_get_nested_value($form_state['values'], $element['#array_parents']); + + if (empty($values)) { + // Field UI does not behave in the same way as normal form operations, and + // values should be extracted from $element['#parents'] instead. + $values = drupal_array_get_nested_value($form_state['values'], $element['#parents']); } - elseif (!empty($reversed) && isset($form_state['field']['#parents'])) { - // Profile 2 exception. - $container = array_shift($reversed); - $field = $form_state['field']['#parents'][$container]['#fields'][$field_name]; - $values = &$form_state['values'][$container]; + + if (!$element['select_other_list']['#required'] && $values['select_other_list'] == '_none') { + // Empty select list option. + form_set_value($element, array('value' => NULL), $form_state); + } + elseif ($element['select_other_list']['#required'] && $values['select_other_list'] == '') { + // Empty select list option for required field. + form_set_value($element, array('value' => ''), $form_state); + form_error($element, t('You must select an option.')); + } + elseif ($element['select_other_list']['#required'] && $values['select_other_list'] == 'other' && !$values['select_other_text_input']) { + // Empty text input for required field. + form_set_value($element, array('value' => NULL), $form_state); + form_error($element['select_other_text_input'], t('You must provide a value for this option.')); + } + elseif ($values['select_other_list'] == 'other' && $values['select_other_text_input']) { + // Non-empty text input value. + form_set_value($element, array('value' => $values['select_other_text_input']), $form_state); + } + elseif ($values['select_other_list'] == 'other' && !$values['select_other_text_input']) { + // Empty text for non-required field. + form_set_value($element, array('value' => NULL), $form_state); } else { - // Catastrophic error..? - form_set_error($element['#name'], t('An error occurred trying to validate this field.')); - watchdog('cck_select_other', 'Could not find field info in form state array for select other field, %name.', array('%name' => $field_name), WATCHDOG_ERROR); + // Non-empty select list value. + form_set_value($element, array('value' => $values['select_other_list']), $form_state); } - if ($field[$langcode]['instance']['required'] && $values[$field_name][$langcode][$delta]['select_other_list'] == 'other' && empty($values[$field_name][$langcode][$delta]['select_other_text_input'])) { - // Empty other field. - form_set_error($element['#name'], t('A non-empty value is required for this option.')); - } + $field = field_info_field($element['#field_name']); - if (!$field[$langcode]['instance']['required'] && $values[$field_name][$langcode][$delta]['select_other_list'] == '_none') { - // Non-required field value. - form_set_value($element, array(NULL), $form_state); + // Validate integer and float values for other options. + if ($field['type'] == 'list_integer' && $values['select_other_list'] == 'other') { + if (!preg_match('/^-?\d+$/', $values['select_other_text_input'])) { + form_error($element['select_other_text_input'], t('Only integers are allowed.')); + } + } + elseif ($field['type'] == 'list_float' && $values['select_other_list'] == 'other') { + if (!is_numeric($values['select_other_text_input'])) { + form_error($element['select_other_text_input'], t('Only valid numbers are allowed.')); + } } } /** + * Attaches Javascript during pre build because this is when array parents + * should be defined to take advantage of modules that alter the element + * structure such as field_collection. + * + * @param $element + * The element array. + * @return array + * The element array. + */ +function cck_select_other_widget_pre_render($element) { + $key = $element['#field_name'] . '_' . $element['#delta']; + $settings = array( + $key => array( + 'list_element' => $element['select_other_list']['#id'], + 'input_element' => $element['select_other_text_input']['#id'], + ), + ); + + $element['#attached'] = array( + 'js' => array( + array( + 'data' => array('CCKSelectOther' => $settings), + 'type' => 'setting', + ), + drupal_get_path('module', 'cck_select_other') . '/cck_select_other.js', + ), + ); + + return $element; +} + +/** * Retrieve options for the select list * @param $field the field instance we're working with * @return an array of options to pass into the Form API. @@ -299,157 +338,6 @@ function cck_select_other_options($field) { } /** - * CCK Select Other widget process callback - * @param $element - * @param &$form_state - * @return $element; - */ -function cck_select_other_process($element, &$form_state) { - if (!isset($element['#name'])) { - return $element; - } - - $keys = $element['#parents']; - - // field_values need to be a reference! - $field_values = &$form_state['values']; - foreach ($keys as $key) { - $field_values = &$field_values[$key]; - } - - // Reverse array parents because of element containers, notably profile2. - $reversed = array_reverse($keys); - - $delta = $reversed[0]; - $langcode = $reversed[1]; - $field_name = $reversed[2]; - - if (isset($field_values) && !empty($field_values)) { - if ($field_values['select_other_list'] == '_none') { - // If we are not a required field, then we do not set a value. - $element['#value'] = ''; - $field_values = array( - 'value' => '', - ); - } - else if ($field_values['select_other_list'] == 'other') { - // Use text input if we have 'other' selected - $element['#value'] = $field_values['select_other_text_input']; - $field_values = array( - 'value' => $field_values['select_other_text_input'], - ); - } - else { - // Use the select list otherwise - $element['#value'] = $field_values['select_other_list']; - $field_values = array( - 'value' => $field_values['select_other_list'], - ); - } - - return $element; - } - else { - $element['#value'] = ''; - if (isset($element['select_other_list']['#default_value'])) { - $element['select_other_list']['#value'] = $element['select_other_list']['#default_value']; - } - } - - return $element; -} - -/** - * Pre render callback for the form so we can recreate the fake form after it gets - * blown away by the CCK process callback. - * @param $element the element - * @param $form_state - * @return $form - */ -function cck_select_other_pre_render($element, $form_state = NULL) { - static $js; - - $errors = form_get_errors(); - if (!empty($errors)) { - // Validation errors for the text input box get lost so need to be injected. - $text_element = $element['#name'] . '[select_other_text_input]'; - if (in_array($text_element, array_keys($errors))) { - $element['select_other_text_input']['#attributes']['class'][] = 'error'; - } - } - - if (!isset($form_state)) { - return $element; - } - - if (!isset($element['#type']) || $element['#type'] <> 'cck_select_other') { - return $element; - } - - // No matches = not our field. - $n = preg_match_all("/[A-Za-z0-9\-\_]+/", $element['#name'], $matches); - if ($n == 0) { - return $element; - } - - // By any chance if we don't have any array keys, get out of here. - $keys = isset($matches[0]) ? $matches[0]: NULL; - if (!isset($keys)) { - return $element; - } - - foreach ($keys as $key => $val) { - $keys[$key] = preg_replace("/_/", '-', $val); - } - $field_id = implode('-', $keys); - - if (!$js) { - drupal_add_js(drupal_get_path('module', 'cck_select_other') . '/cck_select_other.js'); - $js = TRUE; - } - drupal_add_js(array('CCKSelectOther' => array(array('field_id' => $field_id))), array('type' => 'setting')); -} - -/** - * Post-render callback to add javascript functionality - * @param $content - * @param $element - * @return $form - */ -function cck_select_other_post_render($content, $element) { - static $js; - - if ($element['#type'] <> 'cck_select_other') { - return $content; - } - - // No matches = not our field. - $n = preg_match_all("/[A-Za-z0-9\-\_]+/", $element['#name'], $matches); - if ($n == 0) { - return $element; - } - - // By any chance if we don't have any array keys, get out of here. - $keys = isset($matches[0]) ? $matches[0]: NULL; - if (!isset($keys)) { - return $element; - } - - foreach ($keys as $key => $val) { - $keys[$key] = preg_replace("/_/", '-', $val); - } - $field_id = implode('-', $keys); - - if (!$js) { - drupal_add_js(drupal_get_path('module', 'cck_select_other') . '/cck_select_other.js'); - $js = TRUE; - } - drupal_add_js(array('CCKSelectOther' => array(array('field_id' => $field_id))), array('type' => 'setting')); - - return $content; -} - -/** * Implementation of hook_content_migrate_field_alter(). */ function cck_select_other_content_migrate_field_alter(&$field_value) { diff --git a/tests/cck_select_other.test b/tests/cck_select_other.test index 45d1663..f35ca8a 100644 --- a/tests/cck_select_other.test +++ b/tests/cck_select_other.test @@ -1,15 +1,13 @@ t('CCK Select Other Basic Test'), 'description' => t('Test saving values with the CCK Select Other widget.'), - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -207,7 +204,7 @@ class CCKSelectOtherBasicTest extends CCKSelectOtherTest { $this->drupalLogin($this->web_user); $this->drupalPost('node/' . $this->test_node->nid . '/edit', $edit, t('Save')); - $this->assertNoRaw(t('A non-empty value is required for this option.'), t('Did not fail validation for non-required field.')); + $this->assertNoRaw(t('You must provide a value for this option.'), t('Did not fail validation for non-required field.')); $this->drupalLogout(); @@ -220,7 +217,7 @@ class CCKSelectOtherBasicTest extends CCKSelectOtherTest { $field_str = str_replace('_', '-', $this->test_field['field_name']); $this->drupalPost('node/' . $this->test_node->nid . '/edit', $edit, t('Save')); - $this->assertRaw(t('A non-empty value is required for this option.'), t('Failed validation for required field.')); + $this->assertRaw(t('You must provide a value for this option.'), t('Failed validation for required field.')); $elements = $this->xpath('//input[@name="' . $text_field .'" and contains(@class, "error")]'); $this->assertEqual(count($elements), 1, t('Found error class on %field element.', array('%field' => $text_field))); @@ -237,7 +234,7 @@ class CCKSelectOtherHTMLEntitiesTest extends CCKSelectOtherTest { return array( 'name' => 'CCK Select Other HTML Entities', 'description' => 'Assert HTML entities are not double-encoded.', - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -264,8 +261,8 @@ class CCKSelectOtherHTMLEntitiesTest extends CCKSelectOtherTest { } /** - * @class - * + * Assert that allowed values restrict select other list options in the rare + * case that someone decides to use them. */ class CCKSelectOtherAllowedValuesTest extends CCKSelectOtherTest { @@ -273,7 +270,7 @@ class CCKSelectOtherAllowedValuesTest extends CCKSelectOtherTest { return array( 'name' => t('CCK Select Other Allowed Values Test'), 'description' => t('Confirm that allowed values set on a field restrict the widget.'), - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -322,15 +319,14 @@ class CCKSelectOtherAllowedValuesTest extends CCKSelectOtherTest { } /** - * @class - * CCK Select Other PHP Options Test + * Assert that PHP options work correctly in select other list. */ class CCKSelectOtherPHPOptionsTest extends CCKSelectOtherTest { public static function getInfo() { return array( 'name' => t('CCK Select Other PHP Options Test'), 'description' => t('Confirm that php options are generated in the select list.'), - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -375,15 +371,15 @@ class CCKSelectOtherPHPOptionsTest extends CCKSelectOtherTest { } /** - * @class - * CCK Select Other Multiple Fields Test + * Assert that select other list is functional when there are multiple select + * other fields on a page. */ class CCKSelectOtherMultipleFieldsTest extends CCKSelectOtherTest { public static function getInfo() { return array( 'name' => t('CCK Select Other Multiple Fields'), 'description' => t('Tests UI when a content type has multiple select other fields.'), - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -449,15 +445,14 @@ class CCKSelectOtherMultipleFieldsTest extends CCKSelectOtherTest { } /** - * @class CCK Select Other Multiple Value List Field Case - * Tests multiple value list fields where $delta > 1 or -1 (unlimited). + * Assert multiple value list fields where $delta > 1 or -1 (unlimited). */ class CCKSelectOtherMultipleValueListTest extends CCKSelectOtherTest { static public function getInfo() { return array( 'name' => t('CCK Select Other Multiple Values'), 'description' => t('Tests multiple value list field. Where delta is greater than one.'), - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -491,12 +486,12 @@ class CCKSelectOtherMultipleValueListTest extends CCKSelectOtherTest { $name = substr($this->test_field['field_name'], 6); $this->drupalGet('node/' . $this->test_node->nid . '/edit'); - $this->assertOptionSelected('edit-field-' . $name . '-und-0-select-other-list', $this->test_instance['default_value'][0]['value'], t('%b', array('%b' => json_encode($this->test_instance['default_value'])))); + $this->assertOptionSelected('edit-field-' . $name . '-und-0-select-other-list', $this->test_instance['default_value'][0]['value']); $this->assertOptionSelected('edit-field-' . $name . '-und-1-select-other-list', $this->test_instance['default_value'][0]['value']); - // Try to post and make sure it saved both values correctly. - // In unlimited situation, there are two elements by default. - // We'll try add more later. + // Try to post and make sure it saved both values correctly. In unlimited + // situation, there are two elements by default. Test add more function + // later. $options_arr = cck_select_other_options($this->test_instance); do { $firstoption = array_rand(cck_select_other_options($this->test_instance)); @@ -520,7 +515,7 @@ class CCKSelectOtherMultipleValueListTest extends CCKSelectOtherTest { } /** - * @class CCKSelectOtherNotRequiredTest + * Assert that select other list is not saving any values if -none- is selected */ class CCKSelectOtherNotRequiredTest extends CCKSelectOtherTest { @@ -528,7 +523,7 @@ class CCKSelectOtherNotRequiredTest extends CCKSelectOtherTest { return array( 'name' => t('CCK Select Other Not Required'), 'description' => t('Asserts that CCK Select Other is not saving any values if -none- is selected.'), - 'group' => t('Field UI'), + 'group' => 'Field UI', ); } @@ -595,3 +590,139 @@ class CCKSelectOtherNotRequiredTest extends CCKSelectOtherTest { } } + +/** + * Assert that select other field widget can be used with non-text fields. + * + * This is not based on CCKSelectOtherTest case because it's a different field. + */ +class CCKSelectOtherNumericFieldTest extends DrupalWebTestCase { + + static public function getInfo() { + return array( + 'name' => 'CCK Select Other Numeric Field', + 'description' => 'Assert that select other widget behaves correctly for non-text fields.', + 'group' => 'Field UI', + ); + } + + public function setUp() { + parent::setUp(array('cck_select_other')); + + $this->assertTrue(module_exists('cck_select_other'), t('CCK Select Other module is enabled.')); + + $content_type = strtolower($this->randomName(5)); + + /* Setup an admin user */ + $this->admin_user = $this->drupalCreateUser(array('administer content types', 'administer site configuration')); + $this->drupalLogin($this->admin_user); + + /* Create a new content type */ + $settings = array( + 'type' => $content_type, + ); + $this->contentType = $this->drupalCreateContentType($settings); + + /* Create some options for our select other list */ + $this->options = "1\n2\n3\n4\n5\nother|Other"; + + /* Create a new field on our content type */ + $field_label = $this->randomName(5); + $field_name = strtolower($this->randomName(5)); + $bundle_path = 'admin/structure/types/manage/' . $this->contentType->type; + $edit = array( + 'fields[_add_new_field][label]' => $field_label, + 'fields[_add_new_field][field_name]' => $field_name, + 'fields[_add_new_field][type]' => 'list_integer', + 'fields[_add_new_field][widget_type]' => 'cck_select_other', + ); + $this->drupalPost($bundle_path . '/fields', $edit, 'Save'); + $edit = array( + 'instance[required]' => TRUE, + 'instance[widget][settings][select_list_options]' => $this->options, + 'field_' . $field_name . '[und][0][select_other_list]' => '_none', + ); + $this->drupalPost($bundle_path . '/fields/field_' . $field_name, $edit, 'Save settings'); + $this->drupalGet($bundle_path . '/fields/field_' . $field_name); + + $this->test_field = field_info_field('field_' . $field_name); + $this->test_instance = field_info_instance('node', 'field_' . $field_name, $this->contentType->type); + + /* Setup a web user that can create content */ + // @todo bypass node access seems to be the only thing that does not return 403 + $this->web_user = $this->drupalCreateUser(array('access content', 'create ' . $this->contentType->type . ' content', 'delete any ' . $this->contentType->type . ' content', 'bypass node access')); + + $this->drupalLogout(); + + $this->drupalLogin($this->web_user); + + $settings = array( + 'type' => $this->contentType->type, + ); + $this->test_node = $this->drupalCreateNode($settings); + + $this->drupalLogout(); + + $options_arr = cck_select_other_options($this->test_instance); + $this->assertEqual(7, count($options_arr), t('There are 6 = %count options set on the field.', array('%count' => count($options_arr)))); + } + + /** + * Implementation of tearDown() method + */ + public function tearDown() { + $this->drupalLogin($this->web_user); + $this->drupalPost('node/' . $this->test_node->nid . '/delete', array('confirm' => 1), 'Delete'); + $this->drupalLogout(); + + $this->drupalLogin($this->admin_user); + field_delete_field($this->test_field['field_name']); + $this->drupalLogout(); + + $this->test_node = NULL; + $this->test_field = NULL; + $this->test_instance = NULL; + $this->admin_user = NULL; + $this->web_user = NULL; + $this->options = NULL; + + parent::tearDown(); + } + + /** + * Test numeric field CRUD. + */ + public function testSelectOtherList() { + $this->drupalLogin($this->web_user); + + // Save a regular field value. + $value = rand(1, 5); + + $select_field = $this->test_field['field_name'] . '[und][0][select_other_list]'; + $text_field = $this->test_field['field_name'] . '[und][0][select_other_text_input]'; + $edit = array( + $select_field => $value, + ); + + $this->drupalPost('node/' . $this->test_node->nid . '/edit', $edit, t('Save')); + $this->test_node = node_load($this->test_node->nid, NULL, TRUE); + $this->assertRaw($this->test_node->{$this->test_field['field_name']}['und'][0]['value'], t('Select other field data %field matches %match on node.', array('%field' => $this->test_node->{$this->test_field['field_name']}['und'][0]['value'], '%match' => $value))); + + + // Save an other value as an integer. + $value = rand(50, 1000); + $edit[$select_field] = 'other'; + $edit[$text_field] = $value; + + $this->drupalPost('node/' . $this->test_node->nid . '/edit', $edit, t('Save')); + $this->test_node = node_load($this->test_node->nid, NULL, TRUE); + $this->assertRaw($this->test_node->{$this->test_field['field_name']}['und'][0]['value'], t('Select other field data %field matches %match on node.', array('%field' => $this->test_node->{$this->test_field['field_name']}['und'][0]['value'], '%match' => $value))); + + // Attempt to save an other value that is non-integer. + $value = $this->randomName(10); + $edit[$text_field] = $value; + $this->drupalPost('node/' . $this->test_node->nid . '/edit', $edit, t('Save')); + $this->assertText('Only integers are allowed', t('Select other field did not save an illegal value.')); + } + +}