diff options
46 files changed, 652 insertions, 198 deletions
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..f945a5c1a2 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -60,7 +60,7 @@ module ActionDispatch return params end - params.each do |k, v| + params.each_value do |v| case v when Hash encode_params(v) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7b04d6e851..97a9b7be86 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 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/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..9c6d02a62d 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: JSON.dump(request.headers.env) + 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/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 930a28b752..f73fc9d9a3 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -125,7 +125,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/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/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..9c659dd0af 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,11 +24,16 @@ module ActiveRecord base.type_to_sql(type.to_sym, limit, precision, scale) end + def primary_key? + type == :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? + column_options[:column] = self add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key column_sql end @@ -37,7 +41,7 @@ module ActiveRecord 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 b8b2cf9ab1..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? 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/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/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/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/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/associations_test.rb b/activerecord/test/cases/associations_test.rb index 201fa5d5a9..41529cd050 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" diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index aa09df5743..9c73e4ca00 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -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 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..7de2ceae88 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -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/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/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/source/getting_started.md b/guides/source/getting_started.md index a1d7e955c8..a1864c123e 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 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/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 |