composer update

This commit is contained in:
Oliver Davies 2019-01-24 08:00:03 +00:00
parent f6abc3dce2
commit 71dfaca858
1753 changed files with 45274 additions and 14619 deletions

View file

@ -70,9 +70,9 @@ abstract class AggregatorTestBase extends WebTestBase {
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
$this->assert(isset($view_link), 'The message area contains a link to a feed');
$fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']])->fetchField();
$this->assertTrue(!empty($fid), 'The feed found in database.');
return Feed::load($fid);
$fids = \Drupal::entityQuery('aggregator_feed')->condition('title', $edit['title[0][value]'])->condition('url', $edit['url[0][value]'])->execute();
$this->assertNotEmpty($fids, 'The feed found in database.');
return Feed::load(array_values($fids)[0]);
}
/**
@ -179,10 +179,10 @@ abstract class AggregatorTestBase extends WebTestBase {
$this->clickLink('Update items');
// Ensure we have the right number of items.
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()]);
$iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
$feed->items = [];
foreach ($result as $item) {
$feed->items[] = $item->iid;
foreach ($iids as $iid) {
$feed->items[] = $iid;
}
if ($expected_count !== NULL) {
@ -211,11 +211,12 @@ abstract class AggregatorTestBase extends WebTestBase {
* Expected number of feed items.
*/
public function updateAndDelete(FeedInterface $feed, $expected_count) {
$count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
$this->updateFeedItems($feed, $expected_count);
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
$count = $count_query->execute();
$this->assertTrue($count);
$this->deleteFeedItems($feed);
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
$count = $count_query->execute();
$this->assertTrue($count == 0);
}
@ -231,7 +232,7 @@ abstract class AggregatorTestBase extends WebTestBase {
* TRUE if feed is unique.
*/
public function uniqueFeed($feed_name, $feed_url) {
$result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed_name, ':url' => $feed_url])->fetchField();
$result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed_name)->condition('url', $feed_url)->count()->execute();
return (1 == $result);
}

View file

@ -20,31 +20,23 @@ class AggregatorCronTest extends AggregatorTestBase {
// Create feed and test basic updating on cron.
$this->createSampleNodes();
$feed = $this->createFeed();
$count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
$this->cronRun();
$this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
$this->assertEqual(5, $count_query->execute());
$this->deleteFeedItems($feed);
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
$this->assertEqual(0, $count_query->execute());
$this->cronRun();
$this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
$this->assertEqual(5, $count_query->execute());
// Test feed locking when queued for update.
$this->deleteFeedItems($feed);
db_update('aggregator_feed')
->condition('fid', $feed->id())
->fields([
'queued' => REQUEST_TIME,
])
->execute();
$feed->setQueuedTime(REQUEST_TIME)->save();
$this->cronRun();
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
db_update('aggregator_feed')
->condition('fid', $feed->id())
->fields([
'queued' => 0,
])
->execute();
$this->assertEqual(0, $count_query->execute());
$feed->setQueuedTime(0)->save();
$this->cronRun();
$this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField());
$this->assertEqual(5, $count_query->execute());
}
}

View file

@ -67,9 +67,9 @@ abstract class AggregatorTestBase extends BrowserTestBase {
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
$this->assert(isset($view_link), 'The message area contains a link to a feed');
$fid = db_query("SELECT fid FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $edit['title[0][value]'], ':url' => $edit['url[0][value]']])->fetchField();
$this->assertTrue(!empty($fid), 'The feed found in database.');
return Feed::load($fid);
$fids = \Drupal::entityQuery('aggregator_feed')->condition('title', $edit['title[0][value]'])->condition('url', $edit['url[0][value]'])->execute();
$this->assertNotEmpty($fids, 'The feed found in database.');
return Feed::load(array_values($fids)[0]);
}
/**
@ -176,10 +176,10 @@ abstract class AggregatorTestBase extends BrowserTestBase {
$this->clickLink('Update items');
// Ensure we have the right number of items.
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()]);
$iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
$feed->items = [];
foreach ($result as $item) {
$feed->items[] = $item->iid;
foreach ($iids as $iid) {
$feed->items[] = $iid;
}
if ($expected_count !== NULL) {
@ -208,11 +208,12 @@ abstract class AggregatorTestBase extends BrowserTestBase {
* Expected number of feed items.
*/
public function updateAndDelete(FeedInterface $feed, $expected_count) {
$count_query = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->count();
$this->updateFeedItems($feed, $expected_count);
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
$count = $count_query->execute();
$this->assertTrue($count);
$this->deleteFeedItems($feed);
$count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
$count = $count_query->execute();
$this->assertTrue($count == 0);
}
@ -228,7 +229,7 @@ abstract class AggregatorTestBase extends BrowserTestBase {
* TRUE if feed is unique.
*/
public function uniqueFeed($feed_name, $feed_url) {
$result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed_name, ':url' => $feed_url])->fetchField();
$result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed_name)->condition('url', $feed_url)->count()->execute();
return (1 == $result);
}

View file

@ -43,8 +43,8 @@ class DeleteFeedTest extends AggregatorTestBase {
$this->assertResponse(404, 'Deleted feed source does not exist.');
// Check database for feed.
$result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", [':title' => $feed1->label(), ':url' => $feed1->getUrl()])->fetchField();
$this->assertFalse($result, 'Feed not found in database');
$result = \Drupal::entityQuery('aggregator_feed')->condition('title', $feed1->label())->condition('url', $feed1->getUrl())->count()->execute();
$this->assertEquals(0, $result, 'Feed not found in database');
}
}

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\aggregator\Functional;
use Drupal\Core\Url;
use Drupal\aggregator\Entity\Feed;
use Drupal\aggregator\Entity\Item;
/**
* Tests the built-in feed parser with valid feed samples.
@ -57,16 +58,17 @@ class FeedParserTest extends AggregatorTestBase {
$this->assertText('Atom-Powered Robots Run Amok');
$this->assertLinkByHref('http://example.org/2003/12/13/atom03');
$this->assertText('Some text.');
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', [':link' => 'http://example.org/2003/12/13/atom03'])->fetchField(), 'Atom entry id element is parsed correctly.');
$iids = \Drupal::entityQuery('aggregator_item')->condition('link', 'http://example.org/2003/12/13/atom03')->execute();
$item = Item::load(array_values($iids)[0]);
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', $item->getGuid(), 'Atom entry id element is parsed correctly.');
// Check for second feed entry.
$this->assertText('We tried to stop them, but we failed.');
$this->assertLinkByHref('http://example.org/2003/12/14/atom03');
$this->assertText('Some other text.');
$db_guid = db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', [
':link' => 'http://example.org/2003/12/14/atom03',
])->fetchField();
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a', $db_guid, 'Atom entry id element is parsed correctly.');
$iids = \Drupal::entityQuery('aggregator_item')->condition('link', 'http://example.org/2003/12/14/atom03')->execute();
$item = Item::load(array_values($iids)[0]);
$this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-bbbb-80da344efa6a', $item->getGuid(), 'Atom entry id element is parsed correctly.');
}
/**

View file

@ -2,6 +2,8 @@
namespace Drupal\Tests\aggregator\Functional;
use Drupal\aggregator\Entity\Feed;
/**
* Tests OPML import.
*
@ -44,7 +46,8 @@ class ImportOpmlTest extends AggregatorTestBase {
* Submits form filled with invalid fields.
*/
public function validateImportFormFields() {
$before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
$count_query = \Drupal::entityQuery('aggregator_feed')->count();
$before = $count_query->execute();
$edit = [];
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
@ -62,7 +65,7 @@ class ImportOpmlTest extends AggregatorTestBase {
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
$this->assertText(t('The URL invalidUrl://empty is not valid.'), 'Error if the URL is invalid.');
$after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
$after = $count_query->execute();
$this->assertEqual($before, $after, 'No feeds were added during the three last form submissions.');
}
@ -70,7 +73,8 @@ class ImportOpmlTest extends AggregatorTestBase {
* Submits form with invalid, empty, and valid OPML files.
*/
protected function submitImportForm() {
$before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
$count_query = \Drupal::entityQuery('aggregator_feed')->count();
$before = $count_query->execute();
$form['files[upload]'] = $this->getInvalidOpml();
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $form, t('Import'));
@ -80,10 +84,12 @@ class ImportOpmlTest extends AggregatorTestBase {
$this->drupalPostForm('admin/config/services/aggregator/add/opml', $edit, t('Import'));
$this->assertText(t('No new feed has been added.'), 'Attempting to load empty OPML from remote URL.');
$after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
$after = $count_query->execute();
$this->assertEqual($before, $after, 'No feeds were added during the two last form submissions.');
db_delete('aggregator_feed')->execute();
foreach (Feed::loadMultiple() as $feed) {
$feed->delete();
}
$feeds[0] = $this->getFeedEditArray();
$feeds[1] = $this->getFeedEditArray();
@ -96,15 +102,15 @@ class ImportOpmlTest extends AggregatorTestBase {
$this->assertRaw(t('A feed with the URL %url already exists.', ['%url' => $feeds[0]['url[0][value]']]), 'Verifying that a duplicate URL was identified');
$this->assertRaw(t('A feed named %title already exists.', ['%title' => $feeds[1]['title[0][value]']]), 'Verifying that a duplicate title was identified');
$after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
$after = $count_query->execute();
$this->assertEqual($after, 2, 'Verifying that two distinct feeds were added.');
$feeds_from_db = db_query("SELECT title, url, refresh FROM {aggregator_feed}");
$feed_entities = Feed::loadMultiple();
$refresh = TRUE;
foreach ($feeds_from_db as $feed) {
$title[$feed->url] = $feed->title;
$url[$feed->title] = $feed->url;
$refresh = $refresh && $feed->refresh == 900;
foreach ($feed_entities as $feed_entity) {
$title[$feed_entity->getUrl()] = $feed_entity->label();
$url[$feed_entity->label()] = $feed_entity->getUrl();
$refresh = $refresh && $feed_entity->getRefreshRate() == 900;
}
$this->assertEqual($title[$feeds[0]['url[0][value]']], $feeds[0]['title[0][value]'], 'First feed was added correctly.');

View file

@ -3,6 +3,7 @@
namespace Drupal\Tests\aggregator\Functional;
use Drupal\aggregator\Entity\Feed;
use Drupal\aggregator\Entity\Item;
/**
* Update feed items from a feed.
@ -43,26 +44,24 @@ class UpdateFeedItemTest extends AggregatorTestBase {
$view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'aggregator/sources/']);
$this->assert(isset($view_link), 'The message area contains a link to a feed');
$fid = db_query("SELECT fid FROM {aggregator_feed} WHERE url = :url", [':url' => $edit['url[0][value]']])->fetchField();
$feed = Feed::load($fid);
$fids = \Drupal::entityQuery('aggregator_feed')->condition('url', $edit['url[0][value]'])->execute();
$feed = Feed::load(array_values($fids)[0]);
$feed->refreshItems();
$before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
$iids = \Drupal::entityQuery('aggregator_item')->condition('fid', $feed->id())->execute();
$before = Item::load(array_values($iids)[0])->getPostedTime();
// Sleep for 3 second.
sleep(3);
db_update('aggregator_feed')
->condition('fid', $feed->id())
->fields([
'checked' => 0,
'hash' => '',
'etag' => '',
'modified' => 0,
])
->execute();
$feed
->setLastCheckedTime(0)
->setHash('')
->setEtag('')
->setLastModified(0)
->save();
$feed->refreshItems();
$after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', [':fid' => $feed->id()])->fetchField();
$after = Item::load(array_values($iids)[0])->getPostedTime();
$this->assertTrue($before === $after, format_string('Publish timestamp of feed item was not updated (@before === @after)', ['@before' => $before, '@after' => $after]));
// Make sure updating items works even after uninstalling a module

View file

@ -93,7 +93,8 @@
*/
Drupal.behaviors.blockHighlightPlacement = {
attach(context, settings) {
if (settings.blockPlacement) {
// Ensure that the block we are attempting to scroll to actually exists.
if (settings.blockPlacement && $('.js-block-placed').length) {
$(context)
.find('[data-drupal-selector="edit-blocks"]')
.once('block-highlight')

View file

@ -41,7 +41,7 @@
Drupal.behaviors.blockHighlightPlacement = {
attach: function attach(context, settings) {
if (settings.blockPlacement) {
if (settings.blockPlacement && $('.js-block-placed').length) {
$(context).find('[data-drupal-selector="edit-blocks"]').once('block-highlight').each(function () {
var $container = $(this);

View file

@ -193,6 +193,9 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
if ($this->request->query->has('block-placement')) {
$placement = $this->request->query->get('block-placement');
$form['#attached']['drupalSettings']['blockPlacement'] = $placement;
// Remove the block placement from the current request so that it is not
// passed on to any redirect destinations.
$this->request->query->remove('block-placement');
}
// Loop over each region and build blocks.
@ -378,9 +381,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
$entity->save();
}
$this->messenger->addStatus($this->t('The block settings have been updated.'));
// Remove any previously set block placement.
$this->request->query->remove('block-placement');
}
/**

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\block\Plugin\migrate\source\d7;
use Drupal\block\Plugin\migrate\source\Block;
/**
* Gets i18n block data from source database.
*
* @MigrateSource(
* id = "d7_block_translation",
* source_module = "i18n_block"
* )
*/
class BlockTranslation extends Block {
/**
* {@inheritdoc}
*/
public function query() {
// Let the parent set the block table to use, but do not use the parent
// query. Instead build a query so can use an inner join to the selected
// block table.
parent::query();
$query = $this->select('i18n_string', 'i18n')
->fields('i18n')
->fields('b', [
'bid',
'module',
'delta',
'theme',
'status',
'weight',
'region',
'custom',
'visibility',
'pages',
'title',
'cache',
'i18n_mode',
])
->fields('lt', [
'lid',
'translation',
'language',
'plid',
'plural',
'i18n_status',
])
->condition('i18n_mode', 1);
$query->leftjoin($this->blockTable, 'b', ('b.delta = i18n.objectid'));
$query->leftjoin('locales_target', 'lt', 'lt.lid = i18n.lid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The block numeric identifier.'),
'module' => $this->t('The module providing the block.'),
'delta' => $this->t("The block's delta."),
'theme' => $this->t('Which theme the block is placed in.'),
'status' => $this->t('Block enabled status'),
'weight' => $this->t('Block weight within region'),
'region' => $this->t('Theme region within which the block is set'),
'visibility' => $this->t('Visibility'),
'pages' => $this->t('Pages list.'),
'title' => $this->t('Block title.'),
'cache' => $this->t('Cache rule.'),
'i18n_mode' => $this->t('Multilingual mode'),
'lid' => $this->t('Language string ID'),
'textgroup' => $this->t('A module defined group of translations'),
'context' => $this->t('Full string ID for quick search: type:objectid:property.'),
'objectid' => $this->t('Object ID'),
'type' => $this->t('Object type for this string'),
'property' => $this->t('Object property for this string'),
'objectindex' => $this->t('Integer value of Object ID'),
'format' => $this->t('The {filter_format}.format of the string'),
'translation' => $this->t('Translation'),
'language' => $this->t('Language code'),
'plid' => $this->t('Parent lid'),
'plural' => $this->t('Plural index number'),
'i18n_status' => $this->t('Translation needs update'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['delta']['type'] = 'string';
$ids['delta']['alias'] = 'b';
$ids['language']['type'] = 'string';
return $ids;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a context-aware block that uses a not-passed, non-required context.
*
* @Block(
* id = "test_context_aware_no_valid_context_options",
* admin_label = @Translation("Test context-aware block - no valid context options"),
* context_definitions = {
* "email" = @ContextDefinition("email", required = FALSE)
* }
* )
*/
class TestContextAwareNoValidContextOptionsBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => 'Rendered block with no valid context options',
];
}
}

View file

@ -3,6 +3,8 @@
namespace Drupal\Tests\block\Functional;
use Drupal\Component\Utility\Html;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Drupal\Tests\BrowserTestBase;
/**
@ -239,6 +241,13 @@ class BlockUiTest extends BrowserTestBase {
$this->assertText('User context found.');
$this->assertRaw($expected_text);
// Test context mapping form element is not visible if there are no valid
// context options for the block (the test_context_aware_no_valid_context_options
// block has one context defined which is not available for it on the
// Block Layout interface).
$this->drupalGet('admin/structure/block/add/test_context_aware_no_valid_context_options/classy');
$this->assertSession()->fieldNotExists('edit-settings-context-mapping-email');
// Test context mapping allows empty selection for optional contexts.
$this->drupalGet('admin/structure/block/manage/testcontextawareblock');
$edit = [
@ -281,6 +290,24 @@ class BlockUiTest extends BrowserTestBase {
* Tests the block placement indicator.
*/
public function testBlockPlacementIndicator() {
// Test the block placement indicator with using the domain as URL language
// indicator. This causes destination query parameters to be absolute URLs.
\Drupal::service('module_installer')->install(['language', 'locale']);
$this->container = \Drupal::getContainer();
ConfigurableLanguage::createFromLangcode('it')->save();
$config = $this->config('language.types');
$config->set('negotiation.language_interface.enabled', [
LanguageNegotiationUrl::METHOD_ID => -10,
]);
$config->save();
$config = $this->config('language.negotiation');
$config->set('url.source', LanguageNegotiationUrl::CONFIG_DOMAIN);
$config->set('url.domains', [
'en' => \Drupal::request()->getHost(),
'it' => 'it.example.com',
]);
$config->save();
// Select the 'Powered by Drupal' block to be placed.
$block = [];
$block['id'] = strtolower($this->randomMachineName());
@ -289,11 +316,30 @@ class BlockUiTest extends BrowserTestBase {
// After adding a block, it will indicate which block was just added.
$this->drupalPostForm('admin/structure/block/add/system_powered_by_block', $block, t('Save block'));
$this->assertUrl('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
$this->assertSession()->addressEquals('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
// Resaving the block page will remove the block indicator.
// Resaving the block page will remove the block placement indicator.
$this->drupalPostForm(NULL, [], t('Save blocks'));
$this->assertUrl('admin/structure/block/list/classy');
$this->assertSession()->addressEquals('admin/structure/block/list/classy');
// Place another block and test the remove functionality works with the
// block placement indicator. Click the first 'Place block' link to bring up
// the list of blocks to place in the first available region.
$this->clickLink('Place block');
// Select the first available block.
$this->clickLink('Place block');
$block = [];
$block['id'] = strtolower($this->randomMachineName());
$block['theme'] = 'classy';
$this->submitForm([], 'Save block');
$this->assertSession()->addressEquals('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
// Removing a block will remove the block placement indicator.
$this->clickLink('Remove');
$this->submitForm([], 'Remove');
// @todo https://www.drupal.org/project/drupal/issues/2980527 this should be
// 'admin/structure/block/list/classy' but there is a bug.
$this->assertSession()->addressEquals('admin/structure/block');
}
/**

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\Tests\block\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of i18n block translations.
*
* @group migrate_drupal_7
*/
class MigrateBlockContentTranslationTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'text',
'aggregator',
'book',
'block',
'comment',
'forum',
'views',
'block_content',
'config_translation',
'content_translation',
'language',
'statistics',
'taxonomy',
// Required for translation migrations.
'migrate_drupal_multilingual',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['block']);
$this->installConfig(['block_content']);
$this->installEntitySchema('block_content');
$this->executeMigrations([
'language',
'd7_filter_format',
'block_content_type',
'block_content_body_field',
'd7_custom_block',
'd7_user_role',
'd7_block',
'd7_block_translation',
]);
block_rebuild();
}
/**
* Tests the migration of block title translation.
*/
public function testBlockContentTranslation() {
/** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */
$language_manager = $this->container->get('language_manager');
$config = $language_manager->getLanguageConfigOverride('fr', 'block.block.bartik_user_login');
$this->assertSame('fr - User login title', $config->get('settings.label'));
}
}

View file

@ -112,10 +112,10 @@ class MigrateBlockTest extends MigrateDrupal7TestBase {
public function testBlockMigration() {
$this->assertEntity('bartik_system_main', 'system_main_block', [], '', 'content', 'bartik', 0, '', '0');
$this->assertEntity('bartik_search_form', 'search_form_block', [], '', 'sidebar_first', 'bartik', -1, '', '0');
$this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, '', '0');
$this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, 'User login title', 'visible');
$this->assertEntity('bartik_system_powered_by', 'system_powered_by_block', [], '', 'footer_fifth', 'bartik', 10, '', '0');
$this->assertEntity('seven_system_main', 'system_main_block', [], '', 'content', 'seven', 0, '', '0');
$this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, '', '0');
$this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, 'User login title', 'visible');
// The d7_custom_block migration should have migrated a block containing a
// mildly amusing limerick. We'll need its UUID to determine

View file

@ -0,0 +1,147 @@
<?php
namespace Drupal\Tests\block\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests i18n block source plugin.
*
* @covers \Drupal\block\Plugin\migrate\source\d7\BlockTranslation
*
* @group content_translation
*/
class BlockTranslationTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['block', 'migrate_drupal'];
/**
* {@inheritdoc}
*/
public function providerSource() {
// The source data.
$tests[0]['source_data']['block'] = [
[
'bid' => 1,
'module' => 'system',
'delta' => 'main',
'theme' => 'bartik',
'status' => 1,
'weight' => 0,
'region' => 'content',
'custom' => '0',
'visibility' => 0,
'pages' => '',
'title' => '',
'cache' => -1,
'i18n_mode' => 0,
],
[
'bid' => 2,
'module' => 'system',
'delta' => 'navigation',
'theme' => 'bartik',
'status' => 1,
'weight' => 0,
'region' => 'sidebar_first',
'custom' => '0',
'visibility' => 0,
'pages' => '',
'title' => 'Navigation',
'cache' => -1,
'i18n_mode' => 1,
],
];
$tests[0]['source_data']['block_role'] = [
[
'module' => 'block',
'delta' => 1,
'rid' => 2,
],
[
'module' => 'block',
'delta' => 2,
'rid' => 2,
],
[
'module' => 'block',
'delta' => 2,
'rid' => 100,
],
];
$tests[0]['source_data']['i18n_string'] = [
[
'lid' => 1,
'textgroup' => 'block',
'context' => '1',
'objectid' => 'navigation',
'type' => 'system',
'property' => 'title',
'objectindex' => 0,
'format' => '',
],
];
$tests[0]['source_data']['locales_target'] = [
[
'lid' => 1,
'translation' => 'fr - Navigation',
'language' => 'fr',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
];
$tests[0]['source_data']['role'] = [
[
'rid' => 2,
'name' => 'authenticated user',
],
];
$tests[0]['source_data']['system'] = [
[
'filename' => 'modules/system/system.module',
'name' => 'system',
'type' => 'module',
'owner' => '',
'status' => '1',
'throttle' => '0',
'bootstrap' => '0',
'schema_version' => '7055',
'weight' => '0',
'info' => 'a:0:{}',
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'bid' => 2,
'module' => 'system',
'delta' => 'navigation',
'theme' => 'bartik',
'status' => 1,
'weight' => 0,
'region' => 'sidebar_first',
'custom' => '0',
'visibility' => 0,
'pages' => '',
'title' => 'Navigation',
'cache' => -1,
'i18n_mode' => 1,
'lid' => 1,
'translation' => 'fr - Navigation',
'language' => 'fr',
'plid' => 0,
'plural' => 0,
'i18n_status' => 0,
],
];
return $tests;
}
}

View file

@ -293,8 +293,8 @@ class BlockContentAccessHandlerTest extends KernelTestBase {
'forbidden',
],
];
return $cases;
}
return $cases;
}
}

View file

@ -46,6 +46,17 @@ class MigrateCommentTypeTest extends MigrateDrupal7TestBase {
* Tests the migrated comment types.
*/
public function testMigration() {
$comment_fields = [
'comment' => 'Default comment setting',
'comment_default_mode' => 'Default display mode',
'comment_default_per_page' => 'Default comments per page',
'comment_anonymous' => 'Anonymous commenting',
'comment_subject_field' => 'Comment subject field',
'comment_preview' => 'Preview comment',
'comment_form_location' => 'Location of comment submission form',
];
$this->assertArraySubset($comment_fields, $this->migration->getSourcePlugin()->fields());
$this->assertEntity('comment_node_article', 'Article comment');
$this->assertEntity('comment_node_blog', 'Blog entry comment');
$this->assertEntity('comment_node_book', 'Book page comment');

View file

@ -184,11 +184,16 @@ class EntityOperations implements ContainerInjectionInterface {
// Sync translations.
if ($entity->getEntityType()->hasKey('langcode')) {
$entity_langcode = $entity->language()->getId();
if (!$content_moderation_state->hasTranslation($entity_langcode)) {
$content_moderation_state->addTranslation($entity_langcode);
if ($entity->isDefaultTranslation()) {
$content_moderation_state->langcode = $entity_langcode;
}
if ($content_moderation_state->language()->getId() !== $entity_langcode) {
$content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
else {
if (!$content_moderation_state->hasTranslation($entity_langcode)) {
$content_moderation_state->addTranslation($entity_langcode);
}
if ($content_moderation_state->language()->getId() !== $entity_langcode) {
$content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
}
}
}

View file

@ -93,7 +93,7 @@ class ModerationStateFieldItemList extends FieldItemList {
if ($entity->getEntityType()->hasKey('langcode')) {
$langcode = $entity->language()->getId();
if (!$content_moderation_state->hasTranslation($langcode)) {
$content_moderation_state->addTranslation($langcode);
$content_moderation_state->addTranslation($langcode, $content_moderation_state->toArray());
}
if ($content_moderation_state->language()->getId() !== $langcode) {
$content_moderation_state = $content_moderation_state->getTranslation($langcode);

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
/**
* Test content_moderation functionality with content_translation.
*
* @group content_moderation
*/
class ModerationContentTranslationTest extends BrowserTestBase {
use ContentModerationTestTrait;
/**
* A user with permission to bypass access content.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'node',
'locale',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->rootUser);
// Create an Article content type.
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save();
$edit = [
'predefined_langcode' => 'fr',
];
$this->drupalPostForm('admin/config/regional/language/add', $edit, 'Add language');
// Enable content translation on articles.
$this->drupalGet('admin/config/regional/content-language');
$edit = [
'entity_types[node]' => TRUE,
'settings[node][article][translatable]' => TRUE,
'settings[node][article][settings][language][language_alterable]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
// Adding languages requires a container rebuild in the test running
// environment so that multilingual services are used.
$this->rebuildContainer();
}
/**
* Tests existing translations being edited after enabling content moderation.
*/
public function testModerationWithExistingContent() {
// Create a published article in English.
$edit = [
'title[0][value]' => 'Published English node',
'langcode[0][value]' => 'en',
];
$this->drupalPostForm('node/add/article', $edit, 'Save');
$this->assertSession()->pageTextContains('Article Published English node has been created.');
$english_node = $this->drupalGetNodeByTitle('Published English node');
// Add a French translation.
$this->drupalGet('node/' . $english_node->id() . '/translations');
$this->clickLink('Add');
$edit = [
'title[0][value]' => 'Published French node',
];
$this->drupalPostForm(NULL, $edit, 'Save (this translation)');
$this->assertSession()->pageTextContains('Article Published French node has been updated.');
// Install content moderation and enable moderation on Article node type.
\Drupal::service('module_installer')->install(['content_moderation']);
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
$workflow->save();
$this->drupalLogin($this->rootUser);
// Edit the English node.
$this->drupalGet('node/' . $english_node->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
$edit = [
'title[0][value]' => 'Published English new node',
];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Article Published English new node has been updated.');
// Edit the French translation.
$this->drupalGet('fr/node/' . $english_node->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
$edit = [
'title[0][value]' => 'Published French new node',
];
$this->drupalPostForm(NULL, $edit, 'Save (this translation)');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Article Published French new node has been updated.');
}
}

View file

@ -296,7 +296,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Create a French translation.
$french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
$french_node->setUnpublished();
// Revision 1 (fr).
// Revision 2 (fr).
$french_node->save();
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
$this->assertEquals('draft', $french_node->moderation_state->value);
@ -305,7 +305,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Move English node to create another draft.
$english_node = $this->reloadEntity($english_node);
$english_node->moderation_state->value = 'draft';
// Revision 2 (en, fr).
// Revision 3 (en, fr).
$english_node->save();
$english_node = $this->reloadEntity($english_node);
$this->assertEquals('draft', $english_node->moderation_state->value);
@ -316,7 +316,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Publish the French node.
$french_node->moderation_state->value = 'published';
// Revision 3 (en, fr).
// Revision 4 (en, fr).
$french_node->save();
$french_node = $this->reloadEntity($french_node)->getTranslation('fr');
$this->assertTrue($french_node->isPublished());
@ -327,7 +327,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Publish the English node.
$english_node->moderation_state->value = 'published';
// Revision 4 (en, fr).
// Revision 5 (en, fr).
$english_node->save();
$english_node = $this->reloadEntity($english_node);
$this->assertTrue($english_node->isPublished());
@ -336,15 +336,15 @@ class ContentModerationStateTest extends KernelTestBase {
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
$this->assertTrue($french_node->isPublished());
$french_node->moderation_state->value = 'draft';
// Revision 5 (en, fr).
// Revision 6 (en, fr).
$french_node->save();
$french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
$french_node = $this->reloadEntity($english_node, 6)->getTranslation('fr');
$this->assertFalse($french_node->isPublished());
$this->assertTrue($french_node->getTranslation('en')->isPublished());
// Republish the French node.
$french_node->moderation_state->value = 'published';
// Revision 6 (en, fr).
// Revision 7 (en, fr).
$french_node->save();
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
$this->assertTrue($french_node->isPublished());
@ -353,7 +353,7 @@ class ContentModerationStateTest extends KernelTestBase {
$content_moderation_state = ContentModerationState::load(1);
$content_moderation_state->set('moderation_state', 'draft');
$content_moderation_state->setNewRevision(TRUE);
// Revision 7 (en, fr).
// Revision 8 (en, fr).
$content_moderation_state->save();
$english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
@ -366,12 +366,12 @@ class ContentModerationStateTest extends KernelTestBase {
$content_moderation_state = $content_moderation_state->getTranslation('fr');
$content_moderation_state->set('moderation_state', 'draft');
$content_moderation_state->setNewRevision(TRUE);
// Revision 8 (en, fr).
// Revision 9 (en, fr).
$content_moderation_state->save();
$english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
$this->assertEquals('draft', $english_node->moderation_state->value);
$french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
$french_node = $this->reloadEntity($english_node, '9')->getTranslation('fr');
$this->assertEquals('draft', $french_node->moderation_state->value);
// Switching the moderation state to an unpublished state should update the
// entity.
@ -380,7 +380,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Get the default english node.
$english_node = $this->reloadEntity($english_node);
$this->assertTrue($english_node->isPublished());
$this->assertEquals(6, $english_node->getRevisionId());
$this->assertEquals(7, $english_node->getRevisionId());
}
/**
@ -416,25 +416,83 @@ class ContentModerationStateTest extends KernelTestBase {
/**
* Tests that entities with special languages can be moderated.
*
* @dataProvider moderationWithSpecialLanguagesTestCases
*/
public function testModerationWithSpecialLanguages() {
public function testModerationWithSpecialLanguages($original_language, $updated_language) {
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
$workflow->save();
// Create a test entity.
$entity = EntityTestRev::create([
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'langcode' => $original_language,
]);
$entity->save();
$this->assertEquals('draft', $entity->moderation_state->value);
$entity->moderation_state->value = 'published';
$entity->langcode = $updated_language;
$entity->save();
$this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
}
/**
* Test cases for ::testModerationWithSpecialLanguages().
*/
public function moderationWithSpecialLanguagesTestCases() {
return [
'Not specified to not specified' => [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
'English to not specified' => [
'en',
LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
'Not specified to english' => [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
'en',
],
];
}
/**
* Test changing the language of content without adding a translation.
*/
public function testChangingContentLangcode() {
ConfigurableLanguage::createFromLangcode('fr')->save();
NodeType::create([
'type' => 'test_type',
])->save();
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
$workflow->save();
$entity = Node::create([
'title' => 'Test node',
'langcode' => 'en',
'type' => 'test_type',
]);
$entity->save();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertCount(1, $entity->getTranslationLanguages());
$this->assertCount(1, $content_moderation_state->getTranslationLanguages());
$this->assertEquals('en', $entity->langcode->value);
$this->assertEquals('en', $content_moderation_state->langcode->value);
$entity->langcode = 'fr';
$entity->save();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertCount(1, $entity->getTranslationLanguages());
$this->assertCount(1, $content_moderation_state->getTranslationLanguages());
$this->assertEquals('fr', $entity->langcode->value);
$this->assertEquals('fr', $content_moderation_state->langcode->value);
}
/**
* Tests that a non-translatable entity type with a langcode can be moderated.
*/

View file

@ -3,9 +3,11 @@
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
@ -64,6 +66,8 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
$this->testNode->save();
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
$this->testNode = Node::load($this->testNode->id());
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
@ -332,4 +336,37 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
];
}
/**
* Test the field item list when used with existing unmoderated content.
*/
public function testWithExistingUnmoderatedContent() {
$node = Node::create([
'title' => 'Test title',
'type' => 'unmoderated',
]);
$node->save();
$translation = $node->addTranslation('de', $node->toArray());
$translation->title = 'Translated';
$translation->save();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated');
$workflow->save();
// After enabling moderation, both the original node and translation should
// have a published moderation state.
$node = Node::load($node->id());
$translation = $node->getTranslation('de');
$this->assertEquals('published', $node->moderation_state->value);
$this->assertEquals('published', $translation->moderation_state->value);
// After the node has been updated, both the original node and translation
// should still have a value.
$node->title = 'Updated title';
$node->save();
$translation = $node->getTranslation('de');
$this->assertEquals('published', $node->moderation_state->value);
$this->assertEquals('published', $translation->moderation_state->value);
}
}

View file

@ -0,0 +1,44 @@
id: d6_taxonomy_term_localized_translation
label: Taxonomy localized term translations
migration_tags:
- Drupal 6
- Content
- Multilingual
source:
plugin: d6_term_localized_translation
translations: true
process:
# If you are using this file to build a custom migration consider removing
# the tid field to allow incremental migrations.
tid: tid
langcode: language
vid:
plugin: migration
migration: d6_taxonomy_vocabulary
source: vid
name:
-
plugin: callback
source:
- name_translated
- name
callable: array_filter
-
plugin: callback
callable: current
description:
-
plugin: callback
source:
- description_translated
- description
callable: array_filter
-
plugin: callback
callable: current
destination:
plugin: entity:taxonomy_term
translations: true
migration_dependencies:
required:
- d6_taxonomy_term

View file

@ -0,0 +1,77 @@
id: d7_block_translation
label: Block translation
migration_tags:
- Drupal 7
- Configuration
- Multilingual
source:
plugin: d7_block_translation
constants:
dest_label: 'settings/label'
process:
multilingual:
plugin: skip_on_empty
source: i18n_mode
method: row
langcode: language
property: constants/dest_label
translation: translation
id:
-
plugin: migration_lookup
migration: d7_block
source:
- module
- delta
-
plugin: skip_on_empty
method: row
# The plugin process is copied from d7_block.yml
plugin:
-
plugin: static_map
bypass: true
source:
- module
- delta
map:
book:
navigation: book_navigation
comment:
recent: views_block:comments_recent-block_1
forum:
active: forum_active_block
new: forum_new_block
# locale:
# 0: language_block
node:
syndicate: node_syndicate_block
search:
form: search_form_block
statistics:
popular: statistics_popular_block
system:
main: system_main_block
'powered-by': system_powered_by_block
user:
login: user_login_block
# 1: system_menu_block:tools
new: views_block:who_s_new-block_1
online: views_block:who_s_online-who_s_online_block
-
plugin: block_plugin_id
-
plugin: skip_on_empty
method: row
# The theme process is copied from d7_block.yml
theme:
plugin: block_theme
source:
- theme
- default_theme
- admin_theme
destination:
plugin: entity:block
migration_dependencies:
optional:
- d7_block

View file

@ -108,7 +108,12 @@ class MigrateTaxonomyTermTranslationTest extends MigrateDrupal6TestBase {
$this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
$term = $this->treeData[$vid][$tid];
$this->assertEquals($parent_ids, array_filter($term->parents), "Term $tid has correct parents in taxonomy tree");
// PostgreSQL, MySQL and SQLite may not return the parent terms in the same
// order so sort before testing.
sort($parent_ids);
$actual_terms = array_filter($term->parents);
sort($actual_terms);
$this->assertEquals($parent_ids, $actual_terms, "Term $tid has correct parents in taxonomy tree");
}
/**

View file

@ -0,0 +1,147 @@
<?php
namespace Drupal\Tests\datetime\Functional\Views;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\views\Tests\ViewTestData;
/**
* Tests Views filters for datetime fields.
*
* @group datetime
*/
class FilterDateTest extends BrowserTestBase {
/**
* Name of the field.
*
* Note, this is used in the default test view.
*
* @var string
*/
protected $fieldName = 'field_date';
/**
* Nodes to test.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes = [];
/**
* {@inheritdoc}
*/
public static $modules = [
'datetime',
'datetime_test',
'node',
'views',
'views_ui',
];
/**
* {@inheritdoc}
*/
public static $testViews = ['test_filter_datetime'];
/**
* {@inheritdoc}
*
* Create nodes with relative dates of yesterday, today, and tomorrow.
*/
protected function setUp() {
parent::setUp();
$now = \Drupal::time()->getRequestTime();
$admin_user = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($admin_user);
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Add a date field to page nodes.
$fieldStorage = FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
]);
$fieldStorage->save();
$field = FieldConfig::create([
'field_storage' => $fieldStorage,
'bundle' => 'page',
'required' => TRUE,
]);
$field->save();
// Create some nodes.
$dates = [
// Tomorrow.
DrupalDateTime::createFromTimestamp($now + 86400, DateTimeItemInterface::STORAGE_TIMEZONE)->format(DateTimeItemInterface::DATE_STORAGE_FORMAT),
// Today.
DrupalDateTime::createFromTimestamp($now, DateTimeItemInterface::STORAGE_TIMEZONE)->format(DateTimeItemInterface::DATE_STORAGE_FORMAT),
// Yesterday.
DrupalDateTime::createFromTimestamp($now - 86400, DateTimeItemInterface::STORAGE_TIMEZONE)->format(DateTimeItemInterface::DATE_STORAGE_FORMAT),
];
$this->nodes = [];
foreach ($dates as $date) {
$this->nodes[] = $this->drupalCreateNode([
$this->fieldName => [
'value' => $date,
],
]);
}
// Add a node where the date field is empty.
$this->nodes[] = $this->drupalCreateNode();
// Views needs to be aware of the new field.
$this->container->get('views.views_data')->clear();
// Load test views.
ViewTestData::createTestViews(get_class($this), ['datetime_test']);
}
/**
* Tests exposed grouped filters.
*/
public function testExposedGroupedFilters() {
// Expose the empty and not empty operators in a grouped filter.
$this->drupalPostForm('admin/structure/views/nojs/handler/test_filter_datetime/default/filter/' . $this->fieldName . '_value', [], t('Expose filter'));
$this->drupalPostForm(NULL, [], 'Grouped filters');
$edit = [];
$edit['options[group_info][group_items][1][title]'] = 'empty';
$edit['options[group_info][group_items][1][operator]'] = 'empty';
$edit['options[group_info][group_items][2][title]'] = 'not empty';
$edit['options[group_info][group_items][2][operator]'] = 'not empty';
$this->drupalPostForm(NULL, $edit, 'Apply');
// Test that the exposed filter works as expected.
$path = 'test_filter_datetime-path';
$this->drupalPostForm('admin/structure/views/view/test_filter_datetime/edit', [], 'Add Page');
$this->drupalPostForm('admin/structure/views/nojs/display/test_filter_datetime/page_1/path', ['path' => $path], 'Apply');
$this->drupalPostForm(NULL, [], t('Save'));
$this->drupalGet($path);
// Filter the Preview by 'empty'.
$this->getSession()->getPage()->findField($this->fieldName . '_value')->selectOption(1);
$this->getSession()->getPage()->pressButton('Apply');
$results = $this->cssSelect('.view-content .field-content');
$this->assertEquals(1, count($results));
// Filter the Preview by 'not empty'.
$this->getSession()->getPage()->findField($this->fieldName . '_value')->selectOption(2);
$this->getSession()->getPage()->pressButton('Apply');
$results = $this->cssSelect('.view-content .field-content');
$this->assertEquals(3, count($results));
}
}

View file

@ -71,6 +71,15 @@ class FilterDateTest extends DateTimeHandlerTestBase {
$node->save();
$this->nodes[] = $node;
}
// Add a node where the date field is empty.
$node = Node::create([
'title' => $this->randomMachineName(8),
'type' => 'page',
'field_date' => [],
]);
$node->save();
$this->nodes[] = $node;
}
/**
@ -130,6 +139,30 @@ class FilterDateTest extends DateTimeHandlerTestBase {
];
$this->assertIdenticalResultset($view, $expected_result, $this->map);
$view->destroy();
// Test the empty operator.
$view->initHandlers();
$view->filter[$field]->operator = 'empty';
$view->setDisplay('default');
$this->executeView($view);
$expected_result = [
['nid' => $this->nodes[3]->id()],
];
$this->assertIdenticalResultset($view, $expected_result, $this->map);
$view->destroy();
// Test the not empty operator.
$view->initHandlers();
$view->filter[$field]->operator = 'not empty';
$view->setDisplay('default');
$this->executeView($view);
$expected_result = [
['nid' => $this->nodes[0]->id()],
['nid' => $this->nodes[1]->id()],
['nid' => $this->nodes[2]->id()],
];
$this->assertIdenticalResultset($view, $expected_result, $this->map);
$view->destroy();
}
}

View file

@ -1,20 +1,21 @@
<?php
namespace Drupal\field\Tests\EntityReference;
namespace Drupal\Tests\field\Functional\EntityReference;
use Drupal\field\Entity\FieldConfig;
use Behat\Mink\Element\NodeElement;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field_ui\Tests\FieldUiTestTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\Node;
use Drupal\simpletest\WebTestBase;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests for the administrative UI.
*
* @group entity_reference
*/
class EntityReferenceAdminTest extends WebTestBase {
class EntityReferenceAdminTest extends BrowserTestBase {
use FieldUiTestTrait;
@ -65,145 +66,6 @@ class EntityReferenceAdminTest extends WebTestBase {
*/
public function testFieldAdminHandler() {
$bundle_path = 'admin/structure/types/manage/' . $this->type;
// First step: 'Add new field' on the 'Manage fields' page.
$this->drupalGet($bundle_path . '/fields/add-field');
// Check if the commonly referenced entity types appear in the list.
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
$this->drupalPostForm(NULL, [
'label' => 'Test label',
'field_name' => 'test',
'new_storage_type' => 'entity_reference',
], t('Save and continue'));
// Node should be selected by default.
$this->assertFieldByName('settings[target_type]', 'node');
// Check that all entity types can be referenced.
$this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityManager()->getDefinitions()));
// Second step: 'Field settings' form.
$this->drupalPostForm(NULL, [], t('Save field settings'));
// The base handler should be selected by default.
$this->assertFieldByName('settings[handler]', 'default:node');
// The base handler settings should be displayed.
$entity_type_id = 'node';
// Check that the type label is correctly displayed.
$this->assertText('Content type');
$bundles = $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id);
foreach ($bundles as $bundle_name => $bundle_info) {
$this->assertFieldByName('settings[handler_settings][target_bundles][' . $bundle_name . ']');
}
reset($bundles);
// Test the sort settings.
// Option 0: no sort.
$this->assertFieldByName('settings[handler_settings][sort][field]', '_none');
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
// Option 1: sort by field.
$this->drupalPostAjaxForm(NULL, ['settings[handler_settings][sort][field]' => 'nid'], 'settings[handler_settings][sort][field]');
$this->assertFieldByName('settings[handler_settings][sort][direction]', 'ASC');
// Test that a non-translatable base field is a sort option.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='nid']");
// Test that a translatable base field is a sort option.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='title']");
// Test that a configurable field is a sort option.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='body.value']");
// Set back to no sort.
$this->drupalPostAjaxForm(NULL, ['settings[handler_settings][sort][field]' => '_none'], 'settings[handler_settings][sort][field]');
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
// Third step: confirm.
$this->drupalPostForm(NULL, [
'required' => '1',
'settings[handler_settings][target_bundles][' . key($bundles) . ']' => key($bundles),
], t('Save settings'));
// Check that the field appears in the overview form.
$this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="field-test"]/td[1]', 'Test label', 'Field was created and appears in the overview page.');
// Check that the field settings form can be submitted again, even when the
// field is required.
// The first 'Edit' link is for the Body field.
$this->clickLink(t('Edit'), 1);
$this->drupalPostForm(NULL, [], t('Save settings'));
// Switch the target type to 'taxonomy_term' and check that the settings
// specific to its selection handler are displayed.
$field_name = 'node.' . $this->type . '.field_test';
$edit = [
'settings[target_type]' => 'taxonomy_term',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$this->assertFieldByName('settings[handler_settings][auto_create]');
// Switch the target type to 'user' and check that the settings specific to
// its selection handler are displayed.
$field_name = 'node.' . $this->type . '.field_test';
$edit = [
'settings[target_type]' => 'user',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$this->assertFieldByName('settings[handler_settings][filter][type]', '_none');
// Switch the target type to 'node'.
$field_name = 'node.' . $this->type . '.field_test';
$edit = [
'settings[target_type]' => 'node',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
// Try to select the views handler.
$edit = [
'settings[handler]' => 'views',
];
$this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
$this->assertRaw(t('No eligible views were found. <a href=":create">Create a view</a> with an <em>Entity Reference</em> display, or add such a display to an <a href=":existing">existing view</a>.', [
':create' => \Drupal::url('views_ui.add'),
':existing' => \Drupal::url('entity.view.collection'),
]));
$this->drupalPostForm(NULL, $edit, t('Save settings'));
// If no eligible view is available we should see a message.
$this->assertText('The views entity selection mode requires a view.');
// Enable the entity_reference_test module which creates an eligible view.
$this->container->get('module_installer')->install(['entity_reference_test']);
$this->resetAll();
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
$edit = [
'settings[handler_settings][view][view_and_display]' => 'test_entity_reference:entity_reference_1',
];
$this->drupalPostForm(NULL, $edit, t('Save settings'));
$this->assertResponse(200);
// Switch the target type to 'entity_test'.
$edit = [
'settings[target_type]' => 'entity_test',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$edit = [
'settings[handler]' => 'views',
];
$this->drupalPostAjaxForm($bundle_path . '/fields/' . $field_name, $edit, 'settings[handler]');
$edit = [
'required' => FALSE,
'settings[handler_settings][view][view_and_display]' => 'test_entity_reference_entity_test:entity_reference_1',
];
$this->drupalPostForm(NULL, $edit, t('Save settings'));
$this->assertResponse(200);
// Create a new view and display it as a entity reference.
$edit = [
'id' => 'node_test_view',
@ -253,7 +115,7 @@ class EntityReferenceAdminTest extends WebTestBase {
$edit = [
'settings[handler]' => 'views',
];
$this->drupalPostAjaxForm(NULL, $edit, 'settings[handler]');
$this->drupalPostForm(NULL, $edit, t('Change handler'));
$edit = [
'required' => FALSE,
'settings[handler_settings][view][view_and_display]' => 'node_test_view:entity_reference_1',
@ -275,7 +137,7 @@ class EntityReferenceAdminTest extends WebTestBase {
// Try to add a new node and fill the entity reference field.
$this->drupalGet('node/add/' . $this->type);
$result = $this->xpath('//input[@name="field_test_entity_ref_field[0][target_id]" and contains(@data-autocomplete-path, "/entity_reference_autocomplete/node/views/")]');
$target_url = $this->getAbsoluteUrl($result[0]['data-autocomplete-path']);
$target_url = $this->getAbsoluteUrl($result[0]->getAttribute('data-autocomplete-path'));
$this->drupalGet($target_url, ['query' => ['q' => 'Foo']]);
$this->assertRaw($node1->getTitle() . ' (' . $node1->id() . ')');
$this->assertRaw($node2->getTitle() . ' (' . $node2->id() . ')');
@ -446,7 +308,8 @@ class EntityReferenceAdminTest extends WebTestBase {
'settings[handler_settings][target_bundles][' . $vocabularies[1]->id() . ']' => TRUE,
];
// Enable the second vocabulary as a target bundle.
$this->drupalPostAjaxForm($path, $edit, key($edit));
$this->drupalPostForm($path, $edit, 'Save settings');
$this->drupalGet($path);
// Expect a select element with the two vocabularies as options.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][auto_create_bundle]']/option[@value='" . $vocabularies[0]->id() . "']");
$this->assertFieldByXPath("//select[@name='settings[handler_settings][auto_create_bundle]']/option[@value='" . $vocabularies[1]->id() . "']");
@ -513,49 +376,23 @@ class EntityReferenceAdminTest extends WebTestBase {
* The field name.
* @param array $expected_options
* An array of expected options.
*
* @return bool
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertFieldSelectOptions($name, array $expected_options) {
$xpath = $this->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
$fields = $this->xpath($xpath);
if ($fields) {
$field = $fields[0];
$options = $this->getAllOptionsList($field);
$options = $field->findAll('xpath', 'option');
array_walk($options, function (NodeElement &$option) {
$option = $option->getValue();
});
sort($options);
sort($expected_options);
return $this->assertIdentical($options, $expected_options);
$this->assertIdentical($options, $expected_options);
}
else {
return $this->fail('Unable to find field ' . $name);
$this->fail('Unable to find field ' . $name);
}
}
/**
* Extracts all options from a select element.
*
* @param \SimpleXMLElement $element
* The select element field information.
*
* @return array
* An array of option values as strings.
*/
protected function getAllOptionsList(\SimpleXMLElement $element) {
$options = [];
// Add all options items.
foreach ($element->option as $option) {
$options[] = (string) $option['value'];
}
// Loops trough all the option groups
foreach ($element->optgroup as $optgroup) {
$options = array_merge($this->getAllOptionsList($optgroup), $options);
}
return $options;
}
}

View file

@ -0,0 +1,244 @@
<?php
namespace Drupal\Tests\field\FunctionalJavascript\EntityReference;
use Behat\Mink\Element\NodeElement;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests for the administrative UI.
*
* @group entity_reference
*/
class EntityReferenceAdminTest extends WebDriverTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
* Enable path module to ensure that the selection handler does not fail for
* entities with a path field.
*
* @var array
*/
public static $modules = ['node', 'field_ui', 'path', 'taxonomy', 'block', 'views_ui'];
/**
* The name of the content type created for testing purposes.
*
* @var string
*/
protected $type;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
// Create a content type, with underscores.
$type_name = strtolower($this->randomMachineName(8)) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
// Create test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer node fields',
'administer node display',
'administer views',
'create ' . $type_name . ' content',
'edit own ' . $type_name . ' content',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests the Entity Reference Admin UI.
*/
public function testFieldAdminHandler() {
$bundle_path = 'admin/structure/types/manage/' . $this->type;
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// First step: 'Add new field' on the 'Manage fields' page.
$this->drupalGet($bundle_path . '/fields/add-field');
// Check if the commonly referenced entity types appear in the list.
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
$page->findField('new_storage_type')->setValue('entity_reference');
$assert_session->waitForField('label')->setValue('Test');
$machine_name = $assert_session->waitForElement('xpath', '//*[@id="edit-label-machine-name-suffix"]/span[2]/span[contains(text(), "field_test")]');
$this->assertNotEmpty($machine_name);
$page->pressButton('Save and continue');
// Node should be selected by default.
$this->assertFieldByName('settings[target_type]', 'node');
// Check that all entity types can be referenced.
$this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityManager()->getDefinitions()));
// Second step: 'Field settings' form.
$this->drupalPostForm(NULL, [], t('Save field settings'));
// The base handler should be selected by default.
$this->assertFieldByName('settings[handler]', 'default:node');
// The base handler settings should be displayed.
$entity_type_id = 'node';
// Check that the type label is correctly displayed.
$assert_session->pageTextContains('Content type');
$bundles = $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id);
foreach ($bundles as $bundle_name => $bundle_info) {
$this->assertFieldByName('settings[handler_settings][target_bundles][' . $bundle_name . ']');
}
reset($bundles);
// Test the sort settings.
// Option 0: no sort.
$this->assertFieldByName('settings[handler_settings][sort][field]', '_none');
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
// Option 1: sort by field.
$page->findField('settings[handler_settings][sort][field]')->setValue('nid');
$assert_session->waitForField('settings[handler_settings][sort][direction]');
$this->assertFieldByName('settings[handler_settings][sort][direction]', 'ASC');
// Test that a non-translatable base field is a sort option.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='nid']");
// Test that a translatable base field is a sort option.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='title']");
// Test that a configurable field is a sort option.
$this->assertFieldByXPath("//select[@name='settings[handler_settings][sort][field]']/option[@value='body.value']");
// Set back to no sort.
$page->findField('settings[handler_settings][sort][field]')->setValue('_none');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNoFieldByName('settings[handler_settings][sort][direction]');
// Third step: confirm.
$page->findField('settings[handler_settings][target_bundles][' . key($bundles) . ']')->setValue(key($bundles));
$assert_session->assertWaitOnAjaxRequest();
$this->drupalPostForm(NULL, [
'required' => '1',
], t('Save settings'));
// Check that the field appears in the overview form.
$this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="field-test"]/td[1]', 'Test', 'Field was created and appears in the overview page.');
// Check that the field settings form can be submitted again, even when the
// field is required.
// The first 'Edit' link is for the Body field.
$this->clickLink(t('Edit'), 1);
$this->drupalPostForm(NULL, [], t('Save settings'));
// Switch the target type to 'taxonomy_term' and check that the settings
// specific to its selection handler are displayed.
$field_name = 'node.' . $this->type . '.field_test';
$edit = [
'settings[target_type]' => 'taxonomy_term',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$this->assertFieldByName('settings[handler_settings][auto_create]');
// Switch the target type to 'user' and check that the settings specific to
// its selection handler are displayed.
$field_name = 'node.' . $this->type . '.field_test';
$edit = [
'settings[target_type]' => 'user',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$this->assertFieldByName('settings[handler_settings][filter][type]', '_none');
// Switch the target type to 'node'.
$field_name = 'node.' . $this->type . '.field_test';
$edit = [
'settings[target_type]' => 'node',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
// Try to select the views handler.
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$page->findField('settings[handler]')->setValue('views');
$views_text = (string) new FormattableMarkup('No eligible views were found. <a href=":create">Create a view</a> with an <em>Entity Reference</em> display, or add such a display to an <a href=":existing">existing view</a>.', [
':create' => \Drupal::url('views_ui.add'),
':existing' => \Drupal::url('entity.view.collection'),
]);
$assert_session->waitForElement('xpath', '//a[contains(text(), "Create a view")]');
$assert_session->responseContains($views_text);
$this->drupalPostForm(NULL, [], t('Save settings'));
// If no eligible view is available we should see a message.
$assert_session->pageTextContains('The views entity selection mode requires a view.');
// Enable the entity_reference_test module which creates an eligible view.
$this->container->get('module_installer')
->install(['entity_reference_test']);
$this->resetAll();
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$page->findField('settings[handler]')->setValue('views');
$assert_session
->waitForField('settings[handler_settings][view][view_and_display]')
->setValue('test_entity_reference:entity_reference_1');
$this->drupalPostForm(NULL, [], t('Save settings'));
$assert_session->pageTextContains('Saved Test configuration.');
// Switch the target type to 'entity_test'.
$edit = [
'settings[target_type]' => 'entity_test',
];
$this->drupalPostForm($bundle_path . '/fields/' . $field_name . '/storage', $edit, t('Save field settings'));
$this->drupalGet($bundle_path . '/fields/' . $field_name);
$page->findField('settings[handler]')->setValue('views');
$assert_session
->waitForField('settings[handler_settings][view][view_and_display]')
->setValue('test_entity_reference_entity_test:entity_reference_1');
$edit = [
'required' => FALSE,
];
$this->drupalPostForm(NULL, $edit, t('Save settings'));
$assert_session->pageTextContains('Saved Test configuration.');
}
/**
* Checks if a select element contains the specified options.
*
* @param string $name
* The field name.
* @param array $expected_options
* An array of expected options.
*/
protected function assertFieldSelectOptions($name, array $expected_options) {
$xpath = $this->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
$fields = $this->xpath($xpath);
if ($fields) {
$field = $fields[0];
$options = $field->findAll('xpath', 'option');
$optgroups = $field->findAll('xpath', 'optgroup');
foreach ($optgroups as $optgroup) {
$options = array_merge($options, $optgroup->findAll('xpath', 'option'));
}
array_walk($options, function (NodeElement &$option) {
$option = $option->getAttribute('value');
});
sort($options);
sort($expected_options);
$this->assertIdentical($options, $expected_options);
}
else {
$this->fail('Unable to find field ' . $name);
}
}
}

View file

@ -24,6 +24,13 @@ class StringFormatterTest extends KernelTestBase {
*/
public static $modules = ['field', 'text', 'entity_test', 'system', 'filter', 'user'];
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var string
*/
@ -79,6 +86,8 @@ class StringFormatterTest extends KernelTestBase {
'settings' => [],
]);
$this->display->save();
$this->entityTypeManager = \Drupal::entityTypeManager();
}
/**
@ -145,7 +154,7 @@ class StringFormatterTest extends KernelTestBase {
$value2 = $this->randomMachineName();
$entity->{$this->fieldName}->value = $value2;
$entity->save();
$entity_new_revision = \Drupal::entityManager()->getStorage('entity_test_rev')->loadRevision($old_revision_id);
$entity_new_revision = $this->entityTypeManager->getStorage('entity_test_rev')->loadRevision($old_revision_id);
$this->renderEntityFields($entity, $this->display);
$this->assertLink($value2, 0);
@ -154,6 +163,19 @@ class StringFormatterTest extends KernelTestBase {
$this->renderEntityFields($entity_new_revision, $this->display);
$this->assertLink($value, 0);
$this->assertLinkByHref('/entity_test_rev/' . $entity_new_revision->id() . '/revision/' . $entity_new_revision->getRevisionId() . '/view');
// Check that linking to a revisionable entity works if the entity type does
// not specify a 'revision' link template.
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_rev');
$link_templates = $entity_type->getLinkTemplates();
unset($link_templates['revision']);
$entity_type->set('links', $link_templates);
\Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
$this->entityTypeManager->clearCachedDefinitions();
$this->renderEntityFields($entity_new_revision, $this->display);
$this->assertLink($value, 0);
$this->assertLinkByHref($entity->url('canonical'));
}
}

View file

@ -176,7 +176,7 @@ class ManageFieldsFunctionalTest extends BrowserTestBase {
/**
* Tests adding a new field.
*
* @todo Assert properties can bet set in the form and read back in
* @todo Assert properties can be set in the form and read back in
* $field_storage and $fields.
*/
public function createField() {

View file

@ -23,7 +23,7 @@ use Drupal\Core\Template\Attribute;
/**
* The regex pattern used when checking for insecure file types.
*/
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(php|pl|py|cgi|asp|js)(\.|$)/i');
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');
// Load all Field module hooks for File.
require_once __DIR__ . '/file.field.inc';
@ -859,7 +859,6 @@ function _file_save_upload_from_form(array $element, FormStateInterface $form_st
* @todo: move this logic to a service in https://www.drupal.org/node/2244513.
*/
function file_save_upload($form_field_name, $validators = [], $destination = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) {
$user = \Drupal::currentUser();
static $upload_cache;
$all_files = \Drupal::request()->files->get('files', []);
@ -887,176 +886,7 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
$files = [];
foreach ($uploaded_files as $i => $file_info) {
// Check for file upload errors and return FALSE for this file if a lower
// level system error occurred. For a complete list of errors:
// See http://php.net/manual/features.file-upload.errors.php.
switch ($file_info->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
\Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
$files[$i] = FALSE;
continue;
case UPLOAD_ERR_PARTIAL:
case UPLOAD_ERR_NO_FILE:
\Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
$files[$i] = FALSE;
continue;
case UPLOAD_ERR_OK:
// Final check that this is a valid upload, if it isn't, use the
// default error handler.
if (is_uploaded_file($file_info->getRealPath())) {
break;
}
// Unknown error
default:
\Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
$files[$i] = FALSE;
continue;
}
// Begin building file entity.
$values = [
'uid' => $user->id(),
'status' => 0,
'filename' => $file_info->getClientOriginalName(),
'uri' => $file_info->getRealPath(),
'filesize' => $file_info->getSize(),
];
$values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
$file = File::create($values);
$extensions = '';
if (isset($validators['file_validate_extensions'])) {
if (isset($validators['file_validate_extensions'][0])) {
// Build the list of non-munged extensions if the caller provided them.
$extensions = $validators['file_validate_extensions'][0];
}
else {
// If 'file_validate_extensions' is set and the list is empty then the
// caller wants to allow any extension. In this case we have to remove the
// validator or else it will reject all extensions.
unset($validators['file_validate_extensions']);
}
}
else {
// No validator was provided, so add one using the default list.
// Build a default non-munged safe list for file_munge_filename().
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
$validators['file_validate_extensions'] = [];
$validators['file_validate_extensions'][0] = $extensions;
}
if (!empty($extensions)) {
// Munge the filename to protect against possible malicious extension
// hiding within an unknown file type (ie: filename.html.foo).
$file->setFilename(file_munge_filename($file->getFilename(), $extensions));
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
// The destination filename will also later be used to create the URI.
$file->setFilename($file->getFilename() . '.txt');
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
if (!empty($extensions)) {
$validators['file_validate_extensions'][0] .= ' txt';
\Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
}
}
// If the destination is not provided, use the temporary directory.
if (empty($destination)) {
$destination = 'temporary://';
}
// Assert that the destination contains a valid stream.
$destination_scheme = file_uri_scheme($destination);
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
\Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
$files[$i] = FALSE;
continue;
}
$file->source = $form_field_name;
// A file URI may already have a trailing slash or look like "public://".
if (substr($destination, -1) != '/') {
$destination .= '/';
}
$file->destination = file_destination($destination . $file->getFilename(), $replace);
// If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
// there's an existing file so we need to bail.
if ($file->destination === FALSE) {
\Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
$files[$i] = FALSE;
continue;
}
// Add in our check of the file name length.
$validators['file_validate_name_length'] = [];
// Call the validation functions specified by this function's caller.
$errors = file_validate($file, $validators);
// Check for errors.
if (!empty($errors)) {
$message = [
'error' => [
'#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
],
'item_list' => [
'#theme' => 'item_list',
'#items' => $errors,
],
];
// @todo Add support for render arrays in
// \Drupal\Core\Messenger\MessengerInterface::addMessage()?
// @see https://www.drupal.org/node/2505497.
\Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
$files[$i] = FALSE;
continue;
}
$file->setFileUri($file->destination);
if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
\Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
\Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
$files[$i] = FALSE;
continue;
}
// Set the permissions on the new file.
drupal_chmod($file->getFileUri());
// If we are replacing an existing file re-use its database record.
// @todo Do not create a new entity in order to update it. See
// https://www.drupal.org/node/2241865.
if ($replace == FILE_EXISTS_REPLACE) {
$existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
if (count($existing_files)) {
$existing = reset($existing_files);
$file->fid = $existing->id();
$file->setOriginalId($existing->id());
}
}
// If we made it this far it's safe to record this file in the database.
$file->save();
$files[$i] = $file;
// Allow an anonymous user who creates a non-public file to see it. See
// \Drupal\file\FileAccessControlHandler::checkAccess().
if ($user->isAnonymous() && $destination_scheme !== 'public') {
$session = \Drupal::request()->getSession();
$allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
$allowed_temp_files[$file->id()] = $file->id();
$session->set('anonymous_allowed_file_ids', $allowed_temp_files);
}
$files[$i] = _file_save_upload_single($file_info, $form_field_name, $validators, $destination, $replace);
}
// Add files to the cache.
@ -1065,6 +895,201 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
return isset($delta) ? $files[$delta] : $files;
}
/**
* Saves a file upload to a new location.
*
* @param \SplFileInfo $file_info
* The file upload to save.
* @param string $form_field_name
* A string that is the associative array key of the upload form element in
* the form array.
* @param array $validators
* (optional) An associative array of callback functions used to validate the
* file.
* @param bool $destination
* (optional) A string containing the URI that the file should be copied to.
* @param int $replace
* (optional) The replace behavior when the destination file already exists.
*
* @return \Drupal\file\FileInterface|false
* The created file entity or FALSE if the uploaded file not saved.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*
* @internal
* This method should only be called from file_save_upload(). Use that method
* instead.
*
* @see file_save_upload()
*/
function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $validators = [], $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
$user = \Drupal::currentUser();
// Check for file upload errors and return FALSE for this file if a lower
// level system error occurred. For a complete list of errors:
// See http://php.net/manual/features.file-upload.errors.php.
switch ($file_info->getError()) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
\Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
return FALSE;
case UPLOAD_ERR_PARTIAL:
case UPLOAD_ERR_NO_FILE:
\Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
return FALSE;
case UPLOAD_ERR_OK:
// Final check that this is a valid upload, if it isn't, use the
// default error handler.
if (is_uploaded_file($file_info->getRealPath())) {
break;
}
default:
// Unknown error
\Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
return FALSE;
}
// Begin building file entity.
$values = [
'uid' => $user->id(),
'status' => 0,
'filename' => $file_info->getClientOriginalName(),
'uri' => $file_info->getRealPath(),
'filesize' => $file_info->getSize(),
];
$values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
$file = File::create($values);
$extensions = '';
if (isset($validators['file_validate_extensions'])) {
if (isset($validators['file_validate_extensions'][0])) {
// Build the list of non-munged extensions if the caller provided them.
$extensions = $validators['file_validate_extensions'][0];
}
else {
// If 'file_validate_extensions' is set and the list is empty then the
// caller wants to allow any extension. In this case we have to remove the
// validator or else it will reject all extensions.
unset($validators['file_validate_extensions']);
}
}
else {
// No validator was provided, so add one using the default list.
// Build a default non-munged safe list for file_munge_filename().
$extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
$validators['file_validate_extensions'] = [];
$validators['file_validate_extensions'][0] = $extensions;
}
if (!empty($extensions)) {
// Munge the filename to protect against possible malicious extension
// hiding within an unknown file type (ie: filename.html.foo).
$file->setFilename(file_munge_filename($file->getFilename(), $extensions));
}
// Rename potentially executable files, to help prevent exploits (i.e. will
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
// The destination filename will also later be used to create the URI.
$file->setFilename($file->getFilename() . '.txt');
// The .txt extension may not be in the allowed list of extensions. We have
// to add it here or else the file upload will fail.
if (!empty($extensions)) {
$validators['file_validate_extensions'][0] .= ' txt';
\Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
}
}
// If the destination is not provided, use the temporary directory.
if (empty($destination)) {
$destination = 'temporary://';
}
// Assert that the destination contains a valid stream.
$destination_scheme = file_uri_scheme($destination);
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
\Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
return FALSE;
}
$file->source = $form_field_name;
// A file URI may already have a trailing slash or look like "public://".
if (substr($destination, -1) != '/') {
$destination .= '/';
}
$file->destination = file_destination($destination . $file->getFilename(), $replace);
// If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
// there's an existing file so we need to bail.
if ($file->destination === FALSE) {
\Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
return FALSE;
}
// Add in our check of the file name length.
$validators['file_validate_name_length'] = [];
// Call the validation functions specified by this function's caller.
$errors = file_validate($file, $validators);
// Check for errors.
if (!empty($errors)) {
$message = [
'error' => [
'#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
],
'item_list' => [
'#theme' => 'item_list',
'#items' => $errors,
],
];
// @todo Add support for render arrays in
// \Drupal\Core\Messenger\MessengerInterface::addMessage()?
// @see https://www.drupal.org/node/2505497.
\Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
return FALSE;
}
$file->setFileUri($file->destination);
if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
\Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
\Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
return FALSE;
}
// Set the permissions on the new file.
drupal_chmod($file->getFileUri());
// If we are replacing an existing file re-use its database record.
// @todo Do not create a new entity in order to update it. See
// https://www.drupal.org/node/2241865.
if ($replace == FILE_EXISTS_REPLACE) {
$existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
if (count($existing_files)) {
$existing = reset($existing_files);
$file->fid = $existing->id();
$file->setOriginalId($existing->id());
}
}
// If we made it this far it's safe to record this file in the database.
$file->save();
// Allow an anonymous user who creates a non-public file to see it. See
// \Drupal\file\FileAccessControlHandler::checkAccess().
if ($user->isAnonymous() && $destination_scheme !== 'public') {
$session = \Drupal::request()->getSession();
$allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
$allowed_temp_files[$file->id()] = $file->id();
$session->set('anonymous_allowed_file_ids', $allowed_temp_files);
}
return $file;
}
/**
* Determines the preferred upload progress implementation.
*

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests\file\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests multiple file upload.
*
* @group file
*/
class MultipleFileUploadTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['file'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$admin = $this->drupalCreateUser(['administer themes']);
$this->drupalLogin($admin);
}
/**
* Tests multiple file field with all file extensions.
*/
public function testMultipleFileFieldWithAllFileExtensions() {
$theme = 'test_theme_settings';
\Drupal::service('theme_handler')->install([$theme]);
$this->drupalGet("admin/appearance/settings/$theme");
$edit = [];
// Create few files with non-typical extensions.
foreach (['file1.wtf', 'file2.wtf'] as $i => $file) {
$file_path = $this->root . "/sites/default/files/simpletest/$file";
file_put_contents($file_path, 'File with non-default extension.', FILE_APPEND | LOCK_EX);
$edit["files[multi_file][$i]"] = $file_path;
}
// @todo: Replace after https://www.drupal.org/project/drupal/issues/2917885
$this->drupalGet("admin/appearance/settings/$theme");
$submit_xpath = $this->assertSession()->buttonExists('Save configuration')->getXpath();
$client = $this->getSession()->getDriver()->getClient();
$form = $client->getCrawler()->filterXPath($submit_xpath)->form();
$client->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $edit);
$page = $this->getSession()->getPage();
$this->assertNotContains('Only files with the following extensions are allowed', $page->getContent());
$this->assertContains('The configuration options have been saved.', $page->getContent());
$this->assertContains('file1.wtf', $page->getContent());
$this->assertContains('file2.wtf', $page->getContent());
}
}

View file

@ -90,11 +90,11 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
protected $initialized = FALSE;
/**
* Whether already in the process of language initialization.
* Whether language types are in the process of language initialization.
*
* @var bool
* @var bool[]
*/
protected $initializing = FALSE;
protected $initializing = [];
/**
* {@inheritdoc}
@ -213,12 +213,12 @@ class ConfigurableLanguageManager extends LanguageManager implements Configurabl
$this->negotiatedLanguages[$type] = $this->getDefaultLanguage();
if ($this->negotiator && $this->isMultilingual()) {
if (!$this->initializing) {
$this->initializing = TRUE;
if (!isset($this->initializing[$type])) {
$this->initializing[$type] = TRUE;
$negotiation = $this->negotiator->initializeType($type);
$this->negotiatedLanguages[$type] = reset($negotiation);
$this->negotiatedMethods[$type] = key($negotiation);
$this->initializing = FALSE;
unset($this->initializing[$type]);
}
// If the current interface language needs to be retrieved during
// initialization we return the system language. This way string

View file

@ -0,0 +1,189 @@
<?php
namespace Drupal\Tests\language\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Language Negotiation.
*
* Uses different negotiators for content and interface.
*
* @group language
*/
class ConfigurableLanguageManagerTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'language',
'content_translation',
'node',
'locale',
'block',
'system',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
/** @var \Drupal\user\UserInterface $user */
$user = $this->createUser([], '', TRUE);
$this->drupalLogin($user);
ConfigurableLanguage::createFromLangcode('es')->save();
// Create a page node type and make it translatable.
NodeType::create([
'type' => 'page',
'name' => t('Page'),
])->save();
$config = ContentLanguageSettings::loadByEntityTypeBundle('node', 'page');
$config->setDefaultLangcode('en')
->setLanguageAlterable(TRUE)
->save();
// Create a Node with title 'English' and translate it to Spanish.
$node = Node::create([
'type' => 'page',
'title' => 'English',
]);
$node->save();
$node->addTranslation('es', ['title' => 'Español']);
$node->save();
// Enable both language_interface and language_content language negotiation.
\Drupal::getContainer()->get('language_negotiator')->updateConfiguration([
'language_interface',
'language_content',
]);
// Set the preferred language of the user for admin pages to English.
$user->set('preferred_admin_langcode', 'en')->save();
// Make sure node edit pages are administration pages.
$this->config('node.settings')->set('use_admin_theme', '1')->save();
$this->container->get('router.builder')->rebuild();
// Place a Block with a translatable string on the page.
$this->placeBlock('system_powered_by_block', ['region' => 'content']);
// Load the Spanish Node page once, to register the translatable string.
$this->drupalGet('/es/node/1');
// Translate the Powered by string.
/** @var \Drupal\locale\StringStorageInterface $string_storage */
$string_storage = \Drupal::getContainer()->get('locale.storage');
$source = $string_storage->findString(['source' => 'Powered by <a href=":poweredby">Drupal</a>']);
$string_storage->createTranslation([
'lid' => $source->lid,
'language' => 'es',
'translation' => 'Funciona con ...',
])->save();
// Invalidate caches so that the new translation will be used.
Cache::invalidateTags(['rendered', 'locale']);
}
/**
* Test translation with URL and Preferred Admin Language negotiators.
*
* The interface language uses the preferred language for admin pages of the
* user and after that the URL. The Content uses just the URL.
*/
public function testUrlContentTranslationWithPreferredAdminLanguage() {
$assert_session = $this->assertSession();
// Set the interface language to use the preferred administration language
// and then the URL.
/** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */
$language_negotiator = \Drupal::getContainer()->get('language_negotiator');
$language_negotiator->saveConfiguration('language_interface', [
'language-user-admin' => 1,
'language-url' => 2,
'language-selected' => 3,
]);
// Set Content Language Negotiator to use just the URL.
$language_negotiator->saveConfiguration('language_content', [
'language-url' => 4,
'language-selected' => 5,
]);
// See if the full view of the node in english is present and the
// string in the Powered By Block is in English.
$this->drupalGet('/node/1');
$assert_session->pageTextContains('English');
$assert_session->pageTextContains('Powered by');
// Load the spanish node page again and see if both the node and the string
// are translated.
$this->drupalGet('/es/node/1');
$assert_session->pageTextContains('Español');
$assert_session->pageTextContains('Funciona con');
$assert_session->pageTextNotContains('Powered by');
// Check if the Powered by string is shown in English on an
// administration page, and the node content is shown in Spanish.
$this->drupalGet('/es/node/1/edit');
$assert_session->pageTextContains('Español');
$assert_session->pageTextContains('Powered by');
$assert_session->pageTextNotContains('Funciona con');
}
/**
* Test translation with URL and Session Language Negotiators.
*/
public function testUrlContentTranslationWithSessionLanguage() {
$assert_session = $this->assertSession();
/** @var \Drupal\language\LanguageNegotiatorInterface $language_negotiator */
$language_negotiator = \Drupal::getContainer()->get('language_negotiator');
// Set Interface Language Negotiator to Session.
$language_negotiator->saveConfiguration('language_interface', [
'language-session' => 1,
'language-url' => 2,
'language-selected' => 3,
]);
// Set Content Language Negotiator to URL.
$language_negotiator->saveConfiguration('language_content', [
'language-url' => 4,
'language-selected' => 5,
]);
// See if the full view of the node in english is present and the
// string in the Powered By Block is in English.
$this->drupalGet('/node/1');
$assert_session->pageTextContains('English');
$assert_session->pageTextContains('Powered by');
// The language session variable has not been set yet, so
// The string should be in Spanish.
$this->drupalGet('/es/node/1');
$assert_session->pageTextContains('Español');
$assert_session->pageTextNotContains('Powered by');
$assert_session->pageTextContains('Funciona con');
// Set the session language to Spanish but load the English node page.
$this->drupalGet('/node/1', ['query' => ['language' => 'es']]);
$assert_session->pageTextContains('English');
$assert_session->pageTextNotContains('Español');
$assert_session->pageTextContains('Funciona con');
$assert_session->pageTextNotContains('Powered by');
// Set the session language to English but load the node page in Spanish.
$this->drupalGet('/es/node/1', ['query' => ['language' => 'en']]);
$assert_session->pageTextNotContains('English');
$assert_session->pageTextContains('Español');
$assert_session->pageTextNotContains('Funciona con');
$assert_session->pageTextContains('Powered by');
}
}

View file

@ -45,9 +45,9 @@ class MigrateLanguageContentTaxonomyVocabularySettingsTest extends MigrateDrupal
// Set language to vocabulary.
$this->assertLanguageContentSettings($target_entity, 'vocabulary_2_i_1_', 'fr', FALSE, ['enabled' => FALSE]);
// Localize terms.
$this->assertLanguageContentSettings($target_entity, 'vocabulary_3_i_2_', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => TRUE]);
$this->assertLanguageContentSettings($target_entity, 'vocabulary_3_i_2_', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => FALSE]);
// None translation enabled.
$this->assertLanguageContentSettings($target_entity, 'vocabulary_name_much_longer_than', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
$this->assertLanguageContentSettings($target_entity, 'vocabulary_name_much_longer_than', LanguageInterface::LANGCODE_SITE_DEFAULT, TRUE, ['enabled' => TRUE]);
$this->assertLanguageContentSettings($target_entity, 'tags', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
$this->assertLanguageContentSettings($target_entity, 'forums', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);
$this->assertLanguageContentSettings($target_entity, 'type', LanguageInterface::LANGCODE_SITE_DEFAULT, FALSE, ['enabled' => FALSE]);

View file

@ -85,3 +85,32 @@
display: block;
padding-top: 0.55em;
}
#drupal-off-canvas .inline-block-create-button {
display: block;
padding: 24px;
padding-left: 44px;
font-size: 16px;
color: #eee;
background: url(../../../misc/icons/bebebe/plus.svg) transparent 16px no-repeat;
}
#drupal-off-canvas .inline-block-create-button,
#drupal-off-canvas .inline-block-list__item {
margin: 0 -20px;
background-color: #444;
}
#drupal-off-canvas .inline-block-create-button:hover,
#drupal-off-canvas .inline-block-list__item:hover {
background-color: #333;
}
#drupal-off-canvas .inline-block-list {
margin-bottom: 15px;
}
#drupal-off-canvas .inline-block-list__item {
display: block;
padding: 15px 0 15px 25px;
}

View file

@ -9,3 +9,5 @@ dependencies:
- drupal:contextual
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
- drupal:field_ui
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/3003610.
- drupal:block

View file

@ -19,6 +19,7 @@ use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
use Drupal\layout_builder\InlineBlockEntityOperations;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Implements hook_help().
@ -62,8 +63,8 @@ function layout_builder_entity_type_alter(array &$entity_types) {
function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
// Hides the Layout Builder field. It is rendered directly in
// \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
unset($form['fields']['layout_builder__layout']);
$key = array_search('layout_builder__layout', $form['#fields']);
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
if ($key !== FALSE) {
unset($form['#fields'][$key]);
}
@ -177,7 +178,7 @@ function layout_builder_cron() {
function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
// @todo Determine the 'inline_block' blocks should be allowed outside
// of layout_builder https://www.drupal.org/node/2979142.
if ($consumer !== 'layout_builder') {
if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
foreach ($definitions as $id => $definition) {
if ($definition['id'] === 'inline_block') {
unset($definitions[$id]);
@ -202,3 +203,21 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
}
return AccessResult::forbidden();
}
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
foreach ($definitions as $id => $definition) {
// Filter out any layout_builder definition with required contexts.
if ($definition['provider'] === 'layout_builder' && !empty($definition['context'])) {
/** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */
foreach ($definition['context'] as $context) {
if ($context->isRequired()) {
unset($definitions[$id]);
break;
}
}
}
}
}

View file

@ -80,6 +80,19 @@ layout_builder.add_block:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.choose_inline_block:
path: '/layout_builder/choose/inline-block/{section_storage_type}/{section_storage}/{delta}/{region}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList'
_title: 'Add a new Inline Block'
requirements:
_permission: 'configure any layout'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.update_block:
path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:

View file

@ -5,6 +5,7 @@ namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
@ -29,14 +30,24 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
protected $blockManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* ChooseBlockController constructor.
*
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(BlockManagerInterface $block_manager) {
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager) {
$this->blockManager = $block_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
@ -44,7 +55,8 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block')
$container->get('plugin.manager.block'),
$container->get('entity_type.manager')
);
}
@ -63,8 +75,43 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
public function build(SectionStorageInterface $section_storage, $delta, $region) {
$build['#title'] = $this->t('Choose a block');
$build['#type'] = 'container';
$build['#attributes']['class'][] = 'block-categories';
if ($this->entityTypeManager->hasDefinition('block_content_type') && $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple()) {
if (count($types) === 1) {
$type = reset($types);
$plugin_id = 'inline_block:' . $type->id();
if ($this->blockManager->hasDefinition($plugin_id)) {
$url = Url::fromRoute('layout_builder.add_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $plugin_id,
]);
}
}
else {
$url = Url::fromRoute('layout_builder.choose_inline_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]);
}
if (isset($url)) {
$build['add_block'] = [
'#type' => 'link',
'#url' => $url,
'#title' => $this->t('Create @entity_type', [
'@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
]),
'#attributes' => $this->getAjaxAttributes(),
];
$build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
}
}
$block_categories['#type'] = 'container';
$block_categories['#attributes']['class'][] = 'block-categories';
// @todo Explicitly cast delta to an integer, remove this in
// https://www.drupal.org/project/drupal/issues/2984509.
@ -75,35 +122,116 @@ class ChooseBlockController implements ContainerInjectionInterface {
'delta' => $delta,
'region' => $region,
]);
foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) {
$build[$category]['#type'] = 'details';
$build[$category]['#open'] = TRUE;
$build[$category]['#title'] = $category;
$build[$category]['links'] = [
'#theme' => 'links',
];
foreach ($blocks as $block_id => $block) {
$link = [
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
]
),
];
if ($this->isAjax()) {
$link['attributes']['class'][] = 'use-ajax';
$link['attributes']['data-dialog-type'][] = 'dialog';
$link['attributes']['data-dialog-renderer'][] = 'off_canvas';
}
$build[$category]['links']['#links'][] = $link;
$grouped_definitions = $this->blockManager->getGroupedDefinitions($definitions);
foreach ($grouped_definitions as $category => $blocks) {
$block_categories[$category]['#type'] = 'details';
$block_categories[$category]['#open'] = TRUE;
$block_categories[$category]['#title'] = $category;
$block_categories[$category]['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks);
}
$build['block_categories'] = $block_categories;
return $build;
}
/**
* Provides the UI for choosing a new inline block.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
*
* @return array
* A render array.
*/
public function inlineBlockList(SectionStorageInterface $section_storage, $delta, $region) {
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [
'section_storage' => $section_storage,
'region' => $region,
'list' => 'inline_blocks',
]);
$blocks = $this->blockManager->getGroupedDefinitions($definitions);
$build = [];
if (isset($blocks['Inline blocks'])) {
$build['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks['Inline blocks']);
$build['links']['#attributes']['class'][] = 'inline-block-list';
foreach ($build['links']['#links'] as &$link) {
$link['attributes']['class'][] = 'inline-block-list__item';
}
$build['back_button'] = [
'#type' => 'link',
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]
),
'#title' => $this->t('Back'),
'#attributes' => $this->getAjaxAttributes(),
];
}
return $build;
}
/**
* Gets a render array of block links.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
* @param array $blocks
* The information for each block.
*
* @return array
* The block links render array.
*/
protected function getBlockLinks(SectionStorageInterface $section_storage, $delta, $region, array $blocks) {
$links = [];
foreach ($blocks as $block_id => $block) {
$link = [
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
]
),
'attributes' => $this->getAjaxAttributes(),
];
$links[] = $link;
}
return [
'#theme' => 'links',
'#links' => $links,
];
}
/**
* Get dialog attributes if an ajax request.
*
* @return array
* The attributes array.
*/
protected function getAjaxAttributes() {
if ($this->isAjax()) {
return [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
];
}
return [];
}
}

View file

@ -2,6 +2,7 @@
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
@ -24,6 +25,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
use LayoutBuilderContextTrait;
use StringTranslationTrait;
use AjaxHelperTrait;
/**
* The layout tempstore repository.
@ -90,6 +92,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
$this->prepareLayout($section_storage, $is_rebuilding);
$output = [];
if ($this->isAjax()) {
$output['status_messages'] = [
'#type' => 'status_messages',
];
}
$count = 0;
for ($i = 0; $i < $section_storage->count(); $i++) {
$output[] = $this->buildAddSectionLink($section_storage, $count);
@ -114,6 +121,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
* Indicates if the layout is rebuilding.
*/
protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
// If the layout has pending changes, add a warning.
if ($this->layoutTempstoreRepository->has($section_storage)) {
$this->messenger->addWarning($this->t('You have unsaved changes.'));
}
// Only add sections if the layout is new and empty.
if (!$is_rebuilding && $section_storage->count() === 0) {
$sections = [];
@ -269,7 +281,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
],
'remove' => [
'#type' => 'link',
'#title' => $this->t('Remove section'),
'#title' => $this->t('Remove section <span class="visually-hidden">@section</span>', ['@section' => $delta + 1]),
'#url' => Url::fromRoute('layout_builder.remove_section', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,

View file

@ -9,6 +9,7 @@ use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
@ -110,10 +111,10 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
$bundle = $this->getTargetBundle();
if ($new_value) {
$this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
$this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
else {
$this->removeSectionField($entity_type_id, $bundle, 'layout_builder__layout');
$this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
}
@ -274,8 +275,8 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
* The sections.
*/
protected function getRuntimeSections(FieldableEntityInterface $entity) {
if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
return $entity->get('layout_builder__layout')->getSections();
if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
}
return $this->getSections();

View file

@ -5,6 +5,8 @@ namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
@ -88,6 +90,14 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
if ($access->isAllowed()) {
$event->addCacheableDependency($block);
$content = $block->build();
$is_content_empty = Element::isEmpty($content);
$is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface;
// If the content is empty and no placeholder is available, return.
if ($is_content_empty && !$is_placeholder_ready) {
return;
}
$build = [
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
'#theme' => 'block',
@ -96,8 +106,11 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
'#base_plugin_id' => $block->getBaseId(),
'#derivative_plugin_id' => $block->getDerivativeId(),
'#weight' => $event->getComponent()->getWeight(),
'content' => $block->build(),
'content' => $content,
];
if ($is_content_empty && $is_placeholder_ready) {
$build['content']['#markup'] = $block->getPreviewFallbackString();
}
$event->setBuild($build);
}
}

View file

@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
/**
@ -48,8 +49,8 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
$form = parent::form($form, $form_state);
// Remove the Layout Builder field from the list.
$form['#fields'] = array_diff($form['#fields'], ['layout_builder__layout']);
unset($form['fields']['layout_builder__layout']);
$form['#fields'] = array_diff($form['#fields'], [OverridesSectionStorage::FIELD_NAME]);
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$is_enabled = $this->entity->isLayoutBuilderEnabled();
if ($is_enabled) {
@ -133,7 +134,7 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
$query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
->exists('layout_builder__layout');
->exists(OverridesSectionStorage::FIELD_NAME);
if ($bundle_key = $entity_type->getKey('bundle')) {
$query->condition($bundle_key, $display->getTargetBundle());
}

View file

@ -6,6 +6,7 @@ use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Methods to help with entities using the layout builder.
@ -65,7 +66,7 @@ trait LayoutEntityHelperTrait {
return $entity->getSections();
}
elseif ($this->isEntityUsingFieldOverride($entity)) {
return $entity->get('layout_builder__layout')->getSections();
return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
}
return NULL;
}
@ -102,7 +103,7 @@ trait LayoutEntityHelperTrait {
* TRUE if the entity is using a field for a layout override.
*/
protected function isEntityUsingFieldOverride(EntityInterface $entity) {
return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout');
return $entity instanceof FieldableEntityInterface && $entity->hasField(OverridesSectionStorage::FIELD_NAME);
}
}

View file

@ -45,6 +45,15 @@ class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
return $section_storage;
}
/**
* {@inheritdoc}
*/
public function has(SectionStorageInterface $section_storage) {
$id = $section_storage->getStorageId();
$tempstore = $this->getTempstore($section_storage)->get($id);
return !empty($tempstore['section_storage']);
}
/**
* {@inheritdoc}
*/

View file

@ -35,6 +35,17 @@ interface LayoutTempstoreRepositoryInterface {
*/
public function set(SectionStorageInterface $section_storage);
/**
* Checks for the existence of a tempstore version of a section storage.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to check for in tempstore.
*
* @return bool
* TRUE if there is a tempstore version of this section storage.
*/
public function has(SectionStorageInterface $section_storage);
/**
* Removes the tempstore version of a section storage.
*

View file

@ -130,13 +130,22 @@ class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface,
// render array. If the hook is invoked the placeholder will be
// replaced.
// @see ::replaceFieldPlaceholder()
'#markup' => new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]),
'#markup' => $this->getPreviewFallbackString(),
];
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
$entity = $this->getEntity();
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]);
}
/**
* Replaces all placeholders for a given field.
*

View file

@ -17,7 +17,6 @@ use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Psr\Log\LoggerInterface;
@ -160,13 +159,17 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
$build = [];
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
}
if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) {
$build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
/**
* {@inheritdoc}
*/

View file

@ -106,7 +106,7 @@ class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface
$derivative['default_formatter'] = $field_type_definition['default_formatter'];
}
$derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['admin_label'] = $field_definition->getLabel();

View file

@ -3,11 +3,11 @@
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -28,14 +28,24 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
*/
protected $entityTypeManager;
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Constructs a new LayoutBuilderLocalTaskDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->sectionStorageManager = $section_storage_manager;
}
/**
@ -43,7 +53,8 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')
$container->get('entity_type.manager'),
$container->get('plugin.manager.layout_builder.section_storage')
);
}
@ -51,84 +62,13 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->getEntityTypesForOverrides() as $entity_type_id => $entity_type) {
// Overrides.
$this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.view",
'weight' => 15,
'title' => $this->t('Layout'),
'base_route' => "entity.$entity_type_id.canonical",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.save",
'title' => $this->t('Save Layout'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'weight' => 5,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
// @todo This link should be conditionally displayed, see
// https://www.drupal.org/node/2917777.
$this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.revert",
'title' => $this->t('Revert to defaults'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'weight' => 10,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
$section_storage = $this->sectionStorageManager->loadEmpty($plugin_id);
if ($section_storage instanceof SectionStorageLocalTaskProviderInterface) {
$this->derivatives += $section_storage->buildLocalTasks($base_plugin_definition);
}
}
foreach ($this->getEntityTypesForDefaults() as $entity_type_id => $entity_type) {
// Defaults.
$this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.view",
'title' => $this->t('Manage layout'),
'base_route' => "layout_builder.defaults.$entity_type_id.view",
];
$this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.save",
'title' => $this->t('Save Layout'),
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
$this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'weight' => 5,
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
}
return $this->derivatives;
}
/**
* Returns an array of entity types relevant for defaults.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types.
*/
protected function getEntityTypesForDefaults() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
});
}
/**
* Returns an array of entity types relevant for overrides.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types.
*/
protected function getEntityTypesForOverrides() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
});
}
}

View file

@ -32,7 +32,7 @@ use Symfony\Component\Routing\RouteCollection;
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface {
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
/**
* The entity type manager.
@ -196,6 +196,32 @@ class DefaultsSectionStorage extends SectionStorageBase implements ContainerFact
}
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition) {
$local_tasks = [];
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
$local_tasks["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.view",
'title' => $this->t('Manage layout'),
'base_route' => "layout_builder.defaults.$entity_type_id.view",
];
$local_tasks["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.save",
'title' => $this->t('Save Layout'),
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
$local_tasks["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'weight' => 5,
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
}
return $local_tasks;
}
/**
* Returns an array of relevant entity types.
*

View file

@ -30,7 +30,14 @@ use Symfony\Component\Routing\RouteCollection;
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface {
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
/**
* The field name used by this storage.
*
* @var string
*/
const FIELD_NAME = 'layout_builder__layout';
/**
* The entity type manager.
@ -127,8 +134,8 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
if (strpos($id, '.') !== FALSE) {
list($entity_type_id, $entity_id) = explode('.', $id, 2);
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) {
return $entity->get('layout_builder__layout');
if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
return $entity->get(static::FIELD_NAME);
}
}
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
@ -157,6 +164,45 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
}
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition) {
$local_tasks = [];
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
$local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.view",
'weight' => 15,
'title' => $this->t('Layout'),
'base_route' => "entity.$entity_type_id.canonical",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$local_tasks["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.save",
'title' => $this->t('Save Layout'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$local_tasks["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'weight' => 5,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
// @todo This link should be conditionally displayed, see
// https://www.drupal.org/node/2917777.
$local_tasks["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.revert",
'title' => $this->t('Revert to defaults'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'weight' => 10,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
}
return $local_tasks;
}
/**
* Determines if this entity type's ID is stored as an integer.
*

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
/**
* Allows section storage plugins to provide local tasks.
*
* @see \Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver
* @see \Drupal\layout_builder\SectionStorageInterface
*
* @internal
* Layout Builder is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
interface SectionStorageLocalTaskProviderInterface {
/**
* Provides the local tasks dynamically for Layout Builder plugins.
*
* @param mixed $base_plugin_definition
* The definition of the base plugin.
*
* @return array
* An array of full derivative definitions keyed on derivative ID.
*/
public function buildLocalTasks($base_plugin_definition);
}

View file

@ -356,4 +356,13 @@ class Section {
);
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
foreach ($this->components as $uuid => $component) {
$this->components[$uuid] = clone $component;
}
}
}

View file

@ -111,4 +111,17 @@ trait SectionStorageTrait {
return isset($this->getSections()[$delta]);
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
$sections = $this->getSections();
foreach ($sections as $delta => $item) {
$sections[$delta] = clone $item;
}
$this->setSections($sections);
}
}

View file

@ -0,0 +1,3 @@
# See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
block.settings.field_block_test:*:*:*:
type: block.settings.field_block:*:*:*

View file

@ -0,0 +1,6 @@
name: 'Layout Builder test'
type: module
description: 'Support module for testing layout building.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\layout_builder_fieldblock_test\Plugin\Block;
use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock;
/**
* Provides test field block to test with Block UI.
*
* \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest provides
* test coverage of complex AJAX interactions within certain field blocks.
* layout_builder_plugin_filter_block__block_ui_alter() removes certain blocks
* with 'layout_builder' as the provider. To make these blocks available during
* testing, this plugin uses the same deriver but each derivative will have a
* different provider.
*
* @Block(
* id = "field_block_test",
* deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
* )
*
* @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest
* @see layout_builder_plugin_filter_block__block_ui_alter()
*/
class FieldBlock extends LayoutBuilderFieldBlock {
}

View file

@ -22,6 +22,7 @@ class LayoutBuilderTest extends BrowserTestBase {
'layout_builder_views_test',
'layout_test',
'block',
'block_test',
'node',
'layout_builder_test',
];
@ -90,7 +91,7 @@ class LayoutBuilderTest extends BrowserTestBase {
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
// Save the defaults.
$assert_session->linkExists('Save Layout');
$this->clickLink('Save Layout');
@ -105,7 +106,7 @@ class LayoutBuilderTest extends BrowserTestBase {
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
// Add a new block.
$assert_session->linkExists('Add Block');
@ -316,6 +317,11 @@ class LayoutBuilderTest extends BrowserTestBase {
$page->fillField('id', 'myothermenu');
$page->pressButton('Save');
$page->clickLink('Add link');
$page->fillField('title[0][value]', 'My link');
$page->fillField('link[0][uri]', '/');
$page->pressButton('Save');
$this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display', ['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
@ -514,13 +520,68 @@ class LayoutBuilderTest extends BrowserTestBase {
}
/**
* Asserts that a text string only appears once on the page.
* Tests the usage of placeholders for empty blocks.
*
* @param string $needle
* The string to look for.
* @see \Drupal\Core\Block\BlockPluginInterface::getPlaceholderString()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
protected function assertTextAppearsOnce($needle) {
$this->assertEquals(1, substr_count($this->getSession()->getPage()->getContent(), $needle), "'$needle' only appears once on the page.");
public function testBlockPlaceholder() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
// Customize the default view mode.
$this->drupalGet("$field_ui_prefix/display-layout/default");
// Add a block whose content is controlled by state and is empty by default.
$this->clickLink('Add Block');
$this->clickLink('Test block caching');
$page->fillField('settings[label]', 'The block label');
$page->pressButton('Add Block');
$block_content = 'I am content';
$placeholder_content = 'Placeholder for the "The block label" block';
// The block placeholder is displayed and there is no content.
$assert_session->pageTextContains($placeholder_content);
$assert_session->pageTextNotContains($block_content);
// Set block content and reload the page.
\Drupal::state()->set('block_test.content', $block_content);
$this->getSession()->reload();
// The block placeholder is no longer displayed and the content is visible.
$assert_session->pageTextNotContains($placeholder_content);
$assert_session->pageTextContains($block_content);
}
/**
* Tests the Block UI when Layout Builder is installed.
*/
public function testBlockUiListing() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'administer blocks',
]));
$this->drupalGet('admin/structure/block');
$page->clickLink('Place block');
// Ensure that blocks expected to appear are available.
$assert_session->pageTextContains('Test HTML block');
$assert_session->pageTextContains('Block test');
// Ensure that blocks not expected to appear are not available.
$assert_session->pageTextNotContains('Body');
$assert_session->pageTextNotContains('Content fields');
}
}

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\BrowserTestBase;
@ -20,13 +21,6 @@ class LayoutSectionTest extends BrowserTestBase {
*/
public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
/**
* The name of the layout section field.
*
* @var string
*/
protected $fieldName = 'layout_builder__layout';
/**
* {@inheritdoc}
*/
@ -226,7 +220,7 @@ class LayoutSectionTest extends BrowserTestBase {
]);
$entity->addTranslation('es', [
'title' => 'Translated node title',
$this->fieldName => [
OverridesSectionStorage::FIELD_NAME => [
[
'section' => new Section('layout_twocol', [], [
'foo' => new SectionComponent('foo', 'first', [
@ -373,7 +367,7 @@ class LayoutSectionTest extends BrowserTestBase {
'value' => 'The node body',
],
],
$this->fieldName => $section_values,
OverridesSectionStorage::FIELD_NAME => $section_values,
]);
}

View file

@ -16,7 +16,14 @@ class FieldBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['block', 'datetime', 'layout_builder', 'user'];
protected static $modules = [
'block',
'datetime',
'layout_builder',
'user',
// See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
'layout_builder_fieldblock_test',
];
/**
* {@inheritdoc}
@ -67,7 +74,7 @@ class FieldBlockTest extends WebDriverTestBase {
$assert_session->pageTextNotContains('Initial email');
$assert_session->pageTextContains('Date field');
$block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy';
$block_url = 'admin/structure/block/add/field_block_test%3Auser%3Auser%3Afield_date/classy';
$assert_session->linkByHrefExists($block_url);
$this->drupalGet($block_url);

View file

@ -192,8 +192,8 @@ class InlineBlockPrivateFilesTest extends InlineBlockTestBase {
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
$this->clickLink('Basic block');
$this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldValueEquals('Title', '');
$page->findField('Title')->setValue($title);

View file

@ -428,4 +428,74 @@ class InlineBlockTest extends InlineBlockTestBase {
$assert_session->pageTextNotContains('You are not authorized to access this page');
}
/**
* Tests the workflow for adding an inline block depending on number of types.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
* @throws \Behat\Mink\Exception\ExpectationException
*/
public function testAddWorkFlow() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$type_storage = $this->container->get('entity_type.manager')->getStorage('block_content_type');
foreach ($type_storage->loadByProperties() as $type) {
$type->delete();
}
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
]));
// Enable layout builder and overrides.
$this->drupalPostForm(
static::FIELD_UI_PREFIX . '/display/default',
['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
'Save'
);
$layout_default_path = 'admin/structure/types/manage/bundle_with_section_field/display-layout/default';
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
// Confirm that with no block content types the link does not appear.
$assert_session->linkNotExists('Create custom block');
$this->createBlockContentType('basic', 'Basic block');
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
// Confirm with only 1 type the "Create custom block" link goes directly t
// block add form.
$assert_session->linkNotExists('Basic block');
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldExists('Title');
$this->createBlockContentType('advanced', 'Advanced block');
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add Block');
// Confirm that, when more than 1 type exists, "Create custom block" shows a
// list of block types.
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkNotExists('Basic block');
$assert_session->linkNotExists('Advanced block');
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldNotExists('Title');
$assert_session->linkExists('Basic block');
$assert_session->linkExists('Advanced block');
$this->clickLink('Advanced block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldExists('Title');
}
}

View file

@ -71,13 +71,7 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
],
],
]);
$bundle = BlockContentType::create([
'id' => 'basic',
'label' => 'Basic block',
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
$this->createBlockContentType('basic', 'Basic block');
$this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content');
}
@ -146,8 +140,8 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
$this->clickLink('Basic block');
$this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]');
$this->assertNotEmpty($textarea);
@ -219,4 +213,22 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
}
}
/**
* Creates a block content type.
*
* @param string $id
* The block type id.
* @param string $label
* The block type label.
*/
protected function createBlockContentType($id, $label) {
$bundle = BlockContentType::create([
'id' => $id,
'label' => $label,
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderUiTest extends WebDriverTestBase {
/**
* Path prefix for the field UI for the test bundle.
*
* @var string
*/
const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
public static $modules = [
'layout_builder',
'block',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// @todo The Layout Builder UI relies on local tasks; fix in
// https://www.drupal.org/project/drupal/issues/2917777.
$this->drupalPlaceBlock('local_tasks_block');
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
}
/**
* Tests the message indicating unsaved changes.
*/
public function testUnsavedChangesMessage() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Enable layout builder.
$this->drupalPostForm(
static::FIELD_UI_PREFIX . '/display/default',
['layout[enabled]' => TRUE],
'Save'
);
// Make and then cancel changes.
$this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
$page->clickLink('Cancel Layout');
$assert_session->pageTextNotContains('You have unsaved changes.');
// Make and then save changes.
$this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
$page->clickLink('Save Layout');
$assert_session->pageTextNotContains('You have unsaved changes.');
}
/**
* Asserts that modifying a layout works as expected.
*
* @param string $path
* The path to a Layout Builder UI page.
*/
protected function assertModifiedLayout($path) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet($path);
$page->clickLink('Add Section');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('You have unsaved changes.');
$page->clickLink('One column');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContainsOnce('You have unsaved changes.');
// Reload the page.
$this->drupalGet($path);
$assert_session->pageTextContainsOnce('You have unsaved changes.');
}
}

View file

@ -230,11 +230,10 @@ class FieldBlockTest extends EntityKernelTestBase {
* @covers ::build
* @dataProvider providerTestBuild
*/
public function testBuild(PromiseInterface $promise, $in_preview, $expected_markup, $log_message = '', $log_arguments = []) {
public function testBuild(PromiseInterface $promise, $expected_markup, $log_message = '', $log_arguments = []) {
$entity = $this->prophesize(FieldableEntityInterface::class);
$field = $this->prophesize(FieldItemListInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal());
$entity->in_preview = $in_preview;
$field->view(Argument::type('array'))->will($promise);
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
@ -269,40 +268,20 @@ class FieldBlockTest extends EntityKernelTestBase {
*/
public function providerTestBuild() {
$data = [];
$data['array, no preview'] = [
$data['array'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
FALSE,
'The field value',
];
$data['array, preview'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
TRUE,
'The field value',
];
$data['empty array, no preview'] = [
$data['empty array'] = [
new ReturnPromise([[]]),
FALSE,
'',
];
$data['empty array, preview'] = [
new ReturnPromise([[]]),
TRUE,
'Placeholder for the "The Field Label" field',
];
$data['exception, no preview'] = [
$data['exception'] = [
new ThrowPromise(new \Exception('The exception message')),
FALSE,
'',
'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message'],
];
$data['exception, preview'] = [
new ThrowPromise(new \Exception('The exception message')),
TRUE,
'Placeholder for the "The Field Label" field',
'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message'],
];
return $data;
}

View file

@ -2,6 +2,7 @@
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
@ -56,7 +57,7 @@ class LayoutBuilderFieldLayoutCompatibilityTest extends LayoutBuilderCompatibili
// Add a layout override.
$this->enableOverrides();
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
$field_list = $this->entity->get('layout_builder__layout');
$field_list = $this->entity->get(OverridesSectionStorage::FIELD_NAME);
$field_list->appendSection(new Section('layout_onecol'));
$this->entity->save();

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
@ -35,7 +36,7 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
// Add a layout override.
$this->enableOverrides();
$this->entity = $this->reloadEntity($this->entity);
$this->entity->get('layout_builder__layout')->appendSection(new Section('layout_onecol'));
$this->entity->get(OverridesSectionStorage::FIELD_NAME)->appendSection(new Section('layout_onecol'));
$this->entity->save();
// The rendered entity has now changed. The non-configurable field is shown
@ -50,7 +51,7 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
// Removing the layout restores the original rendering of the entity.
$this->entity->get('layout_builder__layout')->removeSection(0);
$this->entity->get(OverridesSectionStorage::FIELD_NAME)->removeSection(0);
$this->entity->save();
$this->assertFieldAttributes($this->entity, $expected_fields);

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Tests the field type for Layout Sections.
@ -42,10 +43,10 @@ class LayoutSectionItemListTest extends SectionStorageTestBase {
}, $section_data);
$entity = EntityTestBaseFieldDisplay::create([
'name' => 'The test entity',
'layout_builder__layout' => $section_data,
OverridesSectionStorage::FIELD_NAME => $section_data,
]);
$entity->save();
return $entity->get('layout_builder__layout');
return $entity->get(OverridesSectionStorage::FIELD_NAME);
}
}

View file

@ -137,6 +137,17 @@ abstract class SectionStorageTestBase extends EntityKernelTestBase {
$this->assertSections($expected);
}
/**
* Tests __clone().
*/
public function testClone() {
$this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
$new_section_storage = clone $this->sectionStorage;
$new_section_storage->getSection(0)->setLayoutSettings(['asdf' => 'qwer']);
$this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
}
/**
* Asserts that the field list has the expected sections.
*

View file

@ -11,6 +11,7 @@ use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
@ -252,6 +253,98 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderInPreviewEmptyBuild() {
$block = $this->prophesize(BlockPluginInterface::class)->willImplement(PreviewFallbackInterface::class);
$block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$block->getConfiguration()->willReturn([]);
$block->getPluginId()->willReturn('block_plugin_id');
$block->getBaseId()->willReturn('block_plugin_id');
$block->getDerivativeId()->willReturn(NULL);
$placeholder_string = 'The placeholder string';
$block->getPreviewFallbackString()->willReturn($placeholder_string);
$block_content = [];
$block->build()->willReturn($block_content);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$event = new SectionComponentBuildRenderArrayEvent($component, [], TRUE);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [
'#theme' => 'block',
'#weight' => 0,
'#configuration' => [],
'#plugin_id' => 'block_plugin_id',
'#base_plugin_id' => 'block_plugin_id',
'#derivative_plugin_id' => NULL,
'content' => $block_content,
];
$expected_build['content']['#markup'] = $placeholder_string;
$expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => ['test'],
'max-age' => 0,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderEmptyBuild() {
$block = $this->prophesize(BlockPluginInterface::class);
$access_result = AccessResult::allowed();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$block->getConfiguration()->willReturn([]);
$block->getPluginId()->willReturn('block_plugin_id');
$block->getBaseId()->willReturn('block_plugin_id');
$block->getDerivativeId()->willReturn(NULL);
$block->build()->willReturn([]);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$event = new SectionComponentBuildRenderArrayEvent($component, [], FALSE);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [];
$expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => ['test'],
'max-age' => -1,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/

View file

@ -16,6 +16,7 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
/**
* @covers ::get
* @covers ::has
*/
public function testGetEmptyTempstore() {
$section_storage = $this->prophesize(SectionStorageInterface::class);
@ -30,12 +31,15 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
$this->assertFalse($repository->has($section_storage->reveal()));
$result = $repository->get($section_storage->reveal());
$this->assertSame($section_storage->reveal(), $result);
}
/**
* @covers ::get
* @covers ::has
*/
public function testGetLoadedTempstore() {
$section_storage = $this->prophesize(SectionStorageInterface::class);
@ -50,6 +54,8 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
$this->assertTrue($repository->has($section_storage->reveal()));
$result = $repository->get($section_storage->reveal());
$this->assertSame($tempstore_section_storage->reveal(), $result);
$this->assertNotSame($section_storage->reveal(), $result);

View file

@ -112,13 +112,13 @@ class OverridesSectionStorageTest extends UnitTestCase {
$entity_storage = $this->prophesize(EntityStorageInterface::class);
$entity_without_layout = $this->prophesize(FieldableEntityInterface::class);
$entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE);
$entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled();
$entity_without_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(FALSE);
$entity_without_layout->get(OverridesSectionStorage::FIELD_NAME)->shouldNotBeCalled();
$entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal());
$entity_with_layout = $this->prophesize(FieldableEntityInterface::class);
$entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE);
$entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value');
$entity_with_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(TRUE);
$entity_with_layout->get(OverridesSectionStorage::FIELD_NAME)->willReturn('the_return_value');
$entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal());
$this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal());

View file

@ -231,6 +231,7 @@ class SectionRenderTest extends UnitTestCase {
* @covers ::toRenderArray
*/
public function testContextAwareBlock() {
$block_content = ['#markup' => 'The block content.'];
$render_array = [
'#theme' => 'block',
'#weight' => 0,
@ -238,7 +239,7 @@ class SectionRenderTest extends UnitTestCase {
'#plugin_id' => 'block_plugin_id',
'#base_plugin_id' => 'block_plugin_id',
'#derivative_plugin_id' => NULL,
'content' => [],
'content' => $block_content,
'#cache' => [
'contexts' => [],
'tags' => [],
@ -251,7 +252,7 @@ class SectionRenderTest extends UnitTestCase {
$access_result = AccessResult::allowed();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result);
$block->build()->willReturn([]);
$block->build()->willReturn($block_content);
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn([]);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);

View file

@ -185,20 +185,11 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
// Set the thumbnail alt.
$media_source = $this->getSource();
$plugin_definition = $media_source->getPluginDefinition();
$this->thumbnail->alt = '';
if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) {
$this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']);
}
else {
$this->thumbnail->alt = $this->t('Thumbnail', [], ['langcode' => $this->langcode->value]);
}
// Set the thumbnail title.
if (!empty($plugin_definition['thumbnail_title_metadata_attribute'])) {
$this->thumbnail->title = $media_source->getMetadata($this, $plugin_definition['thumbnail_title_metadata_attribute']);
}
else {
$this->thumbnail->title = $this->label();
}
return $this;
}

View file

@ -304,6 +304,17 @@ class MediaTypeForm extends EntityForm {
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
// If the media source has not been chosen yet, turn the submit button into
// a button. This rebuilds the form with the media source's configuration
// form visible, instead of saving the media type. This allows users to
// create a media type without JavaScript enabled. With JavaScript enabled,
// this rebuild occurs during an AJAX request.
// @see \Drupal\media\MediaTypeForm::ajaxHandlerData()
if (empty($this->getEntity()->get('source'))) {
$actions['submit']['#type'] = 'button';
}
$actions['submit']['#value'] = $this->t('Save');
$actions['delete']['#value'] = $this->t('Delete');
$actions['delete']['#access'] = $this->entity->access('delete');

View file

@ -7,7 +7,6 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
/**
* Fetches and caches oEmbed resources.
@ -69,8 +68,7 @@ class ResourceFetcher implements ResourceFetcherInterface {
$content = (string) $response->getBody();
if (strstr($format, 'text/xml') || strstr($format, 'application/xml')) {
$encoder = new XmlEncoder();
$data = $encoder->decode($content, 'xml');
$data = $this->parseResourceXml($content, $url);
}
elseif (strstr($format, 'text/javascript') || strstr($format, 'application/json')) {
$data = Json::decode($content);
@ -194,4 +192,42 @@ class ResourceFetcher implements ResourceFetcherInterface {
}
}
/**
* Parses XML resource data.
*
* @param string $data
* The raw XML for the resource.
* @param string $url
* The resource URL.
*
* @return array
* The parsed resource data.
*
* @throws \Drupal\media\OEmbed\ResourceException
* If the resource data could not be parsed.
*/
protected function parseResourceXml($data, $url) {
// Enable userspace error handling.
$was_using_internal_errors = libxml_use_internal_errors(TRUE);
libxml_clear_errors();
$content = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
// Restore the previous error handling behavior.
libxml_use_internal_errors($was_using_internal_errors);
$error = libxml_get_last_error();
if ($error) {
libxml_clear_errors();
throw new ResourceException($error->message, $url);
}
elseif ($content === FALSE) {
throw new ResourceException('The fetched resource could not be parsed.', $url);
}
// Convert XML to JSON so that the parsed resource has a consistent array
// structure, regardless of any XML attributes or quirks of the XML parser.
$data = Json::encode($content);
return Json::decode($data);
}
}

View file

@ -22,7 +22,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* label = @Translation("Image"),
* description = @Translation("Use local images for reusable media."),
* allowed_field_types = {"image"},
* default_thumbnail_filename = "no-thumbnail.png"
* default_thumbnail_filename = "no-thumbnail.png",
* thumbnail_alt_metadata_attribute = "thumbnail_alt_value"
* )
*/
class Image extends File {
@ -138,6 +139,9 @@ class Image extends File {
case 'thumbnail_uri':
return $uri;
case 'thumbnail_alt_value':
return $media->get($this->configuration['source_field'])->alt ?: parent::getMetadata($media, $name);
}
return parent::getMetadata($media, $name);

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<oembed>
<type>video</type>
<version>1.0</version>
<version type="float">1.0</version>
<title>Let's Not Get a Drink Sometime</title>
<https/>
<author_name>CollegeHumor</author_name>

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\Tests\media\Functional;
use Drupal\media\Entity\MediaType;
/**
* Ensures that media UI works correctly without JavaScript.
*
* @group media
*/
class MediaTypeCreationTest extends MediaFunctionalTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'media_test_source',
];
/**
* Tests the media type creation form with only the mandatory options.
*/
public function testMediaTypeCreationForm() {
$machine_name = mb_strtolower($this->randomMachineName());
$this->drupalGet('/admin/structure/media/add');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->fieldExists('label')->setValue($this->randomString());
$this->assertSession()->fieldExists('id')->setValue($machine_name);
$this->assertSession()->selectExists('source')->selectOption('test');
$this->assertSession()->buttonExists('Save')->press();
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->fieldValueEquals('Test config value', 'This is default value.');
$this->assertSession()->buttonExists('Save')->press();
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('admin/structure/media');
$this->assertInstanceOf(MediaType::class, MediaType::load($machine_name));
}
}

View file

@ -248,7 +248,7 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
// Unlimited value field.
'unlimited_value:single_type:create_list' => [FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, [TRUE], TRUE],
// Unlimited value field with the tags widget.
'unlimited_value:single_type:create_list' => [FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, [TRUE], TRUE, 'entity_reference_autocomplete_tags'],
'unlimited_value:single_type:create_list:tags' => [FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, [TRUE], TRUE, 'entity_reference_autocomplete_tags'],
];
}

View file

@ -167,13 +167,13 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase {
],
'thumbnail' => [
[
'alt' => 'Thumbnail',
'alt' => '',
'width' => 180,
'height' => 180,
'target_id' => (int) $thumbnail->id(),
'target_type' => 'file',
'target_uuid' => $thumbnail->uuid(),
'title' => 'Llama',
'title' => NULL,
'url' => $thumbnail->url(),
],
],

View file

@ -59,6 +59,8 @@ class MediaSourceImageTest extends MediaSourceTestBase {
// Make sure the thumbnail is displayed from uploaded image.
$assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'example_1.jpeg');
// Ensure the thumbnail has the correct alt attribute.
$assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'alt', 'Image Alt Text 1');
// Load the media and check that all fields are properly populated.
$media = Media::load(1);

View file

@ -40,7 +40,6 @@ class MediaSourceTest extends MediaKernelTestBase {
'value' => 'Snowball',
],
'thumbnail_uri' => [
'title' => 'Thumbnail',
'value' => 'public://TheSisko.png',
],
]);
@ -230,7 +229,7 @@ class MediaSourceTest extends MediaKernelTestBase {
// Save a media item and make sure thumbnail was added.
\Drupal::state()->set('media_source_test_attributes', [
'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'],
'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
]);
/** @var \Drupal\media\MediaInterface $media */
$media = Media::create([
@ -242,45 +241,46 @@ class MediaSourceTest extends MediaKernelTestBase {
$this->assertSame('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not added to the media item.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
// We expect the title not to be present on the Thumbnail.
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Now change the metadata attribute and make sure that the thumbnail stays
// the same.
\Drupal::state()->set('media_source_test_attributes', [
'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail2.jpg'],
'thumbnail_uri' => ['value' => 'public://thumbnail2.jpg'],
]);
$this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not preserved.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Remove the thumbnail and make sure that it is auto-updated on save.
$media->thumbnail->target_id = NULL;
$this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Change the metadata attribute again, change the source field value too
// and make sure that the thumbnail updates.
\Drupal::state()->set('media_source_test_attributes', [
'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'],
'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
]);
$media->field_media_test->value = 'some_new_value';
$this->assertSame('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Change the thumbnail metadata attribute and make sure that the thumbnail
// is set correctly.
\Drupal::state()->set('media_source_test_attributes', [
'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'],
'alternative_thumbnail_uri' => ['title' => 'Should be used', 'value' => 'public://thumbnail2.jpg'],
'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
'alternative_thumbnail_uri' => ['value' => 'public://thumbnail2.jpg'],
]);
\Drupal::state()->set('media_source_test_definition', ['thumbnail_uri_metadata_attribute' => 'alternative_thumbnail_uri']);
$media = Media::create([
@ -293,14 +293,14 @@ class MediaSourceTest extends MediaKernelTestBase {
$this->assertSame('public://thumbnail2.jpg', $media_source->getMetadata($media, 'alternative_thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.');
$media->save();
$this->assertSame('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'Correct metadata attribute was not used for the thumbnail.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Enable queued thumbnails and make sure that the entity gets the default
// thumbnail initially.
\Drupal::state()->set('media_source_test_definition', []);
\Drupal::state()->set('media_source_test_attributes', [
'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'],
'thumbnail_uri' => ['value' => 'public://thumbnail1.jpg'],
]);
$this->testMediaType->setQueueThumbnailDownloadsStatus(TRUE)->save();
$media = Media::create([
@ -311,8 +311,8 @@ class MediaSourceTest extends MediaKernelTestBase {
$this->assertSame('public://thumbnail1.jpg', $media->getSource()->getMetadata($media, 'thumbnail_uri'), 'Value of the metadata attribute is not correct.');
$media->save();
$this->assertSame('public://media-icons/generic/generic.png', $media->thumbnail->entity->getFileUri(), 'Default thumbnail was not set initially.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Process the queue item and make sure that the thumbnail was updated too.
$queue_name = 'media_entity_thumbnail';
@ -330,18 +330,15 @@ class MediaSourceTest extends MediaKernelTestBase {
$media = Media::load($media->id());
$this->assertSame('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not updated by the queue.');
$this->assertSame('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertSame('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('', $media->thumbnail->alt);
// Set alt and title metadata attributes and make sure they are used for the
// thumbnail.
// Set the alt metadata attribute and make sure it's used for the thumbnail.
\Drupal::state()->set('media_source_test_definition', [
'thumbnail_alt_metadata_attribute' => 'alt',
'thumbnail_title_metadata_attribute' => 'title',
]);
\Drupal::state()->set('media_source_test_attributes', [
'alt' => ['title' => 'Alt text', 'value' => 'This will be alt.'],
'title' => ['title' => 'Title text', 'value' => 'This will be title.'],
'alt' => ['value' => 'This will be alt.'],
]);
$media = Media::create([
'bundle' => $this->testMediaType->id(),
@ -350,8 +347,8 @@ class MediaSourceTest extends MediaKernelTestBase {
]);
$media->save();
$this->assertSame('Boxer', $media->getName(), 'Correct name was not set on the media item.');
$this->assertSame('This will be title.', $media->thumbnail->title, 'Title text was not set on the thumbnail.');
$this->assertSame('This will be alt.', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.');
$this->assertEmpty($media->thumbnail->title);
$this->assertSame('This will be alt.', $media->thumbnail->alt);
}
/**

View file

@ -198,7 +198,7 @@ class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInte
* The menu link ID.
*/
protected function getUuid() {
$this->getDerivativeId();
return $this->getDerivativeId();
}
/**

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\menu_link_content\Unit;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent
*
* @group Menu
*/
class MenuLinkPluginTest extends UnitTestCase {
/**
* @covers ::getUuid
*/
public function testGetInstanceReflection() {
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $menu_link_content_plugin */
$menu_link_content_plugin = $this->prophesize(MenuLinkContent::class);
$menu_link_content_plugin->getDerivativeId()->willReturn('test_id');
$menu_link_content_plugin = $menu_link_content_plugin->reveal();
$class = new \ReflectionClass(MenuLinkContent::class);
$instance_method = $class->getMethod('getUuid');
$instance_method->setAccessible(TRUE);
$this->assertEquals('test_id', $instance_method->invoke($menu_link_content_plugin));
}
}

View file

@ -208,7 +208,14 @@ class MigrationPluginManager extends DefaultPluginManager implements MigrationPl
$migration->set('requirements', $required_dependency_graph[$migration_id]['paths']);
}
}
array_multisort($weights, SORT_DESC, SORT_NUMERIC, $migrations);
// Sort weights, labels, and keys in the same order as each other.
array_multisort(
// Use the numerical weight as the primary sort.
$weights, SORT_DESC, SORT_NUMERIC,
// When migrations have the same weight, sort them alphabetically by ID.
array_keys($migrations), SORT_ASC, SORT_NATURAL,
$migrations
);
return $migrations;
}

View file

@ -584,7 +584,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP
if (!empty($source_id_values)) {
$var_dump = var_export($source_id_values, TRUE);
throw new MigrateException(sprintf("Extra unknown items in source IDs: %s", $var_dump));
throw new MigrateException(sprintf("Extra unknown items for map %s in source IDs: %s", $this->mapTableName(), $var_dump));
}
$query = $this->getDatabase()->select($this->mapTableName(), 'map')

View file

@ -82,7 +82,8 @@ use Drupal\migrate\Row;
* @endcode
*
* If the source value was '2004-12-19T10:19:42-0600' the transformed value
* would be 2004-12-19T10:19:42.
* would be 2004-12-19T10:19:42. Set validate_format to false if your source
* value is '0000-00-00 00:00:00'.
*
* @see \DateTime::createFromFormat()
* @see \Drupal\Component\Datetime\DateTimePlus::__construct()
@ -99,7 +100,7 @@ class FormatDate extends ProcessPluginBase {
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (empty($value)) {
if (empty($value) && $value !== '0' && $value !== 0) {
return '';
}

View file

@ -353,7 +353,7 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter
$row_data = $this->getIterator()->current() + $this->configuration;
$this->fetchNextRow();
$row = new Row($row_data, $this->migration->getSourcePlugin()->getIds(), $this->migration->getDestinationIds());
$row = new Row($row_data, $this->getIds());
// Populate the source key for this row.
$this->currentSourceIds = $row->getSourceIdValues();

View file

@ -108,6 +108,11 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->state = $state;
// If we are using high water, but haven't yet set a high water mark, skip
// joining the map table, as we want to get all available records.
if ($this->getHighWaterProperty() && $this->getHighWater() === NULL) {
$this->configuration['ignore_map'] = TRUE;
}
}
/**
@ -322,7 +327,9 @@ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPlugi
if ($this->getHighWaterProperty()) {
$high_water_field = $this->getHighWaterField();
$high_water = $this->getHighWater();
if ($high_water) {
// We check against NULL because 0 is an acceptable value for the high
// water mark.
if ($high_water !== NULL) {
$conditions->condition($high_water_field, $high_water, '>');
$condition_added = TRUE;
}

View file

@ -136,6 +136,72 @@ class HighWaterTest extends MigrateTestBase {
$this->assertNodeDoesNotExist('Item 3');
}
/**
* Tests that the high water value can be 0.
*/
public function testZeroHighwater() {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
// Expect no rows as everything is below the high water mark.
$this->assertSame(0, $count);
// Test resetting the high water mark to 0.
$this->container->get('keyvalue')->get('migrate:high_water')->set('high_water_test', 0);
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
$this->assertSame(3, $count);
}
/**
* Tests that deleting the high water value causes all rows to be reimported.
*/
public function testNullHighwater() {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
// Expect no rows as everything is below the high water mark.
$this->assertSame(0, $count);
// Test resetting the high water mark.
$this->container->get('keyvalue')->get('migrate:high_water')->delete('high_water_test');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
$this->assertSame(3, $count);
}
/**
* Tests high water property of SqlBase when rows marked for update.
*/

View file

@ -45,6 +45,11 @@ class MigrateEmbeddedDataTest extends KernelTestBase {
$results = [];
/** @var \Drupal\migrate\Row $row */
foreach ($source as $row) {
// The plugin should not mark any rows as stubs. We need to use
// assertSame() here because assertFalse() will pass falsy values (e.g.,
// empty arrays).
$this->assertSame(FALSE, $row->isStub());
$data_row = $row->getSource();
// The "data" row returned by getSource() also includes all source
// configuration - we remove it so we see only the data itself.

View file

@ -529,14 +529,14 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
$this->fail('Too many source IDs should throw');
}
catch (MigrateException $e) {
$this->assertEquals("Extra unknown items in source IDs: array (\n 0 => 3,\n)", $e->getMessage());
$this->assertEquals("Extra unknown items for map migrate_map_sql_idmap_test in source IDs: array (\n 0 => 3,\n)", $e->getMessage());
}
try {
$id_map->lookupDestinationIds(['nid' => 1, 'aaa' => '2']);
$this->fail('Unknown source ID key should throw');
}
catch (MigrateException $e) {
$this->assertEquals("Extra unknown items in source IDs: array (\n 'aaa' => '2',\n)", $e->getMessage());
$this->assertEquals("Extra unknown items for map migrate_map_sql_idmap_test in source IDs: array (\n 'aaa' => '2',\n)", $e->getMessage());
}
// Verify that we are looking up by source_id_hash when all source IDs are

View file

@ -175,6 +175,42 @@ class FormatDateTest extends MigrateProcessTestCase {
// converted from Australia/Sydney to America/Managua timezone.
'expected' => '2004-12-18 17:19:42 America/Managua',
],
'integer_0' => [
'configuration' => [
'from_format' => 'U',
'to_format' => 'Y-m-d',
],
'value' => 0,
'expected' => '1970-01-01',
],
'string_0' => [
'configuration' => [
'from_format' => 'U',
'to_format' => 'Y-m-d',
],
'value' => '0',
'expected' => '1970-01-01',
],
'zeros' => [
'configuration' => [
'from_format' => 'Y-m-d H:i:s',
'to_format' => 'Y-m-d H:i:s e',
'settings' => ['validate_format' => FALSE],
],
'value' => '0000-00-00 00:00:00',
'expected' => '-0001-11-30 00:00:00 Australia/Sydney',
],
'zeros_same_timezone' => [
'configuration' => [
'from_format' => 'Y-m-d H:i:s',
'to_format' => 'Y-m-d H:i:s',
'settings' => ['validate_format' => FALSE],
'from_timezone' => 'UTC',
'to_timezone' => 'UTC',
],
'value' => '0000-00-00 00:00:00',
'expected' => '-0001-11-30 00:00:00',
],
];
}

View file

@ -102,6 +102,7 @@ abstract class DrupalSqlBase extends SqlBase implements ContainerFactoryPluginIn
* {@inheritdoc}
*/
public function checkRequirements() {
parent::checkRequirements();
if ($this->pluginDefinition['requirements_met'] === TRUE) {
if (isset($this->pluginDefinition['source_module'])) {
if ($this->moduleExists($this->pluginDefinition['source_module'])) {
@ -114,7 +115,6 @@ abstract class DrupalSqlBase extends SqlBase implements ContainerFactoryPluginIn
}
}
}
parent::checkRequirements();
}
/**

View file

@ -292,7 +292,6 @@ $connection->insert('actions')
'description' => 'Block current user',
))
->execute();
$connection->schema()->createTable('actions_aid', array(
'fields' => array(
'aid' => array(
@ -331,7 +330,6 @@ $connection->insert('actions_aid')
'aid' => '6',
))
->execute();
$connection->schema()->createTable('aggregator_category', array(
'fields' => array(
'cid' => array(
@ -463,7 +461,6 @@ $connection->insert('aggregator_feed')
'block' => '7',
))
->execute();
$connection->schema()->createTable('aggregator_item', array(
'fields' => array(
'iid' => array(
@ -539,7 +536,6 @@ $connection->insert('aggregator_item')
'guid' => '395218 at https://groups.drupal.org',
))
->execute();
$connection->schema()->createTable('authmap', array(
'fields' => array(
'aid' => array(
@ -617,7 +613,6 @@ $connection->insert('batch')
'batch' => NULL,
))
->execute();
$connection->schema()->createTable('blocks', array(
'fields' => array(
'bid' => array(
@ -1080,7 +1075,6 @@ $connection->insert('blocks')
'cache' => '-1',
))
->execute();
$connection->schema()->createTable('blocks_roles', array(
'fields' => array(
'module' => array(
@ -1125,7 +1119,6 @@ $connection->insert('blocks_roles')
'rid' => '3',
))
->execute();
$connection->schema()->createTable('book', array(
'fields' => array(
'mlid' => array(
@ -1188,7 +1181,6 @@ $connection->insert('book')
'bid' => '8',
))
->execute();
$connection->schema()->createTable('boxes', array(
'fields' => array(
'bid' => array(
@ -1241,7 +1233,6 @@ $connection->insert('boxes')
'format' => '2',
))
->execute();
$connection->schema()->createTable('cache', array(
'fields' => array(
'cid' => array(
@ -2023,7 +2014,6 @@ $connection->insert('comments')
'homepage' => '',
))
->execute();
$connection->schema()->createTable('config', array(
'fields' => array(
'collection' => array(
@ -2063,7 +2053,6 @@ $connection->insert('config')
'data' => 'a:1:{s:4:"path";a:1:{s:9:"temporary";s:4:"/tmp";}}',
))
->execute();
$connection->schema()->createTable('contact', array(
'fields' => array(
'cid' => array(
@ -2141,7 +2130,6 @@ $connection->insert('contact')
'selected' => '0',
))
->execute();
$connection->schema()->createTable('content_field_company', array(
'fields' => array(
'vid' => array(
@ -2219,7 +2207,6 @@ $connection->insert('content_field_company')
'field_company_nid' => '16',
))
->execute();
$connection->schema()->createTable('content_field_image', array(
'fields' => array(
'vid' => array(
@ -2288,7 +2275,6 @@ $connection->insert('content_field_image')
'field_image_data' => 'a:2:{s:3:"alt";s:0:"";s:5:"title";s:0:"";}',
))
->execute();
$connection->schema()->createTable('content_field_multivalue', array(
'fields' => array(
'vid' => array(
@ -2351,7 +2337,6 @@ $connection->insert('content_field_multivalue')
'delta' => '1',
))
->execute();
$connection->schema()->createTable('content_field_test', array(
'fields' => array(
'vid' => array(
@ -2429,7 +2414,6 @@ $connection->insert('content_field_test')
'field_test_format' => '1',
))
->execute();
$connection->schema()->createTable('content_field_test_text_single_checkbox', array(
'fields' => array(
'vid' => array(
@ -2495,7 +2479,6 @@ $connection->insert('content_field_test_text_single_checkbox')
'field_test_text_single_checkbox_value' => '0',
))
->execute();
$connection->schema()->createTable('content_field_test_two', array(
'fields' => array(
'vid' => array(
@ -2581,7 +2564,6 @@ $connection->insert('content_field_test_two')
'field_test_two_value' => '20',
))
->execute();
$connection->schema()->createTable('content_group', array(
'fields' => array(
'group_type' => array(
@ -3073,7 +3055,6 @@ $connection->insert('content_node_field')
'locked' => '0',
))
->execute();
$connection->schema()->createTable('content_node_field_instance', array(
'fields' => array(
'field_name' => array(
@ -3515,7 +3496,6 @@ $connection->insert('content_node_field_instance')
'widget_active' => '1',
))
->execute();
$connection->schema()->createTable('content_type_employee', array(
'fields' => array(
'vid' => array(
@ -3601,7 +3581,6 @@ $connection->insert('content_type_employee')
'field_company_3_nid' => NULL,
))
->execute();
$connection->schema()->createTable('content_type_page', array(
'fields' => array(
'vid' => array(
@ -3687,7 +3666,6 @@ $connection->insert('content_type_page')
'field_reference_2_nid' => '11',
))
->execute();
$connection->schema()->createTable('content_type_story', array(
'fields' => array(
'nid' => array(
@ -4057,7 +4035,6 @@ $connection->insert('content_type_story')
'field_test_string_selectlist_value' => NULL,
))
->execute();
$connection->schema()->createTable('content_type_test_page', array(
'fields' => array(
'vid' => array(
@ -4148,7 +4125,6 @@ $connection->insert('content_type_test_planet')
'vid' => '11',
))
->execute();
$connection->schema()->createTable('date_format_locale', array(
'fields' => array(
'format' => array(
@ -4221,7 +4197,6 @@ $connection->insert('date_format_types')
'locked' => '1',
))
->execute();
$connection->schema()->createTable('date_formats', array(
'fields' => array(
'dfid' => array(
@ -4471,7 +4446,6 @@ $connection->insert('date_formats')
'locked' => '1',
))
->execute();
$connection->schema()->createTable('event', array(
'fields' => array(
'nid' => array(
@ -8271,7 +8245,6 @@ $connection->insert('event_timezones')
'is_dst' => '0',
))
->execute();
$connection->schema()->createTable('files', array(
'fields' => array(
'fid' => array(
@ -8394,7 +8367,6 @@ $connection->insert('files')
'timestamp' => '1420858106',
))
->execute();
$connection->schema()->createTable('filter_formats', array(
'fields' => array(
'format' => array(
@ -8459,7 +8431,6 @@ $connection->insert('filter_formats')
'cache' => '0',
))
->execute();
$connection->schema()->createTable('filters', array(
'fields' => array(
'fid' => array(
@ -8591,7 +8562,6 @@ $connection->insert('filters')
'weight' => '10',
))
->execute();
$connection->schema()->createTable('flood', array(
'fields' => array(
'fid' => array(
@ -8674,7 +8644,6 @@ $connection->insert('forum')
'tid' => '8',
))
->execute();
$connection->schema()->createTable('history', array(
'fields' => array(
'uid' => array(
@ -8770,7 +8739,6 @@ $connection->insert('history')
'timestamp' => '1534014763',
))
->execute();
$connection->schema()->createTable('i18n_blocks', array(
'fields' => array(
'ibid' => array(
@ -8832,7 +8800,6 @@ $connection->insert('i18n_blocks')
'language' => 'zu',
))
->execute();
$connection->schema()->createTable('i18n_strings', array(
'fields' => array(
'lid' => array(
@ -10071,8 +10038,31 @@ $connection->insert('i18n_strings')
'objectindex' => '0',
'format' => '0',
))
->values(array(
'lid' => '1692',
'objectid' => '14',
'type' => 'term',
'property' => 'name',
'objectindex' => '14',
'format' => '0',
))
->values(array(
'lid' => '1693',
'objectid' => '15',
'type' => 'term',
'property' => 'name',
'objectindex' => '15',
'format' => '0',
))
->values(array(
'lid' => '1694',
'objectid' => '14',
'type' => 'term',
'property' => 'description',
'objectindex' => '14',
'format' => '0',
))
->execute();
$connection->schema()->createTable('i18n_variable', array(
'fields' => array(
'name' => array(
@ -10512,7 +10502,6 @@ $connection->insert('i18n_variable')
'value' => 's:1:"1";',
))
->execute();
$connection->schema()->createTable('imagecache_action', array(
'fields' => array(
'actionid' => array(
@ -10598,7 +10587,6 @@ $connection->insert('imagecache_action')
'data' => 'a:3:{s:7:"degrees";s:2:"55";s:6:"random";i:0;s:7:"bgcolor";s:0:"";}',
))
->execute();
$connection->schema()->createTable('imagecache_preset', array(
'fields' => array(
'presetid' => array(
@ -10633,7 +10621,6 @@ $connection->insert('imagecache_preset')
'presetname' => 'big_blue_cheese',
))
->execute();
$connection->schema()->createTable('languages', array(
'fields' => array(
'language' => array(
@ -10769,7 +10756,6 @@ $connection->insert('languages')
'javascript' => '',
))
->execute();
$connection->schema()->createTable('locales_source', array(
'fields' => array(
'lid' => array(
@ -22642,8 +22628,28 @@ $connection->insert('locales_source')
'source' => 'White',
'version' => '1',
))
->values(array(
'lid' => '1692',
'location' => 'term:14:name',
'textgroup' => 'taxonomy',
'source' => 'Talos IV',
'version' => '1',
))
->values(array(
'lid' => '1693',
'location' => 'term:15:name',
'textgroup' => 'taxonomy',
'source' => 'Vulcan',
'version' => '1',
))
->values(array(
'lid' => '1694',
'location' => 'term:14:description',
'textgroup' => 'taxonomy',
'source' => 'The home of Captain Christopher Pike.',
'version' => '1',
))
->execute();
$connection->schema()->createTable('locales_target', array(
'fields' => array(
'lid' => array(
@ -27702,6 +27708,14 @@ $connection->insert('locales_target')
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1672',
'translation' => 'fr - Type',
'language' => 'fr',
'plid' => '0',
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1678',
'translation' => 'fr - I really, really, really love migrating ',
@ -27718,6 +27732,22 @@ $connection->insert('locales_target')
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1692',
'translation' => 'fr - Talos IV',
'language' => 'fr',
'plid' => '0',
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1694',
'translation' => 'fr - The home of Captain Christopher Pike.',
'language' => 'fr',
'plid' => '0',
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '66',
'translation' => 'zu - CCK - Aucune Intégration aux Vues',
@ -27830,6 +27860,14 @@ $connection->insert('locales_target')
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1672',
'translation' => 'zu - Type',
'language' => 'zu',
'plid' => '0',
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1690',
'translation' => 'Okumnyama',
@ -27846,8 +27884,23 @@ $connection->insert('locales_target')
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1693',
'translation' => 'zu - Vulcan',
'language' => 'zu',
'plid' => '0',
'plural' => '0',
'i18n_status' => '0',
))
->values(array(
'lid' => '1694',
'translation' => 'zu - The home of Captain Christopher Pike.',
'language' => 'zu',
'plid' => '0',
'plural' => '0',
'i18n_status' => '0',
))
->execute();
$connection->schema()->createTable('menu_custom', array(
'fields' => array(
'menu_name' => array(
@ -27896,7 +27949,6 @@ $connection->insert('menu_custom')
'description' => 'Secondary links are often used for pages like legal notices, contact details, and other secondary navigation items that play a lesser role than primary links',
))
->execute();
$connection->schema()->createTable('menu_links', array(
'fields' => array(
'menu_name' => array(
@ -34385,7 +34437,6 @@ $connection->insert('menu_links')
'updated' => '0',
))
->execute();
$connection->schema()->createTable('menu_router', array(
'fields' => array(
'path' => array(
@ -40934,6 +40985,28 @@ $connection->insert('menu_router')
'weight' => '0',
'file' => 'sites/all/modules/i18n/i18n.admin.inc',
))
->values(array(
'path' => 'admin/settings/language/i18n/variables',
'load_functions' => '',
'to_arg_functions' => '',
'access_callback' => 'user_access',
'access_arguments' => 'a:1:{i:0;s:29:"administer site configuration";}',
'page_callback' => 'drupal_get_form',
'page_arguments' => 'a:1:{i:0;s:25:"i18n_admin_variables_form";}',
'fit' => '31',
'number_parts' => '5',
'tab_parent' => 'admin/settings/language/i18n',
'tab_root' => 'admin/settings/language',
'title' => 'Variables',
'title_callback' => 't',
'title_arguments' => '',
'type' => '128',
'block_callback' => '',
'description' => 'Multilingual variables.',
'position' => '',
'weight' => '0',
'file' => 'sites/all/modules/i18n/i18n.admin.inc',
))
->values(array(
'path' => 'admin/settings/language/overview',
'load_functions' => '',
@ -43575,7 +43648,6 @@ $connection->insert('menu_router')
'file' => '',
))
->execute();
$connection->schema()->createTable('node', array(
'fields' => array(
'nid' => array(
@ -44070,7 +44142,6 @@ $connection->insert('node')
'translate' => '0',
))
->execute();
$connection->schema()->createTable('node_access', array(
'fields' => array(
'nid' => array(
@ -44141,7 +44212,6 @@ $connection->insert('node_access')
'grant_delete' => '0',
))
->execute();
$connection->schema()->createTable('node_comment_statistics', array(
'fields' => array(
'nid' => array(
@ -44290,7 +44360,6 @@ $connection->insert('node_comment_statistics')
'comment_count' => '1',
))
->execute();
$connection->schema()->createTable('node_counter', array(
'fields' => array(
'nid' => array(
@ -44437,7 +44506,6 @@ $connection->insert('node_counter')
'timestamp' => '1534014763',
))
->execute();
$connection->schema()->createTable('node_revisions', array(
'fields' => array(
'nid' => array(
@ -44787,7 +44855,6 @@ $connection->insert('node_revisions')
'format' => '1',
))
->execute();
$connection->schema()->createTable('node_type', array(
'fields' => array(
'type' => array(
@ -45087,7 +45154,6 @@ $connection->insert('node_type')
'orig_type' => 'test_story',
))
->execute();
$connection->schema()->createTable('permission', array(
'fields' => array(
'pid' => array(
@ -45153,7 +45219,6 @@ $connection->insert('permission')
'tid' => '0',
))
->execute();
$connection->schema()->createTable('profile_fields', array(
'fields' => array(
'fid' => array(
@ -45386,7 +45451,6 @@ $connection->insert('profile_fields')
'options' => '',
))
->execute();
$connection->schema()->createTable('profile_values', array(
'fields' => array(
'fid' => array(
@ -45603,7 +45667,6 @@ $connection->insert('profile_values')
'value' => '1',
))
->execute();
$connection->schema()->createTable('role', array(
'fields' => array(
'rid' => array(
@ -45651,7 +45714,6 @@ $connection->insert('role')
'name' => 'migrate test role 3 that is longer than thirty two characters',
))
->execute();
$connection->schema()->createTable('semaphore', array(
'fields' => array(
'name' => array(
@ -46957,7 +47019,6 @@ $connection->insert('system')
'info' => 'a:13:{s:4:"name";s:10:"Pushbutton";s:11:"description";s:52:"Tabled, multi-column theme in blue and orange tones.";s:7:"version";s:4:"6.38";s:4:"core";s:3:"6.x";s:6:"engine";s:11:"phptemplate";s:7:"project";s:6:"drupal";s:9:"datestamp";s:10:"1456343372";s:7:"regions";a:5:{s:4:"left";s:12:"Left sidebar";s:5:"right";s:13:"Right sidebar";s:7:"content";s:7:"Content";s:6:"header";s:6:"Header";s:6:"footer";s:6:"Footer";}s:8:"features";a:10:{i:0;s:20:"comment_user_picture";i:1;s:7:"favicon";i:2;s:7:"mission";i:3;s:4:"logo";i:4;s:4:"name";i:5;s:17:"node_user_picture";i:6;s:6:"search";i:7;s:6:"slogan";i:8;s:13:"primary_links";i:9;s:15:"secondary_links";}s:11:"stylesheets";a:1:{s:3:"all";a:1:{s:9:"style.css";s:27:"themes/pushbutton/style.css";}}s:7:"scripts";a:1:{s:9:"script.js";s:27:"themes/pushbutton/script.js";}s:10:"screenshot";s:32:"themes/pushbutton/screenshot.png";s:3:"php";s:5:"4.3.5";}',
))
->execute();
$connection->schema()->createTable('term_data', array(
'fields' => array(
'tid' => array(
@ -47091,8 +47152,70 @@ $connection->insert('term_data')
'language' => '',
'trid' => '0',
))
->values(array(
'tid' => '9',
'vid' => '3',
'name' => 'fr - term 4 of vocabulary 3',
'description' => '',
'weight' => '0',
'language' => 'fr',
'trid' => '1',
))
->values(array(
'tid' => '10',
'vid' => '3',
'name' => 'zu - term 4 of vocabulary 3',
'description' => '',
'weight' => '0',
'language' => 'zu',
'trid' => '1',
))
->values(array(
'tid' => '11',
'vid' => '3',
'name' => 'term 7 of vocabulary 3',
'description' => '',
'weight' => '0',
'language' => 'en',
'trid' => '2',
))
->values(array(
'tid' => '12',
'vid' => '3',
'name' => 'fr - term 7 of vocabulary 3',
'description' => '',
'weight' => '0',
'language' => 'fr',
'trid' => '2',
))
->values(array(
'tid' => '13',
'vid' => '3',
'name' => 'zu - term 7 of vocabulary 3',
'description' => '',
'weight' => '0',
'language' => 'zu',
'trid' => '2',
))
->values(array(
'tid' => '14',
'vid' => '5',
'name' => 'Talos IV',
'description' => 'The home of Captain Christopher Pike.',
'weight' => '0',
'language' => '',
'trid' => '0',
))
->values(array(
'tid' => '15',
'vid' => '5',
'name' => 'Vulcan',
'description' => '',
'weight' => '0',
'language' => '',
'trid' => '0',
))
->execute();
$connection->schema()->createTable('term_hierarchy', array(
'fields' => array(
'tid' => array(
@ -47142,6 +47265,34 @@ $connection->insert('term_hierarchy')
'tid' => '8',
'parent' => '0',
))
->values(array(
'tid' => '9',
'parent' => '0',
))
->values(array(
'tid' => '10',
'parent' => '0',
))
->values(array(
'tid' => '11',
'parent' => '0',
))
->values(array(
'tid' => '12',
'parent' => '0',
))
->values(array(
'tid' => '13',
'parent' => '0',
))
->values(array(
'tid' => '14',
'parent' => '0',
))
->values(array(
'tid' => '15',
'parent' => '0',
))
->values(array(
'tid' => '3',
'parent' => '2',
@ -47159,7 +47310,6 @@ $connection->insert('term_hierarchy')
'parent' => '5',
))
->execute();
$connection->schema()->createTable('term_node', array(
'fields' => array(
'nid' => array(
@ -47233,7 +47383,6 @@ $connection->insert('term_node')
'tid' => '8',
))
->execute();
$connection->schema()->createTable('term_relation', array(
'fields' => array(
'trid' => array(
@ -47397,7 +47546,6 @@ $connection->insert('upload')
'weight' => '0',
))
->execute();
$connection->schema()->createTable('url_alias', array(
'fields' => array(
'pid' => array(
@ -47487,7 +47635,6 @@ $connection->insert('url_alias')
'language' => '',
))
->execute();
$connection->schema()->createTable('users', array(
'fields' => array(
'uid' => array(
@ -47809,7 +47956,6 @@ $connection->insert('users')
'timezone_id' => '0',
))
->execute();
$connection->schema()->createTable('users_roles', array(
'fields' => array(
'uid' => array(
@ -47876,7 +48022,6 @@ $connection->insert('users_roles')
'rid' => '5',
))
->execute();
$connection->schema()->createTable('variable', array(
'fields' => array(
'name' => array(
@ -48740,7 +48885,7 @@ $connection->insert('variable')
))
->values(array(
'name' => 'i18ntaxonomy_vocabulary',
'value' => 'a:3:{i:1;s:1:"3";i:2;s:1:"2";i:3;s:1:"1";}',
'value' => 'a:4:{i:1;s:1:"3";i:2;s:1:"2";i:3;s:1:"3";i:5;s:1:"1";}',
))
->values(array(
'name' => 'i18n_lock_node_article',
@ -49143,7 +49288,6 @@ $connection->insert('variable')
'value' => 's:1:"1";',
))
->execute();
$connection->schema()->createTable('vocabulary', array(
'fields' => array(
'vid' => array(
@ -49343,7 +49487,6 @@ $connection->insert('vocabulary')
'language' => '',
))
->execute();
$connection->schema()->createTable('vocabulary_node_types', array(
'fields' => array(
'vid' => array(
@ -49401,7 +49544,6 @@ $connection->insert('vocabulary_node_types')
'type' => 'story',
))
->execute();
$connection->schema()->createTable('watchdog', array(
'fields' => array(
'wid' => array(

Some files were not shown because too many files have changed in this diff Show more