nachokb-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,70 @@
1
+ require "open-uri"
2
+ require "timeout"
3
+ require "simple-rss"
4
+ require "ri_cal"
5
+
6
+ class Feed < ActiveRecord::Base
7
+ TTL = 30.minutes
8
+ TTL_ON_ERROR = 10.minutes
9
+ TIMEOUT = 10 # In seconds
10
+ CONTENT_TYPES = {
11
+ "application/rss+xml" => lambda { |content| SimpleRSS.parse(content) },
12
+ "text/calendar" => lambda { |content| RiCal.parse_string(content) }
13
+ }.with_indifferent_access
14
+ DEFAULT_CONTENT_TYPE = "application/rss+xml"
15
+
16
+ delegate :entries, :items, :to => :parsed_contents
17
+
18
+ def self.content_types
19
+ self::CONTENT_TYPES
20
+ end
21
+
22
+ def self.supported_content_types
23
+ self.content_types.keys
24
+ end
25
+
26
+ def self.default_content_type
27
+ self::DEFAULT_CONTENT_TYPE
28
+ end
29
+
30
+ def content_type
31
+ read_attribute(:content_type) || self.class::DEFAULT_CONTENT_TYPE
32
+ end
33
+
34
+ def parsed_contents
35
+ @parsed_contents ||= parse_content
36
+ end
37
+
38
+ def contents(force_reload = false)
39
+ if force_reload || expires_at.nil? || expires_at < Time.now.utc
40
+ begin
41
+ self.expires_at = Time.now.utc + TTL
42
+ new_contents = remote_contents
43
+ parse_content(new_contents) # Check that we can actually parse it
44
+ write_attribute(:contents, new_contents)
45
+ save
46
+ rescue StandardError, Timeout::Error, SimpleRSSError => exception
47
+ logger.error("Loading feed #{url} failed with #{exception.inspect}")
48
+ self.expires_at = Time.now.utc + TTL_ON_ERROR
49
+ save
50
+ end
51
+ else
52
+ logger.info("Loading feed from cache: #{url}")
53
+ end
54
+
55
+ read_attribute(:contents)
56
+ end
57
+
58
+ def remote_contents
59
+ logger.info("Loading feed from remote: #{url}")
60
+ Timeout.timeout(TIMEOUT) { open(url).read }
61
+ end
62
+
63
+ private
64
+ def parse_content(content = nil)
65
+ parser = self.class.content_types[self.content_type]
66
+ raise ArgumentError.new("Invalid ContentType for #{self.class.name}##{self.id}") unless parser
67
+ content ||= self.contents
68
+ parser.call(content)
69
+ end
70
+ end
@@ -0,0 +1,9 @@
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_and_content_type(self.url, self.content_type).parsed_contents
7
+ instance_eval(self.code) unless self.code.blank?
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ <%= f.cms_text_field :name %>
2
+ <%= f.cms_drop_down :content_type, Feed.supported_content_types, :default => Feed.default_content_type %>
3
+ <%= f.cms_text_field :url %>
4
+ <%= f.cms_text_area :code %>
5
+ <%= f.cms_drop_down :handler, ActionView::Template.template_handler_extensions, :default => "erb" %>
6
+ <%= f.cms_text_area :template, :default_value => @block.class.default_template %>
@@ -0,0 +1 @@
1
+ <%=h @portlet.name %>
@@ -0,0 +1,14 @@
1
+ class AddFeeds < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :feeds do |f|
4
+ f.string :url
5
+ f.string :content_type
6
+ f.text :contents
7
+ f.datetime :expires_at
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :feeds
13
+ end
14
+ end
@@ -0,0 +1 @@
1
+
@@ -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,23 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class CalendarFeedTest < ActiveSupport::TestCase
4
+ def setup
5
+ @feed = Feed.create!(:url => "http://example.com/events.ics", :content_type => "text/calendar")
6
+ @contents = "BEGIN:VEVENT\nDTSTART;VALUE=DATE:20090903\nDTEND;VALUE=DATE:20090904\nDTSTAMP:20090831T192355Z\nDESCRIPTION:\nLOCATION:\nSEQUENCE:0\nSUMMARY:Example\nEND:VEVENT"
7
+ end
8
+
9
+ test "parsed_contents should return the contents parsed by RiCal" do
10
+ @feed.stubs(:contents).returns(@contents)
11
+ RiCal.stubs(:parse_string).with(@contents).returns(parsed_contents = stub)
12
+
13
+ assert_equal parsed_contents, @feed.parsed_contents
14
+ end
15
+
16
+ test "default content_type should be used when no other is specified" do
17
+ assert_equal Feed.default_content_type, Feed.new.content_type
18
+ end
19
+
20
+ test "parse_content should raise an error when content_type is not valid" do
21
+ assert_raise(ArgumentError) { Feed.new(:content_type => "invalid").parsed_contents }
22
+ end
23
+ end
@@ -0,0 +1,77 @@
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
+ 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,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nachokb-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: 2009-09-01 00:00:00 -07: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
+ - !ruby/object:Gem::Dependency
26
+ name: ri_cal
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: A BrowserCMS module which fetches, caches and displays RSS/Atom feeds -- now with iCalendar support!
36
+ email: j@jonathanleighton.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README
43
+ files:
44
+ - app/models/feed.rb
45
+ - app/portlets/feed_portlet.rb
46
+ - app/views/portlets/feed/_form.html.erb
47
+ - app/views/portlets/feed/render.html.erb
48
+ - db/migrate/20090813213104_add_feeds.rb
49
+ - lib/bcms_feeds.rb
50
+ - rails/init.rb
51
+ - README
52
+ has_rdoc: true
53
+ homepage: http://github.com/jonleighton/bcms_feeds
54
+ licenses:
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Feeds in BrowserCMS
79
+ test_files:
80
+ - test/unit/feed_test.rb
81
+ - test/unit/calendar_feed_test.rb
82
+ - test/unit/portlets/feed_portlet_test.rb
83
+ - test/test_helper.rb
84
+ - test/performance/browsing_test.rb