diff options
70 files changed, 885 insertions, 172 deletions
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index c605f1ff04..f03240acb1 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -17,5 +17,5 @@ Gem::Specification.new do |s| s.requirements << 'none' s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.4.1') + s.add_dependency('mail', '~> 2.4.3') end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 96ad3a155c..78ac05389c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,9 @@ ## Rails 4.0.0 (unreleased) ## +* Adds support for layouts when rendering a partial with a given collection. *serabe* + +* Allows the route helper `root` to take a string argument. For example, `root 'pages#main'`. *bcardarella* + * Forms of persisted records use always PATCH (via the `_method` hack). *fxn* * For resources, both PATCH and PUT are routed to the `update` action. *fxn* diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index b205acdb79..089913be3c 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('rack-cache', '~> 1.1') + s.add_dependency('rack-cache', '~> 1.2') s.add_dependency('builder', '~> 3.0.0') s.add_dependency('rack', '~> 1.4.1') s.add_dependency('rack-test', '~> 0.6.1') diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb index e4261068d8..4a95e1f276 100644 --- a/actionpack/lib/abstract_controller/url_for.rb +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -1,10 +1,10 @@ -# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class -# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an -# exception will be raised. -# -# Note that this module is completely decoupled from HTTP - the only requirement is a valid -# <tt>_routes</tt> implementation. module AbstractController + # Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class + # has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an + # exception will be raised. + # + # Note that this module is completely decoupled from HTTP - the only requirement is a valid + # <tt>_routes</tt> implementation. module UrlFor extend ActiveSupport::Concern include ActionDispatch::Routing::UrlFor diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index a0c54dbd84..7c10fcbb8a 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -31,7 +31,6 @@ module ActionController autoload :RequestForgeryProtection autoload :Rescue autoload :Responder - autoload :SessionManagement autoload :Streaming autoload :Testing autoload :UrlFor diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 3b82231b15..e20680ef35 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -192,7 +192,6 @@ module ActionController Renderers::All, ConditionalGet, RackDelegation, - SessionManagement, Caching, MimeResponds, ImplicitRender, diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index cb59af4f85..800752975c 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -6,8 +6,6 @@ module ActionController #:nodoc: module MimeResponds extend ActiveSupport::Concern - include ActionController::ImplicitRender - included do class_attribute :responder, :mimes_for_respond_to self.responder = ActionController::Responder @@ -193,7 +191,7 @@ module ActionController #:nodoc: if collector = retrieve_collector_from_mimes(mimes, &block) response = collector.response - response ? response.call : default_render({}) + response ? response.call : render({}) end end @@ -235,16 +233,8 @@ module ActionController #:nodoc: if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! - - if defined_response = collector.response - if action = options.delete(:action) - render :action => action - else - defined_response.call - end - else - (options.delete(:responder) || self.class.responder).call(self, resources, options) - end + options[:default_response] = collector.response + (options.delete(:responder) || self.class.responder).call(self, resources, options) end end @@ -281,6 +271,7 @@ module ActionController #:nodoc: if format self.content_type ||= format.to_s lookup_context.formats = [format.to_sym] + lookup_context.rendered_format = lookup_context.formats.first collector else head :not_acceptable diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index ccda01ed44..1e8990495c 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -130,6 +130,7 @@ module ActionController #:nodoc: @resources = resources @options = options @action = options.delete(:action) + @default_response = options.delete(:default_response) end delegate :head, :render, :redirect_to, :to => :controller @@ -172,7 +173,7 @@ module ActionController #:nodoc: # responds to :to_format and display it. # def to_format - if get? || !has_errors? + if get? || !has_errors? || response_overridden? default_render else display_errors @@ -226,7 +227,11 @@ module ActionController #:nodoc: # controller. # def default_render - controller.default_render(options) + if @default_response + @default_response.call(options) + else + controller.default_render(options) + end end # Display is just a shortcut to render a resource with the current format. @@ -274,5 +279,9 @@ module ActionController #:nodoc: def json_resource_errors {:errors => resource.errors} end + + def response_overridden? + @default_response.present? + end end end diff --git a/actionpack/lib/action_controller/metal/session_management.rb b/actionpack/lib/action_controller/metal/session_management.rb deleted file mode 100644 index 91d89ff9a4..0000000000 --- a/actionpack/lib/action_controller/metal/session_management.rb +++ /dev/null @@ -1,9 +0,0 @@ -module ActionController #:nodoc: - module SessionManagement #:nodoc: - extend ActiveSupport::Concern - - module ClassMethods - - end - end -end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 1177a703b3..8e7b56dbcc 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -1,25 +1,25 @@ -# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing -# the <tt>_routes</tt> method. Otherwise, an exception will be raised. -# -# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define -# url options like the +host+. In order to do so, this module requires the host class -# to implement +env+ and +request+, which need to be a Rack-compatible. -# -# Example: -# -# class RootUrl -# include ActionController::UrlFor -# include Rails.application.routes.url_helpers -# -# delegate :env, :request, :to => :controller -# -# def initialize(controller) -# @controller = controller -# @url = root_path # named route from the application. -# end -# end -# module ActionController + # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing + # the <tt>_routes</tt> method. Otherwise, an exception will be raised. + # + # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define + # url options like the +host+. In order to do so, this module requires the host class + # to implement +env+ and +request+, which need to be a Rack-compatible. + # + # Example: + # + # class RootUrl + # include ActionController::UrlFor + # include Rails.application.routes.url_helpers + # + # delegate :env, :request, :to => :controller + # + # def initialize(controller) + # @controller = controller + # @url = root_path # named route from the application. + # end + # end + # module UrlFor extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 3509e74d5e..e9a3ec5a22 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -69,6 +69,10 @@ module ActionController # assert_template :partial => '_customer', :locals => { :customer => @customer } # def assert_template(options = {}, message = nil) + # Force body to be read in case the + # template is being streamed + response.body + case options when NilClass, String, Symbol options = options.to_s if Symbol === options diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 2152351703..e039eb1288 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -82,7 +82,7 @@ module Mime class << self TRAILING_STAR_REGEXP = /(text|application)\/\*/ - Q_SEPARATOR_REGEXP = /;\s*q=/ + PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/ def lookup(string) LOOKUP[string] @@ -109,7 +109,7 @@ module Mime def parse(accept_header) if accept_header !~ /,/ - accept_header = accept_header.split(Q_SEPARATOR_REGEXP).first + accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first if accept_header =~ TRAILING_STAR_REGEXP parse_data_with_trailing_star($1) else @@ -119,7 +119,7 @@ module Mime # keep track of creation order to keep the subsequent sort stable list, index = [], 0 accept_header.split(/,/).each do |header| - params, q = header.split(Q_SEPARATOR_REGEXP) + params, q = header.split(PARAMETER_SEPARATOR_REGEXP) if params.present? params.strip! diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 5ab99d1061..c4a915d1ad 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -4,11 +4,12 @@ module ActionDispatch attr_accessor :original_filename, :content_type, :tempfile, :headers def initialize(hash) + @tempfile = hash[:tempfile] + raise(ArgumentError, ':tempfile is required') unless @tempfile + @original_filename = encode_filename(hash[:filename]) @content_type = hash[:type] @headers = hash[:head] - @tempfile = hash[:tempfile] - raise(ArgumentError, ':tempfile is required') unless @tempfile end def read(*args) @@ -19,15 +20,12 @@ module ActionDispatch [:open, :path, :rewind, :size].each do |method| class_eval "def #{method}; @tempfile.#{method}; end" end - + private + def encode_filename(filename) # Encode the filename in the utf8 encoding, unless it is nil - if filename - filename.force_encoding("UTF-8").encode! - else - filename - end + filename.force_encoding("UTF-8").encode! if filename end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 80fcdab643..cd215034dc 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -254,10 +254,15 @@ module ActionDispatch # # For options, see +match+, as +root+ uses it internally. # + # You can also pass a string which will expand + # + # root 'pages#main' + # # You should put the root route at the top of <tt>config/routes.rb</tt>, # because this means it will be matched first. As this is the most popular route # of most Rails applications, this is beneficial. def root(options = {}) + options = { :to => options } if options.is_a?(String) match '/', { :as => :root }.merge(options) end diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb index 2a28e780bf..add8d94b70 100644 --- a/actionpack/lib/action_view/asset_paths.rb +++ b/actionpack/lib/action_view/asset_paths.rb @@ -117,7 +117,7 @@ module ActionView end def relative_url_root - config.relative_url_root + config.relative_url_root || current_request.try(:script_name) end def asset_host_config diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 3628b935b7..245a19deec 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -158,6 +158,43 @@ module ActionView # Name: <%= user.name %> # </div> # + # If a collection is given, the layout will be rendered once for each item in the collection. Just think + # these two snippets have the same output: + # + # <%# app/views/users/_user.html.erb %> + # Name: <%= user.name %> + # + # <%# app/views/users/index.html.erb %> + # <%# This does not use layouts %> + # <ul> + # <% users.each do |user| -%> + # <li> + # <%= render :partial => "user", :locals => { :user => user } %> + # </li> + # <% end -%> + # </ul> + # + # <%# app/views/users/_li_layout.html.erb %> + # <li> + # <%= yield %> + # </li> + # + # <%# app/views/users/index.html.erb %> + # <ul> + # <%= render :partial => "user", :layout => "li_layout", :collection => users %> + # </ul> + # + # Given two users whose names are Alice and Bob, these snippets return: + # + # <ul> + # <li> + # Name: Alice + # </li> + # <li> + # Name: Bob + # </li> + # </ul> + # # You can also apply a layout to a block within any template: # # <%# app/views/users/_chief.html.erb &> @@ -238,7 +275,14 @@ module ActionView spacer = find_template(@options[:spacer_template]).render(@view, @locals) end + if layout = @options[:layout] + layout = find_template(layout) + end + result = @template ? collection_with_template : collection_without_template + + result.map!{|content| layout.render(@view, @locals) { content } } if layout + result.join(spacer).html_safe end @@ -342,9 +386,10 @@ module ActionView locals[as] = object segments << template.render(@view, locals) end - + segments end + def collection_without_template segments, locals, collection_data = [], @locals, @collection_data diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index 20f75fba45..f7df9a6322 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -6,8 +6,13 @@ module ActionView @view = context @details = extract_details(options) template = determine_template(options) - @lookup_context.rendered_format ||= template.formats.first - @lookup_context.formats = template.formats + context = @lookup_context + + unless context.rendered_format + context.rendered_format = template.formats.first + context.formats = template.formats + end + render_template(template, options[:layout], options[:locals]) end diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index bc171e201b..c73b36f05e 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -78,6 +78,13 @@ end class DefaultLayoutController < LayoutTest end +class StreamingLayoutController < LayoutTest + def render(*args) + options = args.extract_options! || {} + super(*args, options.merge(:stream => true)) + end +end + class AbsolutePathLayoutController < LayoutTest layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test') end @@ -122,6 +129,12 @@ class LayoutSetInResponseTest < ActionController::TestCase assert_template :layout => "layouts/layout_test" end + def test_layout_set_when_using_streaming_layout + @controller = StreamingLayoutController.new + get :hello + assert_template :hello + end + def test_layout_set_when_set_in_controller @controller = HasOwnLayoutController.new get :hello diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index ae368842b5..0e4099ddc6 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -132,7 +132,6 @@ class RespondToController < ActionController::Base end end - def iphone_with_html_response_type request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone" @@ -1135,7 +1134,7 @@ class PostController < AbstractPostController around_filter :with_iphone def index - respond_to(:html, :iphone) + respond_to(:html, :iphone, :js) end protected @@ -1182,4 +1181,45 @@ class MimeControllerLayoutsTest < ActionController::TestCase get :index assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body end + + def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout + get :index, :format => :js + assert_equal "Hello Firefox", @response.body + end +end + +class FlashResponder < ActionController::Responder + def initialize(controller, resources, options={}) + super + end + + def to_html + controller.flash[:notice] = 'Success' + super + end +end + +class FlashResponderController < ActionController::Base + self.responder = FlashResponder + respond_to :html + + def index + respond_with Object.new do |format| + format.html { render :text => 'HTML' } + end + end +end + +class FlashResponderControllerTest < ActionController::TestCase + tests FlashResponderController + + def test_respond_with_block_executed + get :index + assert_equal 'HTML', @response.body + end + + def test_flash_responder_executed + get :index + assert_equal 'Success', flash[:notice] + end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 2a7c1f86c6..272a7da8c5 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -437,6 +437,15 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal("/", routes.send(:root_path)) end + def test_named_route_root_without_hash + rs.draw do + root "hello#index" + end + routes = setup_for_named_route + assert_equal("http://test.host/", routes.send(:root_url)) + assert_equal("/", routes.send(:root_path)) + end + def test_named_route_with_regexps rs.draw do match 'page/:year/:month/:day/:title' => 'page#show', :as => 'article', diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 0372c42920..e3f9faaa64 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -75,6 +75,12 @@ class MimeTypeTest < ActiveSupport::TestCase assert_equal expect, Mime::Type.parse(accept) end + test "parse arbitarry media type parameters" do + accept = 'multipart/form-data; boundary="simple boundary"' + expect = [Mime::MULTIPART_FORM] + assert_equal expect, Mime::Type.parse(accept) + end + # Accept header send with user HTTP_USER_AGENT: Sunrise/0.42j (Windows XP) test "parse broken acceptlines" do accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5" diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 0b95291e18..1e3f720fa7 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -12,7 +12,7 @@ module ActionDispatch uf = Http::UploadedFile.new(:filename => 'foo', :tempfile => Object.new) assert_equal 'foo', uf.original_filename end - + def test_filename_should_be_in_utf_8 uf = Http::UploadedFile.new(:filename => 'foo', :tempfile => Object.new) assert_equal "UTF-8", uf.original_filename.encoding.to_s diff --git a/actionpack/test/fixtures/test/_b_layout_for_partial.html.erb b/actionpack/test/fixtures/test/_b_layout_for_partial.html.erb new file mode 100644 index 0000000000..e918ba8f83 --- /dev/null +++ b/actionpack/test/fixtures/test/_b_layout_for_partial.html.erb @@ -0,0 +1 @@ +<b><%= yield %></b>
\ No newline at end of file diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 3d2eeea503..58ff055fc2 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -38,6 +38,7 @@ class AssetTagHelperTest < ActionView::TestCase @controller = BasicController.new @request = Class.new do + attr_accessor :script_name def protocol() 'http://' end def ssl?() false end def host_with_port() 'localhost' end @@ -647,6 +648,13 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal %(<img alt="Rails" src="#{@controller.config.relative_url_root}/images/rails.png?#{expected_time}" />), image_tag("rails.png") end + # Same as above, but with script_name + def test_timebased_asset_id_with_script_name + @request.script_name = "/collaboration/hieraki" + expected_time = File.mtime(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).to_i.to_s + assert_equal %(<img alt="Rails" src="#{@request.script_name}/images/rails.png?#{expected_time}" />), image_tag("rails.png") + end + def test_should_skip_asset_id_on_complete_url assert_equal %(<img alt="Rails" src="http://www.example.com/rails.png" />), image_tag("http://www.example.com/rails.png") end @@ -897,6 +905,31 @@ class AssetTagHelperTest < ActionView::TestCase FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) end + # Same as above, but with script_name + def test_caching_javascript_include_tag_with_script_name + ENV["RAILS_ASSET_ID"] = "" + @request.script_name = "/collaboration/hieraki" + config.perform_caching = true + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/all.js" type="text/javascript"></script>), + javascript_include_tag(:all, :cache => true) + ) + + assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/money.js" type="text/javascript"></script>), + javascript_include_tag(:all, :cache => "money") + ) + + assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + + ensure + FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + end + def test_caching_javascript_include_tag_with_named_paths_and_relative_url_root_when_caching_off ENV["RAILS_ASSET_ID"] = "" @controller.config.relative_url_root = "/collaboration/hieraki" @@ -917,6 +950,27 @@ class AssetTagHelperTest < ActionView::TestCase assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) end + # Same as above, but with script_name + def test_caching_javascript_include_tag_with_named_paths_and_script_name_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + @request.script_name = "/collaboration/hieraki" + config.perform_caching = false + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/robber.js" type="text/javascript"></script>), + javascript_include_tag('robber', :cache => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + + assert_dom_equal( + %(<script src="/collaboration/hieraki/javascripts/robber.js" type="text/javascript"></script>), + javascript_include_tag('robber', :cache => "money", :recursive => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + end + def test_caching_javascript_include_tag_when_caching_off ENV["RAILS_ASSET_ID"] = "" config.perform_caching = false @@ -1144,6 +1198,33 @@ class AssetTagHelperTest < ActionView::TestCase FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) end + # Same as above, but with script_name + def test_caching_stylesheet_link_tag_with_script_name + ENV["RAILS_ASSET_ID"] = "" + @request.script_name = "/collaboration/hieraki" + config.perform_caching = true + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag(:all, :cache => true) + ) + + files_to_be_joined = Dir["#{ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR}/[^all]*.css"] + + expected_mtime = files_to_be_joined.map { |p| File.mtime(p) }.max + assert_equal expected_mtime, File.mtime(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/money.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag(:all, :cache => "money") + ) + + assert File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + ensure + FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + FileUtils.rm_f(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + end + def test_caching_stylesheet_link_tag_with_named_paths_and_relative_url_root_when_caching_off ENV["RAILS_ASSET_ID"] = "" @@ -1165,6 +1246,27 @@ class AssetTagHelperTest < ActionView::TestCase assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) end + # Same as above, but with script_name + def test_caching_stylesheet_link_tag_with_named_paths_and_script_name_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + @request.script_name = "/collaboration/hieraki" + config.perform_caching = false + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag('robber', :cache => true) + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + + assert_dom_equal( + %(<link href="/collaboration/hieraki/stylesheets/robber.css" media="screen" rel="stylesheet" type="text/css" />), + stylesheet_link_tag('robber', :cache => "money") + ) + + assert !File.exist?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + end + diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 7347e15373..122b07d348 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -238,6 +238,10 @@ module RenderTestCases def test_render_partial_with_nil_values_in_collection assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ]) end + + def test_render_partial_with_layout_using_collection_and_template + assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ]) + end def test_render_partial_with_empty_array_should_return_nil assert_nil @view.render(:partial => []) diff --git a/actionpack/test/template/sprockets_helper_test.rb b/actionpack/test/template/sprockets_helper_test.rb index 1c591bdcc2..d4d2dedfb6 100644 --- a/actionpack/test/template/sprockets_helper_test.rb +++ b/actionpack/test/template/sprockets_helper_test.rb @@ -12,6 +12,7 @@ class SprocketsHelperTest < ActionView::TestCase def protocol() 'http://' end def ssl?() false end def host_with_port() 'localhost' end + def script_name() nil end end def setup diff --git a/actionpack/test/ts_isolated.rb b/actionpack/test/ts_isolated.rb index 7430de2299..ae2a0c95f6 100644 --- a/actionpack/test/ts_isolated.rb +++ b/actionpack/test/ts_isolated.rb @@ -3,7 +3,6 @@ $:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib') require 'minitest/autorun' require 'rbconfig' -require 'active_support/core_ext/kernel/reporting' require 'abstract_unit' class TestIsolated < ActiveSupport::TestCase diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 9b121510a9..4861b0a002 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -28,8 +28,8 @@ to integrate with Action Pack out of the box: +ActiveModel::Model+. person.age # => 18 person.valid? # => false -It includes model name instrospection, conversions, translations and -validations, resulting in a class suitable to be used with ActionPack. +It includes model name introspections, conversions, translations and +validations, resulting in a class suitable to be used with Action Pack. See +ActiveModel::Model+ for more examples. Active Model also provides the following functionality to have ORM-like diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index 8445113b64..6825fdc653 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -3,8 +3,8 @@ module ActiveModel # == Active Model Basic Model # # Includes the required interface for an object to interact with +ActionPack+, - # using different +ActiveModel+ modules. It includes model name instrospection, - # conversions, translations and validations . Besides that, it allows you to + # using different +ActiveModel+ modules. It includes model name introspections, + # conversions, translations and validations. Besides that, it allows you to # initialize the object with a hash of attributes, pretty much like # +ActiveRecord+ does. # @@ -51,7 +51,7 @@ module ActiveModel # person = Person.new(:id => 1, :name => 'bob') # person.omg # => true # - # For more detailed information on other functionality available, please refer + # For more detailed information on other functionalities available, please refer # to the specific modules included in +ActiveModel::Model+ (see below). module Model def self.included(base) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7887d59aad..512c52338e 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -28,6 +28,7 @@ module ActiveRecord @target = nil @owner, @reflection = owner, reflection @updated = false + @stale_state = nil reset reset_scope @@ -231,7 +232,8 @@ module ActiveRecord def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) + attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) + record.assign_attributes(attributes, :without_protection => true) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 3e27e85f02..93c243e7b1 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -189,7 +189,7 @@ module ActiveRecord # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) value = read_attribute(attribute) - !value.nil? || (value.respond_to?(:empty?) && !value.empty?) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end # Returns the column object for the named attribute. diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 8e0b3e1402..51389c84d6 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -111,9 +111,8 @@ module ActiveRecord # end # Project.primary_key # => "foo_id" def primary_key=(value) - @original_primary_key = @primary_key if defined?(@primary_key) - @primary_key = value && value.to_s - @quoted_primary_key = nil + @primary_key = value && value.to_s + @quoted_primary_key = nil end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index e33903622b..731c07547a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -375,7 +375,7 @@ module ActiveRecord def tables(name = nil, database = nil, like = nil) #:nodoc: sql = "SHOW TABLES " - sql << "IN #{database} " if database + sql << "IN #{quote_table_name(database)} " if database sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 3f90d25962..e75a2a1cb4 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -193,7 +193,6 @@ module ActiveRecord @attributes = self.class.initialize_attributes(coder['attributes']) @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) - init_internals @new_record = false diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4d73cdd37a..9b2dc096a0 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -133,8 +133,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @original_locking_column = @locking_column if defined?(@locking_column) - @locking_column = value.to_s + @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index b8764217d3..99847ac161 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -114,11 +114,18 @@ module ActiveRecord # You can also just define your own <tt>self.table_name</tt> method; see # the documentation for ActiveRecord::Base#table_name. def table_name=(value) - @original_table_name = @table_name if defined?(@table_name) - @table_name = value && value.to_s - @quoted_table_name = nil - @arel_table = nil - @relation = Relation.new(self, arel_table) + value = value && value.to_s + + if defined?(@table_name) + return if value == @table_name + reset_column_information if connected? + end + + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name + @relation = Relation.new(self, arel_table) end # Returns a quoted version of the table name, used to construct SQL statements. @@ -152,8 +159,7 @@ module ActiveRecord # Sets the value of inheritance_column def inheritance_column=(value) - @original_inheritance_column = inheritance_column - @inheritance_column = value.to_s + @inheritance_column = value.to_s end def sequence_name @@ -165,7 +171,8 @@ module ActiveRecord end def reset_sequence_name #:nodoc: - self.sequence_name = connection.default_sequence_name(table_name, primary_key) + @sequence_name = connection.default_sequence_name(table_name, primary_key) + @explicit_sequence_name = false end # Sets the name of the sequence to use when generating ids to the given @@ -183,8 +190,8 @@ module ActiveRecord # self.sequence_name = "projectseq" # default would have been "project_seq" # end def sequence_name=(value) - @original_sequence_name = @sequence_name if defined?(@sequence_name) @sequence_name = value.to_s + @explicit_sequence_name = true end # Indicates whether the table associated with this class exists diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 7e74fe7d13..cf0092e0e3 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -458,7 +458,7 @@ db_namespace = namespace :db do db_namespace["test:load_schema"].invoke when :sql db_namespace["test:load_structure"].invoke - end + end end # desc "Recreate the test database from an existent structure.sql file" diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb index 0b75983484..6a38211bff 100644 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -1,6 +1,6 @@ #FIXME Remove if ArJdbcMysql will give. module ArJdbcMySQL #:nodoc: - class Error < StandardError + class Error < StandardError #:nodoc: attr_accessor :error_number, :sql_state def initialize msg diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 4b075183c3..d06020b3ce 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -14,7 +14,7 @@ module ActiveRecord def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") - super(I18n.t("activerecord.errors.messages.record_invalid", :errors => errors)) + super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) end end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 99a022461e..f3bb70fb41 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -30,6 +30,10 @@ module ActiveRecord attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } end + def accessible_attributes + attributes.reject(&:reference?) + end + hook_for :test_framework protected diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 5c47f8b241..d56f9f57a4 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -3,5 +3,10 @@ class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> belongs_to :<%= attribute.name %> <% end -%> +<% if !accessible_attributes.empty? -%> + attr_accessible <%= accessible_attributes.map {|a| ":#{a.name}" }.sort.join(', ') %> +<% else -%> + # attr_accessible :title, :body +<% end -%> end <% end -%> diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 7fe2c02c04..5d6de90889 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -46,6 +46,16 @@ module ActiveRecord assert_equal str, value end + def test_tables_quoting + begin + @conn.tables(nil, "foo-bar", nil) + flunk + rescue => e + # assertion for *quoted* database properly + assert_match(/Unknown database 'foo-bar': SHOW TABLES IN `foo-bar`/, e.inspect) + end + end + private def insert(ctx, data) binds = data.map { |name, value| diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index d5676bc522..10fdf88592 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -35,6 +35,17 @@ module ActiveRecord def test_table_exists_wrong_schema assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") end + + def test_tables_quoting + begin + @connection.tables(nil, "foo-bar", nil) + flunk + rescue => e + # assertion for *quoted* database properly + assert_match(/Unknown database 'foo-bar': SHOW TABLES IN `foo-bar`/, e.inspect) + end + end + end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 3967009c82..02a7f6af78 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -130,6 +130,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.car_id end + def test_association_protect_foreign_key + invoice = Invoice.create + + line_item = invoice.line_items.new + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.new :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.build + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.build :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.create + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.create :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + end + def test_association_conditions_bypass_attribute_protection car = Car.create(:name => 'honda') diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 246877bbed..9c05b36426 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -448,6 +448,22 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.car_id end + def test_association_protect_foreign_key + pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") + + ship = pirate.build_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.build_ship :pirate_id => pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship :pirate_id => pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + end + def test_association_conditions_bypass_attribute_protection car = Car.create(:name => 'honda') diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 23bdb09514..ef01476ae4 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -30,9 +30,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase t = Topic.new t.title = "hello there!" t.written_on = Time.now + t.author_name = "" assert t.attribute_present?("title") assert t.attribute_present?("written_on") assert !t.attribute_present?("content") + assert !t.attribute_present?("author_name") + end def test_attribute_present_with_booleans diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 698c3d0cb1..01f647b261 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1479,6 +1479,30 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_clear_cash_when_setting_table_name + Joke.table_name = "cold_jokes" + before_columns = Joke.columns + before_seq = Joke.sequence_name + + Joke.table_name = "funny_jokes" + after_columns = Joke.columns + after_seq = Joke.sequence_name + + assert_not_equal before_columns, after_columns + assert_not_equal before_seq, after_seq unless before_seq.blank? && after_seq.blank? + end + + def test_dont_clear_sequence_name_when_setting_explicitly + Joke.sequence_name = "black_jokes_seq" + Joke.table_name = "cold_jokes" + before_seq = Joke.sequence_name + + Joke.table_name = "funny_jokes" + after_seq = Joke.sequence_name + + assert_equal before_seq, after_seq unless before_seq.blank? && after_seq.blank? + end + def test_set_table_name_symbol_converted_to_string Joke.table_name = :cold_jokes assert_equal 'cold_jokes', Joke.table_name @@ -1920,6 +1944,15 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end + def test_marshalling_new_record_round_trip_with_associations + post = Post.new + post.comments.build + + post = Marshal.load(Marshal.dump(post)) + + assert post.new_record?, "should be a new record" + end + def test_attribute_names assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], Company.attribute_names diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index a802cfbf31..917a03bb34 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -89,14 +89,16 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal Entrant.arel_engine, Bird.arel_engine end - def test_associations_should_work_when_model_has_no_connection - begin - ActiveRecord::Model.remove_connection - assert_nothing_raised ActiveRecord::ConnectionNotEstablished do - College.first.courses.first + unless in_memory_db? + def test_associations_should_work_when_model_has_no_connection + begin + ActiveRecord::Model.remove_connection + assert_nothing_raised ActiveRecord::ConnectionNotEstablished do + College.first.courses.first + end + ensure + ActiveRecord::Model.establish_connection 'arunit' end - ensure - ActiveRecord::Model.establish_connection 'arunit' end end end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 628029f8df..a8e513d81f 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -8,6 +8,16 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase I18n.backend = I18n::Backend::Simple.new end + def reset_i18n_load_path + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + yield + ensure + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + # validates_associated: generate_message(attr_name, :invalid, :message => custom_message, :value => value) def test_generate_message_invalid_with_default_message assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :value => 'title') @@ -35,4 +45,13 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase assert_equal "Validation failed: Title is invalid, Title can't be blank", ActiveRecord::RecordInvalid.new(topic).message end + test "RecordInvalid exception translation falls back to the :errors namespace" do + reset_i18n_load_path do + I18n.backend.store_translations 'en', :errors => {:messages => {:record_invalid => 'fallback message'}} + topic = Topic.new + topic.errors.add(:title, :blank) + assert_equal "fallback message", ActiveRecord::RecordInvalid.new(topic).message + end + end + end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 674e4acfd6..e4a13870d7 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,10 +1,9 @@ require 'active_support/core_ext/hash/keys' -# This class has dubious semantics and we only have it so that -# people can write <tt>params[:key]</tt> instead of <tt>params['key']</tt> -# and they get the same value for both keys. - module ActiveSupport + # This class has dubious semantics and we only have it so that + # people can write <tt>params[:key]</tt> instead of <tt>params['key']</tt> + # and they get the same value for both keys. class HashWithIndifferentAccess < Hash # Always returns true, so that <tt>Array#extract_options!</tt> finds members of this class. diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index 82507c1e03..c167efc1a7 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -1,32 +1,32 @@ -# lazy_load_hooks allows rails to lazily load a lot of components and thus making the app boot faster. Because of -# this feature now there is no need to require <tt>ActiveRecord::Base</tt> at boot time purely to apply configuration. Instead -# a hook is registered that applies configuration once <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is used -# as example but this feature can be applied elsewhere too. -# -# Here is an example where +on_load+ method is called to register a hook. -# -# initializer "active_record.initialize_timezone" do -# ActiveSupport.on_load(:active_record) do -# self.time_zone_aware_attributes = true -# self.default_timezone = :utc -# end -# end -# -# When the entirety of +activerecord/lib/active_record/base.rb+ has been evaluated then +run_load_hooks+ is invoked. -# The very last line of +activerecord/lib/active_record/base.rb+ is: -# -# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) -# module ActiveSupport - @load_hooks = Hash.new {|h,k| h[k] = [] } - @loaded = {} + # lazy_load_hooks allows rails to lazily load a lot of components and thus making the app boot faster. Because of + # this feature now there is no need to require <tt>ActiveRecord::Base</tt> at boot time purely to apply configuration. Instead + # a hook is registered that applies configuration once <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is used + # as example but this feature can be applied elsewhere too. + # + # Here is an example where +on_load+ method is called to register a hook. + # + # initializer "active_record.initialize_timezone" do + # ActiveSupport.on_load(:active_record) do + # self.time_zone_aware_attributes = true + # self.default_timezone = :utc + # end + # end + # + # When the entirety of +activerecord/lib/active_record/base.rb+ has been evaluated then +run_load_hooks+ is invoked. + # The very last line of +activerecord/lib/active_record/base.rb+ is: + # + # ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) + # + @load_hooks = Hash.new { |h,k| h[k] = [] } + @loaded = Hash.new { |h,k| h[k] = [] } def self.on_load(name, options = {}, &block) - if base = @loaded[name] + @loaded[name].each do |base| execute_hook(base, options, block) - else - @load_hooks[name] << [block, options] end + + @load_hooks[name] << [block, options] end def self.execute_hook(base, options, block) @@ -38,7 +38,7 @@ module ActiveSupport end def self.run_load_hooks(name, base = Object) - @loaded[name] = base + @loaded[name] << base @load_hooks[name].each do |hook, options| execute_hook(base, options, hook) end diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 35f400f9df..bfbb838ea7 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,28 +1,28 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' -# The TimeZone class serves as a wrapper around TZInfo::Timezone instances. It allows us to do the following: -# -# * Limit the set of zones provided by TZInfo to a meaningful subset of 142 zones. -# * Retrieve and display zones with a friendlier name (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). -# * Lazily load TZInfo::Timezone instances only when they're needed. -# * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+, +parse+, +at+ and +now+ methods. -# -# If you set <tt>config.time_zone</tt> in the Rails Application, you can access this TimeZone object via <tt>Time.zone</tt>: -# -# # application.rb: -# class Application < Rails::Application -# config.time_zone = "Eastern Time (US & Canada)" -# end -# -# Time.zone # => #<TimeZone:0x514834...> -# Time.zone.name # => "Eastern Time (US & Canada)" -# Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00 -# -# The version of TZInfo bundled with Active Support only includes the definitions necessary to support the zones -# defined by the TimeZone class. If you need to use zones that aren't defined by TimeZone, you'll need to install the TZInfo gem -# (if a recent version of the gem is installed locally, this will be used instead of the bundled version.) module ActiveSupport + # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. It allows us to do the following: + # + # * Limit the set of zones provided by TZInfo to a meaningful subset of 142 zones. + # * Retrieve and display zones with a friendlier name (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). + # * Lazily load TZInfo::Timezone instances only when they're needed. + # * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+, +parse+, +at+ and +now+ methods. + # + # If you set <tt>config.time_zone</tt> in the Rails Application, you can access this TimeZone object via <tt>Time.zone</tt>: + # + # # application.rb: + # class Application < Rails::Application + # config.time_zone = "Eastern Time (US & Canada)" + # end + # + # Time.zone # => #<TimeZone:0x514834...> + # Time.zone.name # => "Eastern Time (US & Canada)" + # Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00 + # + # The version of TZInfo bundled with Active Support only includes the definitions necessary to support the zones + # defined by the TimeZone class. If you need to use zones that aren't defined by TimeZone, you'll need to install the TZInfo gem + # (if a recent version of the gem is installed locally, this will be used instead of the bundled version.) class TimeZone # Keys are Rails TimeZone names, values are TZInfo identifiers MAPPING = { diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index dbb6c71907..4551dd2f2d 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -12,7 +12,6 @@ java_import org.xml.sax.InputSource unless defined? InputSource java_import org.xml.sax.Attributes unless defined? Attributes java_import org.w3c.dom.Node unless defined? Node -# = XmlMini JRuby JDOM implementation module ActiveSupport module XmlMini_JDOM #:nodoc: extend self diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 16570c6aea..26556598fd 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -2,7 +2,6 @@ require 'libxml' require 'active_support/core_ext/object/blank' require 'stringio' -# = XmlMini LibXML implementation module ActiveSupport module XmlMini_LibXML #:nodoc: extend self diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index 2536b1f33e..acc018fd2d 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -2,9 +2,8 @@ require 'libxml' require 'active_support/core_ext/object/blank' require 'stringio' -# = XmlMini LibXML implementation using a SAX-based parser module ActiveSupport - module XmlMini_LibXMLSAX + module XmlMini_LibXMLSAX #:nodoc: extend self # Class that will build the hash while the XML document diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index 04ec9e8ab8..bb0a52bdcf 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -7,7 +7,6 @@ end require 'active_support/core_ext/object/blank' require 'stringio' -# = XmlMini Nokogiri implementation module ActiveSupport module XmlMini_Nokogiri #:nodoc: extend self diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 93fd3dfe57..30b94aac47 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -7,9 +7,8 @@ end require 'active_support/core_ext/object/blank' require 'stringio' -# = XmlMini Nokogiri implementation using a SAX-based parser module ActiveSupport - module XmlMini_NokogiriSAX + module XmlMini_NokogiriSAX #:nodoc: extend self # Class that will build the hash while the XML document diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index a13ad10118..a2a87337a6 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -2,7 +2,6 @@ require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/object/blank' require 'stringio' -# = XmlMini ReXML implementation module ActiveSupport module XmlMini_REXML #:nodoc: extend self diff --git a/activesupport/test/lazy_load_hooks_test.rb b/activesupport/test/lazy_load_hooks_test.rb index 58ccc14324..7851634dbf 100644 --- a/activesupport/test/lazy_load_hooks_test.rb +++ b/activesupport/test/lazy_load_hooks_test.rb @@ -8,6 +8,16 @@ class LazyLoadHooksTest < ActiveSupport::TestCase assert_equal 1, i end + def test_basic_hook_with_two_registrations + i = 0 + ActiveSupport.on_load(:basic_hook_with_two) { i += incr } + assert_equal 0, i + ActiveSupport.run_load_hooks(:basic_hook_with_two, FakeContext.new(2)) + assert_equal 2, i + ActiveSupport.run_load_hooks(:basic_hook_with_two, FakeContext.new(5)) + assert_equal 7, i + end + def test_hook_registered_after_run i = 0 ActiveSupport.run_load_hooks(:registered_after) @@ -16,6 +26,25 @@ class LazyLoadHooksTest < ActiveSupport::TestCase assert_equal 1, i end + def test_hook_registered_after_run_with_two_registrations + i = 0 + ActiveSupport.run_load_hooks(:registered_after_with_two, FakeContext.new(2)) + ActiveSupport.run_load_hooks(:registered_after_with_two, FakeContext.new(5)) + assert_equal 0, i + ActiveSupport.on_load(:registered_after_with_two) { i += incr } + assert_equal 7, i + end + + def test_hook_registered_interleaved_run_with_two_registrations + i = 0 + ActiveSupport.run_load_hooks(:registered_interleaved_with_two, FakeContext.new(2)) + assert_equal 0, i + ActiveSupport.on_load(:registered_interleaved_with_two) { i += incr } + assert_equal 2, i + ActiveSupport.run_load_hooks(:registered_interleaved_with_two, FakeContext.new(5)) + assert_equal 7, i + end + def test_hook_receives_a_context i = 0 ActiveSupport.on_load(:contextual) { i += incr } diff --git a/railties/guides/source/api_app.textile b/railties/guides/source/api_app.textile new file mode 100644 index 0000000000..f2d00c5768 --- /dev/null +++ b/railties/guides/source/api_app.textile @@ -0,0 +1,276 @@ +h2. Using Rails for API-only Apps + +In this guide you will learn: + +* What Rails provides for API-only applications +* How to configure Rails to start without any browser features +* How to decide which middlewares you will want to include +* How to decide which modules to use in your controller + +NOTE: This guide reflects features that have not yet been fully implemented. Docs first :) + +endprologue. + +h3. What is an API app? + +Traditionally, when people said that they used Rails as an "API", they meant +providing a programmatically accessible API alongside their web application. +For example, GitHub provides "an API":http://developer.github.com that you can use from your own custom clients. + +With the advent of client-side frameworks, more developers are using Rails to build a backend that is shared between their web application and other native applications. + +For example, Twitter uses its "public API":https://dev.twitter.com in its web application, which is built as a static site that consumes JSON resources. + +Instead of using Rails to generate dynamic HTML that will communicate with the server through forms and links, many developers are treating their web application as just another client, delivered as static HTML, CSS and JavaScript, and consuming a simple JSON API + +This guide covers building a Rails application that serves JSON resources to an API client *or* client-side framework. + +h3. Why use Rails for JSON APIs? + +The first question a lot of people have when thinking about building a JSON API using Rails is: "isn't using Rails to spit out some JSON overkill? Shouldn't I just use something like Sinatra?" + +For very simple APIs, this may be true. However, even in very HTML-heavy applications, most of an application's logic is actually outside of the view layer. + +The reason most people use Rails is that it provides a set of defaults that allows us to get up and running quickly without having to make a lot of trivial decisions. + +Let's take a look at some of the things that Rails provides out of the box that are still applicable to API applications. + +Handled at the middleware layer: + +* Reloading: Rails applications support transparent reloading. This works even if your application gets big and restarting the server for every request becomes non-viable. +* Development Mode: Rails application come with smart defaults for development, making development pleasant without compromising production-time performance. +* Test Mode: Ditto test mode. +* Logging: Rails applications log every request, with a level of verbosity appropriate for the current mode. Rails logs in development include information about the request environment, database queries, and basic performance information. +* Security: Rails detects and thwarts "IP spoofing attacks":http://en.wikipedia.org/wiki/IP_address_spoofing and handles cryptographic signatures in a "timing attack":http://en.wikipedia.org/wiki/Timing_attack aware way. Don't know what an IP spoofing attack or a timing attack is? Exactly. +* Parameter Parsing: Want to specify your parameters as JSON instead of as a URL-encoded String? No problem. Rails will decode the JSON for you and make it available in +params+. Want to use nested URL-encoded params? That works too. +* Conditional GETs: Rails handles conditional +GET+, (+ETag+ and +Last-Modified+), processing request headers and returning the correct response headers and status code. All you need to do is use the "stale?":http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F check in your controller, and Rails will handle all of the HTTP details for you. +* Caching: If you use +dirty?+ with public cache control, Rails will automatically cache your responses. You can easily configure the cache store. +* HEAD requests: Rails will transparently convert +HEAD+ requests into +GET+ requests, and return just the headers on the way out. This makes +HEAD+ work reliably in all Rails APIs. + +While you could obviously build these up in terms of existing Rack middlewares, I think this list demonstrates that the default Rails middleware stack provides a lot of value, even if you're "just generating JSON". + +Handled at the ActionPack layer: + +* Resourceful Routing: If you're building a RESTful JSON API, you want to be using the Rails router. Clean and conventional mapping from HTTP to controllers means not having to spend time thinking about how to model your API in terms of HTTP. +* URL Generation: The flip side of routing is URL generation. A good API based on HTTP includes URLs (see "the GitHub gist API":http://developer.github.com/v3/gists/ for an example). +* Header and Redirection Responses: +head :no_content+ and +redirect_to user_url(current_user)+ come in handy. Sure, you could manually add the response headers, but why? +* Content Negotiation: The Rails +respond_to+ and +respond_with+ features automatically figure out which MIME type to serve, based on the request's +Accept+ header and available types. If you ever need to add support for types other than JSON (XML, CSV, or some proprietary format), this will come in handy. +* Caching: Rails provides page, action and fragment caching. Fragment caching is especially helpful when building up a nested JSON object. +* Basic, Digest and Token Authentication: Rails comes with out-of-the-box support for three kinds of HTTP authentication. +* Instrumentation: Rails 3.0 added an instrumentation API that will trigger registered handlers for a variety of events, such as action processing, sending a file or data, redirection, and database queries. The payload of each event comes with relevant information (for the action processing event, the payload includes the controller, action, params, request format, request method and the request's full path). +* Generators: This may be passé for advanced Rails users, but it can be nice to generate a resource and get your model, controller, test stubs, and routes created for you in a single command. +* Plugins: Many third-party libraries come with support for Rails that reduces or eliminates the cost of setting up and gluing together the library and the web framework. This includes things like overriding default generators, adding rake tasks, and honoring Rails choices (like the logger and cache backend). + +Of course, the Rails boot process also glues together all registered components. For example, the Rails boot process is what uses your +config/database.yml+ file when configuring ActiveRecord. + +**The short version is**: you may not have thought about which parts of Rails are still applicable even if you remove the view layer, but the answer turns out to be "most of it". + +h3. The Basic Configuration + +If you're building a Rails application that will be an API server first and foremost, you can start with a more limited subset of Rails and add in features as needed. + +You can generate a new bare Rails app: + +<shell> +$ rails new my_api --api +</shell> + +This will do three main things for you: + +* Configure your application to start with a more limited set of middleware than normal. Specifically, it will not include any middleware primarily useful for browser applications (like cookie support) by default. +* Make +ApplicationController+ inherit from +ActionController::API+ instead of +ActionController::Base+. As with middleware, this will leave out any +ActionController+ modules that provide functionality primarily used by browser applications. +* Configure the generators to skip generating views, helpers and assets when you generate a new resource. + +If you want to take an existing app and make it an API app, follow the following steps. + +In +config/application.rb+ add the following lines at the top of the +Application+ class: + +<ruby> +config.middleware.api_only! +config.generators.api_only! +</ruby> + +Change +app/controllers/application_controller.rb+: + +<ruby> +# instead of +class ApplicationController < ActionController::Base +end + +# do +class ApplicationController < ActionController::API +end +</ruby> + +h3. Choosing Middlewares + +An API application comes with the following middlewares by default. + +* +Rack::Cache+: Caches responses with public +Cache-Control+ headers using HTTP caching semantics. See below for more information. +* +Rack::Sendfile+: Uses a front-end server's file serving support from your Rails application. +* +Rack::Lock+: If your application is not marked as threadsafe (+config.threadsafe!+), this middleware will add a mutex around your requests. +* +ActionDispatch::RequestId+: +* +Rails::Rack::Logger+: +* +ActionDispatch::ShowExceptions+: Rescue exceptions and re-dispatch them to an exception handling application +* +ActionDispatch::DebugExceptions+: Log exceptions +* +ActionDispatch::RemoteIp+: Protect against IP spoofing attacks +* +ActionDispatch::Reloader+: In development mode, support code reloading. +* +ActionDispatch::ParamsParser+: Parse XML, YAML and JSON parameters when the request's +Content-Type+ is one of those. +* +ActionDispatch::Head+: Dispatch +HEAD+ requests as +GET+ requests, and return only the status code and headers. +* +Rack::ConditionalGet+: Supports the +stale?+ feature in Rails controllers. +* +Rack::ETag+: Automatically set an +ETag+ on all string responses. This means that if the same response is returned from a controller for the same URL, the server will return a +304 Not Modified+, even if no additional caching steps are taken. This is primarily a client-side optimization; it reduces bandwidth costs but not server processing time. + +Other plugins, including +ActiveRecord+, may add additional middlewares. In general, these middlewares are agnostic to the type of app you are building, and make sense in an API-only Rails application. + +You can get a list of all middlewares in your application via: + +<shell> +$ rake middleware +</shell> + +h4. Using Rack::Cache + +When used with Rails, +Rack::Cache+ uses the Rails cache store for its entity and meta stores. This means that if you use memcache, for your Rails app, for instance, the built-in HTTP cache will use memcache. + +To make use of +Rack::Cache+, you will want to use +stale?+ in your controller. Here's an example of +stale?+ in use. + +<ruby> +def show + @post = Post.find(params[:id]) + + if stale?(:last_modified => @post.updated_at) + render json: @post + end +end +</ruby> + +The call to +stale?+ will compare the +If-Modified-Since+ header in the request with +@post.updated_at+. If the header is newer than the last modified, this action will return a +304 Not Modified+ response. Otherwise, it will render the response and include a +Last-Modified+ header with the response. + +Normally, this mechanism is used on a per-client basis. +Rack::Cache+ allows us to share this caching mechanism across clients. We can enable cross-client caching in the call to +stale?+ + +<ruby> +def show + @post = Post.find(params[:id]) + + if stale?(:last_modified => @post.updated_at, :public => true) + render json: @post + end +end +</ruby> + +This means that +Rack::Cache+ will store off +Last-Modified+ value for a URL in the Rails cache, and add an +If-Modified-Since+ header to any subsequent inbound requests for the same URL. + +Think of it as page caching using HTTP semantics. + +NOTE: The +Rack::Cache+ middleware is always outside of the +Rack::Lock+ mutex, even in single-threaded apps. + +h4. Using Rack::Sendfile + +When you use the +send_file+ method in a Rails controller, it sets the +X-Sendfile+ header. +Rack::Sendfile+ is responsible for actually sending the file. + +If your front-end server supports accelerated file sending, +Rack::Sendfile+ will offload the actual file sending work to the front-end server. + +You can configure the name of the header that your front-end server uses for this purposes using +config.action_dispatch.x_sendfile_header+ in the appropriate environment config file. + +You can learn more about how to use +Rack::Sendfile+ with popular front-ends in "the Rack::Sendfile documentation":http://rubydoc.info/github/rack/rack/master/Rack/Sendfile + +The values for popular servers once they are configured to support accelerated file sending: + +<ruby> +# Apache and lighttpd +config.action_dispatch.x_sendfile_header = "X-Sendfile" + +# nginx +config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" +</ruby> + +Make sure to configure your server to support these options following the instructions in the +Rack::Sendfile+ documentation. + +NOTE: The +Rack::Sendfile+ middleware is always outside of the +Rack::Lock+ mutex, even in single-threaded apps. + +h4. Using ActionDispatch::ParamsParser + ++ActionDispatch::ParamsParser+ will take parameters from the client in JSON and make them available in your controller as +params+. + +To use this, your client will need to make a request with JSON-encoded parameters and specify the +Content-Type+ as +application/json+. + +Here's an example in jQuery: + +<plain> +jQuery.ajax({ + type: 'POST', + url: '/people' + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({ person: { firstName: "Yehuda", lastName: "Katz" } }), + + success: function(json) { } +}); +</plain> + ++ActionDispatch::ParamsParser+ will see the +Content-Type+ and your params will be +{ :person => { :firstName => "Yehuda", :lastName => "Katz" } }+. + +h4. Other Middlewares + +Rails ships with a number of other middlewares that you might want to use in an API app, especially if one of your API clients is the browser: + +* +Rack::SSL+: Redirects any HTTP request to HTTPS. +* +Rack::Runtime+: Adds a header to the response listing the total runtime of the request. +* +Rack::MethodOverride+: Allows the use of the +_method+ hack to route POST requests to other verbs. +* +ActionDispatch::Cookies+: Supports the +cookie+ method in +ActionController+, including support for signed and encrypted cookies. +* +ActionDispatch::Flash+: Supports the +flash+ mechanism in +ActionController+. +* +ActionDispatch::BestStandards+: Tells Internet Explorer to use the most standards-compliant available renderer. In production mode, if ChromeFrame is available, use ChromeFrame. +* Session Management: If a +config.session_store+ is supplied, this middleware makes the session available as the +session+ method in +ActionController+. + +Any of these middlewares can be adding via: + +<ruby> +config.middleware.use Rack::MethodOverride +</ruby> + +h4. Removing Middlewares + +If you don't want to use a middleware that is included by default in the API-only middleware set, you can remove it using +config.middleware.delete+: + +<ruby> +config.middleware.delete ::Rack::Sendfile +</ruby> + +Keep in mind that removing these features may remove support for certain features in +ActionController+. + +h3. Choosing Controller Modules + +An API application (using +ActionController::API+) comes with the following controller modules by default: + +* +AbstractController::Translation+: Support for the +l+ and +t+ localization and translation methods. These delegate to +I18n.translate+ and +I18n.localize+. +* +ActionController::UrlFor+: Makes +url_for+ and friends available. +* +ActionController::Redirecting+: Support for +redirect_to+ +* +ActionController::Renderers::JSON+: Support for +render :json+ +* +ActionController::ConditionalGet+: Support for +stale?+ +* +ActionController::RackDelegation+: Support for the +request+ and +response+ methods returning +ActionDispatch::Request+ and +ActionDispatch::Response+ objects. +* +ActionController::MimeResponds+: Support for content negotiation (+respond_to+, +respond_with+) +* +ActionController::DataStreaming+: Support for +send_file+ and +send_data+ +* +AbstractController::Callbacks+: Support for +before_filter+ and friends +* +ActionController::Instrumentation+: Support for the instrumentation hooks defined by +ActionController+ (see "the source":https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/instrumentation.rb for more). + +Other plugins may add additional modules. You can get a list of all modules included into +ActionController::API+ in the rails console: + +<shell> +$ irb +>> ActionController::API.ancestors - ActionController::Metal.ancestors +</shell> + +h4. Adding Other Modules + +All ActionController modules know about their dependent modules, so you can feel free to include any modules into your controllers, and all dependencies will be included and set up as well. + +Some common modules you might want to add: + +* +ActionController::HTTPAuthentication::Basic+ (or +Digest+ or +Token): Support for basic, digest or token HTTP authentication. +* +ActionController::Rendering+: Support for templating and +ActionView+. +* +AbstractController::Layouts+: Support for layouts when rendering. +* +ActionController::Renderers::XML+: Support for +render :xml+. +* +ActionController::Cookies+: Support for +cookies+, which includes support for signed and encrypted cookies. This requires the cookie middleware. +* +ActionController::Rescue+: Support for +rescue_from+. + +The best place to add a module is in your +ApplicationController+. You can also add modules to individual controllers. diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile index 25201888e7..320f1e9d20 100644 --- a/railties/guides/source/i18n.textile +++ b/railties/guides/source/i18n.textile @@ -521,7 +521,7 @@ h5. Bulk and Namespace Lookup To look up multiple translations at once, an array of keys can be passed: <ruby> -I18n.t [:odd, :even], :scope => 'activerecord.errors.messages' +I18n.t [:odd, :even], :scope => 'errors.messages' # => ["must be odd", "must be even"] </ruby> diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 42665114be..c5567b3350 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -622,6 +622,7 @@ You can specify what Rails should route +"/"+ to with the +root+ method: <ruby> root :to => 'pages#main' +root 'pages#main' # shortcut for the above </ruby> You should put the +root+ route at the top of the file, because it is the most popular route and should be matched first. You also need to delete the +public/index.html+ file for the root route to take effect. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 8d64aff430..10fa63c303 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -239,7 +239,7 @@ module Rails middleware.use ::Rack::Lock unless config.allow_concurrency middleware.use ::Rack::Runtime - middleware.use ::Rack::MethodOverride + middleware.use ::Rack::MethodOverride unless config.middleware.api_only? middleware.use ::ActionDispatch::RequestId middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path) @@ -252,9 +252,9 @@ module Rails end middleware.use ::ActionDispatch::Callbacks - middleware.use ::ActionDispatch::Cookies + middleware.use ::ActionDispatch::Cookies unless config.middleware.api_only? - if config.session_store + if !config.middleware.api_only? && config.session_store if config.force_ssl && !config.session_options.key?(:secure) config.session_options[:secure] = true end @@ -267,7 +267,7 @@ module Rails middleware.use ::Rack::ConditionalGet middleware.use ::Rack::ETag, "no-cache" - if config.action_dispatch.best_standards_support + if !config.middleware.api_only? && config.action_dispatch.best_standards_support middleware.use ::ActionDispatch::BestStandardsSupport, config.action_dispatch.best_standards_support end end diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index f8ad17773a..0efa21d82c 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -9,6 +9,14 @@ module Rails class MiddlewareStackProxy #:nodoc: def initialize @operations = [] + @api_only = false + end + + attr_reader :api_only + alias :api_only? :api_only + + def api_only! + @api_only = true end def insert_before(*args, &block) diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index acf47a03e5..e47784994a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -54,7 +54,7 @@ module <%= app_const_base %> # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. - # config.active_record.whitelist_attributes = true + <%= comment_if :skip_active_record %>config.active_record.whitelist_attributes = true # Specifies wether or not has_many or has_one association option :dependent => :restrict raises # an exception. If set to true, then an ActiveRecord::DeleteRestrictionError exception would be diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 5ad51f8476..0354c08067 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -20,6 +20,7 @@ class LoadingTest < ActiveSupport::TestCase app_file "app/models/post.rb", <<-MODEL class Post < ActiveRecord::Base validates_acceptance_of :title, :accept => "omg" + attr_accessible :title end MODEL diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index a1e6066b16..a190a31fc7 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -52,6 +52,36 @@ module ApplicationTests ], middleware end + test "api middleware stack" do + add_to_config "config.middleware.api_only!" + add_to_config "config.force_ssl = true" + add_to_config "config.action_dispatch.x_sendfile_header = 'X-Sendfile'" + + boot! + + assert_equal [ + "Rack::SSL", + "Rack::Sendfile", + "ActionDispatch::Static", + "Rack::Lock", + "ActiveSupport::Cache::Strategy::LocalCache", + "Rack::Runtime", + "ActionDispatch::RequestId", + "Rails::Rack::Logger", + "ActionDispatch::ShowExceptions", + "ActionDispatch::DebugExceptions", + "ActionDispatch::RemoteIp", + "ActionDispatch::Reloader", + "ActionDispatch::Callbacks", + "ActiveRecord::ConnectionAdapters::ConnectionManagement", + "ActiveRecord::QueryCache", + "ActionDispatch::ParamsParser", + "ActionDispatch::Head", + "Rack::ConditionalGet", + "Rack::ETag" + ], middleware + end + test "Rack::Sendfile is not included by default" do boot! diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index a3c24c392b..cf6f9b90c9 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -202,6 +202,7 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-active-record"] assert_no_file "config/database.yml" assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ + assert_file "config/application.rb", /#\s+config\.active_record\.whitelist_attributes = true/ assert_file "config/application.rb", /#\s+config\.active_record\.dependent_restrict_raises = false/ assert_file "test/test_helper.rb" do |helper_content| assert_no_match(/fixtures :all/, helper_content) @@ -350,6 +351,11 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_active_record_whitelist_attributes_is_present_application_config + run_generator + assert_file "config/application.rb", /config\.active_record\.whitelist_attributes = true/ + end + def test_active_record_dependent_restrict_raises_is_present_application_config run_generator assert_file "config/application.rb", /config\.active_record\.dependent_restrict_raises = false/ diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 156fa86eee..e8d933935d 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -317,4 +317,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end end + + def test_attr_accessible_added_with_non_reference_attributes + run_generator + assert_file 'app/models/account.rb', /attr_accessible :age, :name/ + end + + def test_attr_accessible_added_with_comments_when_no_attributes_present + run_generator ["Account"] + assert_file 'app/models/account.rb', /# attr_accessible :title, :body/ + end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 4fb5e6a4eb..b0be555c4c 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -247,7 +247,10 @@ module TestHelpers :activemodel, :activerecord, :activeresource] - arr - remove_from_config "config.active_record.dependent_restrict_raises = false" if to_remove.include? :activerecord + if to_remove.include? :activerecord + remove_from_config "config.active_record.whitelist_attributes = true" + remove_from_config "config.active_record.dependent_restrict_raises = false" + end $:.reject! {|path| path =~ %r'/(#{to_remove.join('|')})/' } end |