? 728534_4_feedsfeed_0.patch ? 731338-2_content_type_0.patch ? 731338-7_content_type_1.patch Index: feeds.plugins.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.plugins.inc,v retrieving revision 1.4 diff -u -p -r1.4 feeds.plugins.inc --- feeds.plugins.inc 25 Jan 2010 20:03:05 -0000 1.4 +++ feeds.plugins.inc 10 Mar 2010 11:58:15 -0000 @@ -136,7 +136,7 @@ function _feeds_feeds_plugins() { 'description' => 'Create Feed nodes.', 'help' => 'Create Feed nodes from parsed content. Feed nodes are nodes that can import feeds themselves. This can be useful for instance when importing OPML feeds.', 'handler' => array( - 'parent' => 'FeedsProcessor', + 'parent' => 'FeedsNodeProcessor', 'class' => 'FeedsFeedNodeProcessor', 'file' => 'FeedsFeedNodeProcessor.inc', 'path' => $path, Index: plugins/FeedsFeedNodeProcessor.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsFeedNodeProcessor.inc,v retrieving revision 1.8 diff -u -p -r1.8 FeedsFeedNodeProcessor.inc --- plugins/FeedsFeedNodeProcessor.inc 10 Feb 2010 23:49:35 -0000 1.8 +++ plugins/FeedsFeedNodeProcessor.inc 10 Mar 2010 11:58:15 -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(t('Created !number !type nodes.', array('!number' => $batch->created, '!type' => $this->config['content_type']))); - } - elseif ($batch->updated) { - drupal_set_message(t('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(). @@ -66,20 +21,17 @@ class FeedsFeedNodeProcessor extends Fee // user created by hand. throw new Exception(t('This configuration does not support deleting imported items.')); } - + /** * 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; @@ -98,22 +50,22 @@ class FeedsFeedNodeProcessor extends Fee // Have parent class do the iterating. return parent::map($source_item, $target_node); } - + /** * 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) { @@ -129,20 +81,15 @@ class FeedsFeedNodeProcessor extends Fee '' => t('Select'), ) + $types; } - $form = array(); + + // Overwrite content type $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,65 +97,41 @@ 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']; + } + node_object_prepare($target_node); } } - + /** - * Return available mapping targets. + * Override getMappingTarget to issue a warning when no importer content + * type has been selected. */ 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 content type selected'), 'warning'); + return array(); + } + else { + return parent::getMappingTargets(); } - return 0; } - + /** * Helper for retrieving the importer object for the feed nodes to produce. */ @@ -220,4 +143,5 @@ class FeedsFeedNodeProcessor extends Fee throw new Exception(t('Content type to be created is not a valid Feed content type.')); } } + } \ No newline at end of file Index: tests/feeds.test =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds.test,v retrieving revision 1.9 diff -u -p -r1.9 feeds.test --- tests/feeds.test 10 Feb 2010 23:49:35 -0000 1.9 +++ tests/feeds.test 10 Mar 2010 11:58:16 -0000 @@ -821,4 +821,183 @@ 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 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, + ), + ) + ); + } } \ No newline at end of file