modules/field/field.attach.inc | 49 +++++++--- modules/field/tests/field.test | 208 +++++++++++++++++++++++++++++++++++----- 2 files changed, 216 insertions(+), 41 deletions(-) diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc index 5bebe9b..8044887 100644 --- a/modules/field/field.attach.inc +++ b/modules/field/field.attach.inc @@ -555,16 +555,19 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) { * @param $langcode * The language the field values are going to be entered, if no language * is provided the default site language will be used. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. * * @see field_form_get_state() * @see field_form_set_state() */ -function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL) { +function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL, $options = array()) { // Set #parents to 'top-level' by default. $form += array('#parents' => array()); // If no language is provided use the default site language. - $options = array('language' => field_valid_language($langcode)); + $options['language'] = field_valid_language($langcode); $form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options); // Add custom weight handling. @@ -769,13 +772,17 @@ function field_attach_load_revision($entity_type, $entities, $options = array()) * If validation errors are found, a FieldValidationException is thrown. The * 'errors' property contains the array of errors, keyed by field name, * language and delta. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. */ -function field_attach_validate($entity_type, $entity) { +function field_attach_validate($entity_type, $entity, $options = array()) { $errors = array(); // Check generic, field-type-agnostic errors first. - _field_invoke_default('validate', $entity_type, $entity, $errors); + $null = NULL; + _field_invoke_default('validate', $entity_type, $entity, $errors, $null, $options); // Check field-type specific errors. - _field_invoke('validate', $entity_type, $entity, $errors); + _field_invoke('validate', $entity_type, $entity, $errors, $null, $options); // Let other modules validate the entity. // Avoid module_invoke_all() to let $errors be taken by reference. @@ -817,14 +824,17 @@ function field_attach_validate($entity_type, $entity) { * full form structure, or a sub-element of a larger form. * @param $form_state * An associative array containing the current state of the form. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. */ -function field_attach_form_validate($entity_type, $entity, $form, &$form_state) { +function field_attach_form_validate($entity_type, $entity, $form, &$form_state, $options = array()) { // Extract field values from submitted values. _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); // Perform field_level validation. try { - field_attach_validate($entity_type, $entity); + field_attach_validate($entity_type, $entity, $options); } catch (FieldValidationException $e) { // Pass field-level validation errors back to widgets for accurate error @@ -836,7 +846,7 @@ function field_attach_form_validate($entity_type, $entity, $form, &$form_state) field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); } } - _field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state); + _field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state, $options); } } @@ -857,12 +867,15 @@ function field_attach_form_validate($entity_type, $entity, $form, &$form_state) * full form structure, or a sub-element of a larger form. * @param $form_state * An associative array containing the current state of the form. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. */ -function field_attach_submit($entity_type, $entity, $form, &$form_state) { +function field_attach_submit($entity_type, $entity, $form, &$form_state, $options = array()) { // Extract field values from submitted values. - _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); + _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state, $options); - _field_invoke_default('submit', $entity_type, $entity, $form, $form_state); + _field_invoke_default('submit', $entity_type, $entity, $form, $form_state, $options); // Let other modules act on submitting the entity. // Avoid module_invoke_all() to let $form_state be taken by reference. @@ -1093,9 +1106,12 @@ function field_attach_delete_revision($entity_type, $entity) { * @param $langcode * (Optional) The language the field values are to be shown in. If no language * is provided the current language is used. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. */ -function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcode = NULL) { - $options = array('language' => array()); +function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcode = NULL, $options = array()) { + $options['language'] = array(); // To ensure hooks are only run once per entity, only process items without // the _field_view_prepared flag. @@ -1167,14 +1183,17 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcod * @param $langcode * The language the field values are to be shown in. If no language is * provided the current language is used. + * @param array $options + * An associative array of additional options. See field_invoke_method() for + * details. * @return * A renderable array for the field values. */ -function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL) { +function field_attach_view($entity_type, $entity, $view_mode, $langcode = NULL, $options = array()) { // Determine the actual language to display for each field, given the // languages available in the field data. $display_language = field_language($entity_type, $entity, NULL, $langcode); - $options = array('language' => $display_language); + $options['language'] = $display_language; // Invoke field_default_view(). $null = NULL; diff --git a/modules/field/tests/field.test b/modules/field/tests/field.test index 8004178..e74df7d 100644 --- a/modules/field/tests/field.test +++ b/modules/field/tests/field.test @@ -85,12 +85,28 @@ class FieldAttachTestCase extends FieldTestCase { } parent::setUp($modules); - $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); - $this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4); - $this->field = field_create_field($this->field); - $this->field_id = $this->field['id']; - $this->instance = array( - 'field_name' => $this->field_name, + $this->createFieldWithInstance(); + } + + /** + * Create a field and an instance of it. + * + * @param string $suffix + * (optional) A string that should only contain characters that are valid in + * PHP variable names as well. + */ + function createFieldWithInstance($suffix = '') { + $field_name = 'field_name' . $suffix; + $field = 'field' . $suffix; + $field_id = 'field_id' . $suffix; + $instance = 'instance' . $suffix; + + $this->$field_name = drupal_strtolower($this->randomName() . '_field_name' . $suffix); + $this->$field = array('field_name' => $this->$field_name, 'type' => 'test_field', 'cardinality' => 4); + $this->$field = field_create_field($this->$field); + $this->$field_id = $this->{$field}['id']; + $this->$instance = array( + 'field_name' => $this->$field_name, 'entity_type' => 'test_entity', 'bundle' => 'test_bundle', 'label' => $this->randomName() . '_label', @@ -107,7 +123,7 @@ class FieldAttachTestCase extends FieldTestCase { ) ) ); - field_create_instance($this->instance); + field_create_instance($this->$instance); } } @@ -641,13 +657,18 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { * Test field_attach_view() and field_attach_prepare_view(). */ function testFieldAttachView() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); $langcode = LANGUAGE_NONE; + $options = array('field_name' => $this->field_name_2); // Populate values to be displayed. $values = $this->_generateTestFieldValues($this->field['cardinality']); $entity_init->{$this->field_name}[$langcode] = $values; + $values_2 = $this->_generateTestFieldValues($this->field_2['cardinality']); + $entity_init->{$this->field_name_2}[$langcode] = $values_2; // Simple formatter, label displayed. $entity = clone($entity_init); @@ -662,15 +683,47 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { ), ); field_update_instance($this->instance); + $formatter_setting_2 = $this->randomName(); + $this->instance_2['display'] = array( + 'full' => array( + 'label' => 'above', + 'type' => 'field_test_default', + 'settings' => array( + 'test_formatter_setting' => $formatter_setting_2, + ) + ), + ); + field_update_instance($this->instance_2); + // View all fields. field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full'); $entity->content = field_attach_view($entity_type, $entity, 'full'); $output = drupal_render($entity->content); $this->content = $output; - $this->assertRaw($this->instance['label'], "Label is displayed."); + $this->assertRaw($this->instance['label'], "First field's label is displayed."); foreach ($values as $delta => $value) { $this->content = $output; $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); } + $this->assertRaw($this->instance_2['label'], "Second field's label is displayed."); + foreach ($values_2 as $delta => $value) { + $this->content = $output; + $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + } + // View single field (the second field). + field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full', $langcode, $options); + $entity->content = field_attach_view($entity_type, $entity, 'full', $langcode, $options); + $output = drupal_render($entity->content); + $this->content = $output; + $this->assertNoRaw($this->instance['label'], "First field's label is not displayed."); + foreach ($values as $delta => $value) { + $this->content = $output; + $this->assertNoRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + } + $this->assertRaw($this->instance_2['label'], "Second field's label is displayed."); + foreach ($values_2 as $delta => $value) { + $this->content = $output; + $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); + } // Label hidden. $entity = clone($entity_init); @@ -697,7 +750,7 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { $this->content = $output; $this->assertNoRaw($this->instance['label'], "Hidden field: label is not displayed."); foreach ($values as $delta => $value) { - $this->assertNoRaw($value['value'], "Hidden field: value $delta is not displayed."); + $this->assertNoRaw("$formatter_setting|{$value['value']}", "Hidden field: value $delta is not displayed."); } // Multiple formatter. @@ -907,11 +960,13 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { * hook_field_validate. */ function testFieldAttachValidate() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); $langcode = LANGUAGE_NONE; - // Set up values to generate errors + // Set up all but one values of the first field to generate errors. $values = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = -1; @@ -920,6 +975,14 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { $values[1]['value'] = 1; $entity->{$this->field_name}[$langcode] = $values; + // Set up all values of the second field to generate errors. + $values_2 = array(); + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + $values_2[$delta]['value'] = -1; + } + $entity->{$this->field_name_2}[$langcode] = $values_2; + + // Validate all fields. try { field_attach_validate($entity_type, $entity); } @@ -929,26 +992,57 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { foreach ($values as $delta => $value) { if ($value['value'] != 1) { - $this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on value $delta"); - $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on value $delta"); + $this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on first field's value $delta"); + $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on first field's value $delta"); unset($errors[$this->field_name][$langcode][$delta]); } else { - $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on value $delta"); + $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on first field's value $delta"); } } - $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set'); + foreach ($values_2 as $delta => $value) { + $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta"); + $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta"); + unset($errors[$this->field_name_2][$langcode][$delta]); + } + $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set for first field'); + $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field'); + + // Validate a single field. + $options = array('field_name' => $this->field_name_2); + try { + field_attach_validate($entity_type, $entity, $options); + } + catch (FieldValidationException $e) { + $errors = $e->errors; + } + + foreach ($values_2 as $delta => $value) { + $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta"); + $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta"); + unset($errors[$this->field_name_2][$langcode][$delta]); + } + $this->assertFalse(isset($errors[$this->field_name]), 'No validation errors are set for the first field, despite it having errors'); + $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field'); // Check that cardinality is validated. - $entity->{$this->field_name}[$langcode] = $this->_generateTestFieldValues($this->field['cardinality'] + 1); + $entity->{$this->field_name_2}[$langcode] = $this->_generateTestFieldValues($this->field_2['cardinality'] + 1); + // When validating all fields. try { field_attach_validate($entity_type, $entity); } catch (FieldValidationException $e) { $errors = $e->errors; } - $this->assertEqual($errors[$this->field_name][$langcode][0][0]['error'], 'field_cardinality', t('Cardinality validation failed.')); - + $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.'); + // When validating a single field (the second field). + try { + field_attach_validate($entity_type, $entity, $options); + } + catch (FieldValidationException $e) { + $errors = $e->errors; + } + $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.'); } /** @@ -958,34 +1052,59 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { * widgets show up. */ function testFieldAttachForm() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = LANGUAGE_NONE; + // When generating form for all fields. $form = array(); $form_state = form_state_defaults(); field_attach_form($entity_type, $entity, $form, $form_state); - $langcode = LANGUAGE_NONE; - $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "Form title is {$this->instance['label']}"); + $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "First field's form title is {$this->instance['label']}"); + $this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}"); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // field_test_widget uses 'textfield' - $this->assertEqual($form[$this->field_name][$langcode][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); - } + $this->assertEqual($form[$this->field_name][$langcode][$delta]['value']['#type'], 'textfield', "First field's form delta $delta widget is textfield"); + } + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + // field_test_widget uses 'textfield' + $this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield"); + } + + // When generating form for a single field (the second field). + $options = array('field_name' => $this->field_name_2); + $form = array(); + $form_state = form_state_defaults(); + field_attach_form($entity_type, $entity, $form, $form_state, NULL, $options); + + $this->assertFalse(isset($form[$this->field_name]), 'The first field does not exist in the form'); + $this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}"); + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + // field_test_widget uses 'textfield' + $this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield"); + } } /** * Test field_attach_submit(). */ function testFieldAttachSubmit() { + $this->createFieldWithInstance('_2'); + $entity_type = 'test_entity'; - $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $entity_init = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = LANGUAGE_NONE; - // Build the form. + // Build the form for all fields. $form = array(); $form_state = form_state_defaults(); - field_attach_form($entity_type, $entity, $form, $form_state); + field_attach_form($entity_type, $entity_init, $form, $form_state); // Simulate incoming values. + // First field. $values = array(); $weights = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { @@ -999,22 +1118,59 @@ class FieldAttachOtherTestCase extends FieldAttachTestCase { } // Leave an empty value. 'field_test' fields are empty if empty(). $values[1]['value'] = 0; - - $langcode = LANGUAGE_NONE; + // Second field. + $values_2 = array(); + $weights_2 = array(); + for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) { + $values_2[$delta]['value'] = mt_rand(1, 127); + // Assign random weight. + do { + $weight = mt_rand(0, $this->field_2['cardinality']); + } while (in_array($weight, $weights_2)); + $weights_2[$delta] = $weight; + $values_2[$delta]['_weight'] = $weight; + } + // Leave an empty value. 'field_test' fields are empty if empty(). + $values_2[1]['value'] = 0; // Pretend the form has been built. drupal_prepare_form('field_test_entity_form', $form, $form_state); drupal_process_form('field_test_entity_form', $form, $form_state); $form_state['values'][$this->field_name][$langcode] = $values; + $form_state['values'][$this->field_name_2][$langcode] = $values_2; + + // Call field_attach_submit() for all fields. + $entity = clone($entity_init); field_attach_submit($entity_type, $entity, $form, $form_state); asort($weights); + asort($weights_2); $expected_values = array(); + $expected_values_2 = array(); foreach ($weights as $key => $value) { if ($key != 1) { $expected_values[] = array('value' => $values[$key]['value']); } } $this->assertIdentical($entity->{$this->field_name}[$langcode], $expected_values, 'Submit filters empty values'); + foreach ($weights_2 as $key => $value) { + if ($key != 1) { + $expected_values_2[] = array('value' => $values_2[$key]['value']); + } + } + $this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values'); + + // Call field_attach_submit() for a single field (the second field). + $options = array('field_name' => $this->field_name_2); + $entity = clone($entity_init); + field_attach_submit($entity_type, $entity, $form, $form_state, $options); + $expected_values_2 = array(); + foreach ($weights_2 as $key => $value) { + if ($key != 1) { + $expected_values_2[] = array('value' => $values_2[$key]['value']); + } + } + $this->assertFalse(isset($entity->{$this->field_name}), 'The first field does not exist in the entity object'); + $this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values'); } }