bcms_feeds 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,10 @@
1
+ bcms_feeds
2
+ ==========
3
+
4
+ This is a BrowserCMS module which fetches, caches and displays RSS/Atom feeds.
5
+
6
+ For installation instructions see http://www.browsercms.org/doc/guides/html/installing_modules.html
7
+
8
+ To incorporate a feed in your page, use a Feed Portlet. Specify the URL of the feed. SimpleRSS [http://simple-rss.rubyforge.org/] is used for parsing, and a parsed version of the feed will be available in the @feed variable. You can use the code section to manipulate this data as necessary, and the template to format it.
9
+
10
+ Feeds are cached in the database for 30 minutes. If there is a failure fetching a remote feed, the expiry time of the cached feed will be extended by 10 minutes. When fetching remote feeds, the timeout length is 10 seconds.
@@ -0,0 +1,10 @@
1
+ # Filters added to this controller apply to all controllers in the application.
2
+ # Likewise, all the methods added will be available for all controllers.
3
+
4
+ class ApplicationController < ActionController::Base
5
+ helper :all # include all helpers, all the time
6
+ protect_from_forgery # See ActionController::RequestForgeryProtection for details
7
+
8
+ # Scrub sensitive parameters from your log
9
+ # filter_parameter_logging :password
10
+ end
@@ -0,0 +1,3 @@
1
+ # Methods added to this helper will be available to all templates in the application.
2
+ module ApplicationHelper
3
+ end
@@ -0,0 +1,59 @@
1
+ require 'net/http'
2
+ require 'timeout'
3
+ require 'simple-rss'
4
+
5
+ class Feed < ActiveRecord::Base
6
+ TTL = 30.minutes
7
+ TTL_ON_ERROR = 10.minutes
8
+ TIMEOUT = 10 # In seconds
9
+
10
+ delegate :entries, :items, :to => :parsed_contents
11
+
12
+ def parsed_contents
13
+ @parsed_contents ||= SimpleRSS.parse(contents)
14
+ end
15
+
16
+ def contents
17
+ if expires_at.nil? || expires_at < Time.now.utc
18
+ begin
19
+ self.expires_at = Time.now.utc + TTL
20
+ new_contents = remote_contents
21
+ SimpleRSS.parse(new_contents) # Check that we can actually parse it
22
+ write_attribute(:contents, new_contents)
23
+ save
24
+ rescue StandardError, Timeout::Error, SimpleRSSError => exception
25
+ logger.error("Loading feed #{url} failed with #{exception.inspect}")
26
+ self.expires_at = Time.now.utc + TTL_ON_ERROR
27
+ save
28
+ end
29
+ else
30
+ logger.info("Loading feed from cache: #{url}")
31
+ end
32
+ read_attribute(:contents)
33
+ end
34
+
35
+ def remote_contents
36
+ Timeout.timeout(TIMEOUT) {
37
+ simple_get(url)
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def simple_get(url)
44
+ logger.info("Loading feed from remote: #{url}")
45
+ parsed_url = URI.parse(url)
46
+ http = Net::HTTP.start(parsed_url.host, parsed_url.port)
47
+ response = http.request_get(url, 'User-Agent' => "BrowserCMS bcms_feed extension")
48
+ if response.is_a?(Net::HTTPSuccess)
49
+ return response.body
50
+ elsif response.is_a?(Net::HTTPRedirection)
51
+ logger.info("#{url} returned a redirect. Following . . ")
52
+ simple_get(response.header['Location'])
53
+ else
54
+ logger.info("#{url} returned a redirect. Following . . ")
55
+ raise StandardError
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,15 @@
1
+ class FeedPortlet < Portlet
2
+ handler "erb"
3
+
4
+ def render
5
+ raise ArgumentError, "No feed URL specified" if self.url.blank?
6
+ @feed = Feed.find_or_create_by_url(self.url).parsed_contents
7
+ if @portlet.limit.to_i != 0
8
+ @items = @feed.items[0..(@portlet.limit.to_i - 1)]
9
+ else
10
+ @items = @feed.items
11
+ end
12
+ instance_eval(self.code) unless self.code.blank?
13
+ end
14
+
15
+ end
@@ -0,0 +1,11 @@
1
+ <%= f.cms_text_field :name %>
2
+ <%= f.cms_text_field :url %>
3
+ <%= f.cms_text_area :code %>
4
+ <%= f.cms_drop_down :limit,
5
+ [
6
+ ['Unlimited','0'],['1','1'],['2','2'],['3','3'],['4','4'],['5','5'],['6','6'],['7','7'],['8','8'],['9','9'],
7
+ ['10','10'],['11','11'],['12','12'],['13','13'],['14','14'],['15','15'],['16','16'],['17','17'],['18','18'],['19','19'],
8
+ ['20','20']
9
+ ], :instructions => 'Items are available in the @items array and will be limited to the number you select above. You can still access the items via @feed.items, but they will not have a limit applied to them for backwards compatibility.'
10
+ %>
11
+ <%= f.cms_text_area :template, :default_value => @block.class.default_template %>
@@ -0,0 +1,12 @@
1
+ <div class="feed-list feed-number-<%= @portlet.id %>">
2
+ <h1><%= h @feed.title %></h1>
3
+ <ul>
4
+ <% @items.each do |item| %>
5
+ <li>
6
+ <div class="title"><%= link_to item.title, item.link %></div>
7
+ <div class="description"><%= item.description %></div>
8
+ <div class="item-meta">by <%= h item.dc_creator %> on <%= h item.pubDate %></div>
9
+ </li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
@@ -0,0 +1,13 @@
1
+ class AddFeeds < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :feeds do |f|
4
+ f.string :url
5
+ f.text :contents
6
+ f.datetime :expires_at
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :feeds
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ class ModifyFeedsTableToUseMediumtext < ActiveRecord::Migration
2
+ def self.up
3
+ execute "ALTER TABLE feeds MODIFY COLUMN contents MEDIUMTEXT"
4
+ end
5
+
6
+ def self.down
7
+ execute "ALTER TABLE feeds MODIFY COLUMN contents TEXT"
8
+ end
9
+ end
data/lib/bcms_feeds.rb ADDED
@@ -0,0 +1 @@
1
+ require 'bcms_feeds/routes'
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ gem_root = File.expand_path(File.join(File.dirname(__FILE__), ".."))
2
+ Cms.add_to_rails_paths gem_root
3
+ Cms.add_generator_paths gem_root, "db/migrate/[0-9]*_*.rb"
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+ require 'performance_test_help'
3
+
4
+ # Profiling results for each test method are written to tmp/performance.
5
+ class BrowsingTest < ActionController::PerformanceTest
6
+ def test_homepage
7
+ get '/'
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
3
+ require 'test_help'
4
+ require 'mocha'
5
+
6
+ class ActiveSupport::TestCase
7
+ # Transactional fixtures accelerate your tests by wrapping each test method
8
+ # in a transaction that's rolled back on completion. This ensures that the
9
+ # test database remains unchanged so your fixtures don't have to be reloaded
10
+ # between every test method. Fewer database queries means faster tests.
11
+ #
12
+ # Read Mike Clark's excellent walkthrough at
13
+ # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
14
+ #
15
+ # Every Active Record database supports transactions except MyISAM tables
16
+ # in MySQL. Turn off transactional fixtures in this case; however, if you
17
+ # don't care one way or the other, switching from MyISAM to InnoDB tables
18
+ # is recommended.
19
+ #
20
+ # The only drawback to using transactional fixtures is when you actually
21
+ # need to test transactions. Since your test is bracketed by a transaction,
22
+ # any transactions started in your code will be automatically rolled back.
23
+ self.use_transactional_fixtures = true
24
+
25
+ # Instantiated fixtures are slow, but give you @david where otherwise you
26
+ # would need people(:david). If you don't want to migrate your existing
27
+ # test cases which use the @david style and don't mind the speed hit (each
28
+ # instantiated fixtures translates to a database query per test method),
29
+ # then set this back to true.
30
+ self.use_instantiated_fixtures = false
31
+
32
+ # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
33
+ #
34
+ # Note: You'll currently still have to declare fixtures explicitly in integration tests
35
+ # -- they do not yet inherit this setting
36
+ fixtures :all
37
+
38
+ # Add more helper methods to be used by all tests here...
39
+ end
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class FeedTest < ActiveSupport::TestCase
4
+ def setup
5
+ @feed = Feed.create!(:url => "http://example.com/blog.rss")
6
+ @contents = "<feed>Some feed</feed>"
7
+
8
+ now = Time.now
9
+ Time.stubs(:now).returns(now) # Freeze time
10
+
11
+ # small timeout for testing purposes
12
+ Feed.send(:remove_const, :TIMEOUT)
13
+ Feed.const_set(:TIMEOUT, 1)
14
+ end
15
+
16
+ test "remote_contents should fetch the feed" do
17
+ @feed.expects(:open).with("http://example.com/blog.rss").returns(stub(:read => @contents))
18
+ assert_equal @contents, @feed.remote_contents
19
+ end
20
+
21
+ test "remote_contents should raise a Timeout::Error if fetching the feed takes longer then Feed::TIMEOUT" do
22
+ def @feed.open(url)
23
+ sleep(2)
24
+ stubs(:read => "bla")
25
+ end
26
+
27
+ assert_raise(Timeout::Error) { @feed.remote_contents }
28
+ end
29
+
30
+ test "contents with no expiry should return the remote contents and save it" do
31
+ @feed.expires_at = nil
32
+ should_get_remote_contents_and_parse
33
+ end
34
+
35
+ test "contents with an expiry in the past return the remote contents and save it" do
36
+ @feed.expires_at = Time.now - 1.hour
37
+ should_get_remote_contents_and_parse
38
+ end
39
+
40
+ def should_get_remote_contents_and_parse
41
+ @feed.stubs(:remote_contents).returns(@contents)
42
+ SimpleRSS.expects(:parse).with(@contents)
43
+
44
+ assert_equal @contents, @feed.contents
45
+ @feed.reload
46
+ assert_equal @contents, @feed.read_attribute(:contents)
47
+
48
+ # I think the to_i is necessary because of a lack of precision provided by sqlite3. I think.
49
+ # Anyway, without it the comparison fails.
50
+ assert_equal (Time.now.utc + Feed::TTL).to_i, @feed.expires_at.to_i
51
+ end
52
+
53
+ test "contents with the expiry in the future should return the cached contents" do
54
+ @feed.expires_at = Time.now + 1.hour
55
+ @feed.write_attribute(:contents, @contents)
56
+
57
+ assert_equal @contents, @feed.contents
58
+ end
59
+
60
+ test "TTL of cached contents should be extended if there is an error fetching the remote contents, or parsing the feed" do
61
+ [StandardError, Timeout::Error, SimpleRSSError].each do |exception|
62
+ @feed.expires_at = Time.now - 1.hour
63
+ @feed.stubs(:remote_contents).raises(exception)
64
+ @feed.write_attribute(:contents, @contents)
65
+
66
+ assert_equal @contents, @feed.contents
67
+ assert_equal Time.now.utc + Feed::TTL_ON_ERROR, @feed.expires_at
68
+ end
69
+ end
70
+
71
+ test "parsed_contents should return the contents parsed by SimpleRSS" do
72
+ @feed.stubs(:contents).returns(@contents)
73
+ SimpleRSS.stubs(:parse).with(@contents).returns(parsed_contents = stub)
74
+
75
+ assert_equal parsed_contents, @feed.parsed_contents
76
+ end
77
+
78
+ test "Contents can be larger than 64Kb of text in a feed" do
79
+ feed = Feed.new(:url => 'http://www.foo.com', :contents => '<feed>' + 'w' * 300000 + '</feed>', :expires_at => Time.now + 15.minutes)
80
+ feed.save
81
+
82
+ assert_equal 'http://www.foo.com', feed.url
83
+ assert_equal '<feed>' + 'w' * 300000 + '</feed>', feed.contents
84
+ end
85
+ end
@@ -0,0 +1,9 @@
1
+ require File.join(File.dirname(__FILE__), '/../../test_helper')
2
+
3
+ class FeedTest < ActiveSupport::TestCase
4
+
5
+ test "Should be able to create new instance of a portlet" do
6
+ assert FeedPortlet.create!(:name => "New Portlet")
7
+ end
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bcms_feeds
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Jon Leighton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-05-03 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: simple-rss
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: A BrowserCMS module which fetches, caches and displays RSS/Atom feeds
26
+ email: j@jonathanleighton.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README
33
+ files:
34
+ - app/controllers/application_controller.rb
35
+ - app/helpers/application_helper.rb
36
+ - app/models/feed.rb
37
+ - app/portlets/feed_portlet.rb
38
+ - app/views/portlets/feed/_form.html.erb
39
+ - app/views/portlets/feed/render.html.erb
40
+ - db/migrate/20090813213104_add_feeds.rb
41
+ - db/migrate/20100115202209_modify_feeds_table_to_use_mediumtext.rb
42
+ - lib/bcms_feeds.rb
43
+ - rails/init.rb
44
+ - README
45
+ has_rdoc: true
46
+ homepage: http://github.com/jonleighton/bcms_feeds
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options:
51
+ - --charset=UTF-8
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.5
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Feeds in BrowserCMS
73
+ test_files:
74
+ - test/performance/browsing_test.rb
75
+ - test/test_helper.rb
76
+ - test/unit/feed_test.rb
77
+ - test/unit/portlets/feed_portlet_test.rb