? tests/feeds/developmentseed.rss3
Index: feeds.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.install,v
retrieving revision 1.4
diff -u -p -r1.4 feeds.install
--- feeds.install	20 Oct 2009 20:59:04 -0000	1.4
+++ feeds.install	23 Nov 2009 19:27:10 -0000
@@ -164,6 +164,13 @@ function feeds_schema() {
         'not null' => TRUE,
         'description' => t('Unique identifier for the feed item.'),
       ),
+      'hash' => array(
+        'type' => 'varchar',
+        'length' => 32, //The length of the output of MD5
+        'not null' => TRUE,
+        'default' => '',
+        'description' => t('The hash of the item.'),
+      ),
     ),
     'primary key' => array('nid'),
     'indexes' => array(
@@ -291,4 +298,22 @@ function feeds_update_6006() {
   db_add_index($ret, 'feeds_schedule', 'feed_nid', array('feed_nid'));
 
   return $ret;
+}
+
+/**
+ * Add hash column to feeds_node_item.
+ */
+function feeds_update_6007() {
+  $ret = array();
+
+  $spec = array(
+    'type' => 'varchar',
+    'length' => 32, //The length of the output of md5()
+    'not null' => TRUE,
+    'default' => '',
+    'description' => t('The hash of the item.'),
+  );
+  db_add_field($ret, 'feeds_node_item', 'hash', $spec);
+  
+  return $ret;
 }
\ No newline at end of file
Index: plugins/FeedsNodeProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsNodeProcessor.inc,v
retrieving revision 1.15
diff -u -p -r1.15 FeedsNodeProcessor.inc
--- plugins/FeedsNodeProcessor.inc	18 Nov 2009 16:53:48 -0000	1.15
+++ plugins/FeedsNodeProcessor.inc	23 Nov 2009 19:27:10 -0000
@@ -17,36 +17,56 @@ class FeedsNodeProcessor extends FeedsPr
   public function process(FeedsParserResult $parserResult, FeedsSource $source) {
 
     // Count number of created and updated nodes.
-    $created  = $updated = 0;
+    $created = $updated = 0;
 
     foreach ($parserResult->value['items'] as $item) {
       // 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);
-
-        // Add some default information.
-        $node->feeds_node_item->id = $this->id;
-        $node->feeds_node_item->imported = FEEDS_REQUEST_TIME;
-        $node->feeds_node_item->feed_nid = $source->feed_nid;
-
+        $hash = $this->hash($item);
+        $node = new stdClass();
+        
         // If updating populate nid and vid avoiding an expensive node_load().
         if (!empty($nid)) {
+          if ($hash == $this->getHash($nid)) {
+            // No change, nothing to do.
+            continue;
+          }
           $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) {
           $updated++;
         }
         else {
           $created++;
         }
+        
+        $node->type = $this->config['content_type'];
+        $node->feeds_node_item = new stdClass();
+        $node->feeds_node_item->hash = $hash;
+         
+        // Prepare node object.
+        static $included;
+        if (!$included) {
+          module_load_include('inc', 'node', 'node.pages');
+          $included = TRUE;
+        }
+        node_object_prepare($node);
+        $node->log = 'Created/updated by FeedsNodeProcessor';
+        
+        // Execute mappings from $item to $node.
+        $this->map($item, $node);
+        
+        // Add some default information.
+        $node->feeds_node_item->id = $this->id;
+        $node->feeds_node_item->imported = FEEDS_REQUEST_TIME;
+        $node->feeds_node_item->feed_nid = $source->feed_nid;
+        
+        // Assign an aggregated node always to anonymous.
+        // @todo: change to feed node uid to keep in line with feedapi?
+        $node->uid = 0;
+
+        // Save the node.
+        node_save($node);
       }
     }
 
@@ -108,22 +128,7 @@ class FeedsNodeProcessor extends FeedsPr
   /**
    * Execute mapping on an item.
    */
-  protected function map($source_item) {
-    // 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_node_item = new stdClass();
-    node_object_prepare($target_node);
-    // Assign an aggregated node always to anonymous.
-    // @todo: change to feed node uid to keep in line with feedapi.
-    $target_node->uid = 0;
-    $target_node->log = 'Created/updated by FeedsNodeProcessor';
-
+  protected function map($source_item, $target_node) {
     // Load all mappers. parent::map() might invoke their callbacks.
     self::loadMappers();
 
@@ -316,6 +321,29 @@ class FeedsNodeProcessor extends FeedsPr
     }
     $loaded = TRUE;
   }
+  
+  /**
+   * Create MD5 hash of $item array.
+   * @return Always returns a hash, even with empty, NULL, FALSE:
+   *  Empty arrays return 40cd750bba9870f18aada2478b24840a
+   *  Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
+   */
+  protected function hash($item) {
+    return hash('md5', serialize($item));
+  }
+  
+  /**
+   * Retrieve MD5 hash of $nid from DB.
+   * @return Empty string if no item is found, hash otherwise.
+   */
+  protected function getHash($nid) {
+    $hash = db_result(db_query('SELECT hash FROM {feeds_node_item} WHERE nid = %d', $nid));
+    if ($hash) {
+      // Return with the hash.
+      return $hash;
+    }
+    return '';
+  }
 }
 
 /**
@@ -343,4 +371,4 @@ function _feeds_node_delete($nid) {
   }
   watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title));
   drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
-}
\ 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.4
diff -u -p -r1.4 feeds.test
--- tests/feeds.test	12 Nov 2009 23:56:50 -0000	1.4
+++ tests/feeds.test	23 Nov 2009 19:27:11 -0000
@@ -110,6 +110,27 @@ class FeedsRSStoNodesTest extends FeedsW
     // Assert DB status, there still shouldn't be more than 10 items.
     $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_node_item}'));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
+    
+    // Import updated feed file.
+    $feed_url = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/developmentseed.rss3';
+    $this->setUpdateExisting('syndication');
+    $this->editFeedNode($nid, $feed_url);
+    $this->drupalPost('node/' . $nid . '/import', array(), 'Import');
+    $this->assertText('Updated 2 Story nodes.');
+    
+    // Assert accuracy of aggregated information (check 2 updates, one orig).
+    $this->drupalGet('node');
+    $this->assertText('Managing News Translation Workflow: Two Way Translation Updates');
+    $this->assertText('Presenting on Features in Drupal and Managing News');
+    $this->assertText('Scaling the Open Atrium UI');
+    
+    // Import again.
+    $this->drupalPost('node/'. $nid .'/import', array(), 'Import');
+    $this->assertText('There is no new content.');
+    
+    // Assert DB status, there still shouldn't be more than 10 items.
+    $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_node_item}'));
+    $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Now delete all items.
     $this->drupalPost('node/'. $nid .'/delete-items', array(), 'Delete');
@@ -201,6 +222,26 @@ class FeedsRSStoNodesTest extends FeedsW
     // Assert DB status, there still shouldn't be more than 10 items.
     $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_node_item}'));
     $this->assertEqual($count, 10, 'Accurate number of items in database.');
+    
+    // Import updated feed file.
+    $feed_url = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') . '/tests/feeds/developmentseed.rss3';
+    $this->setUpdateExisting('syndication_standalone');
+    $this->importURL('syndication_standalone', $feed_url);
+    $this->assertText('Updated 2 Story nodes.');
+    
+    // Assert accuracy of aggregated information (check 2 updates, one orig).
+    $this->drupalGet('node');
+    $this->assertText('Managing News Translation Workflow: Two Way Translation Updates');
+    $this->assertText('Presenting on Features in Drupal and Managing News');
+    $this->assertText('Scaling the Open Atrium UI');
+    
+    // Import again.
+    $this->drupalPost('import/syndication_standalone', array(), 'Import');
+    $this->assertText('There is no new content.');
+    
+    // Assert DB status, there still shouldn't be more than 10 items.
+    $count = db_result(db_query('SELECT COUNT(*) FROM {feeds_node_item}'));
+    $this->assertEqual($count, 10, 'Accurate number of items in database.');
 
     // Now delete all items.
     $this->drupalPost('import/syndication_standalone/delete-items', array(), 'Delete');
Index: tests/feeds.test.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/tests/feeds.test.inc,v
retrieving revision 1.2
diff -u -p -r1.2 feeds.test.inc
--- tests/feeds.test.inc	23 Oct 2009 22:38:20 -0000	1.2
+++ tests/feeds.test.inc	23 Nov 2009 19:27:11 -0000
@@ -41,7 +41,7 @@ class FeedsWebTestCase extends DrupalWeb
    * Generate an OPML test feed.
    *
    * The purpose of this function is to create a dynamic OPML feed that points
-   * too feeds included in this test.
+   * to feeds included in this test.
    */
   public function generateOPML() {
     $path = $GLOBALS['base_url'] .'/'. drupal_get_path('module', 'feeds') .'/tests/feeds/';
@@ -168,11 +168,36 @@ class FeedsWebTestCase extends DrupalWeb
 
     // Check whether feed got properly added to scheduler.
     $this->assertEqual(1, db_result(db_query('SELECT COUNT(*) FROM {feeds_schedule} WHERE id = "%s" AND feed_nid = %d AND callback = "import" AND last_scheduled_time = 0 AND scheduled = 0', $id, $nid)));
-    // There must be only one entry for 'expire' - no matter how many actuall feed nodes exist.
+    // There must be only one entry for 'expire' - no matter how many actual feed nodes exist.
     $this->assertEqual(1, db_result(db_query('SELECT COUNT(*) FROM {feeds_schedule} WHERE id = "%s" AND callback = "expire" AND last_scheduled_time = 0 AND scheduled = 0', $id)));
 
     return $nid;
   }
+  
+  /**
+  * Edit the configuration of a feed node to test update behavior.
+  *
+  * @param $nid
+  *   The nid to edit.
+  * @param $feed_url
+  *   The new (absolute) feed URL to use.
+  * @param $title
+  *   Optional parameter to change title of feed node.
+  */
+  public function editFeedNode($nid, $feed_url, $title = '') {
+    $edit = array(
+      'title' => $title,
+      'feeds[FeedsHTTPFetcher][source]' => $feed_url,
+    );
+    //Check that the update saved.
+    $this->drupalPost('node/' . $nid . '/edit', $edit, 'Save');
+    $this->assertText('has been updated.');
+     
+    //Check that the URL was updated in the feeds_source table.
+    $source = db_fetch_object(db_query('select * from {feeds_source} WHERE feed_nid = %d', $nid));
+    $config = unserialize($source->config);
+    $this->assertEqual($config['FeedsHTTPFetcher']['source'], $feed_url, t('URL in DB correct.'));
+  }
 
   /**
    * Batch create a variable amount of feed nodes. All will have the
@@ -274,4 +299,22 @@ class FeedsWebTestCase extends DrupalWeb
       }
     }
   }
+  
+  /**
+  * Change FeedsNodeProcessor configuration of 'update_existing'. Default TRUE.
+  *
+  * @param $id
+  *   The ID string of the feed node.
+  *
+  * @param $set_update
+  *   Value to set update_existing to.
+  */
+  public function setUpdateExisting($id, $set_update = TRUE) {
+    //Update feed node to update existing items.
+    $edit = array(
+      'update_existing' => TRUE,
+    );
+    $this->drupalPost('admin/build/feeds/edit/' . $id . '/settings/FeedsNodeProcessor', $edit, 'Save');
+    $this->assertText('have been saved.');
+  }
 }
\ No newline at end of file
