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 | ... | ... |