Index: plugins/FeedsFeedNodeProcessor.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsFeedNodeProcessor.inc,v retrieving revision 1.12 diff -u -p -r1.12 FeedsFeedNodeProcessor.inc --- plugins/FeedsFeedNodeProcessor.inc 28 Apr 2010 22:18:30 -0000 1.12 +++ plugins/FeedsFeedNodeProcessor.inc 1 May 2010 09:56:08 -0000 @@ -10,52 +10,7 @@ * Creates *feed* nodes from feed items. The difference to FeedsNodeProcessor is * that this plugin only creates nodes that are feed nodes themselves. */ -class FeedsFeedNodeProcessor extends FeedsProcessor { - - /** - * Implementation of FeedsProcessor::process(). - */ - public function process(FeedsImportBatch $batch, FeedsSource $source) { - while ($item = $batch->shiftItem()) { - - // If the target item does not exist OR if update_existing is enabled, - // map and save. - if (!$nid = $this->existingItemId($item, $source) || $this->config['update_existing']) { - - // Map item to a node. - $node = $this->map($item); - - // If updating populate nid and vid avoiding an expensive node_load(). - if (!empty($nid)) { - $node->nid = $nid; - $node->vid = db_result(db_query("SELECT vid FROM {node} WHERE nid = %d", $nid)); - } - - // Save the node. - node_save($node); - - if ($nid) { - $batch->updated++; - } - else { - $batch->created++; - } - } - } - - // Set messages. - if ($batch->created) { - drupal_set_message(format_plural($batch->created, 'Created @number @type node.', 'Created @number @type nodes.', array('@number' => $batch->created, '@type' => $this->config['content_type']))); - } - elseif ($batch->updated) { - drupal_set_message(format_plural($batch->updated, 'Updated @number @type node.', 'Updated @number @type nodes.', array('@number' => $batch->updated, '@type' => $this->config['content_type']))); - } - else { - drupal_set_message(t('There is no new content.')); - } - - return FEEDS_BATCH_COMPLETE; - } +class FeedsFeedNodeProcessor extends FeedsNodeProcessor { /** * Implementation of FeedsProcessor::clear(). @@ -70,20 +25,17 @@ class FeedsFeedNodeProcessor extends Fee /** * Execute mapping on an item. */ - protected function map($source_item) { - + protected function map($source_item, $target_node) { // Prepare node object. static $included; if (!$included) { module_load_include('inc', 'node', 'node.pages'); $included = TRUE; } - $target_node = new stdClass(); - $target_node->type = $this->config['content_type']; $target_node->feeds = array(); // Suppress auto import, we may be creating many feeds $target_node->feeds['suppress_import'] = TRUE; - node_object_prepare($target_node); + ($target_node); /* Assign an aggregated node always to current user. @@ -103,17 +55,17 @@ class FeedsFeedNodeProcessor extends Fee * Override parent::configDefaults(). */ public function configDefaults() { - return array( - 'content_type' => '', - 'update_existing' => 0, - 'mappings' => array(), - ); + $defaults = parent::configDefaults(); + $defaults['content_type'] = ''; // reset content type + return $defaults; } /** * Override parent::configForm(). */ public function configForm(&$form_state) { + $form = parent::configForm($form_state); + $feeds = feeds_importer_load_all(); $types = array(); foreach ($feeds as $feed) { @@ -133,16 +85,10 @@ class FeedsFeedNodeProcessor extends Fee $form['content_type'] = array( '#type' => 'select', '#title' => t('Content type'), - '#description' => t('Choose node type to create from this feed. Only node types with attached importer configurations are listed here. Note: Users with "import !feed_id feeds" permissions will be able to import nodes of the content type selected here regardless of the node level permissions. However, users with "clear !feed_id permissions" need to have sufficient node level permissions to delete the imported nodes.', array('!feed_id' => $this->id)), + '#description' => t('Choose default node type to create from this feed. Only node types with attached importer configurations are listed here. Note: Users with "import !feed_id feeds" permissions will be able to import nodes of the content type selected here regardless of the node level permissions. However, users with "clear !feed_id permissions" need to have sufficient node level permissions to delete the imported nodes.', array('!feed_id' => $this->id)), '#options' => $types, '#default_value' => $this->config['content_type'], ); - $form['update_existing'] = array( - '#type' => 'checkbox', - '#title' => t('Update existing items'), - '#description' => t('Check if existing items should be updated from the feed.'), - '#default_value' => $this->config['update_existing'], - ); return $form; } @@ -150,18 +96,22 @@ class FeedsFeedNodeProcessor extends Fee * Override setTargetElement to operate on a target item that is a node. */ public function setTargetElement($target_node, $target_element, $value) { - if ($target_element == 'source') { + parent::setTargetElement($target_node, $target_element, $value); + if ($target_element == 'url') { // Get the class of the feed node importer's fetcher and set the source // property. See feeds_nodeapi() how $node->feeds gets stored. $class = get_class($this->feedNodeImporter()->fetcher); $target_node->feeds[$class]['source'] = $value; } - elseif ($target_element == 'body') { - $target_node->teaser = $value; - $target_node->body = $value; - } - elseif (in_array($target_element, array('title', 'status', 'created'))) { - $target_node->$target_element = $value; + elseif ($target_element == 'type') { + if (feeds_get_importer_id($value)) { + // only accept node types with attached import configuration + $target_node->type = $value; + } + else { + // set default value + $target_node->type = $this->config['content_type']; + } } } @@ -169,44 +119,14 @@ class FeedsFeedNodeProcessor extends Fee * Return available mapping targets. */ public function getMappingTargets() { - $targets = array( - 'title' => array( - 'name' => t('Title'), - 'description' => t('The title of the feed node.'), - ), - 'status' => array( - 'name' => t('Published status'), - 'description' => t('Whether a feed node is published or not. 1 stands for published, 0 for not published.'), - ), - 'created' => array( - 'name' => t('Published date'), - 'description' => t('The UNIX time when a node has been published.'), - ), - 'body' => array( - 'name' => t('Body'), - 'description' => t('The body of the node. The teaser will be the same as the entire body.'), - ), - 'source' => array( - 'name' => t('Feed source'), - 'description' => t('Depending on the selected fetcher, this could be for example a URL or a path to a file.'), - 'optional_unique' => TRUE, - ), - ); - return $targets; - } - - /** - * Get nid of an existing feed item node if available. - */ - protected function existingItemId($source_item, FeedsSource $source) { - - // We only support one unique target: source - foreach ($this->uniqueTargets($source_item) as $target => $value) { - if ($target == 'source') { - return db_result(db_query("SELECT fs.feed_nid FROM {node} n JOIN {feeds_source} fs ON n.nid = fs.feed_nid WHERE fs.id = '%s' AND fs.source = '%s'", $this->feedNodeImporter()->id, $value)); - } + if (empty($this->config['content_type'])) { + // issue warning + drupal_set_message(t('No default content type selected'), 'warning'); + return array(); + } + else { + return parent::getMappingTargets(); } - return 0; } /** Index: tests/feeds.test =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds.test,v retrieving revision 1.15 diff -u -p -r1.15 feeds.test --- tests/feeds.test 28 Apr 2010 20:45:37 -0000 1.15 +++ tests/feeds.test 1 May 2010 09:56:16 -0000 @@ -841,3 +841,182 @@ class FeedsSyndicationParserTestCase ext ); } } + +/** + * Test importing OPML that creates feed nodes. + */ +class FeedsOPMLToFeedNodesTest extends FeedsWebTestCase { + + /** + * Describe this test. + */ + public function getInfo() { + return array( + 'name' => t('OPML import to feed nodes.'), + 'description' => t('Tests a feed configuration with file import, uses OPML parser and a feed node processor.'), + 'group' => t('Feeds'), + ); + } + + /** + * Set up test. + */ + public function setUp() { + parent::setUp('feeds', 'feeds_ui', 'ctools'); + $this->drupalLogin( + $this->drupalCreateUser( + array( + 'administer feeds', 'administer nodes', + ) + ) + ); + } + + /** + * Generate an OPML test feed that points to RSS feeds. + */ + public function generateOPML() { + $path = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/'; + + $output = +' + + + OPML + Fri, 16 Oct 2009 02:53:17 GMT + Nuvole + + + + + + + +'; + + // UTF 8 encode output string and write it to disk + $output = utf8_encode($output); + $file = $this->absolute() .'/'. file_directory_path() .'/test-opml-'. $this->randomName() .'.opml'; + $handle = fopen($file, 'w'); + fwrite($handle, $output); + fclose($handle); + return $file; + } + + /** + * Test feedfeed node and feed node creation. + */ + public function test() { + $this->createFeed('feed'); + $this->createFeedFeed('feedfeed', 'feed'); + + // Import OPML and assert. + $file = $this->generateOPML(); + $this->importFile('feedfeed', $file); + $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_source}')); + // Check that we have 3 feeds added (the feedfeed + two created feeds) + $this->assertEqual($count, 3, 'Found '. $count .' number of items.'); + + // Assert DB status for feed nodes + $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_node_item} WHERE id="%s"', 'feedfeed')); + $this->assertEqual($count, 2, 'Found '. $count .' number of items.'); + + // run cron so that feeds import nodes + $this->drupalGet('cron.php'); + + // Assert DB status for feed nodes. + $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_node_item} WHERE id="%s"', 'feed')); + $this->assertEqual($count, 35, 'Accurate number of items in database: '. $count); + } + + public function createFeedFeed($id, $feed_id) { + // Create our master feed that will import a listing of feeds. + $this->createFeedConfiguration('Import OPML files generating feed node types.', $id); + + // Set and configure plugins. + $this->setPlugin($id, 'FeedsFileFetcher'); + $this->setPlugin($id, 'FeedsOPMLParser'); + $this->setPlugin($id, 'FeedsFeedNodeProcessor'); + + // Change feed node type to recently created 'feed'. + $edit = array( + 'content_type' => $feed_id, + ); + $this->drupalPost('admin/build/feeds/edit/' . $id .'/settings/FeedsFeedNodeProcessor', $edit, 'Save'); + + // Change some of the basic configuration. + $edit = array( + 'content_type' => '', // don't attach + 'import_period' => FEEDS_SCHEDULE_NEVER, + ); + $this->drupalPost('admin/build/feeds/edit/'. $id .'/settings', $edit, 'Save'); + + // Add mappings + $this->addMappings($id, + array( + array( + 'source' => 'title', + 'target' => 'title', + 'unique' => FALSE, + ), + array( + 'source' => 'xmlurl', + 'target' => 'url', + 'unique' => TRUE, + ) + ) + ); + } + + public function createFeed($id) { + // Create content type + $this->drupalCreateContentType(array('type' => $id)); + + // Create feed. + $this->createFeedConfiguration('Feed', $id); + + // Set and configure plugins. + $this->setPlugin($id, 'FeedsHTTPFetcher'); + $this->setPlugin($id, 'FeedsSyndicationParser'); + $this->setPlugin($id, 'FeedsNodeProcessor'); + + // Change some of the basic configuration. + $edit = array( + 'content_type' => $id, + 'import_period' => 0, // we need to schedule import as often as possible, as import on create is disabled here + 'import_on_create' => 1, + ); + $this->drupalPost('admin/build/feeds/edit/' . $id .'/settings', $edit, 'Save'); + + // Add mappings + $this->addMappings($id, + array( + array( + 'source' => 'title', + 'target' => 'title', + 'unique' => FALSE, + ), + array( + 'source' => 'description', + 'target' => 'body', + 'unique' => FALSE, + ), + array( + 'source' => 'timestamp', + 'target' => 'created', + 'unique' => FALSE, + ), + array( + 'source' => 'url', + 'target' => 'url', + 'unique' => TRUE, + ), + array( + 'source' => 'guid', + 'target' => 'guid', + 'unique' => TRUE, + ), + ) + ); + } +}