Commit 47bbb6bf038f061f94ec0c5d405c985d7d2ff986

Authored by Joenio Costa
Committed by Antonio Terceiro
1 parent e4dcc429

ActionItem936: RSS reader block

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?
... ...
app/models/feed_reader_block.rb 0 → 100644
... ... @@ -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
... ...
app/views/box_organizer/_feed_reader_block.rhtml 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<div id='edit-feed-reader-block'>
  2 + <%= labelled_form_field _('Address'), text_field(:block, :address, :size => 40) %>
  3 + <%= labelled_form_field _('Limit of posts to display'), select(:block, :limit, [5, 10, 20, 50]) %>
  4 +</div>
... ...
lib/feed_handler.rb 0 → 100644
... ... @@ -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
... ...
public/stylesheets/blocks/feed-reader-block.css 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +.feed-reader-block-fetched-at {
  2 + color: #CCC;
  3 + font-size: 11px;
  4 + text-align: center;
  5 +}
... ...
script/feed-updater 0 → 100755
... ... @@ -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
... ...
test/fixtures/files/feed.xml 0 → 100644
... ... @@ -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 &lt; 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
... ...
test/unit/feed_handler_test.rb 0 → 100644
... ... @@ -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
... ...
test/unit/feed_reader_block_test.rb 0 → 100644
... ... @@ -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
... ...