diff options
152 files changed, 1645 insertions, 863 deletions
@@ -8,7 +8,7 @@ gemspec gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' -gem 'bcrypt-ruby', '~> 3.0.0' +gem 'bcrypt-ruby', '~> 3.1.0' gem 'jquery-rails', '~> 2.2.0' gem 'turbolinks' gem 'coffee-rails', '~> 4.0.0' @@ -56,7 +56,7 @@ platforms :ruby do group :db do gem 'pg', '>= 0.11.0' gem 'mysql', '>= 2.9.0' - gem 'mysql2', '>= 0.3.10' + gem 'mysql2', '>= 0.3.13' end end @@ -47,20 +47,25 @@ task :install => :build do end desc "Generate documentation for the Rails framework" -Rails::API::RepoTask.new('rdoc') +if ENV['EDGE'] + Rails::API::EdgeTask.new('rdoc') +else + Rails::API::StableTask.new('rdoc') +end desc 'Bump all versions to match version.rb' task :update_versions do require File.dirname(__FILE__) + "/version" File.open("RAILS_VERSION", "w") do |f| - f.puts Rails.version + f.puts Rails::VERSION::STRING end constants = { "activesupport" => "ActiveSupport", "activemodel" => "ActiveModel", "actionpack" => "ActionPack", + "actionview" => "ActionView", "actionmailer" => "ActionMailer", "activerecord" => "ActiveRecord", "railties" => "Rails" diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index e2a6ced1dc..eac3488a62 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,28 @@ +* Skip routes pointing to a redirect or mounted application when generating urls + using an options hash as they aren't relevant and generate incorrect urls. + + Fixes #8018 + + *Andrew White* + +* Move `MissingHelperError` out of the `ClassMethods` module. + + *Yves Senn* + +* Fix an issue where rails raise exception about missing helper where it + should throw `LoadError`. When helper file exists and only loaded file from + this helper does not exist rails should throw LoadError instead of + `MissingHelperError`. + + *Piotr NieÅ‚acny* + +* Fix `ActionDispatch::ParamsParser#parse_formatted_parameters` to rewind body input stream on + parsing json params. + + Fixes #11345 + + *Yuri Bol*, *Paul Nikitochkin* + * Ignore spaces around delimiter in Set-Cookie header. *Yamagishi Kazutoshi* diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc index 1b29abd2d1..08767ae133 100644 --- a/actionpack/RUNNING_UNIT_TESTS.rdoc +++ b/actionpack/RUNNING_UNIT_TESTS.rdoc @@ -15,13 +15,3 @@ To run a single test suite which can be further narrowed down to one test: rake test TEST=path/to/test.rb TESTOPTS="--name=test_something" - -== Dependency on Active Record and database setup - -Test cases in the test/active_record/ directory depend on having -activerecord and sqlite installed. If Active Record is not in -actionpack/../activerecord directory, or the sqlite rubygem is not installed, -these tests are skipped. - -Other tests are runnable from a fresh copy of actionpack without any configuration. - diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 114d3a7c0c..748df26033 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,6 +1,8 @@ require 'rake/testtask' require 'rubygems/package_task' +test_files = Dir.glob('test/{abstract,controller,dispatch,assertions,journey,routing}/**/*_test.rb') + desc "Default Task" task :default => :test @@ -10,7 +12,7 @@ Rake::TestTask.new do |t| # make sure we include the tests in alphabetical order as on some systems # this will not happen automatically and the tests (as a whole) will error - t.test_files = Dir.glob('test/{abstract,controller,dispatch,assertions,journey}/**/*_test.rb').sort + t.test_files = test_files.sort t.warning = true t.verbose = true @@ -19,7 +21,7 @@ end namespace :test do task :isolated do ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) - Dir.glob("test/{abstract,controller,dispatch}/**/*_test.rb").all? do |file| + test_files.all? do |file| sh(ruby, '-w', '-Ilib:test', file) end or raise "Failures" end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 5ae8c6c3b0..e77e4e01e9 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -12,7 +12,24 @@ module AbstractController self._helper_methods = Array.new end + class MissingHelperError < LoadError + def initialize(error, path) + @error = error + @path = "helpers/#{path}.rb" + set_backtrace error.backtrace + + if error.path =~ /^#{path}(\.rb)?$/ + super("Missing helper file helpers/%s.rb" % path) + else + raise error + end + end + end + module ClassMethods + MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError', + 'AbstractController::Helpers::MissingHelperError') + # When a class is inherited, wrap its helper module in a new module. # This ensures that the parent class's module can be changed # independently of the child class's. @@ -134,7 +151,7 @@ module AbstractController begin require_dependency(file_name) rescue LoadError => e - raise MissingHelperError.new(e, file_name) + raise AbstractController::Helpers::MissingHelperError.new(e, file_name) end file_name.camelize.constantize when Module @@ -145,15 +162,6 @@ module AbstractController end end - class MissingHelperError < LoadError - def initialize(error, path) - @error = error - @path = "helpers/#{path}.rb" - set_backtrace error.backtrace - super("Missing helper file helpers/%s.rb" % path) - end - end - private # Makes all the (instance) methods in the helper module available to templates # rendered through this controller. diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index b078beb675..1d77e331f8 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -11,6 +11,23 @@ module ActionController #:nodoc: end module ClassMethods + # Creates new flash types. You can pass as many types as you want to create + # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in + # your controllers and views. For instance: + # + # # in application_controller.rb + # class ApplicationController < ActionController::Base + # add_flash_types :warning + # end + # + # # in your controller + # redirect_to user_path(@user), warning: "Incomplete profile" + # + # # in your view + # <%= warning %> + # + # This method will automatically define a new method for each of the given + # names, and it will be available in your views. def add_flash_types(*types) types.each do |type| next if _flash_types.include?(type) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 4ca1d35489..b4cf8ad2f7 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -226,7 +226,7 @@ module ActionDispatch def raw_post unless @env.include? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(@env['CONTENT_LENGTH'].to_i) + @env['RAW_POST_DATA'] = raw_post_body.read(content_length) raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end @env['RAW_POST_DATA'] diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index e288f026c7..7764763791 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -18,7 +18,11 @@ module ActionDispatch match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) - next if !name && route.requirements.empty? && route.parts.empty? + + # Skip this route unless a name has been provided or it is a + # standard Rails route since we can't determine whether an options + # hash passed to url_for matches a Rack application or a redirect. + next unless name || route.dispatcher? missing_keys = missing_keys(route, parameterized_parts) next unless missing_keys.empty? diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 50e1853094..c8eb0f6f2d 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,6 +16,14 @@ module ActionDispatch @app = app @path = path + # Unwrap any constraints so we can see what's inside for route generation. + # This allows the formatter to skip over any mounted applications or redirects + # that shouldn't be matched when using a url_for without a route name. + while app.is_a?(Routing::Mapper::Constraints) do + app = app.app + end + @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher) + @constraints = constraints @defaults = defaults @required_defaults = nil @@ -93,6 +101,10 @@ module ActionDispatch end end + def dispatcher? + @dispatcher + end + def matches?(request) constraints.all? do |method, value| next true unless request.respond_to?(method) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index fb70b60ef6..b426183488 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -41,7 +41,7 @@ module ActionDispatch when Proc strategy.call(request.raw_post) when :json - data = ActiveSupport::JSON.decode(request.body) + data = ActiveSupport::JSON.decode(request.raw_post) data = {:_json => data} unless data.is_a?(Hash) Request::Utils.deep_munge(data).with_indifferent_access else diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 3ae9f92c0b..0e5dc1fc6c 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -186,10 +186,16 @@ module ActionDispatch klass = Journey::Router::Utils @path_parts.zip(args) do |part, arg| + parameterized_arg = arg.to_param + + if parameterized_arg.nil? || parameterized_arg.empty? + raise_generation_error(args) + end + # Replace each route parameter # e.g. :id for regular parameter or *path for globbing # with ruby string interpolation code - path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(arg.to_param)) + path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(parameterized_arg)) end path end @@ -197,6 +203,25 @@ module ActionDispatch def optimize_routes_generation?(t) t.send(:optimize_routes_generation?) end + + def raise_generation_error(args) + parts, missing_keys = [], [] + + @path_parts.zip(args) do |part, arg| + parameterized_arg = arg.to_param + + if parameterized_arg.nil? || parameterized_arg.empty? + missing_keys << part + end + + parts << [part, arg] + end + + message = "No route matches #{Hash[parts].inspect}" + message << " missing required keys: #{missing_keys.inspect}" + + raise ActionController::UrlGenerationError, message + end end def initialize(route, options) diff --git a/actionpack/test/abstract/helper_test.rb b/actionpack/test/abstract/helper_test.rb index 7960e5b55b..bc3e34684c 100644 --- a/actionpack/test/abstract/helper_test.rb +++ b/actionpack/test/abstract/helper_test.rb @@ -48,6 +48,14 @@ module AbstractController end end + class AbstractInvalidHelpers < AbstractHelpers + include ActionController::Helpers + + path = File.join(File.expand_path('../../fixtures', __FILE__), "helpers_missing") + $:.unshift(path) + self.helpers_path = path + end + class TestHelpers < ActiveSupport::TestCase def setup @controller = AbstractHelpers.new @@ -97,5 +105,22 @@ module AbstractController assert_equal "Hello Default", @controller.response_body end end + + class InvalidHelpersTest < ActiveSupport::TestCase + def test_controller_raise_error_about_real_require_problem + e = assert_raise(LoadError) { AbstractInvalidHelpers.helper(:invalid_require) } + assert_equal "No such file to load -- very_invalid_file_name", e.message + end + + def test_controller_raise_error_about_missing_helper + e = assert_raise(AbstractController::Helpers::MissingHelperError) { AbstractInvalidHelpers.helper(:missing) } + assert_equal "Missing helper file helpers/missing_helper.rb", e.message + end + + def test_missing_helper_error_has_the_right_path + e = assert_raise(AbstractController::Helpers::MissingHelperError) { AbstractInvalidHelpers.helper(:missing) } + assert_equal "helpers/missing_helper.rb", e.path + end + end end end diff --git a/actionpack/test/active_record_unit.rb b/actionpack/test/active_record_unit.rb deleted file mode 100644 index 95fbb112c0..0000000000 --- a/actionpack/test/active_record_unit.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'abstract_unit' - -# Define the essentials -class ActiveRecordTestConnector - cattr_accessor :able_to_connect - cattr_accessor :connected - - # Set our defaults - self.connected = false - self.able_to_connect = true -end - -# Try to grab AR -unless defined?(ActiveRecord) && defined?(FixtureSet) - begin - PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib" - raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR) - $LOAD_PATH.unshift PATH_TO_AR - require 'active_record' - rescue LoadError => e - $stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}" - ActiveRecordTestConnector.able_to_connect = false - end -end -$stderr.flush - - -# Define the rest of the connector -class ActiveRecordTestConnector - class << self - def setup - unless self.connected || !self.able_to_connect - setup_connection - load_schema - require_fixture_models - self.connected = true - end - rescue Exception => e # errors from ActiveRecord setup - $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" - #$stderr.puts " #{e.backtrace.join("\n ")}\n" - self.able_to_connect = false - end - - private - def setup_connection - if Object.const_defined?(:ActiveRecord) - defaults = { :database => ':memory:' } - adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' - options = defaults.merge :adapter => adapter, :timeout => 500 - ActiveRecord::Base.establish_connection(options) - ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options } - ActiveRecord::Base.connection - - Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) - else - raise "Can't setup connection since ActiveRecord isn't loaded." - end - end - - # Load actionpack sqlite tables - def load_schema - File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| - ActiveRecord::Base.connection.execute(sql) unless sql.blank? - end - end - - def require_fixture_models - Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f} - end - end -end - -class ActiveRecordTestCase < ActionController::TestCase - include ActiveRecord::TestFixtures - - # Set our fixture path - if ActiveRecordTestConnector.able_to_connect - self.fixture_path = [FIXTURE_LOAD_PATH] - self.use_transactional_fixtures = false - end - - def self.fixtures(*args) - super if ActiveRecordTestConnector.connected - end - - def run(*args) - super if ActiveRecordTestConnector.connected - end -end - -ActiveRecordTestConnector.setup diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index f7ec6d71b3..e851cc6a63 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -277,7 +277,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end def redirect - redirect_to :action => "get" + redirect_to action_url('get') end end @@ -511,8 +511,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end set.draw do - match ':action', :to => controller, :via => [:get, :post] - get 'get/:action', :to => controller + match ':action', :to => controller, :via => [:get, :post], :as => :action + get 'get/:action', :to => controller, :as => :get_action end self.singleton_class.send(:include, set.url_helpers) diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index b62ed6a8b2..dba9ab688f 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -70,6 +70,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest end end + test 'raw_post is not empty for JSON request' do + with_test_routing do + post '/parse', '{"posts": [{"title": "Post Title"}]}', 'CONTENT_TYPE' => 'application/json' + assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post + end + end + private def assert_parses(expected, actual, headers = {}) with_test_routing do diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 9169658c22..9a77454f30 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -17,10 +17,9 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest end test "parses unbalanced query string with array" do - assert_parses( - {'location' => ["1", "2"], 'age_group' => ["2"]}, - "location[]=1&location[]=2&age_group[]=2" - ) + query = "location[]=1&location[]=2&age_group[]=2" + expected = { 'location' => ["1", "2"], 'age_group' => ["2"] } + assert_parses expected, query end test "parses nested hash" do @@ -30,9 +29,17 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest "note[viewers][viewer][][type]=Group", "note[viewers][viewer][][id]=2" ].join("&") - - expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } } - assert_parses(expected, query) + expected = { + "note" => { + "viewers" => { + "viewer" => [ + { "id" => "1", "type" => "User" }, + { "type" => "Group", "id" => "2" } + ] + } + } + } + assert_parses expected, query end test "parses more complex nesting" do @@ -48,7 +55,6 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest "products[second]=Pc", "=Save" ].join("&") - expected = { "customers" => { "boston" => { @@ -70,13 +76,12 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest "second" => "Pc" } } - assert_parses expected, query end test "parses params with array" do - query = "selected[]=1&selected[]=2&selected[]=3" - expected = { "selected" => [ "1", "2", "3" ] } + query = "selected[]=1&selected[]=2&selected[]=3" + expected = { "selected" => ["1", "2", "3"] } assert_parses expected, query end @@ -88,13 +93,13 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest test "parses params with array prefix and hashes" do query = "a[][b][c]=d" - expected = {"a" => [{"b" => {"c" => "d"}}]} + expected = { "a" => [{ "b" => { "c" => "d" } }] } assert_parses expected, query end test "parses params with complex nesting" do query = "a[][b][c][][d][]=e" - expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]} + expected = { "a" => [{ "b" => { "c" => [{ "d" => ["e"] }] } }] } assert_parses expected, query end @@ -104,7 +109,6 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest "something_else=blah", "logo=#{File.expand_path(__FILE__)}" ].join("&") - expected = { "customers" => { "boston" => { @@ -116,22 +120,20 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest "something_else" => "blah", "logo" => File.expand_path(__FILE__), } - assert_parses expected, query end test "parses params with Safari 2 trailing null character" do - query = "selected[]=1&selected[]=2&selected[]=3\0" - expected = { "selected" => [ "1", "2", "3" ] } + query = "selected[]=1&selected[]=2&selected[]=3\0" + expected = { "selected" => ["1", "2", "3"] } assert_parses expected, query end test "ambiguous params returns a bad request" do with_routing do |set| set.draw do - post ':action', :to => ::UrlEncodedParamsParsingTest::TestController + post ':action', to: ::UrlEncodedParamsParsingTest::TestController end - post "/parse", "foo[]=bar&foo[4]=bar" assert_response :bad_request end @@ -141,7 +143,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest def with_test_routing with_routing do |set| set.draw do - post ':action', :to => ::UrlEncodedParamsParsingTest::TestController + post ':action', to: ::UrlEncodedParamsParsingTest::TestController end yield end @@ -151,8 +153,8 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest with_test_routing do post "/parse", actual assert_response :ok - assert_equal(expected, TestController.last_request_parameters) - assert_utf8(TestController.last_request_parameters) + assert_equal expected, TestController.last_request_parameters + assert_utf8 TestController.last_request_parameters end end @@ -167,11 +169,11 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest object.each_value do |v| case v when Hash - assert_utf8(v) + assert_utf8 v when Array - v.each {|el| assert_utf8(el) } + v.each { |el| assert_utf8 el } else - assert_utf8(v) + assert_utf8 v end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 8e4339aa0c..e4c3ddd3f9 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3620,3 +3620,56 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true) end end + +class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + rack_app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + mount rack_app, at: '/account', as: 'account' + mount rack_app, at: '/:locale/account', as: 'localized_account' + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_mounted_application_doesnt_match_unnamed_route + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true) + end + + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true) + end + end +end + +class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + get '/account', to: redirect('/myaccount'), as: 'account' + get '/:locale/account', to: redirect('/%{locale}/myaccount'), as: 'localized_account' + end + end + + def app + Routes + end + + include Routes.url_helpers + + def test_redirect_doesnt_match_unnamed_route + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true) + end + + assert_raise(ActionController::UrlGenerationError) do + assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true) + end + end +end diff --git a/actionpack/test/fixtures/helpers_missing/invalid_require_helper.rb b/actionpack/test/fixtures/helpers_missing/invalid_require_helper.rb new file mode 100644 index 0000000000..d8801e54d5 --- /dev/null +++ b/actionpack/test/fixtures/helpers_missing/invalid_require_helper.rb @@ -0,0 +1,5 @@ +require 'very_invalid_file_name' + +module InvalidRequireHelper +end + diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 3d52b2e9ee..a286f77633 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -4,9 +4,13 @@ require 'abstract_unit' module ActionDispatch module Journey class TestRouter < ActiveSupport::TestCase + # TODO : clean up routing tests so we don't need this hack + class StubDispatcher < Routing::RouteSet::Dispatcher; end + attr_reader :routes def setup + @app = StubDispatcher.new @routes = Routes.new @router = Router.new(@routes, {}) @formatter = Formatter.new(@routes) @@ -306,7 +310,7 @@ module ActionDispatch def test_nil_path_parts_are_ignored path = Path::Pattern.new "/:controller(/:action(.:format))" - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } @@ -321,7 +325,7 @@ module ActionDispatch str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true) path = Path::Pattern.new str - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} path, _ = @formatter.generate(:path_info, nil, Hash[params], {}) assert_equal '/', path @@ -329,7 +333,7 @@ module ActionDispatch def test_generate_calls_param_proc path = Path::Pattern.new '/:controller(/:action)' - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} parameterized = [] params = [ [:controller, "tasks"], @@ -347,7 +351,7 @@ module ActionDispatch def test_generate_id path = Path::Pattern.new '/:controller(/:action)' - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} path, params = @formatter.generate( :path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) @@ -357,7 +361,7 @@ module ActionDispatch def test_generate_escapes path = Path::Pattern.new '/:controller(/:action)' - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} path, _ = @formatter.generate(:path_info, nil, { :controller => "tasks", @@ -368,7 +372,7 @@ module ActionDispatch def test_generate_extra_params path = Path::Pattern.new '/:controller(/:action)' - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} path, params = @formatter.generate(:path_info, nil, { :id => 1, @@ -382,7 +386,7 @@ module ActionDispatch def test_generate_uses_recall_if_needed path = Path::Pattern.new '/:controller(/:action(/:id))' - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} path, params = @formatter.generate(:path_info, nil, @@ -394,7 +398,7 @@ module ActionDispatch def test_generate_with_name path = Path::Pattern.new '/:controller(/:action)' - @router.routes.add_route nil, path, {}, {}, {} + @router.routes.add_route @app, path, {}, {}, {} path, params = @formatter.generate(:path_info, "tasks", @@ -541,7 +545,7 @@ module ActionDispatch def add_routes router, paths paths.each do |path| path = Path::Pattern.new path - router.routes.add_route nil, path, {}, {}, {} + router.routes.add_route @app, path, {}, {}, {} end end diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb index a5588d95fa..0028aaa629 100644 --- a/actionpack/test/routing/helper_test.rb +++ b/actionpack/test/routing/helper_test.rb @@ -22,7 +22,7 @@ module ActionDispatch x = Class.new { include rs.url_helpers } - assert_raises ActionController::RoutingError do + assert_raises ActionController::UrlGenerationError do x.new.pond_duck_path Duck.new end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 921ea2be6f..60ddace7e4 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,48 @@ +* Fix default rendered format problem when calling `render` without :content_type option. + It should return :html. Fix #11393. + + *Gleb Mazovetskiy* *Oleg* *kennyj* + +* Fix `link_to` with block and url hashes. + + Before: + + link_to(action: 'bar', controller: 'foo') { content_tag(:span, 'Example site') } + # => "<a action=\"bar\" controller=\"foo\"><span>Example site</span></a>" + + After: + + link_to(action: 'bar', controller: 'foo') { content_tag(:span, 'Example site') } + # => "<a href=\"/foo/bar\"><span>Example site</span></a>" + + *Murahashi Sanemat Kenichi* + +* Fix "Stack Level Too Deep" error when redering recursive partials. + + Fixes #11340. + + *Rafael Mendonça França* + +* Added an `enforce_utf8` hash option for `form_tag` method. + + Control to output a hidden input tag with name `utf8` without monkey + patching. + + Before: + + form_tag + # => '<form>..<input name="utf8" type="hidden" value="✓" />..</form>' + + After: + + form_tag + # => '<form>..<input name="utf8" type="hidden" value="✓" />..</form>' + + form_tag({}, { :enforce_utf8 => false }) + # => '<form>....</form>' + + *ma2gedev* + * Remove the deprecated `include_seconds` argument from `distance_of_time_in_words`, pass in an `:include_seconds` hash option to use this feature. diff --git a/actionview/RUNNING_UNIT_TESTS b/actionview/RUNNING_UNIT_TESTS.rdoc index a0c35e1810..a0c35e1810 100644 --- a/actionview/RUNNING_UNIT_TESTS +++ b/actionview/RUNNING_UNIT_TESTS.rdoc diff --git a/actionview/Rakefile b/actionview/Rakefile index 1cbd35cda4..25365d3c3d 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rake/packagetask' require 'rubygems/package_task' desc "Default Task" diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 9324a1ac50..a674e4d7ea 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -7,10 +7,14 @@ module ActionView @@cache = ThreadSafe::Cache.new def self.digest(name, format, finder, options = {}) - cache_key = [name, format] + Array.wrap(options[:dependencies]) - @@cache[cache_key.join('.')] ||= begin + cache_key = ([name, format] + Array.wrap(options[:dependencies])).join('.') + @@cache.fetch(cache_key) do + @@cache[cache_key] ||= nil if options[:partial] # Prevent re-entry + klass = options[:partial] || name.include?("/_") ? PartialDigestor : Digestor - klass.new(name, format, finder, options).digest + digest = klass.new(name, format, finder, options).digest + + @@cache[cache_key] = digest # Store the value end end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 3fa7696b83..142c27ace0 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -38,6 +38,7 @@ module ActionView # * A list of parameters to feed to the URL the form will be posted to. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the # submit behavior. By default this behavior is an ajax submit. + # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output. # # ==== Examples # form_tag('/posts') @@ -719,7 +720,8 @@ module ActionView method_tag(method) + token_tag(authenticity_token) end - tags = utf8_enforcer_tag << method_tag + enforce_utf8 = html_options.delete("enforce_utf8") { true } + tags = (enforce_utf8 ? utf8_enforcer_tag : ''.html_safe) << method_tag content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline') end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 147f9fd8ed..6fe250d3c1 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -254,7 +254,7 @@ module ActionView # # => "<p>Unblinkable.</p>" # # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false) - # # => "<p><blink>Blinkable!</span> It's true.</p>" + # # => "<p><blink>Blinkable!</blink> It's true.</p>" def simple_format(text, html_options = {}, options = {}) wrapper_tag = options.fetch(:wrapper_tag, :p) diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 19e5941971..a4f04b0b3b 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -16,7 +16,7 @@ module ActionView # provided here will only work in the context of a request # (link_to_unless_current, for instance), which must be provided # as a method called #request on the context. - + BUTTON_TAG_METHOD_VERBS = %w{patch put delete} extend ActiveSupport::Concern include TagHelper @@ -172,7 +172,7 @@ module ActionView # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" } # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a> def link_to(name = nil, options = nil, html_options = nil, &block) - html_options, options = options, name if block_given? + html_options, options, name = options, name, block if block_given? options ||= {} html_options = convert_options_to_data_attributes(options, html_options) @@ -289,7 +289,7 @@ module ActionView remote = html_options.delete('remote') method = html_options.delete('method').to_s - method_tag = %w{patch put delete}.include?(method) ? method_tag(method) : ''.html_safe + method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe form_method = method == 'get' ? 'get' : 'post' form_options = html_options.delete('form') || {} diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 4d5c5db80c..668831dff3 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -11,7 +11,7 @@ module ActionView prepend_formats(template.formats) unless context.rendered_format - context.rendered_format = template.formats.first || formats.last + context.rendered_format = template.formats.first || formats.first end render_template(template, options[:layout], options[:locals]) diff --git a/actionview/test/fixtures/digestor/level/_recursion.html.erb b/actionview/test/fixtures/digestor/level/_recursion.html.erb new file mode 100644 index 0000000000..ee5aaf09c3 --- /dev/null +++ b/actionview/test/fixtures/digestor/level/_recursion.html.erb @@ -0,0 +1 @@ +<%= render 'recursion' %> diff --git a/actionview/test/fixtures/digestor/level/recursion.html.erb b/actionview/test/fixtures/digestor/level/recursion.html.erb new file mode 100644 index 0000000000..ee5aaf09c3 --- /dev/null +++ b/actionview/test/fixtures/digestor/level/recursion.html.erb @@ -0,0 +1 @@ +<%= render 'recursion' %> diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 06735c30d3..67e3775f28 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -95,6 +95,31 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_recursion_in_renders + assert digest("level/recursion") # assert recursion is possible + assert_not_nil digest("level/recursion") # assert digest is stored + end + + def test_chaining_the_top_template_on_recursion + assert digest("level/recursion") # assert recursion is possible + + assert_digest_difference("level/recursion") do + change_template("level/recursion") + end + + assert_not_nil digest("level/recursion") # assert digest is stored + end + + def test_chaining_the_partial_template_on_recursion + assert digest("level/recursion") # assert recursion is possible + + assert_digest_difference("level/recursion") do + change_template("level/_recursion") + end + + assert_not_nil digest("level/recursion") # assert digest is stored + end + def test_dont_generate_a_digest_for_missing_templates assert_equal '', digest("nothing/there") end diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 70fc6a588b..22bf438a56 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -12,9 +12,10 @@ class FormTagHelperTest < ActionView::TestCase def hidden_fields(options = {}) method = options[:method] + enforce_utf8 = options.fetch(:enforce_utf8, true) txt = %{<div style="margin:0;padding:0;display:inline">} - txt << %{<input name="utf8" type="hidden" value="✓" />} + txt << %{<input name="utf8" type="hidden" value="✓" />} if enforce_utf8 if method && !%w(get post).include?(method.to_s) txt << %{<input name="_method" type="hidden" value="#{method}" />} end @@ -110,6 +111,20 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_form_tag_enforce_utf8_true + actual = form_tag({}, { :enforce_utf8 => true }) + expected = whole_form("http://www.example.com", :enforce_utf8 => true) + assert_dom_equal expected, actual + assert actual.html_safe? + end + + def test_form_tag_enforce_utf8_false + actual = form_tag({}, { :enforce_utf8 => false }) + expected = whole_form("http://www.example.com", :enforce_utf8 => false) + assert_dom_equal expected, actual + assert actual.html_safe? + end + def test_form_tag_with_block_in_erb output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>") diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 8cffe73cce..928dfb092d 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -41,6 +41,11 @@ module RenderTestCases assert_match "<error>No Comment</error>", @view.render(:template => "comments/empty", :formats => [:xml]) end + def test_rendered_format_without_format + @view.render(:inline => "test") + assert_equal :html, @view.lookup_context.rendered_format + end + def test_render_partial_implicitly_use_format_of_the_rendered_template @view.lookup_context.formats = [:json] assert_equal "Hello world", @view.render(:template => "test/one", :formats => [:html]) diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 8373d7f992..851ea8796f 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -308,6 +308,13 @@ class UrlHelperTest < ActiveSupport::TestCase link_to('/', class: "special") { content_tag(:span, 'Example site') } end + def test_link_tag_using_block_and_hash + assert_dom_equal( + %{<a href="/"><span>Example site</span></a>}, + link_to(url_hash) { content_tag(:span, 'Example site') } + ) + end + def test_link_tag_using_block_in_erb out = render_erb %{<%= link_to('/') do %>Example site<% end %>} assert_equal '<a href="/">Example site</a>', out @@ -336,8 +343,6 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal %{<a href="/">Listing</a>}, link_to_unless(false, "Listing", url_hash) - assert_equal "Showing", link_to_unless(true, "Showing", url_hash) - assert_equal "<strong>Showing</strong>", link_to_unless(true, "Showing", url_hash) { |name| "<strong>#{name}</strong>".html_safe @@ -357,7 +362,6 @@ class UrlHelperTest < ActiveSupport::TestCase def test_link_to_if assert_equal "Showing", link_to_if(false, "Showing", url_hash) assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) - assert_equal "Showing", link_to_if(false, "Showing", url_hash) end def request_for_url(url, opts = {}) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 7156f1bb30..3d6de33e1e 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -18,9 +18,9 @@ module ActiveModel # value to the password_confirmation attribute and the validation # will not be triggered. # - # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password: + # You need to add bcrypt-ruby (~> 3.1.0) to Gemfile to use #has_secure_password: # - # gem 'bcrypt-ruby', '~> 3.0.0' + # gem 'bcrypt-ruby', '~> 3.1.0' # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # @@ -44,7 +44,7 @@ module ActiveModel # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. begin - gem 'bcrypt-ruby', '~> 3.0.0' + gem 'bcrypt-ruby', '~> 3.1.0' require 'bcrypt' rescue LoadError $stderr.puts "You don't have bcrypt-ruby installed in your application. Please add it to your Gemfile and run bundle install" diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 2803f69b6f..2864c2ba11 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -205,7 +205,7 @@ module ActiveModel Serializer.new(self, options).serialize(&block) end - # Sets the model +attributes+ from a JSON string. Returns +self+. + # Sets the model +attributes+ from an XML string. Returns +self+. # # class Person # include ActiveModel::Serializers::Xml diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f59f79112e..8e262b5fd7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,37 @@ +* `change_column` for PostgreSQL adapter respects the `:array` option. + + *Yves Senn* + +* Remove deprecation warning from `attribute_missing` for attributes that are columns. + + *Arun Agrawal* + +* Remove extra decrement of transaction deep level. + + Fixes: #4566 + + *Paul Nikitochkin* + +* Reset @column_defaults when assigning `locking_column`. + We had a potential problem. For example: + + class Post < ActiveRecord::Base + self.column_defaults # if we call this unintentionally before setting locking_column ... + self.locking_column = 'my_locking_column' + end + + Post.column_defaults["my_locking_column"] + => nil # expected value is 0 ! + + *kennyj* + +* Remove extra select and update queries on save/touch/destroy ActiveRecord model + with belongs to reflection with option `touch: true`. + + Fixes: #11288 + + *Paul Nikitochkin* + * Remove deprecated nil-passing to the following `SchemaCache` methods: `primary_keys`, `tables`, `columns` and `columns_hash`. diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 35f13d2438..d3546ce948 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -43,6 +43,8 @@ class Exhibit < ActiveRecord::Base def self.feel(exhibits) exhibits.each { |e| e.feel } end end +def progress_bar(int); print "." if (int%100).zero? ; end + puts 'Generating data...' module ActiveRecord @@ -75,7 +77,7 @@ notes = ActiveRecord::Faker::LOREM.join ' ' today = Date.today puts "Inserting #{RECORDS} users and exhibits..." -RECORDS.times do +RECORDS.times do |record| user = User.create( created_at: today, name: ActiveRecord::Faker.name, @@ -88,7 +90,9 @@ RECORDS.times do user: user, notes: notes ) + progress_bar(record) end +puts "Done!\n" Benchmark.ips(TIME) do |x| ar_obj = Exhibit.find(1) @@ -117,10 +121,18 @@ Benchmark.ips(TIME) do |x| Exhibit.first.look end + x.report 'Model.take' do + Exhibit.take + end + x.report("Model.all limit(100)") do Exhibit.look Exhibit.limit(100) end + x.report("Model.all take(100)") do + Exhibit.look Exhibit.take(100) + end + x.report "Model.all limit(100) with relationship" do Exhibit.feel Exhibit.limit(100).includes(:user) end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index f1bec5787a..bdfafa5066 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -119,7 +119,7 @@ module ActiveRecord # the owner klass.table_name else - reflection.table_name + super end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index d4e1a0dda1..81293e464d 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -92,7 +92,7 @@ module ActiveRecord::Associations::Builder end def self.touch_record(o, foreign_key, name, touch) # :nodoc: - old_foreign_id = o.attribute_was(foreign_key) + old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id klass = o.association(name).klass diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 607ed0da46..6e722616bf 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -15,7 +15,7 @@ module ActiveRecord when :restrict_with_error unless empty? record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + owner.errors.add(:base, :"restrict_dependent_destroy.has_many", record: record) false end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 3ab1ea1ff4..5791602846 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,7 +12,7 @@ module ActiveRecord when :restrict_with_error if load_target record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + owner.errors.add(:base, :"restrict_dependent_destroy.has_one", record: record) false end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index e0715d9dd1..8c528af399 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -25,11 +25,8 @@ module ActiveRecord attr_reader :tables delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection - delegate :table, :table_name, :to => :parent, :prefix => :parent delegate :alias_tracker, :to => :join_dependency - alias :alias_suffix :parent_table_name - def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! @@ -47,6 +44,9 @@ module ActiveRecord @tables = construct_tables.reverse end + def parent_table_name; parent.table_name; end + alias :alias_suffix :parent_table_name + def ==(other) other.class == self.class && other.reflection == reflection && @@ -64,15 +64,20 @@ module ActiveRecord end end - def join_to(manager) + def join_constraints + joins = [] tables = @tables.dup - foreign_table = parent_table + + foreign_table = parent.table foreign_klass = parent.base_klass + scope_chain_iter = reflection.scope_chain.reverse_each + # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - chain.reverse.each_with_index do |reflection, i| + chain.reverse_each do |reflection| table = tables.shift + klass = reflection.klass case reflection.source_macro when :belongs_to @@ -80,11 +85,10 @@ module ActiveRecord foreign_key = reflection.foreign_key when :has_and_belongs_to_many # Join the join table first... - manager.from(join( + joins << join( table, table[reflection.foreign_key]. - eq(foreign_table[reflection.active_record_primary_key]) - )) + eq(foreign_table[reflection.active_record_primary_key])) foreign_table, table = table, tables.shift @@ -95,38 +99,39 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) + constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - scope_chain_items = scope_chain[i] + scope_chain_items = scope_chain_iter.next.map do |item| + if item.is_a?(Relation) + item + else + ActiveRecord::Relation.new(klass, table).instance_exec(self, &item) + end + end if reflection.type - scope_chain_items += [ - ActiveRecord::Relation.new(reflection.klass, table) + scope_chain_items << + ActiveRecord::Relation.new(klass, table) .where(reflection.type => foreign_klass.base_class.name) - ] end - scope_chain_items += [reflection.klass.send(:build_default_scope)].compact + scope_chain_items.concat [klass.send(:build_default_scope)].compact - constraint = scope_chain_items.inject(constraint) do |chain, item| - unless item.is_a?(Relation) - item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item) - end + rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| + left.merge right + end - if item.arel.constraints.empty? - chain - else - chain.and(item.arel.constraints) - end + if rel && !rel.arel.constraints.empty? + constraint = constraint.and rel.arel.constraints end - manager.from(join(table, constraint)) + joins << join(table, constraint) # The current table in this iteration becomes the foreign table in the next - foreign_table, foreign_klass = table, reflection.klass + foreign_table, foreign_klass = table, klass end - manager + joins end # Builds equality condition. @@ -144,13 +149,13 @@ module ActiveRecord # foreign_table #=> #<Arel::Table @name="physicians" ...> # foreign_key #=> id # - def build_constraint(reflection, table, key, foreign_table, foreign_key) + def build_constraint(klass, table, key, foreign_table, foreign_key) constraint = table[key].eq(foreign_table[foreign_key]) - if reflection.klass.finder_needs_type_condition? + if klass.finder_needs_type_condition? constraint = table.create_and([ constraint, - reflection.klass.send(:type_condition, table) + klass.send(:type_condition, table) ]) end @@ -169,11 +174,6 @@ module ActiveRecord def aliased_table_name table.table_alias || table.name end - - def scope_chain - @scope_chain ||= reflection.scope_chain.reverse - end - end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index c07fd67216..208da2cb77 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -176,20 +176,6 @@ module ActiveRecord end end - def attribute_missing(match, *args, &block) # :nodoc: - if self.class.columns_hash[match.attr_name] - ActiveSupport::Deprecation.warn( - "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \ - "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \ - "is a column of the table. If this error has happened through normal usage of Active " \ - "Record (rather than through your own code or external libraries), please report it as " \ - "a bug." - ) - end - - super - end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> # which will all return +true+. It also define the attribute methods if they have @@ -273,7 +259,7 @@ module ActiveRecord # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # # person.attribute_for_inspect(:name) - # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\"" + # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\"" # # person.attribute_for_inspect(:created_at) # # => "\"2012-10-22 00:15:07\"" @@ -281,7 +267,7 @@ module ActiveRecord value = read_attribute(attr_name) if value.is_a?(String) && value.length > 50 - "#{value[0..50]}...".inspect + "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") else diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a8a1847554..c991c870ed 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -343,6 +343,7 @@ module ActiveRecord end records.each do |record| + next if record.destroyed? saved = true diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 41e07fbda9..e6b3c8ec9f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -20,6 +20,12 @@ module ActiveRecord attr_reader :query_cache, :query_cache_enabled + def initialize(*) + super + @query_cache = Hash.new { |h,sql| h[sql] = {} } + @query_cache_enabled = false + end + # Enable the query cache within the block. def cache old, @query_cache_enabled = @query_cache_enabled, true diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cfff7202a3..b82d0fb872 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -95,8 +95,6 @@ module ActiveRecord @last_use = false @logger = logger @pool = pool - @query_cache = Hash.new { |h,sql| h[sql] = {} } - @query_cache_enabled = false @schema_cache = SchemaCache.new self @visitor = nil end @@ -307,8 +305,8 @@ module ActiveRecord # QUOTING ================================================== - # Returns a bind substitution value given a +column+ and list of current - # +binds+. + # Returns a bind substitution value given a bind +index+ and +column+ + # NOTE: The column param is currently being used by the sqlserver-adapter def substitute_at(column, index) Arel::Nodes::BindParam.new '?' end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index a651b6c32e..3fce8de1ba 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -384,8 +384,9 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) clear_cache! quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) + sql_type << "[]" if options[:array] + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index b1fbd38622..3a81e15532 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -15,8 +15,8 @@ en: messages: record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: - one: "Cannot delete record because a dependent %{record} exists" - many: "Cannot delete record because dependent %{record} exist" + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 209de78898..2a7996c4e7 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -138,6 +138,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) + @column_defaults = nil @locking_column = value.to_s end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index ac2d2f2712..44ea8610f2 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -253,19 +253,6 @@ module ActiveRecord @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end - # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key - # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute - # is available. - def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods| - attr_name = attr.to_s - methods[attr.to_sym] = attr_name - methods["#{attr}=".to_sym] = attr_name - methods["#{attr}?".to_sym] = attr_name - methods["#{attr}_before_type_cast".to_sym] = attr_name - end - end - # Resets all the cached information about columns, which will cause them # to be reloaded on the next request. # diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8a9488656b..f470946da5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -527,7 +527,9 @@ module ActiveRecord # def chain @chain ||= begin - chain = source_reflection.chain + through_reflection.chain + a = source_reflection.chain + b = through_reflection.chain + chain = a + b chain[0] = self # Use self so we don't lose the information from :source_type chain end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 612f376c55..4e86e905ed 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -502,12 +502,19 @@ module ActiveRecord # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= begin + relation = self + connection = klass.connection + visitor = connection.visitor + if eager_loading? join_dependency = construct_join_dependency - relation = construct_relation_for_association_find(join_dependency) - klass.connection.to_sql(relation.arel, relation.bind_values) - else - klass.connection.to_sql(arel, bind_values.dup) + relation = construct_relation_for_association_find(join_dependency) + end + + ast = relation.arel.ast + binds = relation.bind_values.dup + visitor.accept(ast) do + connection.quote(*binds.shift.reverse) end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 3b24ed6754..52a538e5b5 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -189,11 +189,7 @@ module ActiveRecord distinct = self.distinct_value if operation == "count" - if select_values.present? - column_name ||= select_values.join(", ") - else - column_name ||= :all - end + column_name ||= select_for_count unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true @@ -361,6 +357,15 @@ module ActiveRecord column ? column.type_cast(value) : value end + # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). + def select_for_count + if select_values.present? + select_values.join(", ") + else + :all + end + end + def build_count_subquery(relation, column_name, distinct) column_alias = Arel.sql('count_column') subquery_alias = Arel.sql('subquery_for_count') diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index bad5886cde..2d3bd563ac 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,5 +1,7 @@ module ActiveRecord module FinderMethods + ONE_AS_ONE = '1 AS one' + # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. @@ -202,7 +204,7 @@ module ActiveRecord relation = construct_relation_for_association_find(construct_join_dependency) return false if ActiveRecord::NullRelation === relation - relation = relation.except(:select, :order).select("1 AS one").limit(1) + relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) case conditions when Array, Hash @@ -236,7 +238,7 @@ module ActiveRecord raise RecordNotFound, error end - protected + private def find_with_associations join_dependency = construct_join_dependency @@ -244,7 +246,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation, 'SQL', relation.bind_values.dup) + rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) join_dependency.instantiate(rows) end end @@ -288,6 +290,12 @@ module ActiveRecord id_rows.map {|row| row[primary_key]} end + def using_limitable_reflections?(reflections) + reflections.none? { |r| r.collection? } + end + + protected + def find_with_ids(*ids) expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? @@ -377,9 +385,5 @@ module ActiveRecord end end end - - def using_limitable_reflections?(reflections) - reflections.none? { |r| r.collection? } - end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 6d047e0331..da13152e01 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -97,19 +97,35 @@ module ActiveRecord def merge_multi_values lhs_wheres = relation.where_values rhs_wheres = values[:where] || [] + lhs_binds = relation.bind_values rhs_binds = values[:bind] || [] removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) - relation.where_values = kept + rhs_wheres - relation.bind_values = filter_binds(lhs_binds, removed) + rhs_binds + where_values = kept + rhs_wheres + bind_values = filter_binds(lhs_binds, removed) + rhs_binds + + conn = relation.klass.connection + bviter = bind_values.each.with_index + where_values.map! do |node| + if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right + (column, _), i = bviter.next + substitute = conn.substitute_at column, i + Arel::Nodes::Equality.new(node.left, substitute) + else + node + end + end + + relation.where_values = where_values + relation.bind_values = bind_values if values[:reordering] # override any order specified in the original relation relation.reorder! values[:order] elsif values[:order] - # merge in order_values from r + # merge in order_values from relation relation.order! values[:order] end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1327cc3c34..d0c56ac3d0 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -100,6 +100,14 @@ module ActiveRecord # firing an additional query. This will often result in a # performance improvement over a simple +join+. # + # You can also specify multiple relationships, like this: + # + # users = User.includes(:address, :friends) + # + # Loading nested relationships is possible using a Hash: + # + # users = User.includes(:address, friends: [:address, :followers]) + # # === conditions # # If you want to add conditions to your included models you'll have @@ -280,16 +288,18 @@ module ActiveRecord args.flatten! validate_order_args args - references = args.reject { |arg| Arel::Node === arg } + references = args.grep(String) references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? # if a symbol is given we prepend the quoted table name - args = args.map { |arg| - arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg + args.map! { |arg| + arg.is_a?(Symbol) ? + Arel::Nodes::Ascending.new(klass.arel_table[arg]) : + arg } - self.order_values = args + self.order_values + self.order_values = args.concat self.order_values self end @@ -946,10 +956,11 @@ module ActiveRecord join_dependency.graft(*stashed_association_joins) - # FIXME: refactor this to build an AST - join_dependency.join_associations.each do |association| - association.join_to(manager) - end + joins = join_dependency.join_associations.map { |association| + association.join_constraints + }.flatten + + joins.each { |join| manager.from join } manager.join_sources.concat join_list diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 62920d936f..dcbf38a89f 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -304,6 +304,7 @@ module ActiveRecord run_callbacks :rollback ensure restore_transaction_record_state(force_restore_state) + clear_transaction_record_state end # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks @@ -360,8 +361,8 @@ module ActiveRecord # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: unless @_start_transaction_state.empty? - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 || force + transaction_level = (@_start_transaction_state[:level] || 0) - 1 + if transaction_level < 1 || force restore_state = @_start_transaction_state was_frozen = restore_state[:frozen?] @attributes = @attributes.dup if @attributes.frozen? @@ -374,7 +375,6 @@ module ActiveRecord @attributes_cache.delete(self.class.primary_key) end @attributes.freeze if was_frozen - @_start_transaction_state.clear end end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 61a3a2ba0f..e5e877f0b0 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -27,6 +27,18 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert @column.array end + def test_change_column_with_array + @connection.add_column :pg_arrays, :snippets, :string, array: true, default: [] + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}" + + PgArray.reset_column_information + column = PgArray.columns.find { |c| c.name == 'snippets' } + + assert_equal :text, column.type + assert_equal [], column.default + assert column.array + end + def test_type_cast_array assert @column diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index cc72ab7b2a..0267cdf6e0 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,4 +1,4 @@ -require "cases/helper" +require 'cases/helper' require 'models/developer' require 'models/project' require 'models/company' @@ -14,6 +14,8 @@ require 'models/sponsor' require 'models/member' require 'models/essay' require 'models/toy' +require 'models/invoice' +require 'models/line_item' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -324,6 +326,45 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic.id)[:replies_count] end + def test_belongs_to_with_touch_option_on_touch + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(1) { line_item.touch } + end + + def test_belongs_to_with_touch_option_on_touch_and_removed_parent + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + line_item.invoice = nil + + assert_queries(2) { line_item.touch } + end + + def test_belongs_to_with_touch_option_on_update + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(2) { line_item.update amount: 10 } + end + + def test_belongs_to_with_touch_option_on_destroy + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(2) { line_item.destroy } + end + + def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + line_item.invoice = Invoice.create! + + assert_queries(3) { line_item.touch } + end + def test_belongs_to_counter_after_update topic = Topic.create!(title: "37s") topic.replies.create!(title: "re: 37s", content: "rails") diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 0f9af8a0c3..c63f48e370 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -437,13 +437,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert george.treasures(true).empty? end - def test_deprecated_push_with_attributes_was_removed - jamis = developers(:jamis) - assert_raise(NoMethodError) do - jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today) - end - end - def test_associations_with_conditions assert_equal 3, projects(:active_record).developers.size assert_equal 1, projects(:active_record).developers_named_david.size diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index ee88fd490a..119e94b831 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -5,6 +5,7 @@ require 'models/reference' require 'models/job' require 'models/reader' require 'models/comment' +require 'models/rating' require 'models/tag' require 'models/tagging' require 'models/author' @@ -616,6 +617,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal post.author.author_favorites, post.author_favorites end + def test_merge_join_association_with_has_many_through_association_proxy + author = authors(:mary) + assert_nothing_raised { author.comments.ratings.to_sql } + end + def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys assert_equal 2, owners(:blackbeard).toys.count end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ee0150558d..baba55ea43 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -32,7 +32,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) - assert_equal '"The First Topic Now Has A Title With\nNewlines And M..."', t.attribute_for_inspect(:title) + assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title) end def test_attribute_present @@ -759,21 +759,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert subklass.method_defined?(:id), "subklass is missing id method" end - def test_dispatching_column_attributes_through_method_missing_deprecated - Topic.define_attribute_methods - - topic = Topic.new(:id => 5) - topic.id = 5 - - topic.method(:id).owner.send(:undef_method, :id) - - assert_deprecated do - assert_equal 5, topic.id - end - ensure - Topic.undefine_attribute_methods - end - def test_read_attribute_with_nil_should_not_asplode assert_equal nil, Topic.new.read_attribute(nil) end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 580aa96ecd..635278abc1 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -705,6 +705,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase ids.each { |id| assert_nil klass.find_by_id(id) } end + def test_should_not_resave_destroyed_association + @pirate.birds.create!(name: :parrot) + @pirate.birds.first.destroy + @pirate.save! + assert @pirate.reload.birds.empty? + end + def test_should_skip_validation_on_has_many_if_marked_for_destruction 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 395f28f280..3a1830a739 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/concurrency/latch' require 'models/post' require 'models/author' require 'models/topic' @@ -1458,21 +1459,20 @@ class BasicsTest < ActiveRecord::TestCase orig_handler = klass.connection_handler new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new after_handler = nil - is_set = false + latch1 = ActiveSupport::Concurrency::Latch.new + latch2 = ActiveSupport::Concurrency::Latch.new t = Thread.new do klass.connection_handler = new_handler - is_set = true - Thread.stop + latch1.release + latch2.await after_handler = klass.connection_handler end - while(!is_set) - Thread.pass - end + latch1.await klass.connection_handler = orig_handler - t.wakeup + latch2.release t.join assert_equal after_handler, new_handler diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index d5365b695e..2da51ea015 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -118,6 +118,7 @@ module ActiveRecord connection = cs.first @pool.remove connection assert_respond_to t.join.value, :execute + connection.close end def test_reap_and_active diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 827fcf3d50..db7d3b80ab 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -17,6 +17,7 @@ class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.table_name = :lock_without_defaults_cust + self.column_defaults # to test @column_defaults caching. self.locking_column = :custom_lock_version end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index ec2926632c..aa606ac8bb 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -16,32 +16,23 @@ module ActiveRecord end def test_add_remove_single_field_using_string_arguments - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name add_column 'test_models', 'last_name', :string - - TestModel.reset_column_information - - assert TestModel.column_methods_hash.key?(:last_name) + assert_column TestModel, :last_name remove_column 'test_models', 'last_name' - - TestModel.reset_column_information - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name end def test_add_remove_single_field_using_symbol_arguments - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name add_column :test_models, :last_name, :string - - TestModel.reset_column_information - assert TestModel.column_methods_hash.key?(:last_name) + assert_column TestModel, :last_name remove_column :test_models, :last_name - - TestModel.reset_column_information - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name end def test_unabstracted_database_dependent_types @@ -168,26 +159,6 @@ module ActiveRecord assert_equal Date, bob.favorite_day.class end - # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column - # therefore no timezone change is done afterwards when default timezone is changed - unless current_adapter?(:OracleAdapter) - # Test DateTime column and defaults, including timezone. - # FIXME: moment of truth may be Time on 64-bit platforms. - if bob.moment_of_truth.is_a?(DateTime) - - with_env_tz 'US/Eastern' do - bob.reload - assert_equal DateTime.local_offset, bob.moment_of_truth.offset - assert_not_equal 0, bob.moment_of_truth.offset - assert_not_equal "Z", bob.moment_of_truth.zone - # US/Eastern is -5 hours from GMT - assert_equal Rational(-5, 24), bob.moment_of_truth.offset - assert_match(/\A-05:00\Z/, bob.moment_of_truth.zone) - assert_equal DateTime::ITALY, bob.moment_of_truth.start - end - end - end - assert_instance_of TrueClass, bob.male? assert_kind_of BigDecimal, bob.wealth end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index e99312c245..ed080b2995 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -177,20 +177,18 @@ class MigrationTest < ActiveRecord::TestCase end def test_filtering_migrations - assert !Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name assert !Reminder.table_exists? name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) + assert_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) - Person.reset_column_information - assert !Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end @@ -237,7 +235,7 @@ class MigrationTest < ActiveRecord::TestCase skip "not supported on #{ActiveRecord::Base.connection.class}" end - assert_not Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name migration = Class.new(ActiveRecord::Migration) { def version; 100 end @@ -253,8 +251,7 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message - Person.reset_column_information - assert_not Person.column_methods_hash.include?(:last_name), + assert_no_column Person, :last_name, "On error, the Migrator should revert schema changes but it did not." end @@ -263,7 +260,7 @@ class MigrationTest < ActiveRecord::TestCase skip "not supported on #{ActiveRecord::Base.connection.class}" end - assert_not Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name migration = Class.new(ActiveRecord::Migration) { def version; 100 end @@ -279,8 +276,7 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message - Person.reset_column_information - assert_not Person.column_methods_hash.include?(:last_name), + assert_no_column Person, :last_name, "On error, the Migrator should revert schema changes but it did not." end @@ -289,7 +285,7 @@ class MigrationTest < ActiveRecord::TestCase skip "not supported on #{ActiveRecord::Base.connection.class}" end - assert_not Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name migration = Class.new(ActiveRecord::Migration) { self.disable_ddl_transaction! @@ -305,12 +301,11 @@ class MigrationTest < ActiveRecord::TestCase e = assert_raise(StandardError) { migrator.migrate } assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name), + assert_column Person, :last_name, "without ddl transactions, the Migrator should not rollback on error but it did." ensure Person.reset_column_information - if Person.column_methods_hash.include?(:last_name) + if Person.column_names.include?('last_name') Person.connection.remove_column('people', 'last_name') end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index a8a9b06ec4..626c6aeaf8 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -16,22 +16,6 @@ class PooledConnectionsTest < ActiveRecord::TestCase @per_test_teardown.each {|td| td.call } end - def checkout_connections - ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3})) - @connections = [] - @timed_out = 0 - - 4.times do - Thread.new do - begin - @connections << ActiveRecord::Base.connection_pool.checkout - rescue ActiveRecord::ConnectionTimeoutError - @timed_out += 1 - end - end.join - end - end - # Will deadlock due to lack of Monitor timeouts in 1.9 def checkout_checkin_connections(pool_size, threads) ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 693b36f35c..7c90f54343 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -9,6 +9,9 @@ module ActiveRecord fixtures :posts, :comments, :authors class FakeKlass < Struct.new(:table_name, :name) + def self.connection + Post.connection + end end def test_construction @@ -201,8 +204,12 @@ module ActiveRecord class RelationMutationTest < ActiveSupport::TestCase class FakeKlass < Struct.new(:table_name, :name) - def quoted_table_name - %{"#{table_name}"} + def arel_table + Post.arel_table + end + + def connection + Post.connection end end @@ -224,7 +231,17 @@ module ActiveRecord test "#order! with symbol prepends the table name" do assert relation.order!(:name).equal?(relation) - assert_equal ['"posts".name ASC'], relation.order_values + node = relation.order_values.first + assert node.ascending? + assert_equal :name, node.expr.name + assert_equal "posts", node.expr.relation.name + end + + test "#order! on non-string does not attempt regexp match for references" do + obj = Object.new + obj.expects(:=~).never + assert relation.order!(obj) + assert_equal [obj], relation.order_values end test '#references!' do @@ -285,7 +302,7 @@ module ActiveRecord assert_equal({foo: 'bar'}, relation.create_with_value) end - test 'merge!' do + def test_merge! assert relation.merge!(where: :foo).equal?(relation) assert_equal [:foo], relation.where_values end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index b205472cf5..d0f8731b9a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1556,4 +1556,21 @@ class RelationTest < ActiveRecord::TestCase merged = left.merge(right) assert_equal binds, merged.bind_values end + + def test_merging_reorders_bind_params + post = Post.first + id_column = Post.columns_hash['id'] + title_column = Post.columns_hash['title'] + + bv = Post.connection.substitute_at id_column, 0 + + right = Post.where(id: bv) + right.bind_values += [[id_column, post.id]] + + left = Post.where(title: bv) + left.bind_values += [[title_column, post.title]] + + merged = left.merge(right) + assert_equal post, merged.first + end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 5c7751b445..4bcc97ec44 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -56,7 +56,10 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scoping_with_threads 2.times do - Thread.new { assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') }.join + Thread.new { + assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') + DeveloperOrderedBySalary.connection.close + }.join end end @@ -360,9 +363,11 @@ class DefaultScopingTest < ActiveRecord::TestCase threads << Thread.new do Thread.current[:long_default_scope] = true assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close end threads << Thread.new do assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close end threads.each(&:join) end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 70b970639a..c33e5cdc00 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -49,6 +49,18 @@ module ActiveRecord assert_queries(0, :ignore_none => true, &block) end + def assert_column(model, column_name, msg=nil) + assert has_column?(model, column_name), msg + end + + def assert_no_column(model, column_name, msg=nil) + assert_not has_column?(model, column_name), msg + end + + def has_column?(model, column_name) + model.reset_column_information + model.column_names.include?(column_name.to_s) + end end class SQLCounter diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 6ba7464203..9c5f2e4724 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -117,6 +117,20 @@ class TransactionTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end + def test_raising_exception_in_nested_transaction_restore_state_in_save + topic = Topic.new + + def topic.after_save_for_transaction + raise 'Make the transaction rollback' + end + + assert_raises(RuntimeError) do + Topic.transaction { topic.save } + end + + assert topic.new_record?, "#{topic.inspect} should be new record" + end + def test_update_should_rollback_on_failure author = Author.find(1) posts_count = author.posts.size @@ -526,22 +540,20 @@ if current_adapter?(:PostgreSQLAdapter) # This will cause transactions to overlap and fail unless they are performed on # separate database connections. def test_transaction_per_thread - assert_nothing_raised do - threads = (1..3).map do - Thread.new do - Topic.transaction do - topic = Topic.find(1) - topic.approved = !topic.approved? - topic.save! - topic.approved = !topic.approved? - topic.save! - end - Topic.connection.close + threads = 3.times.map do + Thread.new do + Topic.transaction do + topic = Topic.find(1) + topic.approved = !topic.approved? + assert topic.save! + topic.approved = !topic.approved? + assert topic.save! end + Topic.connection.close end - - threads.each { |t| t.join } end + + threads.each { |t| t.join } end # Test for dirty reads among simultaneous transactions. diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index af80b1ba42..feb828de31 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -13,7 +13,11 @@ class Author < ActiveRecord::Base has_many :posts_with_extension, :class_name => "Post" has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post' has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post' - has_many :comments, :through => :posts + has_many :comments, through: :posts do + def ratings + Rating.joins(:comment).merge(self) + end + end has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments has_many :comments_with_order_and_conditions, -> { order('comments.body').where("comments.body like 'Thank%'") }, :through => :posts, :source => :comments has_many :comments_with_include, -> { includes(:post) }, :through => :posts, :source => :comments diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 8c07a034cc..c00cbaaa08 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,20 @@ +* Only raise `Module::DelegationError` if it's the source of the exception. + + Fixes #10559 + + *Andrew White* + +* Make `Time.at_with_coercion` retain the second fraction and return local time. + + Fixes #11350 + + *Neer Friedman*, *Andrew White* + +* Make `HashWithIndifferentAccess#select` always return the hash, even when + `Hash#select!` returns `nil`, to allow further chaining. + + *Marc Schütz* + * Remove deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). *Arun Agrawal* diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index c539048a85..308a1b2cd9 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -408,7 +408,7 @@ module ActiveSupport options = merged_options(options) instrument(:exist?, name) do entry = read_entry(namespaced_key(name, options), options) - entry && !entry.expired? + (entry && !entry.expired?) || false end end diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index dbddc7a7b4..37f007c751 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -31,9 +31,7 @@ class Array if block_given? collection.each_slice(number) { |slice| yield(slice) } else - groups = [] - collection.each_slice(number) { |group| groups << group } - groups + collection.each_slice(number).to_a end end @@ -86,13 +84,27 @@ class Array # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] def split(value = nil, &block) - inject([[]]) do |results, element| - if block && block.call(element) || value == element - results << [] - else - results.last << element - end + if block + inject([[]]) do |results, element| + if block.call(element) + results << [] + else + results.last << element + end + results + end + else + results, arr = [[]], self + until arr.empty? + if (idx = index(value)) + results.last.concat(arr.shift(idx)) + arr.shift + results << [] + else + results.last.concat(arr.shift(arr.size)) + end + end results end end diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb index c79745c5aa..8fbbe0d3e9 100644 --- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb @@ -1,3 +1,4 @@ +require 'date' require 'active_support/core_ext/object/acts_like' class DateTime diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 556b1b78f2..bca3800344 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -188,16 +188,17 @@ class Module exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") module_eval(<<-EOS, file, line - 2) - def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - _ = #{to} # _ = client - _.#{method}(#{definition}) # _.name(*args, &block) - rescue NoMethodError # rescue NoMethodError - if _.nil? # if _.nil? - #{exception} # # add helpful message to the exception - else # else - raise # raise - end # end - end # end + def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) + _ = #{to} # _ = client + _.#{method}(#{definition}) # _.name(*args, &block) + rescue NoMethodError => e # rescue NoMethodError => e + location = "%s:%d:in `%s'" % [__FILE__, __LINE__ - 2, '#{method_prefix}#{method}'] # location = "%s:%d:in `%s'" % [__FILE__, __LINE__ - 2, 'customer_name'] + if _.nil? && e.backtrace.first == location # if _.nil? && e.backtrace.first == location + #{exception} # # add helpful message to the exception + else # else + raise # raise + end # end + end # end EOS end end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 8fa8157d65..ee3b6d2b3f 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -59,7 +59,7 @@ class String # str.from(0).to(-1) #=> "hello" # str.from(1).to(-2) #=> "ell" def to(position) - self[0..position] + self[0, position + 1] end # Returns the first character. If a limit is supplied, returns a substring diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index c62bb41416..a4cacaf789 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -41,8 +41,8 @@ class String def truncate(truncate_at, options = {}) return dup unless length > truncate_at - options[:omission] ||= '...' - length_with_room_for_omission = truncate_at - options[:omission].length + omission = options[:omission] || '...' + length_with_room_for_omission = truncate_at - omission.length stop = \ if options[:separator] rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission @@ -50,6 +50,6 @@ class String length_with_room_for_omission end - "#{self[0...stop]}#{options[:omission]}" + "#{self[0, stop]}#{omission}" end end diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb index e3f20eee29..510c884c18 100644 --- a/activesupport/lib/active_support/core_ext/string/zones.rb +++ b/activesupport/lib/active_support/core_ext/string/zones.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/time/zones' class String diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index fa74fee78a..c739cce223 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -33,10 +33,15 @@ class Time # Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime # instances can be used when called with a single argument def at_with_coercion(*args) - if args.size == 1 && args.first.acts_like?(:time) - at_without_coercion(args.first.to_i) + return at_without_coercion(*args) if args.size != 1 + + # Time.at can be called with a time or numerical value + time_or_number = args.first + + if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime) + at_without_coercion(time_or_number.to_f).getlocal else - at_without_coercion(*args) + at_without_coercion(time_or_number) end end alias_method :at_without_coercion, :at diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 95e03ba95c..3da99872c0 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -228,7 +228,7 @@ module ActiveSupport def to_options!; self end def select(*args, &block) - dup.select!(*args, &block) + dup.tap {|hash| hash.select!(*args, &block)} end # Convert to a regular hash with string keys. diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 9ca5a388f8..d063fcb49c 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -72,7 +72,9 @@ module ActiveSupport else string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end - string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::') + string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } + string.gsub!('/', '::') + string end # Makes an underscored, lowercase form from the expression in the string. @@ -87,8 +89,7 @@ module ActiveSupport # # 'SSLError'.underscore.camelize # => "SslError" def underscore(camel_cased_word) - word = camel_cased_word.to_s.dup - word.gsub!('::', '/') + word = camel_cased_word.to_s.gsub('::', '/') word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') @@ -184,7 +185,7 @@ module ActiveSupport # # See also +demodulize+. def deconstantize(path) - path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename + path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename end # Creates a foreign key name from a class name. diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 414960d2b1..54cff6d394 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -244,14 +244,14 @@ module ActiveSupport # # ==== Examples # - # number_to_percentage(100) # => 100.000% - # number_to_percentage('98') # => 98.000% - # number_to_percentage(100, precision: 0) # => 100% - # number_to_percentage(1000, delimiter: '.', separator: ,') # => 1.000,000% - # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% - # number_to_percentage(1000, locale: :fr) # => 1 000,000% - # number_to_percentage('98a') # => 98a% - # number_to_percentage(100, format: '%n %') # => 100 % + # number_to_percentage(100) # => 100.000% + # number_to_percentage('98') # => 98.000% + # number_to_percentage(100, precision: 0) # => 100% + # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000% + # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% + # number_to_percentage(1000, locale: :fr) # => 1 000,000% + # number_to_percentage('98a') # => 98a% + # number_to_percentage(100, format: '%n %') # => 100 % def number_to_percentage(number, options = {}) return unless number options = options.symbolize_keys diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index ae6eaa4b60..d2382f0f5b 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -327,8 +327,8 @@ module CacheStoreBehavior def test_exist @cache.write('foo', 'bar') - assert @cache.exist?('foo') - assert !@cache.exist?('bar') + assert_equal true, @cache.exist?('foo') + assert_equal false, @cache.exist?('bar') end def test_nil_exist diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 8e2c298fc6..9b0a2ff7c9 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -68,12 +68,6 @@ class ConcernTest < ActiveSupport::TestCase assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] end - def test_instance_methods_are_included - @klass.send(:include, Baz) - assert_equal "baz", @klass.new.baz - assert @klass.included_modules.include?(ConcernTest::Baz) - end - def test_included_block_is_ran @klass.send(:include, Baz) assert_equal true, @klass.included_ran diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 0857796036..2d0c56bef5 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -487,6 +487,12 @@ class HashExtTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash end + def test_indifferent_select_returns_a_hash_when_unchanged + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| true} + + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + def test_indifferent_select_bang indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) indifferent_strings.select! {|k,v| v == 1} diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index ccf537e075..283b13ff8b 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -66,6 +66,23 @@ Tester = Struct.new(:client) do delegate :name, :to => :client, :prefix => false end +Product = Struct.new(:name) do + delegate :name, :to => :manufacturer, :prefix => true + delegate :name, :to => :type, :prefix => true + + def manufacturer + @manufacturer ||= begin + nil.unknown_method + end + end + + def type + @type ||= begin + nil.type_name + end + end +end + class ParameterSet delegate :[], :[]=, :to => :@params @@ -264,6 +281,16 @@ class ModuleTest < ActiveSupport::TestCase assert_equal [3], se.ints end + def test_delegation_doesnt_mask_nested_no_method_error_on_nil_receiver + product = Product.new('Widget') + + # Nested NoMethodError is a different name from the delegation + assert_raise(NoMethodError) { product.manufacturer_name } + + # Nested NoMethodError is the same name as the delegation + assert_raise(NoMethodError) { product.type_name } + end + def test_parent assert_equal Yz::Zy, Yz::Zy::Cd.parent assert_equal Yz, Yz::Zy.parent diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index d43cf41201..8741f033b5 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -700,6 +700,21 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_at_with_datetime_returns_local_time + with_env_tz 'US/Eastern' do + dt = DateTime.civil(2000, 1, 1, 0, 0, 0, '+0') + assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(dt) + assert_equal 'EST', Time.at(dt).zone + assert_equal(-18000, Time.at(dt).utc_offset) + + # Daylight savings + dt = DateTime.civil(2000, 7, 1, 1, 0, 0, '+1') + assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(dt) + assert_equal 'EDT', Time.at(dt).zone + assert_equal(-14400, Time.at(dt).utc_offset) + end + end + def test_at_with_time_with_zone assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC'])) @@ -711,6 +726,45 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_at_with_time_with_zone_returns_local_time + with_env_tz 'US/Eastern' do + twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['London']) + assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(twz) + assert_equal 'EST', Time.at(twz).zone + assert_equal(-18000, Time.at(twz).utc_offset) + + # Daylight savings + twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 7, 1, 0, 0, 0), ActiveSupport::TimeZone['London']) + assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(twz) + assert_equal 'EDT', Time.at(twz).zone + assert_equal(-14400, Time.at(twz).utc_offset) + end + end + + def test_at_with_time_microsecond_precision + assert_equal Time.at(Time.utc(2000, 1, 1, 0, 0, 0, 111)).to_f, Time.utc(2000, 1, 1, 0, 0, 0, 111).to_f + end + + def test_at_with_utc_time + with_env_tz 'US/Eastern' do + assert_equal Time.utc(2000), Time.at(Time.utc(2000)) + assert_equal 'UTC', Time.at(Time.utc(2000)).zone + assert_equal(0, Time.at(Time.utc(2000)).utc_offset) + end + end + + def test_at_with_local_time + with_env_tz 'US/Eastern' do + assert_equal Time.local(2000), Time.at(Time.local(2000)) + assert_equal 'EST', Time.at(Time.local(2000)).zone + assert_equal(-18000, Time.at(Time.local(2000)).utc_offset) + + assert_equal Time.local(2000, 7, 1), Time.at(Time.local(2000, 7, 1)) + assert_equal 'EDT', Time.at(Time.local(2000, 7, 1)).zone + assert_equal(-14400, Time.at(Time.local(2000, 7, 1)).utc_offset) + end + end + def test_eql? assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) ) assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index ddcfcc491f..fab3d8ac38 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -80,6 +80,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = old end + def test_to_json_when_wrapping_a_date_time + twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone) + assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(twz) + end + def test_nsec local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)) with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local) diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js index 7e494fb6d8..e4d25dfb21 100644 --- a/guides/assets/javascripts/guides.js +++ b/guides/assets/javascripts/guides.js @@ -1,57 +1,53 @@ -function guideMenu(){ - if (document.getElementById('guides').style.display == "none") { - document.getElementById('guides').style.display = "block"; - } else { - document.getElementById('guides').style.display = "none"; - } -} - -$.fn.selectGuide = function(guide){ +$.fn.selectGuide = function(guide) { $("select", this).val(guide); -} +}; -guidesIndex = { - bind: function(){ +var guidesIndex = { + bind: function() { var currentGuidePath = window.location.pathname; var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1); $(".guides-index-small"). on("change", "select", guidesIndex.navigate). selectGuide(currentGuide); - $(".more-info-button:visible").click(function(e){ + $(document).on("click", ".more-info-button", function(e){ e.stopPropagation(); - if($(".more-info-links").is(":visible")){ + if ($(".more-info-links").is(":visible")) { $(".more-info-links").addClass("s-hidden").unwrap(); } else { $(".more-info-links").wrap("<div class='more-info-container'></div>").removeClass("s-hidden"); } - $(document).on("click", function(e){ - var $button = $(".more-info-button"); - var element; + }); + $("#guidesMenu").on("click", function(e) { + $("#guides").toggle(); + return false; + }); + $(document).on("click", function(e){ + e.stopPropagation(); + var $button = $(".more-info-button"); + var element; - // Cross browser find the element that had the event - if (e.target) element = e.target; - else if (e.srcElement) element = e.srcElement; + // Cross browser find the element that had the event + if (e.target) element = e.target; + else if (e.srcElement) element = e.srcElement; - // Defeat the older Safari bug: - // http://www.quirksmode.org/js/events_properties.html - if (element.nodeType == 3) element = element.parentNode; + // Defeat the older Safari bug: + // http://www.quirksmode.org/js/events_properties.html + if (element.nodeType === 3) element = element.parentNode; - var $element = $(element); + var $element = $(element); - var $container = $element.parents(".more-info-container"); + var $container = $element.parents(".more-info-container"); - // We've captured a click outside the popup - if($container.length == 0){ - $container = $button.next(".more-info-container"); - $container.find(".more-info-links").addClass("s-hidden").unwrap(); - $(document).off("click"); - } - }); + // We've captured a click outside the popup + if($container.length === 0){ + $container = $button.next(".more-info-container"); + $container.find(".more-info-links").addClass("s-hidden").unwrap(); + } }); }, navigate: function(e){ var $list = $(e.target); - url = $list.val(); + var url = $list.val(); window.location = url; } -} +}; diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index ca319c91cf..898f9ff05b 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -129,6 +129,7 @@ body { font-family: Helvetica, Arial, Sans-Serif; text-decoration: none; vertical-align: middle; + cursor: pointer; } .red-button:active { border-top: none; diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index acd2ed5160..3d57012901 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -31,7 +31,7 @@ end gem 'jbuilder', '~> 1.2' # To use ActiveModel has_secure_password -# gem 'bcrypt-ruby', '~> 3.0.0' +# gem 'bcrypt-ruby', '~> 3.1.0' # Use unicorn as the app server # gem 'unicorn' diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock index 888a6b30e2..2d5c50ef5c 100644 --- a/guides/code/getting_started/Gemfile.lock +++ b/guides/code/getting_started/Gemfile.lock @@ -1,135 +1,107 @@ -GIT - remote: git://github.com/rails/activerecord-deprecated_finders.git - revision: 2e7b35d7948cefb2bba96438873d7f7bb1961a03 - specs: - activerecord-deprecated_finders (0.0.2) - -GIT - remote: git://github.com/rails/arel.git - revision: 38d0a222e275d917a2c1d093b24457bafb600a00 - specs: - arel (3.0.2.20120819075748) - -GIT - remote: git://github.com/rails/coffee-rails.git - revision: 052634e6d02d4800d7b021201cc8d5829775b3cd - specs: - coffee-rails (4.0.0.beta) - coffee-script (>= 2.2.0) - railties (>= 4.0.0.beta, < 5.0) - -GIT - remote: git://github.com/rails/sass-rails.git - revision: ae8138a89cac397c0df903dd533e2862902ce8f5 - specs: - sass-rails (4.0.0.beta) - railties (>= 4.0.0.beta, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0.rc0) - tilt (~> 1.3) - -GIT - remote: git://github.com/rails/sprockets-rails.git - revision: 09917104fdb42245fe369612a7b0e3d77e1ba763 - specs: - sprockets-rails (2.0.0.rc1) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (~> 2.8) - -PATH - remote: /Users/steve/src/rails +GEM + remote: https://rubygems.org/ specs: - actionmailer (4.0.0.beta) - actionpack (= 4.0.0.beta) + actionmailer (4.0.0) + actionpack (= 4.0.0) mail (~> 2.5.3) - actionpack (4.0.0.beta) - activesupport (= 4.0.0.beta) + actionpack (4.0.0) + activesupport (= 4.0.0) builder (~> 3.1.0) erubis (~> 2.7.0) - rack (~> 1.4.3) - rack-test (~> 0.6.1) - activemodel (4.0.0.beta) - activesupport (= 4.0.0.beta) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + activemodel (4.0.0) + activesupport (= 4.0.0) builder (~> 3.1.0) - activerecord (4.0.0.beta) - activemodel (= 4.0.0.beta) - activerecord-deprecated_finders (= 0.0.2) - activesupport (= 4.0.0.beta) - arel (~> 3.0.2) - activesupport (4.0.0.beta) - i18n (~> 0.6) - minitest (~> 4.1) + activerecord (4.0.0) + activemodel (= 4.0.0) + activerecord-deprecated_finders (~> 1.0.2) + activesupport (= 4.0.0) + arel (~> 4.0.0) + activerecord-deprecated_finders (1.0.3) + activesupport (4.0.0) + i18n (~> 0.6, >= 0.6.4) + minitest (~> 4.2) multi_json (~> 1.3) thread_safe (~> 0.1) - tzinfo (~> 0.3.33) - rails (4.0.0.beta) - actionmailer (= 4.0.0.beta) - actionpack (= 4.0.0.beta) - activerecord (= 4.0.0.beta) - activesupport (= 4.0.0.beta) - bundler (>= 1.2.2, < 2.0) - railties (= 4.0.0.beta) - sprockets-rails (~> 2.0.0.rc1) - railties (4.0.0.beta) - actionpack (= 4.0.0.beta) - activesupport (= 4.0.0.beta) - rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.15.4, < 2.0) - -GEM - remote: https://rubygems.org/ - specs: - atomic (1.0.1) + tzinfo (~> 0.3.37) + arel (4.0.0) + atomic (1.1.10) builder (3.1.4) + coffee-rails (4.0.0) + coffee-script (>= 2.2.0) + railties (>= 4.0.0.beta, < 5.0) coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.4.0) + coffee-script-source (1.6.3) erubis (2.7.0) execjs (1.4.0) multi_json (~> 1.0) - hike (1.2.1) - i18n (0.6.1) - jbuilder (1.3.0) + hike (1.2.3) + i18n (0.6.4) + jbuilder (1.4.2) activesupport (>= 3.0.0) - jquery-rails (2.2.0) + multi_json (>= 1.2.0) + jquery-rails (3.0.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.7.6) - mail (2.5.3) - i18n (>= 0.4.0) + json (1.8.0) + mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - mime-types (1.19) - minitest (4.4.0) - multi_json (1.5.0) + mime-types (1.23) + minitest (4.7.5) + multi_json (1.7.7) polyglot (0.3.3) - rack (1.4.4) + rack (1.5.2) rack-test (0.6.2) rack (>= 1.0) - rake (10.0.3) - rdoc (3.12) + rails (4.0.0) + actionmailer (= 4.0.0) + actionpack (= 4.0.0) + activerecord (= 4.0.0) + activesupport (= 4.0.0) + bundler (>= 1.3.0, < 2.0) + railties (= 4.0.0) + sprockets-rails (~> 2.0.0) + railties (4.0.0) + actionpack (= 4.0.0) + activesupport (= 4.0.0) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (10.1.0) + rdoc (3.12.2) json (~> 1.4) - sass (3.2.5) - sprockets (2.8.2) + sass (3.2.9) + sass-rails (4.0.0) + railties (>= 4.0.0.beta, < 5.0) + sass (>= 3.1.10) + sprockets-rails (~> 2.0.0) + sdoc (0.3.20) + json (>= 1.1.3) + rdoc (~> 3.10) + sprockets (2.10.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.0.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (~> 2.8) sqlite3 (1.3.7) - thor (0.16.0) + thor (0.18.1) thread_safe (0.1.0) atomic - tilt (1.3.3) - treetop (1.4.12) + tilt (1.4.1) + treetop (1.4.14) polyglot polyglot (>= 0.3.1) - turbolinks (1.0.0) + turbolinks (1.2.0) coffee-rails - tzinfo (0.3.35) - uglifier (1.3.0) + tzinfo (0.3.37) + uglifier (2.1.1) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) @@ -137,14 +109,12 @@ PLATFORMS ruby DEPENDENCIES - activerecord-deprecated_finders! - arel! - coffee-rails! - jbuilder (~> 1.0.1) + coffee-rails + jbuilder (~> 1.2) jquery-rails - rails! - sass-rails! - sprockets-rails! + rails (= 4.0.0) + sass-rails + sdoc sqlite3 turbolinks uglifier (>= 1.0.3) diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb index 0e3d2a6dde..b2d9bcdf7f 100644 --- a/guides/code/getting_started/app/controllers/comments_controller.rb +++ b/guides/code/getting_started/app/controllers/comments_controller.rb @@ -4,7 +4,7 @@ class CommentsController < ApplicationController def create @post = Post.find(params[:post_id]) - @comment = @post.comments.create(params[:comment].permit(:commenter, :body)) + @comment = @post.comments.create(comment_params) redirect_to post_path(@post) end @@ -14,4 +14,10 @@ class CommentsController < ApplicationController @comment.destroy redirect_to post_path(@post) end + + private + + def comment_params + params.require(:comment).permit(:commenter, :body) + end end diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb index 6aa1409170..02689ad67b 100644 --- a/guides/code/getting_started/app/controllers/posts_controller.rb +++ b/guides/code/getting_started/app/controllers/posts_controller.rb @@ -17,7 +17,7 @@ class PostsController < ApplicationController def update @post = Post.find(params[:id]) - if @post.update(params[:post].permit(:title, :text)) + if @post.update(post_params) redirect_to action: :show, id: @post.id else render 'edit' @@ -29,7 +29,7 @@ class PostsController < ApplicationController end def create - @post = Post.new(params[:post].permit(:title, :text)) + @post = Post.new(post_params) if @post.save redirect_to action: :show, id: @post.id @@ -44,4 +44,10 @@ class PostsController < ApplicationController redirect_to action: :index end + + private + + def post_params + params.require(:post).permit(:title, :text) + end end diff --git a/guides/code/getting_started/app/views/welcome/index.html.erb b/guides/code/getting_started/app/views/welcome/index.html.erb index 738e12d7dc..56be8dd3cc 100644 --- a/guides/code/getting_started/app/views/welcome/index.html.erb +++ b/guides/code/getting_started/app/views/welcome/index.html.erb @@ -1,3 +1,4 @@ <h1>Hello, Rails!</h1> <%= link_to "My Blog", controller: "posts" %> +<%= link_to "New Post", new_post_path %> diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 66e26c63cb..006d0cda92 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -50,10 +50,47 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev Major Features -------------- -TODO. Give a list and then talk about each of them briefly. We can point to relevant code commits or documentation from these sections. - [](http://guides.rubyonrails.org/images/rails4_features.png) +### Upgrade + + * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required + * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. + * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. + * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. + * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. + * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. + * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. + * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. + +### ActionPack + + * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). + * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). + * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. + * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Set ETag and Last-Modified headers using `etag` and `fresh_when`. + * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. + * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. + * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView is moved outside of ActionPack. + * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. + +### General + + * **ActiveModel::Model** ([commit](https://github.com/rails/rails/commit/3b822e91d1a6c4eab0064989bbd07aae3a6d0d08)) - `ActiveModel::Model` is extracted from ActiveRecord. `ActiveModel::Model` provides validations and `form_for` for normal Ruby objects. + * **New scope API** ([commit](https://github.com/rails/rails/commit/50cbc03d18c5984347965a94027879623fc44cce)) - Scopes must always use callables. + * **Schema cache dump** ([commit](https://github.com/rails/rails/commit/5ca4fc95818047108e69e22d200e7a4a22969477)) - To improve Rails boot time, instead of loading the schema directly from the database, load the schema from a dump file. + * **Support for specifying transaction isolation level** ([commit](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)) - Choose whether repeatable reads or improved performance (less locking) is more important. + * **Dalli** ([commit](https://github.com/rails/rails/commit/82663306f428a5bbc90c511458432afb26d2f238)) - For the memcache session store, use the Dalli memcache client. + * **Notifications start & finish** ([commit](https://github.com/rails/rails/commit/f08f8750a512f741acb004d0cebe210c5f949f28)) - Active Support instrumentation reports start and finish notifications to subscribers. + * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. Note: Check that the gems you are using are threadsafe. + * **PATCH verb** ([commit](https://github.com/rails/rails/commit/eed9f2539e3ab5a68e798802f464b8e4e95e619e)) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources. + +### Security + + * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. + * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. + * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). + Extraction of features to gems --------------------------- diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index f2abd833aa..ecaee02cce 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -907,6 +907,92 @@ Now the user can request to get a PDF version of a client just by adding ".pdf" GET /clients/1.pdf ``` +### Live Streaming of Arbitrary Data + +Rails allows you to stream more than just files. In fact, you can stream anything +you would like in a response object. The `ActionController::Live` module allows +you to create a persistent connection with a browser. Using this module, you will +be able to send arbitrary data to the browser at specific points in time. + +#### Incorporating Live Streaming + +Including `ActionController::Live` inside of your controller class will provide +all actions inside of the controller the ability to stream data. You can mix in +the module like so: + +```ruby +class MyController < ActionController::Base + include ActionController::Live + + def stream + response.headers['Content-Type'] = 'text/event-stream' + 100.times { + response.stream.write "hello world\n" + sleep 1 + } + ensure + response.stream.close + end +end +``` + +The above code will keep a persistent connection with the browser and send 100 +messages of `"hello world\n"`, each one second apart. + +There are a couple of things to notice in the above example. We need to make +sure to close the response stream. Forgetting to close the stream will leave +the socket open forever. We also have to set the content type to `text/event-stream` +before we write to the response stream. This is because headers cannot be written +after the response has been committed (when `response.committed` returns a truthy +value), which occurs when you `write` or `commit` the response stream. + +#### Example Usage + +Let's suppose that you were making a Karaoke machine and a user wants to get the +lyrics for a particular song. Each `Song` has a particular number of lines and +each line takes time `num_beats` to finish singing. + +If we wanted to return the lyrics in Karaoke fashion (only sending the line when +the singer has finished the previous line), then we could use `ActionController::Live` +as follows: + +```ruby +class LyricsController < ActionController::Base + include ActionController::Live + + def show + response.headers['Content-Type'] = 'text/event-stream' + song = Song.find(params[:id]) + + song.each do |line| + response.stream.write line.lyrics + sleep line.num_beats + end + ensure + response.stream.close + end +end +``` + +The above code sends the next line only after the singer has completed the previous +line. + +#### Streaming Considerations + +Streaming arbitrary data is an extremely powerful tool. As shown in the previous +examples, you can choose when and what to send across a response stream. However, +you should also note the following things: + +* Each response stream creates a new thread and copies over the thread local + variables from the original thread. Having too many thread local variables can + negatively impact performance. Similarly, a large number of threads can also + hinder performance. +* Failing to close the response stream will leave the corresponding socket open + forever. Make sure to call `close` whenever you are using a response stream. +* WEBrick servers buffer all responses, and so including `ActionController::Live` + will not work. You must use a web server which does not automatically buffer + responses. + Log Filtering ------------- diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index bdb51d881d..3542844f33 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -775,8 +775,8 @@ select_day(5) Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. ```ruby -# Generates a select field for minutes that defaults to the minutes for the time provided -select_minute(Time.now + 6.hours) +# Generates a select field for hours that defaults to the hours for the time provided +select_hour(Time.now + 6.hours) ``` #### select_minute @@ -941,9 +941,9 @@ Creates a form and a scope around a specific model object that is used as a base ```html+erb <%= form_for @post do |f| %> <%= f.label :title, 'Title' %>: - <%= f.text_field :title %><br /> + <%= f.text_field :title %><br> <%= f.label :body, 'Body' %>: - <%= f.text_area :body %><br /> + <%= f.text_area :body %><br> <% end %> ``` @@ -1492,7 +1492,7 @@ number_to_human_size(1234567) # => 1.2 MB Formats a number as a percentage string. ```ruby -number_to_percentage(100, :precision => 0) # => 100% +number_to_percentage(100, precision: 0) # => 100% ``` #### number_to_phone diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index d9fb20f3bf..556c2544ff 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -105,7 +105,7 @@ depending on the purpose of these columns. Migrations](migrations.html) to create your tables, this column will be automatically created. -There are also some optional column names that will create additional features +There are also some optional column names that will add additional features to Active Record instances: * `created_at` - Automatically gets set to the current date and time when the @@ -343,7 +343,7 @@ Migrations Rails provides a domain-specific language for managing a database schema called migrations. Migrations are stored in files which are executed against any -database that Active Record support using `rake`. Here's a migration that +database that Active Record supports using `rake`. Here's a migration that creates a table: ```ruby diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 01401cc340..df1dd22971 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -49,8 +49,8 @@ The macro-style class methods can also receive a block. Consider using this styl class User < ActiveRecord::Base validates :login, :email, presence: true - before_create do |user| - user.name = user.login.capitalize if user.name.blank? + before_create do + self.name = login.capitalize if name.blank? end end ``` @@ -343,7 +343,7 @@ By using the `after_commit` callback we can account for this case. ```ruby class PictureFile < ActiveRecord::Base - after_commit :delete_picture_file_from_disk, :on => [:destroy] + after_commit :delete_picture_file_from_disk, on: [:destroy] def delete_picture_file_from_disk if File.exist?(filepath) diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 031a203f08..0592821a14 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -711,7 +711,7 @@ Post.order('id DESC').limit(20).unscope(:order, :limit) = Post.all You can additionally unscope specific where clauses. For example: ```ruby -Post.where(:id => 10).limit(1).unscope(where: :id, :limit).order('id DESC') = Post.order('id DESC') +Post.where(id: 10).limit(1).unscope({ where: :id }, :limit).order('id DESC') = Post.order('id DESC') ``` ### `only` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 3bfc600e7c..d95b587e78 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -121,7 +121,7 @@ database only if the object is valid: The bang versions (e.g. `save!`) raise an exception if the record is invalid. The non-bang versions don't, `save` and `update` return `false`, -`create` just return the objects. +`create` just returns the object. ### Skipping Validations diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index b68c24acfd..6c4d7fe255 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -69,12 +69,12 @@ Rails' old strategy was to append a date-based query string to every asset linke The query string strategy has several disadvantages: -1. **Not all caches will reliably cache content where the filename only differs by query parameters**<br /> +1. **Not all caches will reliably cache content where the filename only differs by query parameters**<br> [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/), "...avoiding a querystring for cacheable resources". He found that in this case 5-20% of requests will not be cached. Query strings in particular do not work at all with some CDNs for cache invalidation. -2. **The file name can change between nodes in multi-server environments.**<br /> +2. **The file name can change between nodes in multi-server environments.**<br> The default query string in Rails 2.x is based on the modification time of the files. When assets are deployed to a cluster, there is no guarantee that the timestamps will be the same, resulting in different values being used depending on which server handles the request. -3. **Too much cache invalidation**<br /> +3. **Too much cache invalidation**<br> When static assets are deployed with each new release of code, the mtime(time of last modification) of _all_ these files changes, forcing all remote clients to fetch them again, even when the content of those assets has not changed. Fingerprinting fixes these problems by avoiding query strings, and by ensuring that filenames are consistent based on their content. @@ -606,7 +606,7 @@ Customizing the Pipeline ### CSS Compression -There is currently one option for compressing CSS, YUI. The [YUI CSS compressor](http://developer.yahoo.com/yui/compressor/css.html) provides minification. +There is currently one option for compressing CSS, YUI. The [YUI CSS compressor](http://yui.github.io/yuicompressor/css.html) provides minification. The following line enables YUI compression, and requires the `yui-compressor` gem. @@ -620,7 +620,7 @@ The `config.assets.compress` must be set to `true` to enable CSS compression. Possible options for JavaScript compression are `:closure`, `:uglifier` and `:yui`. These require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems, respectively. -The default Gemfile includes [uglifier](https://github.com/lautis/uglifier). This gem wraps [UglifierJS](https://github.com/mishoo/UglifyJS) (written for NodeJS) in Ruby. It compresses your code by removing white space. It also includes other optimizations such as changing your `if` and `else` statements to ternary operators where possible. +The default Gemfile includes [uglifier](https://github.com/lautis/uglifier). This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for NodeJS) in Ruby. It compresses your code by removing white space and comments, shortening local variable names, and performing other micro-optimizations such as changing `if` and `else` statements to ternary operators where possible. The following line invokes `uglifier` for JavaScript compression. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 884aa6a9ea..e6a66f3fa1 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -953,7 +953,7 @@ end ##### `includes` -You can use the `includes` method let you specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: +You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby class LineItem < ActiveRecord::Base diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 9bb5d621fc..2f5444c763 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -103,6 +103,8 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.force_ssl` forces all requests to be under HTTPS protocol by using `ActionDispatch::SSL` middleware. +* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to a instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. + * `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all modes except production, where it defaults to `:info`. * `config.log_tags` accepts a list of methods that respond to `request` object. This makes it easy to tag log lines with debug information like subdomain and request id — both very helpful in debugging multi-user production applications. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index b9f42fa51b..24e16ecd40 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -234,8 +234,8 @@ workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). As a compromise, test what your code obviously affects, and if the change is not in railties run the whole test suite of the affected component. If all is -green that's enough to propose your contribution. We have [Travis CI](https -://travis-ci.org/) as a safety net for catching unexpected breakages +green that's enough to propose your contribution. We have [Travis CI](https://travis-ci.org/rails/rails) +as a safety net for catching unexpected breakages elsewhere. TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted. diff --git a/guides/source/engines.md b/guides/source/engines.md index d8120fe244..a77be917a2 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -346,7 +346,7 @@ Next, the partial that this line will render needs to exist. Create a new direct <h3>New comment</h3> <%= form_for [@post, @post.comments.build] do |f| %> <p> - <%= f.label :text %><br /> + <%= f.label :text %><br> <%= f.text_area :text %> </p> <%= f.submit %> @@ -520,7 +520,7 @@ First, the `author_name` text field needs to be added to the `app/views/blorgh/p ```html+erb <div class="field"> - <%= f.label :author_name %><br /> + <%= f.label :author_name %><br> <%= f.text_field :author_name %> </div> ``` diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 7f37a298b1..11e8db9e88 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -553,7 +553,7 @@ outputs (with actual option values omitted for brevity) which results in a `params` hash like ```ruby -{:person => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}} +{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}} ``` When this is passed to `Person.new` (or `update`), Active Record spots that these parameters should all be used to construct the `birth_date` attribute and uses the suffixed information to determine in which order it should pass these parameters to functions such as `Date.civil`. @@ -881,19 +881,19 @@ end ```ruby { - :person => { - :name => 'John Doe', - :addresses_attributes => { - '0' => { - :kind => 'Home', - :street => '221b Baker Street', - }, - '1' => { - :kind => 'Office', - :street => '31 Spooner Street' - } - } + 'person' => { + 'name' => 'John Doe', + 'addresses_attributes' => { + '0' => { + 'kind' => 'Home', + 'street' => '221b Baker Street' + }, + '1' => { + 'kind' => 'Office', + 'street' => '31 Spooner Street' + } } + } } ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index cdb25a1013..a0e0975d62 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -138,7 +138,7 @@ application. Most of the work in this tutorial will happen in the `app/` folder, |config/|Configure your application's runtime rules, routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html)| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| -|Gemfile<br />Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com) | +|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com) | |lib/|Extended modules for your application.| |log/|Application log files.| |public/|The only folder seen to the world as-is. Contains the static files and compiled assets.| @@ -264,11 +264,14 @@ Blog::Application.routes.draw do end ``` -If you run `rake routes`, you'll see that all the routes for the -standard RESTful actions. +If you run `rake routes`, you'll see that it has defined routes for all the +standard RESTful actions. The meaning of the prefix column (and other columns) +will be seen later, but for now notice that Rails has inferred the +singular form `post` and makes meaningful use of the distinction. ```bash $ rake routes + Prefix Verb URI Pattern Controller#Action posts GET /posts(.:format) posts#index POST /posts(.:format) posts#create new_post GET /posts/new(.:format) posts#new @@ -394,9 +397,27 @@ Edit the `form_for` line inside `app/views/posts/new.html.erb` to look like this <%= form_for :post, url: posts_path do |f| %> ``` -In this example, the `posts_path` helper is passed to the `:url` option. What Rails will do with this is that it will point the form to the `create` action of the current controller, the `PostsController`, and will send a `POST` request to that route. - -By using the `post` method rather than the `get` method, Rails will define a route that will only respond to POST methods. The POST method is the typical method used by forms all over the web. +In this example, the `posts_path` helper is passed to the `:url` option. +To see what Rails will do with this, we look back at the output of +`rake routes`: +```bash +$ rake routes + Prefix Verb URI Pattern Controller#Action + posts GET /posts(.:format) posts#index + POST /posts(.:format) posts#create + new_post GET /posts/new(.:format) posts#new +edit_post GET /posts/:id/edit(.:format) posts#edit + post GET /posts/:id(.:format) posts#show + PATCH /posts/:id(.:format) posts#update + PUT /posts/:id(.:format) posts#update + DELETE /posts/:id(.:format) posts#destroy + root / welcome#index +``` +The `posts_path` helper tells Rails to point the form +to the URI Pattern associated with the `posts` prefix; and +the form will (by default) send a `POST` request +to that route. This is associated with the +`create` action of the current controller, the `PostsController`. With the form and its associated route defined, you will be able to fill in the form and then click the submit button to begin the process of creating a new post, so go ahead and do that. When you submit the form, you should see a familiar error: @@ -723,7 +744,7 @@ TIP: In development mode (which is what you're working in by default), Rails reloads your application with every browser request, so there's no need to stop and restart the web server when a change is made. -### Allowing the update of fields +### Adding Some Validation The model file, `app/models/post.rb` is about as simple as it can get: @@ -738,8 +759,6 @@ your Rails models for free, including basic database CRUD (Create, Read, Update, Destroy) operations, data validation, as well as sophisticated search support and the ability to relate multiple models to one another. -### Adding Some Validation - Rails includes methods to help you validate the data that you send to models. Open the `app/models/post.rb` file and edit it: @@ -1018,9 +1037,14 @@ content: ``` Everything except for the `form_for` declaration remained the same. -How `form_for` can figure out the right `action` and `method` attributes when building the form -will be explained in [just a moment](/form_helpers.html#binding-a-form-to-an-object). -For now, let's update the `app/views/posts/new.html.erb` view to use this new partial, rewriting it +The reason we can use this shorter, simpler `form_for` declaration +to stand in for either of the other forms is that `@post` is a *resource* +corresponding to a full set of RESTful routes, and Rails is able to infer +which URI and method to use. +For more information about this use of `form_for`, see +[Resource-oriented style](//api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style). + +Now, let's update the `app/views/posts/new.html.erb` view to use this new partial, rewriting it completely: ```html+erb @@ -1291,11 +1315,11 @@ So first, we'll wire up the Post show template <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> <p> - <%= f.label :commenter %><br /> + <%= f.label :commenter %><br> <%= f.text_field :commenter %> </p> <p> - <%= f.label :body %><br /> + <%= f.label :body %><br> <%= f.text_area :body %> </p> <p> @@ -1371,11 +1395,11 @@ template. This is where we want the comment to show, so let's add that to the <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> <p> - <%= f.label :commenter %><br /> + <%= f.label :commenter %><br> <%= f.text_field :commenter %> </p> <p> - <%= f.label :body %><br /> + <%= f.label :body %><br> <%= f.text_area :body %> </p> <p> @@ -1437,11 +1461,11 @@ following: <h2>Add a comment:</h2> <%= form_for([@post, @post.comments.build]) do |f| %> <p> - <%= f.label :commenter %><br /> + <%= f.label :commenter %><br> <%= f.text_field :commenter %> </p> <p> - <%= f.label :body %><br /> + <%= f.label :body %><br> <%= f.text_area :body %> </p> <p> @@ -1467,11 +1491,11 @@ create a file `app/views/comments/_form.html.erb` containing: ```html+erb <%= form_for([@post, @post.comments.build]) do |f| %> <p> - <%= f.label :commenter %><br /> + <%= f.label :commenter %><br> <%= f.text_field :commenter %> </p> <p> - <%= f.label :body %><br /> + <%= f.label :body %><br> <%= f.text_area :body %> </p> <p> diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index a8e4525c67..57c224c165 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -19,7 +19,7 @@ Ruby on Rails Guides <h3><%= section['name'] %></h3> <dl> <% section['documents'].each do |document| %> - <%= guide(document['name'], document['url'], :work_in_progress => document['work_in_progress']) do %> + <%= guide(document['name'], document['url'], work_in_progress: document['work_in_progress']) do %> <p><%= document['description'] %></p> <% end %> <% end %> diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 11c736585f..26259408b4 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -350,7 +350,7 @@ end The interesting part for a Rails app is the last line, `server.run`. Here we encounter the `wrapped_app` method again, which this time we're going to explore more (even though it was executed before, and -thus memorized by now). +thus memoized by now). ```ruby @wrapped_app ||= build_app app diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index c737a0e9dc..71d3c5638b 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -48,7 +48,7 @@ <ul class="nav"> <li><a class="nav-item" href="index.html">Home</a></li> <li class="guides-index guides-index-large"> - <a href="index.html" onclick="guideMenu(); return false;" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a> + <a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a> <div id="guides" class="clearfix" style="display: none;"> <hr /> <% ['L', 'R'].each do |position| %> @@ -143,7 +143,7 @@ <script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushSql.js"></script> <script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushPlain.js"></script> <script type="text/javascript"> - SyntaxHighlighter.all() + SyntaxHighlighter.all(); $(guidesIndex.bind); </script> </body> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 5908801bc9..5b6e5387ff 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -88,7 +88,7 @@ If we want to display the properties of all the books in our view, we can do so <% end %> </table> -<br /> +<br> <%= link_to "New book", new_book_path %> ``` @@ -1026,7 +1026,7 @@ You can also pass local variables into partials, making them even more powerful ```html+erb <%= form_for(zone) do |f| %> <p> - <b>Zone name</b><br /> + <b>Zone name</b><br> <%= f.text_field :name %> </p> <p> diff --git a/guides/source/migrations.md b/guides/source/migrations.md index 035f9499de..e6d1e71f5e 100644 --- a/guides/source/migrations.md +++ b/guides/source/migrations.md @@ -376,7 +376,7 @@ create_join_table :products, :categories, column_options: {null: true} will create the `product_id` and `category_id` with the `:null` option as `true`. -You can pass the option `:table_name` with you want to customize the table +You can pass the option `:table_name` when you want to customize the table name. For example, ```ruby @@ -841,7 +841,6 @@ class AddFlagToProduct < ActiveRecord::Migration reversible do |dir| dir.up { Product.update_all flag: false } end - Product.update_all flag: false end end ``` diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index 73f541bda4..3becaccb0a 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -327,7 +327,7 @@ The following shows how to replace use `Rack::Builder` instead of the Rails supp config.middleware.clear ``` -<br /> +<br> <strong>Add a `config.ru` file to `Rails.root`</strong> ```ruby diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 0f388d15c4..b7ec747a77 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -131,7 +131,15 @@ The following changes are meant for upgrading your application to Rails 4.0. ### Gemfile -Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that line from your Gemfile when upgrading. +Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that +line from your Gemfile when upgrading. You should also update your application +file (in `config/application.rb`): + +```ruby +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(:default, Rails.env) +``` ### vendor/plugins @@ -143,11 +151,16 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep * The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them. +* In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed. + * Rails 4.0 has changed how orders get stacked in `ActiveRecord::Relation`. In previous versions of Rails, the new order was applied after the previously defined order. But this is no longer true. Check [Active Record Query guide](active_record_querying.html#ordering) for more information. * Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`. -* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) to a smoothly upgrade path. +* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) for a smooth upgrade path. + +* If you are not using Protected Attributes, you can remove any options related to +this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options. * Rails 4.0 requires that scopes use a callable object such as a Proc or lambda: @@ -208,6 +221,11 @@ Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for d * Rails 4.0 deprecates the `dom_id` and `dom_class` methods in controllers (they are fine in views). You will need to include the `ActionView::RecordIdentifier` module in controllers requiring this feature. +* Rails 4.0 deprecates the `:confirm` option for the `link_to` helper. You should +instead rely on a data attribute (e.g. `data: { confirm: 'Are you sure?' }`). +This deprecation also concerns the helpers based on this one (such as `link_to_if` +or `link_to_unless`). + * Rails 4.0 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`. * Rails 4.0 raises an `ArgumentError` if clashing named routes are defined. This can be triggered by explicitly defined named routes or by the `resources` method. Here are two examples that clash with routes named `example_path`: @@ -295,6 +313,12 @@ Active Record Observer and Action Controller Sweeper have been extracted to the ### sprockets-rails * `assets:precompile:primary` has been removed. Use `assets:precompile` instead. +* The `config.assets.compress` option should be changed to +`config.assets.js_compressor` like so for instance: + +```ruby +config.assets.js_compressor = :uglifier +``` ### sass-rails diff --git a/install.rb b/install.rb index 77c3e6048a..bff8fee934 100644 --- a/install.rb +++ b/install.rb @@ -5,7 +5,7 @@ if version.nil? exit(64) end -%w( activesupport activemodel activerecord actionpack actionmailer railties ).each do |framework| +%w( activesupport activemodel activerecord actionpack actionview actionmailer railties ).each do |framework| puts "Installing #{framework}..." `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem` end diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index b80aa944e0..7e7b157483 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,20 @@ +* Added generated unit test for generator generator using new + `test:generators` rake task. + + *Josef Å imánek* + +* Removed `update:application_controller` rake task. + + *Josef Å imánek* + +* Fix `rake environment` to do not eager load modules + + *Paul Nikitochkin* + +* Fix `rake notes` to look into `*.sass` files + + *Yuri Artemev* + * Removed deprecated `Rails.application.railties.engines`. *Arun Agrawal* diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index cadf0fb43e..eccdee7b07 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -18,7 +18,7 @@ you to present the data from database rows as objects and embellish these data o with business logic methods. Although most \Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by the ActiveModel module. You can read more about Active Record in its -{README}[link:/activerecord/README.rdoc]. +{README}[link:files/activerecord/README_rdoc.html]. The Controller layer is responsible for handling incoming HTTP requests and providing a suitable response. Usually this means returning \HTML, but \Rails controllers can also @@ -29,7 +29,7 @@ In \Rails, the Controller and View layers are handled together by Action Pack. These two layers are bundled in a single package due to their heavy interdependence. This is unlike the relationship between Active Record and Action Pack, which are independent. Each of these packages can be used independently outside of \Rails. You -can read more about Action Pack in its {README}[link:/actionpack/README.rdoc]. +can read more about Action Pack in its {README}[link:files/actionpack/README_rdoc.html]. == Getting Started @@ -55,7 +55,7 @@ can read more about Action Pack in its {README}[link:/actionpack/README.rdoc]. 5. Follow the guidelines to start developing your application. You may find the following resources handy: -* The README file created within your application. +* The \README file created within your application. * {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html]. * {Ruby on \Rails Tutorial}[http://ruby.railstutorial.org/ruby-on-rails-tutorial-book]. * {Ruby on \Rails Guides}[http://guides.rubyonrails.org]. diff --git a/railties/Rakefile b/railties/Rakefile index 5728c774bd..a899d069b5 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,10 +1,6 @@ require 'rake/testtask' require 'rubygems/package_task' -require 'date' -require 'rbconfig' - - task :default => :test desc "Run all unit tests" @@ -35,15 +31,6 @@ Rake::TestTask.new('test:regular') do |t| t.verbose = true end -# Update spinoffs ------------------------------------------------------------------- - -desc "Updates application README to the latest version Railties README" -task :update_readme do - readme = "lib/rails/generators/rails/app/templates/README" - rm readme - cp "./README.rdoc", readme -end - # Generate GEM ---------------------------------------------------------------------------- spec = eval(File.read('railties.gemspec')) diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index c829873da4..1f9568fb5f 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -7,7 +7,6 @@ module Rails 'activesupport' => { :include => %w( README.rdoc - CHANGELOG.md lib/active_support/**/*.rb ), :exclude => 'lib/active_support/vendor/*' @@ -16,7 +15,6 @@ module Rails 'activerecord' => { :include => %w( README.rdoc - CHANGELOG.md lib/active_record/**/*.rb ), :exclude => 'lib/active_record/vendor/*' @@ -25,7 +23,6 @@ module Rails 'activemodel' => { :include => %w( README.rdoc - CHANGELOG.md lib/active_model/**/*.rb ) }, @@ -33,19 +30,23 @@ module Rails 'actionpack' => { :include => %w( README.rdoc - CHANGELOG.md lib/abstract_controller/**/*.rb lib/action_controller/**/*.rb lib/action_dispatch/**/*.rb - lib/action_view/**/*.rb ), :exclude => 'lib/action_controller/vendor/*' }, + 'actionview' => { + :include => %w( + README.rdoc + lib/action_view/**/*.rb + ) + }, + 'actionmailer' => { :include => %w( README.rdoc - CHANGELOG.md lib/action_mailer/**/*.rb ), :exclude => 'lib/action_mailer/vendor/*' @@ -54,8 +55,6 @@ module Rails 'railties' => { :include => %w( README.rdoc - CHANGELOG.md - MIT-LICENSE lib/**/*.rb ), :exclude => 'lib/rails/generators/rails/**/templates/**/*.rb' @@ -135,12 +134,20 @@ module Rails def api_dir 'doc/rdoc' end + end + class EdgeTask < RepoTask def rails_version "master@#{`git rev-parse HEAD`[0, 7]}" end end + class StableTask < RepoTask + def rails_version + File.read('RAILS_VERSION').strip + end + end + class AppTask < Task def component_root_dir(gem_name) $:.grep(%r{#{gem_name}[\w.-]*/lib\z}).first[0..-5] diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 6fd01ee768..4faaf5ef1e 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -289,9 +289,9 @@ module Rails railties.each { |r| r.run_tasks_blocks(app) } super require "rails/tasks" - config = self.config task :environment do - config.eager_load = false + ActiveSupport.on_load(:before_initialize) { config.eager_load = false } + require_environment! end end diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 370c906086..570ff02c83 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -20,9 +20,7 @@ module Rails middleware.use ::ActionDispatch::SSL, config.ssl_options end - if config.action_dispatch.x_sendfile_header.present? - middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header - end + middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header if config.serve_static_assets middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 3e8163490f..f32bf772a5 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -9,101 +9,9 @@ aliases = { "r" => "runner" } -help_message = <<-EOT -Usage: rails COMMAND [ARGS] - -The most common rails commands are: - generate Generate new code (short-cut alias: "g") - console Start the Rails console (short-cut alias: "c") - server Start the Rails server (short-cut alias: "s") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" - -In addition to those, there are: - application Generate the Rails application code - destroy Undo code generated with "generate" (short-cut alias: "d") - plugin new Generates skeleton for developing a Rails plugin - runner Run a piece of code in the application environment (short-cut alias: "r") - -All commands can be run with -h (or --help) for more information. -EOT - - command = ARGV.shift command = aliases[command] || command -case command -when 'plugin' - require "rails/commands/plugin" -when 'generate', 'destroy' - require 'rails/generators' - - require APP_PATH - Rails.application.require_environment! - - Rails.application.load_generators - - require "rails/commands/#{command}" - -when 'console' - require 'rails/commands/console' - options = Rails::Console.parse_arguments(ARGV) - - # RAILS_ENV needs to be set before config/application is required - ENV['RAILS_ENV'] = options[:environment] if options[:environment] - - # shift ARGV so IRB doesn't freak - ARGV.shift if ARGV.first && ARGV.first[0] != '-' - - require APP_PATH - Rails.application.require_environment! - Rails::Console.start(Rails.application, options) - -when 'server' - # Change to the application's path if there is no config.ru file in current directory. - # This allows us to run `rails server` from other directories, but still get - # the main config.ru and properly set the tmp directory. - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru")) - - require 'rails/commands/server' - Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. - require APP_PATH - Dir.chdir(Rails.application.root) - server.start - end - -when 'dbconsole' - require 'rails/commands/dbconsole' - Rails::DBConsole.start - -when 'application', 'runner' - require "rails/commands/#{command}" - -when 'new' - if %w(-h --help).include?(ARGV.first) - require 'rails/commands/application' - else - puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" - puts "Type 'rails' for help." - exit(1) - end - -when '--version', '-v' - ARGV.unshift '--version' - require 'rails/commands/application' - -when '-h', '--help' - puts help_message +require 'rails/commands/commands_tasks' -else - puts "Error: Command '#{command}' not recognized" - if %x{rake #{command} --dry-run 2>&1 } && $?.success? - puts "Did you mean: `$ rake #{command}` ?\n\n" - end - puts help_message - exit(1) -end +Rails::CommandsTasks.new(ARGV).run_command!(command) diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index 2ff29418c6..678697f09b 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -1,30 +1,3 @@ -require 'rails/version' - -if ['--version', '-v'].include?(ARGV.first) - puts "Rails #{Rails::VERSION::STRING}" - exit(0) -end - -if ARGV.first != "new" - ARGV[0] = "--help" -else - ARGV.shift - unless ARGV.delete("--no-rc") - customrc = ARGV.index{ |x| x.include?("--rc=") } - railsrc = if customrc - File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, "")) - else - File.join(File.expand_path("~"), '.railsrc') - end - if File.exist?(railsrc) - extra_args_string = File.read(railsrc) - extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten - puts "Using #{extra_args.join(" ")} from #{railsrc}" - ARGV.insert(1, *extra_args) - end - end -end - require 'rails/generators' require 'rails/generators/rails/app/app_generator' @@ -40,4 +13,5 @@ module Rails end end +Rails::Generators::AppPreparer.new(ARGV).prepare! Rails::Generators::AppGenerator.start diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb new file mode 100644 index 0000000000..11524c4ef5 --- /dev/null +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -0,0 +1,170 @@ +module Rails + # This is a class which takes in a rails command and initiates the appropriate + # initiation sequence. + # + # Warning: This class mutates ARGV because some commands require manipulating + # it before they are run. + class CommandsTasks # :nodoc: + attr_reader :argv + + HELP_MESSAGE = <<-EOT +Usage: rails COMMAND [ARGS] + +The most common rails commands are: + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" + +In addition to those, there are: + application Generate the Rails application code + destroy Undo code generated with "generate" (short-cut alias: "d") + plugin new Generates skeleton for developing a Rails plugin + runner Run a piece of code in the application environment (short-cut alias: "r") + +All commands can be run with -h (or --help) for more information. +EOT + + COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) + + def initialize(argv) + @argv = argv + end + + def run_command!(command) + command = parse_command(command) + if COMMAND_WHITELIST.include?(command) + send(command) + else + write_error_message(command) + end + end + + def plugin + require_command!("plugin") + end + + def generate + generate_or_destroy(:generate) + end + + def console + require_command!("console") + options = Rails::Console.parse_arguments(argv) + + # RAILS_ENV needs to be set before config/application is required + ENV['RAILS_ENV'] = options[:environment] if options[:environment] + + # shift ARGV so IRB doesn't freak + shift_argv! + + require_application_and_environment! + Rails::Console.start(Rails.application, options) + end + + def server + set_application_directory! + require_command!("server") + + Rails::Server.new.tap do |server| + # We need to require application after the server sets environment, + # otherwise the --environment option given to the server won't propagate. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start + end + end + + def dbconsole + require_command!("dbconsole") + Rails::DBConsole.start + end + + def application + require_command!("application") + end + + def runner + require_command!("runner") + end + + def new + if %w(-h --help).include?(argv.first) + require_command!("application") + else + exit_with_initialization_warning! + end + end + + def version + argv.unshift '--version' + require_command!("application") + end + + def help + write_help_message + end + + private + + def exit_with_initialization_warning! + puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" + puts "Type 'rails' for help." + exit(1) + end + + def shift_argv! + argv.shift if argv.first && argv.first[0] != '-' + end + + def require_command!(command) + require "rails/commands/#{command}" + end + + def generate_or_destroy(command) + require 'rails/generators' + require_application_and_environment! + Rails.application.load_generators + require "rails/commands/#{command}" + end + + # Change to the application's path if there is no config.ru file in current directory. + # This allows us to run `rails server` from other directories, but still get + # the main config.ru and properly set the tmp directory. + def set_application_directory! + Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru")) + end + + def require_application_and_environment! + require APP_PATH + Rails.application.require_environment! + end + + def write_help_message + puts HELP_MESSAGE + end + + def write_error_message(command) + puts "Error: Command '#{command}' not recognized" + if %x{rake #{command} --dry-run 2>&1 } && $?.success? + puts "Did you mean: `$ rake #{command}` ?\n\n" + end + write_help_message + exit(1) + end + + def parse_command(command) + case command + when '--version', '-v' + 'version' + when '--help', '-h' + 'help' + else + command + end + end + end +end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index d48dcf9ef3..9fb8ea6955 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -300,5 +300,67 @@ module Rails defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder end end + + # This class handles preparation of the arguments before the AppGenerator is + # called. The class provides version or help information if they were + # requested, and also constructs the railsrc file (used for extra configuration + # options). + # + # This class should be called before the AppGenerator is required and started + # since it configures and mutates ARGV correctly. + class AppPreparer # :nodoc + attr_reader :argv + + def initialize(argv = ARGV) + @argv = argv + end + + def prepare! + handle_version_request!(argv.first) + unless handle_invalid_command!(argv.first) + argv.shift + handle_rails_rc! + end + end + + private + + def handle_version_request!(argument) + if ['--version', '-v'].include?(argv.first) + require 'rails/version' + puts "Rails #{Rails::VERSION::STRING}" + exit(0) + end + end + + def handle_invalid_command!(argument) + if argument != "new" + argv[0] = "--help" + end + end + + def handle_rails_rc! + unless argv.delete("--no-rc") + insert_railsrc(railsrc) + end + end + + def railsrc + if (customrc = argv.index{ |x| x.include?("--rc=") }) + File.expand_path(argv.delete_at(customrc).gsub(/--rc=/, "")) + else + File.join(File.expand_path("~"), '.railsrc') + end + end + + def insert_railsrc(railsrc) + if File.exist?(railsrc) + extra_args_string = File.read(railsrc) + extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten + puts "Using #{extra_args.join(" ")} from #{railsrc}" + argv.insert(1, *extra_args) + end + end + end end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 577ff651e5..adc83353b4 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -16,7 +16,7 @@ group :doc do end # Use ActiveModel has_secure_password -# gem 'bcrypt-ruby', '~> 3.0.0' +# gem 'bcrypt-ruby', '~> 3.1.0' # Use unicorn as the app server # gem 'unicorn' diff --git a/railties/lib/rails/generators/rails/generator/USAGE b/railties/lib/rails/generators/rails/generator/USAGE index d28eb3d7d8..799383050c 100644 --- a/railties/lib/rails/generators/rails/generator/USAGE +++ b/railties/lib/rails/generators/rails/generator/USAGE @@ -10,3 +10,4 @@ Example: lib/generators/awesome/awesome_generator.rb lib/generators/awesome/USAGE lib/generators/awesome/templates/ + test/lib/generators/awesome_generator_test.rb diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb index 9a7a516b5b..15d88f06ac 100644 --- a/railties/lib/rails/generators/rails/generator/generator_generator.rb +++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb @@ -10,6 +10,8 @@ module Rails directory '.', generator_dir end + hook_for :test_framework + protected def generator_dir diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index 73e89086a5..0e69aa101f 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -59,9 +59,9 @@ class <%= controller_class_name %>Controller < ApplicationController # Only allow a trusted parameter "white list" through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> - params[<%= ":#{singular_table_name}" %>] + params[:<%= singular_table_name %>] <%- else -%> - params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) <%- end -%> end end diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb new file mode 100644 index 0000000000..d7307398ce --- /dev/null +++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb @@ -0,0 +1,26 @@ +require 'rails/generators/test_unit' + +module TestUnit # :nodoc: + module Generators # :nodoc: + class GeneratorGenerator < Base # :nodoc: + check_class_collision suffix: "GeneratorTest" + + class_option :namespace, type: :boolean, default: true, + desc: "Namespace generator under lib/generators/name" + + def create_generator_files + template 'generator_test.rb', File.join('test/lib/generators', class_path, "#{file_name}_generator_test.rb") + end + + protected + + def generator_path + if options[:namespace] + File.join("generators", regular_class_path, file_name, "#{file_name}_generator") + else + File.join("generators", regular_class_path, "#{file_name}_generator") + end + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb new file mode 100644 index 0000000000..a7f1fc4fba --- /dev/null +++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' +require '<%= generator_path %>' + +<% module_namespacing do -%> +class <%= class_name %>GeneratorTest < Rails::Generators::TestCase + tests <%= class_name %>Generator + destination Rails.root.join('tmp/generators') + setup :prepare_destination + + # test "generator runs without errors" do + # assert_nothing_raised do + # run_generator ["arguments"] + # end + # end +end +<% end -%> diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 2cbb0a435c..290634290f 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -82,7 +82,7 @@ class SourceAnnotationExtractor case item when /\.(builder|rb|coffee|rake)$/ /#\s*(#{tag}):?\s*(.*)$/ - when /\.(css|scss|js)$/ + when /\.(css|scss|sass|js)$/ /\/\/\s*(#{tag}):?\s*(.*)$/ when /\.erb$/ /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 76a95304c3..bd4e7c33e0 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -1,6 +1,6 @@ namespace :rails do - desc "Update configs and some other initially generated files (or use just update:configs, update:bin, or update:application_controller)" - task update: [ "update:configs", "update:bin", "update:application_controller" ] + desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" + task update: [ "update:configs", "update:bin" ] desc "Applies the template supplied by LOCATION=(/path/to/template) or URL" task :template do @@ -62,15 +62,5 @@ namespace :rails do task :bin do invoke_from_app_generator :create_bin_files end - - # desc "Rename application.rb to application_controller.rb" - task :application_controller do - old_style = Rails.root + '/app/controllers/application.rb' - new_style = Rails.root + '/app/controllers/application_controller.rb' - if File.exists?(old_style) && !File.exists?(new_style) - FileUtils.mv(old_style, new_style) - puts "#{old_style} has been renamed to #{new_style}, update your SCM as necessary" - end - end end end diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index 4c4c80ecda..eb620caa00 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -232,8 +232,8 @@ </li> <li> - <h2>Create your database</h2> - <p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p> + <h2>Configure your database</h2> + <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p> </li> </ol> </div> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 739e4ad992..46f7466551 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -6,6 +6,7 @@ require 'active_support/testing/autorun' require 'active_support/test_case' require 'action_controller/test_case' require 'action_dispatch/testing/integration' +require 'rails/generators/test_case' # Config Rails backtrace in tests. require 'rails/backtrace_cleaner' diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 3aa33d1bba..547846c833 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -14,7 +14,7 @@ namespace :test do # Placeholder task for other Railtie and plugins to enhance. See Active Record for an example. end - task :run => ['test:units', 'test:functionals', 'test:integration'] + task :run => ['test:units', 'test:functionals', 'test:generators', 'test:integration'] # Inspired by: http://ngauthier.com/2012/02/quick-tests-with-bash.html desc "Run tests quickly by merging all types and not resetting db" @@ -35,6 +35,10 @@ namespace :test do end end + Rails::TestTask.new(generators: "test:prepare") do |t| + t.pattern = "test/lib/generators/**/*_test.rb" + end + Rails::TestTask.new(units: "test:prepare") do |t| t.pattern = 'test/{models,helpers,unit}/**/*_test.rb' end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 633d864dac..4de8fcaa38 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -91,7 +91,7 @@ module ApplicationTests class UsersController < ApplicationController; end eoruby app_file "app/models/user.rb", <<-eoruby - class User < ActiveRecord::Base; end + class User < ActiveRecord::Base; raise 'should not be reached'; end eoruby ENV['RAILS_ENV'] = 'production' diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 251fe02bc5..31a35a09bb 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -19,7 +19,7 @@ module ApplicationTests end test "default middleware stack" do - add_to_config "config.action_dispatch.x_sendfile_header = 'X-Sendfile'" + add_to_config "config.active_record.migration_error = :page_load" boot! @@ -37,6 +37,7 @@ module ApplicationTests "ActionDispatch::RemoteIp", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", + "ActiveRecord::Migration::CheckPending", "ActiveRecord::ConnectionAdapters::ConnectionManagement", "ActiveRecord::QueryCache", "ActionDispatch::Cookies", @@ -49,12 +50,6 @@ module ApplicationTests ], middleware end - test "Rack::Sendfile is not included by default" do - boot! - - assert !middleware.include?("Rack::Sendfile"), "Rack::Sendfile is not included in the default stack unless you set config.action_dispatch.x_sendfile_header" - end - test "Rack::Cache is not included by default" do boot! @@ -96,6 +91,7 @@ module ApplicationTests boot! assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement") assert !middleware.include?("ActiveRecord::QueryCache") + assert !middleware.include?("ActiveRecord::Migration::CheckPending") end test "removes lock if cache classes is set" do @@ -143,7 +139,7 @@ module ApplicationTests end test "insert middleware after" do - add_to_config "config.middleware.insert_after ActionDispatch::Static, Rack::Config" + add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config" boot! assert_equal "Rack::Config", middleware.second end @@ -151,16 +147,16 @@ module ApplicationTests test "Rails.cache does not respond to middleware" do add_to_config "config.cache_store = :memory_store" boot! - assert_equal "Rack::Runtime", middleware.third + assert_equal "Rack::Runtime", middleware.fourth end test "Rails.cache does respond to middleware" do boot! - assert_equal "Rack::Runtime", middleware.fourth + assert_equal "Rack::Runtime", middleware.fifth end test "insert middleware before" do - add_to_config "config.middleware.insert_before ActionDispatch::Static, Rack::Config" + add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config" boot! assert_equal "Rack::Config", middleware.first end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 3508f4225a..9a92c5f6ff 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -24,6 +24,7 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "// TODO: note in js" app_file "app/assets/stylesheets/application.css", "// TODO: note in css" app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss" + app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass" app_file "app/controllers/application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby" app_file "lib/tasks/task.rake", "# TODO: note in rake" @@ -46,9 +47,10 @@ module ApplicationTests assert_match(/note in js/, output) assert_match(/note in css/, output) assert_match(/note in scss/, output) + assert_match(/note in sass/, output) assert_match(/note in rake/, output) - assert_equal 9, lines.size + assert_equal 10, lines.size lines.each do |line| assert_equal 4, line[0].size diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 8e5310afee..c1cb1c1eba 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -9,7 +9,6 @@ module ApplicationTests def setup build_app boot_rails - FileUtils.rm_rf("#{app_path}/config/environments") end def teardown @@ -56,10 +55,8 @@ module ApplicationTests assert_match "Doing something...", output end - def test_does_not_explode_when_accessing_a_model_with_eager_load + def test_does_not_explode_when_accessing_a_model add_to_config <<-RUBY - config.eager_load = true - rake_tasks do task do_nothing: :environment do Hello.new.world @@ -67,33 +64,38 @@ module ApplicationTests end RUBY - app_file "app/models/hello.rb", <<-RUBY - class Hello - def world - puts "Hello world" + app_file 'app/models/hello.rb', <<-RUBY + class Hello + def world + puts 'Hello world' + end end - end RUBY - output = Dir.chdir(app_path){ `rake do_nothing` } - assert_match "Hello world", output + output = Dir.chdir(app_path) { `rake do_nothing` } + assert_match 'Hello world', output end - def test_should_not_eager_load_model_path_for_rake + def test_should_not_eager_load_model_for_rake add_to_config <<-RUBY - config.eager_load = true - rake_tasks do task do_nothing: :environment do end end RUBY - app_file "app/models/hello.rb", <<-RUBY - raise 'should not be pre-required for rake even `eager_load=true`' + add_to_env_config 'production', <<-RUBY + config.eager_load = true + RUBY + + app_file 'app/models/hello.rb', <<-RUBY + raise 'should not be pre-required for rake even eager_load=true' RUBY - Dir.chdir(app_path){ `rake do_nothing` } + Dir.chdir(app_path) do + assert system('rake do_nothing RAILS_ENV=production'), + 'should not be pre-required for rake even eager_load=true' + end end def test_code_statistics_sanity diff --git a/railties/test/generators/generator_generator_test.rb b/railties/test/generators/generator_generator_test.rb index f4c975fc18..41181aee85 100644 --- a/railties/test/generators/generator_generator_test.rb +++ b/railties/test/generators/generator_generator_test.rb @@ -16,6 +16,10 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase assert_file "lib/generators/awesome/awesome_generator.rb", /class AwesomeGenerator < Rails::Generators::NamedBase/ + assert_file "test/lib/generators/awesome_generator_test.rb", + /class AwesomeGeneratorTest < Rails::Generators::TestCase/ + assert_file "test/lib/generators/awesome_generator_test.rb", + /require 'generators\/awesome\/awesome_generator'/ end def test_namespaced_generator_skeleton @@ -29,6 +33,10 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase assert_file "lib/generators/rails/awesome/awesome_generator.rb", /class Rails::AwesomeGenerator < Rails::Generators::NamedBase/ + assert_file "test/lib/generators/rails/awesome_generator_test.rb", + /class Rails::AwesomeGeneratorTest < Rails::Generators::TestCase/ + assert_file "test/lib/generators/rails/awesome_generator_test.rb", + /require 'generators\/rails\/awesome\/awesome_generator'/ end def test_generator_skeleton_is_created_without_file_name_namespace @@ -42,6 +50,10 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase assert_file "lib/generators/awesome_generator.rb", /class AwesomeGenerator < Rails::Generators::NamedBase/ + assert_file "test/lib/generators/awesome_generator_test.rb", + /class AwesomeGeneratorTest < Rails::Generators::TestCase/ + assert_file "test/lib/generators/awesome_generator_test.rb", + /require 'generators\/awesome_generator'/ end def test_namespaced_generator_skeleton_without_file_name_namespace @@ -55,5 +67,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase assert_file "lib/generators/rails/awesome_generator.rb", /class Rails::AwesomeGenerator < Rails::Generators::NamedBase/ + assert_file "test/lib/generators/rails/awesome_generator_test.rb", + /class Rails::AwesomeGeneratorTest < Rails::Generators::TestCase/ + assert_file "test/lib/generators/rails/awesome_generator_test.rb", + /require 'generators\/rails\/awesome_generator'/ end end diff --git a/tasks/release.rb b/tasks/release.rb index 0c22f812fc..66da67bfd0 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,4 +1,4 @@ -FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionmailer railties ) +FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionview actionmailer railties ) root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip diff --git a/tools/profile b/tools/profile index 665efe049d..fbea67492b 100755 --- a/tools/profile +++ b/tools/profile @@ -4,7 +4,6 @@ ENV['NO_RELOAD'] ||= '1' ENV['RAILS_ENV'] ||= 'development' -Gem.source_index require 'benchmark' module RequireProfiler |