diff --git a/feeds.info b/feeds.info index 11934e0d83268c1c57a51daa187ab736337fb805..c57e20329ef1c3215b2441a7e9935ce60f5551a7 100644 --- a/feeds.info +++ b/feeds.info @@ -46,6 +46,9 @@ files[] = tests/feeds_mapper_config.test files[] = tests/feeds_fetcher_file.test files[] = tests/feeds_mapper_format_config.test files[] = tests/feeds_fetcher_http.test +files[] = tests/feeds_i18n.test +files[] = tests/feeds_i18n_node.test +files[] = tests/feeds_i18n_taxonomy.test files[] = tests/feeds_parser_sitemap.test files[] = tests/feeds_parser_syndication.test files[] = tests/feeds_processor_node.test diff --git a/plugins/FeedsNodeProcessor.inc b/plugins/FeedsNodeProcessor.inc index 2cfdf3061195f345bfff51fb89908ee019852fe6..4170c92d23e1de31f1c1a39e0fbcc79d6ef5787a 100644 --- a/plugins/FeedsNodeProcessor.inc +++ b/plugins/FeedsNodeProcessor.inc @@ -36,11 +36,10 @@ class FeedsNodeProcessor extends FeedsProcessor { * Creates a new node in memory and returns it. */ protected function newEntity(FeedsSource $source) { - $node = new stdClass(); + $node = parent::newEntity($source); $node->type = $this->bundle(); $node->changed = REQUEST_TIME; $node->created = REQUEST_TIME; - $node->language = LANGUAGE_NONE; $node->is_new = TRUE; node_object_prepare($node); // Populate properties that are set by node_object_prepare(). @@ -127,6 +126,8 @@ class FeedsNodeProcessor extends FeedsProcessor { * Validates a node. */ protected function entityValidate($entity) { + parent::entityValidate($entity); + if (!isset($entity->uid) || !is_numeric($entity->uid)) { $entity->uid = $this->config['author']; } diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc index 385ee0a720f2fe099dc8568c87dd19227f814e52..0644febbdf0c0020c5b7cf0493886cbce43cc155 100644 --- a/plugins/FeedsProcessor.inc +++ b/plugins/FeedsProcessor.inc @@ -86,13 +86,22 @@ abstract class FeedsProcessor extends FeedsPlugin { /** * Create a new entity. * - * @param $source + * @param FeedsSource $source * The feeds source that spawns this entity. * - * @return + * @return object * A new entity object. */ - protected abstract function newEntity(FeedsSource $source); + protected function newEntity(FeedsSource $source) { + $entity = new stdClass(); + + $info = $this->entityInfo(); + if (!empty($info['entity keys']['language'])) { + $entity->{$info['entity keys']['language']} = $this->entityLanguage(); + } + + return $entity; + } /** * Load an existing entity. @@ -109,28 +118,46 @@ abstract class FeedsProcessor extends FeedsPlugin { * existing ids first. */ protected function entityLoad(FeedsSource $source, $entity_id) { + $info = $this->entityInfo(); + if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) { $entities = entity_load($this->entityType(), array($entity_id)); - return reset($entities); + $entity = reset($entities); + } + else { + $args = array(':entity_id' => $entity_id); + $table = db_escape_table($info['base table']); + $key = db_escape_field($info['entity keys']['id']); + $entity = db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject(); } - $info = $this->entityInfo(); - - $args = array(':entity_id' => $entity_id); - - $table = db_escape_table($info['base table']); - $key = db_escape_field($info['entity keys']['id']); + if ($entity && !empty($info['entity keys']['language'])) { + $entity->{$info['entity keys']['language']} = $this->entityLanguage(); + } - return db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject(); + return $entity; } /** - * Validate an entity. + * Validates an entity. * * @throws FeedsValidationException $e - * If validation fails. + * Thrown if validation fails. */ - protected function entityValidate($entity) {} + protected function entityValidate($entity) { + $info = $this->entityInfo(); + if (empty($info['entity keys']['language'])) { + return; + } + + // Ensure that a valid language is always set. + $key = $info['entity keys']['language']; + $languages = language_list('enabled'); + + if (empty($entity->$key) || !isset($languages[1][$entity->$key])) { + $entity->$key = $this->entityLanguage(); + } + } /** * Access check for saving an enity. @@ -169,6 +196,28 @@ abstract class FeedsProcessor extends FeedsPlugin { return entity_get_info($this->entityType()); } + /** + * Returns the current language for entities. + * + * This checks if the configuration value is valid. + * + * @return string + * The current language code. + */ + protected function entityLanguage() { + if (!module_exists('locale')) { + // language_list() may return languages even if the locale module is + // disabled. See https://www.drupal.org/node/173227 why. + // When the locale module is disabled, there are no selectable languages + // in the UI, so the content should be imported in LANGUAGE_NONE. + return LANGUAGE_NONE; + } + + $languages = language_list('enabled'); + + return isset($languages[1][$this->config['language']]) ? $this->config['language'] : LANGUAGE_NONE; + } + /** * @} */ @@ -764,6 +813,7 @@ abstract class FeedsProcessor extends FeedsPlugin { 'input_format' => NULL, 'skip_hash_check' => FALSE, 'bundle' => $bundle, + 'language' => LANGUAGE_NONE, ); } @@ -790,6 +840,16 @@ abstract class FeedsProcessor extends FeedsPlugin { ); } + if (module_exists('locale') && !empty($info['entity keys']['language'])) { + $form['language'] = array( + '#type' => 'select', + '#options' => array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name'), + '#title' => t('Language'), + '#required' => TRUE, + '#default_value' => $this->config['language'], + ); + } + $tokens = array('@entities' => strtolower($info['label plural'])); $form['insert_new'] = array( diff --git a/plugins/FeedsTermProcessor.inc b/plugins/FeedsTermProcessor.inc index c600b978771262eac0bec42f66bc0e32d523ad50..bae88a4a481330a6a4d53e550dafbb4829fd1234 100644 --- a/plugins/FeedsTermProcessor.inc +++ b/plugins/FeedsTermProcessor.inc @@ -31,9 +31,10 @@ class FeedsTermProcessor extends FeedsProcessor { */ protected function newEntity(FeedsSource $source) { $vocabulary = $this->vocabulary(); - $term = new stdClass(); + $term = parent::newEntity($source); $term->vid = $vocabulary->vid; $term->vocabulary_machine_name = $vocabulary->machine_name; + return $term; } @@ -41,6 +42,8 @@ class FeedsTermProcessor extends FeedsProcessor { * Validates a term. */ protected function entityValidate($term) { + parent::entityValidate($term); + if (drupal_strlen($term->name) == 0) { throw new FeedsValidationException(t('Term name missing.')); } diff --git a/plugins/FeedsUserProcessor.inc b/plugins/FeedsUserProcessor.inc index 778c72bedb2864f1b5437e07e03881b1caf4a8df..47d26e5cda4b8931b7ad02a07fd5bfddcd2f8c6a 100644 --- a/plugins/FeedsUserProcessor.inc +++ b/plugins/FeedsUserProcessor.inc @@ -37,10 +37,11 @@ class FeedsUserProcessor extends FeedsProcessor { * Creates a new user account in memory and returns it. */ protected function newEntity(FeedsSource $source) { - $account = new stdClass(); + $account = parent::newEntity($source); $account->uid = 0; $account->roles = array_filter($this->config['roles']); $account->status = $this->config['status']; + return $account; } @@ -59,6 +60,8 @@ class FeedsUserProcessor extends FeedsProcessor { * Validates a user account. */ protected function entityValidate($account) { + parent::entityValidate($account); + if (empty($account->name) || empty($account->mail) || !valid_email_address($account->mail)) { throw new FeedsValidationException(t('User name missing or email not valid.')); } diff --git a/tests/feeds/content_i18n.csv b/tests/feeds/content_i18n.csv new file mode 100644 index 0000000000000000000000000000000000000000..13093581272cef60936b2d4b4e01ff4584e77efe --- /dev/null +++ b/tests/feeds/content_i18n.csv @@ -0,0 +1,3 @@ +"guid","title","created","alpha","beta","gamma","delta","body","language" +1,"Lorem ipsum",1251936720,"Lorem",42,"4.2",3.14159265,"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","nl" +2,"Ut wisi enim ad minim veniam",1251932360,"Ut wisi",32,"1.2",5.62951413,"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat." diff --git a/tests/feeds_i18n.test b/tests/feeds_i18n.test new file mode 100644 index 0000000000000000000000000000000000000000..9db997edd56732fe7f81346f22b5f32b67560842 --- /dev/null +++ b/tests/feeds_i18n.test @@ -0,0 +1,133 @@ +<?php + +/** + * @file + * Contains Feedsi18nTestCase. + */ + +/** + * Tests importing data in a language. + */ +class Feedsi18nTestCase extends FeedsMapperTestCase { + + /** + * The entity type to be tested. + * + * @var string + */ + protected $entityType; + + /** + * The processor being used. + * + * @var string + */ + protected $processorName; + + public function setUp($modules = array(), $permissions = array()) { + $modules = array_merge($modules, array( + 'locale', + )); + $permissions = array_merge(array( + 'administer languages', + )); + parent::setUp($modules, $permissions); + + // Setup other languages. + $edit = array( + 'langcode' => 'nl', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertText(t('The language Dutch has been created and can now be used.')); + $edit = array( + 'langcode' => 'de', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertText(t('The language German has been created and can now be used.')); + + // Include FeedsProcessor.inc to make its constants available. + module_load_include('inc', 'feeds', 'plugins/FeedsProcessor'); + + // Create and configure importer. + $this->createImporterConfiguration('Multilingual term importer', 'i18n'); + $this->setPlugin('i18n', 'FeedsFileFetcher'); + $this->setPlugin('i18n', 'FeedsCSVParser'); + $this->setPlugin('i18n', $this->processorName); + } + + /** + * Tests if entities get the language assigned that is set in the processor. + */ + public function testImport() { + // Import content in German. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the entity's language is in German. + $entities = entity_load($this->entityType, array(1, 2)); + foreach ($entities as $entity) { + $this->assertEqual('de', entity_language($this->entityType, $entity)); + } + } + + /** + * Tests if entities get a different language assigned when the processor's language + * is changed. + */ + public function testChangedLanguageImport() { + // Import content in German. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Change processor's language to Dutch. + $this->setSettings('i18n', $this->processorName, array('language' => 'nl')); + + // Re-import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the entity's language is now in Dutch. + $entities = entity_load($this->entityType, array(1, 2)); + foreach ($entities as $entity) { + $this->assertEqual('nl', entity_language($this->entityType, $entity)); + } + } + + /** + * Tests if items are imported in LANGUAGE_NONE if the processor's language is disabled. + */ + public function testDisabledLanguage() { + // Disable the German language. + $path = 'admin/config/regional/language'; + $edit = array( + 'enabled[de]' => FALSE, + ); + $this->drupalPost($path, $edit, t('Save configuration')); + + // Import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the entities have no language assigned. + $entities = entity_load($this->entityType, array(1, 2)); + foreach ($entities as $entity) { + $language = entity_language($this->entityType, $entity); + $this->assertEqual(LANGUAGE_NONE, $language, format_string('The entity is language neutral (actual: !language).', array('!language' => $language))); + } + } + + /** + * Tests if items are imported in LANGUAGE_NONE if the processor's language is removed. + */ + public function testRemovedLanguage() { + // Remove the German language. + $path = 'admin/config/regional/language/delete/de'; + $this->drupalPost($path, array(), t('Delete')); + + // Import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the entities have no language assigned. + $entities = entity_load($this->entityType, array(1, 2)); + foreach ($entities as $entity) { + $language = entity_language($this->entityType, $entity); + $this->assertEqual(LANGUAGE_NONE, $language, format_string('The entity is language neutral (actual: !language).', array('!language' => $language))); + } + } +} diff --git a/tests/feeds_i18n_node.test b/tests/feeds_i18n_node.test new file mode 100644 index 0000000000000000000000000000000000000000..00300eb05ebe49b0c18c67eebf1de269585aa29c --- /dev/null +++ b/tests/feeds_i18n_node.test @@ -0,0 +1,136 @@ +<?php + +/** + * @file + * Contains Feedsi18nNodeTestCase. + */ + +/** + * Tests importing nodes in a language. + */ +class Feedsi18nNodeTestCase extends Feedsi18nTestCase { + + /** + * Name of created content type. + * + * @var string + */ + private $contentType; + + public static function getInfo() { + return array( + 'name' => 'Multilingual content', + 'description' => 'Tests Feeds multilingual support for nodes.', + 'group' => 'Feeds', + 'dependencies' => array('locale'), + ); + } + + public function setUp($modules = array(), $permissions = array()) { + $this->entityType = 'node'; + $this->processorName = 'FeedsNodeProcessor'; + + parent::setUp($modules, $permissions); + + // Create content type. + $this->contentType = $this->createContentType(); + + // Configure importer. + $this->setSettings('i18n', $this->processorName, array( + 'bundle' => $this->contentType, + 'language' => 'de', + 'update_existing' => FEEDS_UPDATE_EXISTING, + 'skip_hash_check' => TRUE, + )); + $this->addMappings('i18n', array( + 0 => array( + 'source' => 'guid', + 'target' => 'guid', + 'unique' => '1', + ), + 1 => array( + 'source' => 'title', + 'target' => 'title', + ), + )); + } + + /** + * Tests if the language setting is available on the processor. + */ + public function testAvailableProcessorLanguageSetting() { + // Check if the language setting is available when the locale module is enabled. + $this->drupalGet('admin/structure/feeds/i18n/settings/FeedsNodeProcessor'); + $this->assertField('language', 'Language field is available on the node processor settings when the locale module is enabled.'); + + // Disable the locale module and check if the language setting is no longer available. + module_disable(array('locale')); + $this->drupalGet('admin/structure/feeds/i18n/settings/FeedsNodeProcessor'); + $this->assertNoField('language', 'Language field is not available on the node processor settings when the locale module is disabled.'); + } + + /** + * Tests processor language setting in combination with language mapping target. + */ + public function testWithLanguageMappingTarget() { + $this->addMappings('i18n', array( + 2 => array( + 'source' => 'language', + 'target' => 'language', + ), + )); + + // Import csv file. The first item has a language specified (Dutch), the second + // one has no language specified and should be imported in the processor's language (German). + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content_i18n.csv'); + + // The first node should be Dutch. + $node = node_load(1); + $this->assertEqual('nl', entity_language('node', $node), 'Item 1 has the Dutch language assigned.'); + + // The second node should be German. + $node = node_load(2); + $this->assertEqual('de', entity_language('node', $node), 'Item 2 has the German language assigned.'); + } + + /** + * Tests if nodes get imported in LANGUAGE_NONE when the locale module gets disabled. + */ + public function testDisabledLocaleModule() { + module_disable(array('locale')); + // Make sure that entity info is reset. + drupal_flush_all_caches(); + drupal_static_reset(); + + // Import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the content has no language assigned. + for ($i = 1; $i <= 2; $i++) { + $node = node_load($i); + $language = entity_language('node', $node); + $this->assertEqual(LANGUAGE_NONE, $language, format_string('The node is language neutral (actual: !language).', array('!language' => $language))); + } + } + + /** + * Tests if nodes get imported in LANGUAGE_NONE when the locale module gets uninstalled. + */ + public function testUninstalledLocaleModule() { + module_disable(array('locale')); + drupal_uninstall_modules(array('locale')); + // Make sure that entity info is reset. + drupal_flush_all_caches(); + drupal_static_reset(); + + // Import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the content has no language assigned. + for ($i = 1; $i <= 2; $i++) { + $node = node_load($i); + $language = entity_language('node', $node); + $this->assertEqual(LANGUAGE_NONE, $language, format_string('The node is language neutral (actual: !language).', array('!language' => $language))); + } + } +} diff --git a/tests/feeds_i18n_taxonomy.test b/tests/feeds_i18n_taxonomy.test new file mode 100644 index 0000000000000000000000000000000000000000..1b69209368122a8627c3739bf2315bd4c5eddc69 --- /dev/null +++ b/tests/feeds_i18n_taxonomy.test @@ -0,0 +1,119 @@ +<?php + +/** + * @file + * Contains Feedsi18nTaxonomyTestCase. + */ + +/** + * Tests importing terms in a language. + */ +class Feedsi18nTaxonomyTestCase extends Feedsi18nTestCase { + + /** + * Name of created vocabulary. + * + * @var string + */ + private $vocabulary; + + public static function getInfo() { + return array( + 'name' => 'Multilingual terms', + 'description' => 'Tests Feeds multilingual support for taxonomy terms.', + 'group' => 'Feeds', + 'dependencies' => array('locale', 'i18n_taxonomy'), + ); + } + + public function setUp($modules = array(), $permissions = array()) { + $this->entityType = 'taxonomy_term'; + $this->processorName = 'FeedsTermProcessor'; + + $modules = array_merge($modules, array( + 'i18n_taxonomy', + )); + parent::setUp($modules, $permissions); + + // Create vocabulary. + $this->vocabulary = strtolower($this->randomName(8)); + $edit = array( + 'name' => $this->vocabulary, + 'machine_name' => $this->vocabulary, + ); + $this->drupalPost('admin/structure/taxonomy/add', $edit, t('Save')); + + // Configure importer. + $this->setSettings('i18n', $this->processorName, array( + 'bundle' => $this->vocabulary, + 'language' => 'de', + 'update_existing' => FEEDS_UPDATE_EXISTING, + 'skip_hash_check' => TRUE, + )); + $this->addMappings('i18n', array( + 0 => array( + 'source' => 'guid', + 'target' => 'guid', + 'unique' => '1', + ), + 1 => array( + 'source' => 'title', + 'target' => 'name', + ), + )); + } + + /** + * Tests if the language setting is available on the processor. + */ + public function testAvailableProcessorLanguageSetting() { + // Check if the language setting is available when the i18n_taxonomy module is enabled. + $this->drupalGet('admin/structure/feeds/i18n/settings/FeedsTermProcessor'); + $this->assertField('language', 'Language field is available on the term processor settings when the i18n_taxonomy module is enabled.'); + + // Disable the i18n_taxonomy module and check if the language setting is no longer available. + module_disable(array('i18n_taxonomy')); + $this->drupalGet('admin/structure/feeds/i18n/settings/FeedsTermProcessor'); + $this->assertNoField('language', 'Language field is not available on the term processor settings when the i18n_taxonomy module is disabled.'); + } + + /** + * Tests if terms get imported in LANGUAGE_NONE when the i18n_taxonomy module gets disabled. + */ + public function testDisabledi18nTaxonomyModule() { + module_disable(array('i18n_taxonomy')); + // Make sure that entity info is reset. + drupal_flush_all_caches(); + drupal_static_reset(); + + // Import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the terms have no language assigned. + $entities = entity_load($this->entityType, array(1, 2)); + foreach ($entities as $entity) { + // Terms shouldn't have a language property. + $this->assertFalse(isset($entity->language), 'The term does not have a language.'); + } + } + + /** + * Tests if terms get imported in LANGUAGE_NONE when the i18n_taxonomy module gets uninstalled. + */ + public function testUninstalledi18nTaxonomyModule() { + module_disable(array('i18n_taxonomy')); + drupal_uninstall_modules(array('i18n_taxonomy')); + // Make sure that entity info is reset. + drupal_flush_all_caches(); + drupal_static_reset(); + + // Import content. + $this->importFile('i18n', $this->absolutePath() . '/tests/feeds/content.csv'); + + // Assert that the terms have no language assigned. + $entities = entity_load($this->entityType, array(1, 2)); + foreach ($entities as $entity) { + $this->assertFalse(isset($entity->language), 'The term does not have a language.'); + } + } +}