diff options
180 files changed, 1990 insertions, 1509 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 652ce0b3d8..5852aeaec2 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -301,12 +301,13 @@ module ActionMailer # end # end # - # Callbacks in ActionMailer are implemented using AbstractController::Callbacks, so you - # can define and configure callbacks in the same manner that you would use callbacks in - # classes that inherit from ActionController::Base. + # Callbacks in Action Mailer are implemented using + # <tt>AbstractController::Callbacks</tt>, so you can define and configure + # callbacks in the same manner that you would use callbacks in classes that + # inherit from <tt>ActionController::Base</tt>. # # Note that unless you have a specific reason to do so, you should prefer using before_action - # rather than after_action in your ActionMailer classes so that headers are parsed properly. + # rather than after_action in your Action Mailer classes so that headers are parsed properly. # # = Previewing emails # diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 6452bf616c..06da0dd27e 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -1,4 +1,6 @@ module ActionMailer + # Provides helper methods for testing Action Mailer, including #assert_emails + # and #assert_no_emails module TestHelper # Asserts that the number of emails sent matches the given number. # diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index a98aec913f..06f80a8fdc 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,7 +1,8 @@ require_relative 'gem_version' module ActionMailer - # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Action Mailer as a + # <tt>Gem::Version</tt>. def self.version gem_version end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 2b4ff7f03d..7da58ca4e7 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,9 +1,18 @@ +* Deprecate all *_filter callbacks in favor of *_action callbacks. + + *Rafael Mendonça França* + +* Fix URL generation with `:trailing_slash` such that it does not add + a trailing slash after `.:format` + + *Dan Langevin* + * Build full URI as string when processing path in integration tests for performance reasons. *Guo Xiang Tan* -* Fix 'Stack level too deep' when rendering `head :ok` in an action method +* Fix `'Stack level too deep'` when rendering `head :ok` in an action method called 'status' in a controller. Fixes #13905. @@ -71,7 +80,7 @@ 4. Use `escape_segment` rather than `escape_path` in URL generation For point 4 there are two exceptions. Firstly, when a route uses wildcard segments - (e.g. *foo) then we use `escape_path` as the value may contain '/' characters. This + (e.g. `*foo`) then we use `escape_path` as the value may contain '/' characters. This means that wildcard routes can't be optimized. Secondly, if a `:controller` segment is used in the path then this uses `escape_path` as the controller may be namespaced. @@ -101,7 +110,7 @@ *Andrew White*, *James Coglan* -* Append link to bad code to backtrace when exception is SyntaxError. +* Append link to bad code to backtrace when exception is `SyntaxError`. *Boris Kuznetsov* @@ -131,4 +140,5 @@ *Tony Wooster* + Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index c00f0d0c6f..acdfb33efa 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -255,7 +255,7 @@ module AbstractController # Checks if the action name is valid and returns false otherwise. def _valid_action_name?(action_name) - action_name.to_s !~ Regexp.new(File::SEPARATOR) + action_name !~ Regexp.new(File::SEPARATOR) end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 69aca308d6..252e297c69 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation' + module AbstractController module Callbacks extend ActiveSupport::Concern @@ -42,21 +44,23 @@ module AbstractController end end - # Skip before, after, and around action callbacks matching any of the names - # Aliased as skip_filter. + # Skip before, after, and around action callbacks matching any of the names. # # ==== Parameters # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_filter + # using #skip_action_callback def skip_action_callback(*names) skip_before_action(*names) skip_after_action(*names) skip_around_action(*names) end - alias_method :skip_filter, :skip_action_callback + def skip_filter(*names) + ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will removed in Rails 5. Use #{callback}_action instead.") + skip_action_callback(*names) + end # Take callback names and an optional callback proc, normalize them, # then call the block with each callback. This allows us to abstract @@ -85,7 +89,6 @@ module AbstractController # :call-seq: before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. - # Aliased as before_filter. ## # :method: prepend_before_action @@ -93,7 +96,6 @@ module AbstractController # :call-seq: prepend_before_action(names, block) # # Prepend a callback before actions. See _insert_callbacks for parameter details. - # Aliased as prepend_before_filter. ## # :method: skip_before_action @@ -101,7 +103,6 @@ module AbstractController # :call-seq: skip_before_action(names) # # Skip a callback before actions. See _insert_callbacks for parameter details. - # Aliased as skip_before_filter. ## # :method: append_before_action @@ -109,7 +110,6 @@ module AbstractController # :call-seq: append_before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. - # Aliased as append_before_filter. ## # :method: after_action @@ -117,7 +117,6 @@ module AbstractController # :call-seq: after_action(names, block) # # Append a callback after actions. See _insert_callbacks for parameter details. - # Aliased as after_filter. ## # :method: prepend_after_action @@ -125,7 +124,6 @@ module AbstractController # :call-seq: prepend_after_action(names, block) # # Prepend a callback after actions. See _insert_callbacks for parameter details. - # Aliased as prepend_after_filter. ## # :method: skip_after_action @@ -133,7 +131,6 @@ module AbstractController # :call-seq: skip_after_action(names) # # Skip a callback after actions. See _insert_callbacks for parameter details. - # Aliased as skip_after_filter. ## # :method: append_after_action @@ -141,7 +138,6 @@ module AbstractController # :call-seq: append_after_action(names, block) # # Append a callback after actions. See _insert_callbacks for parameter details. - # Aliased as append_after_filter. ## # :method: around_action @@ -149,7 +145,6 @@ module AbstractController # :call-seq: around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - # Aliased as around_filter. ## # :method: prepend_around_action @@ -157,7 +152,6 @@ module AbstractController # :call-seq: prepend_around_action(names, block) # # Prepend a callback around actions. See _insert_callbacks for parameter details. - # Aliased as prepend_around_filter. ## # :method: skip_around_action @@ -165,7 +159,6 @@ module AbstractController # :call-seq: skip_around_action(names) # # Skip a callback around actions. See _insert_callbacks for parameter details. - # Aliased as skip_around_filter. ## # :method: append_around_action @@ -173,7 +166,6 @@ module AbstractController # :call-seq: append_around_action(names, block) # # Append a callback around actions. See _insert_callbacks for parameter details. - # Aliased as append_around_filter. # set up before_action, prepend_before_action, skip_before_action, etc. # for each of before, after, and around. @@ -184,7 +176,10 @@ module AbstractController end end - alias_method :"#{callback}_filter", :"#{callback}_action" + define_method "#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will removed in Rails 5. Use #{callback}_action instead.") + send("#{callback}_action", *names, &blk) + end define_method "prepend_#{callback}_action" do |*names, &blk| _insert_callbacks(names, blk) do |name, options| @@ -192,7 +187,10 @@ module AbstractController end end - alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action" + define_method "prepend_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will removed in Rails 5. Use prepend_#{callback}_action instead.") + send("prepend_#{callback}_action", *names, &blk) + end # Skip a before, after or around callback. See _insert_callbacks # for details on the allowed parameters. @@ -202,11 +200,17 @@ module AbstractController end end - alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action" + define_method "skip_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will removed in Rails 5. Use skip_#{callback}_action instead.") + send("skip_#{callback}_action", *names, &blk) + end # *_action is the same as append_*_action alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action - alias_method :"append_#{callback}_filter", :"#{callback}_action" # alias_method :append_before_filter, :before_action + define_method "append_#{callback}_filter" do |*names, &blk| + ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will removed in Rails 5. Use append_#{callback}_action instead.") + send("append_#{callback}_action", *names, &blk) + end end end end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 696fbf6e09..9a427ebfdb 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -30,10 +30,8 @@ module ActionController end end - def build(action, app=nil, &block) - app ||= block + def build(action, app = Proc.new) action = action.to_s - raise "MiddlewareStack#build requires an app" unless app middlewares.reverse.inject(app) do |a, middleware| middleware.valid?(action) ? middleware.build(a) : a @@ -223,13 +221,18 @@ module ActionController # Makes the controller a Rack endpoint that runs the action in the given # +env+'s +action_dispatch.request.path_parameters+ key. def self.call(env) - action(env['action_dispatch.request.path_parameters'][:action]).call(env) + req = ActionDispatch::Request.new env + action(req.path_parameters[:action]).call(env) end # Returns a Rack endpoint for the given action name. def self.action(name, klass = ActionDispatch::Request) - middleware_stack.build(name.to_s) do |env| - new.dispatch(name, klass.new(env)) + if middleware_stack.any? + middleware_stack.build(name) do |env| + new.dispatch(name, klass.new(env)) + end + else + lambda { |env| new.dispatch(name, klass.new(env)) } end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 29ce5abd55..46405cef55 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -78,7 +78,7 @@ module ActionController # respond_to do |format| # format.html # format.csv { render csv: @csvable, filename: @csvable.name } - # } + # end # end # To use renderers and their mime types in more concise ways, see # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 5b22cd1fcd..1ab11392ce 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -4,10 +4,7 @@ require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module Http module Parameters - def initialize(env) - super - @symbolized_path_params = nil - end + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' # Returns both GET and POST \parameters in a single hash. def parameters @@ -25,7 +22,7 @@ module ActionDispatch def path_parameters=(parameters) #:nodoc: @env.delete('action_dispatch.request.parameters') - @env[Routing::RouteSet::PARAMETERS_KEY] = parameters + @env[PARAMETERS_KEY] = parameters end # The same as <tt>path_parameters</tt> with explicitly symbolized keys. @@ -40,11 +37,7 @@ module ActionDispatch # # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @env[Routing::RouteSet::PARAMETERS_KEY] ||= {} - end - - def reset_parameters #:nodoc: - @env.delete("action_dispatch.request.parameters") + @env[PARAMETERS_KEY] ||= {} end private diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index cdb3e44b3a..dfe258e463 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -53,6 +53,17 @@ module ActionDispatch @uuid = nil end + def check_path_parameters! + # If any of the path parameters has an invalid encoding then + # raise since it's likely to trigger errors further on. + path_parameters.each do |key, value| + next unless value.respond_to?(:valid_encoding?) + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + end + def key?(key) @env.key?(key) end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index a6c17f50a5..4cba4f5f37 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -37,13 +37,7 @@ module ActionDispatch path = options[:script_name].to_s.chomp("/") path << options[:path].to_s - if options[:trailing_slash] - if path.include?('?') - path.sub!(/\?/, '/\&') - else - path.sub!(/[^\/]\z|\A\z/, '\&/') - end - end + add_trailing_slash(path) if options[:trailing_slash] result = path @@ -66,6 +60,18 @@ module ActionDispatch private + def add_trailing_slash(path) + # includes querysting + if path.include?('?') + path.sub!(/\?/, '/\&') + # does not have a .format + elsif !path.include?(".") + path.sub!(/[^\/]\z|\A\z/, '\&/') + end + + path + end + def build_host_url(options) if match = options[:host].match(HOST_REGEXP) options[:protocol] ||= match[1] unless options[:protocol] == false diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 8b3081d8fe..6d58323789 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -132,11 +132,6 @@ module ActionDispatch } end - # Returns +true+ if no missing keys are present, otherwise +false+. - def verify_required_parts!(route, parts) - missing_keys(route, parts).empty? - end - def build_cache root = { ___routes: [] } routes.each_with_index do |route, i| diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index cc3c7f20cb..9f0a3af902 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,14 +16,6 @@ module ActionDispatch @app = app @path = path - # Unwrap any constraints so we can see what's inside for route generation. - # This allows the formatter to skip over any mounted applications or redirects - # that shouldn't be matched when using a url_for without a route name. - while app.is_a?(Routing::Mapper::Constraints) do - app = app.app - end - @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher) - @constraints = constraints @defaults = defaults @required_defaults = nil @@ -99,7 +91,7 @@ module ActionDispatch end def dispatcher? - @dispatcher + @app.dispatcher? end def matches?(request) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index c34b44409e..74fa9ee3a2 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -20,61 +20,30 @@ module ActionDispatch # :nodoc: VERSION = '2.0.0' - class NullReq # :nodoc: - attr_reader :env - attr_accessor :path_parameters - def initialize(env) - @env = env - @path_parameters = {} - end - - def request_method - env['REQUEST_METHOD'] - end - - def path_info - env['PATH_INFO'] - end - - def ip - env['REMOTE_ADDR'] - end - - def [](k) - env[k] - end - end - - attr_reader :request_class, :formatter attr_accessor :routes - def initialize(routes, options) - @options = options - @request_class = options[:request_class] || NullReq - @routes = routes + def initialize(routes) + @routes = routes end - def call(env) - env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO']) - - req = request_class.new(env) - - find_routes(env, req).each do |match, parameters, route| - set_params = req.path_parameters - script_name, path_info = env.values_at('SCRIPT_NAME', 'PATH_INFO') + def serve(req) + find_routes(req).each do |match, parameters, route| + set_params = req.path_parameters + path_info = req.path_info + script_name = req.script_name unless route.path.anchored - env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/') - env['PATH_INFO'] = match.post_match + req.script_name = (script_name.to_s + match.to_s).chomp('/') + req.path_info = match.post_match end req.path_parameters = set_params.merge parameters - status, headers, body = route.app.call(env) + status, headers, body = route.app.serve(req) if 'pass' == headers['X-Cascade'] - env['SCRIPT_NAME'] = script_name - env['PATH_INFO'] = path_info + req.script_name = script_name + req.path_info = path_info req.path_parameters = set_params next end @@ -85,13 +54,11 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, ['Not Found']] end - def recognize(req) - rails_req = request_class.new(req.env) - - find_routes(req.env, rails_req).each do |match, parameters, route| + def recognize(rails_req) + find_routes(rails_req).each do |match, parameters, route| unless route.path.anchored - req.env['SCRIPT_NAME'] = match.to_s - req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1') + rails_req.script_name = match.to_s + rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1') end yield(route, parameters) @@ -128,11 +95,14 @@ module ActionDispatch simulator.memos(path) { [] } end - def find_routes env, req + def find_routes req routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| r.path.match(req.path_info) } - routes.concat get_routes_as_head(routes) + + if req.env["REQUEST_METHOD"] === "HEAD" + routes.concat get_routes_as_head(routes) + end routes.sort_by!(&:precedence).select! { |r| r.matches?(req) } @@ -148,7 +118,7 @@ module ActionDispatch def get_routes_as_head(routes) precedence = (routes.map(&:precedence).max || 0) + 1 - routes = routes.select { |r| + routes.select { |r| r.verb === "GET" && !(r.verb === "HEAD") }.map! { |r| Route.new(r.name, @@ -159,8 +129,6 @@ module ActionDispatch route.precedence = r.precedence + precedence end } - routes.flatten! - routes end end end diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb index 6aff10956a..9b28a65200 100644 --- a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb +++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb @@ -2,13 +2,13 @@ <html> <head> <title><%= title %></title> - <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css"> <style> <% stylesheets.each do |style| %> <%= style %> <% end %> </style> - <script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script> </head> <body> <div id="wrapper"> diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index cce0d75af4..6ffa242da4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -148,8 +148,8 @@ // On key press perform a search for matching paths searchElem.onkeyup = function(e){ var userInput = searchElem.value, - defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + sanitizePath(userInput) +'):</th></tr>', - defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + userInput +'):</th></tr>', + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>', noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb new file mode 100644 index 0000000000..88aa13c3e8 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -0,0 +1,10 @@ +module ActionDispatch + module Routing + class Endpoint # :nodoc: + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 71a0c5e826..ea3b2f419d 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -5,22 +5,15 @@ module ActionDispatch module Routing class RouteWrapper < SimpleDelegator def endpoint - rack_app ? rack_app.inspect : "#{controller}##{action}" + app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect end def constraints requirements.except(:controller, :action) end - def rack_app(app = self.app) - @rack_app ||= begin - class_name = app.class.name.to_s - if class_name == "ActionDispatch::Routing::Mapper::Constraints" - rack_app(app.app) - elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/ - app - end - end + def rack_app + app.app end def verb @@ -73,7 +66,7 @@ module ActionDispatch end def engine? - rack_app && rack_app.respond_to?(:routes) + rack_app.respond_to?(:routes) end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7e78b417fa..84a08770f5 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing @@ -15,34 +16,41 @@ module ActionDispatch :controller, :action, :path_names, :constraints, :shallow, :blocks, :defaults, :options] - class Constraints #:nodoc: - def self.new(app, constraints, request = Rack::Request) - if constraints.any? - super(app, constraints, request) - else - app + class Constraints < Endpoint #:nodoc: + attr_reader :app, :constraints + + def initialize(app, constraints, dispatcher_p) + # Unwrap Constraints objects. I don't actually think it's possible + # to pass a Constraints object to this constructor, but there were + # multiple places that kept testing children of this object. I + # *think* they were just being defensive, but I have no idea. + if app.is_a?(self.class) + constraints += app.constraints + app = app.app end - end - attr_reader :app, :constraints + @dispatcher = dispatcher_p - def initialize(app, constraints, request) - @app, @constraints, @request = app, constraints, request + @app, @constraints, = app, constraints end - def matches?(env) - req = @request.new(env) + def dispatcher?; @dispatcher; end + def matches?(req) @constraints.all? do |constraint| (constraint.respond_to?(:matches?) && constraint.matches?(req)) || (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req))) end - ensure - req.reset_parameters end - def call(env) - matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ] + def serve(req) + return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + + if dispatcher? + @app.serve req + else + @app.call req.env + end end private @@ -57,12 +65,18 @@ module ActionDispatch WILDCARD_PATH = %r{\*([^/\)]+)\)?$} attr_reader :scope, :path, :options, :requirements, :conditions, :defaults + attr_reader :to, :default_controller, :default_action def initialize(set, scope, path, options) - @set, @scope, @path, @options = set, scope, path, options + @set, @scope, @path = set, scope, path @requirements, @conditions, @defaults = {}, {}, {} - normalize_options! + options = scope[:options].merge(options) if scope[:options] + @to = options[:to] + @default_controller = options[:controller] || scope[:controller] + @default_action = options[:action] || scope[:action] + + @options = normalize_options!(options) normalize_path! normalize_requirements! normalize_conditions! @@ -94,14 +108,13 @@ module ActionDispatch options[:format] != false && !path.include?(':format') && !path.end_with?('/') end - def normalize_options! - @options.reverse_merge!(scope[:options]) if scope[:options] + def normalize_options!(options) path_without_format = path.sub(/\(\.:format\)$/, '') # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default - if path_without_format.match(WILDCARD_PATH) && @options[:format] != false - @options[$1.to_sym] ||= /.+?/ + if path_without_format.match(WILDCARD_PATH) && options[:format] != false + options[$1.to_sym] ||= /.+?/ end if path_without_format.match(':controller') @@ -111,10 +124,10 @@ module ActionDispatch # controllers with default routes like :controller/:action/:id(.:format), e.g: # GET /admin/products/show/1 # => { controller: 'admin/products', action: 'show', id: '1' } - @options[:controller] ||= /.+?/ + options[:controller] ||= /.+?/ end - @options.merge!(default_controller_and_action) + options.merge!(default_controller_and_action) end def normalize_requirements! @@ -210,7 +223,17 @@ module ActionDispatch end def app - Constraints.new(endpoint, blocks, @set.request_class) + return to if Redirect === to + + if to.respond_to?(:call) + Constraints.new(to, blocks, false) + else + if blocks.any? + Constraints.new(dispatcher, blocks, true) + else + dispatcher + end + end end def default_controller_and_action @@ -296,24 +319,8 @@ module ActionDispatch Journey::Router::Strexp.compile(path, requirements, SEPARATORS) end - def endpoint - to.respond_to?(:call) ? to : dispatcher - end - def dispatcher - Routing::RouteSet::Dispatcher.new(:defaults => defaults) - end - - def to - options[:to] - end - - def default_controller - options[:controller] || scope[:controller] - end - - def default_action - options[:action] || scope[:action] + Routing::RouteSet::Dispatcher.new(defaults) end end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index f8ed0cbe6a..b1b39a5496 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -3,10 +3,11 @@ require 'active_support/core_ext/uri' require 'active_support/core_ext/array/extract_options' require 'rack/utils' require 'action_controller/metal/exceptions' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing - class Redirect # :nodoc: + class Redirect < Endpoint # :nodoc: attr_reader :status, :block def initialize(status, block) @@ -14,17 +15,14 @@ module ActionDispatch @block = block end - def call(env) - req = Request.new(env) + def redirect?; true; end - # If any of the path parameters has an invalid encoding then - # raise since it's likely to trigger errors further on. - req.path_parameters.each do |key, value| - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def call(env) + serve Request.new env + end + def serve(req) + req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) unless uri.host diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 8b4fd26ce2..3ca5abf0fd 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/array/extract_options' require 'action_controller/metal/exceptions' require 'action_dispatch/http/request' +require 'action_dispatch/routing/endpoint' module ActionDispatch module Routing @@ -18,27 +19,17 @@ module ActionDispatch # alias inspect to to_s. alias inspect to_s - PARAMETERS_KEY = 'action_dispatch.request.path_parameters' - - class Dispatcher #:nodoc: - def initialize(options={}) - @defaults = options[:defaults] - @glob_param = options.delete(:glob) + class Dispatcher < Routing::Endpoint #:nodoc: + def initialize(defaults) + @defaults = defaults @controller_class_names = ThreadSafe::Cache.new end - def call(env) - params = env[PARAMETERS_KEY] - - # If any of the path parameters has an invalid encoding then - # raise since it's likely to trigger errors further on. - params.each do |key, value| - next unless value.respond_to?(:valid_encoding?) + def dispatcher?; true; end - unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" - end - end + def serve(req) + req.check_path_parameters! + params = req.path_parameters prepare_params!(params) @@ -47,13 +38,12 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, []] end - dispatch(controller, params[:action], env) + dispatch(controller, params[:action], req.env) end def prepare_params!(params) normalize_controller!(params) merge_default_action!(params) - split_glob_param!(params) if @glob_param end # If this is a default_controller (i.e. a controller specified by the user) @@ -89,10 +79,6 @@ module ActionDispatch def merge_default_action!(params) params[:action] ||= 'index' end - - def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) } - end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -302,8 +288,7 @@ module ActionDispatch @finalized = false @set = Journey::Routes.new - @router = Journey::Router.new(@set, { - :request_class => request_class}) + @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set end @@ -683,7 +668,9 @@ module ActionDispatch end def call(env) - @router.call(env) + req = request_class.new(env) + req.path_info = Journey::Router::Utils.normalize_path(req.path_info) + @router.serve(req) end def recognize_path(path, environment = {}) @@ -697,7 +684,7 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = @request_class.new(env) + req = request_class.new(env) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -708,12 +695,10 @@ module ActionDispatch end old_params = req.path_parameters req.path_parameters = old_params.merge params - dispatcher = route.app - while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do - dispatcher = dispatcher.app - end + app = route.app + if app.matches?(req) && app.dispatcher? + dispatcher = app.app - if dispatcher.is_a?(Dispatcher) if dispatcher.controller(params, false) dispatcher.prepare_params!(params) return params diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index d900f3c7a9..107e62d20f 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -3,7 +3,6 @@ require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' require 'rack/test' -require 'minitest' module ActionDispatch module Integration #:nodoc: diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 8cba049485..07571602e4 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -267,9 +267,11 @@ module AbstractController end class AliasedCallbacks < ControllerWithCallbacks - before_filter :first - after_filter :second - around_filter :aroundz + ActiveSupport::Deprecation.silence do + before_filter :first + after_filter :second + around_filter :aroundz + end def first @text = "Hello world" diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index c87494aa64..b2b01b3fa9 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -2,13 +2,13 @@ require 'abstract_unit' class ActionController::Base class << self - %w(append_around_filter prepend_after_filter prepend_around_filter prepend_before_filter skip_after_filter skip_before_filter skip_filter).each do |pending| + %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action skip_action_callback).each do |pending| define_method(pending) do |*args| $stderr.puts "#{pending} unimplemented: #{args.inspect}" end unless method_defined?(pending) end - def before_filters + def before_actions filters = _process_action_callbacks.select { |c| c.kind == :before } filters.map! { |c| c.raw_filter } end @@ -28,8 +28,8 @@ end class FilterTest < ActionController::TestCase class TestController < ActionController::Base - before_filter :ensure_login - after_filter :clean_up + before_action :ensure_login + after_action :clean_up def show render :inline => "ran action" @@ -42,13 +42,13 @@ class FilterTest < ActionController::TestCase end def clean_up - @ran_after_filter ||= [] - @ran_after_filter << "clean_up" + @ran_after_action ||= [] + @ran_after_action << "clean_up" end end class ChangingTheRequirementsController < TestController - before_filter :ensure_login, :except => [:go_wild] + before_action :ensure_login, :except => [:go_wild] def go_wild render :text => "gobble" @@ -56,9 +56,9 @@ class FilterTest < ActionController::TestCase end class TestMultipleFiltersController < ActionController::Base - before_filter :try_1 - before_filter :try_2 - before_filter :try_3 + before_action :try_1 + before_action :try_2 + before_action :try_3 (1..3).each do |i| define_method "fail_#{i}" do @@ -78,8 +78,8 @@ class FilterTest < ActionController::TestCase end class RenderingController < ActionController::Base - before_filter :before_filter_rendering - after_filter :unreached_after_filter + before_action :before_action_rendering + after_action :unreached_after_action def show @ran_action = true @@ -87,29 +87,29 @@ class FilterTest < ActionController::TestCase end private - def before_filter_rendering + def before_action_rendering @ran_filter ||= [] - @ran_filter << "before_filter_rendering" + @ran_filter << "before_action_rendering" render :inline => "something else" end - def unreached_after_filter - @ran_filter << "unreached_after_filter_after_render" + def unreached_after_action + @ran_filter << "unreached_after_action_after_render" end end - class RenderingForPrependAfterFilterController < RenderingController - prepend_after_filter :unreached_prepend_after_filter + class RenderingForPrependAfterActionController < RenderingController + prepend_after_action :unreached_prepend_after_action private - def unreached_prepend_after_filter - @ran_filter << "unreached_preprend_after_filter_after_render" + def unreached_prepend_after_action + @ran_filter << "unreached_preprend_after_action_after_render" end end - class BeforeFilterRedirectionController < ActionController::Base - before_filter :before_filter_redirects - after_filter :unreached_after_filter + class BeforeActionRedirectionController < ActionController::Base + before_action :before_action_redirects + after_action :unreached_after_action def show @ran_action = true @@ -122,23 +122,23 @@ class FilterTest < ActionController::TestCase end private - def before_filter_redirects + def before_action_redirects @ran_filter ||= [] - @ran_filter << "before_filter_redirects" + @ran_filter << "before_action_redirects" redirect_to(:action => 'target_of_redirection') end - def unreached_after_filter - @ran_filter << "unreached_after_filter_after_redirection" + def unreached_after_action + @ran_filter << "unreached_after_action_after_redirection" end end - class BeforeFilterRedirectionForPrependAfterFilterController < BeforeFilterRedirectionController - prepend_after_filter :unreached_prepend_after_filter_after_redirection + class BeforeActionRedirectionForPrependAfterActionController < BeforeActionRedirectionController + prepend_after_action :unreached_prepend_after_action_after_redirection private - def unreached_prepend_after_filter_after_redirection - @ran_filter << "unreached_prepend_after_filter_after_redirection" + def unreached_prepend_after_action_after_redirection + @ran_filter << "unreached_prepend_after_action_after_redirection" end end @@ -151,8 +151,8 @@ class FilterTest < ActionController::TestCase render :inline => "ran action" end - def show_without_filter - render :inline => "ran action without filter" + def show_without_action + render :inline => "ran action without action" end private @@ -168,70 +168,70 @@ class FilterTest < ActionController::TestCase end class ConditionalCollectionFilterController < ConditionalFilterController - before_filter :ensure_login, :except => [ :show_without_filter, :another_action ] + before_action :ensure_login, :except => [ :show_without_action, :another_action ] end class OnlyConditionSymController < ConditionalFilterController - before_filter :ensure_login, :only => :show + before_action :ensure_login, :only => :show end class ExceptConditionSymController < ConditionalFilterController - before_filter :ensure_login, :except => :show_without_filter + before_action :ensure_login, :except => :show_without_action end class BeforeAndAfterConditionController < ConditionalFilterController - before_filter :ensure_login, :only => :show - after_filter :clean_up_tmp, :only => :show + before_action :ensure_login, :only => :show + after_action :clean_up_tmp, :only => :show end class OnlyConditionProcController < ConditionalFilterController - before_filter(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_filter", true) } + before_action(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_action", true) } end class ExceptConditionProcController < ConditionalFilterController - before_filter(:except => :show_without_filter) {|c| c.instance_variable_set(:"@ran_proc_filter", true) } + before_action(:except => :show_without_action) {|c| c.instance_variable_set(:"@ran_proc_action", true) } end class ConditionalClassFilter - def self.before(controller) controller.instance_variable_set(:"@ran_class_filter", true) end + def self.before(controller) controller.instance_variable_set(:"@ran_class_action", true) end end class OnlyConditionClassController < ConditionalFilterController - before_filter ConditionalClassFilter, :only => :show + before_action ConditionalClassFilter, :only => :show end class ExceptConditionClassController < ConditionalFilterController - before_filter ConditionalClassFilter, :except => :show_without_filter + before_action ConditionalClassFilter, :except => :show_without_action end class AnomolousYetValidConditionController < ConditionalFilterController - before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_filter1", true)}, :except => :show_without_filter) { |c| c.instance_variable_set(:"@ran_proc_filter2", true)} + before_action(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_action1", true)}, :except => :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true)} end class OnlyConditionalOptionsFilter < ConditionalFilterController - before_filter :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) } + before_action :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) } end class ConditionalOptionsFilter < ConditionalFilterController - before_filter :ensure_login, :if => Proc.new { |c| true } - before_filter :clean_up_tmp, :if => Proc.new { |c| false } + before_action :ensure_login, :if => Proc.new { |c| true } + before_action :clean_up_tmp, :if => Proc.new { |c| false } end class ConditionalOptionsSkipFilter < ConditionalFilterController - before_filter :ensure_login - before_filter :clean_up_tmp + before_action :ensure_login + before_action :clean_up_tmp - skip_before_filter :ensure_login, if: -> { false } - skip_before_filter :clean_up_tmp, if: -> { true } + skip_before_action :ensure_login, if: -> { false } + skip_before_action :clean_up_tmp, if: -> { true } end class ClassController < ConditionalFilterController - before_filter ConditionalClassFilter + before_action ConditionalClassFilter end class PrependingController < TestController - prepend_before_filter :wonderful_life - # skip_before_filter :fire_flash + prepend_before_action :wonderful_life + # skip_before_action :fire_flash private def wonderful_life @@ -241,8 +241,8 @@ class FilterTest < ActionController::TestCase end class SkippingAndLimitedController < TestController - skip_before_filter :ensure_login - before_filter :ensure_login, :only => :index + skip_before_action :ensure_login + before_action :ensure_login, :only => :index def index render :text => 'ok' @@ -254,9 +254,9 @@ class FilterTest < ActionController::TestCase end class SkippingAndReorderingController < TestController - skip_before_filter :ensure_login - before_filter :find_record - before_filter :ensure_login + skip_before_action :ensure_login + before_action :find_record + before_action :ensure_login def index render :text => 'ok' @@ -270,10 +270,10 @@ class FilterTest < ActionController::TestCase end class ConditionalSkippingController < TestController - skip_before_filter :ensure_login, :only => [ :login ] - skip_after_filter :clean_up, :only => [ :login ] + skip_before_action :ensure_login, :only => [ :login ] + skip_after_action :clean_up, :only => [ :login ] - before_filter :find_user, :only => [ :change_password ] + before_action :find_user, :only => [ :change_password ] def login render :inline => "ran action" @@ -291,8 +291,8 @@ class FilterTest < ActionController::TestCase end class ConditionalParentOfConditionalSkippingController < ConditionalFilterController - before_filter :conditional_in_parent_before, :only => [:show, :another_action] - after_filter :conditional_in_parent_after, :only => [:show, :another_action] + before_action :conditional_in_parent_before, :only => [:show, :another_action] + after_action :conditional_in_parent_after, :only => [:show, :another_action] private @@ -308,20 +308,20 @@ class FilterTest < ActionController::TestCase end class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController - skip_before_filter :conditional_in_parent_before, :only => :another_action - skip_after_filter :conditional_in_parent_after, :only => :another_action + skip_before_action :conditional_in_parent_before, :only => :another_action + skip_after_action :conditional_in_parent_after, :only => :another_action end class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController - skip_before_filter :conditional_in_parent_before, :only => :show + skip_before_action :conditional_in_parent_before, :only => :show end class ProcController < PrependingController - before_filter(proc { |c| c.instance_variable_set(:"@ran_proc_filter", true) }) + before_action(proc { |c| c.instance_variable_set(:"@ran_proc_action", true) }) end class ImplicitProcController < PrependingController - before_filter { |c| c.instance_variable_set(:"@ran_proc_filter", true) } + before_action { |c| c.instance_variable_set(:"@ran_proc_action", true) } end class AuditFilter @@ -367,7 +367,7 @@ class FilterTest < ActionController::TestCase end class AuditController < ActionController::Base - before_filter(AuditFilter) + before_action(AuditFilter) def show render :text => "hello" @@ -375,14 +375,14 @@ class FilterTest < ActionController::TestCase end class AroundFilterController < PrependingController - around_filter AroundFilter.new + around_action AroundFilter.new end class BeforeAfterClassFilterController < PrependingController begin filter = AroundFilter.new - before_filter filter - after_filter filter + before_action filter + after_action filter end end @@ -394,18 +394,18 @@ class FilterTest < ActionController::TestCase super() end - before_filter { |c| c.class.execution_log << " before procfilter " } - prepend_around_filter AroundFilter.new + before_action { |c| c.class.execution_log << " before procfilter " } + prepend_around_action AroundFilter.new - after_filter { |c| c.class.execution_log << " after procfilter " } - append_around_filter AppendedAroundFilter.new + after_action { |c| c.class.execution_log << " after procfilter " } + append_around_action AppendedAroundFilter.new end class MixedSpecializationController < ActionController::Base class OutOfOrder < StandardError; end - before_filter :first - before_filter :second, :only => :foo + before_action :first + before_action :second, :only => :foo def foo render :text => 'foo' @@ -426,7 +426,7 @@ class FilterTest < ActionController::TestCase end class DynamicDispatchController < ActionController::Base - before_filter :choose + before_action :choose %w(foo bar baz).each do |action| define_method(action) { render :text => action } @@ -439,9 +439,9 @@ class FilterTest < ActionController::TestCase end class PrependingBeforeAndAfterController < ActionController::Base - prepend_before_filter :before_all - prepend_after_filter :after_all - before_filter :between_before_all_and_after_all + prepend_before_action :before_all + prepend_after_action :after_all + before_action :between_before_all_and_after_all def before_all @ran_filter ||= [] @@ -473,7 +473,7 @@ class FilterTest < ActionController::TestCase end class RescuedController < ActionController::Base - around_filter RescuingAroundFilterWithBlock.new + around_action RescuingAroundFilterWithBlock.new def show raise ErrorToRescue.new("Something made the bad noise.") @@ -482,10 +482,10 @@ class FilterTest < ActionController::TestCase class NonYieldingAroundFilterController < ActionController::Base - before_filter :filter_one - around_filter :non_yielding_filter - before_filter :filter_two - after_filter :filter_three + before_action :filter_one + around_action :non_yielding_action + before_action :action_two + after_action :action_three def index render :inline => "index" @@ -498,24 +498,24 @@ class FilterTest < ActionController::TestCase @filters << "filter_one" end - def filter_two - @filters << "filter_two" + def action_two + @filters << "action_two" end - def non_yielding_filter + def non_yielding_action @filters << "it didn't yield" @filter_return_value end - def filter_three - @filters << "filter_three" + def action_three + @filters << "action_three" end end class ImplicitActionsController < ActionController::Base - before_filter :find_only, :only => :edit - before_filter :find_except, :except => :edit + before_action :find_only, :only => :edit + before_action :find_except, :except => :edit private @@ -528,7 +528,7 @@ class FilterTest < ActionController::TestCase end end - def test_non_yielding_around_filters_not_returning_false_do_not_raise + def test_non_yielding_around_actions_not_returning_false_do_not_raise controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", true assert_nothing_raised do @@ -536,7 +536,7 @@ class FilterTest < ActionController::TestCase end end - def test_non_yielding_around_filters_returning_false_do_not_raise + def test_non_yielding_around_actions_returning_false_do_not_raise controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", false assert_nothing_raised do @@ -544,64 +544,64 @@ class FilterTest < ActionController::TestCase end end - def test_after_filters_are_not_run_if_around_filter_returns_false + def test_after_actions_are_not_run_if_around_action_returns_false controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", false test_process(controller, "index") assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] end - def test_after_filters_are_not_run_if_around_filter_does_not_yield + def test_after_actions_are_not_run_if_around_action_does_not_yield controller = NonYieldingAroundFilterController.new controller.instance_variable_set "@filter_return_value", true test_process(controller, "index") assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters'] end - def test_added_filter_to_inheritance_graph - assert_equal [ :ensure_login ], TestController.before_filters + def test_added_action_to_inheritance_graph + assert_equal [ :ensure_login ], TestController.before_actions end def test_base_class_in_isolation - assert_equal [ ], ActionController::Base.before_filters + assert_equal [ ], ActionController::Base.before_actions end - def test_prepending_filter - assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_filters + def test_prepending_action + assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_actions end - def test_running_filters + def test_running_actions test_process(PrependingController) assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"] end - def test_running_filters_with_proc + def test_running_actions_with_proc test_process(ProcController) - assert assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] end - def test_running_filters_with_implicit_proc + def test_running_actions_with_implicit_proc test_process(ImplicitProcController) - assert assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] end - def test_running_filters_with_class + def test_running_actions_with_class test_process(AuditController) assert assigns["was_audited"] end - def test_running_anomolous_yet_valid_condition_filters + def test_running_anomolous_yet_valid_condition_actions test_process(AnomolousYetValidConditionController) assert_equal %w( ensure_login ), assigns["ran_filter"] - assert assigns["ran_class_filter"] - assert assigns["ran_proc_filter1"] - assert assigns["ran_proc_filter2"] + assert assigns["ran_class_action"] + assert assigns["ran_proc_action1"] + assert assigns["ran_proc_action2"] - test_process(AnomolousYetValidConditionController, "show_without_filter") + test_process(AnomolousYetValidConditionController, "show_without_action") assert_nil assigns["ran_filter"] - assert !assigns["ran_class_filter"] - assert !assigns["ran_proc_filter1"] - assert !assigns["ran_proc_filter2"] + assert !assigns["ran_class_action"] + assert !assigns["ran_proc_action1"] + assert !assigns["ran_proc_action2"] end def test_running_conditional_options @@ -614,59 +614,59 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login ), assigns["ran_filter"] end - def test_skipping_class_filters + def test_skipping_class_actions test_process(ClassController) - assert_equal true, assigns["ran_class_filter"] + assert_equal true, assigns["ran_class_action"] skipping_class_controller = Class.new(ClassController) do - skip_before_filter ConditionalClassFilter + skip_before_action ConditionalClassFilter end test_process(skipping_class_controller) - assert_nil assigns['ran_class_filter'] + assert_nil assigns['ran_class_action'] end - def test_running_collection_condition_filters + def test_running_collection_condition_actions test_process(ConditionalCollectionFilterController) assert_equal %w( ensure_login ), assigns["ran_filter"] - test_process(ConditionalCollectionFilterController, "show_without_filter") + test_process(ConditionalCollectionFilterController, "show_without_action") assert_nil assigns["ran_filter"] test_process(ConditionalCollectionFilterController, "another_action") assert_nil assigns["ran_filter"] end - def test_running_only_condition_filters + def test_running_only_condition_actions test_process(OnlyConditionSymController) assert_equal %w( ensure_login ), assigns["ran_filter"] - test_process(OnlyConditionSymController, "show_without_filter") + test_process(OnlyConditionSymController, "show_without_action") assert_nil assigns["ran_filter"] test_process(OnlyConditionProcController) - assert assigns["ran_proc_filter"] - test_process(OnlyConditionProcController, "show_without_filter") - assert !assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] + test_process(OnlyConditionProcController, "show_without_action") + assert !assigns["ran_proc_action"] test_process(OnlyConditionClassController) - assert assigns["ran_class_filter"] - test_process(OnlyConditionClassController, "show_without_filter") - assert !assigns["ran_class_filter"] + assert assigns["ran_class_action"] + test_process(OnlyConditionClassController, "show_without_action") + assert !assigns["ran_class_action"] end - def test_running_except_condition_filters + def test_running_except_condition_actions test_process(ExceptConditionSymController) assert_equal %w( ensure_login ), assigns["ran_filter"] - test_process(ExceptConditionSymController, "show_without_filter") + test_process(ExceptConditionSymController, "show_without_action") assert_nil assigns["ran_filter"] test_process(ExceptConditionProcController) - assert assigns["ran_proc_filter"] - test_process(ExceptConditionProcController, "show_without_filter") - assert !assigns["ran_proc_filter"] + assert assigns["ran_proc_action"] + test_process(ExceptConditionProcController, "show_without_action") + assert !assigns["ran_proc_action"] test_process(ExceptConditionClassController) - assert assigns["ran_class_filter"] - test_process(ExceptConditionClassController, "show_without_filter") - assert !assigns["ran_class_filter"] + assert assigns["ran_class_action"] + test_process(ExceptConditionClassController, "show_without_action") + assert !assigns["ran_class_action"] end def test_running_only_condition_and_conditional_options @@ -674,70 +674,70 @@ class FilterTest < ActionController::TestCase assert_not assigns["ran_conditional_index_proc"] end - def test_running_before_and_after_condition_filters + def test_running_before_and_after_condition_actions test_process(BeforeAndAfterConditionController) assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"] - test_process(BeforeAndAfterConditionController, "show_without_filter") + test_process(BeforeAndAfterConditionController, "show_without_action") assert_nil assigns["ran_filter"] end - def test_around_filter + def test_around_action test_process(AroundFilterController) assert assigns["before_ran"] assert assigns["after_ran"] end - def test_before_after_class_filter + def test_before_after_class_action test_process(BeforeAfterClassFilterController) assert assigns["before_ran"] assert assigns["after_ran"] end - def test_having_properties_in_around_filter + def test_having_properties_in_around_action test_process(AroundFilterController) assert_equal "before and after", assigns["execution_log"] end - def test_prepending_and_appending_around_filter + def test_prepending_and_appending_around_action test_process(MixedFilterController) assert_equal " before aroundfilter before procfilter before appended aroundfilter " + " after appended aroundfilter after procfilter after aroundfilter ", MixedFilterController.execution_log end - def test_rendering_breaks_filtering_chain + def test_rendering_breaks_actioning_chain response = test_process(RenderingController) assert_equal "something else", response.body assert !assigns["ran_action"] end - def test_before_filter_rendering_breaks_filtering_chain_for_after_filter + def test_before_action_rendering_breaks_actioning_chain_for_after_action test_process(RenderingController) - assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + assert_equal %w( before_action_rendering ), assigns["ran_filter"] assert !assigns["ran_action"] end - def test_before_filter_redirects_breaks_filtering_chain_for_after_filter - test_process(BeforeFilterRedirectionController) + def test_before_action_redirects_breaks_actioning_chain_for_after_action + test_process(BeforeActionRedirectionController) assert_response :redirect - assert_equal "http://test.host/filter_test/before_filter_redirection/target_of_redirection", redirect_to_url - assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url + assert_equal %w( before_action_redirects ), assigns["ran_filter"] end - def test_before_filter_rendering_breaks_filtering_chain_for_preprend_after_filter - test_process(RenderingForPrependAfterFilterController) - assert_equal %w( before_filter_rendering ), assigns["ran_filter"] + def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action + test_process(RenderingForPrependAfterActionController) + assert_equal %w( before_action_rendering ), assigns["ran_filter"] assert !assigns["ran_action"] end - def test_before_filter_redirects_breaks_filtering_chain_for_preprend_after_filter - test_process(BeforeFilterRedirectionForPrependAfterFilterController) + def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action + test_process(BeforeActionRedirectionForPrependAfterActionController) assert_response :redirect - assert_equal "http://test.host/filter_test/before_filter_redirection_for_prepend_after_filter/target_of_redirection", redirect_to_url - assert_equal %w( before_filter_redirects ), assigns["ran_filter"] + assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url + assert_equal %w( before_action_redirects ), assigns["ran_filter"] end - def test_filters_with_mixed_specialization_run_in_order + def test_actions_with_mixed_specialization_run_in_order assert_nothing_raised do response = test_process(MixedSpecializationController, 'bar') assert_equal 'bar', response.body @@ -758,7 +758,7 @@ class FilterTest < ActionController::TestCase end end - def test_running_prepended_before_and_after_filter + def test_running_prepended_before_and_after_action test_process(PrependingBeforeAndAfterController) assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"] end @@ -775,26 +775,26 @@ class FilterTest < ActionController::TestCase assert_equal %w( find_record ensure_login ), assigns["ran_filter"] end - def test_conditional_skipping_of_filters + def test_conditional_skipping_of_actions test_process(ConditionalSkippingController, "login") assert_nil assigns["ran_filter"] test_process(ConditionalSkippingController, "change_password") assert_equal %w( ensure_login find_user ), assigns["ran_filter"] test_process(ConditionalSkippingController, "login") - assert !@controller.instance_variable_defined?("@ran_after_filter") + assert !@controller.instance_variable_defined?("@ran_after_action") test_process(ConditionalSkippingController, "change_password") - assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_filter") + assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action") end - def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional + def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional test_process(ChildOfConditionalParentController) assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] test_process(ChildOfConditionalParentController, 'another_action') assert_nil assigns['ran_filter'] end - def test_condition_skipping_of_filters_when_siblings_also_have_conditions + def test_condition_skipping_of_actions_when_siblings_also_have_conditions test_process(ChildOfConditionalParentController) assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter'] test_process(AnotherChildOfConditionalParentController) @@ -808,7 +808,7 @@ class FilterTest < ActionController::TestCase assert_nil assigns['ran_filter'] end - def test_a_rescuing_around_filter + def test_a_rescuing_around_action response = nil assert_nothing_raised do response = test_process(RescuedController) @@ -818,7 +818,7 @@ class FilterTest < ActionController::TestCase assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) end - def test_filters_obey_only_and_except_for_implicit_actions + def test_actions_obey_only_and_except_for_implicit_actions test_process(ImplicitActionsController, 'show') assert_equal 'Except', assigns(:except) assert_nil assigns(:only) @@ -852,7 +852,7 @@ class PostsController < ActionController::Base include AroundExceptions end - module_eval %w(raises_before raises_after raises_both no_raise no_filter).map { |action| "def #{action}; default_action end" }.join("\n") + module_eval %w(raises_before raises_after raises_both no_raise no_action).map { |action| "def #{action}; default_action end" }.join("\n") private def default_action @@ -861,9 +861,9 @@ class PostsController < ActionController::Base end class ControllerWithSymbolAsFilter < PostsController - around_filter :raise_before, :only => :raises_before - around_filter :raise_after, :only => :raises_after - around_filter :without_exception, :only => :no_raise + around_action :raise_before, :only => :raises_before + around_action :raise_after, :only => :raises_after + around_action :without_exception, :only => :no_raise private def raise_before @@ -895,7 +895,7 @@ class ControllerWithFilterClass < PostsController end end - around_filter YieldingFilter, :only => :raises_after + around_action YieldingFilter, :only => :raises_after end class ControllerWithFilterInstance < PostsController @@ -906,11 +906,11 @@ class ControllerWithFilterInstance < PostsController end end - around_filter YieldingFilter.new, :only => :raises_after + around_action YieldingFilter.new, :only => :raises_after end class ControllerWithProcFilter < PostsController - around_filter(:only => :no_raise) do |c,b| + around_action(:only => :no_raise) do |c,b| c.instance_variable_set(:"@before", true) b.call c.instance_variable_set(:"@after", true) @@ -918,14 +918,14 @@ class ControllerWithProcFilter < PostsController end class ControllerWithNestedFilters < ControllerWithSymbolAsFilter - around_filter :raise_before, :raise_after, :without_exception, :only => :raises_both + around_action :raise_before, :raise_after, :without_exception, :only => :raises_both end class ControllerWithAllTypesOfFilters < PostsController - before_filter :before - around_filter :around - after_filter :after - around_filter :around_again + before_action :before + around_action :around + after_action :after + around_action :around_again private def before @@ -951,8 +951,8 @@ class ControllerWithAllTypesOfFilters < PostsController end class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters - skip_filter :around_again - skip_filter :after + skip_action_callback :around_again + skip_action_callback :after end class YieldingAroundFiltersTest < ActionController::TestCase @@ -963,7 +963,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase assert_nothing_raised { test_process(controller,'no_raise') } assert_nothing_raised { test_process(controller,'raises_before') } assert_nothing_raised { test_process(controller,'raises_after') } - assert_nothing_raised { test_process(controller,'no_filter') } + assert_nothing_raised { test_process(controller,'no_action') } end def test_with_symbol @@ -992,7 +992,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase assert assigns['after'] end - def test_nested_filters + def test_nested_actions controller = ControllerWithNestedFilters assert_nothing_raised do begin @@ -1008,31 +1008,31 @@ class YieldingAroundFiltersTest < ActionController::TestCase end end - def test_filter_order_with_all_filter_types + def test_action_order_with_all_action_types test_process(ControllerWithAllTypesOfFilters,'no_raise') assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', assigns['ran_filter'].join(' ') end - def test_filter_order_with_skip_filter_method + def test_action_order_with_skip_action_method test_process(ControllerWithTwoLessFilters,'no_raise') assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ') end - def test_first_filter_in_multiple_before_filter_chain_halts + def test_first_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_1') assert_equal ' ', response.body assert_equal 1, controller.instance_variable_get(:@try) end - def test_second_filter_in_multiple_before_filter_chain_halts + def test_second_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_2') assert_equal ' ', response.body assert_equal 2, controller.instance_variable_get(:@try) end - def test_last_filter_in_multiple_before_filter_chain_halts + def test_last_action_in_multiple_before_action_chain_halts controller = ::FilterTest::TestMultipleFiltersController.new response = test_process(controller, 'fail_3') assert_equal ' ', response.body diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index 7396c850ad..2ddc07ef72 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -81,8 +81,8 @@ module BareMetalTest assert_nil headers['Content-Length'] end - test "head :continue (101) does not return a content-type header" do - headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + test "head :switching_protocols (101) does not return a content-type header" do + headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second assert_nil headers['Content-Type'] assert_nil headers['Content-Length'] end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index b22bc2dc25..9dc6d77012 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -87,7 +87,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_symbols_with_dashes rs.draw do get '/:artist/:song-omg', :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -99,7 +99,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_id_with_dash rs.draw do get '/journey/:id', :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -111,7 +111,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_dash_with_custom_regexp rs.draw do get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -124,7 +124,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_pre_dash rs.draw do get '/:artist/omg-:song', :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end @@ -136,7 +136,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_pre_dash_with_custom_regexp rs.draw do get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env| - resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters [200, {}, [resp]] } end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 18a5d8b094..1141feeff7 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -164,7 +164,7 @@ XML end class DefaultUrlOptionsCachingController < ActionController::Base - before_filter { @dynamic_opt = 'opt' } + before_action { @dynamic_opt = 'opt' } def test_url_options_reset render text: url_for(params) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 051431ce26..a427113763 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3368,15 +3368,14 @@ end class TestAltApp < ActionDispatch::IntegrationTest class AltRequest - attr_accessor :path_parameters + attr_accessor :path_parameters, :path_info, :script_name + attr_reader :env def initialize(env) @path_parameters = {} @env = env - end - - def path_info - "/" + @path_info = "/" + @script_name = "" end def request_method diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 92544230b2..f7a06cfed4 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -49,6 +49,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal 'foo: "bar"', response.body end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_nil_session_value @@ -57,6 +59,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal 'foo: nil', response.body end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_session_value_after_session_reset @@ -76,6 +80,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_from_nonexistent_session @@ -85,6 +91,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_equal 'foo: nil', response.body assert_nil cookies['_session_id'], "should only create session on write, not read" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_setting_session_value_after_session_reset @@ -106,6 +114,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_not_equal session_id, response.body end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_getting_session_id @@ -119,6 +129,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_deserializes_unloaded_class @@ -133,6 +145,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success end end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_doesnt_write_session_cookie_if_session_id_is_already_exists @@ -145,6 +159,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists" end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end def test_prevents_session_fixation @@ -160,6 +176,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_response :success assert_not_equal session_id, cookies['_session_id'] end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace end rescue LoadError, RuntimeError, Dalli::DalliError $stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again." diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index 910ff8a80f..a4dfd0a63d 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -15,6 +15,8 @@ module TestUrlGeneration Routes.draw do get "/foo", :to => "my_route_generating#index", :as => :foo + resources :bars + mount MyRouteGeneratingController.action(:index), at: '/bar' end @@ -109,6 +111,22 @@ module TestUrlGeneration test "omit subdomain when key is blank" do assert_equal "http://example.com/foo", foo_url(subdomain: "") end + + test "generating URLs with trailing slashes" do + assert_equal "/bars.json", bars_path( + trailing_slash: true, + format: 'json' + ) + end + + test "generating URLS with querystring and trailing slashes" do + assert_equal "/bars.json?a=b", bars_path( + trailing_slash: true, + a: 'b', + format: 'json' + ) + end + end end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 9a8d644f7b..1a2106a3c5 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -5,23 +5,22 @@ module ActionDispatch module Journey class TestRouter < ActiveSupport::TestCase # TODO : clean up routing tests so we don't need this hack - class StubDispatcher < Routing::RouteSet::Dispatcher; end + class StubDispatcher < Routing::RouteSet::Dispatcher + def initialize + super({}) + end + def dispatcher?; true; end + end attr_reader :routes def setup @app = StubDispatcher.new @routes = Routes.new - @router = Router.new(@routes, {}) + @router = Router.new(@routes) @formatter = Formatter.new(@routes) end - def test_request_class_reader - klass = Object.new - router = Router.new(routes, :request_class => klass) - assert_equal klass, router.request_class - end - class FakeRequestFeeler < Struct.new(:env, :called) def new env self.env = env @@ -39,7 +38,7 @@ module ActionDispatch end def test_dashes - router = Router.new(routes, {}) + router = Router.new(routes) exp = Router::Strexp.new '/foo-bar-baz', {}, ['/.?'] path = Path::Pattern.new exp @@ -55,7 +54,7 @@ module ActionDispatch end def test_unicode - router = Router.new(routes, {}) + router = Router.new(routes) #match the escaped version of /ほげ exp = Router::Strexp.new '/%E3%81%BB%E3%81%92', {}, ['/.?'] @@ -73,7 +72,7 @@ module ActionDispatch def test_request_class_and_requirements_success klass = FakeRequestFeeler.new nil - router = Router.new(routes, {:request_class => klass }) + router = Router.new(routes) requirements = { :hello => /world/ } @@ -82,7 +81,7 @@ module ActionDispatch routes.add_route nil, path, requirements, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/foo/10' + env = rails_env({'PATH_INFO' => '/foo/10'}, klass) router.recognize(env) do |r, params| assert_equal({:id => '10'}, params) end @@ -93,7 +92,7 @@ module ActionDispatch def test_request_class_and_requirements_fail klass = FakeRequestFeeler.new nil - router = Router.new(routes, {:request_class => klass }) + router = Router.new(routes) requirements = { :hello => /mom/ } @@ -102,7 +101,7 @@ module ActionDispatch router.routes.add_route nil, path, requirements, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/foo/10' + env = rails_env({'PATH_INFO' => '/foo/10'}, klass) router.recognize(env) do |r, params| flunk 'route should not be found' end @@ -111,21 +110,26 @@ module ActionDispatch assert_equal env.env, klass.env end - class CustomPathRequest < Router::NullReq + class CustomPathRequest < ActionDispatch::Request def path_info env['custom.path_info'] end + + def path_info=(x) + env['custom.path_info'] = x + end end def test_request_class_overrides_path_info - router = Router.new(routes, {:request_class => CustomPathRequest }) + router = Router.new(routes) exp = Router::Strexp.new '/bar', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {}, {} - env = rails_env 'PATH_INFO' => '/foo', 'custom.path_info' => '/bar' + env = rails_env({'PATH_INFO' => '/foo', + 'custom.path_info' => '/bar'}, CustomPathRequest) recognized = false router.recognize(env) do |r, params| @@ -207,20 +211,23 @@ module ActionDispatch def test_X_Cascade add_routes @router, [ "/messages(.:format)" ] - resp = @router.call({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }) + resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })) assert_equal ['Not Found'], resp.last assert_equal 'pass', resp[1]['X-Cascade'] assert_equal 404, resp.first end def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes + route_set = Routing::RouteSet.new + mapper = Routing::Mapper.new route_set + strexp = Router::Strexp.new("/", {}, ['/', '.', '?'], false) path = Path::Pattern.new strexp app = lambda { |env| [200, {}, ['success!']] } - @router.routes.add_route(app, path, {}, {}, {}) + mapper.get '/weblog', :to => app env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog') - resp = @router.call(env) + resp = route_set.call env assert_equal ['success!'], resp.last assert_equal '', env['SCRIPT_NAME'] end @@ -561,8 +568,8 @@ module ActionDispatch RailsEnv = Struct.new(:env) - def rails_env env - RailsEnv.new rack_env env + def rails_env env, klass = ActionDispatch::Request + klass.new env end def rack_env env diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 824cdaa45e..f53cce32b0 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -11,7 +11,7 @@ module ActionView # the assets exist before linking to them: # # image_tag("rails.png") - # # => <img alt="Rails" src="/assets/rails.png" /> + # # => <img alt="Rails" src="/images/rails.png" /> # stylesheet_link_tag("application") # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" /> module AssetTagHelper @@ -20,7 +20,8 @@ module ActionView include AssetUrlHelper include TagHelper - # Returns an HTML script tag for each of the +sources+ provided. + # Returns an HTML script tag for each of the +sources+ provided. If + # you don't specify an extension, <tt>.js</tt> will be appended automatically. # # Sources may be paths to JavaScript files. Relative paths are assumed to be relative # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document @@ -33,19 +34,19 @@ module ActionView # last argument. # # When the Asset Pipeline is enabled, you can pass the name of your manifest as - # source, and include other JavaScript or CoffeeScript files inside the manifest. + # source and include other JavaScript or CoffeeScript files inside the manifest. # # javascript_include_tag "xmlhr" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/javascripts/xmlhr.js?1284139606"></script> # # javascript_include_tag "template.jst", extname: false - # # => <script src="/assets/template.jst?1284139606"></script> + # # => <script src="/javascripts/template.jst?1284139606"></script> # # javascript_include_tag "xmlhr.js" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/javascripts/xmlhr.js?1284139606"></script> # # javascript_include_tag "common.javascript", "/elsewhere/cools" - # # => <script src="/assets/common.javascript?1284139606"></script> + # # => <script src="/javascripts/common.javascript?1284139606"></script> # # <script src="/elsewhere/cools.js?1423139606"></script> # # javascript_include_tag "http://www.example.com/xmlhr" @@ -72,22 +73,22 @@ module ActionView # apply to all media types. # # stylesheet_link_tag "style" - # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "style.css" - # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "http://www.example.com/style.css" # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" /> # # stylesheet_link_tag "style", media: "all" - # # => <link href="/assets/style.css" media="all" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="all" rel="stylesheet" /> # # stylesheet_link_tag "style", media: "print" - # # => <link href="/assets/style.css" media="print" rel="stylesheet" /> + # # => <link href="/stylesheets/style.css" media="print" rel="stylesheet" /> # # stylesheet_link_tag "random.styles", "/css/stylish" - # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" /> + # # => <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" /> # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys @@ -158,17 +159,17 @@ module ActionView # respectively: # # favicon_link_tag - # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" /> + # # => <link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" /> # # favicon_link_tag 'myicon.ico' - # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" /> + # # => <link href="/images/myicon.ico" rel="shortcut icon" type="image/x-icon" /> # # Mobile Safari looks for a different link tag, pointing to an image that # will be used if you add the page to the home screen of an iOS device. # The following call would generate such a tag: # # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' - # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> + # # => <link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" /> def favicon_link_tag(source='favicon.ico', options={}) tag('link', { :rel => 'shortcut icon', @@ -258,19 +259,19 @@ module ActionView # ==== Examples # # video_tag("trailer") - # # => <video src="/videos/trailer" /> + # # => <video src="/videos/trailer"></video> # video_tag("trailer.ogg") - # # => <video src="/videos/trailer.ogg" /> + # # => <video src="/videos/trailer.ogg"></video> # video_tag("trailer.ogg", controls: true, autobuffer: true) - # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" /> + # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") - # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" /> + # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/images/screenshot.png"></video> # video_tag("/trailers/hd.avi", size: "16x16") - # # => <video src="/trailers/hd.avi" width="16" height="16" /> + # # => <video src="/trailers/hd.avi" width="16" height="16"></video> # video_tag("/trailers/hd.avi", size: "16") - # # => <video height="16" src="/trailers/hd.avi" width="16" /> + # # => <video height="16" src="/trailers/hd.avi" width="16"></video> # video_tag("/trailers/hd.avi", height: '32', width: '32') - # # => <video height="32" src="/trailers/hd.avi" width="32" /> + # # => <video height="32" src="/trailers/hd.avi" width="32"></video> # video_tag("trailer.ogg", "trailer.flv") # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> # video_tag(["trailer.ogg", "trailer.flv"]) @@ -289,11 +290,11 @@ module ActionView # your public audios directory. # # audio_tag("sound") - # # => <audio src="/audios/sound" /> + # # => <audio src="/audios/sound"></audio> # audio_tag("sound.wav") - # # => <audio src="/audios/sound.wav" /> + # # => <audio src="/audios/sound.wav"></audio> # audio_tag("sound.wav", autoplay: true, controls: true) - # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" /> + # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio> # audio_tag("sound.wav", "sound.mid") # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio> def audio_tag(*sources) diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index ae684af87b..d86e7e490c 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -7,10 +7,10 @@ module ActionView # urls. # # image_path("rails.png") - # # => "/assets/rails.png" + # # => "/images/rails.png" # # image_url("rails.png") - # # => "http://www.example.com/assets/rails.png" + # # => "http://www.example.com/images/rails.png" # # === Using asset hosts # @@ -113,13 +113,13 @@ module ActionView # # All other asset *_path helpers delegate through this method. # - # asset_path "application.js" # => /application.js - # asset_path "application", type: :javascript # => /javascripts/application.js - # asset_path "application", type: :stylesheet # => /stylesheets/application.css + # asset_path "application.js" # => /assets/application.js + # asset_path "application", type: :javascript # => /assets/application.js + # asset_path "application", type: :stylesheet # => /assets/application.css # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def asset_path(source, options = {}) - source = source.to_s return "" unless source.present? + source = source.to_s return source if source =~ URI_REGEXP tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') @@ -153,7 +153,7 @@ module ActionView # All other options provided are forwarded to +asset_path+ call. # # asset_url "application.js" # => http://example.com/application.js - # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/javascripts/application.js + # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js # def asset_url(source, options = {}) path_to_asset(source, options.merge(:protocol => :request)) @@ -231,7 +231,7 @@ module ActionView # Computes the path to a javascript asset in the public javascripts directory. # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) # Full paths from the document root will be passed through. - # Used internally by javascript_include_tag to build the script path. + # Used internally by +javascript_include_tag+ to build the script path. # # javascript_path "xmlhr" # => /javascripts/xmlhr.js # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js @@ -251,7 +251,7 @@ module ActionView alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route # Computes the path to a stylesheet asset in the public stylesheets directory. - # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). + # If the +source+ filename has no extension, .css will be appended (except for explicit URIs). # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. # @@ -276,9 +276,9 @@ module ActionView # Full paths from the document root will be passed through. # Used internally by +image_tag+ to build the image path: # - # image_path("edit") # => "/assets/edit" - # image_path("edit.png") # => "/assets/edit.png" - # image_path("icons/edit.png") # => "/assets/icons/edit.png" + # image_path("edit") # => "/images/edit" + # image_path("edit.png") # => "/images/edit.png" + # image_path("icons/edit.png") # => "/images/icons/edit.png" # image_path("/icons/edit.png") # => "/icons/edit.png" # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png" # @@ -342,9 +342,9 @@ module ActionView # Computes the path to a font asset. # Full paths from the document root will be passed through. # - # font_path("font") # => /assets/font - # font_path("font.ttf") # => /assets/font.ttf - # font_path("dir/font.ttf") # => /assets/dir/font.ttf + # font_path("font") # => /fonts/font + # font_path("font.ttf") # => /fonts/font.ttf + # font_path("dir/font.ttf") # => /fonts/dir/font.ttf # font_path("/dir/font.ttf") # => /dir/font.ttf # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf def font_path(source, options = {}) diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 10dcb5c28c..1e818083cc 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -109,8 +109,8 @@ module ActionView # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option> # # <option>Out</option></select> # - # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input' - # # => <select class="form_input" id="access" multiple="multiple" name="access[]"><option>Read</option> + # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input', id: 'unique_id' + # # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option> # # <option>Write</option></select> # # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 017302d40f..c92d090cce 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -62,8 +62,8 @@ module ActionView # # The view class must have the following methods: # View.new[lookup_context, assigns, controller] - # Create a new ActionView instance for a controller - # View#render[options] + # Create a new ActionView instance for a controller and we can also pass the arguments. + # View#render(option) # Returns String with the rendered template # # Override this method in a module to change the default behavior. diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 3dd973d64b..736745c3cd 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,31 @@ +* Fix redefine a has_and_belongs_to_many inside inherited class + Fixing regression case, where redefining the same has_an_belongs_to_many + definition into a subclass would raise. + + Fixes #14983. + + *arthurnn* + +* Add a properties API to allow custom types and type casting behavior + to be specified. Will enable many edge cases to be deprecated, and + allow for additional interesting features in the future. + + *Sean Griffin* + +* Fix has_and_belongs_to_many public reflection. + When defining a has_and_belongs_to_many, internally we convert that to two has_many. + But as `reflections` is a public API, people expect to see the right macro. + + Fixes #14682. + + *arthurnn* + +* Fixed serialization for records with an attribute named `format`. + + Fixes #15188. + + *Godfrey Chan* + * When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum` on a NullRelation should return a Hash. @@ -16,13 +44,14 @@ * Change belongs_to touch to be consistent with timestamp updates - If a model is set up with a belongs_to: touch relatinoship the parent + If a model is set up with a belongs_to: touch relationship the parent record will only be touched if the record was modified. This makes it consistent with timestamp updating on the record itself. *Brock Trappitt* -* Fixed the inferred table name of a HABTM auxiliar table inside a schema. +* Fixed the inferred table name of a has_and_belongs_to_many auxiliar + table inside a schema. Fixes #14824 @@ -65,7 +94,7 @@ *Aaron Nelson* -* Fix how to calculate associated class name when using namespaced `has_and_belongs_to_many` +* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many association. Fixes #14709. @@ -118,7 +147,7 @@ *Innokenty Mikhailov* -* Allow the PostgreSQL adapter to handle bigserial pk types again. +* Allow the PostgreSQL adapter to handle bigserial primary key types again. Fixes #10410. @@ -133,10 +162,10 @@ *Yves Senn* -* Fixed HABTM's CollectionAssociation size calculation. +* Fixed has_and_belongs_to_many's CollectionAssociation size calculation. - HABTM should fall back to using the normal CollectionAssociation's size - calculation if the collection is not cached or loaded. + has_and_belongs_to_many should fall back to using the normal CollectionAssociation's + size calculation if the collection is not cached or loaded. Fixes #14913, #14914. @@ -180,10 +209,10 @@ *Eric Chahin*, *Aaron Nelson*, *Kevin Casey* -* Stringify all variable keys of mysql connection configuration. +* Stringify all variables keys of MySQL connection configuration. - When the `sql_mode` variable for mysql adapters is set in the configuration - as a `String`, it was ignored and overwritten by the strict mode option. + When `sql_mode` variable for MySQL adapters set in configuration as `String` + was ignored and overwritten by strict mode option. Fixes #14895. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 727ee5f65f..07dfc448e7 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -50,7 +50,7 @@ module ActiveRecord def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names - source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } + source_associations = reflection.through_reflection.klass._reflections.keys super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -151,7 +151,7 @@ module ActiveRecord association = association_instance_get(name) if association.nil? - raise AssociationNotFoundError.new(self, name) unless reflection = self.class.reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name) association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end @@ -202,12 +202,13 @@ module ActiveRecord # For instance, +attributes+ and +connection+ would be bad choices for association names. # # == Auto-generated methods + # See also Instance Public methods below for more details. # # === Singular associations (one-to-one) # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other | X | X | X + # other(force_reload=false) | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -217,7 +218,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others | X | X | X + # others(force_reload=false) | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -1576,6 +1577,8 @@ module ActiveRecord scope = nil end + habtm_reflection = ActiveRecord::Reflection::AssociationReflection.new(:has_and_belongs_to_many, name, scope, options, self) + builder = Builder::HasAndBelongsToMany.new name, self, options join_model = builder.through_model @@ -1589,6 +1592,7 @@ module ActiveRecord Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection + middle_reflection.parent_reflection = [name.to_s, habtm_reflection] include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -1609,6 +1613,7 @@ module ActiveRecord end has_many name, scope, hm_options, &extension + self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection] end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 85109aee6c..a6a1947148 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -32,8 +32,18 @@ module ActiveRecord join.left.downcase.scan( /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ ).size - else + elsif join.respond_to? :left join.left.table_name == name ? 1 : 0 + else + # this branch is reached by two tests: + # + # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 + # with :posts + # + # activerecord/test/cases/associations/eager_test.rb:1133 + # with :comments + # + 0 end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 9ad2d2fb12..4a04303fb8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -160,7 +160,7 @@ module ActiveRecord def marshal_load(data) reflection_name, ivars = data ivars.each { |name, val| instance_variable_set(name, val) } - @reflection = @owner.class.reflect_on_association(reflection_name) + @reflection = @owner.class._reflect_on_association(reflection_name) end def initialize_attributes(record) #:nodoc: diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 30b11c01eb..0ad5206980 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -66,13 +66,13 @@ module ActiveRecord::Associations::Builder def self.add_left_association(name, options) belongs_to name, options - self.left_reflection = reflect_on_association(name) + self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym belongs_to rhs_name, options - self.right_reflection = reflect_on_association(rhs_name) + self.right_reflection = _reflect_on_association(rhs_name) end } diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index f5e911c739..2727e23870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -100,7 +100,8 @@ module ActiveRecord # Hence this method. def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + reflection.klass._reflections.values.any? { |inverse_reflection| + :belongs_to == inverse_reflection.macro && inverse_reflection.counter_cache_column == counter_name } end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 5842be3a7b..01173b68f3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -207,7 +207,7 @@ module ActiveRecord end def find_reflection(klass, name) - klass.reflect_on_association(name) or + klass._reflect_on_association(name) or raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 42571d6af0..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -143,6 +143,7 @@ module ActiveRecord def grouped_records(association, records) h = {} records.each do |record| + next unless record assoc = record.association(association) klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 6c2403d87e..a0a0214eae 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -48,7 +48,11 @@ module ActiveRecord end private - def method_body; raise NotImplementedError; end + + # Override this method in the subclasses for method body. + def method_body(method_name, const_name) + raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." + end end module ClassMethods @@ -66,6 +70,7 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: + return false if @attribute_methods_generated # Use a mutex; we don't want two thread simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do @@ -456,7 +461,7 @@ module ActiveRecord end def pk_attribute?(name) - column_for_attribute(name).primary + name == self.class.primary_key end def typecasted_attribute_value(name) diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 53a9c874bf..47c6f94ba7 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -65,6 +65,8 @@ module ActiveRecord end class Type # :nodoc: + delegate :type, :type_cast_for_database, to: :@column + def initialize(column) @column = column end @@ -77,10 +79,6 @@ module ActiveRecord end end - def type - @column.type - end - def accessor ActiveRecord::Store::IndifferentHashAccessor end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index dfebb2cf56..6149ac4906 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,6 +2,8 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class Type # :nodoc: + delegate :type, :type_cast_for_database, to: :@column + def initialize(column) @column = column end @@ -10,10 +12,6 @@ module ActiveRecord value = @column.type_cast(value) value.acts_like?(:time) ? value.in_time_zone : value end - - def type - @column.type - end end extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 1a4d2957ec..74e2a8e6b9 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -147,6 +147,7 @@ module ActiveRecord private def define_non_cyclic_method(name, &block) + return if method_defined?(name) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations @@ -179,30 +180,28 @@ module ActiveRecord validation_method = :"validate_associated_records_for_#{reflection.name}" collection = reflection.collection? - unless method_defined?(save_method) - if collection - before_save :before_save_collection_association - - define_non_cyclic_method(save_method) { save_collection_association(reflection) } - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create save_method - after_update save_method - elsif reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } - before_save save_method - end + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create save_method + after_update save_method + elsif reflection.macro == :has_one + define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method + else + define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method end if reflection.validate? && !method_defined?(validation_method) @@ -273,9 +272,11 @@ module ActiveRecord # go through nested autosave associations that are loaded in memory (without loading # any new ones), and return true if is changed for autosave def nested_records_changed_for_autosave? - self.class.reflect_on_all_autosave_associations.any? do |reflection| - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index db4d5f0129..8b0fffcf06 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -19,6 +19,7 @@ require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' +require 'active_record/properties' module ActiveRecord #:nodoc: # = Active Record @@ -321,6 +322,7 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store + include Properties end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 75501852ed..f836e60988 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,26 +9,22 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) + # FIXME: The only case we get an object other than nil or a real column + # is `SchemaStatements#add_column` with a PG array that has a non-empty default + # value. Is this really the only case? Are we missing tests for other types? + # We should have a real column object passed (or nil) here, and check for that + # instead + if column.respond_to?(:type_cast_for_database) + value = column.type_cast_for_database(value) + end + case value when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return "'#{quote_string(value)}'" unless column - - case column.type - when :integer then value.to_i.to_s - when :float then value.to_f.to_s - else - "'#{quote_string(value)}'" - end - - when true, false - if column && column.type == :integer - value ? '1' : '0' - else - value ? quoted_true : quoted_false - end - # BigDecimals need to be put in a non-normalized form and quoted. + "'#{quote_string(value.to_s)}'" + when true then quoted_true + when false then quoted_false when nil then "NULL" + # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') when Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" @@ -47,30 +43,25 @@ module ActiveRecord return value.id end - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return value unless column - - case column.type - when :integer then value.to_i - when :float then value.to_f - else - value - end + # FIXME: The only case we get an object other than nil or a real column + # is `SchemaStatements#add_column` with a PG array that has a non-empty default + # value. Is this really the only case? Are we missing tests for other types? + # We should have a real column object passed (or nil) here, and check for that + # instead + if column.respond_to?(:type_cast_for_database) + value = column.type_cast_for_database(value) + end - when true, false - if column && column.type == :integer - value ? 1 : 0 - else - value ? 't' : 'f' - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then nil + case value + when Symbol, ActiveSupport::Multibyte::Chars + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s('F') - when Numeric then value when Date, Time then quoted_date(value) - when Symbol then value.to_s + when *types_which_need_no_typecasting + value else to_type = column ? " to #{column.type}" : "" raise TypeError, "can't cast #{value.class}#{to_type}" @@ -109,10 +100,18 @@ module ActiveRecord "'t'" end + def unquoted_true + 't' + end + def quoted_false "'f'" end + def unquoted_false + 'f' + end + def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal @@ -124,6 +123,12 @@ module ActiveRecord value.to_s(:db) end + + private + + def types_which_need_no_typecasting + [nil, Numeric, String] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index cdf0cbe218..ac14740cfe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -20,15 +20,8 @@ module ActiveRecord def prepare_column_options(column, types) spec = {} spec[:name] = column.name.inspect - - # AR has an optimization which handles zero-scale decimals as integers. This - # code ensures that the dumper still dumps the column as a decimal. - spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type - 'decimal' - else - column.type.to_s - end - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:type] = column.type.to_s + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ea7703c82c..6ecd4efdc8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -396,7 +396,8 @@ module ActiveRecord precision = extract_precision(sql_type) if scale == 0 - Type::Integer.new(precision: precision) + # FIXME: Remove this class as well + Type::DecimalWithoutScale.new(precision: precision) else Type::Decimal.new(precision: precision, scale: scale) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index d3f8470c30..82e62786ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -178,17 +178,6 @@ module ActiveRecord true end - def type_cast(value, column) - case value - when TrueClass - 1 - when FalseClass - 0 - else - super - end - end - # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # @@ -234,8 +223,6 @@ module ActiveRecord if value.kind_of?(String) && column && column.type == :binary s = value.unpack("H*")[0] "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") else super end @@ -253,10 +240,18 @@ module ActiveRecord QUOTED_TRUE end + def unquoted_true + 1 + end + def quoted_false QUOTED_FALSE end + def unquoted_false + 0 + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 704868c058..86232f9d3f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -14,11 +14,12 @@ module ActiveRecord end attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function - attr_accessor :primary, :coder + attr_accessor :coder alias :encoded? :coder - delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, :type_cast_for_write, to: :cast_type + delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, + :type_cast_for_write, :type_cast_for_database, to: :cast_type # Instantiates a new column in the table. # @@ -36,7 +37,6 @@ module ActiveRecord @null = null @default = extract_default(default) @default_function = nil - @primary = nil @coder = nil end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 0cbedb0987..f7bad20f00 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -6,13 +6,6 @@ module ActiveRecord "(#{point[0]},#{point[1]})" end - def string_to_point(string) # :nodoc: - if string[0] == '(' && string[-1] == ')' - string = string[1...-1] - end - string.split(',').map{ |v| Float(v) } - end - def string_to_bit(value) # :nodoc: case value when /^0x/i diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index b55766bde0..9a5e2d05ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -6,18 +6,16 @@ module ActiveRecord class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - def initialize(name, default, cast_type, sql_type = nil, null = true) - default_value = self.class.extract_value_from_default(default) - + def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) if sql_type =~ /\[\]$/ @array = true - super(name, default_value, cast_type, sql_type[0..sql_type.length - 3], null) + super(name, default, cast_type, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, default_value, cast_type, sql_type, null) + super(name, default, cast_type, sql_type, null) end - @default_function = default if has_default_function?(default_value, default) + @default_function = default_function end # :stopdoc: @@ -38,78 +36,9 @@ module ActiveRecord end # :startdoc: - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - case default - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ - $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil - end - end - def accessor cast_type.accessor end - - private - - def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)} === default) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 2769a8d3b4..f9531ddee3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -5,7 +5,10 @@ module ActiveRecord class Point < Type::String def type_cast(value) if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_point value + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + value.split(',').map{ |v| Float(v) } else value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb new file mode 100644 index 0000000000..bcfd605165 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -0,0 +1,134 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnMethods + def xml(*args) + options = args.extract_options! + column(args[0], 'xml', options) + end + + def tsvector(*args) + options = args.extract_options! + column(args[0], 'tsvector', options) + end + + def int4range(name, options = {}) + column(name, 'int4range', options) + end + + def int8range(name, options = {}) + column(name, 'int8range', options) + end + + def tsrange(name, options = {}) + column(name, 'tsrange', options) + end + + def tstzrange(name, options = {}) + column(name, 'tstzrange', options) + end + + def numrange(name, options = {}) + column(name, 'numrange', options) + end + + def daterange(name, options = {}) + column(name, 'daterange', options) + end + + def hstore(name, options = {}) + column(name, 'hstore', options) + end + + def ltree(name, options = {}) + column(name, 'ltree', options) + end + + def inet(name, options = {}) + column(name, 'inet', options) + end + + def cidr(name, options = {}) + column(name, 'cidr', options) + end + + def macaddr(name, options = {}) + column(name, 'macaddr', options) + end + + def uuid(name, options = {}) + column(name, 'uuid', options) + end + + def json(name, options = {}) + column(name, 'json', options) + end + + def citext(name, options = {}) + column(name, 'citext', options) + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, options = {}) + return super unless type == :uuid + options[:default] = options.fetch(:default, 'uuid_generate_v4()') + options[:primary_key] = true + column name, type, options + end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def create_column_definition(name, type) + PostgreSQL::ColumnDefinition.new name, type + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index c04a1d7178..484c44dc8d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -5,7 +5,7 @@ module ActiveRecord private def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" add_column_options!(sql, column_options(o)) end @@ -179,7 +179,9 @@ module ActiveRecord # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + default_value = extract_value_from_default(default) + default_function = extract_default_function(default_value, default) + PostgreSQLColumn.new(column_name, default_value, oid, type, notnull == 'f', default_function) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e3a2422160..027169ae3c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -6,6 +6,7 @@ require 'active_record/connection_adapters/postgresql/column' require 'active_record/connection_adapters/postgresql/oid' require 'active_record/connection_adapters/postgresql/quoting' require 'active_record/connection_adapters/postgresql/referential_integrity' +require 'active_record/connection_adapters/postgresql/schema_definitions' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' @@ -73,139 +74,6 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - - module ColumnMethods - def xml(*args) - options = args.extract_options! - column(args[0], 'xml', options) - end - - def tsvector(*args) - options = args.extract_options! - column(args[0], 'tsvector', options) - end - - def int4range(name, options = {}) - column(name, 'int4range', options) - end - - def int8range(name, options = {}) - column(name, 'int8range', options) - end - - def tsrange(name, options = {}) - column(name, 'tsrange', options) - end - - def tstzrange(name, options = {}) - column(name, 'tstzrange', options) - end - - def numrange(name, options = {}) - column(name, 'numrange', options) - end - - def daterange(name, options = {}) - column(name, 'daterange', options) - end - - def hstore(name, options = {}) - column(name, 'hstore', options) - end - - def ltree(name, options = {}) - column(name, 'ltree', options) - end - - def inet(name, options = {}) - column(name, 'inet', options) - end - - def cidr(name, options = {}) - column(name, 'cidr', options) - end - - def macaddr(name, options = {}) - column(name, 'macaddr', options) - end - - def uuid(name, options = {}) - column(name, 'uuid', options) - end - - def json(name, options = {}) - column(name, 'json', options) - end - - def citext(name, options = {}) - column(name, 'citext', options) - end - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - # Defines the primary key field. - # Use of the native PostgreSQL UUID type is supported, and can be used - # by defining your tables as such: - # - # create_table :stuffs, id: :uuid do |t| - # t.string :content - # t.timestamps - # end - # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: - # - # create_table :stuffs, id: false do |t| - # t.primary_key :id, :uuid, default: nil - # t.uuid :foo_id - # t.timestamps - # end - # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. - # - # Note that setting the UUID primary key default value to +nil+ will - # require you to assure that you always provide a UUID value before saving - # a record (as primary keys cannot be +nil+). This might be done via the - # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. - def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options - end - - def citext(name, options = {}) - column(name, 'citext', options) - end - - def column(name, type = nil, options = {}) - super - column = self[name] - column.array = options[:array] - - self - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new name, type - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { @@ -251,13 +119,13 @@ module ActiveRecord ADAPTER_NAME end - def schema_creation + def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end # Adds `:array` option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) + def prepare_column_options(column, types) # :nodoc: spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array spec[:default] = "\"#{column.default_function}\"" if column.default_function @@ -509,7 +377,7 @@ module ActiveRecord end def update_table_definition(table_name, base) #:nodoc: - Table.new(table_name, base) + PostgreSQL::Table.new(table_name, base) end protected @@ -538,12 +406,12 @@ module ActiveRecord private - def get_oid_type(oid, fmod, column_name, sql_type = '') + def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc: if !type_map.key?(oid) load_additional_types(type_map, [oid]) end - type_map.fetch(normalize_oid_type(oid, fmod), sql_type) { + type_map.fetch(oid, fmod, sql_type) { warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." Type::Value.new.tap do |cast_type| type_map.register_type(oid, cast_type) @@ -551,24 +419,7 @@ module ActiveRecord } end - OID_FOR_DECIMAL_TREATED_AS_INT = 23 # :nodoc: - - def normalize_oid_type(ftype, fmod) - # The type for the numeric depends on the width of the field, - # so we'll do something special here. - # - # When dealing with decimal columns: - # - # places after decimal = fmod - 4 & 0xffff - # places before decimal = (fmod - 4) >> 16 & 0xffff - if ftype == 1700 && (fmod - 4 & 0xffff).zero? - OID_FOR_DECIMAL_TREATED_AS_INT - else - ftype - end - end - - def initialize_type_map(m) + def initialize_type_map(m) # :nodoc: register_class_with_limit m, 'int2', OID::Integer m.alias_type 'int4', 'int2' m.alias_type 'int8', 'int2' @@ -610,20 +461,29 @@ module ActiveRecord m.alias_type 'lseg', 'varchar' m.alias_type 'box', 'varchar' - m.register_type 'timestamp' do |_, sql_type| + m.register_type 'timestamp' do |_, _, sql_type| precision = extract_precision(sql_type) OID::DateTime.new(precision: precision) end - m.register_type 'numeric' do |_, sql_type| + m.register_type 'numeric' do |_, fmod, sql_type| precision = extract_precision(sql_type) scale = extract_scale(sql_type) - OID::Decimal.new(precision: precision, scale: scale) - end - m.register_type OID_FOR_DECIMAL_TREATED_AS_INT do |_, sql_type| - precision = extract_precision(sql_type) - OID::Integer.new(precision: precision) + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if fmod && (fmod - 4 & 0xffff).zero? + # FIXME: Remove this class, and the second argument to + # lookups on PG + Type::DecimalWithoutScale.new(precision: precision) + else + OID::Decimal.new(precision: precision, scale: scale) + end end load_additional_types(m) @@ -637,7 +497,78 @@ module ActiveRecord end end - def load_additional_types(type_map, oids = nil) + # Extracts the value from a PostgreSQL column default definition. + def extract_value_from_default(default) # :nodoc: + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + + case default + when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m + $1 + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + $1 + # Character types + when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m + $1.gsub(/''/, "'") + # Binary data types + when /\A'(.*)'::bytea\z/m + $1 + # Date/time types + when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ + $1 + when /\A'(.*)'::interval\z/ + $1 + # Boolean type + when 'true' + true + when 'false' + false + # Geometric types + when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ + $1 + # Network address types + when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ + $1 + # Bit string types + when /\AB'(.*)'::"?bit(?: varying)?"?\z/ + $1 + # XML type + when /\A'(.*)'::xml\z/m + $1 + # Arrays + when /\A'(.*)'::"?\D+"?\[\]\z/ + $1 + # Hstore + when /\A'(.*)'::hstore\z/ + $1 + # JSON + when /\A'(.*)'::json\z/ + $1 + # Object identifier types + when /\A-?\d+\z/ + $1 + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil + end + end + + def extract_default_function(default_value, default) # :nodoc: + default if has_default_function?(default_value, default) + end + + def has_default_function?(default_value, default) # :nodoc: + !default_value && (%r{\w+\(.*\)} === default) + end + + def load_additional_types(type_map, oids = nil) # :nodoc: if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -831,23 +762,13 @@ module ActiveRecord end_sql end - def extract_pg_identifier_from_name(name) # :nodoc: - match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) - - if match_data - rest = name[match_data[0].length, name.length] - rest = rest[1, rest.length] if rest.start_with? "." - [match_data[1], (rest.length > 0 ? rest : nil)] - end - end - def extract_table_ref_from_insert_sql(sql) # :nodoc: sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end def create_table_definition(name, temporary, options, as = nil) # :nodoc: - TableDefinition.new native_database_types, name, temporary, options, as + PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index e5c9f6f54a..4d8afcf16a 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -12,11 +12,10 @@ module ActiveRecord @columns_hash = {} @primary_keys = {} @tables = {} - prepare_default_proc end def primary_keys(table_name) - @primary_keys[table_name] + @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. @@ -29,9 +28,9 @@ module ActiveRecord # Add internal cache for table with +table_name+. def add(table_name) if table_exists?(table_name) - @primary_keys[table_name] - @columns[table_name] - @columns_hash[table_name] + primary_keys(table_name) + columns(table_name) + columns_hash(table_name) end end @@ -40,14 +39,16 @@ module ActiveRecord end # Get the columns for a table - def columns(table) - @columns[table] + def columns(table_name) + @columns[table_name] ||= connection.columns(table_name) end # Get the columns for a table as a hash, key is the column name # value is the column object. - def columns_hash(table) - @columns_hash[table] + def columns_hash(table_name) + @columns_hash[table_name] ||= Hash[columns(table_name).map { |col| + [col.name, col] + }] end # Clears out internal caches @@ -76,32 +77,11 @@ 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 { |val| - Hash[val] - } + [@version, @columns, @columns_hash, @primary_keys, @tables] end def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array - prepare_default_proc - end - - private - - def prepare_default_proc - @columns.default_proc = Proc.new do |h, table_name| - h[table_name] = connection.columns(table_name) - end - - @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns(table_name).map { |col| - [col.name, col] - }] - end - - @primary_keys.default_proc = Proc.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type.rb b/activerecord/lib/active_record/connection_adapters/type.rb index 395a4160a8..bab7a3ff7e 100644 --- a/activerecord/lib/active_record/connection_adapters/type.rb +++ b/activerecord/lib/active_record/connection_adapters/type.rb @@ -7,6 +7,7 @@ require 'active_record/connection_adapters/type/boolean' require 'active_record/connection_adapters/type/date' require 'active_record/connection_adapters/type/date_time' require 'active_record/connection_adapters/type/decimal' +require 'active_record/connection_adapters/type/decimal_without_scale' require 'active_record/connection_adapters/type/float' require 'active_record/connection_adapters/type/integer' require 'active_record/connection_adapters/type/string' diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb index 4b2d1a66e0..60afe44de1 100644 --- a/activerecord/lib/active_record/connection_adapters/type/binary.rb +++ b/activerecord/lib/active_record/connection_adapters/type/binary.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Binary < Value # :nodoc: + class Binary < Value def type :binary end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb index 2337bdd563..0d97379189 100644 --- a/activerecord/lib/active_record/connection_adapters/type/boolean.rb +++ b/activerecord/lib/active_record/connection_adapters/type/boolean.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Boolean < Value # :nodoc: + class Boolean < Value def type :boolean end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb index 1e7205fd0b..e8becbe1f4 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Date < Value # :nodoc: + class Date < Value def type :date end diff --git a/activerecord/lib/active_record/connection_adapters/type/date_time.rb b/activerecord/lib/active_record/connection_adapters/type/date_time.rb index c34f4c5a53..64f5d05301 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date_time.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class DateTime < Value # :nodoc: + class DateTime < Value include TimeValue def type diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb index ac5af4b963..e93906ba19 100644 --- a/activerecord/lib/active_record/connection_adapters/type/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Decimal < Value # :nodoc: + class Decimal < Value include Numeric def type diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb b/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb new file mode 100644 index 0000000000..e58c6e198d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb @@ -0,0 +1,13 @@ +require 'active_record/connection_adapters/type/integer' + +module ActiveRecord + module ConnectionAdapters + module Type + class DecimalWithoutScale < Integer # :nodoc: + def type + :decimal + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/float.rb b/activerecord/lib/active_record/connection_adapters/type/float.rb index 51cfa5d86a..f2427d2dfa 100644 --- a/activerecord/lib/active_record/connection_adapters/type/float.rb +++ b/activerecord/lib/active_record/connection_adapters/type/float.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Float < Value # :nodoc: + class Float < Value include Numeric def type @@ -12,6 +12,8 @@ module ActiveRecord ::Float end + alias type_cast_for_database type_cast + private def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb index 8f3469434c..596f4de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/type/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/type/integer.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Integer < Value # :nodoc: + class Integer < Value include Numeric def type @@ -12,6 +12,8 @@ module ActiveRecord ::Fixnum end + alias type_cast_for_database type_cast + private def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb index 55f0e1ee1c..471f949e09 100644 --- a/activerecord/lib/active_record/connection_adapters/type/string.rb +++ b/activerecord/lib/active_record/connection_adapters/type/string.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class String < Value # :nodoc: + class String < Value def type :string end diff --git a/activerecord/lib/active_record/connection_adapters/type/text.rb b/activerecord/lib/active_record/connection_adapters/type/text.rb index ee5842a3fc..61095ebb38 100644 --- a/activerecord/lib/active_record/connection_adapters/type/text.rb +++ b/activerecord/lib/active_record/connection_adapters/type/text.rb @@ -3,7 +3,7 @@ require 'active_record/connection_adapters/type/string' module ActiveRecord module ConnectionAdapters module Type - class Text < String # :nodoc: + class Text < String def type :text end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb index 4dd201e3fe..bc331b0fa7 100644 --- a/activerecord/lib/active_record/connection_adapters/type/time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/time.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Time < Value # :nodoc: + class Time < Value include TimeValue def type diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb index 54a3e9dd7a..60b443004c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/value.rb +++ b/activerecord/lib/active_record/connection_adapters/type/value.rb @@ -1,9 +1,11 @@ module ActiveRecord module ConnectionAdapters module Type - class Value # :nodoc: + class Value attr_reader :precision, :scale, :limit + # Valid options are +precision+, +scale+, and +limit+. + # They are only used when dumping schema. def initialize(options = {}) options.assert_valid_keys(:precision, :scale, :limit) @precision = options[:precision] @@ -11,8 +13,13 @@ module ActiveRecord @limit = options[:limit] end + # The simplified that this object represents. Subclasses + # should override this method. def type; end + # Takes an input from the database, or from attribute setters, + # and casts it to a type appropriate for this object. This method + # should not be overriden by subclasses. Instead, override `cast_value`. def type_cast(value) cast_value(value) unless value.nil? end @@ -21,6 +28,10 @@ module ActiveRecord value end + def type_cast_for_database(value) + type_cast_for_write(value) + end + def text? false end @@ -39,7 +50,9 @@ module ActiveRecord private - def cast_value(value) + # Responsible for casting values from external sources to the appropriate + # type. Called by `type_cast` for all values except `nil`. + def cast_value(value) # :api: public value end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 4571cc0786..07eafef788 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -286,6 +286,8 @@ module ActiveRecord @new_record = false + self.class.define_attribute_methods + run_callbacks :find run_callbacks :initialize diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 71e176a328..05c4b13016 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -20,7 +20,7 @@ module ActiveRecord def reset_counters(id, *counters) object = find(id) counters.each do |counter_association| - has_many_association = reflect_on_association(counter_association.to_sym) + has_many_association = _reflect_on_association(counter_association.to_sym) unless has_many_association has_many = reflect_on_all_associations(:has_many) has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } @@ -34,8 +34,7 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ @@ -167,7 +166,7 @@ module ActiveRecord end def each_counter_cached_associations - reflections.each do |name, reflection| + _reflections.each do |name, reflection| yield association(name) if reflection.belongs_to? && reflection.counter_cache_column end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 47d32fae05..d40bea5ea7 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -649,7 +649,7 @@ module ActiveRecord model_class end - reflection_class.reflect_on_all_associations.each do |association| + reflection_class._reflections.values.each do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index aa1166750f..a4e10ed2e7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -217,20 +217,6 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - # 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| - col = col.dup - col.primary = (col.name == primary_key) - col - end - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] - end - def column_types # :nodoc: @column_types ||= decorate_columns(columns_hash.dup) end @@ -271,7 +257,7 @@ module ActiveRecord # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns - @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end # Resets all the cached information about columns, which will cause them @@ -308,8 +294,6 @@ module ActiveRecord @arel_engine = nil @column_defaults = nil @column_names = nil - @columns = nil - @columns_hash = nil @column_types = nil @content_columns = nil @dynamic_methods_hash = nil diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 29ed499b1b..7dc7169a02 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -305,7 +305,7 @@ module ActiveRecord options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank attr_names.each do |association_name| - if reflection = reflect_on_association(association_name) + if reflection = _reflect_on_association(association_name) reflection.autosave = true add_autosave_association_callbacks(reflection) @@ -542,7 +542,7 @@ module ActiveRecord end def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end end diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/properties.rb new file mode 100644 index 0000000000..39c39ad9ff --- /dev/null +++ b/activerecord/lib/active_record/properties.rb @@ -0,0 +1,107 @@ +module ActiveRecord + module Properties + extend ActiveSupport::Concern + + Type = ConnectionAdapters::Type + + module ClassMethods + # Defines or overrides a property on this model. This allows customization of + # Active Record's type casting behavior, as well as adding support for user defined + # types. + # + # ==== Examples + # + # The type detected by Active Record can be overriden. + # + # # db/schema.rb + # create_table :store_listings, force: true do |t| + # t.decimal :price_in_cents + # end + # + # # app/models/store_listing.rb + # class StoreListing < ActiveRecord::Base + # end + # + # store_listing = StoreListing.new(price_in_cents: '10.1') + # + # # before + # store_listing.price_in_cents # => BigDecimal.new(10.1) + # + # class StoreListing < ActiveRecord::Base + # property :price_in_cents, Type::Integer.new + # end + # + # # after + # store_listing.price_in_cents # => 10 + # + # Users may also define their own custom types, as long as they respond to the methods + # defined on the value type. The `type_cast` method on your type object will be called + # with values both from the database, and from your controllers. See + # `ActiveRecord::Properties::Type::Value` for the expected API. It is recommended that your + # type objects inherit from an existing type, or the base value type. + # + # class MoneyType < ActiveRecord::Type::Integer + # def type_cast(value) + # if value.include?('$') + # price_in_dollars = value.gsub(/\$/, '').to_f + # price_in_dollars * 100 + # else + # value.to_i + # end + # end + # end + # + # class StoreListing < ActiveRecord::Base + # property :price_in_cents, MoneyType.new + # end + # + # store_listing = StoreListing.new(price_in_cents: '$10.00') + # store_listing.price_in_cents # => 1000 + def property(name, cast_type) + name = name.to_s + user_provided_columns[name] = ConnectionAdapters::Column.new(name, nil, cast_type) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)).each do |column| + if Type::DecimalWithoutScale === column.cast_type + ActiveSupport::Deprecation.warn <<-MESSAGE.strip_heredoc + Decimal columns with 0 scale being automatically treated as integers + is deprecated, and will be removed in a future version of Rails. If + you'd like to keep this behavior, add + + property :#{column.name}, Type::Integer.new + + to your #{name} model. + MESSAGE + end + end + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + def reset_column_information # :nodoc: + super + + @columns = nil + @columns_hash = nil + end + + private + + def user_provided_columns + @user_provided_columns ||= {} + end + + def add_user_provided_columns(schema_columns) + schema_columns.reject { |column| + user_provided_columns.key? column.name + } + user_provided_columns.values + end + end + end +end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0eec6774a0..dd80ec6274 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -6,9 +6,9 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :reflections + class_attribute :_reflections class_attribute :aggregate_reflections - self.reflections = {} + self._reflections = {} self.aggregate_reflections = {} end @@ -24,7 +24,7 @@ module ActiveRecord end def self.add_reflection(ar, name, reflection) - ar.reflections = ar.reflections.merge(name.to_s => reflection) + ar._reflections = ar._reflections.merge(name.to_s => reflection) end def self.add_aggregate_reflection(ar, name, reflection) @@ -53,6 +53,24 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end + # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # + # Account.reflections # => {balance: AggregateReflection} + # + # @api public + def reflections + ref = {} + _reflections.each do |name, reflection| + parent_name, parent_reflection = reflection.parent_reflection + if parent_name + ref[parent_name] = parent_reflection + else + ref[name] = reflection + end + end + ref + end + # Returns an array of AssociationReflection objects for all the # associations in the class. If you only want to reflect on a certain # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, @@ -63,6 +81,7 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # + # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections @@ -73,11 +92,19 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # + # @api public def reflect_on_association(association) reflections[association.to_s] end + # @api private + def _reflect_on_association(association) #:nodoc: + _reflections[association.to_s] + end + # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. + # + # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end @@ -129,6 +156,10 @@ module ActiveRecord def autosave=(autosave) @automatic_inverse_of = false @options[:autosave] = autosave + _, parent_reflection = self.parent_reflection + if parent_reflection + parent_reflection.autosave = autosave + end end # Returns the class for the macro. @@ -193,10 +224,11 @@ module ActiveRecord end attr_reader :type, :foreign_type + attr_accessor :parent_reflection # [:name, Reflection] def initialize(macro, name, scope, options, active_record) super - @collection = :has_many == macro + @collection = [:has_many, :has_and_belongs_to_many].include?(macro) @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @@ -330,12 +362,12 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def inverse_of return unless inverse_name - @inverse_of ||= klass.reflect_on_association inverse_name + @inverse_of ||= klass._reflect_on_association inverse_name end def polymorphic_inverse_of(associated_class) if has_inverse? - if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) + if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of]) inverse_relationship else raise InverseOfAssociationNotFoundError.new(self, associated_class) @@ -436,7 +468,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym begin - reflection = klass.reflect_on_association(inverse_name) + reflection = klass._reflect_on_association(inverse_name) rescue NameError # Give up: we couldn't compute the klass type so we won't be able # to find any associations either. @@ -535,7 +567,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection - through_reflection.klass.reflect_on_association(source_reflection_name) + through_reflection.klass._reflect_on_association(source_reflection_name) end # Returns the AssociationReflection object specified in the <tt>:through</tt> option @@ -551,7 +583,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - active_record.reflect_on_association(options[:through]) + active_record._reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -658,7 +690,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq names = names.find_all { |n| - through_reflection.klass.reflect_on_association(n) + through_reflection.klass._reflect_on_association(n) } if names.length > 1 diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 56cf9bcd27..d155517b18 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -277,7 +277,7 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass.reflect_on_association(group_attrs.first.to_sym) + association = @klass._reflect_on_association(group_attrs.first.to_sym) associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index db32ae12a8..47e90e9021 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -336,7 +336,16 @@ module ActiveRecord end def find_with_associations - join_dependency = construct_join_dependency + # NOTE: the JoinDependency constructed here needs to know about + # any joins already present in `self`, so pass them in + # + # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 + # incorrect SQL is generated. In that case, the join dependency for + # SpecialCategorizations is constructed without knowledge of the + # preexisting join in joins_values to categorizations (by way of + # the `has_many :through` for categories). + # + join_dependency = construct_join_dependency(joins_values) aliases = join_dependency.aliases relation = select aliases.columns diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index d40f276968..eff5c8f09c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -26,7 +26,7 @@ module ActiveRecord queries << '1=0' else table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) + association = klass._reflect_on_association(column.to_sym) value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) @@ -55,7 +55,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass.reflect_on_association(column.to_sym) + if klass && reflection = klass._reflect_on_association(column.to_sym) if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) queries << build(table[reflection.foreign_type], base_class) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 2f6c34ac08..78dba8be06 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -2,28 +2,33 @@ module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: def call(attribute, value) + return attribute.in([]) if value.empty? + values = value.map { |x| x.is_a?(Base) ? x.id : x } ranges, values = values.partition { |v| v.is_a?(Range) } + nils, values = values.partition(&:nil?) - values_predicate = if values.include?(nil) - values = values.compact - + values_predicate = case values.length - when 0 - attribute.eq(nil) - when 1 - attribute.eq(values.first).or(attribute.eq(nil)) - else - attribute.in(values).or(attribute.eq(nil)) + when 0 then NullPredicate + when 1 then attribute.eq(values.first) + else attribute.in(values) end - else - attribute.in(values) + + unless nils.empty? + values_predicate = values_predicate.or(attribute.eq(nil)) end array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } end + + module NullPredicate + def self.or(other) + other + end + end end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index a607e9ac87..1262b2c291 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -573,15 +573,11 @@ WARNING end end - def where!(opts = :chain, *rest) # :nodoc: - if opts == :chain - WhereChain.new(self) - else - references!(PredicateBuilder.references(opts)) if Hash === opts + def where!(opts, *rest) # :nodoc: + references!(PredicateBuilder.references(opts)) if Hash === opts - self.where_values += build_where(opts, rest) - self - end + self.where_values += build_where(opts, rest) + self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 9a19483da3..e586744818 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -4,7 +4,7 @@ module ActiveRecord def validate(record) super attributes.each do |attribute| - next unless record.class.reflect_on_association(attribute) + next unless record.class._reflect_on_association(attribute) associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index ee080451a9..b6fccc9b94 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -47,7 +47,7 @@ module ActiveRecord end def build_relation(klass, table, attribute, value) #:nodoc: - if reflection = klass.reflect_on_association(attribute) + if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key value = value.attributes[reflection.primary_key_column.name] unless value.nil? end @@ -74,7 +74,7 @@ module ActiveRecord def scope_relation(record, table, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class.reflect_on_association(scope_item) + if reflection = record.class._reflect_on_association(scope_item) scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 1f55cce352..972abf7cdc 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -122,7 +122,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase assert_equal "Champs-Élysées", composite.address.street composite.address = FullAddress.new("Paris", "Rue Basse") - skip "Saving with custom OID type is currently not supported." composite.save! assert_equal 'Paris', composite.reload.address.city diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index b9e296ed8f..b6c6e38f62 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -129,7 +129,7 @@ class SchemaTest < ActiveRecord::TestCase SQL song = Song.create - album = Album.create + Album.create assert_equal song, Song.includes(:albums).references(:albums).first ensure ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index b478db749d..13b754d226 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -60,7 +60,6 @@ class CopyTableTest < ActiveRecord::TestCase assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type assert_equal original_id.limit, copied_id.limit - assert_equal original_id.primary, copied_id.primary end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 0c4f06d6a9..8c9a051eea 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,10 +15,10 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - column = Struct.new(:type, :name).new(:string, "foo") + cast_type = Type::String.new binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, column) + assert_equal expected, @conn.type_cast(binary, cast_type) end def test_type_cast_symbol @@ -84,7 +84,7 @@ module ActiveRecord assert_raise(TypeError) { @conn.type_cast(obj, nil) } end - def test_quoted_id + def test_type_cast_object_which_responds_to_quoted_id quoted_id_obj = Class.new { def quoted_id "'zomg'" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ebcf453305..4bd4486b41 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1167,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase ) end + test "deep preload" do + post = Post.preload(author: :posts, comments: :post).first + + assert_predicate post.author.association(:posts), :loaded? + assert_predicate post.comments.first.association(:post), :loaded? + end + test "preloading does not cache has many association subset when preloaded with a through association" do author = Author.includes(:comments_with_order_and_conditions, :posts).first assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size } @@ -1196,6 +1203,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + test "preloading the same association twice works" do + Member.create! + members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a + assert_no_queries { + members_with_membership = members.select(&:current_membership) + assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size + } + end + test "preloading with a polymorphic association and using the existential predicate" do assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer 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 878f1877db..8d8201ddae 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 @@ -70,6 +70,14 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end +class SubDeveloper < Developer + self.table_name = 'developers' + has_and_belongs_to_many :special_projects, + :join_table => 'developers_projects', + :foreign_key => "project_id", + :association_foreign_key => "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -814,7 +822,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [], Pirate.where(id: redbeard.id) end - test "has and belongs to many associations on new records use null relations" do + def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects assert_no_queries do assert_equal [], projects @@ -860,4 +868,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_includes magazine.articles, article end + + def test_redefine_habtm + child = SubDeveloper.new("name" => "Aredridel") + child.special_projects << SpecialProject.new("name" => "Special Project") + assert_equal true, child.save + end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index b23517b2f9..07cf65a760 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -126,4 +126,14 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a assert_equal 2, categories.size end + + test "the correct records are loaded when including an aliased association" do + author = Author.create! name: "Jon" + author.categories.create! name: 'Not Special' + author.special_categories.create! name: 'Special' + + categories = author.categories.eager_load(:special_categorizations).order(:name).to_a + assert_equal 0, categories.first.special_categorizations.size + assert_equal 1, categories.second.special_categorizations.size + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7c7c1fbfbd..d65c4b0638 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -102,8 +102,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_columns_should_obey_set_primary_key - pk = Subscriber.columns.find { |x| x.name == 'nick' } - assert pk.primary, 'nick should be primary key' + pk = Subscriber.columns_hash[Subscriber.primary_key] + assert_equal 'nick', pk.name, 'nick should be primary key' end def test_primary_key_with_no_id @@ -518,6 +518,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find('1-meowmeow'), Topic.find(1) end + def test_find_by_slug_with_array + assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + end + def test_equality_of_new_records assert_not_equal Topic.new, Topic.new assert_equal false, Topic.new == Topic.new @@ -980,6 +984,10 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new + property :atoms_in_universe, Type::Integer.new end def test_big_decimal_conditions diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 40f73cd68c..0bc7ee6d64 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -21,7 +21,7 @@ module ActiveRecord super @connection = ActiveRecord::Base.connection @subscriber = LogListener.new - @pk = Topic.columns.find { |c| c.primary } + @pk = Topic.columns_hash[Topic.primary_key] @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end @@ -60,12 +60,10 @@ module ActiveRecord end def test_logs_bind_vars - pk = Topic.columns.find { |x| x.primary } - payload = { :name => 'SQL', :sql => 'select * from topics where id = ?', - :binds => [[pk, 10]] + :binds => [[@pk, 10]] } event = ActiveSupport::Notifications::Event.new( 'foo', @@ -87,7 +85,7 @@ module ActiveRecord }.new logger.sql event - assert_match([[pk.name, 10]].inspect, logger.debugs.first) + assert_match([[@pk.name, 10]].inspect, logger.debugs.first) end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index b8de78934e..b9445ee072 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -15,6 +15,10 @@ Company.has_many :accounts class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new + property :atoms_in_universe, Type::Integer.new end class CalculationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb deleted file mode 100644 index 3257f5bed8..0000000000 --- a/activerecord/test/cases/column_test.rb +++ /dev/null @@ -1,159 +0,0 @@ -require "cases/helper" -require 'models/company' - -module ActiveRecord - module ConnectionAdapters - class ColumnTest < ActiveRecord::TestCase - def test_type_cast_boolean - column = Column.new("field", nil, Type::Boolean.new) - assert column.type_cast('').nil? - assert column.type_cast(nil).nil? - - assert column.type_cast(true) - assert column.type_cast(1) - assert column.type_cast('1') - assert column.type_cast('t') - assert column.type_cast('T') - assert column.type_cast('true') - assert column.type_cast('TRUE') - assert column.type_cast('on') - assert column.type_cast('ON') - - # explicitly check for false vs nil - assert_equal false, column.type_cast(false) - assert_equal false, column.type_cast(0) - assert_equal false, column.type_cast('0') - assert_equal false, column.type_cast('f') - assert_equal false, column.type_cast('F') - assert_equal false, column.type_cast('false') - assert_equal false, column.type_cast('FALSE') - assert_equal false, column.type_cast('off') - assert_equal false, column.type_cast('OFF') - assert_equal false, column.type_cast(' ') - assert_equal false, column.type_cast("\u3000\r\n") - assert_equal false, column.type_cast("\u0000") - assert_equal false, column.type_cast('SOMETHING RANDOM') - end - - def test_type_cast_string - column = Column.new("field", nil, Type::String.new) - assert_equal "1", column.type_cast(true) - assert_equal "0", column.type_cast(false) - assert_equal "123", column.type_cast(123) - end - - def test_type_cast_integer - column = Column.new("field", nil, Type::Integer.new) - assert_equal 1, column.type_cast(1) - assert_equal 1, column.type_cast('1') - assert_equal 1, column.type_cast('1ignore') - assert_equal 0, column.type_cast('bad1') - assert_equal 0, column.type_cast('bad') - assert_equal 1, column.type_cast(1.7) - assert_equal 0, column.type_cast(false) - assert_equal 1, column.type_cast(true) - assert_nil column.type_cast(nil) - end - - def test_type_cast_non_integer_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_nil column.type_cast([1,2]) - assert_nil column.type_cast({1 => 2}) - assert_nil column.type_cast((1..2)) - end - - def test_type_cast_activerecord_to_integer - column = Column.new("field", nil, Type::Integer.new) - firm = Firm.create(:name => 'Apple') - assert_nil column.type_cast(firm) - end - - def test_type_cast_object_without_to_i_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_nil column.type_cast(Object.new) - end - - def test_type_cast_nan_and_infinity_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_nil column.type_cast(Float::NAN) - assert_nil column.type_cast(1.0/0.0) - end - - def test_type_cast_float - column = Column.new("field", nil, Type::Float.new) - assert_equal 1.0, column.type_cast("1") - end - - def test_type_cast_decimal - column = Column.new("field", nil, Type::Decimal.new) - assert_equal BigDecimal.new("0"), column.type_cast(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), column.type_cast(123.0) - assert_equal BigDecimal.new("1"), column.type_cast(:"1") - end - - def test_type_cast_binary - column = Column.new("field", nil, Type::Binary.new) - assert_equal nil, column.type_cast(nil) - assert_equal "1", column.type_cast("1") - assert_equal 1, column.type_cast(1) - end - - def test_type_cast_time - column = Column.new("field", nil, Type::Time.new) - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, column.type_cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - column = Column.new("field", nil, Type::DateTime.new) - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") - end - - def test_type_cast_date - column = Column.new("field", nil, Type::Date.new) - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, column.type_cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_equal 1800, column.type_cast(30.minutes) - assert_equal 7200, column.type_cast(2.hours) - end - - def test_string_to_time_with_timezone - [:utc, :local].each do |zone| - with_timezone_config default: zone do - column = Column.new("field", nil, Type::DateTime.new) - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), column.type_cast("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - if current_adapter?(:SQLite3Adapter) - def test_binary_encoding - column = Column.new("field", nil, SQLite3Binary.new) - utf8_string = "a string".encode(Encoding::UTF_8) - type_cast = column.type_cast(utf8_string) - - assert_equal Encoding::ASCII_8BIT, type_cast.encoding - end - end - end - end -end diff --git a/activerecord/test/cases/connection_adapters/type/type_map_test.rb b/activerecord/test/cases/connection_adapters/type/type_map_test.rb index 3abd7a276e..e85bbc8dc7 100644 --- a/activerecord/test/cases/connection_adapters/type/type_map_test.rb +++ b/activerecord/test/cases/connection_adapters/type/type_map_test.rb @@ -125,6 +125,16 @@ module ActiveRecord assert_equal mapping.lookup(3), 'string' assert_kind_of Type::Value, mapping.lookup(4) end + + def test_clear_mappings + time = Time.new + mapping = TypeMap.new + + mapping.register_type(/time/i, time) + mapping.clear + + assert_not_equal mapping.lookup('time'), time + end end end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 18df30faf5..3958c3bfff 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -77,12 +77,16 @@ module ActiveRecord assert_lookup_type :integer, 'tinyint' assert_lookup_type :integer, 'smallint' assert_lookup_type :integer, 'bigint' - assert_lookup_type :integer, 'decimal(2)' - assert_lookup_type :integer, 'decimal(2,0)' - assert_lookup_type :integer, 'numeric(2)' - assert_lookup_type :integer, 'numeric(2,0)' - assert_lookup_type :integer, 'number(2)' - assert_lookup_type :integer, 'number(2,0)' + end + + def test_decimal_without_scale + types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal :decimal, cast_type.type + assert_equal 2, cast_type.type_cast(2.1) + end end private diff --git a/activerecord/test/cases/custom_properties_test.rb b/activerecord/test/cases/custom_properties_test.rb new file mode 100644 index 0000000000..9598f0299c --- /dev/null +++ b/activerecord/test/cases/custom_properties_test.rb @@ -0,0 +1,64 @@ +require 'cases/helper' + +class OverloadedType < ActiveRecord::Base + property :overloaded_float, Type::Integer.new + property :overloaded_string_with_limit, Type::String.new(limit: 50) + property :non_existent_decimal, Type::Decimal.new +end + +class UnoverloadedType < ActiveRecord::Base + self.table_name = 'overloaded_types' +end + +module ActiveRecord + class CustomPropertiesTest < ActiveRecord::TestCase + def test_overloading_types + data = OverloadedType.new + + data.overloaded_float = "1.1" + data.unoverloaded_float = "1.1" + + assert_equal 1, data.overloaded_float + assert_equal 1.1, data.unoverloaded_float + end + + def test_overloaded_properties_save + data = OverloadedType.new + + data.overloaded_float = "2.2" + data.save! + data.reload + + assert_equal 2, data.overloaded_float + assert_equal 2.0, UnoverloadedType.last.overloaded_float + end + + def test_properties_assigned_in_constructor + data = OverloadedType.new(overloaded_float: '3.3') + + assert_equal 3, data.overloaded_float + end + + def test_overloaded_properties_with_limit + assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit + assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + end + + def test_nonexistent_property + data = OverloadedType.new(non_existent_decimal: 1) + + assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_raise ActiveRecord::UnknownAttributeError do + UnoverloadedType.new(non_existent_decimal: 1) + end + end + + def test_overloaded_properties_have_no_default + data = OverloadedType.new + unoverloaded_data = UnoverloadedType.new + + assert_nil data.overloaded_float + assert unoverloaded_data.overloaded_float + end + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index eaf2cada9d..937646b09a 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -9,6 +9,7 @@ require 'active_record' require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' +require 'active_support/core_ext/string/strip' require 'support/config' require 'support/connection' diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 455ec78f68..aa679d4a35 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -11,7 +11,13 @@ require MIGRATIONS_ROOT + "/rename/1_we_need_things" require MIGRATIONS_ROOT + "/rename/2_rename_things" require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" -class BigNumber < ActiveRecord::Base; end +class BigNumber < ActiveRecord::Base + unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) + property :value_of_e, Type::Integer.new + end + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new +end class Reminder < ActiveRecord::Base; end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 56d0dd6a77..c719918fd7 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -149,38 +149,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key end - def test_two_models_with_same_table_but_different_primary_key - k1 = Class.new(ActiveRecord::Base) - k1.table_name = 'posts' - k1.primary_key = 'id' - - k2 = Class.new(ActiveRecord::Base) - k2.table_name = 'posts' - k2.primary_key = 'title' - - assert k1.columns.find { |c| c.name == 'id' }.primary - assert !k1.columns.find { |c| c.name == 'title' }.primary - assert k1.columns_hash['id'].primary - assert !k1.columns_hash['title'].primary - - assert !k2.columns.find { |c| c.name == 'id' }.primary - assert k2.columns.find { |c| c.name == 'title' }.primary - assert !k2.columns_hash['id'].primary - assert k2.columns_hash['title'].primary - end - - def test_models_with_same_table_have_different_columns - k1 = Class.new(ActiveRecord::Base) - k1.table_name = 'posts' - - k2 = Class.new(ActiveRecord::Base) - k2.table_name = 'posts' - - k1.columns.zip(k2.columns).each do |col1, col2| - assert !col1.equal?(col2) - end - end - def test_auto_detect_primary_key_from_schema MixedCaseMonkey.reset_primary_key assert_equal "monkeyID", MixedCaseMonkey.primary_key diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index e2439b9a24..bbd5298da1 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -3,14 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class QuotingTest < ActiveRecord::TestCase - class FakeColumn < ActiveRecord::ConnectionAdapters::Column - attr_accessor :type - - def initialize type - @type = type - end - end - def setup @quoter = Class.new { include Quoting }.new end @@ -101,12 +93,12 @@ module ActiveRecord def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer)) + assert_equal '1', @quoter.quote(true, Type::Integer.new) end def test_quote_false assert_equal @quoter.quoted_false, @quoter.quote(false, nil) - assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer)) + assert_equal '0', @quoter.quote(false, Type::Integer.new) end def test_quote_float @@ -166,26 +158,26 @@ module ActiveRecord end def test_quote_string_int_column - assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer)) - assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer)) + assert_equal "1", @quoter.quote('1', Type::Integer.new) + assert_equal "1", @quoter.quote('1.2', Type::Integer.new) end def test_quote_string_float_column - assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float)) - assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float)) + assert_equal "1.0", @quoter.quote('1', Type::Float.new) + assert_equal "1.2", @quoter.quote('1.2', Type::Float.new) end def test_quote_as_mb_chars_binary_column string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary)) + assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new) end def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new) end def test_string_with_crazy_column - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end def test_quote_duration @@ -193,7 +185,7 @@ module ActiveRecord end def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) + assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new) end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c085fcf161..e6603f28be 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -200,7 +200,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_should_not_raise_error_when_compared_to_other_object - assert_nothing_raised { Firm.reflections['clients'] == Object.new } + assert_not_equal Object.new, Firm._reflections['clients'] + end + + def test_has_and_belongs_to_many_reflection + assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro + assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name end def test_has_many_through_reflection diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index c6decaad89..b9e69bdb08 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -99,7 +99,7 @@ module ActiveRecord assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual assert_equal 'ruby on rails', bind.last end - + def test_rewhere_with_one_condition relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 4b146c11bc..88df997a2f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -552,13 +552,6 @@ class RelationTest < ActiveRecord::TestCase end end - def test_deep_preload - post = Post.preload(author: :posts, comments: :post).first - - assert_predicate post.author.association(:posts), :loaded? - assert_predicate post.comments.first.association(:post), :loaded? - end - def test_preload_applies_to_all_chained_preloaded_scopes assert_queries(3) do post = Post.with_comments.with_tags.first @@ -750,6 +743,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], relation.to_a end + def test_typecasting_where_with_array + ids = Author.pluck(:id) + slugs = ids.map { |id| "#{id}-as-a-slug" } + + assert_equal Author.all.to_a, Author.where(id: slugs).to_a + end + def test_find_all_using_where_with_relation david = authors(:david) # switching the lines below would succeed in current rails diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 61bca976f7..9602252b2e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,10 +1,8 @@ require "cases/helper" class SchemaDumperTest < ActiveRecord::TestCase - def setup - super + setup do ActiveRecord::SchemaMigration.create_table - @stream = StringIO.new end def standard_dump @@ -25,7 +23,8 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_magic_comment - assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + output = standard_dump + assert_match "# encoding: #{@stream.external_encoding.name}", output end def test_schema_dump diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index c46060a646..7dd1f10ce9 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -1,8 +1,11 @@ require "cases/helper" require 'models/contact' require 'models/topic' +require 'models/book' class SerializationTest < ActiveRecord::TestCase + fixtures :books + FORMATS = [ :xml, :json ] def setup @@ -65,4 +68,20 @@ class SerializationTest < ActiveRecord::TestCase ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end + + def test_read_attribute_for_serialization_with_format_after_init + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new(format: 'paperback') + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end + + def test_read_attribute_for_serialization_with_format_after_find + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.find(books(:awdr).id) + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb new file mode 100644 index 0000000000..5d5f442d3a --- /dev/null +++ b/activerecord/test/cases/types_test.rb @@ -0,0 +1,159 @@ +require "cases/helper" +require 'models/company' + +module ActiveRecord + module ConnectionAdapters + class TypesTest < ActiveRecord::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.type_cast('').nil? + assert type.type_cast(nil).nil? + + assert type.type_cast(true) + assert type.type_cast(1) + assert type.type_cast('1') + assert type.type_cast('t') + assert type.type_cast('T') + assert type.type_cast('true') + assert type.type_cast('TRUE') + assert type.type_cast('on') + assert type.type_cast('ON') + + # explicitly check for false vs nil + assert_equal false, type.type_cast(false) + assert_equal false, type.type_cast(0) + assert_equal false, type.type_cast('0') + assert_equal false, type.type_cast('f') + assert_equal false, type.type_cast('F') + assert_equal false, type.type_cast('false') + assert_equal false, type.type_cast('FALSE') + assert_equal false, type.type_cast('off') + assert_equal false, type.type_cast('OFF') + assert_equal false, type.type_cast(' ') + assert_equal false, type.type_cast("\u3000\r\n") + assert_equal false, type.type_cast("\u0000") + assert_equal false, type.type_cast('SOMETHING RANDOM') + end + + def test_type_cast_string + type = Type::String.new + assert_equal "1", type.type_cast(true) + assert_equal "0", type.type_cast(false) + assert_equal "123", type.type_cast(123) + end + + def test_type_cast_integer + type = Type::Integer.new + assert_equal 1, type.type_cast(1) + assert_equal 1, type.type_cast('1') + assert_equal 1, type.type_cast('1ignore') + assert_equal 0, type.type_cast('bad1') + assert_equal 0, type.type_cast('bad') + assert_equal 1, type.type_cast(1.7) + assert_equal 0, type.type_cast(false) + assert_equal 1, type.type_cast(true) + assert_nil type.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + type = Type::Integer.new + assert_nil type.type_cast([1,2]) + assert_nil type.type_cast({1 => 2}) + assert_nil type.type_cast((1..2)) + end + + def test_type_cast_activerecord_to_integer + type = Type::Integer.new + firm = Firm.create(:name => 'Apple') + assert_nil type.type_cast(firm) + end + + def test_type_cast_object_without_to_i_to_integer + type = Type::Integer.new + assert_nil type.type_cast(Object.new) + end + + def test_type_cast_nan_and_infinity_to_integer + type = Type::Integer.new + assert_nil type.type_cast(Float::NAN) + assert_nil type.type_cast(1.0/0.0) + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.type_cast("1") + end + + def test_type_cast_decimal + type = Type::Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast(123.0) + assert_equal BigDecimal.new("1"), type.type_cast(:"1") + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.type_cast(nil) + assert_equal "1", type.type_cast("1") + assert_equal 1, type.type_cast(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast(' ') + assert_equal nil, type.type_cast('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.type_cast(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast(' ') + assert_equal nil, type.type_cast('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.type_cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.type_cast(30.minutes) + assert_equal 7200, type.type_cast(2.hours) + end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + if current_adapter?(:SQLite3Adapter) + def test_binary_encoding + type = SQLite3Binary.new + utf8_string = "a string".encode(Encoding::UTF_8) + type_cast = type.type_cast(utf8_string) + + assert_equal Encoding::ASCII_8BIT, type_cast.encoding + end + end + end + end +end diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index fb48645456..abe56752c6 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -2,8 +2,10 @@ awdr: author_id: 1 id: 1 name: "Agile Web Development with Rails" + format: "paperback" rfr: author_id: 1 id: 2 name: "Ruby for Rails" + format: "ebook" diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 8c52ad2724..4cce58f4f4 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -103,6 +103,7 @@ ActiveRecord::Schema.define do create_table :books, force: true do |t| t.integer :author_id + t.string :format t.column :name, :string t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 @@ -854,6 +855,12 @@ ActiveRecord::Schema.define do execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" end + + create_table :overloaded_types, force: true do |t| + t.float :overloaded_float, default: 500 + t.float :unoverloaded_float + t.string :overloaded_string_with_limit, limit: 255 + end end Course.connection.create_table :courses, force: true do |t| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 2a992f60bb..6915a491b0 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,17 @@ +* DateTime `advance` now supports partial days. + + Before: + + DateTime.now.advance(days: 1, hours: 12) + + After: + + DateTime.now.advance(days: 1.5) + + Fixes #12005. + + *Shay Davidson* + * `Hash#deep_transform_keys` and `Hash#deep_transform_keys!` now transform hashes in nested arrays. This change also applies to `Hash#deep_stringify_keys`, `Hash#deep_stringify_keys!`, `Hash#deep_symbolize_keys` and diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 67f58bc0fe..caa499dfa2 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -5,6 +5,8 @@ class Array # %w( a b c d ).from(2) # => ["c", "d"] # %w( a b c d ).from(10) # => [] # %w().from(0) # => [] + # %w( a b c d ).from(-2) # => ["c", "d"] + # %w( a b c ).from(-10) # => [] def from(position) self[position, length] || [] end @@ -15,8 +17,10 @@ class Array # %w( a b c d ).to(2) # => ["a", "b", "c"] # %w( a b c d ).to(10) # => ["a", "b", "c", "d"] # %w().to(0) # => [] + # %w( a b c d ).to(-2) # => ["a", "b", "c"] + # %w( a b c ).to(-10) # => [] def to(position) - first position + 1 + self[0..position] end # Equal to <tt>self[1]</tt>. diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 73ad0aa097..289ca12b5e 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -53,6 +53,16 @@ class DateTime # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, # <tt>:minutes</tt>, <tt>:seconds</tt>. def advance(options) + unless options[:weeks].nil? + options[:weeks], partial_weeks = options[:weeks].divmod(1) + options[:days] = options.fetch(:days, 0) + 7 * partial_weeks + end + + unless options[:days].nil? + options[:days], partial_days = options[:days].divmod(1) + options[:hours] = options.fetch(:hours, 0) + 24 * partial_days + end + d = to_date.advance(options) datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ @@ -63,7 +73,7 @@ class DateTime if seconds_to_advance.zero? datetime_advanced_by_date else - datetime_advanced_by_date.since seconds_to_advance + datetime_advanced_by_date.since(seconds_to_advance) end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 59675d744e..8a545e4386 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -180,10 +180,11 @@ module ActiveSupport #:nodoc: Dependencies.load_missing_constant(from_mod, const_name) end - # Dependencies assumes the name of the module reflects the nesting (unless - # it can be proven that is not the case), and the path to the file that - # defines the constant. Anonymous modules cannot follow these conventions - # and we assume therefore the user wants to refer to a top-level constant. + # We assume that the name of the module reflects the nesting + # (unless it can be proven that is not the case) and the path to the file + # that defines the constant. Anonymous modules cannot follow these + # conventions and therefore we assume that the user wants to refer to a + # top-level constant. def guess_for_anonymous(const_name) if Object.const_defined?(const_name) raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module" diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 09eb732ef7..0ae641d05b 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -105,8 +105,7 @@ module ActiveSupport # We define it as a workaround to Ruby 2.0.0-p353 bug. # For more information, check rails/rails#13055. - # It should be dropped once a new Ruby patch-level - # release after 2.0.0-p353 happens. + # Remove it when we drop support for 2.0.0-p353. def ===(other) #:nodoc: value === other end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 2fb5c04316..e6c125bfdd 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -1,5 +1,3 @@ -gem 'minitest' # make sure we get the gem, not stdlib -require 'minitest' require 'active_support/testing/tagged_logging' require 'active_support/testing/setup_and_teardown' require 'active_support/testing/assertions' diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 92a593965e..ea2d3391bd 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -1,5 +1,3 @@ -require 'active_support' - module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index e0e54f47e4..bd1b818717 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -10,12 +10,16 @@ class ArrayExtAccessTests < ActiveSupport::TestCase assert_equal %w( a b c d ), %w( a b c d ).from(0) assert_equal %w( c d ), %w( a b c d ).from(2) assert_equal %w(), %w( a b c d ).from(10) + assert_equal %w( d e ), %w( a b c d e ).from(-2) + assert_equal %w(), %w( a b c d e ).from(-10) end def test_to assert_equal %w( a ), %w( a b c d ).to(0) assert_equal %w( a b c ), %w( a b c d ).to(2) assert_equal %w( a b c d ), %w( a b c d ).to(10) + assert_equal %w( a b c ), %w( a b c d ).to(-2) + assert_equal %w(), %w( a b c ).to(-10) end def test_second_through_tenth diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 0a40aeb96c..224172e39f 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -162,6 +162,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9) end + def test_advance_partial_days + assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5) + assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 0.5) + assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5, :months => 1) + end + def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas # If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead. assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1) diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index b0b4738eb3..eb8b0d878e 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -498,10 +498,10 @@ class InflectorTest < ActiveSupport::TestCase end %w(plurals singulars uncountables humans acronyms).each do |scope| - ActiveSupport::Inflector.inflections do |inflect| - define_method("test_clear_inflections_with_#{scope}") do - with_dup do - # clear the inflections + define_method("test_clear_inflections_with_#{scope}") do + with_dup do + # clear the inflections + ActiveSupport::Inflector.inflections do |inflect| inflect.clear(scope) assert_equal [], inflect.send(scope) end @@ -516,9 +516,10 @@ class InflectorTest < ActiveSupport::TestCase # there are module functions that access ActiveSupport::Inflector.inflections, # so we need to replace the singleton itself. def with_dup - original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__) - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original.dup) + original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) + yield ensure - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original) + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) end end diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb index 8be8c51f07..21e4ba0cee 100644 --- a/activesupport/test/subscriber_test.rb +++ b/activesupport/test/subscriber_test.rb @@ -23,6 +23,7 @@ end # Monkey patch subscriber to test that only one subscriber per method is added. class TestSubscriber + remove_method :open_party def open_party(event) events << event end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 0b52db5670..4cd4a9d70c 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,17 @@ +* Change Posts to Articles in Getting Started sample application in order to +better align with the actual guides. + + * John Kelly Ferguson* + +* Update all Rails 4.1.0 references to 4.1.1 within the guides and code. + + * John Kelly Ferguson* + +* Split up rows in the Explain Queries table of the ActiveRecord Querying section +in order to improve readability. + + * John Kelly Ferguson* + * Change all non-HTTP method 'post' references to 'article'. *John Kelly Ferguson* diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index 091a87aa4c..13a0ef44a9 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -2,11 +2,11 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.1.0' +gem 'rails', '4.1.1' # Use SQLite3 as the database for Active Record gem 'sqlite3' # Use SCSS for stylesheets -gem 'sass-rails', '~> 4.0.1' +gem 'sass-rails', '~> 4.0.3' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .js.coffee assets and views diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock index a2ab76c908..f26cf58e6a 100644 --- a/guides/code/getting_started/Gemfile.lock +++ b/guides/code/getting_started/Gemfile.lock @@ -1,34 +1,33 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.1.0) - actionpack (= 4.1.0) - actionview (= 4.1.0) + actionmailer (4.1.1) + actionpack (= 4.1.1) + actionview (= 4.1.1) mail (~> 2.5.4) - actionpack (4.1.0) - actionview (= 4.1.0) - activesupport (= 4.1.0) + actionpack (4.1.1) + actionview (= 4.1.1) + activesupport (= 4.1.1) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.0) - activesupport (= 4.1.0) + actionview (4.1.1) + activesupport (= 4.1.1) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.0) - activesupport (= 4.1.0) + activemodel (4.1.1) + activesupport (= 4.1.1) builder (~> 3.1) - activerecord (4.1.0) - activemodel (= 4.1.0) - activesupport (= 4.1.0) + activerecord (4.1.1) + activemodel (= 4.1.1) + activesupport (= 4.1.1) arel (~> 5.0.0) - activesupport (4.1.0) + activesupport (4.1.1) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - arel (5.0.0) - atomic (1.1.14) + arel (5.0.1.20140414130214) builder (3.2.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -52,52 +51,52 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.25.1) - minitest (5.2.1) - multi_json (1.8.4) - polyglot (0.3.3) + minitest (5.3.4) + multi_json (1.10.1) + polyglot (0.3.4) rack (1.5.2) rack-test (0.6.2) rack (>= 1.0) - rails (4.1.0) - actionmailer (= 4.1.0) - actionpack (= 4.1.0) - actionview (= 4.1.0) - activemodel (= 4.1.0) - activerecord (= 4.1.0) - activesupport (= 4.1.0) + rails (4.1.1) + actionmailer (= 4.1.1) + actionpack (= 4.1.1) + actionview (= 4.1.1) + activemodel (= 4.1.1) + activerecord (= 4.1.1) + activesupport (= 4.1.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.0) - sprockets-rails (~> 2.0.0) - railties (4.1.0) - actionpack (= 4.1.0) - activesupport (= 4.1.0) + railties (= 4.1.1) + sprockets-rails (~> 2.0) + railties (4.1.1) + actionpack (= 4.1.1) + activesupport (= 4.1.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.1) + rake (10.3.2) rdoc (4.1.1) json (~> 1.4) - sass (3.2.13) - sass-rails (4.0.1) + sass (3.2.19) + sass-rails (4.0.3) railties (>= 4.0.0, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0) + sass (~> 3.2.0) + sprockets (~> 2.8, <= 2.11.0) + sprockets-rails (~> 2.0) sdoc (0.4.0) json (~> 1.8) rdoc (~> 4.0, < 5.0) spring (1.0.0) - sprockets (2.10.1) + sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.1) + sprockets-rails (2.1.3) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) sqlite3 (1.3.8) - thor (0.18.1) - thread_safe (0.1.3) - atomic + thor (0.19.1) + thread_safe (0.3.3) tilt (1.4.1) treetop (1.4.15) polyglot @@ -117,8 +116,8 @@ DEPENDENCIES coffee-rails (~> 4.0.0) jbuilder (~> 2.0) jquery-rails - rails (= 4.1.0) - sass-rails (~> 4.0.1) + rails (= 4.1.1) + sass-rails (~> 4.0.3) sdoc (~> 0.4.0) spring sqlite3 diff --git a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee b/guides/code/getting_started/app/assets/javascripts/articles.js.coffee index 24f83d18bb..24f83d18bb 100644 --- a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee +++ b/guides/code/getting_started/app/assets/javascripts/articles.js.coffee diff --git a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss b/guides/code/getting_started/app/assets/stylesheets/articles.css.scss index 1a7e15390c..cca548710d 100644 --- a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss +++ b/guides/code/getting_started/app/assets/stylesheets/articles.css.scss @@ -1,3 +1,3 @@ -// Place all the styles related to the posts controller here. +// Place all the styles related to the articles controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/guides/code/getting_started/app/controllers/articles_controller.rb b/guides/code/getting_started/app/controllers/articles_controller.rb new file mode 100644 index 0000000000..275b84e8b7 --- /dev/null +++ b/guides/code/getting_started/app/controllers/articles_controller.rb @@ -0,0 +1,53 @@ +class ArticlesController < ApplicationController + + http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] + + def index + @articles = Article.all + end + + def show + @article = Article.find(params[:id]) + end + + def edit + @article = Article.find(params[:id]) + end + + def update + @article = Article.find(params[:id]) + + if @article.update(article_params) + redirect_to action: :show, id: @article.id + else + render 'edit' + end + end + + def new + @article = Article.new + end + + def create + @article = Article.new(article_params) + + if @article.save + redirect_to action: :show, id: @article.id + else + render 'new' + end + end + + def destroy + @article = Article.find(params[:id]) + @article.destroy + + redirect_to action: :index + end + + private + + def article_params + params.require(:article).permit(:title, :text) + end +end diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb index b2d9bcdf7f..61813b1003 100644 --- a/guides/code/getting_started/app/controllers/comments_controller.rb +++ b/guides/code/getting_started/app/controllers/comments_controller.rb @@ -3,16 +3,16 @@ class CommentsController < ApplicationController http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy def create - @post = Post.find(params[:post_id]) - @comment = @post.comments.create(comment_params) - redirect_to post_path(@post) + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) + redirect_to article_path(@article) end def destroy - @post = Post.find(params[:post_id]) - @comment = @post.comments.find(params[:id]) + @article = Article.find(params[:article_id]) + @comment = @article.comments.find(params[:id]) @comment.destroy - redirect_to post_path(@post) + redirect_to article_path(@article) end private diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb deleted file mode 100644 index 02689ad67b..0000000000 --- a/guides/code/getting_started/app/controllers/posts_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -class PostsController < ApplicationController - - http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] - - def index - @posts = Post.all - end - - def show - @post = Post.find(params[:id]) - end - - def edit - @post = Post.find(params[:id]) - end - - def update - @post = Post.find(params[:id]) - - if @post.update(post_params) - redirect_to action: :show, id: @post.id - else - render 'edit' - end - end - - def new - @post = Post.new - end - - def create - @post = Post.new(post_params) - - if @post.save - redirect_to action: :show, id: @post.id - else - render 'new' - end - end - - def destroy - @post = Post.find(params[:id]) - @post.destroy - - redirect_to action: :index - end - - private - - def post_params - params.require(:post).permit(:title, :text) - end -end diff --git a/guides/code/getting_started/app/helpers/articles_helper.rb b/guides/code/getting_started/app/helpers/articles_helper.rb new file mode 100644 index 0000000000..2968277595 --- /dev/null +++ b/guides/code/getting_started/app/helpers/articles_helper.rb @@ -0,0 +1,2 @@ +module ArticlesHelper +end diff --git a/guides/code/getting_started/app/helpers/posts_helper.rb b/guides/code/getting_started/app/helpers/posts_helper.rb deleted file mode 100644 index a7b8cec898..0000000000 --- a/guides/code/getting_started/app/helpers/posts_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module PostsHelper -end diff --git a/guides/code/getting_started/app/models/post.rb b/guides/code/getting_started/app/models/article.rb index 64e0d721fd..6fc7888be2 100644 --- a/guides/code/getting_started/app/models/post.rb +++ b/guides/code/getting_started/app/models/article.rb @@ -1,6 +1,6 @@ -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments, dependent: :destroy - + validates :title, presence: true, length: { minimum: 5 } diff --git a/guides/code/getting_started/app/models/comment.rb b/guides/code/getting_started/app/models/comment.rb index 4e76c5b5b0..e2646a324f 100644 --- a/guides/code/getting_started/app/models/comment.rb +++ b/guides/code/getting_started/app/models/comment.rb @@ -1,3 +1,3 @@ class Comment < ActiveRecord::Base - belongs_to :post + belongs_to :article end diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/articles/_form.html.erb index f2f83585e1..87e3353ed2 100644 --- a/guides/code/getting_started/app/views/posts/_form.html.erb +++ b/guides/code/getting_started/app/views/articles/_form.html.erb @@ -1,10 +1,10 @@ -<%= form_for @post do |f| %> - <% if @post.errors.any? %> +<%= form_for @article do |f| %> + <% if @article.errors.any? %> <div id="error_explanation"> - <h2><%= pluralize(@post.errors.count, "error") %> prohibited - this post from being saved:</h2> + <h2><%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved:</h2> <ul> - <% @post.errors.full_messages.each do |msg| %> + <% @article.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> diff --git a/guides/code/getting_started/app/views/posts/edit.html.erb b/guides/code/getting_started/app/views/articles/edit.html.erb index 393e7430d0..14236e2a98 100644 --- a/guides/code/getting_started/app/views/posts/edit.html.erb +++ b/guides/code/getting_started/app/views/articles/edit.html.erb @@ -1,5 +1,5 @@ -<h1>Edit post</h1> - +<h1>Edit article</h1> + <%= render 'form' %> - + <%= link_to 'Back', action: :index %> diff --git a/guides/code/getting_started/app/views/articles/index.html.erb b/guides/code/getting_started/app/views/articles/index.html.erb new file mode 100644 index 0000000000..80e9c8c60c --- /dev/null +++ b/guides/code/getting_started/app/views/articles/index.html.erb @@ -0,0 +1,21 @@ +<h1>Listing Articles</h1> +<table> + <tr> + <th>Title</th> + <th>Text</th> + <th></th> + <th></th> + <th></th> + </tr> + +<% @articles.each do |article| %> + <tr> + <td><%= article.title %></td> + <td><%= article.text %></td> + <td><%= link_to 'Show', action: :show, id: article.id %></td> + <td><%= link_to 'Edit', action: :edit, id: article.id %></td> + <td><%= link_to 'Destroy', { action: :destroy, id: article.id }, + method: :delete, data: { confirm: 'Are you sure?' } %></td> + </tr> +<% end %> +</table> diff --git a/guides/code/getting_started/app/views/posts/new.html.erb b/guides/code/getting_started/app/views/articles/new.html.erb index efa81038ec..652b1c9c0b 100644 --- a/guides/code/getting_started/app/views/posts/new.html.erb +++ b/guides/code/getting_started/app/views/articles/new.html.erb @@ -1,5 +1,5 @@ -<h1>New post</h1> - +<h1>New article</h1> + <%= render 'form' %> - + <%= link_to 'Back', action: :index %> diff --git a/guides/code/getting_started/app/views/articles/show.html.erb b/guides/code/getting_started/app/views/articles/show.html.erb new file mode 100644 index 0000000000..6959c80bdb --- /dev/null +++ b/guides/code/getting_started/app/views/articles/show.html.erb @@ -0,0 +1,18 @@ +<p> + <strong>Title:</strong> + <%= @article.title %> +</p> + +<p> + <strong>Text:</strong> + <%= @article.text %> +</p> + +<h2>Comments</h2> +<%= render @article.comments %> + +<h2>Add a comment:</h2> +<%= render "comments/form" %> + +<%= link_to 'Edit Article', edit_article_path(@article) %> | +<%= link_to 'Back to Articles', articles_path %> diff --git a/guides/code/getting_started/app/views/comments/_comment.html.erb b/guides/code/getting_started/app/views/comments/_comment.html.erb index 593493339e..f7cbfaebfa 100644 --- a/guides/code/getting_started/app/views/comments/_comment.html.erb +++ b/guides/code/getting_started/app/views/comments/_comment.html.erb @@ -2,14 +2,14 @@ <strong>Commenter:</strong> <%= comment.commenter %> </p> - + <p> <strong>Comment:</strong> <%= comment.body %> </p> <p> - <%= link_to 'Destroy Comment', [comment.post, comment], + <%= link_to 'Destroy Comment', [comment.article, comment], method: :delete, data: { confirm: 'Are you sure?' } %> </p> diff --git a/guides/code/getting_started/app/views/comments/_form.html.erb b/guides/code/getting_started/app/views/comments/_form.html.erb index 00cb3a08f0..5850c41a17 100644 --- a/guides/code/getting_started/app/views/comments/_form.html.erb +++ b/guides/code/getting_started/app/views/comments/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_for([@post, @post.comments.build]) do |f| %> +<%= form_for([@article, @article.comments.build]) do |f| %> <p> <%= f.label :commenter %><br /> <%= f.text_field :commenter %> diff --git a/guides/code/getting_started/app/views/posts/index.html.erb b/guides/code/getting_started/app/views/posts/index.html.erb deleted file mode 100644 index 7369f0396f..0000000000 --- a/guides/code/getting_started/app/views/posts/index.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<h1>Listing Posts</h1> -<table> - <tr> - <th>Title</th> - <th>Text</th> - <th></th> - <th></th> - <th></th> - </tr> - -<% @posts.each do |post| %> - <tr> - <td><%= post.title %></td> - <td><%= post.text %></td> - <td><%= link_to 'Show', action: :show, id: post.id %></td> - <td><%= link_to 'Edit', action: :edit, id: post.id %></td> - <td><%= link_to 'Destroy', { action: :destroy, id: post.id }, - method: :delete, data: { confirm: 'Are you sure?' } %></td> - </tr> -<% end %> -</table> diff --git a/guides/code/getting_started/app/views/posts/show.html.erb b/guides/code/getting_started/app/views/posts/show.html.erb deleted file mode 100644 index e99e9edbb3..0000000000 --- a/guides/code/getting_started/app/views/posts/show.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<p> - <strong>Title:</strong> - <%= @post.title %> -</p> - -<p> - <strong>Text:</strong> - <%= @post.text %> -</p> - -<h2>Comments</h2> -<%= render @post.comments %> - -<h2>Add a comment:</h2> -<%= render "comments/form" %> - -<%= link_to 'Edit Post', edit_post_path(@post) %> | -<%= link_to 'Back to Posts', posts_path %> diff --git a/guides/code/getting_started/app/views/welcome/index.html.erb b/guides/code/getting_started/app/views/welcome/index.html.erb index 56be8dd3cc..1cabd0d217 100644 --- a/guides/code/getting_started/app/views/welcome/index.html.erb +++ b/guides/code/getting_started/app/views/welcome/index.html.erb @@ -1,4 +1,4 @@ <h1>Hello, Rails!</h1> -<%= link_to "My Blog", controller: "posts" %> -<%= link_to "New Post", new_post_path %> +<%= link_to "My Blog", controller: "articles" %> +<%= link_to "New Article", new_article_path %> diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb index 65d273b58d..97abca99b9 100644 --- a/guides/code/getting_started/config/routes.rb +++ b/guides/code/getting_started/config/routes.rb @@ -1,5 +1,5 @@ Rails.application.routes.draw do - resources :posts do + resources :articles do resources :comments end diff --git a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb b/guides/code/getting_started/db/migrate/20130122042648_create_articles.rb index 602bef31ab..6bb255e89f 100644 --- a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb +++ b/guides/code/getting_started/db/migrate/20130122042648_create_articles.rb @@ -1,6 +1,6 @@ -class CreatePosts < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration def change - create_table :posts do |t| + create_table :articles do |t| t.string :title t.text :text diff --git a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb index 3e51f9c0f7..1f765839ac 100644 --- a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb +++ b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb @@ -3,7 +3,7 @@ class CreateComments < ActiveRecord::Migration create_table :comments do |t| t.string :commenter t.text :body - t.references :post, index: true + t.references :article, index: true t.timestamps end diff --git a/guides/code/getting_started/db/schema.rb b/guides/code/getting_started/db/schema.rb index 101fe712a1..be40f7cb0e 100644 --- a/guides/code/getting_started/db/schema.rb +++ b/guides/code/getting_started/db/schema.rb @@ -13,21 +13,21 @@ ActiveRecord::Schema.define(version: 20130122045842) do - create_table "comments", force: true do |t| - t.string "commenter" - t.text "body" - t.integer "post_id" + create_table "articles", force: true do |t| + t.string "title" + t.text "text" t.datetime "created_at" t.datetime "updated_at" end - add_index "comments", ["post_id"], name: "index_comments_on_post_id" - - create_table "posts", force: true do |t| - t.string "title" - t.text "text" + create_table "comments", force: true do |t| + t.string "commenter" + t.text "body" + t.integer "article_id" t.datetime "created_at" t.datetime "updated_at" end + add_index "comments", ["article_id"], name: "index_comments_on_article_id" + end diff --git a/guides/code/getting_started/test/controllers/posts_controller_test.rb b/guides/code/getting_started/test/controllers/articles_controller_test.rb index 7a6ee4f1db..361aa0f47f 100644 --- a/guides/code/getting_started/test/controllers/posts_controller_test.rb +++ b/guides/code/getting_started/test/controllers/articles_controller_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase # test "the truth" do # assert true # end diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/articles.yml index 46b01c3bb4..46b01c3bb4 100644 --- a/guides/code/getting_started/test/fixtures/posts.yml +++ b/guides/code/getting_started/test/fixtures/articles.yml diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml index 9e409d8a61..05ad26f051 100644 --- a/guides/code/getting_started/test/fixtures/comments.yml +++ b/guides/code/getting_started/test/fixtures/comments.yml @@ -3,9 +3,9 @@ one: commenter: MyString body: MyText - post_id: + article_id: two: commenter: MyString body: MyText - post_id: + article_id: diff --git a/guides/code/getting_started/test/helpers/articles_helper_test.rb b/guides/code/getting_started/test/helpers/articles_helper_test.rb new file mode 100644 index 0000000000..b341344067 --- /dev/null +++ b/guides/code/getting_started/test/helpers/articles_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class ArticlesHelperTest < ActionView::TestCase +end diff --git a/guides/code/getting_started/test/helpers/posts_helper_test.rb b/guides/code/getting_started/test/helpers/posts_helper_test.rb deleted file mode 100644 index 48549c2ea1..0000000000 --- a/guides/code/getting_started/test/helpers/posts_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class PostsHelperTest < ActionView::TestCase -end diff --git a/guides/code/getting_started/test/models/post_test.rb b/guides/code/getting_started/test/models/article_test.rb index 6d9d463a71..11c8abe5f4 100644 --- a/guides/code/getting_started/test/models/post_test.rb +++ b/guides/code/getting_started/test/models/article_test.rb @@ -1,6 +1,6 @@ require 'test_helper' -class PostTest < ActiveSupport::TestCase +class ArticleTest < ActiveSupport::TestCase # test "the truth" do # assert true # end diff --git a/guides/code/getting_started/test/test_helper.rb b/guides/code/getting_started/test/test_helper.rb index f91a4375dc..ecbaf77ea7 100644 --- a/guides/code/getting_started/test/test_helper.rb +++ b/guides/code/getting_started/test/test_helper.rb @@ -6,9 +6,6 @@ class ActiveSupport::TestCase ActiveRecord::Migration.check_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 697ddd70cb..ee8cf4ade6 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1703,12 +1703,19 @@ may yield ``` EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ -| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | -| 1 | SIMPLE | articles | ALL | NULL | NULL | NULL | NULL | 1 | Using where | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ ++----+-------------+----------+-------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+----------+-------+---------------+ +| 1 | SIMPLE | users | const | PRIMARY | +| 1 | SIMPLE | articles | ALL | NULL | ++----+-------------+----------+-------+---------------+ ++---------+---------+-------+------+-------------+ +| key | key_len | ref | rows | Extra | ++---------+---------+-------+------+-------------+ +| PRIMARY | 4 | const | 1 | | +| NULL | NULL | NULL | 1 | Using where | ++---------+---------+-------+------+-------------+ + 2 rows in set (0.00 sec) ``` @@ -1742,19 +1749,32 @@ yields ``` EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ -| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ ++----+-------------+-------+-------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+-------+-------+---------------+ +| 1 | SIMPLE | users | const | PRIMARY | ++----+-------------+-------+-------+---------------+ ++---------+---------+-------+------+-------+ +| key | key_len | ref | rows | Extra | ++---------+---------+-------+------+-------+ +| PRIMARY | 4 | const | 1 | | ++---------+---------+-------+------+-------+ + 1 row in set (0.00 sec) EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1) -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ -| 1 | SIMPLE | articles | ALL | NULL | NULL | NULL | NULL | 1 | Using where | -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ ++----+-------------+----------+------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+----------+------+---------------+ +| 1 | SIMPLE | articles | ALL | NULL | ++----+-------------+----------+------+---------------+ ++------+---------+------+------+-------------+ +| key | key_len | ref | rows | Extra | ++------+---------+------+------+-------------+ +| NULL | NULL | NULL | 1 | Using where | ++------+---------+------+------+-------------+ + + 1 row in set (0.00 sec) ``` diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 0061c83a0c..6efc64c385 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -378,13 +378,13 @@ About your application's environment Ruby version 1.9.3 (x86_64-linux) RubyGems version 1.3.6 Rack version 1.3 -Rails version 4.1.0 +Rails version 4.1.1 JavaScript Runtime Node.js (V8) -Active Record version 4.1.0 -Action Pack version 4.1.0 -Action View version 4.1.0 -Action Mailer version 4.1.0 -Active Support version 4.1.0 +Active Record version 4.1.1 +Action Pack version 4.1.1 +Action View version 4.1.1 +Action Mailer version 4.1.1 +Active Support version 4.1.1 Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 2dd00bed2e..7a9e1beb23 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -729,13 +729,47 @@ Rails will now prepend "/app1" when generating links. #### Using Passenger -Passenger makes it easy to run your application in a subdirectory. You can find -the relevant configuration in the -[passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). +Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). #### Using a Reverse Proxy -TODO +Deploying your application using a reverse proxy has definite advantages over traditional deploys. They allow you to have more control over your server by layering the components required by your application. + +Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers. + +One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy. + +In this case, you would need to configure the proxy server (nginx, apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. + +You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it. + +Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your nginx config may include: + +``` +upstream application_server { + server 0.0.0.0:8080 +} + +server { + listen 80; + server_name localhost; + + root /root/path/to/your_app/public; + + try_files $uri/index.html $uri.html @app; + + location @app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://application_server; + } + + # some other configuration +} +``` + +Be sure to read the [nginx documentation](http://nginx.org/en/docs/) for the most up-to-date information. #### Considerations when deploying to a subdirectory diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index d3a96daf7b..133ef58fd6 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -215,6 +215,36 @@ Rails follows a simple set of coding style conventions: The above are guidelines - please use your best judgment in using them. +### Benchmark Your Code + +If your change has an impact on the performance of Rails, please use the +[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide +benchmark results for comparison. + +Here's an example of using benchmark-ips: + +```ruby +require 'benchmark/ips' + +Benchmark.ips do |x| + x.report('addition') { 1 + 2 } + x.report('addition with send') { 1.send(:+, 2) } +end +``` + +This will generate a report with the following information: + +``` +Calculating ------------------------------------- + addition 69114 i/100ms + addition with send 64062 i/100ms +------------------------------------------------- + addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s + addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s +``` + +Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information. + ### Running Tests It is not customary in Rails to run the full test suite before pushing diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index e79ebae818..5f738b76af 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -308,7 +308,7 @@ For example: ```bash => Booting WEBrick -=> Rails 4.1.0 application starting in development on http://0.0.0.0:3000 +=> Rails 4.1.1 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option) => Ctrl-C to shutdown server @@ -421,11 +421,11 @@ then `backtrace` will supply the answer. --> #0 ArticlesController.index at /PathTo/project/test_app/app/controllers/articles_controller.rb:8 #1 ActionController::ImplicitRender.send_action(method#String, *args#Array) - at /PathToGems/actionpack-4.1.0/lib/action_controller/metal/implicit_render.rb:4 + at /PathToGems/actionpack-4.1.1/lib/action_controller/metal/implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb:189 + at /PathToGems/actionpack-4.1.1/lib/abstract_controller/base.rb:189 #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass) - at /PathToGems/actionpack-4.1.0/lib/action_controller/metal/rendering.rb:10 + at /PathToGems/actionpack-4.1.1/lib/action_controller/metal/rendering.rb:10 ... ``` @@ -437,7 +437,7 @@ context. ``` (byebug) frame 2 -[184, 193] in /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb +[184, 193] in /PathToGems/actionpack-4.1.1/lib/abstract_controller/base.rb 184: # is the intended way to override action dispatching. 185: # 186: # Notice that the first argument is the method to be dispatched @@ -654,7 +654,7 @@ instruction to be executed. In this case, the activesupport's `week` method. ``` (byebug) step -[50, 59] in /PathToGems/activesupport-4.1.0/lib/active_support/core_ext/numeric/time.rb +[50, 59] in /PathToGems/activesupport-4.1.1/lib/active_support/core_ext/numeric/time.rb 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) 51: end 52: alias :day :days diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 34ce570545..530232f3f3 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -125,7 +125,7 @@ run the following: $ bin/rails --version ``` -If it says something like "Rails 4.1.0", you are ready to continue. +If it says something like "Rails 4.1.1", you are ready to continue. ### Creating the Blog Application diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 8faf03e58c..f0230b428b 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -13,7 +13,7 @@ After reading this guide, you will know: Markdown ------- -Guides are written in [GitHub Flavored Markdown](http://github.github.com/github-flavored-markdown/). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics), and [additional documentation](http://github.github.com/github-flavored-markdown/) on the differences from traditional Markdown. +Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics). Prologue -------- diff --git a/guides/source/testing.md b/guides/source/testing.md index e9a5e0d7ab..4149146c4c 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -699,8 +699,6 @@ A simple integration test that exercises multiple controllers: require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest - fixtures :users - test "login and browse site" do # login via https https! @@ -727,10 +725,7 @@ Here's an example of multiple sessions and custom DSL in an integration test require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest - fixtures :users - test "login and browse site" do - # User david logs in david = login(:david) # User guest logs in diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index ba6a0feeef..1343f0a628 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,9 +1,12 @@ +* Replace double quotes with single quotes while adding an entry into Gemfile. + + *Alexander Belaev* + * Default `config.assets.digest` to `true` in development. *Dan Kang* -* Load database configuration from the first - database.yml available in paths. +* Load database configuration from the first `database.yml` available in paths. *Pier-Olivier Thibault* diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index dce734b54e..04ce38f841 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -156,7 +156,8 @@ module Rails args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? } klass.start(args, config) else - puts "Could not find generator #{namespace}." + puts "Could not find generator '#{namespace}'. Please choose a generator below." + print_generators end end @@ -199,17 +200,6 @@ module Rails # Show help message with available generators. def self.help(command = 'generate') - lookup! - - namespaces = subclasses.map{ |k| k.namespace } - namespaces.sort! - - groups = Hash.new { |h,k| h[k] = [] } - namespaces.each do |namespace| - base = namespace.split(':').first - groups[base] << namespace - end - puts "Usage: rails #{command} GENERATOR [args] [options]" puts puts "General options:" @@ -222,6 +212,20 @@ module Rails puts "Please choose a generator below." puts + print_generators + end + + def self.print_generators + lookup! + + namespaces = subclasses.map{ |k| k.namespace } + namespaces.sort! + + groups = Hash.new { |h,k| h[k] = [] } + namespaces.each do |namespace| + base = namespace.split(':').first + groups[base] << namespace + end # Print Rails defaults first. rails = groups.delete("rails") rails.map! { |n| n.sub(/^rails:/, '') } diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 625f031c94..a239874df0 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -20,9 +20,9 @@ module Rails # Set the message to be shown in logs. Uses the git repo if one is given, # otherwise use name (version). - parts, message = [ name.inspect ], name + parts, message = [ quote(name) ], name if version ||= options.delete(:version) - parts << version.inspect + parts << quote(version) message << " (#{version})" end message = options[:git] if options[:git] @@ -30,7 +30,7 @@ module Rails log :gemfile, message options.each do |option, value| - parts << "#{option}: #{value.inspect}" + parts << "#{option}: #{quote(value)}" end in_root do @@ -68,7 +68,7 @@ module Rails log :source, source in_root do - prepend_file "Gemfile", "source #{source.inspect}\n", verbose: false + prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false end end @@ -255,6 +255,15 @@ module Rails end end + # Surround string with single quotes if there is no quotes. + # Otherwise fall back to double quotes + def quote(str) + if str.include?("'") + str.inspect + else + "'#{str}'" + end + end end end end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 5a92ab3e95..b7da44ca2d 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -30,7 +30,12 @@ module Rails protected attr_reader :file_name - alias :singular_name :file_name + + # FIXME: We are avoiding to use alias because a bug on thor that make + # this method public and add it to the task list. + def singular_name + file_name + end # Wrap block with namespace of current application # if namespace exists and is not skipped diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 448b6f4845..5bdbd58097 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -16,7 +16,7 @@ source 'https://rubygems.org' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' -# Use unicorn as the app server +# Use Unicorn as the app server # gem 'unicorn' # Use Capistrano for deployment diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index 6b011e577a..87b8fe3516 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -5,9 +5,6 @@ require 'rails/test_help' class ActiveSupport::TestCase <% unless options[:skip_active_record] -%> # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - # - # Note: You'll currently still have to declare fixtures explicitly in integration tests - # -- they do not yet inherit this setting fixtures :all <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index 0ba899176c..c338a0bdb1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -19,6 +19,10 @@ APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__) load 'rails/tasks/engine.rake' <% end %> +<% if engine? -%> +load 'rails/tasks/statistics.rake' +<% end %> + <% unless options[:skip_gemspec] -%> Bundler::GemHelper.install_tasks diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 908c4ce65e..49e5431a16 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -5,7 +5,7 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH layout -> { request.xhr? ? false : 'application' } - before_filter :require_local! + before_action :require_local! def index redirect_to action: :routes diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index dd318f52e5..32740d66da 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -3,8 +3,8 @@ require 'rails/application_controller' class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - before_filter :require_local! - before_filter :find_preview, only: :preview + before_action :require_local! + before_action :find_preview, only: :preview def index @previews = ActionMailer::Preview.all @@ -70,4 +70,4 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @email end end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index 3b7f358a5b..df74643a59 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -2,7 +2,7 @@ if RUBY_VERSION < '1.9.3' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 4 prefers to run on Ruby 2.0. + Rails 4 prefers to run on Ruby 2.1 or newer. You're running #{desc} diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index c1674c72ad..ae5a7d2759 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -1,3 +1,6 @@ +# while having global constant is not good, +# many 3rd party tools depend on it, like rspec-rails, cucumber-rails, etc +# so if will be removed - deprecation warning is needed STATS_DIRECTORIES = [ %w(Controllers app/controllers), %w(Helpers app/helpers), @@ -13,10 +16,12 @@ STATS_DIRECTORIES = [ %w(Integration\ tests test/integration), %w(Functional\ tests\ (old) test/functional), %w(Unit\ tests \ (old) test/unit) -].collect { |name, dir| [ name, "#{Rails.root}/#{dir}" ] }.select { |name, dir| File.directory?(dir) } +].collect do |name, dir| + [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] +end.select { |name, dir| File.directory?(dir) } -desc "Report code statistics (KLOCs, etc) from the application" +desc "Report code statistics (KLOCs, etc) from the application or engine" task :stats do require 'rails/code_statistics' CodeStatistics.new(*STATS_DIRECTORIES).to_s -end +end
\ No newline at end of file diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 0db40c1d32..6d6de0fb52 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -41,13 +41,13 @@ class ActionsTest < Rails::Generators::TestCase def test_add_source_adds_source_to_gemfile run_generator action :add_source, 'http://gems.github.com' - assert_file 'Gemfile', /source "http:\/\/gems\.github\.com"/ + assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/ end def test_gem_should_put_gem_dependency_in_gemfile run_generator action :gem, 'will-paginate' - assert_file 'Gemfile', /gem "will\-paginate"/ + assert_file 'Gemfile', /gem 'will\-paginate'/ end def test_gem_with_version_should_include_version_in_gemfile @@ -55,7 +55,7 @@ class ActionsTest < Rails::Generators::TestCase action :gem, 'rspec', '>=2.0.0.a5' - assert_file 'Gemfile', /gem "rspec", ">=2.0.0.a5"/ + assert_file 'Gemfile', /gem 'rspec', '>=2.0.0.a5'/ end def test_gem_should_insert_on_separate_lines @@ -66,8 +66,8 @@ class ActionsTest < Rails::Generators::TestCase action :gem, 'rspec' action :gem, 'rspec-rails' - assert_file 'Gemfile', /^gem "rspec"$/ - assert_file 'Gemfile', /^gem "rspec-rails"$/ + assert_file 'Gemfile', /^gem 'rspec'$/ + assert_file 'Gemfile', /^gem 'rspec-rails'$/ end def test_gem_should_include_options @@ -75,7 +75,15 @@ class ActionsTest < Rails::Generators::TestCase action :gem, 'rspec', github: 'dchelimsky/rspec', tag: '1.2.9.rc1' - assert_file 'Gemfile', /gem "rspec", github: "dchelimsky\/rspec", tag: "1\.2\.9\.rc1"/ + assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/ + end + + def test_gem_falls_back_to_inspect_if_string_contains_single_quote + run_generator + + action :gem, 'rspec', ">=2.0'0" + + assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/ end def test_gem_group_should_wrap_gems_in_a_group @@ -89,7 +97,7 @@ class ActionsTest < Rails::Generators::TestCase gem 'fakeweb' end - assert_file 'Gemfile', /\ngroup :development, :test do\n gem "rspec-rails"\nend\n\ngroup :test do\n gem "fakeweb"\nend/ + assert_file 'Gemfile', /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/ end def test_environment_should_include_data_in_environment_initializer_block diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb index 31e07bc8da..31c2d846e2 100644 --- a/railties/test/generators/argv_scrubber_test.rb +++ b/railties/test/generators/argv_scrubber_test.rb @@ -1,5 +1,5 @@ -require 'active_support/test_case' require 'active_support/testing/autorun' +require 'active_support/test_case' require 'rails/generators/rails/app/app_generator' require 'tempfile' diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index 7871399dd7..b136239795 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -1,5 +1,5 @@ -require 'active_support/test_case' require 'active_support/testing/autorun' +require 'active_support/test_case' require 'rails/generators/app_base' module Rails diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index eac28badfe..8d6dbf80c2 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -21,8 +21,10 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_invoke_when_generator_is_not_found - output = capture(:stdout){ Rails::Generators.invoke :unknown } - assert_equal "Could not find generator unknown.\n", output + name = :unknown + output = capture(:stdout){ Rails::Generators.invoke name } + assert_match "Could not find generator '#{name}'", output + assert_match "scaffold", output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments |