Commit 47bbb6bf038f061f94ec0c5d405c985d7d2ff986
Committed by
Antonio Terceiro
1 parent
e4dcc429
Exists in
master
and in
28 other branches
ActionItem936: RSS reader block
Showing
10 changed files
with
349 additions
and
1 deletions
Show diff stats
app/controllers/my_profile/profile_design_controller.rb
| ... | ... | @@ -5,7 +5,7 @@ class ProfileDesignController < BoxOrganizerController |
| 5 | 5 | protect 'edit_profile_design', :profile |
| 6 | 6 | |
| 7 | 7 | def available_blocks |
| 8 | - blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock ] | |
| 8 | + blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock ] | |
| 9 | 9 | |
| 10 | 10 | # blocks exclusive for organizations |
| 11 | 11 | if profile.has_members? | ... | ... |
| ... | ... | @@ -0,0 +1,57 @@ |
| 1 | +class FeedReaderBlock < Block | |
| 2 | + | |
| 3 | + include DatesHelper | |
| 4 | + | |
| 5 | + settings_items :address, :type => :string | |
| 6 | + settings_items :limit, :type => :integer | |
| 7 | + settings_items :fetched_at, :type => :date | |
| 8 | + | |
| 9 | + settings_items :feed_title, :type => :string | |
| 10 | + settings_items :feed_items, :type => :array | |
| 11 | + | |
| 12 | + before_create do |block| | |
| 13 | + block.limit = 5 | |
| 14 | + block.feed_items = [] | |
| 15 | + end | |
| 16 | + | |
| 17 | + def self.description | |
| 18 | + _('List the latest N posts from a given RSS feed.') | |
| 19 | + end | |
| 20 | + | |
| 21 | + def help | |
| 22 | + _('This block can be used to create a list of latest N posts from a given RSS feed. You should only enter the RSS feed address.') | |
| 23 | + end | |
| 24 | + | |
| 25 | + def default_title | |
| 26 | + self.feed_title.nil? ? _('Feed Reader') : self.feed_title | |
| 27 | + end | |
| 28 | + | |
| 29 | + def formatted_feed_content | |
| 30 | + if self.fetched_at.nil? or self.feed_items.empty? | |
| 31 | + return ("<p class='feed-reader-block-error'>%s</p>" % _('Feed content was not loaded yet')) | |
| 32 | + else | |
| 33 | + return "<ul class='feed-reader-block-list'>" + | |
| 34 | + self.feed_items.map{ |item| "<li><a href='#{item[:link]}' class='feed-reader-block-item'>#{item[:title]}</a></li>" }.join("\n") + | |
| 35 | + "</ul>" + | |
| 36 | + "<div class='feed-reader-block-fetched-at'>#{_("Updated: %s") % show_date(self.fetched_at)}</div>" | |
| 37 | + end | |
| 38 | + end | |
| 39 | + | |
| 40 | + def add_item(title, link, date, content) | |
| 41 | + self.feed_items << {:title => title, :link => link} | |
| 42 | + end | |
| 43 | + | |
| 44 | + def clean | |
| 45 | + self.feed_items = [] | |
| 46 | + self.feed_title = nil | |
| 47 | + end | |
| 48 | + | |
| 49 | + def content | |
| 50 | + block_title(title) + formatted_feed_content | |
| 51 | + end | |
| 52 | + | |
| 53 | + def editable? | |
| 54 | + true | |
| 55 | + end | |
| 56 | + | |
| 57 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | +require 'feedparser' | |
| 2 | +require 'open-uri' | |
| 3 | + | |
| 4 | +class FeedHandler | |
| 5 | + | |
| 6 | + def parse(content) | |
| 7 | + raise FeedHandler::ParseError, "Content is nil" if content.nil? | |
| 8 | + begin | |
| 9 | + return FeedParser::Feed::new(content) | |
| 10 | + rescue Exception => ex | |
| 11 | + raise FeedHandler::ParseError, ex.to_s | |
| 12 | + end | |
| 13 | + end | |
| 14 | + | |
| 15 | + def fetch(address) | |
| 16 | + begin | |
| 17 | + content = "" | |
| 18 | + open(address) do |s| content = s.read end | |
| 19 | + return content | |
| 20 | + rescue Exception => ex | |
| 21 | + raise FeedHandler::FetchError, ex.to_s | |
| 22 | + end | |
| 23 | + end | |
| 24 | + | |
| 25 | + def process(container) | |
| 26 | + content = fetch(container.address) | |
| 27 | + container.fetched_at = Time.now | |
| 28 | + parse = parse(content) | |
| 29 | + container.feed_title = parse.title | |
| 30 | + parse.items[0..container.limit-1].each do |item| | |
| 31 | + container.add_item(item.title, item.link, item.date, item.content) | |
| 32 | + end | |
| 33 | + end | |
| 34 | + | |
| 35 | + class ParseError < Exception; end | |
| 36 | + class FetchError < Exception; end | |
| 37 | + | |
| 38 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | +#!/usr/bin/env ruby | |
| 2 | +require File.dirname(__FILE__) + '/../config/environment' | |
| 3 | + | |
| 4 | +FeedReaderBlock.find(:all).each do |feed_block| | |
| 5 | + unless feed_block.address.nil? | |
| 6 | + begin | |
| 7 | + feed_block.clean | |
| 8 | + handler = FeedHandler.new | |
| 9 | + handler.process(feed_block) | |
| 10 | + feed_block.save! | |
| 11 | + RAILS_DEFAULT_LOGGER.info("%s ID %d fetched at %s" % [feed_block.class.name, feed_block.id, feed_block.fetched_at]) | |
| 12 | + rescue FeedHandler::ParseError => ex | |
| 13 | + RAILS_DEFAULT_LOGGER.warn("Error parsing content from %s ID %d\n%s" % [feed_block.class.name, feed_block.id, ex.to_s]) | |
| 14 | + rescue FeedHandler::FetchError => ex | |
| 15 | + RAILS_DEFAULT_LOGGER.warn("Error fetching content from %s ID %d\n%s" % [feed_block.class.name, feed_block.id, ex.to_s]) | |
| 16 | + rescue Exception => ex | |
| 17 | + RAILS_DEFAULT_LOGGER.warn("Unknown error from %s ID %d\n%s" % [feed_block.class.name, feed_block.id, ex.to_s]) | |
| 18 | + end | |
| 19 | + end | |
| 20 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,44 @@ |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<rss version="2.0"> | |
| 3 | + <channel> | |
| 4 | + <title>Feed for unit tests</title> | |
| 5 | + <link>http://localhost/feed-test</link> | |
| 6 | + <description>Feed content</description> | |
| 7 | + <language>pt_BR</language> | |
| 8 | + <item> | |
| 9 | + <title>Last POST</title> | |
| 10 | + <description> | |
| 11 | + Cursus justo nec urna. Suspendisse potenti. In hac | |
| 12 | + habitasse platea dictumst. Cras quis lacus. Vestibulum rhoncus | |
| 13 | + congue lacus. | |
| 14 | + </description> | |
| 15 | + <pubDate>Wed, 11 Mar 2009 19:15:57 -0300</pubDate> | |
| 16 | + <link>http://localhost/last-post</link> | |
| 17 | + <guid>http://localhost/last-post</guid> | |
| 18 | + </item> | |
| 19 | + <item> | |
| 20 | + <title>Second POST</title> | |
| 21 | + <description> | |
| 22 | + Suspendisse tincidunt mi vel metus. Vivamus non urna in nisi gravida congue. | |
| 23 | + Aenean semper orci a eros. Praesent dictum. Maecenas pharetra odio ut dui. | |
| 24 | + Pellentesque ut orci. Sed lobortis, velit at laoreet suscipit, quam est | |
| 25 | + sagittis nibh. | |
| 26 | + </description> | |
| 27 | + <pubDate>Wed, 18 Feb 2009 11:01:53 -0300</pubDate> | |
| 28 | + <link>http://localhost/second-post</link> | |
| 29 | + <guid>http://localhost/second-post</guid> | |
| 30 | + </item> | |
| 31 | + <item> | |
| 32 | + <title>First POST</title> | |
| 33 | + <description> | |
| 34 | + Lacus at sapien suscipit tempus. Proin pulvinar velit | |
| 35 | + sed nulla. Curabitur aliquet leo ac massa. Praesent posuere lectus | |
| 36 | + vitae odio. Donec imperdiet urna vel ante. In semper accumsan diam. | |
| 37 | + Vestibulum porta justo. Suspendisse egestas commodo eros. | |
| 38 | + </description> | |
| 39 | + <pubDate>Sun, 18 Jan 2009 11:34:30 -0300</pubDate> | |
| 40 | + <link>http://localhost/first-post</link> | |
| 41 | + <guid>http://localhost/first-post</guid> | |
| 42 | + </item> | |
| 43 | + </channel> | |
| 44 | +</rss> | ... | ... |
test/functional/profile_design_controller_test.rb
| ... | ... | @@ -289,4 +289,28 @@ class ProfileDesignControllerTest < Test::Unit::TestCase |
| 289 | 289 | assert_equal !state, block.visible? |
| 290 | 290 | end |
| 291 | 291 | |
| 292 | + should 'offer to create feed reader block' do | |
| 293 | + get :add_block, :profile => 'designtestuser' | |
| 294 | + assert_tag :tag => 'input', :attributes => { :id => 'type_feedreaderblock', :value => 'FeedReaderBlock' } | |
| 295 | + end | |
| 296 | + | |
| 297 | + should 'be able to edit FeedReaderBlock' do | |
| 298 | + @box1.blocks << FeedReaderBlock.new(:address => 'feed address') | |
| 299 | + | |
| 300 | + get :edit, :profile => 'designtestuser', :id => @box1.blocks[-1].id | |
| 301 | + | |
| 302 | + assert_response :success | |
| 303 | + assert_tag :tag => 'input', :attributes => { :name => "block[address]", :value => 'feed address' } | |
| 304 | + assert_tag :tag => 'select', :attributes => { :name => "block[limit]" } | |
| 305 | + end | |
| 306 | + | |
| 307 | + should 'be able to save FeedReaderBlock configurations' do | |
| 308 | + @box1.blocks << FeedReaderBlock.new(:address => 'feed address') | |
| 309 | + | |
| 310 | + post :save, :profile => 'designtestuser', :id => @box1.blocks[-1].id, :block => {:address => 'new feed address', :limit => '20'} | |
| 311 | + | |
| 312 | + assert_equal 'new feed address', @box1.blocks[-1].address | |
| 313 | + assert_equal 20, @box1.blocks[-1].limit | |
| 314 | + end | |
| 315 | + | |
| 292 | 316 | end | ... | ... |
| ... | ... | @@ -0,0 +1,83 @@ |
| 1 | +require File.dirname(__FILE__) + '/../test_helper' | |
| 2 | + | |
| 3 | +class FeedHandlerTest < Test::Unit::TestCase | |
| 4 | + | |
| 5 | + class FeedContainer | |
| 6 | + attr_accessor :limit | |
| 7 | + attr_accessor :fetched_at | |
| 8 | + attr_accessor :feed_title | |
| 9 | + attr_accessor :feed_items | |
| 10 | + attr_accessor :address | |
| 11 | + def initialize | |
| 12 | + self.limit = 5 | |
| 13 | + self.feed_title = "Feed Container Mocked" | |
| 14 | + self.feed_items = [] | |
| 15 | + self.address = 'test/fixtures/files/feed.xml' | |
| 16 | + end | |
| 17 | + def add_item(title, link, date, content) | |
| 18 | + self.feed_items << title | |
| 19 | + end | |
| 20 | + end | |
| 21 | + | |
| 22 | + def setup | |
| 23 | + @handler = FeedHandler.new | |
| 24 | + @container = FeedContainer.new | |
| 25 | + end | |
| 26 | + attr_reader :handler, :container | |
| 27 | + | |
| 28 | + should 'fetch feed content' do | |
| 29 | + content = handler.fetch(container.address) | |
| 30 | + assert_match /<description>Feed content<\/description>/, content | |
| 31 | + assert_match /<title>Feed for unit tests<\/title>/, content | |
| 32 | + end | |
| 33 | + | |
| 34 | + should 'parse feed content' do | |
| 35 | + content = "" | |
| 36 | + open(container.address) do |s| content = s.read end | |
| 37 | + parse = handler.parse(content) | |
| 38 | + assert_equal 'Feed for unit tests', parse.title | |
| 39 | + assert_equal 'http://localhost/feed-test', parse.link | |
| 40 | + assert_equal 'Last POST', parse.items[0].title | |
| 41 | + end | |
| 42 | + | |
| 43 | + should 'process feed and populate container' do | |
| 44 | + handler.process(container) | |
| 45 | + assert_equal 'Feed for unit tests', container.feed_title | |
| 46 | + assert_equal ["Last POST", "Second POST", "First POST"], container.feed_items | |
| 47 | + end | |
| 48 | + | |
| 49 | + should 'raise exception when parser nil' do | |
| 50 | + handler = FeedHandler.new | |
| 51 | + assert_raise FeedHandler::ParseError do | |
| 52 | + handler.parse(nil) | |
| 53 | + end | |
| 54 | + end | |
| 55 | + | |
| 56 | + should 'raise exception when parser invalid content' do | |
| 57 | + handler = FeedHandler.new | |
| 58 | + assert_raise FeedHandler::ParseError do | |
| 59 | + handler.parse('<invalid>content</invalid>') | |
| 60 | + end | |
| 61 | + end | |
| 62 | + | |
| 63 | + should 'raise exception when fetch nil' do | |
| 64 | + handler = FeedHandler.new | |
| 65 | + assert_raise FeedHandler::FetchError do | |
| 66 | + handler.fetch(nil) | |
| 67 | + end | |
| 68 | + end | |
| 69 | + | |
| 70 | + should 'raise exception when fetch invalid address' do | |
| 71 | + handler = FeedHandler.new | |
| 72 | + assert_raise FeedHandler::FetchError do | |
| 73 | + handler.fetch('bli://invalid@address') | |
| 74 | + end | |
| 75 | + end | |
| 76 | + | |
| 77 | + should 'save only latest N posts from feed' do | |
| 78 | + container.limit = 1 | |
| 79 | + handler.process(container) | |
| 80 | + assert_equal 1, container.feed_items.size | |
| 81 | + end | |
| 82 | + | |
| 83 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,73 @@ |
| 1 | +require File.dirname(__FILE__) + '/../test_helper' | |
| 2 | + | |
| 3 | +class FeedReaderBlockTest < ActiveSupport::TestCase | |
| 4 | + | |
| 5 | + include DatesHelper | |
| 6 | + | |
| 7 | + def setup | |
| 8 | + @feed = FeedReaderBlock.new | |
| 9 | + @fetched_at = Time.now | |
| 10 | + @feed.fetched_at = @fetched_at | |
| 11 | + @feed.save! | |
| 12 | + end | |
| 13 | + attr_reader :feed, :fetched_at | |
| 14 | + | |
| 15 | + should 'default describe' do | |
| 16 | + assert_not_equal Block.description, FeedReaderBlock.description | |
| 17 | + end | |
| 18 | + | |
| 19 | + should 'have address and limit' do | |
| 20 | + assert_respond_to feed, :address | |
| 21 | + assert_respond_to feed, :limit | |
| 22 | + end | |
| 23 | + | |
| 24 | + should 'default value of limit' do | |
| 25 | + assert_equal 5, feed.limit | |
| 26 | + end | |
| 27 | + | |
| 28 | + should 'is editable' do | |
| 29 | + assert feed.editable? | |
| 30 | + end | |
| 31 | + | |
| 32 | + should 'display feed posts from content' do | |
| 33 | + feed.feed_items = [] | |
| 34 | + %w[ last-post second-post first-post ].each do |i| | |
| 35 | + feed.feed_items << {:title => i, :link => "http://localhost/#{i}"} | |
| 36 | + end | |
| 37 | + feed.feed_title = 'Feed for unit tests' | |
| 38 | + feed_content = feed.content | |
| 39 | + assert_tag_in_string feed_content, :tag => 'h3', :content => 'Feed for unit tests' | |
| 40 | + assert_tag_in_string feed_content, :tag => 'a', :attributes => { :href => 'http://localhost/last-post' }, :content => 'last-post' | |
| 41 | + assert_tag_in_string feed_content, :tag => 'a', :attributes => { :href => 'http://localhost/second-post' }, :content => 'second-post' | |
| 42 | + assert_tag_in_string feed_content, :tag => 'a', :attributes => { :href => 'http://localhost/first-post' }, :content => 'first-post' | |
| 43 | + end | |
| 44 | + | |
| 45 | + should 'display channel title as title by default' do | |
| 46 | + feed.feed_title = 'Feed for unit tests' | |
| 47 | + assert_equal 'Feed for unit tests', feed.title | |
| 48 | + end | |
| 49 | + | |
| 50 | + should 'display default title when hasnt feed_content' do | |
| 51 | + assert_equal 'Feed Reader', feed.title | |
| 52 | + end | |
| 53 | + | |
| 54 | + should 'notice when content not fetched yet' do | |
| 55 | + assert_tag_in_string feed.content, :tag => 'p', :content => 'Feed content was not loaded yet' | |
| 56 | + end | |
| 57 | + | |
| 58 | + should 'display last fetched date' do | |
| 59 | + feed.feed_items = ['one', 'two'] | |
| 60 | + assert_tag_in_string feed.content, :tag => 'div', | |
| 61 | + :content => "Updated: #{show_date(@fetched_at)}", | |
| 62 | + :attributes => {:class => 'feed-reader-block-fetched-at'} | |
| 63 | + end | |
| 64 | + | |
| 65 | + should 'clear feed title and items' do | |
| 66 | + feed.feed_items = %w[ last-post second-post first-post ] | |
| 67 | + feed.feed_title = 'Feed Test' | |
| 68 | + feed.clean | |
| 69 | + assert_nil feed.feed_title | |
| 70 | + assert_equal [], feed.feed_items | |
| 71 | + end | |
| 72 | + | |
| 73 | +end | ... | ... |