diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index 6327656..6278472 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\field\FieldStorageConfigInterface; @@ -201,7 +202,10 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac return FALSE; } - return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original); + $current_schema = $this->getSchemaFromStorageDefinition($storage_definition); + $this->processFieldStorageSchema($current_schema); + + return $current_schema != $this->loadFieldSchemaData($original); } /** @@ -852,6 +856,7 @@ protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_ * The field schema data array. */ protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) { + $this->processFieldStorageSchema($schema); $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema); } @@ -1102,6 +1107,23 @@ protected function processIdentifierSchema(&$schema, $key) { } /** + * Processes the schema for a field storage definition. + * + * @param array &$field_storage_schema + * An array that contains the schema data for a field storage definition. + */ + protected function processFieldStorageSchema(array &$field_storage_schema) { + // Clean up some schema properties that should not be taken into account + // after a field storage has been created. + foreach ($field_storage_schema as $table_name => $table_schema) { + foreach ($table_schema['fields'] as $key => $schema) { + unset($field_storage_schema[$table_name]['fields'][$key]['initial']); + unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']); + } + } + } + + /** * Performs the specified operation on a field. * * This figures out whether the field is stored in a dedicated or shared table @@ -1613,29 +1635,54 @@ protected function hasNullFieldPropertyData($table_name, $column_name) { * - foreign keys: The schema definition for the foreign keys. * * @throws \Drupal\Core\Field\FieldException - * Exception thrown if the schema contains reserved column names. + * Exception thrown if the schema contains reserved column names or if the + * initial values definition is invalid. */ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) { $schema = []; + $table_mapping = $this->storage->getTableMapping(); $field_schema = $storage_definition->getSchema(); // Check that the schema does not include forbidden column names. - if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) { + if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) { throw new FieldException("Illegal field column names on {$storage_definition->getName()}"); } $field_name = $storage_definition->getName(); $base_table = $this->storage->getBaseTable(); + // Define the initial values, if any. + $initial_value = $initial_value_from_field = []; + $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition)); + if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) { + if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) { + $initial_value = $initial_storage_value[0]; + } + + if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) { + // Check that the field used for populating initial values is valid. + if (!isset($this->fieldStorageDefinitions[$initial_value_field_name])) { + throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist."); + } + + if ($storage_definition->getType() !== $this->fieldStorageDefinitions[$initial_value_field_name]->getType()) { + throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match."); + } + + if (!$table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$initial_value_field_name])) { + throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables."); + } + + $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name); + } + } + // A shared table contains rows for entities where the field is empty // (since other fields stored in the same table might not be empty), thus // the only columns that can be 'not null' are those for required - // properties of required fields. However, even those would break in the - // case where a new field is added to a table that contains existing rows. - // For now, we only hardcode 'not null' to a couple "entity keys", in order - // to keep their indexes optimized. - // @todo Revisit once we have support for 'initial' in - // https://www.drupal.org/node/2346019. + // properties of required fields. For now, we only hardcode 'not null' to a + // few "entity keys", in order to keep their indexes optimized. + // @todo Fix this in https://www.drupal.org/node/2841291. $not_null_keys = $this->entityType->getKeys(); // Label fields are not necessarily required. unset($not_null_keys['label']); @@ -1654,6 +1701,14 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st $schema['fields'][$schema_field_name] = $column_schema; $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys); + + // Use the initial value of the field storage, if available. + if ($initial_value && isset($initial_value[$field_column_name])) { + $schema['fields'][$schema_field_name]['initial'] = $initial_value[$field_column_name]; + } + elseif (!empty($initial_value_from_field)) { + $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name]; + } } if (!empty($field_schema['indexes'])) { diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 37cc103..16df7c6 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -510,6 +510,83 @@ public function setDefaultValueCallback($callback) { } /** + * Returns the initial value for the field. + * + * @return array + * The initial value for the field, as a numerically indexed array of items, + * each item being a property/value array (array() for no default value). + */ + public function getInitialValue() { + $value = isset($this->definition['initial_value']) ? $this->definition['initial_value'] : []; + + // Normalize into the "array keyed by delta" format. + if (isset($value) && !is_array($value)) { + $value = [ + [$this->getMainPropertyName() => $value], + ]; + } + + return $value; + } + + /** + * Sets an initial value for the field. + * + * @param mixed $value + * The initial value for the field. This can be either: + * - a literal, in which case it will be assigned to the first property of + * the first item; + * - a numerically indexed array of items, each item being a property/value + * array; + * - a non-numerically indexed array, in which case the array is assumed to + * be a property/value array and used as the first item; + * - an empty array for no initial value. + * + * @return $this + */ + public function setInitialValue($value) { + if ($value === NULL) { + $value = []; + } + // Unless the value is an empty array, we may need to transform it. + if (!is_array($value) || !empty($value)) { + if (!is_array($value)) { + $value = [[$this->getMainPropertyName() => $value]]; + } + elseif (is_array($value) && !is_numeric(array_keys($value)[0])) { + $value = [0 => $value]; + } + } + $this->definition['initial_value'] = $value; + + return $this; + } + + /** + * Returns the name of the field that will be used for getting initial values. + * + * @return string|null + * The field name. + */ + public function getInitialValueFromField() { + return isset($this->definition['initial_value_from_field']) ? $this->definition['initial_value_from_field'] : NULL; + } + + /** + * Sets a field that will be used for getting initial values. + * + * @param string $field_name + * The name of the field that will be used for getting initial values. + * + * @return $this + */ + public function setInitialValueFromField($field_name) { + $this->definition['initial_value_from_field'] = $field_name; + + return $this; + } + + /** * {@inheritdoc} */ public function getOptionsProvider($property_name, FieldableEntityInterface $entity) { diff --git a/core/modules/block_content/block_content.install b/core/modules/block_content/block_content.install index 6af2ac4..73cbdd8 100644 --- a/core/modules/block_content/block_content.install +++ b/core/modules/block_content/block_content.install @@ -5,7 +5,10 @@ * Install, update and uninstall functions for the block_content module. */ +use Drupal\block_content\BlockContentStorageSchema; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Add 'revision_translation_affected' field to 'block_content' entities. @@ -72,3 +75,53 @@ function block_content_update_8300() { $definition_update_manager->updateEntityType($entity_type); } + +/** + * Add a publishing status field for block_content entities. + */ +function block_content_update_8400() { + // Add the published entity key to the block_content entity type. + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + $entity_type = $definition_update_manager->getEntityType('block_content'); + $entity_keys = $entity_type->getKeys(); + $entity_keys['published'] = 'status'; + $entity_type->set('entity_keys', $entity_keys); + $definition_update_manager->updateEntityType($entity_type); + + // Install the publishing status field to the block_content entity type. + $status = BaseFieldDefinition::create('boolean') + ->setLabel(new TranslatableMarkup('Publishing status')) + ->setDescription(new TranslatableMarkup('A boolean indicating the published state.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE) + ->setRequired(TRUE) + ->setDefaultValue(TRUE); + + $has_content_translation_status_field = \Drupal::moduleHandler()->moduleExists('content_translation') && $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'block_content'); + if ($has_content_translation_status_field) { + $status->setInitialValueFromField('content_translation_status'); + } + else { + $status->setInitialValue(1); + } + $definition_update_manager->installFieldStorageDefinition('status', 'block_content', 'block_content', $status); + + // Uninstall the 'content_translation_status' field if needed. + $database = \Drupal::database(); + if ($has_content_translation_status_field) { + // First we have to remove the field data. + $database->update($entity_type->getDataTable()) + ->fields(['content_translation_status' => NULL]) + ->execute(); + + // A site may have disabled revisionability for this entity type. + if ($entity_type->isRevisionable()) { + $database->update($entity_type->getRevisionDataTable()) + ->fields(['content_translation_status' => NULL]) + ->execute(); + } + + $content_translation_status = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'block_content'); + $definition_update_manager->uninstallFieldStorageDefinition($content_translation_status); + } +} diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php index d0c19c5..6760b39 100644 --- a/core/modules/block_content/src/BlockContentAccessControlHandler.php +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php @@ -19,7 +19,7 @@ class BlockContentAccessControlHandler extends EntityAccessControlHandler { */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { if ($operation === 'view') { - return AccessResult::allowed(); + return AccessResult::allowedIf($entity->isPublished() || $account->hasPermission('administer blocks')); } return parent::checkAccess($entity, $operation, $account); } diff --git a/core/modules/block_content/src/BlockContentInterface.php b/core/modules/block_content/src/BlockContentInterface.php index 130cae1..75fdc59 100644 --- a/core/modules/block_content/src/BlockContentInterface.php +++ b/core/modules/block_content/src/BlockContentInterface.php @@ -4,12 +4,13 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\RevisionLogInterface; /** * Provides an interface defining a custom block entity. */ -interface BlockContentInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface { +interface BlockContentInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityPublishedInterface { /** * Returns the block revision log message. diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index b7c3d4d..bcaf912 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -3,7 +3,9 @@ namespace Drupal\block_content\Entity; use Drupal\Core\Entity\ContentEntityBase; +use Drupal\Core\Entity\EditorialContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; +use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -50,7 +52,8 @@ * "bundle" = "type", * "label" = "info", * "langcode" = "langcode", - * "uuid" = "uuid" + * "uuid" = "uuid", + * "published" = "status", * }, * revision_metadata_keys = { * "revision_user" = "revision_user", @@ -67,9 +70,7 @@ * caching. * See https://www.drupal.org/node/2284917#comment-9132521 for more information. */ -class BlockContent extends ContentEntityBase implements BlockContentInterface { - - use EntityChangedTrait; +class BlockContent extends EditorialContentEntityBase implements BlockContentInterface { /** * The theme the block is being created in. @@ -173,6 +174,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['type']->setLabel(t('Block type')) ->setDescription(t('The block type.')); + $fields['revision_log']->setDescription(t('The log entry explaining the changes in this revision.')); + $fields['info'] = BaseFieldDefinition::create('string') ->setLabel(t('Block description')) ->setDescription(t('A brief description of your block.')) @@ -186,35 +189,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', TRUE) ->addConstraint('UniqueField', []); - $fields['revision_log'] = BaseFieldDefinition::create('string_long') - ->setLabel(t('Revision log message')) - ->setDescription(t('The log entry explaining the changes in this revision.')) - ->setRevisionable(TRUE) - ->setDisplayOptions('form', [ - 'type' => 'string_textarea', - 'weight' => 25, - 'settings' => [ - 'rows' => 4, - ], - ]); - $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) ->setDescription(t('The time that the custom block was last edited.')) ->setTranslatable(TRUE) ->setRevisionable(TRUE); - $fields['revision_created'] = BaseFieldDefinition::create('created') - ->setLabel(t('Revision create time')) - ->setDescription(t('The time that the current revision was created.')) - ->setRevisionable(TRUE); - - $fields['revision_user'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Revision user')) - ->setDescription(t('The user ID of the author of the current revision.')) - ->setSetting('target_type', 'user') - ->setRevisionable(TRUE); - $fields['revision_translation_affected'] = BaseFieldDefinition::create('boolean') ->setLabel(t('Revision translation affected')) ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.')) diff --git a/core/modules/block_content/src/Tests/BlockContentUpdateTest.php b/core/modules/block_content/src/Tests/Update/BlockContentUpdateTest.php similarity index 57% rename from core/modules/block_content/src/Tests/BlockContentUpdateTest.php rename to core/modules/block_content/src/Tests/Update/BlockContentUpdateTest.php index 77c12c6..888607e 100644 --- a/core/modules/block_content/src/Tests/BlockContentUpdateTest.php +++ b/core/modules/block_content/src/Tests/Update/BlockContentUpdateTest.php @@ -1,6 +1,6 @@ databaseDumpFiles = [ - __DIR__ . '/../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8-rc1.filled.standard.php.gz', ]; } @@ -43,4 +44,27 @@ public function testSimpleUpdates() { $this->assertEqual('block_content_field_revision', $entity_type->getRevisionDataTable()); } + /** + * Tests adding a status field to the block content entity type. + * + * @see block_content_update_8400() + */ + public function testStatusFieldAddition() { + $schema = \Drupal::database()->schema(); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Run updates. + $this->runUpdates(); + + // Check that the field exists and has the correct label. + $updated_field = $entity_definition_update_manager->getFieldStorageDefinition('status', 'block_content'); + $this->assertEqual('Publishing status', $updated_field->getLabel()); + + $content_translation_status = $entity_definition_update_manager->getFieldStorageDefinition('content_translation_status', 'block_content'); + $this->assertNull($content_translation_status); + + $this->assertFalse($schema->fieldExists('block_content_field_revision', 'content_translation_status')); + $this->assertFalse($schema->fieldExists('block_content_field_data', 'content_translation_status')); + } + } diff --git a/core/modules/block_content/tests/src/Functional/UnpublishedBlockTest.php b/core/modules/block_content/tests/src/Functional/UnpublishedBlockTest.php new file mode 100644 index 0000000..082a1ae --- /dev/null +++ b/core/modules/block_content/tests/src/Functional/UnpublishedBlockTest.php @@ -0,0 +1,49 @@ + 'Test block', + 'type' => 'basic', + ]); + $block_content->save(); + + $this->placeBlock('block_content:' . $block_content->uuid()); + + $this->drupalGet(''); + $page = $this->getSession()->getPage(); + $this->assertTrue($page->has('css', '.block-block-content' . $block_content->uuid())); + + $block_content->setPublished(FALSE); + $block_content->save(); + + $this->drupalGet(''); + $page = $this->getSession()->getPage(); + $this->assertFalse($page->has('css', '.block-block-content' . $block_content->uuid())); + } +} diff --git a/core/modules/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install index dc12d9f..67b161c 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -5,6 +5,8 @@ * Installation functions for Content Translation module. */ +use \Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use \Drupal\Core\Language\LanguageInterface; use \Drupal\Core\Url; /** @@ -44,3 +46,57 @@ function content_translation_update_8001() { function content_translation_update_8002() { \Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions(); } + +/** + * Fix the initial values for content translation metadata fields. + */ +function content_translation_update_8400() { + $database = \Drupal::database(); + /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */ + $content_translation_manager = \Drupal::service('content_translation.manager'); + /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */ + $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + foreach ($content_translation_manager->getSupportedEntityTypes() as $entity_type_id => $entity_type_definition) { + $storage = $entity_type_manager->getStorage($entity_type_id); + if ($storage instanceof SqlEntityStorageInterface) { + $entity_type = $entity_definition_update_manager->getEntityType($entity_type_id); + $storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id); + + // Since the entity type is managed by Content Translation, we can assume + // that it is translatable, so we use the data and revision data tables. + $tables_to_update = [$entity_type->getDataTable()]; + if ($entity_type->isRevisionable()) { + $tables_to_update += [$entity_type->getRevisionDataTable()]; + } + + foreach ($tables_to_update as $table_name) { + // Fix the values of the 'content_translation_source' field. + if (isset($storage_definitions['content_translation_source'])) { + $database->update($table_name) + ->fields(['content_translation_source' => LanguageInterface::LANGCODE_NOT_SPECIFIED]) + ->isNull('content_translation_source') + ->execute(); + } + + // Fix the values of the 'content_translation_outdated' field. + if (isset($storage_definitions['content_translation_outdated'])) { + $database->update($table_name) + ->fields(['content_translation_outdated' => 0]) + ->isNull('content_translation_outdated') + ->execute(); + } + + // Fix the values of the 'content_translation_status' field. + if (isset($storage_definitions['content_translation_status'])) { + $database->update($table_name) + ->fields(['content_translation_status' => 1]) + ->isNull('content_translation_status') + ->execute(); + } + } + } + } +} diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index c1688b9..3f7baa7 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -117,14 +117,16 @@ public function getFieldDefinitions() { ->setDescription(t('The source language from which this translation was created.')) ->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setInitialValue(LanguageInterface::LANGCODE_NOT_SPECIFIED); $definitions['content_translation_outdated'] = BaseFieldDefinition::create('boolean') ->setLabel(t('Translation outdated')) ->setDescription(t('A boolean indicating whether this translation needs to be updated.')) ->setDefaultValue(FALSE) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setInitialValue(0); if (!$this->hasAuthor()) { $definitions['content_translation_uid'] = BaseFieldDefinition::create('entity_reference') @@ -143,7 +145,8 @@ public function getFieldDefinitions() { ->setDescription(t('A boolean indicating whether the translation is visible to non-translators.')) ->setDefaultValue(TRUE) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setInitialValue(1); } if (!$this->hasCreatedTime()) { diff --git a/core/modules/content_translation/src/Tests/Update/ContentTranslationUpdateTest.php b/core/modules/content_translation/src/Tests/Update/ContentTranslationUpdateTest.php new file mode 100644 index 0000000..85b952c --- /dev/null +++ b/core/modules/content_translation/src/Tests/Update/ContentTranslationUpdateTest.php @@ -0,0 +1,100 @@ +database = \Drupal::database(); + $this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager(); + $this->entityManager = \Drupal::entityManager(); + $this->state = \Drupal::state(); + } + + /** + * {@inheritdoc} + */ + public function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz', + ]; + } + + /** + * Tests that initial values for metadata fields are populated correctly. + */ + public function testContentTranslationUpdate8400() { + $this->updateEntityTypeToTranslatable(); + + // The test database dump contains NULL values for + // 'content_translation_source', 'content_translation_outdated' and + // 'content_translation_status' for the first 50 test entities. + // @see _entity_test_update_create_test_entities() + $first_entity_record = $this->database->select('entity_test_update_data', 'etud') + ->fields('etud') + ->condition('etud.id', 1) + ->execute() + ->fetchAllAssoc('id'); + $this->assertNull($first_entity_record[1]->content_translation_source); + $this->assertNull($first_entity_record[1]->content_translation_outdated); + $this->assertNull($first_entity_record[1]->content_translation_status); + + $this->runUpdates(); + + // After running the updates, all those fields should be populated with + // their default values. + $first_entity_record = $this->database->select('entity_test_update_data', 'etud') + ->fields('etud') + ->condition('etud.id', 1) + ->execute() + ->fetchAllAssoc('id'); + $this->assertEqual(LanguageInterface::LANGCODE_NOT_SPECIFIED, $first_entity_record[1]->content_translation_source); + $this->assertEqual(0, $first_entity_record[1]->content_translation_outdated); + $this->assertEqual(1, $first_entity_record[1]->content_translation_status); + } + +} diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php index bfd65cf..08ff643 100644 --- a/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php +++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; use Drupal\views\Views; @@ -124,6 +125,11 @@ public function testTaxonomyTermView() { // query anymore. // @see \Drupal\views\Plugin\views\filter\LanguageFilter::query() $node->delete(); + // We also have to remove the nodes created by the parent ::setUp() method + // if we want to be able to uninstall the Content Translation module. + foreach (Node::loadMultiple() as $node) { + $node->delete(); + } \Drupal::service('module_installer')->uninstall(['content_translation', 'language']); $view = Views::getView('taxonomy_term'); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 646ad2e..92733bc 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityTypeEvents; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionEvents; use Drupal\Core\Language\LanguageInterface; use Drupal\entity_test_update\Entity\EntityTestUpdate; @@ -817,4 +818,119 @@ public function testLongNameFieldIndexes() { $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.'); } + /** + * Tests adding a base field with initial values. + */ + public function testInitialValue() { + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $db_schema = $this->database->schema(); + + // Create two entities before adding the base field. + /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ + $storage->create()->save(); + $storage->create()->save(); + + // Add a base field with an initial value. + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new base field')) + ->setInitialValue('test value'); + + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Check that the initial values have been applied. + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $entities = $storage->loadMultiple(); + $this->assertEquals('test value', $entities[1]->get('new_base_field')->value); + $this->assertEquals('test value', $entities[2]->get('new_base_field')->value); + } + + /** + * Tests adding a base field with initial values inherited from another field. + */ + public function testInitialValueFromField() { + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $db_schema = $this->database->schema(); + + // Create two entities before adding the base field. + /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ + $storage->create(['name' => 'First entity'])->save(); + $storage->create(['name' => 'Second entity'])->save(); + + // Add a base field with an initial value inherited from another field. + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('name'); + + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Check that the initial values have been applied. + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $entities = $storage->loadMultiple(); + $this->assertEquals('First entity', $entities[1]->get('new_base_field')->value); + $this->assertEquals('Second entity', $entities[2]->get('new_base_field')->value); + } + + /** + * Tests the error handling when using initial values from another field. + */ + public function testInitialValueFromFieldErrorHandling() { + // Check that setting invalid values for 'initial value from field' doesn't + // work. + try { + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('field_that_does_not_exist'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->fail('Using a non-existent field as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage()); + $this->pass('Using a non-existent field as initial value does not work.'); + } + + try { + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('integer') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('name'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->fail('Using a field of a different type as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage()); + $this->pass('Using a field of a different type as initial value does not work.'); + } + + try { + // Add a base field that will not be stored in the shared tables. + $initial_field = BaseFieldDefinition::create('string') + ->setName('initial_field') + ->setLabel(t('An initial field')) + ->setCardinality(2); + $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field); + + // Now add the base field which will try to use the previously added field + // as the source of its initial values. + $new_base_field = BaseFieldDefinition::create('string') + ->setName('new_base_field') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('initial_field'); + $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field); + $this->fail('Using a field that is not stored in the shared tables as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage()); + $this->pass('Using a field that is not stored in the shared tables as initial value does not work.'); + } + } + } diff --git a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php index 341ed9c..616f6ba 100644 --- a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php @@ -195,6 +195,52 @@ public function testFieldDefaultValue() { } /** + * Tests field initial value. + * + * @covers ::getInitialValue + * @covers ::setInitialValue + */ + public function testFieldInitialValue() { + $definition = BaseFieldDefinition::create($this->fieldType); + $default_value = [ + 'value' => $this->randomMachineName(), + ]; + $expected_default_value = [$default_value]; + $definition->setInitialValue($default_value); + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->getMock(); + // Set the field item list class to be used to avoid requiring the typed + // data manager to retrieve it. + $definition->setClass('Drupal\Core\Field\FieldItemList'); + $this->assertEquals($expected_default_value, $definition->getInitialValue($entity)); + + $data_definition = $this->getMockBuilder('Drupal\Core\TypedData\DataDefinition') + ->disableOriginalConstructor() + ->getMock(); + $data_definition->expects($this->any()) + ->method('getClass') + ->will($this->returnValue('Drupal\Core\Field\FieldItemBase')); + $definition->setItemDefinition($data_definition); + + // Set default value only with a literal. + $definition->setInitialValue($default_value['value']); + $this->assertEquals($expected_default_value, $definition->getInitialValue($entity)); + + // Set default value with an indexed array. + $definition->setInitialValue($expected_default_value); + $this->assertEquals($expected_default_value, $definition->getInitialValue($entity)); + + // Set default value with an empty array. + $definition->setInitialValue([]); + $this->assertEquals([], $definition->getInitialValue($entity)); + + // Set default value with NULL. + $definition->setInitialValue(NULL); + $this->assertEquals([], $definition->getInitialValue($entity)); + } + + /** * Tests field translatable methods. * * @covers ::isTranslatable