diff options
109 files changed, 1136 insertions, 355 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adc5eb6914..6d3cf072c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,14 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the team](http://contributors.rubyonrails.org)! -Please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide before submitting any code. +* If you want to submit a bug report please make sure to follow our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue). + +* If you want to submit a patch, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide. *We only accept bug reports and pull requests in GitHub*. -If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). +* If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). -If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. +* If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. Thanks! :heart: :heart: :heart: <br /> Rails Team diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 157a038b7c..b23d0668d3 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,24 @@ ## Rails 4.0.0 (unreleased) ## +* Integration and functional tests allow headers and rack env + variables to be passed when performing requests. + Fixes #6513. + + Example: + + # integration test + get "/success", {}, "HTTP_REFERER" => "http://test.com/", + "Accepts" => "text/plain, text/html" + + # functional test + @request.headers["Accepts"] = "text/plain, text/html" + + *Yves Senn* + +* Http::Headers respects headers that are not prefixed with HTTP_ + + *Yves Senn* + * Fix incorrectly appended square brackets to a multiple select box if an explicit name has been given and it already ends with "[]" diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 9d628c916f..fb664a69dd 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -98,6 +98,10 @@ module ActionController def merge_default_headers(original, default) Header.new self, super end + + def handle_conditional_get! + super unless committed? + end end def process(name) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 9dae78d25d..41b5228872 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -451,37 +451,55 @@ module ActionController end - # Executes a request simulating GET HTTP method and set/volley the response + # Simulate a GET request with the given parameters. + # + # - +action+: The controller action to call. + # - +parameters+: The HTTP parameters that you want to pass. This may + # be +nil+, a Hash, or a String that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). + # - +session+: A Hash of parameters to store in the session. This may be +nil+. + # - +flash+: A Hash of parameters to store in the flash. This may be +nil+. + # + # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with + # +#post+, +#patch+, +#put+, +#delete+, and +#head+. + # Note that the request method is not verified. The different methods are + # available to make the tests more expressive. def get(action, *args) process(action, "GET", *args) end - # Executes a request simulating POST HTTP method and set/volley the response + # Simulate a POST request with the given parameters and set/volley the response. + # See +#get+ for more details. def post(action, *args) process(action, "POST", *args) end - # Executes a request simulating PATCH HTTP method and set/volley the response + # Simulate a PATCH request with the given parameters and set/volley the response. + # See +#get+ for more details. def patch(action, *args) process(action, "PATCH", *args) end - # Executes a request simulating PUT HTTP method and set/volley the response + # Simulate a PUT request with the given parameters and set/volley the response. + # See +#get+ for more details. def put(action, *args) process(action, "PUT", *args) end - # Executes a request simulating DELETE HTTP method and set/volley the response + # Simulate a DELETE request with the given parameters and set/volley the response. + # See +#get+ for more details. def delete(action, *args) process(action, "DELETE", *args) end - # Executes a request simulating HEAD HTTP method and set/volley the response + # Simulate a HEAD request with the given parameters and set/volley the response. + # See +#get+ for more details. def head(action, *args) process(action, "HEAD", *args) end - # Executes a request simulating OPTIONS HTTP method and set/volley the response + # Simulate a OPTIONS request with the given parameters and set/volley the response. + # See +#get+ for more details. def options(action, *args) process(action, "OPTIONS", *args) end diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index dc04d4577b..2666cd4b0a 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,38 +1,62 @@ module ActionDispatch module Http class Headers + CGI_VARIABLES = %w( + CONTENT_TYPE CONTENT_LENGTH + HTTPS AUTH_TYPE GATEWAY_INTERFACE + PATH_INFO PATH_TRANSLATED QUERY_STRING + REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER + REQUEST_METHOD SCRIPT_NAME + SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE + ) + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ + include Enumerable + attr_reader :env def initialize(env = {}) - @headers = env + @env = env + end + + def [](key) + @env[env_name(key)] end - def [](header_name) - @headers[env_name(header_name)] + def []=(key, value) + @env[env_name(key)] = value end - def []=(k,v); @headers[k] = v; end - def key?(k); @headers.key? k; end + def key?(key); @env.key? key; end alias :include? :key? - def fetch(header_name, *args, &block) - @headers.fetch env_name(header_name), *args, &block + def fetch(key, *args, &block) + @env.fetch env_name(key), *args, &block end def each(&block) - @headers.each(&block) + @env.each(&block) end - private + def merge(headers_or_env) + headers = Http::Headers.new(env.dup) + headers.merge!(headers_or_env) + headers + end - # Converts a HTTP header name to an environment variable name if it is - # not contained within the headers hash. - def env_name(header_name) - @headers.include?(header_name) ? header_name : cgi_name(header_name) + def merge!(headers_or_env) + headers_or_env.each do |key, value| + self[env_name(key)] = value + end end - def cgi_name(k) - "HTTP_#{k.upcase.gsub(/-/, '_')}" + private + def env_name(key) + key = key.to_s + if key =~ HTTP_HEADER + key = key.upcase.tr('-', '_') + key = "HTTP_" + key unless CGI_VARIABLES.include?(key) + end + key end end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 446862aad0..246d9c121a 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -18,7 +18,7 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - encode_params(params).with_indifferent_access + params.with_indifferent_access end end alias :params :parameters @@ -50,40 +50,33 @@ module ActionDispatch private + # Convert nested Hash to HashWithIndifferentAccess + # and UTF-8 encode both keys and values in nested Hash. + # # TODO: Validate that the characters are UTF-8. If they aren't, # you'll get a weird error down the road, but our form handling # should really prevent that from happening - def encode_params(params) + def normalize_encode_params(params) if params.is_a?(String) return params.force_encoding(Encoding::UTF_8).encode! elsif !params.is_a?(Hash) return params end + new_hash = {} params.each do |k, v| - case v - when Hash - encode_params(v) - when Array - v.map! {|el| encode_params(el) } - else - encode_params(v) - end - end - end - - # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess - def normalize_parameters(value) - case value - when Hash - h = {} - value.each { |k, v| h[k] = normalize_parameters(v) } - h.with_indifferent_access - when Array - value.map { |e| normalize_parameters(e) } - else - value + new_key = k.is_a?(String) ? k.dup.force_encoding("UTF-8").encode! : k + new_hash[new_key] = + case v + when Hash + normalize_encode_params(v) + when Array + v.map! {|el| normalize_encode_params(el) } + else + normalize_encode_params(v) + end end + new_hash.with_indifferent_access end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7b04d6e851..aff2172788 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -156,14 +156,31 @@ module ActionDispatch @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end + # Returns the +String+ full path including params of the last URL requested. + # + # app.get "/articles" + # app.request.fullpath # => "/articles" + # + # app.get "/articles?page=2" + # app.request.fullpath # => "/articles?page=2" def fullpath @fullpath ||= super end + # Returns the original request URL as a +String+ + # + # app.get "/articles?page=2" + # app.request.original_url + # # => "http://www.example.com/articles?page=2" def original_url base_url + original_fullpath end + # The +String+ MIME type of the request + # + # app.get "/articles" + # app.request.media_type + # # => "application/x-www-form-urlencoded" def media_type content_mime_type.to_s end @@ -256,7 +273,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {}) + @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {}) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -264,7 +281,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {}) + @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {}) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 67cb7fbcb5..319d0197d1 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -75,16 +75,16 @@ module ActionDispatch end module Upload # :nodoc: - # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess and replace - # file upload hash with UploadedFile objects - def normalize_parameters(value) + # Replace file upload hash with UploadedFile objects + # when normalize and encode parameters. + def normalize_encode_params(value) if Hash === value && value.has_key?(:tempfile) UploadedFile.new(value) else super end end - private :normalize_parameters + private :normalize_encode_params end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index ed4e88aab6..56c31255f3 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -17,7 +17,7 @@ module ActionDispatch # a Hash, or a String that is appropriately encoded # (<tt>application/x-www-form-urlencoded</tt> or # <tt>multipart/form-data</tt>). - # - +headers+: Additional headers to pass, as a Hash. The headers will be + # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be # merged into the Rack env hash. # # This method returns a Response object, which one can use to @@ -28,44 +28,44 @@ module ActionDispatch # # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with # +#post+, +#patch+, +#put+, +#delete+, and +#head+. - def get(path, parameters = nil, headers = nil) - process :get, path, parameters, headers + def get(path, parameters = nil, headers_or_env = nil) + process :get, path, parameters, headers_or_env end # Performs a POST request with the given parameters. See +#get+ for more # details. - def post(path, parameters = nil, headers = nil) - process :post, path, parameters, headers + def post(path, parameters = nil, headers_or_env = nil) + process :post, path, parameters, headers_or_env end # Performs a PATCH request with the given parameters. See +#get+ for more # details. - def patch(path, parameters = nil, headers = nil) - process :patch, path, parameters, headers + def patch(path, parameters = nil, headers_or_env = nil) + process :patch, path, parameters, headers_or_env end # Performs a PUT request with the given parameters. See +#get+ for more # details. - def put(path, parameters = nil, headers = nil) - process :put, path, parameters, headers + def put(path, parameters = nil, headers_or_env = nil) + process :put, path, parameters, headers_or_env end # Performs a DELETE request with the given parameters. See +#get+ for # more details. - def delete(path, parameters = nil, headers = nil) - process :delete, path, parameters, headers + def delete(path, parameters = nil, headers_or_env = nil) + process :delete, path, parameters, headers_or_env end # Performs a HEAD request with the given parameters. See +#get+ for more # details. - def head(path, parameters = nil, headers = nil) - process :head, path, parameters, headers + def head(path, parameters = nil, headers_or_env = nil) + process :head, path, parameters, headers_or_env end # Performs a OPTIONS request with the given parameters. See +#get+ for # more details. - def options(path, parameters = nil, headers = nil) - process :options, path, parameters, headers + def options(path, parameters = nil, headers_or_env = nil) + process :options, path, parameters, headers_or_env end # Performs an XMLHttpRequest request with the given parameters, mirroring @@ -74,11 +74,11 @@ module ActionDispatch # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart # string; the headers are a hash. - def xml_http_request(request_method, path, parameters = nil, headers = nil) - headers ||= {} - headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, parameters, headers) + def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil) + headers_or_env ||= {} + headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + process(request_method, path, parameters, headers_or_env) end alias xhr :xml_http_request @@ -95,40 +95,40 @@ module ActionDispatch # redirect. Note that the redirects are followed until the response is # not a redirect--this means you may run into an infinite loop if your # redirect loops back to itself. - def request_via_redirect(http_method, path, parameters = nil, headers = nil) - process(http_method, path, parameters, headers) + def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil) + process(http_method, path, parameters, headers_or_env) follow_redirect! while redirect? status end # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def get_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:get, path, parameters, headers) + def get_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:get, path, parameters, headers_or_env) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def post_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:post, path, parameters, headers) + def post_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:post, path, parameters, headers_or_env) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def patch_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:patch, path, parameters, headers) + def patch_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:patch, path, parameters, headers_or_env) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def put_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:put, path, parameters, headers) + def put_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:put, path, parameters, headers_or_env) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def delete_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:delete, path, parameters, headers) + def delete_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:delete, path, parameters, headers_or_env) end end @@ -268,8 +268,7 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, parameters = nil, rack_env = nil) - rack_env ||= {} + def process(method, path, parameters = nil, headers_or_env = nil) if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -300,10 +299,12 @@ module ActionDispatch "CONTENT_TYPE" => "application/x-www-form-urlencoded", "HTTP_ACCEPT" => accept } + # this modifies the passed env directly + Http::Headers.new(env).merge!(headers_or_env || {}) session = Rack::Test::Session.new(_mock_session) - env.merge!(rack_env) + env.merge!(env) # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 3dae1fc87a..6b6a7edc1d 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -433,7 +433,7 @@ module ActionView builder = instantiate_builder(object_name, object, options) output = capture(builder, &block) - html_options[:multipart] = builder.multipart? + html_options[:multipart] ||= builder.multipart? form_tag(options[:url] || {}, html_options) { output } end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 720890eeb9..946db1df79 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -324,7 +324,8 @@ module ActionView end def locals_code #:nodoc: - @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join + # Double assign to suppress the dreaded 'assigned but unused variable' warning + @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join end def method_name #:nodoc: diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 72b882539c..c3bdf74d93 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -573,6 +573,21 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest def test_generate_url_without_controller assert_equal 'http://www.example.com/foo', url_for(:controller => "foo") end + + def test_pass_headers + get "/success", {}, "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" + + assert_equal "http://nohost.com", @request.env["HTTP_HOST"] + assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"] + end + + def test_pass_env + get "/success", {}, "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 3b1a07d7af..5755444a65 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -48,6 +48,10 @@ module ActionController end response.stream.close end + + def with_stale + render :text => 'stale' if stale?(:etag => "123") + end end tests TestController @@ -117,5 +121,16 @@ module ActionController assert_equal 'zomg', response.body assert response.stream.closed?, 'stream should be closed' end + + def test_stale_without_etag + get :with_stale + assert_equal 200, @response.status.to_i + end + + def test_stale_with_etag + @request.if_none_match = Digest::MD5.hexdigest("123") + get :with_stale + assert_equal 304, @response.status.to_i + end end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index df31338f09..38b9794b4d 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -57,6 +57,10 @@ class TestCaseTest < ActionController::TestCase render :text => request.protocol end + def test_headers + render text: request.headers.env.to_json + end + def test_html_output render :text => <<HTML <html> @@ -626,6 +630,24 @@ XML assert_equal 2004, page[:year] end + test "set additional HTTP headers" do + @request.headers['Referer'] = "http://nohost.com/home" + @request.headers['Content-Type'] = "application/rss+xml" + get :test_headers + parsed_env = JSON.parse(@response.body) + assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"] + assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"] + end + + test "set additional env variables" do + @request.headers['HTTP_REFERER'] = "http://example.com/about" + @request.headers['CONTENT_TYPE'] = "application/json" + get :test_headers + parsed_env = JSON.parse(@response.body) + assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"] + assert_equal "application/json", parsed_env["CONTENT_TYPE"] + end + def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new assert_kind_of String, @request.path_parameters['id'] diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index 42432510c3..9e37b96951 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -1,41 +1,137 @@ -require 'abstract_unit' +require "abstract_unit" class HeaderTest < ActiveSupport::TestCase - def setup + setup do @headers = ActionDispatch::Http::Headers.new( - "HTTP_CONTENT_TYPE" => "text/plain" + "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" ) end - def test_each + test "#new does not normalize the data" do + headers = ActionDispatch::Http::Headers.new( + "Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com") + + assert_equal({"Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com"}, headers.env) + end + + test "#env returns the headers as env variables" do + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#each iterates through the env variables" do headers = [] @headers.each { |pair| headers << pair } - assert_equal [["HTTP_CONTENT_TYPE", "text/plain"]], headers + assert_equal [["CONTENT_TYPE", "text/plain"], + ["HTTP_REFERER", "/some/page"]], headers + end + + test "set new headers" do + @headers["Host"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] + end + + test "headers can contain numbers" do + @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ==" + + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["Content-MD5"] + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["HTTP_CONTENT_MD5"] + end + + test "set new env variables" do + @headers["HTTP_HOST"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] end - def test_setter - @headers['foo'] = "bar" - assert_equal "bar", @headers['foo'] + test "key?" do + assert @headers.key?("CONTENT_TYPE") + assert @headers.include?("CONTENT_TYPE") end - def test_key? - assert @headers.key?('HTTP_CONTENT_TYPE') - assert @headers.include?('HTTP_CONTENT_TYPE') + test "fetch with block" do + assert_equal "omg", @headers.fetch("notthere") { "omg" } end - def test_fetch_with_block - assert_equal 'omg', @headers.fetch('notthere') { 'omg' } + test "accessing http header" do + assert_equal "/some/page", @headers["Referer"] + assert_equal "/some/page", @headers["referer"] + assert_equal "/some/page", @headers["HTTP_REFERER"] end - test "content type" do + test "accessing special header" do assert_equal "text/plain", @headers["Content-Type"] assert_equal "text/plain", @headers["content-type"] assert_equal "text/plain", @headers["CONTENT_TYPE"] - assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"] end test "fetch" do assert_equal "text/plain", @headers.fetch("content-type", nil) - assert_equal "not found", @headers.fetch('not-found', 'not found') + assert_equal "not found", @headers.fetch("not-found", "not found") + end + + test "#merge! headers with mutation" do + @headers.merge!("Host" => "http://example.test", + "Content-Type" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.test", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#merge! env with mutation" do + @headers.merge!("HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "merge without mutation" do + combined = @headers.merge("HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, combined.env) + + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "env variables with . are not modified" do + headers = ActionDispatch::Http::Headers.new + headers.merge! "rack.input" => "", + "rack.request.cookie_hash" => "", + "action_dispatch.logger" => "" + + assert_equal(["action_dispatch.logger", + "rack.input", + "rack.request.cookie_hash"], headers.env.keys.sort) + end + + test "symbols are treated as strings" do + headers = ActionDispatch::Http::Headers.new + headers.merge!(:SERVER_NAME => "example.com", + "HTTP_REFERER" => "/", + :Host => "test.com") + assert_equal "example.com", headers["SERVER_NAME"] + assert_equal "/", headers[:HTTP_REFERER] + assert_equal "test.com", headers["HTTP_HOST"] + end + + test "headers directly modifies the passed environment" do + env = {"HTTP_REFERER" => "/"} + headers = ActionDispatch::Http::Headers.new(env) + headers['Referer'] = "http://example.com/" + headers.merge! "CONTENT_TYPE" => "text/plain" + assert_equal({"HTTP_REFERER"=>"http://example.com/", + "CONTENT_TYPE"=>"text/plain"}, env) end end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 399f15199c..3c30a705e9 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -1,13 +1,15 @@ +# encoding: utf-8 require 'abstract_unit' class MultipartParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self - attr_accessor :last_request_parameters + attr_accessor :last_request_parameters, :last_parameters end def parse self.class.last_request_parameters = request.request_parameters + self.class.last_parameters = request.parameters head :ok end @@ -30,6 +32,23 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param')) end + test "parse single utf8 parameter" do + assert_equal({ 'Iñtërnâtiônà lizætiøn_name' => 'Iñtërnâtiônà lizætiøn_value'}, + parse_multipart('single_utf8_param'), "request.request_parameters") + assert_equal( + 'Iñtërnâtiônà lizætiøn_value', + TestController.last_parameters['Iñtërnâtiônà lizætiøn_name'], "request.parameters") + end + + test "parse bracketed utf8 parameter" do + assert_equal({ 'Iñtërnâtiônà lizætiøn_name' => { + 'Iñtërnâtiônà lizætiøn_nested_name' => 'Iñtërnâtiônà lizætiøn_value'} }, + parse_multipart('bracketed_utf8_param'), "request.request_parameters") + assert_equal( + {'Iñtërnâtiônà lizætiøn_nested_name' => 'Iñtërnâtiônà lizætiøn_value'}, + TestController.last_parameters['Iñtërnâtiônà lizætiøn_name'], "request.parameters") + end + test "parses text file" do params = parse_multipart('text_file') assert_equal %w(file foo), params.keys.sort diff --git a/actionpack/test/fixtures/multipart/bracketed_utf8_param b/actionpack/test/fixtures/multipart/bracketed_utf8_param new file mode 100644 index 0000000000..976ca44a45 --- /dev/null +++ b/actionpack/test/fixtures/multipart/bracketed_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônà lizætiøn_name[Iñtërnâtiônà lizætiøn_nested_name]"
+
+Iñtërnâtiônà lizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/single_utf8_param b/actionpack/test/fixtures/multipart/single_utf8_param new file mode 100644 index 0000000000..b86f62d1e1 --- /dev/null +++ b/actionpack/test/fixtures/multipart/single_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônà lizætiøn_name"
+
+Iñtërnâtiônà lizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 268bab6ad2..dff0b8bdc2 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -2791,8 +2791,8 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_html_options_adds_options_to_form_tag - form_for(@post, html: { id: 'some_form', class: 'some_class' }) do |f| end - expected = whole_form("/posts/123", "some_form", "some_class", method: "patch") + form_for(@post, html: { id: 'some_form', class: 'some_class', multipart: true }) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data") assert_dom_equal expected, output_buffer end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index c6d7b0b5d3..6ba0c7cd6b 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -111,7 +111,7 @@ * Changed `ActiveModel::Serializers::Xml::Serializer#add_associations` to by default propagate `:skip_types, :dasherize, :camelize` keys to included associations. - It can be overriden on each association by explicitly specifying the option on one + It can be overridden on each association by explicitly specifying the option on one or more associations *Anthony Alberto* diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index baaf842222..d3ec78157c 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -194,7 +194,7 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end - test 'acessing a suffixed attribute' do + test 'accessing a suffixed attribute' do m = ModelWithAttributes2.new m.attributes = { 'foo' => 'bar' } diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 38ba3cc152..aa683f4152 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -245,7 +245,7 @@ class NamingHelpersTest < ActiveModel::TestCase end def test_uncountable - assert uncountable?(@uncountable), "Expected 'sheep' to be uncoutable" + assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable" assert !uncountable?(@klass), "Expected 'contact' to be countable" end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index a9d32808da..d3723fa45b 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -213,7 +213,7 @@ class ValidationsTest < ActiveModel::TestCase assert_equal 'is too short (minimum is 2 characters)', t.errors[key][0] end - def test_validaton_with_if_and_on + def test_validation_with_if_and_on Topic.validates_presence_of :title, :if => Proc.new{|x| x.author_name = "bad"; true }, :on => :update t = Topic.new(:title => "") @@ -361,7 +361,7 @@ class ValidationsTest < ActiveModel::TestCase def test_dup_validity_is_independent Topic.validates_presence_of :title - topic = Topic.new("title" => "Litterature") + topic = Topic.new("title" => "Literature") topic.valid? duped = topic.dup diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 943c2dec16..e5ab6bac58 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,95 @@ ## Rails 4.0.0 (unreleased) ## +* `rake db:create` does not change permissions of the MySQL root user. + Fixes #8079. + + *Yves Senn* + +* The length of the `version` column in the `schema_migrations` table + created by the `mysql2` adapter is 191 if the encoding is "utf8mb4". + + The "utf8" encoding in MySQL has support for a maximum of 3 bytes per character, + and only contains characters from the BMP. The recently added + [utf8mb4](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html) + encoding extends the support to four bytes. As of this writing, said encoding + is supported in the betas of the `mysql2` gem. + + Setting the encoding to "utf8mb4" has + [a few implications](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-upgrading.html). + This change addresses the max length for indexes, which is 191 instead of 255. + + *Xavier Noria* + +* Counter caches on associations will now stay valid when attributes are + updated (not just when records are created or destroyed), for example, + when calling `update_attributes`. The following code now works: + + class Comment < ActiveRecord::Base + belongs_to :post, counter_cache: true + end + + class Post < ActiveRecord::Base + has_many :comments + end + + post = Post.create + comment = Comment.create + + post.comments << comment + post.save.reload.comments_count # => 1 + comment.update_attributes(post_id: nil) + + post.save.reload.comments_count # => 0 + + Updating the id of a `belongs_to` object with the id of a new object will + also keep the count accurate. + + *John Wang* + +* Referencing join tables implicitly was deprecated. There is a + possibility that these deprecation warnings are shown even if you + don't make use of that feature. You can now disable the feature entirely. + Fixes #9712. + + Example: + + # in your configuration + config.active_record.disable_implicit_join_references = true + + # or directly + ActiveRecord::Base.disable_implicit_join_references = true + + *Yves Senn* + +* The `:distinct` option for `Relation#count` is deprecated. You + should use `Relation#distinct` instead. + + Example: + + # Before + Post.select(:author_name).count(distinct: true) + + # After + Post.select(:author_name).distinct.count + + *Yves Senn* + +* Rename `Relation#uniq` to `Relation#distinct`. `#uniq` is still + available as an alias but we encourage to use `#distinct` instead. + Also `Relation#uniq_value` is aliased to `Relation#distinct_value`, + this is a temporary solution and you should migrate to `distinct_value`. + + *Yves Senn* + +* Fix quoting for sqlite migrations using copy_table_contents() with binary + columns. + + These would fail with "SQLite3::SQLException: unrecognized token" because + the column was not being passed to quote() so the data was not quoted + correctly. + + *Matthew M. Boedicker* + * Promotes `change_column_null` to the migrations API. This macro sets/removes `NOT NULL` constraints, and accepts an optional argument to replace existing `NULL`s if needed. The adapters for SQLite, MySQL, PostgreSQL, and (at least) @@ -116,7 +206,7 @@ *Aaron Weiner* -* Warn when `rake db:structure:dump` with a mysl database and +* Warn when `rake db:structure:dump` with a MySQL database and `mysqldump` is not in the PATH or fails. Fixes #9518. diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 89a62f0873..3e3475f709 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,6 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 4.0.0.beta1' + s.add_dependency 'arel', '~> 4.0.0.beta2' s.add_dependency 'activerecord-deprecated_finders', '~> 0.0.3' end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 06da5f2e0d..0c670bdaa1 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,6 +241,7 @@ module ActiveRecord # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X + # others.distinct | X | X | X # others.uniq | X | X | X # others.reset | X | X | X # diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index c5fb1fe2c7..a9525436fb 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -22,7 +22,7 @@ module ActiveRecord private def column_for(table_name, column_name) - columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns = alias_tracker.connection.schema_cache.columns_hash(table_name) columns[column_name] end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 97b1ff18e2..fbcb21118d 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -21,11 +21,13 @@ module ActiveRecord::Associations::Builder def add_counter_cache_callbacks(reflection) cache_column = reflection.counter_cache_column + foreign_key = reflection.foreign_key mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def belongs_to_counter_cache_after_create_for_#{name} record = #{name} record.class.increment_counter(:#{cache_column}, record.id) unless record.nil? + @_after_create_counter_called = true end def belongs_to_counter_cache_before_destroy_for_#{name} @@ -34,10 +36,28 @@ module ActiveRecord::Associations::Builder record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end + + def belongs_to_counter_cache_after_update_for_#{name} + if (@_after_create_counter_called ||= false) + @_after_create_counter_called = false + elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize}) + model = #{name.to_s.camelize} + foreign_key_was = self.#{foreign_key}_was + foreign_key = self.#{foreign_key} + + if foreign_key && model.respond_to?(:increment_counter) + model.increment_counter(:#{cache_column}, foreign_key) + end + if foreign_key_was && model.respond_to?(:decrement_counter) + model.decrement_counter(:#{cache_column}, foreign_key_was) + end + end + end CODE model.after_create "belongs_to_counter_cache_after_create_for_#{name}" model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" + model.after_update "belongs_to_counter_cache_after_update_for_#{name}" klass = reflection.class_name.safe_constantize klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index c992f51dbb..906560bd44 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -34,7 +34,7 @@ module ActiveRecord reload end - CollectionProxy.new(klass, self) + @proxy ||= CollectionProxy.new(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -174,13 +174,14 @@ module ActiveRecord reflection.klass.count_by_sql(custom_counter_sql) else - if association_scope.uniq_value + relation = scope + if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options[:distinct] = true + relation = relation.distinct end - value = scope.count(column_name, count_options) + value = relation.count(column_name) limit = options[:limit] offset = options[:offset] @@ -246,14 +247,14 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.uniq_value + if association_scope.distinct_value target.uniq.size else target.size end elsif !loaded? && !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array) + elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else @@ -306,12 +307,13 @@ module ActiveRecord end end - def uniq + def distinct seen = {} load_target.find_all do |record| seen[record.id] = true unless seen.key?(record.id) end end + alias uniq distinct # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. @@ -352,7 +354,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? - if association_scope.uniq_value && index = @target.index(record) + if association_scope.distinct_value && index = @target.index(record) @target[index] = record else @target << record diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 543204abac..c2add32aa6 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -649,11 +649,12 @@ module ActiveRecord # # #<Pet name: "Fancy-Fancy"> # # ] # - # person.pets.select(:name).uniq + # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def uniq - @association.uniq + def distinct + @association.distinct end + alias uniq distinct # Count all records using SQL. # diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index f59565ae77..b7b4d7e3ae 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -23,9 +23,10 @@ module ActiveRecord if options[:dependent] == :destroy # No point in executing the counter update since we're going to destroy the parent anyway load_target.each(&:mark_for_destruction) + destroy_all + else + delete_all end - - delete_all end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 9a662d3f53..38bc7ce7da 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -6,7 +6,7 @@ module ActiveRecord def associated_records_by_owner super.each do |owner, records| - records.uniq! if reflection_scope.uniq_value + records.uniq! if reflection_scope.distinct_value end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 3e454b713a..931209b07b 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -90,7 +90,7 @@ module ActiveRecord base_name.foreign_key else if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys[table_name] + connection.schema_cache.primary_keys(table_name) else 'id' end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 42206de8fc..902dbd148e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -16,7 +16,6 @@ module ActiveRecord # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: - def string_to_binary(value) value end @@ -25,19 +24,24 @@ module ActiveRecord base.type_to_sql(type.to_sym, limit, precision, scale) end + def primary_key? + type.to_sym == :primary_key + end + def to_sql column_sql = "#{base.quote_column_name(name)} #{sql_type}" column_options = {} column_options[:null] = null unless null.nil? column_options[:default] = default unless default.nil? - add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key + column_options[:column] = self + add_column_options!(column_sql, column_options) unless primary_key? column_sql end private def add_column_options!(sql, options) - base.add_column_options!(sql, options.merge(:column => self)) + base.add_column_options!(sql, options) end end @@ -73,15 +77,6 @@ module ActiveRecord @base = base end - def xml(*args) - raise NotImplementedError unless %w{ - sqlite mysql mysql2 - }.include? @base.adapter_name.downcase - - options = args.extract_options! - column(args[0], :text, options) - end - # Appends a primary key definition to the table definition. # Can be called multiple times, but this is probably not a good idea. def primary_key(name) @@ -292,19 +287,23 @@ module ActiveRecord # concatenated together. This string can then be prepended and appended to # to generate the final SQL to create the table. def to_sql - @columns.map { |c| c.to_sql } * ', ' + columns.map { |c| c.to_sql } * ', ' end private + def create_column_definition(base, name, type) + ColumnDefinition.new base, name, type + end + def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type + definition = create_column_definition base, name, type @columns << definition @columns_hash[name] = definition definition end def primary_key_column_name - primary_key_column = columns.detect { |c| c.type == :primary_key } + primary_key_column = columns.detect { |c| c.primary_key? } primary_key_column && primary_key_column.name end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 4f670d46d9..cd4409295f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -172,7 +172,14 @@ module ActiveRecord # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) td = create_table_definition - td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + + unless options[:id] == false + pk = options.fetch(:primary_key) { + Base.get_primary_key table_name.to_s.singularize + } + + td.primary_key pk + end yield td if block_given? @@ -403,7 +410,7 @@ module ActiveRecord end # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag - # indicates wheter the value can be +NULL+. For example + # indicates whether the value can be +NULL+. For example # # change_column_null(:users, :nickname, false) # diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 20a5ca2baa..25b8aef617 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -38,6 +38,15 @@ module ActiveRecord configure_connection end + MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 + def initialize_schema_migrations_table + if @config[:encoding] == 'utf8mb4' + ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) + else + ActiveRecord::SchemaMigration.create_table + end + end + def supports_explain? true end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 47e2e3928f..43f991b362 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -51,9 +51,12 @@ module ActiveRecord super end when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" + if column.sql_type == 'money' || [:string, :text].include?(column.type) + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + else + super + end when String case column.sql_type when 'bytea' then "'#{escape_bytea(value)}'" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index cfcc783904..dfa4c3967a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -338,13 +338,14 @@ module ActiveRecord self end + def xml(options = {}) + column(args[0], :text, options) + end + private - def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type - @columns << definition - @columns_hash[name] = definition - definition + def create_column_definition(base, name, type) + ColumnDefinition.new base, name, type end end @@ -600,7 +601,7 @@ module ActiveRecord end def disable_extension(name) - exec_query("DROP EXTENSION IF EXISTS #{name} CASCADE").tap { + exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap { reload_type_map } end @@ -631,7 +632,13 @@ module ActiveRecord if options[:array] || options[:column].try(:array) sql << '[]' end - super + + column = options.fetch(:column) { return super } + if column.type == :uuid && options[:default] =~ /\(\)/ + sql << " DEFAULT #{options[:default]}" + else + super + end end # Set the authorized user for this session diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 5839d1d3b4..1d7a22e831 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,7 +1,9 @@ +require 'active_support/deprecation/reporting' + module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :primary_keys, :tables, :version + attr_reader :version attr_accessor :connection def initialize(conn) @@ -14,6 +16,15 @@ module ActiveRecord prepare_default_proc end + def primary_keys(table_name = nil) + if table_name + @primary_keys[table_name] + else + ActiveSupport::Deprecation.warn('call primary_keys with a table name!') + @primary_keys.dup + end + end + # A cached lookup for table existence. def table_exists?(name) return @tables[name] if @tables.key? name @@ -30,12 +41,22 @@ module ActiveRecord end end + def tables(name = nil) + if name + @tables[name] + else + ActiveSupport::Deprecation.warn('call tables with a name!') + @tables.dup + end + end + # Get the columns for a table def columns(table = nil) if table @columns[table] else - @columns + ActiveSupport::Deprecation.warn('call columns with a table name!') + @columns.dup end end @@ -45,7 +66,8 @@ module ActiveRecord if table @columns_hash[table] else - @columns_hash + ActiveSupport::Deprecation.warn('call columns_hash with a table name!') + @columns_hash.dup end end @@ -58,6 +80,12 @@ module ActiveRecord @version = nil end + def size + [@columns, @columns_hash, @primary_keys, @tables].map { |x| + x.size + }.inject :+ + end + # Clear out internal caches for table with +table_name+. def clear_table_cache!(table_name) @columns.delete table_name @@ -69,9 +97,9 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val| - self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } - end + [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| + Hash[val] + } end def marshal_load(array) @@ -87,7 +115,7 @@ module ActiveRecord end @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| + h[table_name] = Hash[columns(table_name).map { |col| [col.name, col] }] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 84fa1c7d5a..d3ffee3a8b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -583,9 +583,17 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) + + raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + + column_values = columns.map do |col| + quote(row[column_mappings[col]], raw_column_mappings[col]) + end + + sql << column_values * ', ' sql << ')' exec_query sql end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 72371be657..aa56219755 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -69,6 +69,14 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true + ## + # :singleton-method: + # Disable implicit join references. This feature was deprecated with Rails 4. + # If you don't make use of implicit references but still see deprecation warnings + # you can disable the feature entirely. This will be the default with Rails 4.1. + mattr_accessor :disable_implicit_join_references, instance_writer: false + self.disable_implicit_join_references = false + class_attribute :connection_handler, instance_writer: false self.connection_handler = ConnectionAdapters::ConnectionHandler.new end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 81f92db271..81cca37939 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset + # * +counters+ - One or more association counters to reset # # ==== Examples # @@ -21,6 +21,7 @@ module ActiveRecord object = find(id) counters.each do |association| has_many_association = reflect_on_association(association.to_sym) + raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 85fb4be992..ac2d2f2712 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -205,7 +205,7 @@ module ActiveRecord # Returns an array of column objects for the table associated with this class. def columns - @columns ||= connection.schema_cache.columns[table_name].map do |col| + @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index e04a3d0976..902fd90c54 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord delegate :find_each, :find_in_batches, :to => :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :references, :none, :to => :all + :having, :create_with, :uniq, :distinct, :references, :none, :to => :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all # Executes a custom SQL query against your database and returns all the results. The results will diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 13f3bf7085..99117b74c5 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -49,7 +49,7 @@ module ActiveRecord Rails.logger.extend ActiveSupport::Logger.broadcast console end - runner do |app| + runner do require "active_record/base" end @@ -64,7 +64,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end - initializer "active_record.migration_error" do |app| + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending" @@ -158,7 +158,7 @@ module ActiveRecord end # Expose database runtime to controller for logging. - initializer "active_record.log_runtime" do |app| + initializer "active_record.log_runtime" do require "active_record/railties/controller_runtime" ActiveSupport.on_load(:action_controller) do include ActiveRecord::Railties::ControllerRuntime diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ad54ba55b6..037097d2dd 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,7 @@ module ActiveRecord :extending] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, - :reverse_order, :uniq, :create_with] + :reverse_order, :distinct, :create_with] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS @@ -506,6 +506,12 @@ module ActiveRecord includes_values & joins_values end + # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ + # to maintain backwards compatibility. Use +distinct_value+ instead. + def uniq_value + distinct_value + end + # Compares two relations for equality. def ==(other) case other @@ -589,7 +595,8 @@ module ActiveRecord if (references_values - joined_tables).any? true - elsif (string_tables - joined_tables).any? + elsif !ActiveRecord::Base.disable_implicit_join_references && + (string_tables - joined_tables).any? ActiveSupport::Deprecation.warn( "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ "that are referenced in a string SQL snippet. For example: \n" \ @@ -603,7 +610,10 @@ module ActiveRecord "From now on, you must explicitly tell Active Record when you are referencing a table " \ "from a string:\n" \ "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" + " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \ + "\n" \ + "If you don't rely on implicit join references you can disable the feature entirely" \ + "by setting `config.active_record.disable_implicit_join_references = true`." ) true else diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7f95181c67..be011b22af 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -11,7 +11,7 @@ module ActiveRecord # Person.count(:all) # # => performs a COUNT(*) (:all is an alias for '*') # - # Person.count(:age, distinct: true) + # Person.distinct.count(:age) # # => counts the number of different age values # # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, @@ -198,8 +198,13 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase - # If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count) - distinct = options[:distinct] || self.uniq_value + # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + distinct = self.distinct_value + if options.has_key?(:distinct) + ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \ + "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)" + distinct = options[:distinct] + end if operation == "count" column_name ||= (select_for_count || :all) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b7960936cf..10a31109d5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -710,20 +710,22 @@ module ActiveRecord # User.select(:name) # # => Might return two records with the same name # - # User.select(:name).uniq - # # => Returns 1 record per unique name + # User.select(:name).distinct + # # => Returns 1 record per distinct name # - # User.select(:name).uniq.uniq(false) + # User.select(:name).distinct.distinct(false) # # => You can also remove the uniqueness - def uniq(value = true) - spawn.uniq!(value) + def distinct(value = true) + spawn.distinct!(value) end + alias uniq distinct - # Like #uniq, but modifies relation in place. - def uniq!(value = true) # :nodoc: - self.uniq_value = value + # Like #distinct, but modifies relation in place. + def distinct!(value = true) # :nodoc: + self.distinct_value = value self end + alias uniq! distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -814,7 +816,7 @@ module ActiveRecord build_select(arel, select_values.uniq) - arel.distinct(uniq_value) + arel.distinct(distinct_value) arel.from(build_from) if from_value arel.lock(lock_value) if lock_value diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 9830abe7d8..6077144265 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -13,18 +13,21 @@ module ActiveRecord "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" end - def self.create_table + def self.create_table(limit=nil) unless connection.table_exists?(table_name) - connection.create_table(table_name, :id => false) do |t| - t.column :version, :string, :null => false + version_options = {null: false} + version_options[:limit] = limit if limit + + connection.create_table(table_name, id: false) do |t| + t.column :version, :string, version_options end - connection.add_index table_name, :version, :unique => true, :name => index_name + connection.add_index table_name, :version, unique: true, name: index_name end end def self.drop_table if connection.table_exists?(table_name) - connection.remove_index table_name, :name => index_name + connection.remove_index table_name, name: index_name connection.drop_table(table_name) end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 10696258c9..50569d2462 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -26,7 +26,9 @@ module ActiveRecord $stdout.print error.error establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options - connection.execute grant_statement.gsub(/\s+/, ' ').strip + if configuration['username'] != 'root' + connection.execute grant_statement.gsub(/\s+/, ' ').strip + end establish_connection configuration else $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index 3a3cf86d73..fd94a2d038 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -2,8 +2,12 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> +<% end -%> <% if options[:timestamps] %> t.timestamps <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 056f55470c..808598699b 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,10 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> +<% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> <% end -%> +<% if attributes.any?(&:password_digest?) -%> + has_secure_password +<% end -%> end <% end -%> diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 98596a7f62..e76617b845 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -84,7 +84,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { x.save } assert_nothing_raised { Group.find_by_order('y') } assert_nothing_raised { Group.find(1) } - x = Group.find(1) end # has_one association with reserved-word table name diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb new file mode 100644 index 0000000000..9ecd901eac --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter + class SchemaMigrationsTest < ActiveRecord::TestCase + def test_initializes_schema_migrations_for_encoding_utf8mb4 + conn = ActiveRecord::Base.connection + + smtn = ActiveRecord::Migrator.schema_migrations_table_name + conn.drop_table(smtn) if conn.table_exists?(smtn) + + config = conn.instance_variable_get(:@config) + original_encoding = config[:encoding] + + config[:encoding] = 'utf8mb4' + conn.initialize_schema_migrations_table + + assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) + ensure + config[:encoding] = original_encoding + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 685f0ea74f..b3429648ee 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -44,6 +44,14 @@ module ActiveRecord c = Column.new(nil, 1, 'float') assert_equal "'Infinity'", @conn.quote(infinity, c) end + + def test_quote_cast_numeric + fixnum = 666 + c = Column.new(nil, nil, 'string') + assert_equal "'666'", @conn.quote(fixnum, c) + c = Column.new(nil, nil, 'text') + assert_equal "'666'", @conn.quote(fixnum, c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb new file mode 100644 index 0000000000..53002c5265 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -0,0 +1,43 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlUUIDTest < ActiveRecord::TestCase + class UUID < ActiveRecord::Base + self.table_name = 'pg_uuids' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip "do not test on PG without uuid-ossp" + end + + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.transaction do + @connection.create_table('pg_uuids', id: :uuid) do |t| + t.string 'name' + t.uuid 'other_uuid', default: 'uuid_generate_v4()' + end + end + end + + def teardown + @connection.execute 'drop table if exists pg_uuids' + end + + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index d03d1dd94c..21fb111c91 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CopyTableTest < ActiveRecord::TestCase - fixtures :customers, :companies, :comments + fixtures :customers, :companies, :comments, :binaries def setup @connection = ActiveRecord::Base.connection @@ -72,6 +72,10 @@ class CopyTableTest < ActiveRecord::TestCase end end + def test_copy_table_with_binary_column + test_copy_table 'binaries', 'binaries2' + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 80bca7f63e..e693d34f99 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -55,7 +55,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_nothing_raised do assert_equal 4, categories.count assert_equal 4, categories.to_a.count - assert_equal 3, categories.count(:distinct => true) + assert_equal 3, categories.distinct.count assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes end end 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 1b1b479f1a..84bdca3a97 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 @@ -316,7 +316,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev.projects << projects(:active_record) assert_equal 3, dev.projects.size - assert_equal 1, dev.projects.uniq.size + assert_equal 1, dev.projects.distinct.size end def test_uniq_before_the_fact diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 1ddd380f23..781b87741d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -789,6 +789,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_calling_update_attributes_on_id_changes_the_counter_cache + topic = Topic.order("id ASC").first + original_count = topic.replies.to_a.size + assert_equal original_count, topic.replies_count + + first_reply = topic.replies.first + first_reply.update_attributes(:parent_id => nil) + assert_equal original_count - 1, topic.reload.replies_count + + first_reply.update_attributes(:parent_id => topic.id) + assert_equal original_count, topic.reload.replies_count + end + + def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache + topic1 = Topic.find(1) + topic2 = Topic.find(3) + original_count1 = topic1.replies.to_a.size + original_count2 = topic2.replies.to_a.size + + reply1 = topic1.replies.first + reply2 = topic2.replies.first + + reply1.update_attributes(:parent_id => topic2.id) + assert_equal original_count1 - 1, topic1.reload.replies_count + assert_equal original_count2 + 1, topic2.reload.replies_count + + reply2.update_attributes(:parent_id => topic1.id) + assert_equal original_count1, topic1.reload.replies_count + assert_equal original_count2, topic2.reload.replies_count + end + def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 4f246f575e..918783e8f1 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -82,7 +82,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length - authors_with_welcoming_post_titles = Author.all.merge!(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) + authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id') assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 10ec33be75..c05481dd91 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -397,14 +397,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_many - assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id } + assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id } end def test_include_has_many_through_polymorphic_has_many author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } end end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index e355ed3495..e75d43bda8 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -410,7 +410,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Mary and Bob both have posts in misc, but they are the only ones. authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id) - assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id) + assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id) # Check the polymorphism of taggings is being observed correctly (in both joins) authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel') diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 201fa5d5a9..a06bacafca 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -95,7 +95,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload firm = Firm.new("name" => "A New Firm, Inc") firm.save - firm.clients.each {|c|} # forcing to load all clients + firm.clients.each {} # forcing to load all clients assert firm.clients.empty?, "New firm shouldn't have client objects" assert_equal 0, firm.clients.size, "New firm should have 0 clients" @@ -237,6 +237,11 @@ class AssociationProxyTest < ActiveRecord::TestCase assert david.projects.scope.is_a?(ActiveRecord::Relation) assert_equal david.projects, david.projects.scope end + + test "proxy object is cached" do + david = developers(:david) + assert david.projects.equal?(david.projects) + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index aa09df5743..960c27da3e 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1105,7 +1105,7 @@ class BasicsTest < ActiveRecord::TestCase res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count end assert_equal res6, res7 end @@ -1358,9 +1358,9 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache - c1 = Post.connection.schema_cache.columns['posts'] + c1 = Post.connection.schema_cache.columns('posts') ActiveRecord::Base.clear_cache! - c2 = Post.connection.schema_cache.columns['posts'] + c2 = Post.connection.schema_cache.columns('posts') assert_not_equal c1, c2 end @@ -1499,6 +1499,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal scope, Bird.uniq end + def test_distinct_delegates_to_scoped + scope = stub + Bird.stubs(:all).returns(mock(:distinct => scope)) + assert_equal scope, Bird.distinct + end + def test_table_name_with_2_abstract_subclasses assert_equal "photos", Photo.table_name end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index be49e948fc..c645523905 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -305,8 +305,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_selected_field_with_include - assert_equal 6, Account.includes(:firm).count(:distinct => true) - assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) + assert_equal 6, Account.includes(:firm).distinct.count + assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count end def test_should_not_perform_joined_include_by_default @@ -341,7 +341,18 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 5, Account.count(:firm_id) end - def test_count_with_uniq + def test_count_distinct_option_is_deprecated + assert_deprecated do + assert_equal 4, Account.select(:credit_limit).count(distinct: true) + end + + assert_deprecated do + assert_equal 6, Account.select(:credit_limit).count(distinct: false) + end + end + + def test_count_with_distinct + assert_equal 4, Account.select(:credit_limit).distinct.count assert_equal 4, Account.select(:credit_limit).uniq.count end @@ -351,7 +362,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_field_in_joined_table assert_equal 5, Account.joins(:firm).count('companies.id') - assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) + assert_equal 4, Account.joins(:firm).distinct.count('companies.id') end def test_should_count_field_in_joined_table_with_group_by @@ -455,7 +466,7 @@ class CalculationsTest < ActiveRecord::TestCase approved_topics_count = Topic.group(:approved).count(:author_name)[true] assert_equal approved_topics_count, 3 # Count the number of distinct authors for approved Topics - distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] + distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true] assert_equal distinct_authors_for_approved_count, 2 end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 541e983758..ecad7c942f 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -9,49 +9,46 @@ module ActiveRecord end def test_primary_key - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 'id', @cache.primary_keys('posts') end def test_primary_key_for_non_existent_table - assert_nil @cache.primary_keys['omgponies'] + assert_nil @cache.primary_keys('omgponies') end def test_caches_columns - columns = @cache.columns['posts'] - assert_equal columns, @cache.columns['posts'] + columns = @cache.columns('posts') + assert_equal columns, @cache.columns('posts') end def test_caches_columns_hash - columns_hash = @cache.columns_hash['posts'] - assert_equal columns_hash, @cache.columns_hash['posts'] + columns_hash = @cache.columns_hash('posts') + assert_equal columns_hash, @cache.columns_hash('posts') end def test_clearing - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache.clear! - assert_equal 0, @cache.columns.size - assert_equal 0, @cache.columns_hash.size - assert_equal 0, @cache.tables.size - assert_equal 0, @cache.primary_keys.size + assert_equal 0, @cache.size end def test_dump_and_load - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns['posts'].size - assert_equal 12, @cache.columns_hash['posts'].size - assert @cache.tables['posts'] - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 12, @cache.columns('posts').size + assert_equal 12, @cache.columns_hash('posts').size + assert @cache.tables('posts') + assert_equal 'id', @cache.primary_keys('posts') end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 23e64bee7e..e6af29282c 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -185,7 +185,7 @@ module ActiveRecord assert_not_nil connection threads = [] 4.times do |i| - threads << Thread.new(i) do |pool_count| + threads << Thread.new(i) do connection = pool.connection assert_not_nil connection connection.close diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index fc46a249c8..7d06fb5093 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -118,7 +118,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "reset the right counter if two have the same foreign key" do michael = people(:michael) assert_nothing_raised(ActiveRecord::StatementInvalid) do - Person.reset_counters(michael.id, :followers) + Person.reset_counters(michael.id, :friends_too) end end @@ -131,4 +131,11 @@ class CounterCacheTest < ActiveRecord::TestCase Subscriber.reset_counters(subscriber.id, 'books') end end + + test "the passed symbol needs to be an association name" do + e = assert_raises(ArgumentError) do + Topic.reset_counters(@topic.id, :replies_count) + end + assert_equal "'Topic' has no association called 'replies_count'", e.message + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index a9fa107749..e505fe9f18 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -82,7 +82,7 @@ class FinderTest < ActiveRecord::TestCase # ensures +exists?+ runs valid SQL by excluding order value def test_exists_with_order - assert Topic.order(:id).uniq.exists? + assert Topic.order(:id).distinct.exists? end def test_exists_with_includes_limit_and_empty_result diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fd0b05cb77..9ca980fdf6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -278,5 +278,17 @@ module ActiveRecord assert_equal [NullRelation], relation.extending_values assert relation.is_a?(NullRelation) end + + test "distinct!" do + relation.distinct! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + + test "uniq! was replaced by distinct!" do + relation.uniq! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 26cbb03892..9008c2785e 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -492,6 +492,7 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } end @@ -789,11 +790,11 @@ class RelationTest < ActiveRecord::TestCase def test_count_with_distinct posts = Post.all - assert_equal 3, posts.count(:comments_count, :distinct => true) - assert_equal 11, posts.count(:comments_count, :distinct => false) + assert_equal 3, posts.distinct(true).count(:comments_count) + assert_equal 11, posts.distinct(false).count(:comments_count) - assert_equal 3, posts.select(:comments_count).count(:distinct => true) - assert_equal 11, posts.select(:comments_count).count(:distinct => false) + assert_equal 3, posts.distinct(true).select(:comments_count).count + assert_equal 11, posts.distinct(false).select(:comments_count).count end def test_count_explicit_columns @@ -1223,6 +1224,16 @@ class RelationTest < ActiveRecord::TestCase end end + def test_turn_off_eager_loading_with_conditions_on_joins + original_value = ActiveRecord::Base.disable_implicit_join_references + ActiveRecord::Base.disable_implicit_join_references = true + + scope = Topic.where(author_email_address: 'my.example@gmail.com').includes(:replies) + assert_not scope.eager_loading? + ensure + ActiveRecord::Base.disable_implicit_join_references = original_value + end + def test_ordering_with_extra_spaces assert_equal authors(:david), Author.order('id DESC , name DESC').last end @@ -1269,7 +1280,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end - def test_uniq + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1277,11 +1288,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct.map(&:name) assert_equal ['Foo'], query.uniq.map(&:name) end assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct(true).map(&:name) assert_equal ['Foo'], query.uniq(true).map(&:name) end + assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index dadcca5b7f..816bd62751 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -71,7 +71,7 @@ module ActiveRecord return skip("only tested on mysql") end - @connection = stub(:create_database => true, :execute => true) + @connection = stub("Connection", create_database: true) @error = Mysql::Error.new "Invalid permissions" @configuration = { 'adapter' => 'mysql', @@ -90,6 +90,7 @@ module ActiveRecord end def test_root_password_is_requested + assert_permissions_granted_for "pat" skip "only if mysql is available" unless defined?(::Mysql) $stdin.expects(:gets).returns("secret\n") @@ -97,6 +98,7 @@ module ActiveRecord end def test_connection_established_as_root + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', 'database' => nil, @@ -108,6 +110,7 @@ module ActiveRecord end def test_database_created_by_root + assert_permissions_granted_for "pat" @connection.expects(:create_database). with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') @@ -115,12 +118,18 @@ module ActiveRecord end def test_grant_privileges_for_normal_user - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;") + assert_permissions_granted_for "pat" + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' ActiveRecord::Tasks::DatabaseTasks.create @configuration end def test_connection_established_as_normal_user + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).returns do ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', @@ -142,6 +151,13 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end + + private + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + end end class MySQLDBDropTest < ActiveRecord::TestCase diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml index 1ee09175bf..ae0abe0162 100644 --- a/activerecord/test/fixtures/friendships.yml +++ b/activerecord/test/fixtures/friendships.yml @@ -1,4 +1,4 @@ Connection 1: id: 1 - person_id: 1 - friend_id: 2
\ No newline at end of file + friend_id: 1 + follower_id: 2 diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index e640a38f1f..0ec05e8d56 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -5,6 +5,7 @@ michael: number1_fan_id: 3 gender: M followers_count: 1 + friends_too_count: 1 david: id: 2 first_name: David @@ -12,6 +13,7 @@ david: number1_fan_id: 1 gender: M followers_count: 1 + friends_too_count: 1 susan: id: 3 first_name: Susan @@ -19,3 +21,4 @@ susan: number1_fan_id: 1 gender: F followers_count: 1 + friends_too_count: 1 diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8423411474..a96899ae10 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -30,8 +30,8 @@ class Author < ActiveRecord::Base has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments has_many :funky_comments, :through => :posts, :source => :comments - has_many :ordered_uniq_comments, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments - has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments has_many :special_posts @@ -78,7 +78,7 @@ class Author < ActiveRecord::Base has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category' has_many :categorized_posts, :through => :categorizations, :source => :post - has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post + has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post has_many :nothings, :through => :kateggorisatons, :class_name => 'Category' @@ -91,7 +91,7 @@ class Author < ActiveRecord::Base has_many :post_categories, :through => :posts, :source => :categories has_many :tagging_tags, :through => :taggings, :source => :tag - has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts + has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags has_many :tags_with_primary_key, :through => :posts diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index ce81a37966..5458a28cc9 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,7 +2,7 @@ class Book < ActiveRecord::Base has_many :authors has_many :citations, :foreign_key => 'book1_id' - has_many :references, -> { uniq }, :through => :citations, :source => :reference_of + has_many :references, -> { distinct }, :through => :citations, :source => :reference_of has_many :subscriptions has_many :subscribers, :through => :subscriptions diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb index 6b4f7acc38..4b411ca8e0 100644 --- a/activerecord/test/models/friendship.rb +++ b/activerecord/test/models/friendship.rb @@ -1,4 +1,6 @@ class Friendship < ActiveRecord::Base belongs_to :friend, class_name: 'Person' - belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count + # friend_too exists to test a bug, and probably shouldn't be used elsewhere + belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count + belongs_to :follower, class_name: 'Person' end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 6cfd443e75..69d4d7df1a 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,4 @@ class Liquid < ActiveRecord::Base self.table_name = :liquid - has_many :molecules, -> { uniq } + has_many :molecules, -> { distinct } end - diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index fa717ef8d6..2985160d28 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -8,7 +8,10 @@ class Person < ActiveRecord::Base has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) }, :through => :readers, :source => :post - has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :friendships, foreign_key: 'friend_id' + # friends_too exists to test a bug, and probably shouldn't be used elsewhere + has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :followers, through: :friendships has_many :references has_many :bad_references diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 90273adafc..f893754b9f 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,11 +1,11 @@ class Project < ActiveRecord::Base - has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' } + has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' } has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer" - has_and_belongs_to_many :selected_developers, -> { uniq.select "developers.*" }, :class_name => "Developer" + has_and_belongs_to_many :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer" has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer' has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer" - has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer" - has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer" has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer" ActiveSupport::Deprecation.silence do diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ae779c702a..a952738e84 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -280,7 +280,7 @@ ActiveRecord::Schema.define do create_table :friendships, :force => true do |t| t.integer :friend_id - t.integer :person_id + t.integer :follower_id end create_table :goofy_string_id, :force => true, :id => false do |t| @@ -494,6 +494,7 @@ ActiveRecord::Schema.define do t.integer :lock_version, :null => false, :default => 0 t.string :comments t.integer :followers_count, :default => 0 + t.integer :friends_too_count, :default => 0 t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 9d26b8ba3e..9e6b77e2f3 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,6 +1,6 @@ ## Rails 4.0.0 (unreleased) ## -* Fix deletion of empty directories in ActiveSupport::Cache::FileStore. +* Fix deletion of empty directories in `ActiveSupport::Cache::FileStore`. *Charles Jones* diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 822c9d98ae..e3788ed54f 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -15,8 +15,6 @@ namespace :test do end end -# Create compressed packages -dist_dirs = [ "lib", "test"] spec = eval(File.read('activesupport.gemspec')) diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 5d8d09aa69..e51ab9ddbc 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -73,42 +73,44 @@ class Class instance_reader = instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) - # We use class_eval here rather than define_method because class_attribute - # may be used in a performance sensitive context therefore the overhead that - # define_method introduces may become significant. attrs.each do |name| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def self.#{name}() nil end - def self.#{name}?() !!#{name} end + define_singleton_method(name) { nil } + define_singleton_method("#{name}?") { !!public_send(name) } - def self.#{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end + ivar = "@#{name}" + + define_singleton_method("#{name}=") do |val| + singleton_class.class_eval do + remove_possible_method(name) + define_method(name) { val } + end - if singleton_class? - class_eval do - remove_possible_method(:#{name}) - def #{name} - defined?(@#{name}) ? @#{name} : singleton_class.#{name} + if singleton_class? + class_eval do + remove_possible_method(name) + define_method(name) do + if instance_variable_defined? ivar + instance_variable_get ivar + else + singleton_class.send name end end end - val end + val + end - if instance_reader - remove_possible_method :#{name} - def #{name} - defined?(@#{name}) ? @#{name} : self.class.#{name} - end - - def #{name}? - !!#{name} + if instance_reader + remove_possible_method name + define_method(name) do + if instance_variable_defined?(ivar) + instance_variable_get ivar + else + self.class.public_send name end end - RUBY + define_method("#{name}?") { !!public_send(name) } + end attr_writer name if instance_writer end diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 002f57f4bb..4b880cb5dc 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -239,7 +239,7 @@ module ActiveSupport # Compare #name and TZInfo identifier to a supplied regexp, returning +true+ # if a match is found. def =~(re) - return true if name =~ re || MAPPING[name] =~ re + re === name || re === MAPPING[name] end # Returns a textual representation of this time zone. diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index ede08e23d5..acd320dbe0 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -1,7 +1,7 @@ require 'logger' require 'abstract_unit' require 'active_support/cache' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' class CacheKeyTest < ActiveSupport::TestCase def test_entry_legacy_optional_ivars @@ -564,7 +564,7 @@ module LocalCacheBehavior end module AutoloadingCacheBehavior - include DependeciesTestHelpers + include DependenciesTestHelpers def test_simple_autoloading with_autoloading_fixtures do @cache.write('foo', E.new) diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index ac79b15fa8..8f3f710dfd 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -1,10 +1,10 @@ require 'abstract_unit' require 'active_support/core_ext/marshal' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' class MarshalTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation - include DependeciesTestHelpers + include DependenciesTestHelpers def teardown ActiveSupport::Dependencies.clear diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 8c7d00cae1..3744d50864 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -153,22 +153,16 @@ end class NumericExtSizeTest < ActiveSupport::TestCase def test_unit_in_terms_of_another - relationships = { - 1024.bytes => 1.kilobyte, - 1024.kilobytes => 1.megabyte, - 3584.0.kilobytes => 3.5.megabytes, - 3584.0.megabytes => 3.5.gigabytes, - 1.kilobyte ** 4 => 1.terabyte, - 1024.kilobytes + 2.megabytes => 3.megabytes, - 2.gigabytes / 4 => 512.megabytes, - 256.megabytes * 20 + 5.gigabytes => 10.gigabytes, - 1.kilobyte ** 5 => 1.petabyte, - 1.kilobyte ** 6 => 1.exabyte - } - - relationships.each do |left, right| - assert_equal right, left - end + assert_equal 1024.bytes, 1.kilobyte + assert_equal 1024.kilobytes, 1.megabyte + assert_equal 3584.0.kilobytes, 3.5.megabytes + assert_equal 3584.0.megabytes, 3.5.gigabytes + assert_equal 1.kilobyte ** 4, 1.terabyte + assert_equal 1024.kilobytes + 2.megabytes, 3.megabytes + assert_equal 2.gigabytes / 4, 512.megabytes + assert_equal 256.megabytes * 20 + 5.gigabytes, 10.gigabytes + assert_equal 1.kilobyte ** 5, 1.petabyte + assert_equal 1.kilobyte ** 6, 1.exabyte end def test_units_as_bytes_independently diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 8d1a8c628c..92f996f9a4 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -47,7 +47,7 @@ class ToQueryTest < ActiveSupport::TestCase end private - def assert_query_equal(expected, actual, message = nil) + def assert_query_equal(expected, actual) assert_equal expected.split('&'), actual.to_query.split('&') end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 8803ca3348..115a4e894d 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'pp' require 'active_support/dependencies' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' module ModuleWithMissing mattr_accessor :missing_count @@ -20,7 +20,7 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear end - include DependeciesTestHelpers + include DependenciesTestHelpers def test_depend_on_path skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0' diff --git a/activesupport/test/dependecies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 4b46d32fb4..9268512a97 100644 --- a/activesupport/test/dependecies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -1,4 +1,4 @@ -module DependeciesTestHelpers +module DependenciesTestHelpers def with_loading(*from) old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load this_dir = File.dirname(__FILE__) diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 9c3b5d0667..84c3154e53 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -232,6 +232,22 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Time.utc(2012, 5, 28, 7, 0, 0), twz.utc end + def test_parse_doesnt_use_local_dst + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['UTC'] + twz = zone.parse('2013-03-10 02:00:00') + assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time + end + end + + def test_parse_handles_dst_jump + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.parse('2013-03-10 02:00:00') + assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time + end + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get('America/New_York') zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png Binary files differindex c6750e1ae1..c0de53555d 100644 --- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png +++ b/guides/assets/images/getting_started/unknown_action_create_for_posts.png diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 4a4f814917..7355f6816c 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -76,6 +76,7 @@ The methods are: * `reorder` * `reverse_order` * `select` +* `distinct` * `uniq` * `where` @@ -580,10 +581,10 @@ ActiveModel::MissingAttributeError: missing attribute: <attribute> Where `<attribute>` is the attribute you asked for. The `id` method will not raise the `ActiveRecord::MissingAttributeError`, so just be careful when working with associations because they need the `id` method to function properly. -If you would like to only grab a single record per unique value in a certain field, you can use `uniq`: +If you would like to only grab a single record per unique value in a certain field, you can use `distinct`: ```ruby -Client.select(:name).uniq +Client.select(:name).distinct ``` This would generate SQL like: @@ -595,10 +596,10 @@ SELECT DISTINCT name FROM clients You can also remove the uniqueness constraint: ```ruby -query = Client.select(:name).uniq +query = Client.select(:name).distinct # => Returns unique names -query.uniq(false) +query.distinct(false) # => Returns all names, even if there are duplicates ``` @@ -1438,7 +1439,7 @@ Client.where(active: true).pluck(:id) # SELECT id FROM clients WHERE active = 1 # => [1, 2, 3] -Client.uniq.pluck(:role) +Client.distinct.pluck(:role) # SELECT DISTINCT role FROM clients # => ['admin', 'member', 'guest'] diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index a1d7e955c8..cd23b5ee15 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -103,7 +103,7 @@ To verify that you have everything installed correctly, you should be able to ru $ rails --version ``` -If it says something like "Rails 3.2.9", you are ready to continue. +If it says something like "Rails 4.0.0", you are ready to continue. ### Creating the Blog Application @@ -568,7 +568,7 @@ interested in. We also use an instance variable (prefixed by `@`) to hold a reference to the post object. We do this because Rails will pass all instance variables to the view. -Now, create a new file `app/view/posts/show.html.erb` with the following +Now, create a new file `app/views/posts/show.html.erb` with the following content: ```html+erb diff --git a/guides/source/testing.md b/guides/source/testing.md index 40bf2603c4..1937cbf17a 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -501,6 +501,21 @@ You also have access to three instance variables in your functional tests: * `@request` - The request * `@response` - The response +### Setting Headers and CGI variables + +Headers and cgi variables can be set directly on the `@request` +instance variable: + +```ruby +# setting a HTTP Header +@request.headers["Accepts"] = "text/plain, text/html" +get :index # simulate the request with custom header + +# setting a CGI variable +@request.headers["HTTP_REFERER"] = "http://example.com/home" +post :create # simulate the request with custom env variable +``` + ### Testing Templates and Layouts If you want to make sure that the response rendered the correct template and layout, you can use the `assert_template` diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index cb43781f52..0941bc7e58 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -103,6 +103,32 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur * 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 correctly prefers the first named route defined in `config/routes.rb` if a clashing route is found later. Check the output of `rake routes` before upgrading and remove unused named routes to avoid issues. + +```ruby + # config/routes.rb + get 'one' => 'test#example', as: :example + get 'two' => 'test#example', as: :example + + # Rails 3 + <%= example_path %> # => '/two' + + # Rails 4 + <%= example_path %> # => '/one' +``` + +```ruby + # config/routes.rb + resources :examples + get 'clashing/:id' => 'test#example', as: :example + + # Rails 3 + <%= example_path(1) %> # => '/clashing/1' + + # Rails 4 + <%= example_path(1) %> # => '/examples/1' +``` + * Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example: ```ruby diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e3b1bc37c6..60a823de15 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,23 +1,16 @@ ## Rails 4.0.0 (unreleased) ## +* Add support for generate scaffold password:digest -## Rails 4.0.0.beta1 (February 25, 2013) ## -* Change Service pages(404, etc). *Stanislav Sobolev* - -* Improve `rake stats` for JavaScript and CoffeeScript: ignore block comments - and calculates number of functions. - - *Hendy Tanata* - -* Ability to use a custom builder by passing `--builder` (or `-b`) has been removed. Consider - using application template instead. See this guide for more detail: - http://guides.rubyonrails.org/rails_application_templates.html - - *Prem Sichanugrist* + * adds password_digest attribute to the migration + * adds has_secure_password to the model + * adds password and password_confirmation password_fields to _form.html + * omits password from index.html and show.html + * adds password and password_confirmation to the controller + * adds unencrypted password and password_confirmation to the controller test + * adds encrypted password_digest to the fixture -* fix rake db:* tasks to work with DATABASE_URL and without config/database.yml - - *Terence Lee* + *Sam Ruby* * Rails now generates a `test/test_helper.rb` file with `fixtures :all` commented out by default, since we don't want to force loading all fixtures for user when a single test is run. However, @@ -47,10 +40,30 @@ For more information, see `rails test --help`. - This command will eventually replace `rake test:*` and `rake test` tasks + This command will eventually replace `rake test:*` and `rake test` tasks. *Prem Sichanugrist and Chris Toomey* +* Improve service pages with new layout (404, etc). *Stanislav Sobolev* + + +## Rails 4.0.0.beta1 (February 25, 2013) ## + +* Improve `rake stats` for JavaScript and CoffeeScript: ignore block comments + and calculates number of functions. + + *Hendy Tanata* + +* Ability to use a custom builder by passing `--builder` (or `-b`) has been removed. + Consider using application template instead. See this guide for more detail: + http://guides.rubyonrails.org/rails_application_templates.html + + *Prem Sichanugrist* + +* Fix `rake db:*` tasks to work with `DATABASE_URL` and without `config/database.yml`. + + *Terence Lee* + * Add notice message for destroy action in scaffold generator. *Rahul P. Chaudhari* diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index cdb29a8156..ddf45d196a 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -43,7 +43,10 @@ module Rails end def app - @app ||= super.respond_to?(:to_app) ? super.to_app : super + @app ||= begin + app = super + app.respond_to?(:to_app) ? app.to_app : app + end end def opt_parser diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index 32546936e3..85a1b01cc6 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -13,8 +13,17 @@ <% attributes.each do |attribute| -%> <div class="field"> +<% if attribute.password_digest? -%> + <%%= f.label :password %><br /> + <%%= f.password_field :password %> + </div> + <div> + <%%= f.label :password_confirmation %><br /> + <%%= f.password_field :password_confirmation %> +<% else -%> <%%= f.label :<%= attribute.name %> %><br /> <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %> +<% end -%> </div> <% end -%> <div class="actions"> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 90d8db1df5..d2fd99fdcb 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -3,7 +3,7 @@ <table> <thead> <tr> -<% attributes.each do |attribute| -%> +<% attributes.reject(&:password_digest?).each do |attribute| -%> <th><%= attribute.human_name %></th> <% end -%> <th></th> @@ -15,7 +15,7 @@ <tbody> <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <tr> -<% attributes.each do |attribute| -%> +<% attributes.reject(&:password_digest?).each do |attribute| -%> <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> <% end -%> <td><%%= link_to 'Show', <%= singular_table_name %> %></td> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb index daae72270f..5e634153be 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb @@ -1,6 +1,6 @@ <p id="notice"><%%= notice %></p> -<% attributes.each do |attribute| -%> +<% attributes.reject(&:password_digest?).each do |attribute| -%> <p> <strong><%= attribute.human_name %>:</strong> <%%= @<%= singular_table_name %>.<%= attribute.name %> %> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 4ae8756ed0..5e2784c4b0 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -130,6 +130,10 @@ module Rails @has_uniq_index end + def password_digest? + name == 'password' && type == :digest + end + def inject_options "".tap { |s| @attr_options.each { |k,v| s << ", #{k}: #{v.inspect}" } } end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 9965db98de..8b4f52bb3b 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -163,6 +163,7 @@ module Rails def attributes_names @attributes_names ||= attributes.each_with_object([]) do |a, names| names << a.column_name + names << 'password_confirmation' if a.password_digest? names << "#{a.name}_type" if a.polymorphic? end end diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml index c9d505c84a..90a92e6982 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -1,22 +1,20 @@ # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - <% unless attributes.empty? -%> -one: +<% %w(one two).each do |name| %> +<%= name %>: <% attributes.each do |attribute| -%> + <%- if attribute.password_digest? -%> + password_digest: <%%= BCrypt::Password.create('secret') %> + <%- else -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> - <%- if attribute.polymorphic? -%> - <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %> <%- end -%> -<% end -%> - -two: -<% attributes.each do |attribute| -%> - <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- if attribute.polymorphic? -%> <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %> <%- end -%> <% end -%> +<% end -%> <% else -%> + # This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 8f3ecaadea..2e1f55f2a6 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -21,7 +21,11 @@ module TestUnit # :nodoc: return if attributes_names.empty? attributes_names.map do |name| - "#{name}: @#{singular_table_name}.#{name}" + if %w(password password_confirmation).include?(name) && attributes.any?(&:password_digest?) + "#{name}: 'secret'" + else + "#{name}: @#{singular_table_name}.#{name}" + end end.sort.join(', ') end end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index de44bf9e4d..3c247f32c0 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -79,7 +79,6 @@ namespace :test do # Display deprecation message task :deprecated do - task_name = ARGV.first ActiveSupport::Deprecation.warn "`rake #{ARGV.first}` is deprecated with no replacement." end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 357f472a3f..b29d1e018e 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -271,4 +271,45 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end end + + def test_scaffold_generator_password_digest + run_generator ["user", "name", "password:digest"] + + assert_file "app/models/user.rb", /has_secure_password/ + + assert_migration "db/migrate/create_users.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.string :name/, up) + assert_match(/t\.string :password_digest/, up) + end + end + + assert_file "app/controllers/users_controller.rb" do |content| + assert_instance_method :user_params, content do |m| + assert_match(/permit\(:name, :password, :password_confirmation\)/, m) + end + end + + assert_file "app/views/users/_form.html.erb" do |content| + assert_match(/<%= f\.password_field :password %>/, content) + assert_match(/<%= f\.password_field :password_confirmation %>/, content) + end + + assert_file "app/views/users/index.html.erb" do |content| + assert_no_match(/password/, content) + end + + assert_file "app/views/users/show.html.erb" do |content| + assert_no_match(/password/, content) + end + + assert_file "test/controllers/users_controller_test.rb" do |content| + assert_match(/password: 'secret'/, content) + assert_match(/password_confirmation: 'secret'/, content) + end + + assert_file "test/fixtures/users.yml" do |content| + assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content) + end + end end |