From 8b2439e5e42fa8fa8e74538938eed186a0258b4a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 21 Sep 2007 22:17:35 +0000 Subject: [PATCH] Added AtomFeedHelper (slightly improved from the atom_feed_helper plugin) [DHH] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7529 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/CHANGELOG | 2 + .../action_view/helpers/atom_feed_helper.rb | 111 ++++++++++++++++++ .../test/template/atom_feed_helper_test.rb | 101 ++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 actionpack/lib/action_view/helpers/atom_feed_helper.rb create mode 100644 actionpack/test/template/atom_feed_helper_test.rb diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index f53bf22853..202ddba8ab 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Added AtomFeedHelper (slightly improved from the atom_feed_helper plugin) [DHH] + * Prevent errors when generating routes for uncountable resources, (i.e. sheep where plural == singluar). map.resources :sheep now creates sheep_index_url for the collection and sheep_url for the specific item. [Koz] * Added support for HTTP Only cookies (works in IE6+ and FF 2.0.5+) as an improvement for XSS attacks #8895 [lifo/Spakman] diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb new file mode 100644 index 0000000000..c3ceecbf54 --- /dev/null +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -0,0 +1,111 @@ +# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other +# template languages). +module ActionView + module Helpers #:nodoc: + module AtomFeedHelper + # Full usage example: + # + # config/routes.rb: + # ActionController::Routing::Routes.draw do |map| + # map.resources :posts + # map.root :controller => "posts" + # end + # + # app/controllers/posts_controller.rb: + # class PostsController < ApplicationController::Base + # # GET /posts.html + # # GET /posts.atom + # def index + # @posts = Post.find(:all) + # + # respond_to do |format| + # format.html + # format.atom + # end + # end + # end + # + # app/views/posts/index.atom.builder: + # atom_feed do |feed| + # feed.title("My great blog!") + # feed.updated((@posts.first.created_at)) + # + # for post in @posts + # feed.entry(post) do |entry| + # entry.title(post.title) + # entry.content(post.body, :type => 'html') + # + # entry.author do |author| + # author.name("DHH") + # end + # end + # end + # end + # + # The options are for atom_feed are: + # + # * :language: Defaults to "en-US". + # * :root_url: The HTML alternative that this feed is doubling for. Defaults to / on the current host. + # * :url: The URL for this feed. Defaults to the current URL. + # + # atom_feed yields a AtomFeedBuilder instance. + def atom_feed(options = {}, &block) + xml = options[:xml] || eval("xml", block.binding) + xml.instruct! + + xml.feed "xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom' do + xml.id("tag:#{request.host}:#{request.request_uri.split(".")[0].gsub("/", "")}") + xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port)) + + if options[:url] + xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url) + end + + yield AtomFeedBuilder.new(xml, self) + end + end + + + class AtomFeedBuilder + def initialize(xml, view) + @xml, @view = xml, view + end + + # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used. + def updated(date_or_time = nil) + @xml.updated((date_or_time || Time.now.utc).xmlschema) + end + + # Creates an entry tag for a specific record and prefills the id using class and id. + # + # Options: + # + # * :updated: Time of update. Defaults to the created_at attribute on the record if one such exists. + # * :published: Time first published. Defaults to the updated_at attribute on the record if one such exists. + # * :url: The URL for this entry. Defaults to the polymorphic_url for the record. + def entry(record, options = {}) + @xml.entry do + @xml.id("tag:#{@view.request.host_with_port}:#{record.class}#{record.id}") + + if options[:published] || (record.respond_to?(:created_at) && record.created_at) + @xml.published((options[:published] || record.created_at).xmlschema) + end + + if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at) + @xml.updated((options[:updated] || record.updated_at).xmlschema) + end + + @xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:url] || @view.polymorphic_url(record)) + + yield @xml + end + end + + private + def method_missing(method, *arguments) + @xml.__send__(method, *arguments) + end + end + end + end +end \ No newline at end of file diff --git a/actionpack/test/template/atom_feed_helper_test.rb b/actionpack/test/template/atom_feed_helper_test.rb new file mode 100644 index 0000000000..45b1cae7c2 --- /dev/null +++ b/actionpack/test/template/atom_feed_helper_test.rb @@ -0,0 +1,101 @@ +require "#{File.dirname(__FILE__)}/../abstract_unit" + +Post = Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) + +class PostsController < ActionController::Base + FEEDS = {} + FEEDS["defaults"] = <<-EOT + atom_feed do |feed| + feed.title("My great blog!") + feed.updated((@posts.first.created_at)) + + for post in @posts + feed.entry(post) do |entry| + entry.title(post.title) + entry.content(post.body, :type => 'html') + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + FEEDS["entry_options"] = <<-EOT + atom_feed do |feed| + feed.title("My great blog!") + feed.updated((@posts.first.created_at)) + + for post in @posts + feed.entry(post, :url => "/otherstuff/" + post.to_param, :updated => Time.utc(2007, 1, post.id)) do |entry| + entry.title(post.title) + entry.content(post.body, :type => 'html') + + entry.author do |author| + author.name("DHH") + end + end + end + end + EOT + + def index + @posts = [ + Post.new(1, "1", "Hello One", "Something COOL!", Time.utc(2007, 12, 12, 15), Time.utc(2007, 12, 12, 15)), + Post.new(2, "2", "Hello Two", "Something Boring", Time.utc(2007, 12, 12, 15)), + ] + + render :inline => FEEDS[params[:id]], :type => :builder + end +end + +class RenderTest < Test::Unit::TestCase + def setup + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @controller = PostsController.new + + @request.host = "www.nextangle.com" + end + + def test_feed_should_use_default_language_if_none_is_given + with_restful_routing(:posts) do + get :index, :id => "defaults" + assert_match %r{xml:lang="en-US"}, @response.body + end + end + + def test_feed_should_include_two_entries + with_restful_routing(:posts) do + get :index, :id => "defaults" + assert_select "entry", 2 + end + end + + def test_entry_should_only_use_published_if_created_at_is_present + with_restful_routing(:posts) do + get :index, :id => "defaults" + assert_select "published", 1 + end + end + + def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record + with_restful_routing(:posts) do + get :index, :id => "entry_options" + + assert_select "updated", Time.utc(2007, 1, 1).xmlschema + assert_select "updated", Time.utc(2007, 1, 2).xmlschema + end + end + + + private + def with_restful_routing(resources) + with_routing do |set| + set.draw do |map| + map.resources(resources) + end + yield + end + end +end \ No newline at end of file