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. <strong>Note:</strong> Users with "import !feed_id feeds" permissions will be able to <strong>import</strong> 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 <strong>default</strong> node type to create from this feed. Only node types with attached importer configurations are listed here. <strong>Note:</strong> Users with "import !feed_id feeds" permissions will be able to <strong>import</strong> 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 =
+'<?xml version="1.0" encoding="utf-8"?>
+<opml version="1.1">
+<head>
+    <title>OPML</title>
+    <dateCreated>Fri, 16 Oct 2009 02:53:17 GMT</dateCreated>
+    <ownerName>Nuvole</ownerName>
+</head>
+<body>
+  <outline text="Feeds test group" >
+       <outline title="Devseed News" text="" xmlUrl="'. $path .'developmentseed.rss2" />
+       <outline title="Drupal Planet" text="" xmlUrl="'. $path .'drupalplanet.rss2" />
+    </outline>
+</body>
+</opml>';    
+
+    // 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,
+        ),
+      )
+    ); 
+  } 
+}
