diff --git a/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php b/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php index 3771d81..8c7433c 100644 --- a/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php +++ b/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Entity\Display; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; /** * Provides a common interface for entity view displays. @@ -51,4 +52,39 @@ public function build(FieldableEntityInterface $entity); */ public function buildMultiple(array $entities); + /** + * Returns a renderable array for the specified field. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity being displayed. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to be displayed. + * @param string $type + * The formatter type. + * @param array $settings + * The formatter settings. + * + * @return array + * A renderable array for the field. + */ + public function buildField(FieldableEntityInterface $entity, FieldDefinitionInterface $field_definition, $type, $settings); + + /** + * Returns a renderable array for the specified field of a set of entities. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface[] $entities + * The entities being displayed. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to be displayed. + * @param string $type + * The formatter type. + * @param array $settings + * The formatter settings. + * + * @return array + * A renderable array for the field, indexed by the same keys as the + * $entities array parameter. + */ + public function buildFieldMultiple(array $entities, FieldDefinitionInterface $field_definition, $type, $settings); + } diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php index efffdbc..c9a92b4 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php @@ -9,10 +9,12 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Entity\EntityDisplayBase; use Drupal\Core\Entity\EntityDisplayPluginCollection; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Entity\EntityDisplayBase; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FormatterInterface; /** * Configuration entity that contains display options for all components of a @@ -225,22 +227,7 @@ public function buildMultiple(array $entities) { // Run field formatters. foreach ($this->getComponents() as $name => $options) { if ($formatter = $this->getRenderer($name)) { - // Group items across all entities and pass them to the formatter's - // prepareView() method. - $grouped_items = array(); - foreach ($entities as $id => $entity) { - $items = $entity->get($name); - $items->filterEmptyItems(); - $grouped_items[$id] = $items; - } - $formatter->prepareView($grouped_items); - - // Then let the formatter build the output for each entity. - foreach ($entities as $id => $entity) { - $items = $grouped_items[$id]; - $build_list[$id][$name] = $formatter->view($items); - $build_list[$id][$name]['#access'] = $items->access('view'); - } + $this->formatFieldMultiple($build_list, $entities, $formatter, $name); } } @@ -265,6 +252,87 @@ public function buildMultiple(array $entities) { } /** + * Formats a field for multiple entities. + * + * @param array &$build_list + * A reference to a renderable array, indexed by the same keys as the + * $entities array parameter. + * @param \Drupal\Core\Entity\FieldableEntityInterface[] $entities + * The entities being displayed. + * @param \Drupal\Core\Field\FormatterInterface $formatter + * The field formatter to be used. + * @param string $field_name + * The name of the field to be formatted. + * @param string $field_key + * (optional) The key every field render array will be keyed with in the + * build list. Defaults to the field name. + */ + protected function formatFieldMultiple(array &$build_list, array $entities, FormatterInterface $formatter, $field_name, $field_key = NULL) { + if (!isset($field_key)) { + $field_key = $field_name; + } + + // Group items across all entities and pass them to the formatter's + // prepareView() method. + $grouped_items = array(); + foreach ($entities as $index => $entity) { + $items = $entity->get($field_name); + $items->filterEmptyItems(); + $grouped_items[$index] = $items; + } + $formatter->prepareView($grouped_items); + + foreach ($entities as $index => $entity) { + /** @var \Drupal\Core\Field\FieldItemListInterface $items */ + $items = $grouped_items[$index]; + // If a field key is explicitly not specified, it means we do not need the + // additional nesting level. + if ($field_key) { + $build = &$build_list[$index][$field_key]; + } + else { + $build = &$build_list[$index]; + } + // Then let the formatter build the output for each entity. + $build = $formatter->view($items); + $build['#access'] = $items->access('view'); + } + } + + /** + * {@inheritdoc} + */ + public function buildField(FieldableEntityInterface $entity, FieldDefinitionInterface $field_definition, $type, $settings) { + $build_list = $this->buildFieldMultiple([$entity], $field_definition, $type, $settings); + return reset($build_list); + } + + /** + * {@inheritdoc} + */ + public function buildFieldMultiple(array $entities, FieldDefinitionInterface $field_definition, $type, $settings) { + $build_list = []; + + $options = [ + 'field_definition' => $field_definition, + 'configuration' => array( + 'type' => $type, + 'settings' => $settings, + 'label' => 'hidden', + 'weight' => 0, + ), + 'view_mode' => static::CUSTOM_MODE, + ]; + + /** @var \Drupal\Core\Field\FormatterInterface $formatter */ + if ($formatter = $this->pluginManager->getInstance($options)) { + $this->formatFieldMultiple($build_list, $entities, $formatter, $field_definition->getName(), FALSE); + } + + return $build_list; + } + + /** * {@inheritdoc} */ public function getPluginCollections() { diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 352679c..40eaeeb 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -855,7 +855,7 @@ public function getEntityTypeLabels($group = FALSE) { public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = array()) { $translation = $entity; - if ($entity instanceof TranslatableInterface) { + if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { if (empty($langcode)) { $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); } diff --git a/core/lib/Drupal/Core/Field/FormatterInterface.php b/core/lib/Drupal/Core/Field/FormatterInterface.php index 4ba0efa..cc915fe 100644 --- a/core/lib/Drupal/Core/Field/FormatterInterface.php +++ b/core/lib/Drupal/Core/Field/FormatterInterface.php @@ -60,7 +60,7 @@ public function settingsSummary(); * items. * * @param \Drupal\Core\Field\FieldItemListInterface[] $entities_items - * Array of field values, keyed by entity ID. + * An array of field item values. */ public function prepareView(array $entities_items); diff --git a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php index ab40e6f..e32753e 100644 --- a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php +++ b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php @@ -60,6 +60,7 @@ protected function setUp() { public function testCommentFieldName() { $view = Views::getView('test_comment_field_name'); $this->executeView($view); + $view->render(); $expected_result = [ [ @@ -84,6 +85,8 @@ public function testCommentFieldName() { $this->container->get('account_switcher')->switchTo(new AnonymousUserSession()); $view = Views::getView('test_comment_field_name'); $this->executeView($view); + $view->render(); + // Test that data rendered. $this->assertIdentical($this->comment->getFieldName(), $view->field['field_name']->advancedRender($view->result[0])); $this->assertIdentical($this->customComment->getFieldName(), $view->field['field_name']->advancedRender($view->result[1])); diff --git a/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php b/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php index 92353f0..aeb67f5 100644 --- a/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php +++ b/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php @@ -137,6 +137,7 @@ public function _testSimpleFieldRender() { $view = Views::getView('test_view_fieldapi'); $this->prepareView($view); $this->executeView($view); + $view->render(); // Tests that the rendered fields match the actual value of the fields. for ($i = 0; $i < 3; $i++) { @@ -153,6 +154,7 @@ public function _testInaccessibleFieldRender() { $view = Views::getView('test_view_fieldapi'); $this->prepareView($view); $this->executeView($view); + $view->render(); // Check that the field handler for the hidden field is correctly removed // from the display. @@ -178,6 +180,7 @@ public function _testFormatterSimpleFieldRender() { 'trim_length' => 3, ); $this->executeView($view); + $view->render(); // Make sure that the formatter works as expected. // @TODO: actually there should be a specific formatter. @@ -196,6 +199,7 @@ public function _testMultipleFieldRender() { $view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE; $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3; $this->executeView($view); + $view->render(); for ($i = 0; $i < 3; $i++) { $rendered_field = $view->style_plugin->getField($i, $field_name); @@ -205,7 +209,7 @@ public function _testMultipleFieldRender() { foreach ($pure_items as $j => $item) { $items[] = $pure_items[$j]['value']; } - $this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.'); + $this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited.'); } // Test that an empty field is rendered without error. @@ -218,6 +222,7 @@ public function _testMultipleFieldRender() { $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3; $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_offset'] = 1; $this->executeView($view); + $view->render(); for ($i = 0; $i < 3; $i++) { $rendered_field = $view->style_plugin->getField($i, $field_name); @@ -227,7 +232,7 @@ public function _testMultipleFieldRender() { foreach ($pure_items as $j => $item) { $items[] = $pure_items[$j]['value']; } - $this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.'); + $this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited and the offset is correct.'); } $view->destroy(); @@ -238,6 +243,7 @@ public function _testMultipleFieldRender() { $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3; $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_reversed'] = TRUE; $this->executeView($view); + $view->render(); for ($i = 0; $i < 3; $i++) { $rendered_field = $view->style_plugin->getField($i, $field_name); @@ -248,7 +254,7 @@ public function _testMultipleFieldRender() { foreach ($pure_items as $j => $item) { $items[] = $pure_items[$j]['value']; } - $this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.'); + $this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited and they are reversed.'); } $view->destroy(); @@ -259,6 +265,7 @@ public function _testMultipleFieldRender() { $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_first_last'] = TRUE; $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_reversed'] = FALSE; $this->executeView($view); + $view->render(); for ($i = 0; $i < 3; $i++) { $rendered_field = $view->style_plugin->getField($i, $field_name); @@ -266,7 +273,7 @@ public function _testMultipleFieldRender() { $pure_items = $this->nodes[$i]->{$field_name}->getValue(); $items[] = $pure_items[0]['value']; $items[] = $pure_items[4]['value']; - $this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.'); + $this->assertEqual($rendered_field, implode(', ', $items), 'Items are limited to first and last.'); } $view->destroy(); @@ -277,6 +284,7 @@ public function _testMultipleFieldRender() { $view->displayHandlers->get('default')->options['fields'][$field_name]['group_rows'] = TRUE; $view->displayHandlers->get('default')->options['fields'][$field_name]['separator'] = ':'; $this->executeView($view); + $view->render(); for ($i = 0; $i < 3; $i++) { $rendered_field = $view->style_plugin->getField($i, $field_name); @@ -286,7 +294,7 @@ public function _testMultipleFieldRender() { foreach ($pure_items as $j => $item) { $items[] = $pure_items[$j]['value']; } - $this->assertEqual($rendered_field, implode(':', $items), 'Make sure that the amount of items is limited.'); + $this->assertEqual($rendered_field, implode(':', $items), 'The amount of items is limited and the custom separator is correct.'); } $view->destroy(); @@ -296,6 +304,7 @@ public function _testMultipleFieldRender() { $view->displayHandlers->get('default')->options['fields'][$field_name]['delta_limit'] = 3; $view->displayHandlers->get('default')->options['fields'][$field_name]['separator'] = '

test

'; $this->executeView($view); + $view->render(); for ($i = 0; $i < 3; $i++) { $rendered_field = $view->style_plugin->getField($i, $field_name); @@ -305,7 +314,7 @@ public function _testMultipleFieldRender() { foreach ($pure_items as $j => $item) { $items[] = $pure_items[$j]['value']; } - $this->assertEqual($rendered_field, implode('

test

', $items), 'Make sure that the amount of items is limited.'); + $this->assertEqual($rendered_field, implode('

test

', $items), 'The custom separator is correctly escaped.'); } $view->destroy(); } diff --git a/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php b/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php index 75623f4..91c8532 100644 --- a/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php +++ b/core/modules/file/src/Tests/Views/ExtensionViewsFieldTest.php @@ -72,6 +72,7 @@ public function testFileExtensionTarOption() { $view = Views::getView('file_extension_view'); $view->setDisplay(); $this->executeView($view); + $view->render(); // Test without the tar option. $this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png'); @@ -86,6 +87,7 @@ public function testFileExtensionTarOption() { $view->field['extension']->options['settings']['extension_detect_tar'] = TRUE; $this->executeView($view); + $view->render(); $this->assertEqual($view->field['extension']->advancedRender($view->result[0]), 'png'); $this->assertEqual($view->field['extension']->advancedRender($view->result[1]), 'tar'); diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module index f179ec8..824c571 100644 --- a/core/modules/rdf/rdf.module +++ b/core/modules/rdf/rdf.module @@ -216,6 +216,11 @@ function rdf_entity_prepare_view($entity_type, array $entities, array $displays) $field_mapping = $mapping->getPreparedFieldMapping($name); if ($field_mapping) { foreach ($entity->get($name) as $item) { + // @todo Initialize field item attributes, if they are missing. Remove + // this in https://www.drupal.org/node/2450477. + if (!isset($item->_attributes)) { + $item->_attributes = array(); + } $item->_attributes += rdf_rdfa_attributes($field_mapping, $item->toArray()); } } diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php index c8549e6..7d513eb 100644 --- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php @@ -383,10 +383,20 @@ protected function doTestEntityTranslationAPI($entity_type) { // Verify that changing the default translation flag causes an exception to // be thrown. - $message = 'The default translation flag cannot be changed.'; foreach ($entity->getTranslationLanguages() as $t_langcode => $language) { $translation = $entity->getTranslation($t_langcode); $default = $translation->isDefaultTranslation(); + + $message = 'The default translation flag can be reassigned the same value.'; + try { + $translation->{$default_langcode_key}->value = $default; + $this->pass($message); + } + catch (\LogicException $e) { + $this->fail($message); + } + + $message = 'The default translation flag cannot be changed.'; try { $translation->{$default_langcode_key}->value = !$default; $this->fail($message); @@ -394,6 +404,7 @@ protected function doTestEntityTranslationAPI($entity_type) { catch (\LogicException $e) { $this->pass($message); } + $this->assertEqual($translation->{$default_langcode_key}->value, $default); } diff --git a/core/modules/system/tests/modules/entity_test/entity_test.views.inc b/core/modules/system/tests/modules/entity_test/entity_test.views.inc new file mode 100644 index 0000000..d74af47 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/entity_test.views.inc @@ -0,0 +1,9 @@ +field['name']->options['link_to_user'] = TRUE; $view->field['name']->options['type'] = 'user_name'; $view->field['name']->init($view, $view->getDisplay('default')); + $view->field['name']->options['id'] = 'name'; $this->executeView($view); $anon_name = $this->config('user.settings')->get('anonymous'); diff --git a/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php b/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php index 8470efd..697ae8a 100644 --- a/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php +++ b/core/modules/views/src/Entity/Render/ConfigurableLanguageRenderer.php @@ -15,7 +15,7 @@ /** * Renders entities in a configured language. */ -class ConfigurableLanguageRenderer extends RendererBase { +class ConfigurableLanguageRenderer extends EntityTranslationRendererBase { /** * A specific language code for rendering if available. diff --git a/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php b/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php index bab75d8..dbd3a72 100644 --- a/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php +++ b/core/modules/views/src/Entity/Render/DefaultLanguageRenderer.php @@ -12,7 +12,7 @@ /** * Renders entities in their default language. */ -class DefaultLanguageRenderer extends RendererBase { +class DefaultLanguageRenderer extends EntityTranslationRendererBase { /** * {@inheritdoc} diff --git a/core/modules/views/src/Entity/Render/EntityFieldRenderer.php b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php new file mode 100644 index 0000000..7dc07b9 --- /dev/null +++ b/core/modules/views/src/Entity/Render/EntityFieldRenderer.php @@ -0,0 +1,272 @@ +entityManager = $entity_manager; + $this->formatterPluginManager = $formatter_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return $this->getEntityTranslationRenderer()->getCacheContexts(); + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeId() { + return $this->entityType->id(); + } + + /** + * {@inheritdoc} + */ + protected function getEntityManager() { + return $this->entityManager; + } + + /** + * {@inheritdoc} + */ + + protected function getLanguageManager() { + return $this->languageManager; + } + + /** + * {@inheritdoc} + */ + protected function getView() { + return $this->view; + } + + /** + * {@inheritdoc} + */ + public function query(QueryPluginBase $query, $relationship = NULL) { + $this->getEntityTranslationRenderer()->query($query, $relationship); + } + + /** + * Renders all fields of all rows using entity display objects. + * + * @param \Drupal\views\ResultRow[] $values + * An array of all ResultRow objects returned from the query. + * + * @see \Drupal\Core\Entity\Entity\EntityViewDisplay + */ + protected function buildFields(array $values) { + $build = []; + if ($renderable_fields = $this->getRenderableFieldIds()) { + // Identify all relationships. We will also need one representative field + // for each relationship. + $relationhips = []; + foreach ($renderable_fields as $field_id) { + /** @var \Drupal\views\Plugin\views\field\Field $field */ + $field = $this->view->field[$field_id]; + $relationhips[$field->options['relationship']] = $field; + } + + // For each row, and each relationship in the row, retrieve the entity to + // be processed, and take the correct translation. + $entities = []; + foreach ($values as $result_row) { + foreach ($relationhips as $relationhip_id => $field) { + $entity = $field->getEntity($result_row); + $entities[$relationhip_id][$entity->bundle()][$result_row->index] = $this->getEntityTranslation($entity, $result_row); + } + } + + // @todo Note : given the current code in EVD->buildFieldMultiple(), we don't + // actually need to create an EVD for each entity_type / bundle. This shows + // that EVD->buildFieldMultiple() should rather go in a stateless service, + // since it does in fact not depend on the values in the EVD. + $display = new EntityViewDisplay([ + 'targetEntityType' => $this->getEntityTypeId(), + 'bundle' => FALSE, + 'status' => TRUE, + ], 'entity_view_display'); + + // Render each field + foreach ($renderable_fields as $field_id) { + /** @var \Drupal\views\Plugin\views\field\Field $field */ + $field = $this->view->field[$field_id]; + + // Run the formatter across all entities for the field's relationship, + // grouping entities by bundle. + foreach ($entities[$field->options['relationship']] as $bundle => $bundle_entities) { + // At this point, we can use the "real" field definition for the bundle + // we are rendering. + $field_definition = $this->entityManager->getFieldDefinitions($this->getEntityTypeId(), $bundle)[$field->definition['field_name']]; + $field_build = $display->buildFieldMultiple($bundle_entities, $field_definition, $field->options['type'], $field->options['settings']); + array_walk($field_build, function ($value, $index) use (&$build, $field_id) { + $build[$index][$field_id] = $value; + }); + } + } + } + + return $build; + } + + /** + * Returns a list of names of entity fields to be rendered. + * + * @return string[] + * An associative array of views fields. + */ + protected function getRenderableFieldIds() { + if (!isset($this->renderableFieldIds)) { + $this->renderableFieldIds = []; + foreach ($this->view->field as $field_id => $field) { + if ($field instanceof Field && $field->getEntityType() == $this->getEntityTypeId()) { + $this->renderableFieldIds[] = $field_id; + } + } + } + return $this->renderableFieldIds; + } + + /** + * Return the code of the language the field should be displayed in. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object the field value being processed is attached to. + * @param \Drupal\views\ResultRow $row + * The result row the field value being processed belongs to. + * + * @return \Drupal\Core\Entity\FieldableEntityInterface + * The entity translation object for the passed row. + */ + protected function getEntityTranslation(EntityInterface $entity, ResultRow $row) { + // We assume the same language should be used for all entity fields + // belonging to a single row, even if they are attached to different entity + // types. Below we apply language fallback to ensure a valid value is always + // picked. + $langcode = $this->getEntityTranslationRenderer()->getLangcode($row); + return $this->entityManager->getTranslationFromContext($entity, $langcode); + } + + /** + * {@inheritdoc} + */ + public function preRender(array $result) { + // For efficient preRender() populated $this->build with the render arrays for all fields + // in all rows + $this->build = $this->buildFields($result); + } + + /** + * Renders entity field data. + * + * @param \Drupal\views\ResultRow $row + * A single row of the query result. + * @param \Drupal\views\Plugin\views\field\Field $field + * (optional) A field to be rendered. + * + * @return array + * A renderable array for the entity data contained in the result row. + */ + public function render(ResultRow $row, Field $field = NULL) { + if (isset($field)) { + $field_id = $field->options['id']; + // preRender() initializes $this->build with the render arrays for all + // fields across all rows. We simply pick the one for the row / field we + // are being asked to render, and remove it from $this->build to free + // memory as we progress. + if (isset($this->build[$row->index][$field_id])) { + $build = $this->build[$row->index][$field_id]; + unset($this->build[$row->index][$field_id]); + } + else { + // In the uncommon case where a field gets rendered several times + // (typically through direct Views API calls), the pre-computed render + // array was removed be the unset() above. We the manually rebuild the + // render array for the row. + $build = $this->buildFields([$row])[$row->index][$field_id]; + } + } + else { + // Same logic as above, in the case where we are being called for a whole + // row. + if (isset($this->build[$row->index])) { + $build = $this->build[$row->index]; + unset($this->build[$row->index]); + } + else { + $build = $this->buildFields([$row])[$row->index]; + } + } + + return $build; + } + +} diff --git a/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php b/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php index 1be7364..f874c75 100644 --- a/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php +++ b/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php @@ -10,25 +10,25 @@ use Drupal\views\Plugin\views\PluginBase; /** - * Trait used to instantiate the view's entity language render. + * Trait used to instantiate the view's entity translation renderer. */ trait EntityTranslationRenderTrait { /** * The renderer to be used to render the entity row. * - * @var \Drupal\views\Entity\Render\RendererBase + * @var \Drupal\views\Entity\Render\EntityTranslationRendererBase */ - protected $entityLanguageRenderer; + protected $entityTranslationRenderer; /** * Returns the current renderer. * - * @return \Drupal\views\Entity\Render\RendererBase + * @return \Drupal\views\Entity\Render\EntityTranslationRendererBase * The configured renderer. */ protected function getEntityTranslationRenderer() { - if (!isset($this->entityLanguageRenderer)) { + if (!isset($this->entityTranslationRenderer)) { $view = $this->getView(); $rendering_language = $view->display_handler->getOption('rendering_language'); $langcode = NULL; @@ -52,9 +52,9 @@ protected function getEntityTranslationRenderer() { } $class = '\Drupal\views\Entity\Render\\' . $renderer; $entity_type = $this->getEntityManager()->getDefinition($this->getEntityTypeId()); - $this->entityLanguageRenderer = new $class($view, $this->getLanguageManager(), $entity_type, $langcode); + $this->entityTranslationRenderer = new $class($view, $this->getLanguageManager(), $entity_type, $langcode); } - return $this->entityLanguageRenderer; + return $this->entityTranslationRenderer; } /** diff --git a/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php b/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php new file mode 100644 index 0000000..9ceb84e --- /dev/null +++ b/core/modules/views/src/Entity/Render/EntityTranslationRendererBase.php @@ -0,0 +1,64 @@ +view->rowPlugin->entityManager->getViewBuilder($this->entityType->id()); + + /** @var \Drupal\views\ResultRow $row */ + foreach ($result as $row) { + $entity = $row->_entity; + $entity->view = $this->view; + $this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row)); + } + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $row) { + $entity_id = $row->_entity->id(); + return $this->build[$entity_id]; + } + +} diff --git a/core/modules/views/src/Entity/Render/RendererBase.php b/core/modules/views/src/Entity/Render/RendererBase.php index a0baec6..9bbb7e7 100644 --- a/core/modules/views/src/Entity/Render/RendererBase.php +++ b/core/modules/views/src/Entity/Render/RendererBase.php @@ -8,7 +8,6 @@ namespace Drupal\views\Entity\Render; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\query\QueryPluginBase; @@ -16,7 +15,7 @@ use Drupal\views\ViewExecutable; /** - * Defines a base class for entity row renderers. + * Defines a base class for entity renderers. */ abstract class RendererBase implements CacheablePluginInterface { @@ -46,7 +45,7 @@ * * @var array */ - protected $build = array(); + protected $build; /** * Constructs a renderer object. @@ -72,24 +71,6 @@ public function isCacheable() { } /** - * {@inheritdoc} - */ - public function getCacheContexts() { - return []; - } - - /** - * Returns the language code associated to the given row. - * - * @param \Drupal\views\ResultRow $row - * The result row. - * - * @return string - * A language code. - */ - abstract public function getLangcode(ResultRow $row); - - /** * Alters the query if needed. * * @param \Drupal\views\Plugin\views\query\QueryPluginBase $query @@ -97,38 +78,26 @@ public function getCacheContexts() { * @param string $relationship * (optional) The relationship, used by a field. */ - public function query(QueryPluginBase $query, $relationship = NULL) { - } + abstract public function query(QueryPluginBase $query, $relationship = NULL); /** - * Runs before each row is rendered. + * Runs before each entity is rendered. * * @param $result * The full array of results from the query. */ public function preRender(array $result) { - $view_builder = $this->view->rowPlugin->entityManager->getViewBuilder($this->entityType->id()); - - /** @var \Drupal\views\ResultRow $row */ - foreach ($result as $row) { - $entity = $row->_entity; - $entity->view = $this->view; - $this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row)); - } } /** - * Renders a row object. + * Renders entity data. * * @param \Drupal\views\ResultRow $row * A single row of the query result. * * @return array - * The renderable array of a single row. + * A renderable array for the entity data contained in the result row. */ - public function render(ResultRow $row) { - $entity_id = $row->_entity->id(); - return $this->build[$entity_id]; - } + abstract public function render(ResultRow $row); } diff --git a/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php b/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php index cf0c296..05e5883 100644 --- a/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php +++ b/core/modules/views/src/Entity/Render/TranslationLanguageRenderer.php @@ -14,7 +14,7 @@ /** * Renders entity translations in their active language. */ -class TranslationLanguageRenderer extends RendererBase { +class TranslationLanguageRenderer extends EntityTranslationRendererBase { /** * Stores the field alias of the langcode column. diff --git a/core/modules/views/src/FieldAPIHandlerTrait.php b/core/modules/views/src/FieldAPIHandlerTrait.php index 8a7fd12..48530cc 100644 --- a/core/modules/views/src/FieldAPIHandlerTrait.php +++ b/core/modules/views/src/FieldAPIHandlerTrait.php @@ -41,7 +41,7 @@ * @return \Drupal\Core\Field\FieldDefinitionInterface * The field definition used by this handler. */ - protected function getFieldDefinition() { + public function getFieldDefinition() { if (!$this->fieldDefinition) { $field_storage_config = $this->getFieldStorageDefinition(); $this->fieldDefinition = BaseFieldDefinition::createFromFieldStorageDefinition($field_storage_config); diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php index 66dbdeb..14e1218 100644 --- a/core/modules/views/src/Plugin/views/field/Field.php +++ b/core/modules/views/src/Plugin/views/field/Field.php @@ -16,13 +16,12 @@ use Drupal\Core\Field\FormatterPluginManager; use Drupal\Core\Form\FormHelper; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\FieldAPIHandlerTrait; -use Drupal\views\Entity\Render\EntityTranslationRenderTrait; +use Drupal\views\Entity\Render\EntityFieldRenderer; use Drupal\views\Plugin\CacheablePluginInterface; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ResultRow; @@ -30,7 +29,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * A field that displays fieldapi fields. + * A field that displays entity field data. * * @ingroup views_field_handlers * @@ -39,8 +38,6 @@ * @ViewsField("field") */ class Field extends FieldPluginBase implements CacheablePluginInterface, MultiItemsFieldHandlerInterface { - use EntityTranslationRenderTrait; - use FieldAPIHandlerTrait; /** @@ -114,6 +111,13 @@ class Field extends FieldPluginBase implements CacheablePluginInterface, MultiIt protected $fieldTypePluginManager; /** + * Static cache for ::getEntityFieldRenderer(). + * + * @var \Drupal\views\Entity\Render\EntityFieldRenderer + */ + protected $entityFieldRenderer; + + /** * Constructs a \Drupal\field\Plugin\views\field\Field object. * * @param array $configuration @@ -201,13 +205,6 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o /** * {@inheritdoc} */ - public function getEntityTypeId() { - return $this->getEntityType(); - } - - /** - * {@inheritdoc} - */ protected function getEntityManager() { return $this->entityManager; } @@ -215,19 +212,6 @@ protected function getEntityManager() { /** * {@inheritdoc} */ - protected function getLanguageManager() { - return $this->languageManager; - } - /** - * {@inheritdoc} - */ - protected function getView() { - return $this->view; - } - - /** - * {@inheritdoc} - */ public function access(AccountInterface $account) { $access_control_handler = $this->entityManager->getAccessControlHandler($this->getEntityType()); return $access_control_handler->fieldAccess('view', $this->getFieldDefinition(), $account); @@ -271,8 +255,8 @@ public function query($use_groupby = FALSE) { $this->addAdditionalFields($fields); } - // Let the configured entity translation renderer alter the query if needed. - $this->getEntityTranslationRenderer()->query($this->query, $this->relationship); + // Let the entity field renderer alter the query if needed. + $this->getEntityFieldRenderer()->query($this->query, $this->relationship); } /** @@ -685,6 +669,7 @@ public function submitGroupByForm(&$form, FormStateInterface $form_state) { */ public function renderItems($items) { if (!empty($items)) { + $items = $this->prepareItemsByDelta($items); if ($this->options['multi_type'] == 'separator' || !$this->options['group_rows']) { $separator = $this->options['multi_type'] == 'separator' ? SafeMarkup::checkAdminXss($this->options['separator']) : ''; $build = [ @@ -706,6 +691,107 @@ public function renderItems($items) { } /** + * Adapts the $items according to the delta configuration. + * + * The method filters limits the shown deltas, reorders the items as well as + * takens into account an offset. + * + * @param array $all_values + * The items for individual rendering. + * + * @return array + * The manipulated items. + */ + protected function prepareItemsByDelta(array $all_values) { + if ($this->options['delta_reversed']) { + $all_values = array_reverse($all_values); + } + + // We are supposed to show only certain deltas. + if ($this->limit_values) { + $row = $this->view->result[$this->view->row_index]; + + // Offset is calculated differently when row grouping for a field is + // not enabled. Since there are multiple rows, the delta needs to be + // taken into account, so that different values are shown per row. + if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($row->{$this->aliases['delta']})) { + $delta_limit = 1; + $offset = $row->{$this->aliases['delta']}; + } + // Single fields don't have a delta available so choose 0. + elseif (!$this->options['group_rows'] && !$this->multiple) { + $delta_limit = 1; + $offset = 0; + } + else { + $delta_limit = $this->options['delta_limit']; + $offset = intval($this->options['delta_offset']); + + // We should only get here in this case if there's an offset, and + // in that case we're limiting to all values after the offset. + if ($delta_limit === 0) { + $delta_limit = count($all_values) - $offset; + } + } + + // Determine if only the first and last values should be shown + $delta_first_last = $this->options['delta_first_last']; + + $new_values = array(); + for ($i = 0; $i < $delta_limit; $i++) { + $new_delta = $offset + $i; + + if (isset($all_values[$new_delta])) { + // If first-last option was selected, only use the first and last values + if (!$delta_first_last + // Use the first value. + || $new_delta == $offset + // Use the last value. + || $new_delta == ($delta_limit + $offset - 1)) { + $new_values[] = $all_values[$new_delta]; + } + } + } + $all_values = $new_values; + } + + return $all_values; + } + + /** + * {@inheritdoc} + */ + public function preRender(&$values) { + parent::preRender($values); + $this->getEntityFieldRenderer()->preRender($values); + } + + /** + * Returns the entity field renderer. + * + * @return \Drupal\views\Entity\Render\EntityFieldRenderer + * The entity field renderer. + */ + protected function getEntityFieldRenderer() { + $entity_type_id = $this->getEntityType(); + if (!isset($this->entityFieldRenderer[$entity_type_id])) { + if (!empty($this->view->field)) { + foreach ($this->view->field as $field) { + if ($field instanceof Field && isset($field->entityFieldRenderer[$entity_type_id])) { + $this->entityFieldRenderer[$entity_type_id] = $field->entityFieldRenderer[$entity_type_id]; + break; + } + } + } + if (!isset($this->entityFieldRenderer[$entity_type_id])) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + $this->entityFieldRenderer[$entity_type_id] = new EntityFieldRenderer($this->view, $this->languageManager, $entity_type, $this->entityManager, $this->formatterPluginManager); + } + } + return $this->entityFieldRenderer[$entity_type_id]; + } + + /** * Gets an array of items for the field. * * @param \Drupal\views\ResultRow $values @@ -715,41 +801,31 @@ public function renderItems($items) { * An array of items for the field. */ public function getItems(ResultRow $values) { - $original_entity = $this->getEntity($values); - if (!$original_entity) { - return array(); - } - $entity = $this->process_entity($values, $original_entity); - if (!$entity) { - return array(); - } + $build = $this->getEntityFieldRenderer()->render($values, $this); - $display = array( - 'type' => $this->options['type'], - 'settings' => $this->options['settings'], - 'label' => 'hidden', - ); - $render_array = $entity->get($this->definition['field_name'])->view($display); - - $items = array(); + if (!$build) { + return []; + } if ($this->options['field_api_classes']) { - return array(array('rendered' => $this->renderer->render($render_array))); + return [['rendered' => $this->renderer->render($build)]]; } - foreach (Element::children($render_array) as $count) { - $items[$count]['rendered'] = $render_array[$count]; - // FieldItemListInterface::view() adds an #access property to the render - // array that determines whether or not the current user is allowed to - // view the field in the context of the current entity. We need to respect - // this parameter when we pull out the children of the field array for + // Render using the formatted data itself. + $items = []; + foreach (Element::children($build) as $delta) { + $items[$delta]['rendered'] = $build[$delta]; + // EntityViewDisplay::build() adds an #access property to the render array + // that determines whether or not the current user is allowed to view the + // field in the context of the current entity. We need to respect this + // parameter when we pull out the children of the field array for // rendering. - if (isset($render_array['#access'])) { - $items[$count]['rendered']['#access'] = $render_array['#access']; + if (isset($build['#access'])) { + $items[$delta]['rendered']['#access'] = $build['#access']; } // Only add the raw field items (for use in tokens) if the current user // has access to view the field content. - if ((!isset($items[$count]['rendered']['#access']) || $items[$count]['rendered']['#access']) && !empty($render_array['#items'][$count])) { - $items[$count]['raw'] = $render_array['#items'][$count]; + if ((!isset($items[$delta]['rendered']['#access']) || $items[$delta]['rendered']['#access']) && !empty($build['#items'][$delta])) { + $items[$delta]['raw'] = $build['#items'][$delta]; } } return $items; @@ -765,15 +841,15 @@ public function getItems(ResultRow $values) { * The result row object containing the values. * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be processed. + * @param bool $clone + * (optional) Whether a clone of the specified entity should processed. + * Defaults to TRUE. * - * @return - * TRUE if the processing completed successfully, otherwise FALSE. + * @return bool|\Drupal\Core\Entity\FieldableEntityInterface + * Returns the processed entity if successful, else returns FALSE. */ - function process_entity(ResultRow $values, EntityInterface $entity) { - $processed_entity = clone $entity; - - $langcode = $this->getFieldLangcode($processed_entity, $values); - $processed_entity = $processed_entity->getTranslation($langcode); + public function processEntity(ResultRow $values, EntityInterface $entity, $clone = TRUE) { + $processed_entity = $clone ? clone $entity : $entity; // If we are grouping, copy our group fields into the cloned entity. // It's possible this will cause some weirdness, but there's only @@ -811,57 +887,6 @@ function process_entity(ResultRow $values, EntityInterface $entity) { return FALSE; } - // We are supposed to show only certain deltas. - if ($this->limit_values && !empty($processed_entity->{$this->definition['field_name']})) { - $all_values = !empty($processed_entity->{$this->definition['field_name']}) ? $processed_entity->{$this->definition['field_name']}->getValue() : array(); - if ($this->options['delta_reversed']) { - $all_values = array_reverse($all_values); - } - - // Offset is calculated differently when row grouping for a field is - // not enabled. Since there are multiple rows, the delta needs to be - // taken into account, so that different values are shown per row. - if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($values->{$this->aliases['delta']})) { - $delta_limit = 1; - $offset = $values->{$this->aliases['delta']}; - } - // Single fields don't have a delta available so choose 0. - elseif (!$this->options['group_rows'] && !$this->multiple) { - $delta_limit = 1; - $offset = 0; - } - else { - $delta_limit = $this->options['delta_limit']; - $offset = intval($this->options['delta_offset']); - - // We should only get here in this case if there's an offset, and - // in that case we're limiting to all values after the offset. - if ($delta_limit === 0) { - $delta_limit = count($all_values) - $offset; - } - } - - // Determine if only the first and last values should be shown - $delta_first_last = $this->options['delta_first_last']; - - $new_values = array(); - for ($i = 0; $i < $delta_limit; $i++) { - $new_delta = $offset + $i; - - if (isset($all_values[$new_delta])) { - // If first-last option was selected, only use the first and last values - if (!$delta_first_last - // Use the first value. - || $new_delta == $offset - // Use the last value. - || $new_delta == ($delta_limit + $offset - 1)) { - $new_values[] = $all_values[$new_delta]; - } - } - } - $processed_entity->{$this->definition['field_name']} = $new_values; - } - return $processed_entity; } @@ -900,39 +925,6 @@ protected function addSelfTokens(&$tokens, $item) { } /** - * Return the code of the language the field should be displayed in. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object the field value being processed is attached to. - * @param \Drupal\views\ResultRow $row - * The result row the field value being processed belongs to. - * - * @return string - * The field language code. - */ - protected function getFieldLangcode(EntityInterface $entity, ResultRow $row) { - if ($this->getFieldDefinition()->isTranslatable()) { - // Even if the current field is not attached to the main entity, we use it - // to determine the field language, as we assume the same language should - // be used for all values belonging to a single row, when possible. Below - // we apply language fallback to ensure a valid value is always picked. - $langcode = $this->getEntityTranslationRenderer()->getLangcode($row); - - // Give the Entity Field API a chance to fallback to a different language - // (or LanguageInterface::LANGCODE_NOT_SPECIFIED), in case the field has - // no data for the selected language. FieldItemListInterface::view() does - // this as well, but since the returned language code is used before - // calling it, the fallback needs to happen explicitly. - $langcode = $this->entityManager->getTranslationFromContext($entity, $langcode)->language()->getId(); - - return $langcode; - } - else { - return LanguageInterface::LANGCODE_NOT_SPECIFIED; - } - } - - /** * {@inheritdoc} */ public function calculateDependencies() { @@ -961,9 +953,7 @@ public function isCacheable() { * {@inheritdoc} */ public function getCacheContexts() { - $contexts = $this->getEntityTranslationRenderer()->getCacheContexts(); - - return $contexts; + return $this->getEntityFieldRenderer()->getCacheContexts(); } /** @@ -980,11 +970,19 @@ protected function getTableMapping() { * {@inheritdoc} */ public function getValue(ResultRow $values, $field = NULL) { - if ($field === NULL) { - return $this->getEntity($values)->{$this->definition['field_name']}->value; + /** @var \Drupal\Core\Field\FieldItemListInterface $field_item_list */ + $field_item_list = $this->getEntity($values)->{$this->definition['field_name']}; + $field_item_definition = $field_item_list->getFieldDefinition(); + + if ($field_item_definition->getFieldStorageDefinition()->getCardinality() == 1) { + return $field ? $field_item_list->$field : $field_item_list->value; } - return $this->getEntity($values)->{$this->definition['field_name']}->$field; + $values = []; + foreach ($field_item_list as $field_item) { + $values[] = $field ? $field_item->$field : $field_item->value; + } + return $values; } } diff --git a/core/modules/views/src/Tests/Handler/FieldFieldTest.php b/core/modules/views/src/Tests/Handler/FieldFieldTest.php index 4982cdd..0496ef4 100644 --- a/core/modules/views/src/Tests/Handler/FieldFieldTest.php +++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php @@ -7,10 +7,12 @@ namespace Drupal\views\Tests\Handler; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\entity_test\Entity\EntityTest; use Drupal\entity_test\Entity\EntityTestRev; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\user\Entity\User; use Drupal\views\Plugin\views\field\Field; use Drupal\views\Tests\ViewUnitTestBase; use Drupal\views\Views; @@ -31,7 +33,7 @@ class FieldFieldTest extends ViewUnitTestBase { /** * {@inheritdoc} */ - public static $testViews = ['test_field_field_test', 'test_field_field_revision_test']; + public static $testViews = ['test_field_field_test', 'test_field_alias_test', 'test_field_field_complex_test', 'test_field_field_revision_test', 'test_field_field_revision_complex_test']; /** * The stored test entities. @@ -48,14 +50,45 @@ class FieldFieldTest extends ViewUnitTestBase { protected $entityRevision; /** + * Stores a couple of test users. + * + * @var \Drupal\user\UserInterface[] + */ + protected $testUsers; + + /** + * The admin user. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); $this->installEntitySchema('entity_test_rev'); + // Bypass any field access. + $this->adminUser = User::create([ + 'admin_role' => true, + ]); + $this->adminUser->save(); + $this->container->get('current_user')->setAccount($this->adminUser); + + $this->testUsers = []; + for ($i = 0; $i < 5; $i++) { + $this->testUsers[$i] = User::create([ + 'name' => 'test ' . $i, + 'timezone' => User::getAllowedTimezones()[$i], + ]); + $this->testUsers[$i]->save(); + } + // Setup a field storage and field, but also change the views data for the // entity_test entity type. $field_storage = FieldStorageConfig::create([ @@ -72,11 +105,30 @@ protected function setUp() { ]); $field->save(); + $field_storage_multiple = FieldStorageConfig::create([ + 'field_name' => 'field_test_multiple', + 'type' => 'integer', + 'entity_type' => 'entity_test', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ]); + $field_storage_multiple->save(); + + $field_multiple = FieldConfig::create([ + 'field_name' => 'field_test_multiple', + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ]); + $field_multiple->save(); + $random_number = (string) 30856; + $random_number_multiple = (string) 1370359990; for ($i = 0; $i < 5; $i++) { $this->entities[$i] = $entity = EntityTest::create([ 'bundle' => 'entity_test', + 'name' => 'test ' . $i, 'field_test' => $random_number[$i], + 'field_test_multiple' => [$random_number_multiple[$i * 2], $random_number_multiple[$i * 2 + 1]], + 'user_id' => $this->testUsers[$i]->id(), ]); $entity->save(); } @@ -97,30 +149,58 @@ protected function setUp() { ]); $field->save(); + $field_storage_multiple = FieldStorageConfig::create([ + 'field_name' => 'field_test_multiple', + 'type' => 'integer', + 'entity_type' => 'entity_test_rev', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ]); + $field_storage_multiple->save(); + + $field_multiple = FieldConfig::create([ + 'field_name' => 'field_test_multiple', + 'entity_type' => 'entity_test_rev', + 'bundle' => 'entity_test_rev', + ]); + $field_multiple->save(); + $this->entityRevision = []; $this->entityRevision[0] = $entity = EntityTestRev::create([ 'name' => 'base value', 'field_test' => 1, + 'field_test_multiple' => [1, 3, 7], + 'user_id' => $this->testUsers[0]->id(), ]); $entity->save(); + $original_entity = clone $entity; - $entity = clone $entity; + $entity = clone $original_entity; $entity->setNewRevision(TRUE); $entity->name->value = 'revision value1'; $entity->field_test->value = 2; + $entity->field_test_multiple[0]->value = 0; + $entity->field_test_multiple[1]->value = 3; + $entity->field_test_multiple[2]->value = 5; + $entity->user_id->target_id = $this->testUsers[1]->id(); $entity->save(); $this->entityRevision[1] = $entity; - $entity = clone $entity; + $entity = clone $original_entity; $entity->setNewRevision(TRUE); $entity->name->value = 'revision value2'; $entity->field_test->value = 3; + $entity->field_test_multiple[0]->value = 9; + $entity->field_test_multiple[1]->value = 9; + $entity->field_test_multiple[2]->value = 9; + $entity->user_id->target_id = $this->testUsers[2]->id(); $entity->save(); $this->entityRevision[2] = $entity; - $this->entityRevision[0] = $entity = EntityTestRev::create([ + $this->entityRevision[3] = $entity = EntityTestRev::create([ 'name' => 'next entity value', 'field_test' => 4, + 'field_test_multiple' => [2, 9, 9], + 'user_id' => $this->testUsers[3]->id(), ]); $entity->save(); @@ -171,7 +251,7 @@ public function testSimpleExecute() { */ public function testSimpleRender() { $executable = Views::getView('test_field_field_test'); - $executable->execute(); + $executable->render(); $this->assertEqual(1, $executable->getStyle()->getField(0, 'id')); $this->assertEqual(3, $executable->getStyle()->getField(0, 'field_test')); @@ -186,6 +266,114 @@ public function testSimpleRender() { } /** + * Tests the result of a view with complex field configuration. + * + * A complex field configuration contains multiple times the same field, with + * different delta limit / offset. + */ + public function testFieldAlias() { + $executable = Views::getView('test_field_alias_test'); + $executable->execute(); + + $this->assertTrue($executable->field['id'] instanceof Field); + $this->assertTrue($executable->field['name'] instanceof Field); + $this->assertTrue($executable->field['name_alias'] instanceof Field); + + $this->assertIdenticalResultset($executable, + [ + ['id' => 1, 'name' => 'test 0', 'name_alias' => 'test 0'], + ['id' => 2, 'name' => 'test 1', 'name_alias' => 'test 1'], + ['id' => 3, 'name' => 'test 2', 'name_alias' => 'test 2'], + ['id' => 4, 'name' => 'test 3', 'name_alias' => 'test 3'], + ['id' => 5, 'name' => 'test 4', 'name_alias' => 'test 4'], + ], + ['id' => 'id', 'name' => 'name', 'name_alias' => 'name_alias'] + ); + } + + /** + * Tests the result of a view with complex field configuration. + * + * A complex field configuration contains multiple times the same field, with + * different delta limit / offset. + */ + public function testFieldAliasRender() { + $executable = Views::getView('test_field_alias_test'); + $executable->render(); + + for ($i = 0; $i < 5; $i++) { + $this->assertEqual($i + 1, $executable->getStyle()->getField($i, 'id')); + $this->assertEqual('test ' . $i, $executable->getStyle()->getField($i, 'name')); + $this->assertEqual('test ' . $i . '', $executable->getStyle()->getField($i, 'name_alias')); + } + } + + /** + * Tests the result of a view with complex field configuration. + * + * A complex field configuration contains multiple times the same field, with + * different delta limit / offset. + */ + public function testComplexExecute() { + $executable = Views::getView('test_field_field_complex_test'); + $executable->execute(); + + $timezones = []; + foreach ($this->testUsers as $user) { + $timezones[] = $user->getTimeZone(); + } + + $this->assertTrue($executable->field['field_test_multiple'] instanceof Field); + $this->assertTrue($executable->field['field_test_multiple_1'] instanceof Field); + $this->assertTrue($executable->field['field_test_multiple_2'] instanceof Field); + $this->assertTrue($executable->field['timezone'] instanceof Field); + + $this->assertIdenticalResultset($executable, + [ + ['timezone' => $timezones[0], 'field_test_multiple' => [1, 3], 'field_test_multiple_1' => [1, 3], 'field_test_multiple_2' => [1, 3]], + ['timezone' => $timezones[1], 'field_test_multiple' => [7, 0], 'field_test_multiple_1' => [7, 0], 'field_test_multiple_2' => [7, 0]], + ['timezone' => $timezones[2], 'field_test_multiple' => [3, 5], 'field_test_multiple_1' => [3, 5], 'field_test_multiple_2' => [3, 5]], + ['timezone' => $timezones[3], 'field_test_multiple' => [9, 9], 'field_test_multiple_1' => [9, 9], 'field_test_multiple_2' => [9, 9]], + ['timezone' => $timezones[4], 'field_test_multiple' => [9, 0], 'field_test_multiple_1' => [9, 0], 'field_test_multiple_2' => [9, 0]], + ], + ['timezone' => 'timezone', 'field_test_multiple' => 'field_test_multiple', 'field_test_multiple_1' => 'field_test_multiple_1', 'field_test_multiple_2' => 'field_test_multiple_2'] + ); + } + + /** + * Tests the output of a view with complex field configuration. + */ + public function testComplexRender() { + $executable = Views::getView('test_field_field_complex_test'); + $executable->render(); + + $this->assertEqual($this->testUsers[0]->getTimeZone(), $executable->getStyle()->getField(0, 'timezone')); + $this->assertEqual("1, 3", $executable->getStyle()->getField(0, 'field_test_multiple')); + $this->assertEqual("1", $executable->getStyle()->getField(0, 'field_test_multiple_1')); + $this->assertEqual("3", $executable->getStyle()->getField(0, 'field_test_multiple_2')); + + $this->assertEqual($this->testUsers[1]->getTimeZone(), $executable->getStyle()->getField(1, 'timezone')); + $this->assertEqual("7, 0", $executable->getStyle()->getField(1, 'field_test_multiple')); + $this->assertEqual("7", $executable->getStyle()->getField(1, 'field_test_multiple_1')); + $this->assertEqual("0", $executable->getStyle()->getField(1, 'field_test_multiple_2')); + + $this->assertEqual($this->testUsers[2]->getTimeZone(), $executable->getStyle()->getField(2, 'timezone')); + $this->assertEqual("3, 5", $executable->getStyle()->getField(2, 'field_test_multiple')); + $this->assertEqual("3", $executable->getStyle()->getField(2, 'field_test_multiple_1')); + $this->assertEqual("5", $executable->getStyle()->getField(2, 'field_test_multiple_2')); + + $this->assertEqual($this->testUsers[3]->getTimeZone(), $executable->getStyle()->getField(3, 'timezone')); + $this->assertEqual("9, 9", $executable->getStyle()->getField(3, 'field_test_multiple')); + $this->assertEqual("9", $executable->getStyle()->getField(3, 'field_test_multiple_1')); + $this->assertEqual("9", $executable->getStyle()->getField(3, 'field_test_multiple_2')); + + $this->assertEqual($this->testUsers[4]->getTimeZone(), $executable->getStyle()->getField(4, 'timezone')); + $this->assertEqual("9, 0", $executable->getStyle()->getField(4, 'field_test_multiple')); + $this->assertEqual("9", $executable->getStyle()->getField(4, 'field_test_multiple_1')); + $this->assertEqual("0", $executable->getStyle()->getField(4, 'field_test_multiple_2')); + } + + /** * Tests the revision result. */ public function testRevisionExecute() { @@ -207,11 +395,11 @@ public function testRevisionExecute() { } /** - * Tests the output of a revision view with base fields and configurable fields. + * Tests the output of a revision view with base and configurable fields. */ public function testRevisionRender() { $executable = Views::getView('test_field_field_revision_test'); - $executable->execute(); + $executable->render(); $this->assertEqual(1, $executable->getStyle()->getField(0, 'id')); $this->assertEqual(1, $executable->getStyle()->getField(0, 'revision_id')); @@ -232,7 +420,72 @@ public function testRevisionRender() { $this->assertEqual(4, $executable->getStyle()->getField(3, 'revision_id')); $this->assertEqual(4, $executable->getStyle()->getField(3, 'field_test')); $this->assertEqual('next entity value', $executable->getStyle()->getField(3, 'name')); + } + /** + * Tests the revision result. + */ + public function testRevisionComplexExecute() { + $executable = Views::getView('test_field_field_revision_complex_test'); + $executable->execute(); + + $timezones = []; + foreach ($this->testUsers as $user) { + $timezones[] = $user->getTimeZone(); + } + + $this->assertTrue($executable->field['id'] instanceof Field); + $this->assertTrue($executable->field['revision_id'] instanceof Field); + $this->assertTrue($executable->field['timezone'] instanceof Field); + $this->assertTrue($executable->field['field_test_multiple'] instanceof Field); + $this->assertTrue($executable->field['field_test_multiple_1'] instanceof Field); + $this->assertTrue($executable->field['field_test_multiple_2'] instanceof Field); + + $this->assertIdenticalResultset($executable, + [ + ['id' => 1, 'field_test' => 1, 'revision_id' => 1, 'uid' => $this->testUsers[0]->id(), 'timezone' => $timezones[0], 'field_test_multiple' => [1, 3, 7], 'field_test_multiple_1' => [1, 3, 7], 'field_test_multiple_2' => [1, 3, 7]], + ['id' => 1, 'field_test' => 2, 'revision_id' => 2, 'uid' => $this->testUsers[1]->id(), 'timezone' => $timezones[1], 'field_test_multiple' => [0, 3, 5], 'field_test_multiple_1' => [0, 3, 5], 'field_test_multiple_2' => [0, 3, 5]], + ['id' => 1, 'field_test' => 3, 'revision_id' => 3, 'uid' => $this->testUsers[2]->id(), 'timezone' => $timezones[2], 'field_test_multiple' => [9, 9, 9], 'field_test_multiple_1' => [9, 9, 9], 'field_test_multiple_2' => [9, 9, 9]], + ['id' => 2, 'field_test' => 4, 'revision_id' => 4, 'uid' => $this->testUsers[3]->id(), 'timezone' => $timezones[3], 'field_test_multiple' => [2, 9, 9], 'field_test_multiple_1' => [2, 9, 9], 'field_test_multiple_2' => [2, 9, 9]], + ], + ['entity_test_rev_revision_id' => 'id', 'revision_id' => 'revision_id', 'users_field_data_entity_test_rev_revision_uid' => 'uid', 'timezone' => 'timezone', 'field_test_multiple' => 'field_test_multiple', 'field_test_multiple_1' => 'field_test_multiple_1', 'field_test_multiple_2' => 'field_test_multiple_2'] + ); + } + + /** + * Tests the output of a revision view with base fields and configurable fields. + */ + public function testRevisionComplexRender() { + $executable = Views::getView('test_field_field_revision_complex_test'); + $executable->render(); + + $this->assertEqual(1, $executable->getStyle()->getField(0, 'id')); + $this->assertEqual(1, $executable->getStyle()->getField(0, 'revision_id')); + $this->assertEqual($this->testUsers[0]->getTimeZone(), $executable->getStyle()->getField(0, 'timezone')); + $this->assertEqual('1, 3, 7', $executable->getStyle()->getField(0, 'field_test_multiple')); + $this->assertEqual('1', $executable->getStyle()->getField(0, 'field_test_multiple_1')); + $this->assertEqual('3, 7', $executable->getStyle()->getField(0, 'field_test_multiple_2')); + + $this->assertEqual(1, $executable->getStyle()->getField(1, 'id')); + $this->assertEqual(2, $executable->getStyle()->getField(1, 'revision_id')); + $this->assertEqual($this->testUsers[1]->getTimeZone(), $executable->getStyle()->getField(1, 'timezone')); + $this->assertEqual('0, 3, 5', $executable->getStyle()->getField(1, 'field_test_multiple')); + $this->assertEqual('0', $executable->getStyle()->getField(1, 'field_test_multiple_1')); + $this->assertEqual('3, 5', $executable->getStyle()->getField(1, 'field_test_multiple_2')); + + $this->assertEqual(1, $executable->getStyle()->getField(2, 'id')); + $this->assertEqual(3, $executable->getStyle()->getField(2, 'revision_id')); + $this->assertEqual($this->testUsers[2]->getTimeZone(), $executable->getStyle()->getField(2, 'timezone')); + $this->assertEqual('9, 9, 9', $executable->getStyle()->getField(2, 'field_test_multiple')); + $this->assertEqual('9', $executable->getStyle()->getField(2, 'field_test_multiple_1')); + $this->assertEqual('9, 9', $executable->getStyle()->getField(2, 'field_test_multiple_2')); + + $this->assertEqual(2, $executable->getStyle()->getField(3, 'id')); + $this->assertEqual(4, $executable->getStyle()->getField(3, 'revision_id')); + $this->assertEqual($this->testUsers[3]->getTimeZone(), $executable->getStyle()->getField(3, 'timezone')); + $this->assertEqual('2, 9, 9', $executable->getStyle()->getField(3, 'field_test_multiple')); + $this->assertEqual('2', $executable->getStyle()->getField(3, 'field_test_multiple_1')); + $this->assertEqual('9, 9', $executable->getStyle()->getField(3, 'field_test_multiple_2')); } } diff --git a/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php b/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php index a5f18d2..9a6a6cc 100644 --- a/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php +++ b/core/modules/views/src/Tests/Handler/FieldGroupRowsTest.php @@ -77,6 +77,7 @@ public function testGroupRows() { // Test grouped rows. $this->executeView($view); + $view->render(); $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a, b, c'); // Change the group_rows checkbox to false. @@ -85,8 +86,13 @@ public function testGroupRows() { // Test ungrouped rows. $this->executeView($view); + $view->render(); + + $view->row_index = 0; $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a'); + $view->row_index = 1; $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[1]), 'b'); + $view->row_index = 2; $this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[2]), 'c'); } diff --git a/core/modules/views/src/Tests/ViewResultAssertionTrait.php b/core/modules/views/src/Tests/ViewResultAssertionTrait.php index b8ad34f..f88c8c5 100644 --- a/core/modules/views/src/Tests/ViewResultAssertionTrait.php +++ b/core/modules/views/src/Tests/ViewResultAssertionTrait.php @@ -113,7 +113,18 @@ protected function assertIdenticalResultsetHelper($view, $expected_result, $colu $row = array(); foreach ($column_map as $expected_column) { // The comparison will be done on the string representation of the value. - $row[$expected_column] = (string) (is_object($value) ? $value->$expected_column : $value[$expected_column]); + if (is_object($value)) { + $row[$expected_column] = (string) $value->$expected_column; + } + // This case is about fields with multiple values. + elseif (is_array($value[$expected_column])) { + foreach (array_keys($value[$expected_column]) as $delta) { + $row[$expected_column][$delta] = (string) $value[$expected_column][$delta]; + } + } + else { + $row[$expected_column] = (string) $value[$expected_column]; + } } $expected_result[$key] = $row; } @@ -140,4 +151,3 @@ protected function assertIdenticalResultsetHelper($view, $expected_result, $colu } } - diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_alias_test.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_alias_test.yml new file mode 100644 index 0000000..6e178cc --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_alias_test.yml @@ -0,0 +1,64 @@ +langcode: und +status: true +dependencies: { } +id: test_field_alias_test +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: none + fields: + id: + id: id + table: entity_test + field: id + plugin_id: field + entity_type: entity_test + entity_field: id + name: + id: name + table: entity_test + field: name + plugin_id: field + entity_type: entity_test + entity_field: name + type: string + settings: + link_to_entity: false + name_alias: + id: name_alias + table: entity_test + field: name_alias + plugin_id: field + entity_type: entity_test + entity_field: name + type: string + settings: + link_to_entity: true + relationships: + user_id: + table: entity_test + field: user_id + id: user_id + plugin_id: standard + sorts: + id: + id: id + table: entity_test + field: id + plugin_id: standard + order: asc + style: + type: html_list + display_plugin: default + display_title: Master + id: default + position: 0 diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_complex_test.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_complex_test.yml new file mode 100644 index 0000000..58ee430 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_complex_test.yml @@ -0,0 +1,79 @@ +langcode: und +status: true +dependencies: { } +id: test_field_field_complex_test +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: none + fields: + id: + id: id + table: entity_test + field: id + plugin_id: field + entity_type: entity_test + entity_field: id + field_test_multiple: + id: field_test_multiple + table: entity_test__field_test_multiple + field: field_test_multiple + plugin_id: field + entity_type: entity_test + entity_field: field_test_multiple + delta_limit: 0 + group_rows: true + field_test_multiple_1: + id: field_test_multiple_1 + table: entity_test__field_test_multiple + field: field_test_multiple + plugin_id: field + entity_type: entity_test + entity_field: field_test_multiple + delta_limit: 1 + group_rows: true + field_test_multiple_2: + id: field_test_multiple_2 + table: entity_test__field_test_multiple + field: field_test_multiple + plugin_id: field + entity_type: entity_test + entity_field: field_test_multiple + delta_limit: 0 + delta_offset: 1 + group_rows: true + timezone: + id: timezone + table: users_field_data + field: timezone + plugin_id: field + relationship: user_id + alter: {} + relationships: + user_id: + table: entity_test + field: user_id + id: user_id + plugin_id: standard + sorts: + id: + id: id + table: entity_test + field: id + plugin_id: standard + order: asc + style: + type: html_list + display_plugin: default + display_title: Master + id: default + position: 0 diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_complex_test.yml similarity index 50% copy from core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml copy to core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_complex_test.yml index 5a49a08..b0122a3 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_complex_test.yml @@ -1,7 +1,7 @@ langcode: und status: true dependencies: { } -id: test_field_field_revision_test +id: test_field_field_revision_complex_test module: views description: '' tag: '' @@ -30,20 +30,47 @@ display: plugin_id: field entity_type: entity_test_rev entity_field: revision_id - field_test: - id: field_test - table: entity_test__field_test - field: field_test + field_test_multiple: + id: field_test_multiple + table: entity_test_rev__field_test_multiple + field: field_test_multiple plugin_id: field entity_type: entity_test_rev - entity_field: field_test - name: - id: name + entity_field: field_test_multiple + delta_limit: 0 + group_rows: true + field_test_multiple_1: + id: field_test_multiple_1 + table: entity_test_rev__field_test_multiple + field: field_test_multiple + plugin_id: field + entity_type: entity_test_rev + entity_field: field_test_multiple + delta_limit: 1 + group_rows: true + field_test_multiple_2: + id: field_test_multiple_2 + table: entity_test_rev__field_test_multiple + field: field_test_multiple + plugin_id: field + entity_type: entity_test_rev + entity_field: field_test_multiple + delta_limit: 0 + delta_offset: 1 + group_rows: true + timezone: + id: timezone + table: users_field_data + field: timezone + plugin_id: field + relationship: user_id + alter: {} + relationships: + user_id: table: entity_test_rev_revision - field: name - plugin_id: field - entity_type: entity_test_rev - entity_field: name + field: user_id + id: user_id + plugin_id: standard sorts: revision_id: id: revision_id diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml index 5a49a08..d85d4d1 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_field_revision_test.yml @@ -32,7 +32,7 @@ display: entity_field: revision_id field_test: id: field_test - table: entity_test__field_test + table: entity_test_rev__field_test field: field_test plugin_id: field entity_type: entity_test_rev diff --git a/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php b/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php index 9a04103..0e45537 100644 --- a/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/field/FieldPluginBaseTest.php @@ -492,12 +492,12 @@ public function providerTestRenderAsLinkWithPathAndTokens() { /** * Sets up a test field. * - * @return \Drupal\Tests\views\Unit\Plugin\field\TestField|\PHPUnit_Framework_MockObject_MockObject + * @return \Drupal\Tests\views\Unit\Plugin\field\FieldPluginBaseTestField|\PHPUnit_Framework_MockObject_MockObject * The test field. */ protected function setupTestField(array $options = []) { - /** @var \Drupal\Tests\views\Unit\Plugin\field\TestField $field */ - $field = $this->getMock('Drupal\Tests\views\Unit\Plugin\field\TestField', ['l'], [$this->configuration, $this->pluginId, $this->pluginDefinition]); + /** @var \Drupal\Tests\views\Unit\Plugin\field\FieldPluginBaseTestField $field */ + $field = $this->getMock('Drupal\Tests\views\Unit\Plugin\field\FieldPluginBaseTestField', ['l'], [$this->configuration, $this->pluginId, $this->pluginDefinition]); $field->init($this->executable, $this->display, $options); $field->setLinkGenerator($this->linkGenerator); @@ -506,7 +506,7 @@ protected function setupTestField(array $options = []) { } -class TestField extends FieldPluginBase { +class FieldPluginBaseTestField extends FieldPluginBase { public function setLinkGenerator(LinkGeneratorInterface $link_generator) { $this->linkGenerator = $link_generator; diff --git a/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php b/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php index 9ca73d4..90bd2cc 100644 --- a/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php @@ -7,9 +7,11 @@ namespace Drupal\Tests\views\Unit\Plugin\field; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Tests\UnitTestCase; use Drupal\Tests\views\Unit\Plugin\HandlerTestTrait; use Drupal\views\Plugin\views\field\Field; +use Drupal\views\ResultRow; use Symfony\Component\DependencyInjection\ContainerBuilder; /** @@ -438,6 +440,7 @@ public function testQueryWithGroupByForBaseField() { ]; $handler = new Field([], 'field', $definition, $this->entityManager, $this->formatterPluginManager, $this->fieldTypePluginManager, $this->languageManager, $this->renderer); $handler->view = $this->executable; + $handler->view->field = [$handler]; $this->setupLanguageRenderer($handler, $definition); @@ -499,6 +502,7 @@ public function testQueryWithGroupByForConfigField() { ]; $handler = new Field([], 'field', $definition, $this->entityManager, $this->formatterPluginManager, $this->fieldTypePluginManager, $this->languageManager, $this->renderer); $handler->view = $this->executable; + $handler->view->field = [$handler]; $this->setupLanguageRenderer($handler, $definition); @@ -551,6 +555,85 @@ public function testQueryWithGroupByForConfigField() { } /** + * @covers ::prepareItemsByDelta + * + * @dataProvider providerTestPrepareItemsByDelta + */ + public function testPrepareItemsByDelta(array $options, array $expected_values) { + $definition = [ + 'entity_type' => 'test_entity', + 'field_name' => 'integer', + ]; + $handler = new FieldTestField([], 'field', $definition, $this->entityManager, $this->formatterPluginManager, $this->fieldTypePluginManager, $this->languageManager, $this->renderer); + $handler->view = $this->executable; + $handler->view->field = [$handler]; + + $this->setupLanguageRenderer($handler, $definition); + + $field_storage = $this->getConfigFieldStorage(); + $field_storage->expects($this->any()) + ->method('getCardinality') + ->willReturn(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + + $this->entityManager->expects($this->any()) + ->method('getFieldStorageDefinitions') + ->with('test_entity') + ->willReturn([ + 'integer' => $field_storage, + ]); + + $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface'); + $table_mapping + ->expects($this->any()) + ->method('getFieldColumnName') + ->with($field_storage, 'value') + ->willReturn('integer_value'); + $entity_storage = $this->getMock('Drupal\Core\Entity\Sql\SqlEntityStorageInterface'); + $entity_storage->expects($this->any()) + ->method('getTableMapping') + ->willReturn($table_mapping); + $this->entityManager->expects($this->any()) + ->method('getStorage') + ->with('test_entity') + ->willReturn($entity_storage); + + $options = [ + 'group_column' => 'value', + 'group_columns' => [], + 'table' => 'test_entity__integer', + ] + $options; + $handler->init($this->executable, $this->display, $options); + + $this->executable->row_index = 0; + $this->executable->result = [0 => new ResultRow([])]; + + $items = [3, 1, 4, 1, 5, 9]; + $this->assertEquals($expected_values, $handler->executePrepareItemsByDelta($items)); + } + + /** + * Provides test data for testPrepareItemsByDelta(). + */ + public function providerTestPrepareItemsByDelta() { + $data = []; + + // Let's display all values. + $data[] = [[], [3, 1, 4, 1, 5, 9]]; + // Test just reversed deltas. + $data[] = [['delta_reversed' => TRUE], [9, 5, 1, 4, 1, 3]]; + + // Test combinations of delta limit, offset and first_last. + $data[] = [['group_rows' => TRUE, 'delta_limit' => 3], [3, 1, 4]]; + $data[] = [['group_rows' => TRUE, 'delta_limit' => 3, 'delta_offset' => 2], [4, 1, 5]]; + $data[] = [['group_rows' => TRUE, 'delta_reversed' => TRUE, 'delta_limit' => 3, 'delta_offset' => 2], [1, 4, 1]]; + $data[] = [['group_rows' => TRUE, 'delta_first_last' => TRUE], [3, 9]]; + $data[] = [['group_rows' => TRUE, 'delta_limit' => 1, 'delta_first_last' => TRUE], [3]]; + $data[] = [['group_rows' => TRUE, 'delta_offset' => 1, 'delta_first_last' => TRUE], [1, 9]]; + + return $data; + } + + /** * Returns a mocked base field storage object. * * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|\PHPUnit_Framework_MockObject_MockObject @@ -640,3 +723,11 @@ protected function setupLanguageRenderer(Field $handler, $definition) { } } + +class FieldTestField extends Field { + + public function executePrepareItemsByDelta(array $all_values) { + return $this->prepareItemsByDelta($all_values); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php index 5762e9e..397cb9d 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php @@ -1139,6 +1139,9 @@ public function testGetTranslationFromContext() { ->method('getTranslation') ->with('custom_langcode') ->will($this->returnValue($translated_entity)); + $entity->expects($this->any()) + ->method('getTranslationLanguages') + ->will($this->returnValue([new Language(['id' => 'en']), new Language(['id' => 'custom_langcode'])])); $this->assertSame($entity, $this->entityManager->getTranslationFromContext($entity)); $this->assertSame($translated_entity, $this->entityManager->getTranslationFromContext($entity, 'custom_langcode'));