Commit 47bbb6bf038f061f94ec0c5d405c985d7d2ff986
Committed by
Antonio Terceiro
1 parent
e4dcc429
Exists in
master
and in
29 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,7 +5,7 @@ class ProfileDesignController < BoxOrganizerController | ||
5 | protect 'edit_profile_design', :profile | 5 | protect 'edit_profile_design', :profile |
6 | 6 | ||
7 | def available_blocks | 7 | def available_blocks |
8 | - blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock ] | 8 | + blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock ] |
9 | 9 | ||
10 | # blocks exclusive for organizations | 10 | # blocks exclusive for organizations |
11 | if profile.has_members? | 11 | if profile.has_members? |
@@ -0,0 +1,57 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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,4 +289,28 @@ class ProfileDesignControllerTest < Test::Unit::TestCase | ||
289 | assert_equal !state, block.visible? | 289 | assert_equal !state, block.visible? |
290 | end | 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 | end | 316 | end |
@@ -0,0 +1,83 @@ | @@ -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 @@ | @@ -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 |