diff options
151 files changed, 2997 insertions, 2698 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index be1f53faf5..92e94ee463 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,4 +1,9 @@ -* Fix 'Stack level too deep' when rendering `head :ok` in an action method +* 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 called 'status' in a controller. Fixes #13905. @@ -66,7 +71,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. @@ -96,7 +101,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* diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 696fbf6e09..70ca99f01c 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 diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 2eb7853aa6..3111992f82 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -90,17 +90,29 @@ module ActionController end def authenticate(request, &login_procedure) - unless request.authorization.blank? + if has_basic_credentials?(request) login_procedure.call(*user_name_and_password(request)) end end + def has_basic_credentials?(request) + request.authorization.present? && (auth_scheme(request) == 'Basic') + end + def user_name_and_password(request) decode_credentials(request).split(':', 2) end def decode_credentials(request) - ::Base64.decode64(request.authorization.split(' ', 2).last || '') + ::Base64.decode64(auth_param(request) || '') + end + + def auth_scheme(request) + request.authorization.split(' ', 2).first + end + + def auth_param(request) + request.authorization.split(' ', 2).second end def encode_credentials(user_name, password) diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 2812038938..136e086d0d 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -14,7 +14,7 @@ module ActionController include ActionController::RackDelegation include ActionController::UrlFor - # Redirects the browser to the target specified in +options+. This parameter can take one of three forms: + # Redirects the browser to the target specified in +options+. This parameter can be any one of: # # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. @@ -24,6 +24,8 @@ module ActionController # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # + # === Examples: + # # redirect_to action: "show", id: 5 # redirect_to post # redirect_to "http://www.rubyonrails.org" @@ -32,7 +34,7 @@ module ActionController # redirect_to :back # redirect_to proc { edit_post_url(@post) } # - # The redirection happens as a "302 Found" header unless otherwise specified. + # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: # # redirect_to post_url(@post), status: :found # redirect_to action: 'atom', status: :moved_permanently @@ -60,8 +62,10 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback - # behavior for this case by rescuing ActionController::RedirectBackError. + # When using <tt>redirect_to :back</tt>, if there is no referrer, + # <tt>ActionController::RedirectBackError</tt> will be raised. You + # may specify some fallback behavior for this case by rescuing + # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 37d4a96ee1..07265be3fe 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -23,12 +23,12 @@ module ActionController include AbstractController::UrlFor def url_options - @_url_options ||= super.reverse_merge( + @_url_options ||= { :host => request.host, :port => request.optional_port, :protocol => request.protocol, - :_recall => request.symbolized_path_parameters - ).freeze + :_recall => request.path_parameters + }.merge(super).freeze if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index c6a8f581de..e6695ffc90 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -199,7 +199,7 @@ module ActionController value = value.dup end - if extra_keys.include?(key.to_sym) + if extra_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) @@ -208,7 +208,7 @@ module ActionController value = value.to_param end - path_parameters[key.to_s] = value + path_parameters[key] = value end end @@ -583,6 +583,7 @@ module ActionController end parameters, session, flash = args + parameters ||= {} # Ensure that numbers and symbols passed as params are converted to # proper params, as is the case when engaging rack. @@ -601,7 +602,6 @@ module ActionController @request.env['REQUEST_METHOD'] = http_method - parameters ||= {} controller_class_name = @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path @@ -695,7 +695,7 @@ module ActionController :only_path => true, :action => action, :relative_url_root => nil, - :_recall => @request.symbolized_path_parameters) + :_recall => @request.path_parameters) url, query_string = @routes.url_for(options).split("?", 2) @@ -706,7 +706,7 @@ module ActionController end def html_format?(parameters) - return true unless parameters.is_a?(Hash) + return true unless parameters.key?(:format) Mime.fetch(parameters[:format]) { Mime['html'] }.html? end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index dcb299ed03..5b22cd1fcd 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -24,14 +24,13 @@ module ActionDispatch alias :params :parameters def path_parameters=(parameters) #:nodoc: - @symbolized_path_params = nil - @env.delete("action_dispatch.request.parameters") - @env["action_dispatch.request.path_parameters"] = parameters + @env.delete('action_dispatch.request.parameters') + @env[Routing::RouteSet::PARAMETERS_KEY] = parameters end # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @symbolized_path_params ||= path_parameters.symbolize_keys + path_parameters end # Returns a hash with the \parameters used to form the \path of the request. @@ -41,7 +40,7 @@ module ActionDispatch # # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @env["action_dispatch.request.path_parameters"] ||= {} + @env[Routing::RouteSet::PARAMETERS_KEY] ||= {} end def reset_parameters #:nodoc: diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index c9860af909..a6c17f50a5 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -30,19 +30,25 @@ module ActionDispatch end def url_for(options) + unless options[:host] || options[:only_path] + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end + path = options[:script_name].to_s.chomp("/") path << options[:path].to_s - result = build_host_url(options) - if options[:trailing_slash] if path.include?('?') - result << path.sub(/\?/, '/\&') + path.sub!(/\?/, '/\&') else - result << path.sub(/[^\/]\z|\A\z/, '\&/') + path.sub!(/[^\/]\z|\A\z/, '\&/') end - else - result << path + end + + result = path + + unless options[:only_path] + result.prepend build_host_url(options) end if options.key? :params @@ -61,28 +67,25 @@ module ActionDispatch private def build_host_url(options) - unless options[:host] || options[:only_path] - raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + if match = options[:host].match(HOST_REGEXP) + options[:protocol] ||= match[1] unless options[:protocol] == false + options[:host] = match[2] + options[:port] = match[3] unless options.key?(:port) end - result = "" + options[:protocol] = normalize_protocol(options) + options[:host] = normalize_host(options) + options[:port] = normalize_port(options) - unless options[:only_path] - if match = options[:host].match(HOST_REGEXP) - options[:protocol] ||= match[1] unless options[:protocol] == false - options[:host] = match[2] - options[:port] = match[3] unless options.key?(:port) - end - - options[:protocol] = normalize_protocol(options) - options[:host] = normalize_host(options) - options[:port] = normalize_port(options) + result = options[:protocol] - result << options[:protocol] - result << rewrite_authentication(options) - result << options[:host] - result << ":#{options[:port]}" if options[:port] + if options[:user] && options[:password] + result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" end + + result << options[:host] + result << ":#{options[:port]}" if options[:port] + result end @@ -94,14 +97,6 @@ module ActionDispatch (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil? end - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" - else - "" - end - end - def normalize_protocol(options) case options[:protocol] when nil diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 57f0963731..6d58323789 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -12,7 +12,7 @@ module ActionDispatch @cache = nil end - def generate(type, name, options, recall = {}, parameterize = nil) + def generate(name, options, recall = {}, parameterize = nil) constraints = recall.merge(options) missing_keys = [] @@ -30,6 +30,12 @@ module ActionDispatch parameterized_parts.key?(key) || route.defaults.key?(key) end + defaults = route.defaults + required_parts = route.required_parts + parameterized_parts.delete_if do |key, value| + value.to_s == defaults[key].to_s && !required_parts.include?(key) + end + return [route.format(parameterized_parts), params] end @@ -74,12 +80,12 @@ module ActionDispatch if named_routes.key?(name) yield named_routes[name] else - routes = non_recursive(cache, options.to_a) + routes = non_recursive(cache, options) hash = routes.group_by { |_, r| r.score(options) } hash.keys.sort.reverse_each do |score| - next if score < 0 + break if score < 0 hash[score].sort_by { |i, _| i }.each do |_, route| yield route @@ -90,14 +96,14 @@ module ActionDispatch def non_recursive(cache, options) routes = [] - stack = [cache] + queue = [cache] - while stack.any? - c = stack.shift + while queue.any? + c = queue.shift routes.concat(c[:___routes]) if c.key?(:___routes) options.each do |pair| - stack << c[pair] if c.key?(pair) + queue << c[pair] if c.key?(pair) end end @@ -126,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/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index fb155e516f..cb0a02c298 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -30,6 +30,10 @@ module ActionDispatch @offsets = nil end + def build_formatter + Visitors::FormatBuilder.new.accept(spec) + end + def ast @spec.grep(Nodes::Symbol).each do |node| re = @requirements[node.to_sym] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 2b399d3ee3..cc3c7f20cb 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -31,6 +31,7 @@ module ActionDispatch @parts = nil @decorated_ast = nil @precedence = 0 + @path_formatter = @path.build_formatter end def ast @@ -72,15 +73,7 @@ module ActionDispatch alias :segment_keys :parts def format(path_options) - path_options.delete_if do |key, value| - value.to_s == defaults[key].to_s && !required_parts.include?(key) - end - - Visitors::Formatter.new(path_options).accept(path.spec) - end - - def optimized_path - Visitors::OptimizedPath.new.accept(path.spec) + @path_formatter.evaluate path_options end def optional_parts diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 36561c71a1..2ead6a4eb3 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -20,60 +20,31 @@ module ActionDispatch # :nodoc: VERSION = '2.0.0' - class NullReq # :nodoc: - attr_reader :env - def initialize(env) - @env = env - 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 - @params_key = options[:parameters_key] - @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']) - - find_routes(env).each do |match, parameters, route| - script_name, path_info, set_params = env.values_at('SCRIPT_NAME', - 'PATH_INFO', - @params_key) + 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 - env[@params_key] = (set_params || {}).merge parameters + req.path_parameters = set_params.merge parameters - status, headers, body = route.app.call(env) + status, headers, body = route.app.call(req.env) if 'pass' == headers['X-Cascade'] - env['SCRIPT_NAME'] = script_name - env['PATH_INFO'] = path_info - env[@params_key] = set_params + req.script_name = script_name + req.path_info = path_info + req.path_parameters = set_params next end @@ -83,14 +54,14 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, ['Not Found']] end - def recognize(req) - find_routes(req.env).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, nil, parameters) + yield(route, parameters) end end @@ -124,9 +95,7 @@ module ActionDispatch simulator.memos(path) { [] } end - def find_routes env - req = request_class.new(env) - + def find_routes req routes = filter_routes(req.path_info).concat custom_routes.find_all { |r| r.path.match(req.path_info) } @@ -136,11 +105,11 @@ module ActionDispatch routes.map! { |r| match_data = r.path.match(req.path_info) - match_names = match_data.names.map { |n| n.to_sym } - match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) } - info = Hash[match_names.zip(match_values).find_all { |_, y| y }] - - [match_data, r.defaults.merge(info), r] + path_parameters = r.defaults.dup + match_data.names.zip(match_data.captures) { |name,val| + path_parameters[name.to_sym] = Utils.unescape_uri(val) if val + } + [match_data, path_parameters, r] } end diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 616c2d26d4..52b4c8b489 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,14 +1,57 @@ # encoding: utf-8 -require 'thread_safe' - module ActionDispatch module Journey # :nodoc: + class Format + ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } + ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } + + class Parameter < Struct.new(:name, :escaper) + def escape(value); escaper.call value; end + end + + def self.required_path(symbol) + Parameter.new symbol, ESCAPE_PATH + end + + def self.required_segment(symbol) + Parameter.new symbol, ESCAPE_SEGMENT + end + + def initialize(parts) + @parts = parts + @children = [] + @parameters = [] + + parts.each_with_index do |object,i| + case object + when Journey::Format + @children << i + when Parameter + @parameters << i + end + end + end + + def evaluate(hash) + parts = @parts.dup + + @parameters.each do |index| + param = parts[index] + value = hash[param.name] + return ''.freeze unless value + parts[index] = param.escape value + end + + @children.each { |index| parts[index] = parts[index].evaluate(hash) } + + parts.join + end + end + module Visitors # :nodoc: class Visitor # :nodoc: - DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k| - h[k] = :"visit_#{k}" - } + DISPATCH_CACHE = {} def accept(node) visit(node) @@ -38,11 +81,41 @@ module ActionDispatch def visit_STAR(n); unary(n); end def terminal(node); end - %w{ LITERAL SYMBOL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ + def visit_LITERAL(n); terminal(n); end + def visit_SYMBOL(n); terminal(n); end + def visit_SLASH(n); terminal(n); end + def visit_DOT(n); terminal(n); end + + private_instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim end end + class FormatBuilder < Visitor # :nodoc: + def accept(node); Journey::Format.new(super); end + def terminal(node); [node.left]; end + + def binary(node) + visit(node.left) + visit(node.right) + end + + def visit_GROUP(n); [Journey::Format.new(unary(n))]; end + + def visit_STAR(n) + [Journey::Format.required_path(n.left.to_sym)] + end + + def visit_SYMBOL(n) + symbol = n.to_sym + if symbol == :controller + [Journey::Format.required_path(symbol)] + else + [Journey::Format.required_segment(symbol)] + end + end + end + # Loop through the requirements AST class Each < Visitor # :nodoc: attr_reader :block @@ -52,8 +125,8 @@ module ActionDispatch end def visit(node) - super block.call(node) + super end end @@ -77,91 +150,6 @@ module ActionDispatch end end - class OptimizedPath < Visitor # :nodoc: - def accept(node) - Array(visit(node)) - end - - private - - def visit_CAT(node) - [visit(node.left), visit(node.right)].flatten - end - - def visit_SYMBOL(node) - node.left[1..-1].to_sym - end - - def visit_STAR(node) - visit(node.left) - end - - def visit_GROUP(node) - [] - end - - %w{ LITERAL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ - end - end - - # Used for formatting urls (url_for) - class Formatter < Visitor # :nodoc: - attr_reader :options, :consumed - - def initialize(options) - @options = options - @consumed = {} - end - - private - def escape_path(value) - Router::Utils.escape_path(value) - end - - def escape_segment(value) - Router::Utils.escape_segment(value) - end - - def visit_GROUP(node) - if consumed == options - nil - else - route = visit(node.left) - route.include?("\0") ? nil : route - end - end - - def terminal(node) - node.left - end - - def binary(node) - [visit(node.left), visit(node.right)].join - end - - def nary(node) - node.children.map { |c| visit(c) }.join - end - - def visit_STAR(node) - if value = options[node.left.to_sym] - escape_path(value) - end - end - - def visit_SYMBOL(node) - key = node.to_sym - - if value = options[key] - consumed[key] = value - key == :controller ? escape_path(value) : escape_segment(value) - else - "\0" - end - end - end - class Dot < Visitor # :nodoc: def initialize @nodes = [] 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/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index cbb2d475b1..6c8944e067 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -32,9 +32,8 @@ module ActionDispatch end def render_html(status) - found = false - path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale - path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path)) + path = "#{public_path}/#{status}.#{I18n.locale}.html" + path = "#{public_path}/#{status}.html" unless (found = File.exist?(path)) if found || File.exist?(path) render_format(status, 'text/html', File.read(path)) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 58c7f5330e..f39fd1ea35 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -16,14 +16,6 @@ module ActionDispatch :shallow, :blocks, :defaults, :options] class Constraints #:nodoc: - def self.new(app, constraints, request = Rack::Request) - if constraints.any? - super(app, constraints, request) - else - app - end - end - attr_reader :app, :constraints def initialize(app, constraints, request) @@ -47,7 +39,7 @@ module ActionDispatch private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request] + constraint.arity == 1 ? [request] : [request.path_parameters, request] end end @@ -57,12 +49,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 +92,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 +108,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 +207,11 @@ module ActionDispatch end def app - Constraints.new(endpoint, blocks, @set.request_class) + if blocks.any? + Constraints.new(endpoint, blocks, @set.request_class) + else + endpoint + end end def default_controller_and_action @@ -301,19 +302,7 @@ module ActionDispatch 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 b08e62543b..f8ed0cbe6a 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -19,13 +19,13 @@ module ActionDispatch # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. - req.symbolized_path_parameters.each do |key, value| + req.path_parameters.each do |key, value| unless value.valid_encoding? raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" end end - uri = URI.parse(path(req.symbolized_path_parameters, req)) + uri = URI.parse(path(req.path_parameters, req)) unless uri.host if relative_path?(uri.path) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index e699419f23..40c767e685 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -21,9 +21,8 @@ module ActionDispatch PARAMETERS_KEY = 'action_dispatch.request.path_parameters' class Dispatcher #:nodoc: - def initialize(options={}) - @defaults = options[:defaults] - @glob_param = options.delete(:glob) + def initialize(defaults) + @defaults = defaults @controller_class_names = ThreadSafe::Cache.new end @@ -53,7 +52,6 @@ module ActionDispatch 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 +87,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 @@ -165,10 +159,8 @@ module ActionDispatch def initialize(route, options) super - @klass = Journey::Router::Utils @required_parts = @route.required_parts @arg_size = @required_parts.size - @optimized_path = @route.optimized_path end def call(t, args) @@ -184,18 +176,14 @@ module ActionDispatch private def optimized_helper(args) - params = Hash[parameterize_args(args)] + params = parameterize_args(args) missing_keys = missing_keys(params) unless missing_keys.empty? raise_generation_error(params, missing_keys) end - @optimized_path.map{ |segment| replace_segment(params, segment) }.join - end - - def replace_segment(params, segment) - Symbol === segment ? @klass.escape_segment(params[segment]) : segment + @route.format params end def optimize_routes_generation?(t) @@ -203,7 +191,9 @@ module ActionDispatch end def parameterize_args(args) - @required_parts.zip(args.map(&:to_param)) + params = {} + @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v } + params end def missing_keys(args) @@ -226,21 +216,23 @@ module ActionDispatch end def call(t, args) - options = t.url_options.merge @options - hash = handle_positional_args(t, args, options, @segment_keys) + controller_options = t.url_options + options = controller_options.merge @options + hash = handle_positional_args(controller_options, args, options, @segment_keys) t._routes.url_for(hash) end - def handle_positional_args(t, args, result, keys) + def handle_positional_args(controller_options, args, result, path_params) inner_options = args.extract_options! if args.size > 0 - if args.size < keys.size - 1 # take format into account - keys -= t.url_options.keys - keys -= result.keys + if args.size < path_params.size - 1 # take format into account + path_params -= controller_options.keys + path_params -= result.keys end - keys -= inner_options.keys - result.merge!(Hash[keys.zip(args)]) + path_params.each { |param| + result[param] = inner_options[param] || args.shift + } end result.merge!(inner_options) @@ -304,9 +296,7 @@ module ActionDispatch @finalized = false @set = Journey::Routes.new - @router = Journey::Router.new(@set, { - :parameters_key => PARAMETERS_KEY, - :request_class => request_class}) + @router = Journey::Router.new @set @formatter = Journey::Formatter.new @set end @@ -598,7 +588,7 @@ module ActionDispatch # Generates a path from routes, returns [path, params]. # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate - @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) + @set.formatter.generate(named_route, options, recall, PARAMETERIZE) end def different_controller? @@ -671,19 +661,24 @@ module ActionDispatch RESERVED_OPTIONS.each { |ro| path_options.delete ro } path, params = generate(path_options, recall) - params.merge!(options[:params] || {}) - - ActionDispatch::Http::URL.url_for(options.merge!({ - :path => path, - :script_name => script_name, - :params => params, - :user => user, - :password => password - })) + + if options.key? :params + params.merge! options[:params] + end + + options[:path] = path + options[:script_name] = script_name + options[:params] = params + options[:user] = user + options[:password] = password + + ActionDispatch::Http::URL.url_for(options) 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,8 +692,8 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = @request_class.new(env) - @router.recognize(req) do |route, _matches, params| + req = request_class.new(env) + @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| if value.is_a?(String) @@ -706,8 +701,8 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] - env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) + 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 diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index cc6b763093..d900f3c7a9 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -300,13 +300,7 @@ module ActionDispatch # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri - uri = URI.parse('/') - uri.scheme ||= env['rack.url_scheme'] - uri.host ||= env['SERVER_NAME'] - uri.port ||= env['SERVER_PORT'].try(:to_i) - uri += path - - session.request(uri.to_s, env) + session.request(build_full_uri(path, env), env) @request_count += 1 @request = ActionDispatch::Request.new(session.last_request.env) @@ -319,6 +313,10 @@ module ActionDispatch return response.status end + + def build_full_uri(path, env) + "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" + end end module Runner diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 90548d4294..9052fc6962 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -129,6 +129,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_response :unauthorized end + test "authentication request with wrong scheme" do + header = 'Bearer ' + encode_credentials('David', 'Goliath').split(' ', 2)[1] + @request.env['HTTP_AUTHORIZATION'] = header + get :search + assert_response :unauthorized + end + private def encode_credentials(username, password) diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index fbc10baf21..18a5d8b094 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -662,7 +662,7 @@ XML def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new - assert_kind_of String, @request.path_parameters['id'] + assert_kind_of String, @request.path_parameters[:id] end def test_array_path_parameter_handled_properly @@ -673,17 +673,17 @@ XML end get :test_params, :path => ['hello', 'world'] - assert_equal ['hello', 'world'], @request.path_parameters['path'] - assert_equal 'hello/world', @request.path_parameters['path'].to_param + assert_equal ['hello', 'world'], @request.path_parameters[:path] + assert_equal 'hello/world', @request.path_parameters[:path].to_param end end def test_assert_realistic_path_parameters get :test_params, :id => 20, :foo => Object.new - # All elements of path_parameters should use string keys + # All elements of path_parameters should use Symbol keys @request.path_parameters.keys.each do |key| - assert_kind_of String, key + assert_kind_of Symbol, key end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index cae6b312b6..a427113763 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1723,7 +1723,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "whatever/:controller(/:action(/:id))" end - get 'whatever/foo/bar' + get '/whatever/foo/bar' assert_equal 'foo#bar', @response.body assert_equal 'http://www.example.com/whatever/foo/bar/1', @@ -1735,10 +1735,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "whatever/:controller(/:action(/:id))", :id => /\d+/ end - get 'whatever/foo/bar/show' + get '/whatever/foo/bar/show' assert_equal 'foo/bar#show', @response.body - get 'whatever/foo/bar/show/1' + get '/whatever/foo/bar/show/1' assert_equal 'foo/bar#show', @response.body assert_equal 'http://www.example.com/whatever/foo/bar/show', @@ -2287,12 +2287,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "(/user/:username)/photos" => "photos#index" end - get 'user/bob/photos' + get '/user/bob/photos' assert_equal 'photos#index', @response.body assert_equal 'http://www.example.com/user/bob/photos', url_for(:controller => "photos", :action => "index", :username => "bob") - get 'photos' + get '/photos' assert_equal 'photos#index', @response.body assert_equal 'http://www.example.com/photos', url_for(:controller => "photos", :action => "index", :username => nil) @@ -3368,12 +3368,14 @@ end class TestAltApp < ActionDispatch::IntegrationTest class AltRequest + 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 @@ -3492,7 +3494,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest resources :storage_files, :controller => 'admin/storage_files' end - get 'storage_files' + get '/storage_files' assert_equal "admin/storage_files#index", @response.body end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index e54b64e0f3..db2d3bc10d 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -5,23 +5,21 @@ 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 + 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 +37,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 @@ -48,14 +46,14 @@ module ActionDispatch env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false - router.recognize(env) do |r, _, params| + router.recognize(env) do |r, params| called = true end assert called 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', {}, ['/.?'] @@ -65,7 +63,7 @@ module ActionDispatch env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false - router.recognize(env) do |r, _, params| + router.recognize(env) do |r, params| called = true end assert called @@ -73,7 +71,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,8 +80,8 @@ module ActionDispatch routes.add_route nil, path, requirements, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/foo/10' - router.recognize(env) do |r, _, params| + env = rails_env({'PATH_INFO' => '/foo/10'}, klass) + router.recognize(env) do |r, params| assert_equal({:id => '10'}, params) end @@ -93,7 +91,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,8 +100,8 @@ module ActionDispatch router.routes.add_route nil, path, requirements, {:id => nil}, {} - env = rails_env 'PATH_INFO' => '/foo/10' - router.recognize(env) do |r, _, params| + env = rails_env({'PATH_INFO' => '/foo/10'}, klass) + router.recognize(env) do |r, params| flunk 'route should not be found' end @@ -111,24 +109,29 @@ 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| + router.recognize(env) do |r, params| recognized = true end @@ -144,7 +147,7 @@ module ActionDispatch env = rails_env 'PATH_INFO' => '/whois/example.com' list = [] - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| list << r end assert_equal 2, list.length @@ -160,7 +163,7 @@ module ActionDispatch ] assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, nil, { :id => '10' }, { }) + @formatter.generate(nil, { :id => '10' }, { }) end end @@ -169,11 +172,11 @@ module ActionDispatch Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) ] - path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :id => '10' }, { }) assert_equal '/foo/10', path assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) + @formatter.generate(nil, { :id => 'aa' }, { }) end end @@ -182,13 +185,13 @@ module ActionDispatch Router::Strexp.new("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) ] - path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) + path, _ = @formatter.generate(nil, { :id => '10' }, { }) assert_equal '/foo/10', path - path, _ = @formatter.generate(:path_info, nil, { }, { }) + path, _ = @formatter.generate(nil, { }, { }) assert_equal '/foo', path - path, _ = @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) + path, _ = @formatter.generate(nil, { :id => 'aa' }, { }) assert_equal '/foo/aa', path end @@ -199,7 +202,7 @@ module ActionDispatch @router.routes.add_route nil, path, {}, {}, route_name error = assert_raises(ActionController::UrlGenerationError) do - @formatter.generate(:path_info, route_name, { }, { }) + @formatter.generate(route_name, { }, { }) end assert_match(/missing required keys: \[:id\]/, error.message) @@ -207,7 +210,7 @@ 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 @@ -220,7 +223,7 @@ module ActionDispatch @router.routes.add_route(app, path, {}, {}, {}) env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog') - resp = @router.call(env) + resp = @router.serve rails_env env assert_equal ['success!'], resp.last assert_equal '', env['SCRIPT_NAME'] end @@ -230,12 +233,12 @@ module ActionDispatch @router.routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo/10' - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal({:id => '10'}, params) end env = rails_env 'PATH_INFO' => '/foo' - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal({:id => nil}, params) end end @@ -287,14 +290,14 @@ module ActionDispatch def test_required_part_in_recall add_routes @router, [ "/messages/:a/:b" ] - path, _ = @formatter.generate(:path_info, nil, { :a => 'a' }, { :b => 'b' }) + path, _ = @formatter.generate(nil, { :a => 'a' }, { :b => 'b' }) assert_equal "/messages/a/b", path end def test_splat_in_recall add_routes @router, [ "/*path" ] - path, _ = @formatter.generate(:path_info, nil, { }, { :path => 'b' }) + path, _ = @formatter.generate(nil, { }, { :path => 'b' }) assert_equal "/b", path end @@ -304,7 +307,7 @@ module ActionDispatch "/messages/:id(.:format)" ] - path, _ = @formatter.generate(:path_info, nil, { :id => 10 }, { :action => 'index' }) + path, _ = @formatter.generate(nil, { :id => 10 }, { :action => 'index' }) assert_equal "/messages/index/10", path end @@ -315,7 +318,7 @@ module ActionDispatch params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } - path, _ = @formatter.generate(:path_info, nil, params, extras) + path, _ = @formatter.generate(nil, params, extras) assert_equal '/tasks', path end @@ -327,7 +330,7 @@ module ActionDispatch @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, nil, Hash[params], {}) + path, _ = @formatter.generate(nil, Hash[params], {}) assert_equal '/', path end @@ -340,7 +343,6 @@ module ActionDispatch [:action, "show"] ] @formatter.generate( - :path_info, nil, Hash[params], {}, @@ -354,7 +356,7 @@ module ActionDispatch @router.routes.add_route @app, path, {}, {}, {} path, params = @formatter.generate( - :path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) + nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) assert_equal '/tasks/show', path assert_equal({:id => 1}, params) end @@ -363,8 +365,8 @@ module ActionDispatch path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, - nil, { :controller => "tasks", + path, _ = @formatter.generate(nil, + { :controller => "tasks", :action => "a/b c+d", }, {}) assert_equal '/tasks/a%2Fb%20c+d', path @@ -374,7 +376,7 @@ module ActionDispatch path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, _ = @formatter.generate(:path_info, + path, _ = @formatter.generate( nil, { :controller => "admin/tasks", :action => "a/b c+d", }, {}) @@ -385,7 +387,7 @@ module ActionDispatch path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, params = @formatter.generate(:path_info, + path, params = @formatter.generate( nil, { :id => 1, :controller => "tasks", :action => "show", @@ -399,7 +401,7 @@ module ActionDispatch path = Path::Pattern.new '/:controller(/:action(/:id))' @router.routes.add_route @app, path, {}, {}, {} - path, params = @formatter.generate(:path_info, + path, params = @formatter.generate( nil, {:controller =>"tasks", :id => 10}, {:action =>"index"}) @@ -411,7 +413,7 @@ module ActionDispatch path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route @app, path, {}, {}, {} - path, params = @formatter.generate(:path_info, + path, params = @formatter.generate( "tasks", {:controller=>"tasks"}, {:controller=>"tasks", :action=>"index"}) @@ -432,7 +434,7 @@ module ActionDispatch env = rails_env 'PATH_INFO' => request_path called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -454,7 +456,7 @@ module ActionDispatch env = rails_env 'PATH_INFO' => request_path called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -482,7 +484,7 @@ module ActionDispatch :id => '10' } - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -498,7 +500,7 @@ module ActionDispatch env = rails_env 'PATH_INFO' => '/books/list.rss' expected = { :controller => 'books', :action => 'list', :format => 'rss' } called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal route, r assert_equal(expected, params) called = true @@ -519,7 +521,7 @@ module ActionDispatch "REQUEST_METHOD" => "HEAD" called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| called = true end @@ -543,7 +545,7 @@ module ActionDispatch "REQUEST_METHOD" => "POST" called = false - @router.recognize(env) do |r, _, params| + @router.recognize(env) do |r, params| assert_equal post, r called = true end @@ -562,8 +564,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/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/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 0c3ed9e8ca..4033eb5808 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -52,8 +52,6 @@ module ActiveModel raise end - attr_reader :password - include InstanceMethodsOnActivation if options.fetch(:validations, true) @@ -92,6 +90,8 @@ module ActiveModel BCrypt::Password.new(password_digest) == unencrypted_password && self end + attr_reader :password + # Encrypts the password into the +password_digest+ attribute, only if the # new password is not blank. # diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 55a3cbfd60..041872034f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,4 +1,35 @@ -* Fixed the inferred table name of a HABTM auxiliar table inside a schema. +* 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. + + *Kuldeep Aggarwal* + +* Fixed serialized fields returning serialized data after being updated with + `update_column`. + + *Simon Hørup Eskildsen* + +* Fixed polymorphic eager loading when using a String as foreign key. + + Fixes #14734. + + *Lauro Caetano* + +* Change belongs_to touch to be consistent with timestamp updates + + If a model is set up with a belongs_to: touch relatinoship 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 has_and_belongs_to_many auxiliar + table inside a schema. Fixes #14824 @@ -41,7 +72,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. @@ -94,7 +125,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. @@ -109,10 +140,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. @@ -156,10 +187,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. @@ -434,12 +465,6 @@ *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca* -* Save `has_one` association even if the record doesn't changed. - - Fixes #14407. - - *Rafael Mendonça França* - * Use singular table name in generated migrations when `ActiveRecord::Base.pluralize_table_names` is `false`. @@ -518,6 +543,12 @@ *Troy Kruthoff*, *Lachlan Sylvester* +* Only save has_one associations if record has changes. + Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one + object did not get saved to the db. + + *Alan Kennedy* + * Allow strings to specify the `#order` value. Example: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 727ee5f65f..8d77fad2d5 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -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 diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 47cc1f4b34..3998aca23e 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -103,7 +103,7 @@ module ActiveRecord::Associations::Builder BelongsTo.touch_record(record, foreign_key, n, touch) } - model.after_save callback + model.after_save callback, if: :changed? model.after_touch callback model.after_destroy callback end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 20bd4947dc..42571d6af0 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -112,13 +112,14 @@ module ActiveRecord end def preloaders_for_hash(association, records, scope) - parent, child = association.to_a.first # hash should only be of length 1 + association.flat_map { |parent, child| + loaders = preloaders_for_one parent, records, scope - loaders = preloaders_for_one parent, records, scope - - recs = loaders.flat_map(&:preloaded_records).uniq - loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } + loaders } end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index bf461070e0..63773bd5e1 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -57,9 +57,15 @@ module ActiveRecord end def owners_by_key - @owners_by_key ||= owners.group_by do |owner| - owner[owner_key_name] - end + @owners_by_key ||= if key_conversion_required? + owners.group_by do |owner| + owner[owner_key_name].to_s + end + else + owners.group_by do |owner| + owner[owner_key_name] + end + end end def options @@ -93,13 +99,28 @@ module ActiveRecord records_by_owner end + def key_conversion_required? + association_key_type != owner_key_type + end + + def association_key_type + @klass.column_types[association_key_name.to_s].type + end + + def owner_key_type + @model.column_types[owner_key_name.to_s].type + end + def load_slices(slices) @preloaded_records = slices.flat_map { |slice| records_for(slice) } @preloaded_records.map { |record| - [record, record[association_key_name]] + key = record[association_key_name] + key = key.to_s if key_conversion_required? + + [record, key] } end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 8bd51dc71f..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 @@ -200,6 +205,7 @@ module ActiveRecord # this is probably horribly slow, but should only happen at most once for a given AR class attribute_method.bind(self).call(*args, &block) else + return super unless respond_to_missing?(method, true) send(method, *args, &block) end else @@ -455,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 c3466153d6..53a9c874bf 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -142,6 +142,14 @@ module ActiveRecord end end + def raw_type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :serialized) + else + super + end + end + def _field_changed?(attr, old, value) if self.class.serialized_attributes.include?(attr) old != value diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c853fc0917..56441d7324 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -55,6 +55,27 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, :type_cast_attribute_for_write) + end + + def raw_write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, :raw_type_cast_attribute_for_write) + end + + private + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end + + def type_cast_attribute_for_write(column, value) + return value unless column + + column.type_cast_for_write value + end + alias_method :raw_type_cast_attribute_for_write, :type_cast_attribute_for_write + + def write_attribute_with_type_cast(attr_name, value, type_cast_method) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key @attributes_cache.delete(attr_name) @@ -67,24 +88,11 @@ module ActiveRecord end if column || @attributes.has_key?(attr_name) - @attributes[attr_name] = type_cast_attribute_for_write(column, value) + @attributes[attr_name] = send(type_cast_method, column, value) else raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end end - alias_method :raw_write_attribute, :write_attribute - - private - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) - end - - def type_cast_attribute_for_write(column, value) - return value unless column - - column.type_cast_for_write value - end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 80cf7572df..1a4d2957ec 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -381,15 +381,16 @@ module ActiveRecord def save_has_one_association(reflection) association = association_instance_get(reflection.name) record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? record.destroy - else + elsif autosave != false key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key 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 0c55dbb037..ca5db4095e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -374,26 +374,31 @@ module ActiveRecord end def initialize_type_map(m) # :nodoc: - m.register_type %r(boolean)i, Type::Boolean.new - m.register_type %r(char)i, Type::String.new - m.register_type %r(binary)i, Type::Binary.new - m.alias_type %r(blob)i, 'binary' - m.register_type %r(text)i, Type::Text.new - m.alias_type %r(clob)i, 'text' - m.register_type %r(date)i, Type::Date.new - m.register_type %r(time)i, Type::Time.new - m.alias_type %r(timestamp)i, 'datetime' - m.register_type %r(datetime)i, Type::DateTime.new - m.alias_type %r(numeric)i, 'decimal' - m.alias_type %r(number)i, 'decimal' - m.register_type %r(float)i, Type::Float.new - m.alias_type %r(double)i, 'float' - m.register_type %r(int)i, Type::Integer.new + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_limit m, %r(date)i, Type::Date + register_class_with_limit m, %r(time)i, Type::Time + register_class_with_limit m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer + + m.alias_type %r(blob)i, 'binary' + m.alias_type %r(clob)i, 'text' + m.alias_type %r(timestamp)i, 'datetime' + m.alias_type %r(numeric)i, 'decimal' + m.alias_type %r(number)i, 'decimal' + m.alias_type %r(double)i, 'float' + m.register_type(%r(decimal)i) do |sql_type| - if Type.extract_scale(sql_type) == 0 - Type::Integer.new + scale = extract_scale(sql_type) + precision = extract_precision(sql_type) + + if scale == 0 + Type::DecimalWithoutScale.new(precision: precision) else - Type::Decimal.new + Type::Decimal.new(precision: precision, scale: scale) end end end @@ -403,6 +408,28 @@ module ActiveRecord initialize_type_map(type_map) end + def register_class_with_limit(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + limit = extract_limit(args.last) + klass.new(limit: limit) + end + end + + def extract_scale(sql_type) # :nodoc: + case sql_type + when /\((\d+)\)/ then 0 + when /\((\d+)(,(\d+))\)/ then $3.to_i + end + end + + def extract_precision(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ + end + + def extract_limit(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((.*)\)/ + end + def translate_exception_class(e, sql) message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger 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 852b7105d3..d3f8470c30 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -86,44 +86,12 @@ module ActiveRecord sql_type =~ /blob/i || type == :text end - # Must return the relevant concrete adapter - def adapter - raise NotImplementedError - end - def case_sensitive? collation && !collation.match(/_ci$/) end private - def extract_limit(sql_type) - case sql_type - when /^enum\((.+)\)/i - $1.split(',').map{|enum| enum.strip.length - 2}.max - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - when /^float/i; 24 - when /^double/i; 53 - else - super - end - end - # MySQL misreports NOT NULL column default when none is given. # We can't detect this for columns which may have a legitimate '' # default (string) but we can for others (integer, datetime, boolean, @@ -249,10 +217,9 @@ module ActiveRecord raise NotImplementedError end - # Overridden by the adapters to instantiate their specific Column type. def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: cast_type = lookup_cast_type(sql_type) - Column.new(field, default, cast_type, sql_type, null, collation, extra) + Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end # Must return the Mysql error number from the exception, if the exception has an @@ -637,10 +604,29 @@ module ActiveRecord protected - def initialize_type_map(m) + def initialize_type_map(m) # :nodoc: super + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + Type::String.new(limit: limit) + end + + m.register_type %r(tinytext)i, Type::Text.new(limit: 255) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) + m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) + m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) + m.register_type %r(^int)i, Type::Integer.new(limit: 4) + m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) + m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) + m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) + m.register_type %r(^float)i, Type::Float.new(limit: 24) + m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans - m.alias_type %r(enum)i, 'varchar' m.alias_type %r(set)i, 'varchar' m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 25a9cdafcf..42aabd6d7f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,12 +13,12 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :default, :cast_type, :limit, :null, :sql_type, :precision, :scale, :default_function - attr_accessor :primary, :coder + attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function + attr_accessor :coder alias :encoded? :coder - delegate :type, to: :cast_type + delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, :type_cast_for_write, to: :cast_type # Instantiates a new column in the table. # @@ -34,88 +34,21 @@ module ActiveRecord @cast_type = cast_type @sql_type = sql_type @null = null - @limit = extract_limit(sql_type) - @precision = extract_precision(sql_type) - @scale = extract_scale(sql_type) @default = extract_default(default) @default_function = nil - @primary = nil @coder = nil end - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - def has_default? !default.nil? end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime, :time then Time - when :date then Date - when :text, :string, :binary then String - when :boolean then Object - end - end - - def binary? - type == :binary - end - - # Casts a Ruby value to something appropriate for writing to the database. - # Numeric columns will typecast boolean and string to appropriate numeric - # values. - def type_cast_for_write(value) - return value unless number? - - case value - when FalseClass - 0 - when TrueClass - 1 - when String - value.presence - else - value - end - end - # Casts value to an appropriate instance. def type_cast(value) - return nil if value.nil? - return coder.load(value) if encoded? - - klass = self.class - - case type - when :string, :text - case value - when TrueClass; "1" - when FalseClass; "0" - else - value.to_s - end - when :integer then klass.value_to_integer(value) - when :float then value.to_f - when :decimal then klass.value_to_decimal(value) - when :datetime then klass.string_to_time(value) - when :time then klass.string_to_dummy_time(value) - when :date then klass.value_to_date(value) - when :binary then klass.binary_to_string(value) - when :boolean then klass.value_to_boolean(value) - else value + if encoded? + coder.load(value) + else + cast_type.type_cast(value) end end @@ -130,142 +63,6 @@ module ActiveRecord def extract_default(default) type_cast(default) end - - class << self - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def value_to_date(value) - if value.is_a?(String) - return nil if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - dummy_time_string = "2000-01-01 #{string}" - - fast_string_to_time(dummy_time_string) || begin - time_hash = Date._parse(dummy_time_string) - return nil if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.empty? - nil - else - TRUE_VALUES.include?(value) - end - end - - # Used to convert values to integer. - # handle the case when an integer column is used to store boolean values - def value_to_integer(value) - case value - when TrueClass, FalseClass - value ? 1 : 0 - else - value.to_i rescue nil - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return nil unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end - end - - private - delegate :extract_scale, to: Type - - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end end end # :startdoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 2e39168374..0a14cdfe89 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,13 +29,6 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column # :nodoc: - def adapter - Mysql2Adapter - end - end - ADAPTER_NAME = 'Mysql2' def initialize(connection, logger, connection_options, config) @@ -69,11 +62,6 @@ module ActiveRecord end end - def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: - cast_type = lookup_cast_type(sql_type) - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) - end - def error_number(exception) exception.error_number if exception.respond_to?(:error_number) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index bf09bfe217..aa8a91ed39 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -66,35 +66,6 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column #:nodoc: - def self.string_to_time(value) - return super unless Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - end - - def self.string_to_dummy_time(v) - return super unless Mysql::Time === v - new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) - end - - def self.string_to_date(v) - return super unless Mysql::Time === v - new_date(v.year, v.month, v.day) - end - - def adapter - MysqlAdapter - end - end - ADAPTER_NAME = 'MySQL' class StatementPool < ConnectionAdapters::StatementPool @@ -156,11 +127,6 @@ module ActiveRecord end end - def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: - cast_type = lookup_cast_type(sql_type) - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) - end - def error_number(exception) # :nodoc: exception.errno if exception.respond_to?(:errno) end @@ -296,126 +262,70 @@ module ActiveRecord @connection.insert_id end - module Fields - class Type - def type; end - - def type_cast_for_write(value) - value + module Fields # :nodoc: + class DateTime < Type::DateTime + def cast_value(value) + if Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Identity < Type - def type_cast(value); value; end - end - - class Integer < Type - def type_cast(value) - return if value.nil? - - value.to_i rescue value ? 1 : 0 - end - end - - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class DateTime < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_time value + class Time < Type::Time + def cast_value(value) + if Mysql::Time === value + new_time( + 2000, + 01, + 01, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Time < Type - def type; :time; end + class << self + TYPES = ConnectionAdapters::Type::HashLookupTypeMap.new # :nodoc: - def type_cast(value) - return if value.nil? + delegate :register_type, :alias_type, to: :TYPES - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_dummy_time value + def find_type(field) + if field.type == Mysql::Field::TYPE_TINY && field.length > 1 + TYPES.lookup(Mysql::Field::TYPE_LONG) + else + TYPES.lookup(field.type) + end end end - class Float < Type - def type; :float; end - - def type_cast(value) - return if value.nil? - - value.to_f - end - end - - class Decimal < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Boolean < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_boolean value - end - end - - TYPES = {} - - # Register an MySQL +type_id+ with a typecasting object in - # +type+. - def self.register_type(type_id, type) - TYPES[type_id] = type - end - - def self.alias_type(new, old) - TYPES[new] = TYPES[old] - end - - def self.find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES[Mysql::Field::TYPE_LONG] - else - TYPES.fetch(field.type) { Fields::Identity.new } - end - end - - register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new - register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + register_type Mysql::Field::TYPE_TINY, Type::Boolean.new + register_type Mysql::Field::TYPE_LONG, Type::Integer.new alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new - register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new - register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATE, Type::Date.new register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + register_type Mysql::Field::TYPE_FLOAT, Type::Float.new + end - Mysql::Field.constants.grep(/TYPE/).map { |class_name| - Mysql::Field.const_get class_name - }.reject { |const| TYPES.key? const }.each do |const| - register_type const, Fields::Identity.new - end + def initialize_type_map(m) # :nodoc: + super + m.register_type %r(datetime)i, Fields::DateTime.new + m.register_type %r(time)i, Fields::Time.new end def exec_without_stmt(sql, name = 'SQL') # :nodoc: @@ -433,7 +343,7 @@ module ActiveRecord fields << field_name if field.decimals > 0 - types[field_name] = Fields::Decimal.new + types[field_name] = Type::Decimal.new else types[field_name] = Fields.find_type field end @@ -449,7 +359,7 @@ module ActiveRecord end end - def execute_and_free(sql, name = nil) + def execute_and_free(sql, name = nil) # :nodoc: result = execute(sql, name) ret = yield result result.free @@ -462,7 +372,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_delete(sql, name, binds) + def exec_delete(sql, name, binds) # :nodoc: affected_rows = 0 exec_query(sql, name, binds) do |n| diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index a14381acb6..f7bad20f00 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -6,26 +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_time(string) # :nodoc: - return string unless String === string - - case string - when 'infinity'; Float::INFINITY - when '-infinity'; -Float::INFINITY - when / BC$/ - super("-" + string.sub(/ BC$/, "")) - else - super - end - 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 1dd8acc257..9a5e2d05ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -6,27 +6,16 @@ module ActiveRecord class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - def initialize(name, default, oid_type, sql_type = nil, null = true) - @oid_type = oid_type - 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, oid_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, oid_type, sql_type, null) + super(name, default, cast_type, sql_type, null) end - @default_function = default if has_default_function?(default_value, default) - end - - def number? - !array && super - end - - def text? - !array && super + @default_function = default_function end # :stopdoc: @@ -44,127 +33,12 @@ module ActiveRecord require 'active_record/connection_adapters/postgresql/array_parser' include PostgreSQL::ArrayParser end - - attr_accessor :money_precision 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 - - # Casts a Ruby value to something appropriate for writing to PostgreSQL. - # see ActiveRecord::ConnectionAdapters::Class#type_cast_for_write - # see ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Type - def type_cast_for_write(value) - if @oid_type.respond_to?(:type_cast_for_write) - @oid_type.type_cast_for_write(value) - else - super - end - end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @oid_type.type_cast value - end - def accessor - @oid_type.accessor + cast_type.accessor end - - private - - def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)} === default) - end - - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - super - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 90bf6c6d1a..2494e19f84 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,509 +1,32 @@ +require 'active_record/connection_adapters/postgresql/oid/infinity' + +require 'active_record/connection_adapters/postgresql/oid/array' +require 'active_record/connection_adapters/postgresql/oid/bit' +require 'active_record/connection_adapters/postgresql/oid/bytea' +require 'active_record/connection_adapters/postgresql/oid/cidr' +require 'active_record/connection_adapters/postgresql/oid/date' +require 'active_record/connection_adapters/postgresql/oid/date_time' +require 'active_record/connection_adapters/postgresql/oid/decimal' +require 'active_record/connection_adapters/postgresql/oid/enum' +require 'active_record/connection_adapters/postgresql/oid/float' +require 'active_record/connection_adapters/postgresql/oid/hstore' +require 'active_record/connection_adapters/postgresql/oid/inet' +require 'active_record/connection_adapters/postgresql/oid/integer' +require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/money' +require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/range' +require 'active_record/connection_adapters/postgresql/oid/specialized_string' +require 'active_record/connection_adapters/postgresql/oid/time' +require 'active_record/connection_adapters/postgresql/oid/uuid' +require 'active_record/connection_adapters/postgresql/oid/vector' + +require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' + module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Type < Type::Value - def infinity(options = {}) - ::Float::INFINITY * (options[:negative] ? -1 : 1) - end - end - - class Identity < Type - def type_cast(value) - value - end - end - - class String < Type - def type; :string end - - def type_cast(value) - return if value.nil? - - value.to_s - end - end - - class SpecializedString < OID::String - def type; @type end - - def initialize(type) - @type = type - end - end - - class Text < OID::String - def type; :text end - end - - class Bit < Type - def type; :string end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_bit value - else - value - end - end - end - - class Bytea < Type - def type; :binary end - - def type_cast(value) - return if value.nil? - PGconn.unescape_bytea value - end - end - - class Money < Type - def type; :decimal end - - def type_cast(value) - return if value.nil? - return value unless ::String === value - - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - # Negative values are represented as follows: - # (3) -$2.55 - # (4) ($2.55) - - value.sub!(/^\((.+)\)$/, '-\1') # (4) - case value - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - value.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Vector < Type - attr_reader :delim, :subtype - - # +delim+ corresponds to the `typdelim` column in the pg_types - # table. +subtype+ is derived from the `typelem` column in the - # pg_types table. - def initialize(delim, subtype) - @delim = delim - @subtype = subtype - end - - # FIXME: this should probably split on +delim+ and use +subtype+ - # to cast the values. Unfortunately, the current Rails behavior - # is to just return the string. - def type_cast(value) - value - end - end - - class Point < Type - def type; :string end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_point value - else - value - end - end - end - - class Array < Type - def type; @subtype.type end - - attr_reader :subtype - def initialize(subtype) - @subtype = subtype - end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype - else - value - end - end - end - - class Range < Type - attr_reader :subtype, :type - - def initialize(subtype, type) - @subtype = subtype - @type = type - end - - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast(value) - end - - def type_cast(value) - return if value.nil? || value == 'empty' - return value if value.is_a?(::Range) - - extracted = extract_bounds(value) - from = type_cast_single extracted[:from] - to = type_cast_single extracted[:to] - - if !infinity?(from) && extracted[:exclude_start] - if from.respond_to?(:succ) - from = from.succ - ActiveSupport::Deprecation.warn <<-MESSAGE -Excluding the beginning of a Range is only partialy supported through `#succ`. -This is not reliable and will be removed in the future. - MESSAGE - else - raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" - end - end - ::Range.new(from, to, extracted[:exclude_end]) - end - end - - class Integer < Type - def type; :integer end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_integer value - end - end - - class Boolean < Type - def type; :boolean end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_boolean value - end - end - - class DateTime < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::PostgreSQLColumn.string_to_time value - end - end - - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class Time < Type - def type; :time end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end - end - - class Float < Type - def type; :float end - - def type_cast(value) - case value - when nil; nil - when 'Infinity'; ::Float::INFINITY - when '-Infinity'; -::Float::INFINITY - when 'NaN'; ::Float::NAN - else - value.to_f - end - end - end - - class Decimal < Type - def type; :decimal end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value - end - - def infinity(options = {}) - BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) - end - end - - class Enum < Type - def type; :enum end - - def type_cast(value) - value.to_s - end - end - - class Hstore < Type - def type; :hstore end - - def type_cast_for_write(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string value - end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_hstore value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class Cidr < Type - def type; :cidr end - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_cidr value - end - end - class Inet < Cidr - def type; :inet end - end - - class Json < Type - def type; :json end - - def type_cast_for_write(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string value - end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_json value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class Uuid < Type - def type; :uuid end - def type_cast(value) - value.presence - end - end - - class TypeMap - def initialize - @mapping = {} - end - - def []=(oid, type) - @mapping[oid] = type - end - - def [](oid) - @mapping[oid] - end - - def clear - @mapping.clear - end - - def key?(oid) - @mapping.key? oid - end - - def fetch(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? - ftype = 23 - end - - @mapping.fetch(ftype) { |oid| yield oid, fmod } - end - end - - # This class uses the data from PostgreSQL pg_type table to build - # the OID -> Type mapping. - # - OID is and integer representing the type. - # - Type is an OID::Type object. - # This class has side effects on the +store+ passed during initialization. - class TypeMapInitializer # :nodoc: - def initialize(store) - @store = store - end - - def run(records) - mapped, nodes = records.partition { |row| OID.registered_type? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - composites, nodes = nodes.partition { |row| row['typelem'] != '0' } - - mapped.each { |row| register_mapped_type(row) } - enums.each { |row| register_enum_type(row) } - domains.each { |row| register_domain_type(row) } - arrays.each { |row| register_array_type(row) } - ranges.each { |row| register_range_type(row) } - composites.each { |row| register_composite_type(row) } - end - - private - def register_mapped_type(row) - register row['oid'], OID::NAMES[row['typname']] - end - - def register_enum_type(row) - register row['oid'], OID::Enum.new - end - - def register_array_type(row) - if subtype = @store[row['typelem'].to_i] - register row['oid'], OID::Array.new(subtype) - end - end - - def register_range_type(row) - if subtype = @store[row['rngsubtype'].to_i] - register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) - end - end - - def register_domain_type(row) - if base_type = @store[row["typbasetype"].to_i] - register row['oid'], base_type - else - warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." - end - end - - def register_composite_type(row) - if subtype = @store[row['typelem'].to_i] - register row['oid'], OID::Vector.new(row['typdelim'], subtype) - end - end - - def register(oid, oid_type) - oid = oid.to_i - - raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? - return if @store.key?(oid) - - @store[oid] = oid_type - end - end - - # When the PG adapter connects, the pg_type table is queried. The - # key of this hash maps to the `typname` column from the table. - # type_map is then dynamically built with oids as the key and type - # objects as values. - NAMES = Hash.new { |h,k| # :nodoc: - h[k] = OID::Identity.new - } - - # Register an OID type named +name+ with a typecasting object in - # +type+. +name+ should correspond to the `typname` column in - # the `pg_type` table. - def self.register_type(name, type) - NAMES[name] = type - end - - # Alias the +old+ type to the +new+ type. - def self.alias_type(new, old) - NAMES[new] = NAMES[old] - end - - # Is +name+ a registered type? - def self.registered_type?(name) - NAMES.key? name - end - - register_type 'int2', OID::Integer.new - alias_type 'int4', 'int2' - alias_type 'int8', 'int2' - alias_type 'oid', 'int2' - register_type 'numeric', OID::Decimal.new - register_type 'float4', OID::Float.new - alias_type 'float8', 'float4' - register_type 'text', OID::Text.new - register_type 'varchar', OID::String.new - alias_type 'char', 'varchar' - alias_type 'name', 'varchar' - alias_type 'bpchar', 'varchar' - register_type 'bool', OID::Boolean.new - register_type 'bit', OID::Bit.new - alias_type 'varbit', 'bit' - register_type 'timestamp', OID::DateTime.new - alias_type 'timestamptz', 'timestamp' - register_type 'date', OID::Date.new - register_type 'time', OID::Time.new - - register_type 'money', OID::Money.new - register_type 'bytea', OID::Bytea.new - register_type 'point', OID::Point.new - register_type 'hstore', OID::Hstore.new - register_type 'json', OID::Json.new - register_type 'cidr', OID::Cidr.new - register_type 'inet', OID::Inet.new - register_type 'uuid', OID::Uuid.new - register_type 'xml', SpecializedString.new(:xml) - register_type 'tsvector', SpecializedString.new(:tsvector) - register_type 'macaddr', SpecializedString.new(:macaddr) - register_type 'citext', SpecializedString.new(:citext) - register_type 'ltree', SpecializedString.new(:ltree) - - # FIXME: why are we keeping these types as strings? - alias_type 'interval', 'varchar' - alias_type 'path', 'varchar' - alias_type 'line', 'varchar' - alias_type 'polygon', 'varchar' - alias_type 'circle', 'varchar' - alias_type 'lseg', 'varchar' - alias_type 'box', 'varchar' end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb new file mode 100644 index 0000000000..0e9dcd8c0c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Array < Type::Value + attr_reader :subtype + delegate :type, to: :subtype + + def initialize(subtype) + @subtype = subtype + end + + def type_cast(value) + if ::String === value + ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb new file mode 100644 index 0000000000..9b2d887d07 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bit < Type::String + def type_cast(value) + if ::String === value + ConnectionAdapters::PostgreSQLColumn.string_to_bit value + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb new file mode 100644 index 0000000000..36c53d8732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bytea < Type::Binary + def cast_value(value) + PGconn.unescape_bytea value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb new file mode 100644 index 0000000000..507c3a62b0 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Cidr < Type::Value + def type + :cidr + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb new file mode 100644 index 0000000000..3c30ad5fec --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb new file mode 100644 index 0000000000..9ccbf71159 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class DateTime < Type::DateTime + include Infinity + + def cast_value(value) + if value.is_a?(::String) + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + super("-" + value.sub(/ BC$/, "")) + else + super + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb new file mode 100644 index 0000000000..ed4b8911d9 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Decimal < Type::Decimal + def infinity(options = {}) + BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb new file mode 100644 index 0000000000..5fed8b0f89 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Enum < Type::Value + def type + :enum + end + + def type_cast(value) + value.to_s + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb new file mode 100644 index 0000000000..9753d71461 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Float < Type::Float + include Infinity + + def type_cast(value) + case value + when nil then nil + when 'Infinity' then ::Float::INFINITY + when '-Infinity' then -::Float::INFINITY + when 'NaN' then ::Float::NAN + else value.to_f + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb new file mode 100644 index 0000000000..98f369a7f8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -0,0 +1,25 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Hstore < Type::Value + def type + :hstore + end + + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string value + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb new file mode 100644 index 0000000000..7ed8f5f031 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Inet < Cidr + def type + :inet + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb new file mode 100644 index 0000000000..d438ffa140 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + module Infinity + def infinity(options = {}) + options[:negative] ? -::Float::INFINITY : ::Float::INFINITY + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb new file mode 100644 index 0000000000..388d3dd9ed --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Integer < Type::Integer + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb new file mode 100644 index 0000000000..42bf5656f4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -0,0 +1,25 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Json < Type::Value + def type + :json + end + + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string value + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb new file mode 100644 index 0000000000..697dceb7c2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -0,0 +1,39 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Money < Type::Decimal + include Infinity + + class_attribute :precision + + def scale + 2 + end + + def cast_value(value) + return value unless ::String === value + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + + value.sub!(/^\((.+)\)$/, '-\1') # (4) + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + super(value) + end + end + 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 new file mode 100644 index 0000000000..f9531ddee3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Point < Type::String + def type_cast(value) + if ::String === value + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + value.split(',').map{ |v| Float(v) } + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb new file mode 100644 index 0000000000..c2262c1599 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Range < Type::Value + attr_reader :subtype, :type + + def initialize(subtype, type) + @subtype = subtype + @type = type + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast(value) + end + + def cast_value(value) + return if value == 'empty' + return value if value.is_a?(::Range) + + extracted = extract_bounds(value) + from = type_cast_single extracted[:from] + to = type_cast_single extracted[:to] + + if !infinity?(from) && extracted[:exclude_start] + if from.respond_to?(:succ) + from = from.succ + ActiveSupport::Deprecation.warn <<-MESSAGE +Excluding the beginning of a Range is only partialy supported through `#succ`. +This is not reliable and will be removed in the future. + MESSAGE + else + raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" + end + end + ::Range.new(from, to, extracted[:exclude_end]) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb new file mode 100644 index 0000000000..7b1ca16bc4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class SpecializedString < Type::String + attr_reader :type + + def initialize(type) + @type = type + end + + def text? + false + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb new file mode 100644 index 0000000000..ea1f599b0f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Time < Type::Time + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb new file mode 100644 index 0000000000..28f7a4eafb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -0,0 +1,85 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + # This class uses the data from PostgreSQL pg_type table to build + # the OID -> Type mapping. + # - OID is and integer representing the type. + # - Type is an OID::Type object. + # This class has side effects on the +store+ passed during initialization. + class TypeMapInitializer # :nodoc: + def initialize(store) + @store = store + end + + def run(records) + nodes = records.reject { |row| @store.key? row['oid'].to_i } + mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + + mapped.each { |row| register_mapped_type(row) } + enums.each { |row| register_enum_type(row) } + domains.each { |row| register_domain_type(row) } + arrays.each { |row| register_array_type(row) } + ranges.each { |row| register_range_type(row) } + composites.each { |row| register_composite_type(row) } + end + + private + def register_mapped_type(row) + alias_type row['oid'], row['typname'] + end + + def register_enum_type(row) + register row['oid'], OID::Enum.new + end + + def register_array_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Array.new(subtype) + end + end + + def register_range_type(row) + if subtype = @store.lookup(row['rngsubtype'].to_i) + register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) + end + end + + def register_domain_type(row) + if base_type = @store.lookup(row["typbasetype"].to_i) + register row['oid'], base_type + else + warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." + end + end + + def register_composite_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Vector.new(row['typdelim'], subtype) + end + end + + def register(oid, oid_type) + oid = assert_valid_registration(oid, oid_type) + @store.register_type(oid, oid_type) + end + + def alias_type(oid, target) + oid = assert_valid_registration(oid, target) + @store.alias_type(oid, target) + end + + def assert_valid_registration(oid, oid_type) + raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? + oid.to_i + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb new file mode 100644 index 0000000000..0ed5491887 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Uuid < Type::Value + def type + :uuid + end + + def type_cast(value) + value.presence + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb new file mode 100644 index 0000000000..2f7d1be197 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Vector < Type::Value + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + def type_cast(value) + value + end + end + 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 539ba38c4a..9e53d10bb4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -178,8 +178,10 @@ module ActiveRecord def columns(table_name) # 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) - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) + 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 39fa75518b..eacb26a254 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -369,7 +369,7 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end - @type_map = OID::TypeMap.new + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true @@ -538,18 +538,167 @@ module ActiveRecord private - def get_oid_type(oid, fmod, column_name) + def get_oid_type(oid, fmod, column_name, sql_type = '') if !type_map.key?(oid) - initialize_type_map(type_map, [oid]) + load_additional_types(type_map, [oid]) end - type_map.fetch(oid, fmod) { + 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_map[oid] = OID::Identity.new + Type::Value.new.tap do |cast_type| + type_map.register_type(oid, cast_type) + end } end - def initialize_type_map(type_map, oids = nil) + def initialize_type_map(m) + register_class_with_limit m, 'int2', OID::Integer + m.alias_type 'int4', 'int2' + m.alias_type 'int8', 'int2' + m.alias_type 'oid', 'int2' + m.register_type 'float4', OID::Float.new + m.alias_type 'float8', 'float4' + m.register_type 'text', Type::Text.new + register_class_with_limit m, 'varchar', Type::String + m.alias_type 'char', 'varchar' + m.alias_type 'name', 'varchar' + m.alias_type 'bpchar', 'varchar' + m.register_type 'bool', Type::Boolean.new + m.register_type 'bit', OID::Bit.new + m.alias_type 'varbit', 'bit' + m.alias_type 'timestamptz', 'timestamp' + m.register_type 'date', OID::Date.new + m.register_type 'time', OID::Time.new + + m.register_type 'money', OID::Money.new + m.register_type 'bytea', OID::Bytea.new + m.register_type 'point', OID::Point.new + m.register_type 'hstore', OID::Hstore.new + m.register_type 'json', OID::Json.new + m.register_type 'cidr', OID::Cidr.new + m.register_type 'inet', OID::Inet.new + m.register_type 'uuid', OID::Uuid.new + m.register_type 'xml', OID::SpecializedString.new(:xml) + m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) + m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) + m.register_type 'citext', OID::SpecializedString.new(:citext) + m.register_type 'ltree', OID::SpecializedString.new(:ltree) + + # FIXME: why are we keeping these types as strings? + m.alias_type 'interval', 'varchar' + m.alias_type 'path', 'varchar' + m.alias_type 'line', 'varchar' + m.alias_type 'polygon', 'varchar' + m.alias_type 'circle', 'varchar' + m.alias_type 'lseg', 'varchar' + m.alias_type 'box', 'varchar' + + m.register_type 'timestamp' do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::DateTime.new(precision: precision) + end + + m.register_type 'numeric' do |_, fmod, sql_type| + precision = extract_precision(sql_type) + scale = extract_scale(sql_type) + + # 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? + Type::DecimalWithoutScale.new(precision: precision) + else + OID::Decimal.new(precision: precision, scale: scale) + end + end + + load_additional_types(m) + end + + def extract_limit(sql_type) # :nodoc: + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + else super + end + end + + # Extracts the value from a PostgreSQL column default definition. + def 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 extract_default_function(default_value, default) + default if has_default_function?(default_value, default) + end + + def has_default_function?(default_value, default) + !default_value && (%r{\w+\(.*\)} === default) + end + + def load_additional_types(type_map, oids = nil) if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -649,7 +798,7 @@ module ActiveRecord # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision # should know about this but can't detect it there, so deal with it here. - PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 + OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 configure_connection rescue ::PG::Error => error 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/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 03ff0d4ead..a5e2619cb8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -41,14 +41,12 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Column < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value + class SQLite3Binary < Type::Binary # :nodoc: + def cast_value(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) end + value end end @@ -395,7 +393,7 @@ module ActiveRecord sql_type = field['type'] cast_type = lookup_cast_type(sql_type) - SQLite3Column.new(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) + Column.new(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -502,6 +500,12 @@ module ActiveRecord end protected + + def initialize_type_map(m) + super + m.register_type(/binary/i, SQLite3Binary.new) + end + def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) end diff --git a/activerecord/lib/active_record/connection_adapters/type.rb b/activerecord/lib/active_record/connection_adapters/type.rb index 0268e0d569..bab7a3ff7e 100644 --- a/activerecord/lib/active_record/connection_adapters/type.rb +++ b/activerecord/lib/active_record/connection_adapters/type.rb @@ -1,27 +1,25 @@ +require 'active_record/connection_adapters/type/numeric' +require 'active_record/connection_adapters/type/time_value' require 'active_record/connection_adapters/type/value' + require 'active_record/connection_adapters/type/binary' 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' require 'active_record/connection_adapters/type/text' require 'active_record/connection_adapters/type/time' + require 'active_record/connection_adapters/type/type_map' +require 'active_record/connection_adapters/type/hash_lookup_type_map' module ActiveRecord module ConnectionAdapters module Type # :nodoc: - class << self - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb index 168d824d3d..4b2d1a66e0 100644 --- a/activerecord/lib/active_record/connection_adapters/type/binary.rb +++ b/activerecord/lib/active_record/connection_adapters/type/binary.rb @@ -5,6 +5,14 @@ module ActiveRecord def type :binary end + + def binary? + true + end + + def klass + ::String + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb index 938d227632..2337bdd563 100644 --- a/activerecord/lib/active_record/connection_adapters/type/boolean.rb +++ b/activerecord/lib/active_record/connection_adapters/type/boolean.rb @@ -5,6 +5,16 @@ module ActiveRecord def type :boolean end + + private + + def cast_value(value) + if value == '' + nil + else + Column::TRUE_VALUES.include?(value) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb index 1632f3c8f4..1e7205fd0b 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date.rb @@ -5,6 +5,39 @@ module ActiveRecord def type :date end + + def klass + ::Date + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + def fast_string_to_date(string) + if string =~ Column::Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end end end 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 1485fd3d80..c34f4c5a53 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date_time.rb @@ -2,9 +2,33 @@ module ActiveRecord module ConnectionAdapters module Type class DateTime < Value # :nodoc: + include TimeValue + def type :datetime end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb index 5b39ea9e2f..ac5af4b963 100644 --- a/activerecord/lib/active_record/connection_adapters/type/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/type/decimal.rb @@ -2,9 +2,25 @@ module ActiveRecord module ConnectionAdapters module Type class Decimal < Value # :nodoc: + include Numeric + def type :decimal end + + def klass + ::BigDecimal + end + + private + + def cast_value(value) + if value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end end end end 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 089169e7c9..51cfa5d86a 100644 --- a/activerecord/lib/active_record/connection_adapters/type/float.rb +++ b/activerecord/lib/active_record/connection_adapters/type/float.rb @@ -2,9 +2,21 @@ module ActiveRecord module ConnectionAdapters module Type class Float < Value # :nodoc: + include Numeric + def type :float end + + def klass + ::Float + end + + private + + def cast_value(value) + value.to_f + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..bb1abc77ff --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type, *args) + @mapping.fetch(type, proc { default_value }).call(type, *args) + end + + def fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end + + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb index 5510a11bd4..8f3469434c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/type/integer.rb @@ -2,9 +2,25 @@ module ActiveRecord module ConnectionAdapters module Type class Integer < Value # :nodoc: + include Numeric + def type :integer end + + def klass + ::Fixnum + end + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else value.to_i rescue nil + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/numeric.rb b/activerecord/lib/active_record/connection_adapters/type/numeric.rb new file mode 100644 index 0000000000..a3379831cb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/numeric.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast_for_write(value) + case value + when true then 1 + when false then 0 + when ::String then value.presence + else super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb index 0feb4299f5..55f0e1ee1c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/string.rb +++ b/activerecord/lib/active_record/connection_adapters/type/string.rb @@ -5,6 +5,24 @@ module ActiveRecord def type :string end + + def text? + true + end + + def klass + ::String + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else value.to_s + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb index a3a687a8ad..4dd201e3fe 100644 --- a/activerecord/lib/active_record/connection_adapters/type/time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/time.rb @@ -2,9 +2,26 @@ module ActiveRecord module ConnectionAdapters module Type class Time < Value # :nodoc: + include TimeValue + def type :time end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/time_value.rb b/activerecord/lib/active_record/connection_adapters/type/time_value.rb new file mode 100644 index 0000000000..e9ca4adeda --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/time_value.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module ConnectionAdapters + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/type_map.rb b/activerecord/lib/active_record/connection_adapters/type/type_map.rb index d89171a820..48b8b51417 100644 --- a/activerecord/lib/active_record/connection_adapters/type/type_map.rb +++ b/activerecord/lib/active_record/connection_adapters/type/type_map.rb @@ -6,13 +6,13 @@ module ActiveRecord @mapping = {} end - def lookup(lookup_key) + def lookup(lookup_key, *args) matching_pair = @mapping.reverse_each.detect do |key, _| key === lookup_key end if matching_pair - matching_pair.last.call(lookup_key) + matching_pair.last.call(lookup_key, *args) else default_value end @@ -29,9 +29,9 @@ module ActiveRecord end def alias_type(key, target_key) - register_type(key) do |sql_type| + register_type(key) do |sql_type, *args| metadata = sql_type[/\(.*\)/, 0] - lookup("#{target_key}#{metadata}") + lookup("#{target_key}#{metadata}", *args) end end diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb index f7d7b9351b..54a3e9dd7a 100644 --- a/activerecord/lib/active_record/connection_adapters/type/value.rb +++ b/activerecord/lib/active_record/connection_adapters/type/value.rb @@ -2,7 +2,46 @@ module ActiveRecord module ConnectionAdapters module Type class Value # :nodoc: + attr_reader :precision, :scale, :limit + + def initialize(options = {}) + options.assert_valid_keys(:precision, :scale, :limit) + @precision = options[:precision] + @scale = options[:scale] + @limit = options[:limit] + end + def type; end + + def type_cast(value) + cast_value(value) unless value.nil? + end + + def type_cast_for_write(value) + value + end + + def text? + false + end + + def number? + false + end + + def binary? + false + end + + def klass + ::Object + end + + private + + def cast_value(value) + value + end end 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/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index aa1166750f..8449fb1266 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -219,16 +219,12 @@ module ActiveRecord # Returns an array of column objects for the table associated with this class. def columns - @columns ||= connection.schema_cache.columns(table_name).map do |col| - col = col.dup - col.primary = (col.name == primary_key) - col - end + connection.schema_cache.columns(table_name) 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] }] + connection.schema_cache.columns_hash(table_name) end def column_types # :nodoc: @@ -271,7 +267,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 +304,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 e6195e48a5..29ed499b1b 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -516,7 +516,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) + ConnectionAdapters::Type::Boolean.new.type_cast(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 05d0c41678..807c301596 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -23,7 +23,7 @@ module ActiveRecord end def size - 0 + calculate :size, nil end def empty? @@ -47,14 +47,28 @@ module ActiveRecord end def sum(*) - 0 + calculate :sum, nil + end + + def average(*) + calculate :average, nil + end + + def minimum(*) + calculate :minimum, nil + end + + def maximum(*) + calculate :maximum, nil end def calculate(operation, _column_name, _options = {}) # TODO: Remove _options argument as soon we remove support to # activerecord-deprecated_finders. - if operation == :count + if [:count, :sum, :size].include? operation group_values.any? ? Hash.new : 0 + elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? + Hash.new else nil end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 416f2305d2..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. @@ -950,7 +946,6 @@ WARNING [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) - attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) bv_len = bind_values.length tmp_opts, bind_values = create_binds(opts, bv_len) diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index c20030ca64..34c2008ab4 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -89,16 +89,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end def test_type_cast_array - data = '{1,2,3}' - oid_type = @column.instance_variable_get('@oid_type').subtype - # we are getting the instance variable in this test, but in the - # normal use of string_to_array, it's called from the OID::Array - # class and will have the OID instance that will provide the type - # casting - array = @column.class.string_to_array data, oid_type - assert_equal(['1', '2', '3'], array) - assert_equal(['1', '2', '3'], @column.type_cast(data)) - + assert_equal(['1', '2', '3'], @column.type_cast('{1,2,3}')) assert_equal([], @column.type_cast('{}')) assert_equal([nil], @column.type_cast('{NULL}')) end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 1e7071c136..1f55cce352 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -83,7 +83,7 @@ end class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase include PostgresqlCompositeBehavior - class FullAddressType + class FullAddressType < ActiveRecord::ConnectionAdapters::Type::Value def type; :full_address end def type_cast(value) @@ -102,15 +102,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase def setup super - @registration = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID - @registration.register_type "full_address", FullAddressType.new - end - - def teardown - super - - # there is currently no clean way to unregister a OID::Type - @registration::NAMES.delete("full_address") + @connection.type_map.register_type "full_address", FullAddressType.new end def test_column diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index ea433d391f..0dad89c67a 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -1,14 +1,6 @@ require "cases/helper" require 'support/ddl_helper' -class PostgresqlArray < ActiveRecord::Base -end - -class PostgresqlTsvector < ActiveRecord::Base -end - -class PostgresqlMoney < ActiveRecord::Base -end class PostgresqlNumber < ActiveRecord::Base end @@ -16,18 +8,12 @@ end class PostgresqlTime < ActiveRecord::Base end -class PostgresqlNetworkAddress < ActiveRecord::Base -end - class PostgresqlBitString < ActiveRecord::Base end class PostgresqlOid < ActiveRecord::Base end -class PostgresqlTimestampWithZone < ActiveRecord::Base -end - class PostgresqlLtree < ActiveRecord::Base end @@ -36,16 +22,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.execute("set lc_monetary = 'C'") - - @connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')") - - @first_tsvector = PostgresqlTsvector.find(1) - - @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") - @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") - @first_money = PostgresqlMoney.find(1) - @second_money = PostgresqlMoney.find(2) @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") @@ -57,29 +33,15 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) - @connection.execute("INSERT INTO postgresql_network_addresses (id, cidr_address, inet_address, mac_address) VALUES(1, '192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')") - @first_network_address = PostgresqlNetworkAddress.find(1) - @connection.execute("INSERT INTO postgresql_bit_strings (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')") @first_bit_string = PostgresqlBitString.find(1) @connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)") @first_oid = PostgresqlOid.find(1) - - @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") end teardown do - [PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress, - PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone].each(&:delete_all) - end - - def test_data_type_of_tsvector_types - assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type - end - - def test_data_type_of_money_types - assert_equal :decimal, @first_money.column_for_attribute(:wealth).type + [PostgresqlNumber, PostgresqlTime, PostgresqlBitString, PostgresqlOid].each(&:delete_all) end def test_data_type_of_number_types @@ -92,12 +54,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type end - def test_data_type_of_network_address_types - assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type - assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type - assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type - end - def test_data_type_of_bit_string_types assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type @@ -107,34 +63,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end - def test_tsvector_values - assert_equal "'text' 'vector'", @first_tsvector.text_vector - end - - def test_money_values - assert_equal 567.89, @first_money.wealth - assert_equal(-567.89, @second_money.wealth) - end - - def test_money_type_cast - column = PostgresqlMoney.columns_hash['wealth'] - assert_equal(12345678.12, column.type_cast("$12,345,678.12")) - assert_equal(12345678.12, column.type_cast("$12.345.678,12")) - assert_equal(-1.15, column.type_cast("-$1.15")) - assert_equal(-2.25, column.type_cast("($2.25)")) - end - - def test_update_tsvector - new_text_vector = "'new' 'text' 'vector'" - @first_tsvector.text_vector = new_text_vector - assert @first_tsvector.save - assert @first_tsvector.reload - @first_tsvector.text_vector = new_text_vector - assert @first_tsvector.save - assert @first_tsvector.reload - assert_equal new_text_vector, @first_tsvector.text_vector - end - def test_number_values assert_equal 123.456, @first_number.single assert_equal 123456.789, @first_number.double @@ -148,15 +76,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-21 days', @first_time.scaled_time_interval end - def test_network_address_values_ipaddr - cidr_address = IPAddr.new '192.168.0.0/24' - inet_address = IPAddr.new '172.16.1.254' - - assert_equal cidr_address, @first_network_address.cidr_address - assert_equal inet_address, @first_network_address.inet_address - assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address - end - def test_bit_string_values assert_equal '00010101', @first_bit_string.bit_string assert_equal '00010101', @first_bit_string.bit_string_varying @@ -166,14 +85,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal 1234, @first_oid.obj_id end - def test_update_money - new_value = BigDecimal.new('123.45') - @first_money.wealth = new_value - assert @first_money.save - assert @first_money.reload - assert_equal new_value, @first_money.wealth - end - def test_update_number new_single = 789.012 new_double = 789012.345 @@ -192,20 +103,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '2 years 00:03:00', @first_time.time_interval end - def test_update_network_address - new_inet_address = '10.1.2.3/32' - new_cidr_address = '10.0.0.0/8' - new_mac_address = 'bc:de:f0:12:34:56' - @first_network_address.cidr_address = new_cidr_address - @first_network_address.inet_address = new_inet_address - @first_network_address.mac_address = new_mac_address - assert @first_network_address.save - assert @first_network_address.reload - assert_equal @first_network_address.cidr_address, new_cidr_address - assert_equal @first_network_address.inet_address, new_inet_address - assert_equal @first_network_address.mac_address, new_mac_address - end - def test_update_bit_string new_bit_string = '11111111' new_bit_string_varying = '0xFF' @@ -223,20 +120,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save } end - def test_invalid_network_address - @first_network_address.cidr_address = 'invalid addr' - assert_nil @first_network_address.cidr_address - assert_equal 'invalid addr', @first_network_address.cidr_address_before_type_cast - assert @first_network_address.save - - @first_network_address.reload - - @first_network_address.inet_address = 'invalid addr' - assert_nil @first_network_address.inet_address - assert_equal 'invalid addr', @first_network_address.inet_address_before_type_cast - assert @first_network_address.save - end - def test_update_oid new_value = 567890 @first_oid.obj_id = new_value @@ -244,32 +127,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert @first_oid.reload assert_equal new_value, @first_oid.obj_id end - - def test_timestamp_with_zone_values_with_rails_time_zone_support - with_timezone_config default: :utc, aware_attributes: true do - @connection.reconnect! - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time - end - ensure - @connection.reconnect! - end - - def test_timestamp_with_zone_values_without_rails_time_zone_support - with_timezone_config default: :local, aware_attributes: false do - @connection.reconnect! - # make sure to use a non-UTC time zone - @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time - end - ensure - @connection.reconnect! - end end class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb new file mode 100644 index 0000000000..4442abcbc4 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlFullTextTest < ActiveRecord::TestCase + class PostgresqlTsvector < ActiveRecord::Base; end + + def test_tsvector_column + column = PostgresqlTsvector.columns_hash["text_vector"] + assert_equal :tsvector, column.type + assert_equal "tsvector", column.sql_type + assert_not column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + end + + def test_update_tsvector + PostgresqlTsvector.create text_vector: "'text' 'vector'" + tsvector = PostgresqlTsvector.first + assert_equal "'text' 'vector'", tsvector.text_vector + + tsvector.text_vector = "'new' 'text' 'vector'" + tsvector.save! + assert tsvector.reload + assert_equal "'new' 'text' 'vector'", tsvector.text_vector + end +end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb new file mode 100644 index 0000000000..e109f1682b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -0,0 +1,54 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlMoneyTest < ActiveRecord::TestCase + class PostgresqlMoney < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("set lc_monetary = 'C'") + end + + def test_column + column = PostgresqlMoney.columns_hash["wealth"] + assert_equal :decimal, column.type + assert_equal "money", column.sql_type + assert_equal 2, column.scale + assert column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + end + + def test_money_values + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") + + first_money = PostgresqlMoney.find(1) + second_money = PostgresqlMoney.find(2) + assert_equal 567.89, first_money.wealth + assert_equal(-567.89, second_money.wealth) + end + + def test_money_type_cast + column = PostgresqlMoney.columns_hash['wealth'] + assert_equal(12345678.12, column.type_cast("$12,345,678.12")) + assert_equal(12345678.12, column.type_cast("$12.345.678,12")) + assert_equal(-1.15, column.type_cast("-$1.15")) + assert_equal(-2.25, column.type_cast("($2.25)")) + end + + def test_create_and_update_money + money = PostgresqlMoney.create(wealth: "987.65") + assert_equal 987.65, money.wealth + + new_value = BigDecimal.new('123.45') + money.wealth = new_value + money.save! + money.reload + assert_equal new_value, money.wealth + end +end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb new file mode 100644 index 0000000000..e99af07970 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlNetworkTest < ActiveRecord::TestCase + class PostgresqlNetworkAddress < ActiveRecord::Base + end + + def test_cidr_column + column = PostgresqlNetworkAddress.columns_hash["cidr_address"] + assert_equal :cidr, column.type + assert_equal "cidr", column.sql_type + assert_not column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + end + + def test_inet_column + column = PostgresqlNetworkAddress.columns_hash["inet_address"] + assert_equal :inet, column.type + assert_equal "inet", column.sql_type + assert_not column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + end + + def test_macaddr_column + column = PostgresqlNetworkAddress.columns_hash["mac_address"] + assert_equal :macaddr, column.type + assert_equal "macaddr", column.sql_type + assert_not column.number? + assert_not column.text? + assert_not column.binary? + assert_not column.array + end + + def test_network_types + PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24', + inet_address: '172.16.1.254/32', + mac_address: '01:23:45:67:89:0a') + + address = PostgresqlNetworkAddress.first + assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address + assert_equal IPAddr.new('172.16.1.254'), address.inet_address + assert_equal '01:23:45:67:89:0a', address.mac_address + + address.cidr_address = '10.1.2.3/32' + address.inet_address = '10.0.0.0/8' + address.mac_address = 'bc:de:f0:12:34:56' + + address.save! + assert address.reload + assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address + assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address + assert_equal 'bc:de:f0:12:34:56', address.mac_address + end + + def test_invalid_network_address + invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr', + inet_address: 'invalid addr') + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast + assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast + assert invalid_address.save + + invalid_address.reload + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_nil invalid_address.cidr_address_before_type_cast + assert_nil invalid_address.inet_address_before_type_cast + end +end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 51846e22d9..218c59247e 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,13 +10,13 @@ module ActiveRecord end def test_type_cast_true - c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean') + c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') assert_equal 't', @conn.type_cast(true, nil) assert_equal 't', @conn.type_cast(true, c) end def test_type_cast_false - c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean') + c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end @@ -47,9 +47,9 @@ module ActiveRecord def test_quote_cast_numeric fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, OID::String.new, 'varchar') + c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, OID::Text.new, 'text') + c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') assert_equal "'666'", @conn.quote(fixnum, c) end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 4d29a20e66..d4102bf7be 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,6 +2,47 @@ require 'cases/helper' require 'models/developer' require 'models/topic' +class PostgresqlTimestampTest < ActiveRecord::TestCase + class PostgresqlTimestampWithZone < ActiveRecord::Base; end + + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") + end + + teardown do + PostgresqlTimestampWithZone.delete_all + end + + def test_timestamp_with_zone_values_with_rails_time_zone_support + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end + + def test_timestamp_with_zone_values_without_rails_time_zone_support + with_timezone_config default: :local, aware_attributes: false do + @connection.reconnect! + # make sure to use a non-UTC time zone + @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end +end + class TimestampTest < ActiveRecord::TestCase fixtures :topics @@ -84,18 +125,18 @@ class TimestampTest < ActiveRecord::TestCase private - def pg_datetime_precision(table_name, column_name) - results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"] + def pg_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name end + result && result["datetime_precision"] + end - def activerecord_column_option(tablename, column_name, option) - result = ActiveRecord::Base.connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name end + result && result.send(option) + end end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 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/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 3b484a0d64..9c92dc1141 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -369,6 +369,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(2) { line_item.update amount: 10 } end + def test_belongs_to_with_touch_option_on_empty_update + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(0) { line_item.save } + end + def test_belongs_to_with_touch_option_on_destroy line_item = LineItem.create! Invoice.create!(line_items: [line_item]) @@ -563,6 +570,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert companies(:first_client).readonly_firm.readonly? end + def test_test_polymorphic_assignment_foreign_key_type_string + comment = Comment.first + comment.author = Author.first + comment.resource = Member.first + comment.save + + assert_equal Comment.all.to_a, + Comment.includes(:author).to_a + + assert_equal Comment.all.to_a, + Comment.includes(:resource).to_a + end + def test_polymorphic_assignment_foreign_type_field_updating # should update when assigning a saved record sponsor = Sponsor.new diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 495b43c9e5..4c96c2f4fd 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -843,6 +843,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal !real_topic.title?, klass.find(real_topic.id).title? end + def test_calling_super_when_parent_does_not_define_method_raises_error + klass = new_topic_like_ar_class do + def some_method_that_is_not_on_super + super + end + end + + assert_raise(NoMethodError) do + klass.new.some_method_that_is_not_on_super + end + end + private def new_topic_like_ar_class(&block) diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index f7584c3a51..09892d50ba 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -683,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end + @ship.pirate.catchphrase = "Changed Catchphrase" + assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship end + def test_should_save_changed_has_one_changed_object_if_child_is_saved + @pirate.ship.name = "NewName" + assert @pirate.save + assert_equal "NewName", @pirate.ship.reload.name + end + + def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved + @pirate.ship.expects(:save).never + assert @pirate.save + end + # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal assert !@ship.pirate.marked_for_destruction? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7c7c1fbfbd..c565daba65 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 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/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 23e6254577..45e48900ee 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -34,7 +34,7 @@ module ActiveRecord # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, Type::String.new, "varchar(20)") + column = Column.new("title", nil, Type::String.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -42,7 +42,7 @@ module ActiveRecord end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", Type::String.new, "varchar(20)") + column = Column.new("title", "Hello", Type::String.new(limit: 20)) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) @@ -50,7 +50,7 @@ module ActiveRecord end def test_should_specify_not_null_if_null_option_is_false - column = Column.new("title", "Hello", Type::String.new, "varchar(20)", false) + column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false) column_def = ColumnDefinition.new( column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb deleted file mode 100644 index fffcb19e56..0000000000 --- a/activerecord/test/cases/column_test.rb +++ /dev/null @@ -1,158 +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 - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - if current_adapter?(:SQLite3Adapter) - def test_binary_encoding - column = SQLite3Column.new("field", nil, Type::Binary.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 300c2ed225..3abd7a276e 100644 --- a/activerecord/test/cases/connection_adapters/type/type_map_test.rb +++ b/activerecord/test/cases/connection_adapters/type/type_map_test.rb @@ -88,6 +88,23 @@ module ActiveRecord assert_equal mapping.lookup('varchar'), binary end + def test_additional_lookup_args + mapping = TypeMap.new + + mapping.register_type(/varchar/i) do |type, limit| + if limit > 255 + 'text' + else + 'string' + end + end + mapping.alias_type(/string/i, 'varchar') + + assert_equal mapping.lookup('varchar', 200), 'string' + assert_equal mapping.lookup('varchar', 400), 'text' + assert_equal mapping.lookup('string', 400), 'text' + end + def test_requires_value_or_block mapping = TypeMap.new @@ -95,6 +112,19 @@ module ActiveRecord mapping.register_type(/only key/i) end end + + def test_lookup_non_strings + mapping = HashLookupTypeMap.new + + mapping.register_type(1, 'string') + mapping.register_type(2, 'int') + mapping.alias_type(3, 1) + + assert_equal mapping.lookup(1), 'string' + assert_equal mapping.lookup(2), 'int' + assert_equal mapping.lookup(3), 'string' + assert_kind_of Type::Value, mapping.lookup(4) + 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/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/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 6ab1bd8c8b..4b146c11bc 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -366,6 +366,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) end + def test_null_relation_sum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).sum(:id) + assert_equal 0, ac.engines.count + ac.save + assert_equal Hash.new, ac.engines.group(:id).sum(:id) + assert_equal 0, ac.engines.count + end def test_null_relation_count ac = Aircraft.new @@ -376,6 +384,42 @@ class RelationTest < ActiveRecord::TestCase assert_equal 0, ac.engines.count end + def test_null_relation_size + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).size + assert_equal 0, ac.engines.size + ac.save + assert_equal Hash.new, ac.engines.group(:id).size + assert_equal 0, ac.engines.size + end + + def test_null_relation_average + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).average(:id) + assert_equal nil, ac.engines.average(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).average(:id) + assert_equal nil, ac.engines.average(:id) + end + + def test_null_relation_minimum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) + assert_equal nil, ac.engines.minimum(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) + assert_equal nil, ac.engines.minimum(:id) + end + + def test_null_relation_maximum + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) + assert_equal nil, ac.engines.maximum(:id) + ac.save + assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) + assert_equal nil, ac.engines.maximum(:id) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -508,6 +552,13 @@ 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 diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index fd0ef2f89f..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 @@ -353,9 +352,9 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers if current_adapter?(:OracleAdapter) - assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output + assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output else - assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output + assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output end end 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/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 5609cf310c..c8f9d7cf87 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -244,4 +244,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase type = Topic.column_types["content"] assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) end + + def test_serialized_column_should_unserialize_after_update_column + t = Topic.create(content: "first") + assert_equal("first", t.content) + + t.update_column(:content, Topic.serialized_attributes["content"].dump("second")) + assert_equal("second", t.content) + end + + def test_serialized_column_should_unserialize_after_update_attribute + t = Topic.create(content: "first") + assert_equal("first", t.content) + + t.update_attribute(:content, "second") + assert_equal("second", t.content) + 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/models/comment.rb b/activerecord/test/models/comment.rb index bf0162d09b..15970758db 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -7,6 +7,9 @@ class Comment < ActiveRecord::Base scope :created, -> { all } belongs_to :post, :counter_cache => true + belongs_to :author, polymorphic: true + belongs_to :resource, polymorphic: true + has_many :ratings belongs_to :first_post, :foreign_key => :post_id diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index d448fccc5b..c15ee5022e 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 @@ -187,6 +188,9 @@ ActiveRecord::Schema.define do t.integer :taggings_count, default: 0 t.integer :children_count, default: 0 t.integer :parent_id + t.references :author, polymorphic: true + t.string :resource_id + t.string :resource_type end create_table :companies, force: true do |t| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 79133c7a40..2a992f60bb 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,16 @@ +* `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 + `Hash#deep_symbolize_keys!`. + + *OZAWA Sakuro* + +* Fixed confusing `DelegationError` in `Module#delegate`. + + See #15186. + + *Vladimir Yarotsky* + * Fixed `ActiveSupport::Subscriber` so that no duplicate subscriber is created when a subscriber method is redefined. 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/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 3d41aa8572..28536e32a4 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -75,34 +75,26 @@ class Hash # Returns a new hash with all keys converted by the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_transform_keys{ |key| key.to_s.upcase } # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}} def deep_transform_keys(&block) - result = {} - each do |key, value| - result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value - end - result + _deep_transform_keys_in_object(self, &block) end # Destructively convert all keys by using the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_transform_keys!(&block) - keys.each do |key| - value = delete(key) - self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value - end - self + _deep_transform_keys_in_object!(self, &block) end # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # @@ -114,14 +106,14 @@ class Hash # Destructively convert all keys to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_stringify_keys! deep_transform_keys!{ |key| key.to_s } end # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. This includes the keys from the root hash - # and from all nested hashes. + # and from all nested hashes and arrays. # # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } } # @@ -133,8 +125,38 @@ class Hash # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_symbolize_keys! deep_transform_keys!{ |key| key.to_sym rescue key } end + + private + # support methods for deep transforming nested hashes and arrays + def _deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object({}) do |(key, value), result| + result[yield(key)] = _deep_transform_keys_in_object(value, &block) + end + when Array + object.map {|e| _deep_transform_keys_in_object(e, &block) } + else + object + end + end + + def _deep_transform_keys_in_object!(object, &block) + case object + when Hash + object.keys.each do |key| + value = object.delete(key) + object[yield(key)] = _deep_transform_keys_in_object!(value, &block) + end + object + when Array + object.map! {|e| _deep_transform_keys_in_object!(e, &block)} + else + object + end + end end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index f855833a24..e926392952 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -170,38 +170,26 @@ class Module # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' - # The following generated methods call the target exactly once, storing + # The following generated method calls the target exactly once, storing # the returned value in a dummy variable. # # Reason is twofold: On one hand doing less calls is in general better. # On the other hand it could be that the target has side-effects, # whereas conceptually, from the user point of view, the delegator should # be doing one call. - if allow_nil - method_def = [ - "def #{method_prefix}#{method}(#{definition})", # def customer_name(*args, &block) - "_ = #{to}", # _ = client - "if !_.nil? || nil.respond_to?(:#{method})", # if !_.nil? || nil.respond_to?(:name) - " _.#{method}(#{definition})", # _.name(*args, &block) - "end", # end - "end" # end - ].join ';' - else - exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - method_def = [ - "def #{method_prefix}#{method}(#{definition})", # def customer_name(*args, &block) - " _ = #{to}", # _ = client - " _.#{method}(#{definition})", # _.name(*args, &block) - "rescue NoMethodError => e", # rescue NoMethodError => e - " if _.nil? && e.name == :#{method}", # if _.nil? && e.name == :name - " #{exception}", # # add helpful message to the exception - " else", # else - " raise", # raise - " end", # end - "end" # end - ].join ';' - end + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + " _ = #{to}", + " if !_.nil? || nil.respond_to?(:#{method})", + " _.#{method}(#{definition})", + " else", + " #{exception unless allow_nil}", + " end", + "end" + ].join ';' module_eval(method_def, file, line) 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/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/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index ad354a4c30..cb706d77c2 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -46,6 +46,10 @@ class HashExtTest < ActiveSupport::TestCase @nested_illegal_symbols = { [] => { [] => 3} } @upcase_strings = { 'A' => 1, 'B' => 2 } @nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } } + @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] } + @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] } + @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] } + @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] } end def test_methods @@ -84,6 +88,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } end def test_deep_transform_keys_not_mutates @@ -109,6 +116,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } end def test_deep_transform_keys_with_bang_mutates @@ -134,6 +144,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_symbols, @nested_symbols.deep_symbolize_keys assert_equal @nested_symbols, @nested_strings.deep_symbolize_keys assert_equal @nested_symbols, @nested_mixed.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_symbolize_keys end def test_deep_symbolize_keys_not_mutates @@ -159,6 +172,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_symbols, @nested_symbols.deep_dup.deep_symbolize_keys! assert_equal @nested_symbols, @nested_strings.deep_dup.deep_symbolize_keys! assert_equal @nested_symbols, @nested_mixed.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_symbolize_keys! end def test_deep_symbolize_keys_with_bang_mutates @@ -204,6 +220,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_strings, @nested_symbols.deep_stringify_keys assert_equal @nested_strings, @nested_strings.deep_stringify_keys assert_equal @nested_strings, @nested_mixed.deep_stringify_keys + assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_stringify_keys + assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_stringify_keys + assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_stringify_keys end def test_deep_stringify_keys_not_mutates @@ -229,6 +248,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_strings, @nested_symbols.deep_dup.deep_stringify_keys! assert_equal @nested_strings, @nested_strings.deep_dup.deep_stringify_keys! assert_equal @nested_strings, @nested_mixed.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_stringify_keys! end def test_deep_stringify_keys_with_bang_mutates diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index ff6e21854e..380f5ad42b 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -72,7 +72,7 @@ Product = Struct.new(:name) do def type @type ||= begin - nil.type_name + :thing_without_same_method_name_as_delegated.name end 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 14ad4fd424..0b52db5670 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,7 @@ +* Change all non-HTTP method 'post' references to 'article'. + + *John Kelly Ferguson* + * Updates the maintenance policy to match the latest versions of Rails *Matias Korhonen* diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index dbf7ff311d..1ea18ba9f5 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -47,8 +47,7 @@ module RailsGuides end def dom_id_text(text) - text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/[^a-z0-9]+/, ' ') - .strip.gsub(/\s+/, '-') + text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/\s+/, '-') end def engine diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index b700d1c861..ef7ef5a50e 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -28,22 +28,22 @@ For each controller there is an associated directory in the `app/views` director Let's take a look at what Rails does by default when creating a new resource using the scaffold generator: ```bash -$ bin/rails generate scaffold post +$ bin/rails generate scaffold article [...] invoke scaffold_controller - create app/controllers/posts_controller.rb + create app/controllers/articles_controller.rb invoke erb - create app/views/posts - create app/views/posts/index.html.erb - create app/views/posts/edit.html.erb - create app/views/posts/show.html.erb - create app/views/posts/new.html.erb - create app/views/posts/_form.html.erb + create app/views/articles + create app/views/articles/index.html.erb + create app/views/articles/edit.html.erb + create app/views/articles/show.html.erb + create app/views/articles/new.html.erb + create app/views/articles/_form.html.erb [...] ``` There is a naming convention for views in Rails. Typically, the views share their name with the associated controller action, as you can see above. -For example, the index controller action of the `posts_controller.rb` will use the `index.html.erb` view file in the `app/views/posts` directory. +For example, the index controller action of the `articles_controller.rb` will use the `index.html.erb` view file in the `app/views/articles` directory. The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Later on this guide you can find a more detailed documentation of each one of these three components. @@ -276,23 +276,23 @@ Partial Layouts Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally for the entire action, but they work in a similar fashion. -Let's say we're displaying a post on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Post`: +Let's say we're displaying an article on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Article`: ```ruby -Post.create(body: 'Partial Layouts are cool!') +Article.create(body: 'Partial Layouts are cool!') ``` -In the `show` template, we'll render the `_post` partial wrapped in the `box` layout: +In the `show` template, we'll render the `_article` partial wrapped in the `box` layout: -**posts/show.html.erb** +**articles/show.html.erb** ```erb -<%= render partial: 'post', layout: 'box', locals: {post: @post} %> +<%= render partial: 'article', layout: 'box', locals: {article: @article} %> ``` -The `box` layout simply wraps the `_post` partial in a `div`: +The `box` layout simply wraps the `_article` partial in a `div`: -**posts/_box.html.erb** +**articles/_box.html.erb** ```html+erb <div class='box'> @@ -300,13 +300,13 @@ The `box` layout simply wraps the `_post` partial in a `div`: </div> ``` -The `_post` partial wraps the post's `body` in a `div` with the `id` of the post using the `div_for` helper: +The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper: -**posts/_post.html.erb** +**articles/_article.html.erb** ```html+erb -<%= div_for(post) do %> - <p><%= post.body %></p> +<%= div_for(article) do %> + <p><%= article.body %></p> <% end %> ``` @@ -314,22 +314,22 @@ this would output the following: ```html <div class='box'> - <div id='post_1'> + <div id='article_1'> <p>Partial Layouts are cool!</p> </div> </div> ``` -Note that the partial layout has access to the local `post` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. +Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. -You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_post` partial, we could do this instead: +You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead: -**posts/show.html.erb** +**articles/show.html.erb** ```html+erb -<% render(layout: 'box', locals: {post: @post}) do %> - <%= div_for(post) do %> - <p><%= post.body %></p> +<% render(layout: 'box', locals: {article: @article}) do %> + <%= div_for(article) do %> + <p><%= article.body %></p> <% end %> <% end %> ``` @@ -356,18 +356,18 @@ This module provides methods for generating container tags, such as `div`, for y Renders a container tag that relates to your Active Record Object. -For example, given `@post` is the object of `Post` class, you can do: +For example, given `@article` is the object of `Article` class, you can do: ```html+erb -<%= content_tag_for(:tr, @post) do %> - <td><%= @post.title %></td> +<%= content_tag_for(:tr, @article) do %> + <td><%= @article.title %></td> <% end %> ``` This will generate this HTML output: ```html -<tr id="post_1234" class="post"> +<tr id="article_1234" class="article"> <td>Hello World!</td> </tr> ``` @@ -375,34 +375,34 @@ This will generate this HTML output: You can also supply HTML attributes as an additional option hash. For example: ```html+erb -<%= content_tag_for(:tr, @post, class: "frontpage") do %> - <td><%= @post.title %></td> +<%= content_tag_for(:tr, @article, class: "frontpage") do %> + <td><%= @article.title %></td> <% end %> ``` Will generate this HTML output: ```html -<tr id="post_1234" class="post frontpage"> +<tr id="article_1234" class="article frontpage"> <td>Hello World!</td> </tr> ``` -You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@posts` is an array of two `Post` objects: +You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@articles` is an array of two `Article` objects: ```html+erb -<%= content_tag_for(:tr, @posts) do |post| %> - <td><%= post.title %></td> +<%= content_tag_for(:tr, @articles) do |article| %> + <td><%= article.title %></td> <% end %> ``` Will generate this HTML output: ```html -<tr id="post_1234" class="post"> +<tr id="article_1234" class="article"> <td>Hello World!</td> </tr> -<tr id="post_1235" class="post"> +<tr id="article_1235" class="article"> <td>Ruby on Rails Rocks!</td> </tr> ``` @@ -412,15 +412,15 @@ Will generate this HTML output: This is actually a convenient method which calls `content_tag_for` internally with `:div` as the tag name. You can pass either an Active Record object or a collection of objects. For example: ```html+erb -<%= div_for(@post, class: "frontpage") do %> - <td><%= @post.title %></td> +<%= div_for(@article, class: "frontpage") do %> + <td><%= @article.title %></td> <% end %> ``` Will generate this HTML output: ```html -<div id="post_1234" class="post frontpage"> +<div id="article_1234" class="article frontpage"> <td>Hello World!</td> </div> ``` @@ -590,14 +590,14 @@ This helper makes building an Atom feed easy. Here's a full usage example: **config/routes.rb** ```ruby -resources :posts +resources :articles ``` -**app/controllers/posts_controller.rb** +**app/controllers/articles_controller.rb** ```ruby def index - @posts = Post.all + @articles = Article.all respond_to do |format| format.html @@ -606,20 +606,20 @@ def index end ``` -**app/views/posts/index.atom.builder** +**app/views/articles/index.atom.builder** ```ruby atom_feed do |feed| - feed.title("Posts Index") - feed.updated((@posts.first.created_at)) + feed.title("Articles Index") + feed.updated((@articles.first.created_at)) - @posts.each do |post| - feed.entry(post) do |entry| - entry.title(post.title) - entry.content(post.body, type: 'html') + @articles.each do |article| + feed.entry(article) do |entry| + entry.title(article.title) + entry.content(article.body, type: 'html') entry.author do |author| - author.name(post.author_name) + author.name(article.author_name) end end end @@ -697,7 +697,7 @@ For example, let's say we have a standard application layout, but also a special </html> ``` -**app/views/posts/special.html.erb** +**app/views/articles/special.html.erb** ```html+erb <p>This is a special page.</p> @@ -714,7 +714,7 @@ For example, let's say we have a standard application layout, but also a special Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute. ```ruby -date_select("post", "published_on") +date_select("article", "published_on") ``` #### datetime_select @@ -722,7 +722,7 @@ date_select("post", "published_on") Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based attribute. ```ruby -datetime_select("post", "published_on") +datetime_select("article", "published_on") ``` #### distance_of_time_in_words @@ -904,10 +904,10 @@ The params hash has a nested person value, which can therefore be accessed with Returns a checkbox tag tailored for accessing a specified attribute. ```ruby -# Let's say that @post.validated? is 1: -check_box("post", "validated") -# => <input type="checkbox" id="post_validated" name="post[validated]" value="1" /> -# <input name="post[validated]" type="hidden" value="0" /> +# Let's say that @article.validated? is 1: +check_box("article", "validated") +# => <input type="checkbox" id="article_validated" name="article[validated]" value="1" /> +# <input name="article[validated]" type="hidden" value="0" /> ``` #### fields_for @@ -939,7 +939,7 @@ file_field(:user, :avatar) Creates a form and a scope around a specific model object that is used as a base for questioning about values for the fields. ```html+erb -<%= form_for @post do |f| %> +<%= form_for @article do |f| %> <%= f.label :title, 'Title' %>: <%= f.text_field :title %><br> <%= f.label :body, 'Body' %>: @@ -961,8 +961,8 @@ hidden_field(:user, :token) Returns a label tag tailored for labelling an input field for a specified attribute. ```ruby -label(:post, :title) -# => <label for="post_title">Title</label> +label(:article, :title) +# => <label for="article_title">Title</label> ``` #### password_field @@ -979,11 +979,11 @@ password_field(:login, :pass) Returns a radio button tag for accessing a specified attribute. ```ruby -# Let's say that @post.category returns "rails": -radio_button("post", "category", "rails") -radio_button("post", "category", "java") -# => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> -# <input type="radio" id="post_category_java" name="post[category]" value="java" /> +# Let's say that @article.category returns "rails": +radio_button("article", "category", "rails") +radio_button("article", "category", "java") +# => <input type="radio" id="article_category_rails" name="article[category]" value="rails" checked="checked" /> +# <input type="radio" id="article_category_java" name="article[category]" value="java" /> ``` #### text_area @@ -1002,8 +1002,8 @@ text_area(:comment, :text, size: "20x30") Returns an input tag of the "text" type tailored for accessing a specified attribute. ```ruby -text_field(:post, :title) -# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" /> +text_field(:article, :title) +# => <input type="text" id="article_title" name="article[title]" value="#{@article.title}" /> ``` #### email_field @@ -1035,28 +1035,28 @@ Returns `select` and `option` tags for the collection of existing return values Example object structure for use with this method: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base belongs_to :author end class Author < ActiveRecord::Base - has_many :posts + has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -Sample usage (selecting the associated Author for an instance of Post, `@post`): +Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) ``` -If `@post.author_id` is 1, this would return: +If `@article.author_id` is 1, this would return: ```html -<select name="post[author_id]"> +<select name="article[author_id]"> <option value="">Please select</option> <option value="1" selected="selected">D. Heinemeier Hansson</option> <option value="2">D. Thomas</option> @@ -1071,33 +1071,33 @@ Returns `radio_button` tags for the collection of existing return values of `met Example object structure for use with this method: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base belongs_to :author end class Author < ActiveRecord::Base - has_many :posts + has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -Sample usage (selecting the associated Author for an instance of Post, `@post`): +Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) +collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial) ``` -If `@post.author_id` is 1, this would return: +If `@article.author_id` is 1, this would return: ```html -<input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" /> -<label for="post_author_id_1">D. Heinemeier Hansson</label> -<input id="post_author_id_2" name="post[author_id]" type="radio" value="2" /> -<label for="post_author_id_2">D. Thomas</label> -<input id="post_author_id_3" name="post[author_id]" type="radio" value="3" /> -<label for="post_author_id_3">M. Clark</label> +<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" checked="checked" /> +<label for="article_author_id_1">D. Heinemeier Hansson</label> +<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" /> +<label for="article_author_id_2">D. Thomas</label> +<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" /> +<label for="article_author_id_3">M. Clark</label> ``` #### collection_check_boxes @@ -1107,34 +1107,34 @@ Returns `check_box` tags for the collection of existing return values of `method Example object structure for use with this method: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_and_belongs_to_many :authors end class Author < ActiveRecord::Base - has_and_belongs_to_many :posts + has_and_belongs_to_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -Sample usage (selecting the associated Authors for an instance of Post, `@post`): +Sample usage (selecting the associated Authors for an instance of Article, `@article`): ```ruby -collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) +collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial) ``` -If `@post.author_ids` is [1], this would return: +If `@article.author_ids` is [1], this would return: ```html -<input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" /> -<label for="post_author_ids_1">D. Heinemeier Hansson</label> -<input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" /> -<label for="post_author_ids_2">D. Thomas</label> -<input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" /> -<label for="post_author_ids_3">M. Clark</label> -<input name="post[author_ids][]" type="hidden" value="" /> +<input id="article_author_ids_1" name="article[author_ids][]" type="checkbox" value="1" checked="checked" /> +<label for="article_author_ids_1">D. Heinemeier Hansson</label> +<input id="article_author_ids_2" name="article[author_ids][]" type="checkbox" value="2" /> +<label for="article_author_ids_2">D. Thomas</label> +<input id="article_author_ids_3" name="article[author_ids][]" type="checkbox" value="3" /> +<label for="article_author_ids_3">M. Clark</label> +<input name="article[author_ids][]" type="hidden" value="" /> ``` #### country_options_for_select @@ -1222,13 +1222,13 @@ Create a select tag and a series of contained option tags for the provided objec Example: ```ruby -select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) ``` -If `@post.person_id` is 1, this would become: +If `@article.person_id` is 1, this would become: ```html -<select name="post[person_id]"> +<select name="article[person_id]"> <option value=""></option> <option value="1" selected="selected">David</option> <option value="2">Sam</option> @@ -1303,10 +1303,10 @@ file_field_tag 'attachment' Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`. ```html+erb -<%= form_tag '/posts' do %> +<%= form_tag '/articles' do %> <div><%= submit_tag 'Save' %></div> <% end %> -# => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> +# => <form action="/articles" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> ``` #### hidden_field_tag @@ -1368,8 +1368,8 @@ select_tag "people", "<option>David</option>" Creates a submit button with the text provided as the caption. ```ruby -submit_tag "Publish this post" -# => <input name="commit" type="submit" value="Publish this post" /> +submit_tag "Publish this article" +# => <input name="commit" type="submit" value="Publish this article" /> ``` #### text_area_tag @@ -1377,8 +1377,8 @@ submit_tag "Publish this post" Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. ```ruby -text_area_tag 'post' -# => <textarea id="post" name="post"></textarea> +text_area_tag 'article' +# => <textarea id="article" name="article"></textarea> ``` #### text_field_tag @@ -1602,7 +1602,7 @@ Localized Views Action View has the ability render different templates depending on the current locale. -For example, suppose you have a `PostsController` with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. +For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages. @@ -1616,6 +1616,6 @@ def set_expert_locale end ``` -Then you could create special views like `app/views/posts/show.expert.html.erb` that would only be displayed to expert users. +Then you could create special views like `app/views/articles/show.expert.html.erb` that would only be displayed to expert users. You can read more about the Rails Internationalization (I18n) API [here](i18n.html). diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index e815f6b674..21022f1abb 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -82,13 +82,13 @@ by underscores. Examples: * Model Class - Singular with the first letter of each word capitalized (e.g., `BookClub`). -| Model / Class | Table / Schema | -| ------------- | -------------- | -| `Post` | `posts` | -| `LineItem` | `line_items` | -| `Deer` | `deers` | -| `Mouse` | `mice` | -| `Person` | `people` | +| Model / Class | Table / Schema | +| ---------------- | -------------- | +| `Article` | `articles` | +| `LineItem` | `line_items` | +| `Deer` | `deers` | +| `Mouse` | `mice` | +| `Person` | `people` | ### Schema Conventions @@ -120,9 +120,9 @@ to Active Record instances: * `(association_name)_type` - Stores the type for [polymorphic associations](association_basics.html#polymorphic-associations). * `(table_name)_count` - Used to cache the number of belonging objects on - associations. For example, a `comments_count` column in a `Post` class that + associations. For example, a `comments_count` column in a `Articles` class that has many instances of `Comment` will cache the number of existent comments - for each post. + for each article. NOTE: While these column names are optional, they are in fact reserved by Active Record. Steer clear of reserved keywords unless you want the extra functionality. For example, `type` is a reserved keyword used to designate a table using Single Table Inheritance (STI). If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling. diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index fbcce325ed..f0ae3c729e 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -261,27 +261,27 @@ WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Relational Callbacks -------------------- -Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many posts. A user's posts should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Post` model: +Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model: ```ruby class User < ActiveRecord::Base - has_many :posts, dependent: :destroy + has_many :articles, dependent: :destroy end -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base after_destroy :log_destroy_action def log_destroy_action - puts 'Post destroyed' + puts 'Article destroyed' end end >> user = User.first => #<User id: 1> ->> user.posts.create! -=> #<Post id: 1, user_id: 1> +>> user.articles.create! +=> #<Article id: 1, user_id: 1> >> user.destroy -Post destroyed +Article destroyed => #<User id: 1> ``` @@ -328,7 +328,7 @@ When writing conditional callbacks, it is possible to mix both `:if` and `:unles ```ruby class Comment < ActiveRecord::Base after_create :send_email_to_author, if: :author_wants_emails?, - unless: Proc.new { |comment| comment.post.ignore_comments? } + unless: Proc.new { |comment| comment.article.ignore_comments? } end ``` diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 2a76df156c..697ddd70cb 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -472,8 +472,8 @@ Client.where('locked' => true) In the case of a belongs_to relationship, an association key can be used to specify the model if an Active Record object is used as the value. This method works with polymorphic relationships as well. ```ruby -Post.where(author: author) -Author.joins(:posts).where(posts: { author: author }) +Article.where(author: author) +Author.joins(:articles).where(articles: { author: author }) ``` NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`. @@ -511,7 +511,7 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) `NOT` SQL queries can be built by `where.not`. ```ruby -Post.where.not(author: author) +Article.where.not(author: author) ``` In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. @@ -690,32 +690,32 @@ Overriding Conditions You can specify certain conditions to be removed using the `unscope` method. For example: ```ruby -Post.where('id > 10').limit(20).order('id asc').except(:order) +Article.where('id > 10').limit(20).order('id asc').except(:order) ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE id > 10 LIMIT 20 +SELECT * FROM articles WHERE id > 10 LIMIT 20 # Original query without `unscope` -SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20 +SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20 ``` You can additionally unscope specific where clauses. For example: ```ruby -Post.where(id: 10, trashed: false).unscope(where: :id) -# SELECT "posts".* FROM "posts" WHERE trashed = 0 +Article.where(id: 10, trashed: false).unscope(where: :id) +# SELECT "articles".* FROM "articles" WHERE trashed = 0 ``` A relation which has used `unscope` will affect any relation it is merged in to: ```ruby -Post.order('id asc').merge(Post.unscope(:order)) -# SELECT "posts".* FROM "posts" +Article.order('id asc').merge(Article.unscope(:order)) +# SELECT "articles".* FROM "articles" ``` ### `only` @@ -723,16 +723,16 @@ Post.order('id asc').merge(Post.unscope(:order)) You can also override conditions using the `only` method. For example: ```ruby -Post.where('id > 10').limit(20).order('id desc').only(:order, :where) +Article.where('id > 10').limit(20).order('id desc').only(:order, :where) ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE id > 10 ORDER BY id DESC +SELECT * FROM articles WHERE id > 10 ORDER BY id DESC # Original query without `only` -SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20 +SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 ``` @@ -741,25 +741,27 @@ SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20 The `reorder` method overrides the default scope order. For example: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base .. .. has_many :comments, -> { order('posted_at DESC') } end -Post.find(10).comments.reorder('name') +Article.find(10).comments.reorder('name') ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE id = 10 ORDER BY name +SELECT * FROM articles WHERE id = 10 +SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` In case the `reorder` clause is not used, the SQL executed would be: ```sql -SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC +SELECT * FROM articles WHERE id = 10 +SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC ``` ### `reverse_order` @@ -795,25 +797,25 @@ This method accepts **no** arguments. The `rewhere` method overrides an existing, named where condition. For example: ```ruby -Post.where(trashed: true).rewhere(trashed: false) +Article.where(trashed: true).rewhere(trashed: false) ``` The SQL that would be executed: ```sql -SELECT * FROM posts WHERE `trashed` = 0 +SELECT * FROM articles WHERE `trashed` = 0 ``` In case the `rewhere` clause is not used, ```ruby -Post.where(trashed: true).where(trashed: false) +Article.where(trashed: true).where(trashed: false) ``` the SQL executed would be: ```sql -SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0 +SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0 ``` Null Relation @@ -822,21 +824,21 @@ Null Relation The `none` method returns a chainable relation with no records. Any subsequent conditions chained to the returned relation will continue generating empty relations. This is useful in scenarios where you need a chainable response to a method or a scope that could return zero results. ```ruby -Post.none # returns an empty Relation and fires no queries. +Article.none # returns an empty Relation and fires no queries. ``` ```ruby -# The visible_posts method below is expected to return a Relation. -@posts = current_user.visible_posts.where(name: params[:name]) +# The visible_articles method below is expected to return a Relation. +@articles = current_user.visible_articles.where(name: params[:name]) -def visible_posts +def visible_articles case role when 'Country Manager' - Post.where(country: country) + Article.where(country: country) when 'Reviewer' - Post.published + Article.published when 'Bad User' - Post.none # => returning [] or nil breaks the caller code in this case + Article.none # => returning [] or nil breaks the caller code in this case end end ``` @@ -963,21 +965,21 @@ WARNING: This method only works with `INNER JOIN`. Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clauses for those associations when using the `joins` method. -For example, consider the following `Category`, `Post`, `Comment`, `Guest` and `Tag` models: +For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models: ```ruby class Category < ActiveRecord::Base - has_many :posts + has_many :articles end -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base belongs_to :category has_many :comments has_many :tags end class Comment < ActiveRecord::Base - belongs_to :post + belongs_to :article has_one :guest end @@ -986,7 +988,7 @@ class Guest < ActiveRecord::Base end class Tag < ActiveRecord::Base - belongs_to :post + belongs_to :article end ``` @@ -995,64 +997,64 @@ Now all of the following will produce the expected join queries using `INNER JOI #### Joining a Single Association ```ruby -Category.joins(:posts) +Category.joins(:articles) ``` This produces: ```sql SELECT categories.* FROM categories - INNER JOIN posts ON posts.category_id = categories.id + INNER JOIN articles ON articles.category_id = categories.id ``` -Or, in English: "return a Category object for all categories with posts". Note that you will see duplicate categories if more than one post has the same category. If you want unique categories, you can use `Category.joins(:posts).uniq`. +Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`. #### Joining Multiple Associations ```ruby -Post.joins(:category, :comments) +Article.joins(:category, :comments) ``` This produces: ```sql -SELECT posts.* FROM posts - INNER JOIN categories ON posts.category_id = categories.id - INNER JOIN comments ON comments.post_id = posts.id +SELECT articles.* FROM articles + INNER JOIN categories ON articles.category_id = categories.id + INNER JOIN comments ON comments.article_id = articles.id ``` -Or, in English: "return all posts that have a category and at least one comment". Note again that posts with multiple comments will show up multiple times. +Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times. #### Joining Nested Associations (Single Level) ```ruby -Post.joins(comments: :guest) +Article.joins(comments: :guest) ``` This produces: ```sql -SELECT posts.* FROM posts - INNER JOIN comments ON comments.post_id = posts.id +SELECT articles.* FROM articles + INNER JOIN comments ON comments.article_id = articles.id INNER JOIN guests ON guests.comment_id = comments.id ``` -Or, in English: "return all posts that have a comment made by a guest." +Or, in English: "return all articles that have a comment made by a guest." #### Joining Nested Associations (Multiple Level) ```ruby -Category.joins(posts: [{ comments: :guest }, :tags]) +Category.joins(articles: [{ comments: :guest }, :tags]) ``` This produces: ```sql SELECT categories.* FROM categories - INNER JOIN posts ON posts.category_id = categories.id - INNER JOIN comments ON comments.post_id = posts.id + INNER JOIN articles ON articles.category_id = categories.id + INNER JOIN comments ON comments.article_id = articles.id INNER JOIN guests ON guests.comment_id = comments.id - INNER JOIN tags ON tags.post_id = posts.id + INNER JOIN tags ON tags.article_id = articles.id ``` ### Specifying Conditions on the Joined Tables @@ -1121,18 +1123,18 @@ Active Record lets you eager load any number of associations with a single `Mode #### Array of Multiple Associations ```ruby -Post.includes(:category, :comments) +Article.includes(:category, :comments) ``` -This loads all the posts and the associated category and comments for each post. +This loads all the articles and the associated category and comments for each article. #### Nested Associations Hash ```ruby -Category.includes(posts: [{ comments: :guest }, :tags]).find(1) +Category.includes(articles: [{ comments: :guest }, :tags]).find(1) ``` -This will find the category with id 1 and eager load all of the associated posts, the associated posts' tags and comments, and every comment's guest association. +This will find the category with id 1 and eager load all of the associated articles, the associated articles' tags and comments, and every comment's guest association. ### Specifying Conditions on Eager Loaded Associations @@ -1141,18 +1143,18 @@ Even though Active Record lets you specify conditions on the eager loaded associ However if you must do this, you may use `where` as you would normally. ```ruby -Post.includes(:comments).where("comments.visible" => true) +Article.includes(:comments).where("comments.visible" => true) ``` This would generate a query which contains a `LEFT OUTER JOIN` whereas the `joins` method would generate one using the `INNER JOIN` function instead. ```ruby - SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1) + SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1) ``` If there was no `where` condition, this would generate the normal set of two queries. -If, in the case of this `includes` query, there were no comments for any posts, all the posts would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned. +If, in the case of this `includes` query, there were no comments for any articles, all the articles would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned. Scopes ------ @@ -1162,7 +1164,7 @@ Scoping allows you to specify commonly-used queries which can be referenced as m To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base scope :published, -> { where(published: true) } end ``` @@ -1170,7 +1172,7 @@ end This is exactly the same as defining a class method, and which you use is a matter of personal preference: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base def self.published where(published: true) end @@ -1180,7 +1182,7 @@ end Scopes are also chainable within scopes: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base scope :published, -> { where(published: true) } scope :published_and_commented, -> { published.where("comments_count > 0") } end @@ -1189,14 +1191,14 @@ end To call this `published` scope we can call it on either the class: ```ruby -Post.published # => [published posts] +Article.published # => [published articles] ``` -Or on an association consisting of `Post` objects: +Or on an association consisting of `Article` objects: ```ruby category = Category.first -category.posts.published # => [published posts belonging to this category] +category.articles.published # => [published articles belonging to this category] ``` ### Passing in arguments @@ -1204,7 +1206,7 @@ category.posts.published # => [published posts belonging to this category] Your scope can take arguments: ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base scope :created_before, ->(time) { where("created_at < ?", time) } end ``` @@ -1212,13 +1214,13 @@ end Call the scope as if it were a class method: ```ruby -Post.created_before(Time.zone.now) +Article.created_before(Time.zone.now) ``` However, this is just duplicating the functionality that would be provided to you by a class method. ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base def self.created_before(time) where("created_at < ?", time) end @@ -1228,7 +1230,7 @@ end Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects: ```ruby -category.posts.created_before(time) +category.articles.created_before(time) ``` ### Applying a default scope @@ -1591,20 +1593,20 @@ You can also use `any?` and `many?` to check for existence on a model or relatio ```ruby # via a model -Post.any? -Post.many? +Article.any? +Article.many? # via a named scope -Post.recent.any? -Post.recent.many? +Article.recent.any? +Article.recent.many? # via a relation -Post.where(published: true).any? -Post.where(published: true).many? +Article.where(published: true).any? +Article.where(published: true).many? # via an association -Post.first.categories.any? -Post.first.categories.many? +Article.first.categories.any? +Article.first.categories.many? ``` Calculations @@ -1694,18 +1696,18 @@ Running EXPLAIN You can run EXPLAIN on the queries triggered by relations. For example, ```ruby -User.where(id: 1).joins(:posts).explain +User.where(id: 1).joins(:articles).explain ``` may yield ``` -EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 +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 | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | +| 1 | SIMPLE | articles | ALL | NULL | NULL | NULL | NULL | 1 | Using where | +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ 2 rows in set (0.00 sec) ``` @@ -1716,15 +1718,15 @@ Active Record performs a pretty printing that emulates the one of the database shells. So, the same query running with the PostgreSQL adapter would yield instead ``` -EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1 +EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 QUERY PLAN ------------------------------------------------------------------------------ Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - Join Filter: (posts.user_id = users.id) + Join Filter: (articles.user_id = users.id) -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) Index Cond: (id = 1) - -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - Filter: (posts.user_id = 1) + -> Seq Scan on articles (cost=0.00..28.88 rows=8 width=4) + Filter: (articles.user_id = 1) (6 rows) ``` @@ -1733,7 +1735,7 @@ may need the results of previous ones. Because of that, `explain` actually executes the query, and then asks for the query plans. For example, ```ruby -User.where(id: 1).includes(:posts).explain +User.where(id: 1).includes(:articles).explain ``` yields @@ -1747,11 +1749,11 @@ EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ 1 row in set (0.00 sec) -EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1) +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 | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | +| 1 | SIMPLE | articles | ALL | NULL | NULL | NULL | NULL | 1 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec) ``` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 6bedec1e63..cb459626d5 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1129,15 +1129,15 @@ generating a scaffold, Rails will put some ERB into the `_form.html.erb` that it generates that displays the full list of errors on that model. Assuming we have a model that's been saved in an instance variable named -`@post`, it looks like this: +`@article`, it looks like this: ```ruby -<% if @post.errors.any? %> +<% 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> @@ -1151,7 +1151,7 @@ the entry. ``` <div class="field_with_errors"> - <input id="post_title" name="post[title]" size="30" type="text" value=""> + <input id="article_title" name="article[title]" size="30" type="text" value=""> </div> ``` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index dfe9d30698..4f37bf971a 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -3838,7 +3838,7 @@ The name may be given as a symbol or string. A symbol is tested against the bare TIP: A symbol can represent a fully-qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically. -For example, when an action of `PostsController` is called Rails tries optimistically to use `PostsHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `posts_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases: +For example, when an action of `ArticlesController` is called Rails tries optimistically to use `ArticlesHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `articles_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases: ```ruby def default_helper_module! @@ -3861,7 +3861,7 @@ Active Support adds `is_missing?` to `LoadError`, and also assigns that class to Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). -For example, when an action of `PostsController` is called Rails tries to load `posts_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases: +For example, when an action of `ArticlesController` is called Rails tries to load `articles_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases: ```ruby def default_helper_module! diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index d947c5d9d5..7033947468 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -364,7 +364,7 @@ INFO. Options passed to fetch will be merged with the payload. | ------ | --------------------- | | `:key` | Key used in the store | -INFO. Cache stores my add their own keys +INFO. Cache stores may add their own keys ```ruby { diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index f01675b50d..2a3bb4e34d 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -110,14 +110,14 @@ The results of expressions follow them and are introduced by "# => ", vertically If a line is too long, the comment may be placed on the next line: ```ruby -# label(:post, :title) -# # => <label for="post_title">Title</label> +# label(:article, :title) +# # => <label for="article_title">Title</label> # -# label(:post, :title, "A short title") -# # => <label for="post_title">A short title</label> +# label(:article, :title, "A short title") +# # => <label for="article_title">A short title</label> # -# label(:post, :title, "A short title", class: "title_label") -# # => <label for="post_title" class="title_label">A short title</label> +# label(:article, :title, "A short title", class: "title_label") +# # => <label for="article_title" class="title_label">A short title</label> ``` Avoid using any printing methods like `puts` or `p` for that purpose. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index df38bd7321..22f6f5e7d6 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1725,58 +1725,58 @@ mostly useful together with the `:through` option. ```ruby class Person < ActiveRecord::Base has_many :readings - has_many :posts, through: :readings + has_many :articles, through: :readings end person = Person.create(name: 'John') -post = Post.create(name: 'a1') -person.posts << post -person.posts << post -person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">] -Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>] +article = Article.create(name: 'a1') +person.articles << article +person.articles << article +person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">] +Reading.all.inspect # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>] ``` -In the above case there are two readings and `person.posts` brings out both of -them even though these records are pointing to the same post. +In the above case there are two readings and `person.articles` brings out both of +them even though these records are pointing to the same article. Now let's set `distinct`: ```ruby class Person has_many :readings - has_many :posts, -> { distinct }, through: :readings + has_many :articles, -> { distinct }, through: :readings end person = Person.create(name: 'Honda') -post = Post.create(name: 'a1') -person.posts << post -person.posts << post -person.posts.inspect # => [#<Post id: 7, name: "a1">] -Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>] +article = Article.create(name: 'a1') +person.articles << article +person.articles << article +person.articles.inspect # => [#<Article id: 7, name: "a1">] +Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>] ``` -In the above case there are still two readings. However `person.posts` shows -only one post because the collection loads only unique records. +In the above case there are still two readings. However `person.articles` shows +only one article because the collection loads only unique records. If you want to make sure that, upon insertion, all of the records in the persisted association are distinct (so that you can be sure that when you inspect the association that you will never find duplicate records), you should add a unique index on the table itself. For example, if you have a table named -`person_posts` and you want to make sure all the posts are unique, you could +`person_articles` and you want to make sure all the articles are unique, you could add the following in a migration: ```ruby -add_index :person_posts, :post, unique: true +add_index :person_articles, :article, unique: true ``` Note that checking for uniqueness using something like `include?` is subject to race conditions. Do not attempt to use `include?` to enforce distinctness -in an association. For instance, using the post example from above, the +in an association. For instance, using the article example from above, the following code would be racy because multiple users could be attempting this at the same time: ```ruby -person.posts << post unless person.posts.include?(post) +person.articles << article unless person.articles.include?(article) ``` #### When are Objects Saved? diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 4dfafc153d..0061c83a0c 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -355,7 +355,8 @@ To get the full backtrace for running rake task you can pass the option ```bash $ bin/rake --tasks rake about # List versions of all Rails frameworks and the environment -rake assets:clean # Remove compiled assets +rake assets:clean # Remove old compiled assets +rake assets:clobber # Remove compiled assets rake assets:precompile # Compile all the assets named in config.assets.precompile rake db:create # Create the database from config/database.yml for the current Rails.env ... @@ -393,7 +394,12 @@ Database schema version 20110805173523 ### `assets` -You can precompile the assets in `app/assets` using `rake assets:precompile` and remove those compiled assets using `rake assets:clean`. +You can precompile the assets in `app/assets` using `rake assets:precompile`, +and remove older compiled assets using `rake assets:clean`. The `assets:clean` +task allows for rolling deploys that may still be linking to an old asset while +the new assets are being built. + +If you want to clear `public/assets` completely, you can use `rake assets:clobber`. ### `db` @@ -448,7 +454,7 @@ You can also use custom annotations in your code and list them using `rake notes ```bash $ bin/rake notes:custom ANNOTATION=BUG (in /home/foobar/commandsapp) -app/models/post.rb: +app/models/article.rb: * [ 23] Have to fix this one before pushing! ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index e2283df184..2dd00bed2e 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -388,13 +388,13 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` -* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::PostsController` which renders this template: +* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template: ```erb - <%= render @post %> + <%= render @article %> ``` - The default setting is `true`, which uses the partial at `/admin/posts/_post.erb`. Setting the value to `false` would render `/posts/_post.erb`, which is the same behavior as rendering from a non-namespaced controller such as `PostsController`. + The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`. * `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index ef4633ac1a..d3a96daf7b 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -361,9 +361,9 @@ it should not be necessary to visit a webpage to check the history. Description can have multiple paragraphs and you can use code examples inside, just indent it with 4 spaces: - class PostsController + class ArticlesController def index - respond_with Post.limit(10) + respond_with Article.limit(10) end end diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index b067d9efb7..e79ebae818 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -26,17 +26,17 @@ One common task is to inspect the contents of a variable. In Rails, you can do t The `debug` helper will return a \<pre> tag that renders the object using the YAML format. This will generate human-readable data from any object. For example, if you have this code in a view: ```html+erb -<%= debug @post %> +<%= debug @article %> <p> <b>Title:</b> - <%= @post.title %> + <%= @article.title %> </p> ``` You'll see something like this: ```yaml ---- !ruby/object:Post +--- !ruby/object Article attributes: updated_at: 2008-09-05 22:55:47 body: It's a very helpful guide for debugging your Rails app. @@ -55,10 +55,10 @@ Title: Rails debugging guide Displaying an instance variable, or any other object or method, in YAML format can be achieved this way: ```html+erb -<%= simple_format @post.to_yaml %> +<%= simple_format @article.to_yaml %> <p> <b>Title:</b> - <%= @post.title %> + <%= @article.title %> </p> ``` @@ -67,7 +67,7 @@ The `to_yaml` method converts the method to YAML format leaving it more readable As a result of this, you will have something like this in your view: ```yaml ---- !ruby/object:Post +--- !ruby/object Article attributes: updated_at: 2008-09-05 22:55:47 body: It's a very helpful guide for debugging your Rails app. @@ -88,7 +88,7 @@ Another useful method for displaying object values is `inspect`, especially when <%= [1, 2, 3, 4, 5].inspect %> <p> <b>Title:</b> - <%= @post.title %> + <%= @article.title %> </p> ``` @@ -153,18 +153,18 @@ logger.fatal "Terminating application, raised unrecoverable error!!!" Here's an example of a method instrumented with extra logging: ```ruby -class PostsController < ApplicationController +class ArticlesController < ApplicationController # ... def create - @post = Post.new(params[:post]) - logger.debug "New post: #{@post.attributes.inspect}" - logger.debug "Post should be valid: #{@post.valid?}" - - if @post.save - flash[:notice] = 'Post was successfully created.' - logger.debug "The post was saved and now the user is going to be redirected..." - redirect_to(@post) + @article = Article.new(params[:article]) + logger.debug "New article: #{@article.attributes.inspect}" + logger.debug Article should be valid: #{@article.valid?}" + + if @article.save + flash[:notice] = Article was successfully created.' + logger.debug "The article was saved and now the user is going to be redirected..." + redirect_to(@article) else render action: "new" end @@ -177,21 +177,20 @@ end Here's an example of the log generated when this controller action is executed: ``` -Processing PostsController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] +Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4 - Parameters: {"commit"=>"Create", "post"=>{"title"=>"Debugging Rails", + Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, - "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"posts"} -New post: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", - "published"=>false, "created_at"=>nil} -Post should be valid: true - Post Create (0.000443) INSERT INTO "posts" ("updated_at", "title", "body", "published", + "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"} +New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", + "published"=>false, "created_at"=>nil} Article should be valid: true + Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published", "created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails', 'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54') -The post was saved and now the user is going to be redirected... -Redirected to #<Post:0x20af760> -Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts] +The article was saved and now the user is going to be redirected... +Redirected to # Article:0x20af760> +Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/articles] ``` Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia. @@ -286,17 +285,17 @@ Before the prompt, the code around the line that is about to be run will be displayed and the current line will be marked by '=>'. Like this: ``` -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb +[1, 10] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) ``` @@ -320,19 +319,19 @@ For example: Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200 ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" -Processing by PostsController#index as HTML +Processing by ArticlesController#index as HTML -[3, 12] in /PathTo/project/app/controllers/posts_controller.rb +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) ``` @@ -365,15 +364,15 @@ To see the previous ten lines you should type `list-` (or `l-`) ``` (byebug) l- -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb - 1 class PostsController < ApplicationController - 2 before_action :set_post, only: [:show, :edit, :update, :destroy] +[1, 10] in /PathTo/project/app/controllers/articles_controller.rb + 1 class ArticlesController < ApplicationController + 2 before_action :set_article, only: [:show, :edit, :update, :destroy] 3 - 4 # GET /posts - 5 # GET /posts.json + 4 # GET /articles + 5 # GET /articles.json 6 def index 7 byebug - 8 @posts = Post.find_recent + 8 @articles = Article.find_recent 9 10 respond_to do |format| @@ -386,17 +385,17 @@ the code again you can type `list=` ``` (byebug) list= -[3, 12] in /PathTo/project/app/controllers/posts_controller.rb +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) ``` @@ -419,8 +418,8 @@ then `backtrace` will supply the answer. ``` (byebug) where ---> #0 PostsController.index - at /PathTo/project/test_app/app/controllers/posts_controller.rb:8 +--> #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 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) @@ -487,17 +486,17 @@ This example shows how you can print the instance variables defined within the current context: ``` -[3, 12] in /PathTo/project/app/controllers/posts_controller.rb +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb 3: - 4: # GET /posts - 5: # GET /posts.json + 4: # GET /articles + 5: # GET /articles.json 6: def index 7: byebug -=> 8: @posts = Post.find_recent +=> 8: @articles = Article.find_recent 9: 10: respond_to do |format| 11: format.html # index.html.erb - 12: format.json { render json: @posts } + 12: format.json { render json: @articles } (byebug) instance_variables [:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, @@ -512,15 +511,15 @@ command later in this guide). ``` (byebug) next -[5, 14] in /PathTo/project/app/controllers/posts_controller.rb - 5 # GET /posts.json +[5, 14] in /PathTo/project/app/controllers/articles_controller.rb + 5 # GET /articles.json 6 def index 7 byebug - 8 @posts = Post.find_recent + 8 @articles = Article.find_recent 9 => 10 respond_to do |format| 11 format.html # index.html.erb - 12 format.json { render json: @posts } + 12 format.json { render json: @articles } 13 end 14 end 15 @@ -530,11 +529,11 @@ command later in this guide). And then ask again for the instance_variables: ``` -(byebug) instance_variables.include? "@posts" +(byebug) instance_variables.include? "@articles" true ``` -Now `@posts` is included in the instance variables, because the line defining it +Now `@articles` is included in the instance variables, because the line defining it was executed. TIP: You can also step into **irb** mode with the command `irb` (of course!). @@ -564,7 +563,7 @@ example, to check that we have no local variables currently defined. You can also inspect for an object method this way: ``` -(byebug) var instance Post.new +(byebug) var instance Article.new @_start_transaction_state = {} @aggregation_cache = {} @association_cache = {} @@ -581,8 +580,8 @@ You can use also `display` to start watching variables. This is a good way of tracking the values of a variable while the execution goes on. ``` -(byebug) display @posts -1: @posts = nil +(byebug) display @articles +1: @articles = nil ``` The variables inside the displaying list will be printed with their values after @@ -611,10 +610,10 @@ For example, consider the following situation: ```ruby Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200 -Processing by PostsController#index as HTML +Processing by ArticlesController#index as HTML -[1, 8] in /home/davidr/Proyectos/test_app/app/models/post.rb - 1: class Post < ActiveRecord::Base +[1, 8] in /home/davidr/Proyectos/test_app/app/models/article.rb + 1: class Article < ActiveRecord::Base 2: 3: def self.find_recent(limit = 10) 4: byebug @@ -634,15 +633,15 @@ the method, so `byebug` will jump to next next line of the previous frame. (byebug) next Next went up a frame because previous frame finished -[4, 13] in /PathTo/project/test_app/app/controllers/posts_controller.rb - 4: # GET /posts - 5: # GET /posts.json +[4, 13] in /PathTo/project/test_app/app/controllers/articles_controller.rb + 4: # GET /articles + 5: # GET /articles.json 6: def index - 7: @posts = Post.find_recent + 7: @articles = Article.find_recent 8: => 9: respond_to do |format| 10: format.html # index.html.erb - 11: format.json { render json: @posts } + 11: format.json { render json: @articles } 12: end 13: end @@ -693,20 +692,20 @@ _expression_ works the same way as with file:line. For example, in the previous situation ``` -[4, 13] in /PathTo/project/app/controllers/posts_controller.rb - 4: # GET /posts - 5: # GET /posts.json +[4, 13] in /PathTo/project/app/controllers/articles_controller.rb + 4: # GET /articles + 5: # GET /articles.json 6: def index - 7: @posts = Post.find_recent + 7: @articles = Article.find_recent 8: => 9: respond_to do |format| 10: format.html # index.html.erb - 11: format.json { render json: @posts } + 11: format.json { render json: @articles } 12: end 13: end (byebug) break 11 -Created breakpoint 1 at /PathTo/project/app/controllers/posts_controller.rb:11 +Created breakpoint 1 at /PathTo/project/app/controllers/articles_controller.rb:11 ``` @@ -716,7 +715,7 @@ supply a number, it lists that breakpoint. Otherwise it lists all breakpoints. ``` (byebug) info breakpoints Num Enb What -1 y at /PathTo/project/app/controllers/posts_controller.rb:11 +1 y at /PathTo/project/app/controllers/articles_controller.rb:11 ``` To delete breakpoints: use the command `delete _n_` to remove the breakpoint diff --git a/guides/source/engines.md b/guides/source/engines.md index 724c3d9021..1321fa3870 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -36,14 +36,14 @@ engine **can** be a plugin, and a plugin **can** be an engine. The engine that will be created in this guide will be called "blorgh". The engine will provide blogging functionality to its host applications, allowing -for new posts and comments to be created. At the beginning of this guide, you +for new articles and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll see how to hook it into an application. Engines can also be isolated from their host applications. This means that an application is able to have a path provided by a routing helper such as -`posts_path` and use an engine also that provides a path also called -`posts_path`, and the two would not clash. Along with this, controllers, models +`articles_path` and use an engine also that provides a path also called +`articles_path`, and the two would not clash. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide. @@ -197,12 +197,12 @@ within the `Engine` class definition. Without it, classes generated in an engine **may** conflict with an application. What this isolation of the namespace means is that a model generated by a call -to `bin/rails g model`, such as `bin/rails g model post`, won't be called `Post`, but -instead be namespaced and called `Blorgh::Post`. In addition, the table for the -model is namespaced, becoming `blorgh_posts`, rather than simply `posts`. -Similar to the model namespacing, a controller called `PostsController` becomes -`Blorgh::PostsController` and the views for that controller will not be at -`app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced +to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but +instead be namespaced and called `Blorgh::Article`. In addition, the table for the +model is namespaced, becoming `blorgh_articles`, rather than simply `articles`. +Similar to the model namespacing, a controller called `ArticlesController` becomes +`Blorgh::ArticlesController` and the views for that controller will not be at +`app/views/articles`, but `app/views/blorgh/articles` instead. Mailers are namespaced as well. Finally, routes will also be isolated within the engine. This is one of the most @@ -283,74 +283,74 @@ created in the `test` directory as well. For example, you may wish to create a Providing engine functionality ------------------------------ -The engine that this guide covers provides posting and commenting functionality -and follows a similar thread to the [Getting Started +The engine that this guide covers provides submitting articles and commenting +functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists. -### Generating a Post Resource +### Generating an Article Resource -The first thing to generate for a blog engine is the `Post` model and related +The first thing to generate for a blog engine is the `Article` model and related controller. To quickly generate this, you can use the Rails scaffold generator. ```bash -$ bin/rails generate scaffold post title:string text:text +$ bin/rails generate scaffold article title:string text:text ``` This command will output this information: ``` invoke active_record -create db/migrate/[timestamp]_create_blorgh_posts.rb -create app/models/blorgh/post.rb +create db/migrate/[timestamp]_create_blorgh_articles.rb +create app/models/blorgh/article.rb invoke test_unit -create test/models/blorgh/post_test.rb -create test/fixtures/blorgh/posts.yml +create test/models/blorgh/article_test.rb +create test/fixtures/blorgh/articles.yml invoke resource_route - route resources :posts + route resources :articles invoke scaffold_controller -create app/controllers/blorgh/posts_controller.rb +create app/controllers/blorgh/articles_controller.rb invoke erb -create app/views/blorgh/posts -create app/views/blorgh/posts/index.html.erb -create app/views/blorgh/posts/edit.html.erb -create app/views/blorgh/posts/show.html.erb -create app/views/blorgh/posts/new.html.erb -create app/views/blorgh/posts/_form.html.erb +create app/views/blorgh/articles +create app/views/blorgh/articles/index.html.erb +create app/views/blorgh/articles/edit.html.erb +create app/views/blorgh/articles/show.html.erb +create app/views/blorgh/articles/new.html.erb +create app/views/blorgh/articles/_form.html.erb invoke test_unit -create test/controllers/blorgh/posts_controller_test.rb +create test/controllers/blorgh/articles_controller_test.rb invoke helper -create app/helpers/blorgh/posts_helper.rb +create app/helpers/blorgh/articles_helper.rb invoke test_unit -create test/helpers/blorgh/posts_helper_test.rb +create test/helpers/blorgh/articles_helper_test.rb invoke assets invoke js -create app/assets/javascripts/blorgh/posts.js +create app/assets/javascripts/blorgh/articles.js invoke css -create app/assets/stylesheets/blorgh/posts.css +create app/assets/stylesheets/blorgh/articles.css invoke css create app/assets/stylesheets/scaffold.css ``` The first thing that the scaffold generator does is invoke the `active_record` generator, which generates a migration and a model for the resource. Note here, -however, that the migration is called `create_blorgh_posts` rather than the -usual `create_posts`. This is due to the `isolate_namespace` method called in +however, that the migration is called `create_blorgh_articles` rather than the +usual `create_articles`. This is due to the `isolate_namespace` method called in the `Blorgh::Engine` class's definition. The model here is also namespaced, -being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due +being placed at `app/models/blorgh/article.rb` rather than `app/models/article.rb` due to the `isolate_namespace` call within the `Engine` class. Next, the `test_unit` generator is invoked for this model, generating a model -test at `test/models/blorgh/post_test.rb` (rather than -`test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml` -(rather than `test/fixtures/posts.yml`). +test at `test/models/blorgh/article_test.rb` (rather than +`test/models/article_test.rb`) and a fixture at `test/fixtures/blorgh/articles.yml` +(rather than `test/fixtures/articles.yml`). After that, a line for the resource is inserted into the `config/routes.rb` file -for the engine. This line is simply `resources :posts`, turning the +for the engine. This line is simply `resources :articles`, turning the `config/routes.rb` file for the engine into this: ```ruby Blorgh::Engine.routes.draw do - resources :posts + resources :articles end ``` @@ -362,18 +362,18 @@ be isolated from those routes that are within the application. The [Routes](#routes) section of this guide describes it in detail. Next, the `scaffold_controller` generator is invoked, generating a controller -called `Blorgh::PostsController` (at -`app/controllers/blorgh/posts_controller.rb`) and its related views at -`app/views/blorgh/posts`. This generator also generates a test for the -controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper -(`app/helpers/blorgh/posts_controller.rb`). +called `Blorgh::ArticlesController` (at +`app/controllers/blorgh/articles_controller.rb`) and its related views at +`app/views/blorgh/articles`. This generator also generates a test for the +controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper +(`app/helpers/blorgh/articles_controller.rb`). Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: ```ruby module Blorgh - class PostsController < ApplicationController + class ArticlesController < ApplicationController ... end end @@ -382,22 +382,22 @@ end NOTE: The `ApplicationController` class being inherited from here is the `Blorgh::ApplicationController`, not an application's `ApplicationController`. -The helper inside `app/helpers/blorgh/posts_helper.rb` is also namespaced: +The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced: ```ruby module Blorgh - module PostsHelper + module ArticlesHelper ... end end ``` This helps prevent conflicts with any other engine or application that may have -a post resource as well. +a article resource as well. Finally, the assets for this resource are generated in two files: -`app/assets/javascripts/blorgh/posts.js` and -`app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little +`app/assets/javascripts/blorgh/articles.js` and +`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. By default, the scaffold styling is not applied to the engine because the @@ -412,46 +412,46 @@ tag of this layout: You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open -`http://localhost:3000/blorgh/posts` you will see the default scaffold that has +`http://localhost:3000/blorgh/articles` you will see the default scaffold that has been generated. Click around! You've just generated your first engine's first functions. If you'd rather play around in the console, `rails console` will also work just -like a Rails application. Remember: the `Post` model is namespaced, so to -reference it you must call it as `Blorgh::Post`. +like a Rails application. Remember: the `Article` model is namespaced, so to +reference it you must call it as `Blorgh::Article`. ```ruby ->> Blorgh::Post.find(1) -=> #<Blorgh::Post id: 1 ...> +>> Blorgh::Article.find(1) +=> #<Blorgh::Article id: 1 ...> ``` -One final thing is that the `posts` resource for this engine should be the root +One final thing is that the `articles` resource for this engine should be the root of the engine. Whenever someone goes to the root path where the engine is -mounted, they should be shown a list of posts. This can be made to happen if +mounted, they should be shown a list of articles. This can be made to happen if this line is inserted into the `config/routes.rb` file inside the engine: ```ruby -root to: "posts#index" +root to: "articles#index" ``` -Now people will only need to go to the root of the engine to see all the posts, -rather than visiting `/posts`. This means that instead of -`http://localhost:3000/blorgh/posts`, you only need to go to +Now people will only need to go to the root of the engine to see all the articles, +rather than visiting `/articles`. This means that instead of +`http://localhost:3000/blorgh/articles`, you only need to go to `http://localhost:3000/blorgh` now. ### Generating a Comments Resource -Now that the engine can create new blog posts, it only makes sense to add +Now that the engine can create new articles, it only makes sense to add commenting functionality as well. To do this, you'll need to generate a comment -model, a comment controller and then modify the posts scaffold to display +model, a comment controller and then modify the articles scaffold to display comments and allow people to create new ones. From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: a `post_id` integer +`Comment` model, with the related table having two columns: a `article_id` integer and `text` text column. ```bash -$ bin/rails generate model Comment post_id:integer text:text +$ bin/rails generate model Comment article_id:integer text:text ``` This will output the following: @@ -474,17 +474,17 @@ table: $ bin/rake db:migrate ``` -To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and +To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and add this line before the "Edit" link: ```html+erb <h3>Comments</h3> -<%= render @post.comments %> +<%= render @article.comments %> ``` This line will require there to be a `has_many` association for comments defined -on the `Blorgh::Post` model, which there isn't right now. To define one, open -`app/models/blorgh/post.rb` and add this line into the model: +on the `Blorgh::Article` model, which there isn't right now. To define one, open +`app/models/blorgh/article.rb` and add this line into the model: ```ruby has_many :comments @@ -494,7 +494,7 @@ Turning the model into this: ```ruby module Blorgh - class Post < ActiveRecord::Base + class Article < ActiveRecord::Base has_many :comments end end @@ -505,9 +505,9 @@ NOTE: Because the `has_many` is defined inside a class that is inside the model for these objects, so there's no need to specify that using the `:class_name` option here. -Next, there needs to be a form so that comments can be created on a post. To add -this, put this line underneath the call to `render @post.comments` in -`app/views/blorgh/posts/show.html.erb`: +Next, there needs to be a form so that comments can be created on a article. To add +this, put this line underneath the call to `render @article.comments` in +`app/views/blorgh/articles/show.html.erb`: ```erb <%= render "blorgh/comments/form" %> @@ -519,7 +519,7 @@ directory at `app/views/blorgh/comments` and in it a new file called ```html+erb <h3>New comment</h3> -<%= form_for [@post, @post.comments.build] do |f| %> +<%= form_for [@article, @article.comments.build] do |f| %> <p> <%= f.label :text %><br> <%= f.text_area :text %> @@ -529,12 +529,12 @@ directory at `app/views/blorgh/comments` and in it a new file called ``` When this form is submitted, it is going to attempt to perform a `POST` request -to a route of `/posts/:post_id/comments` within the engine. This route doesn't -exist at the moment, but can be created by changing the `resources :posts` line +to a route of `/articles/:article_id/comments` within the engine. This route doesn't +exist at the moment, but can be created by changing the `resources :articles` line inside `config/routes.rb` into these lines: ```ruby -resources :posts do +resources :articles do resources :comments end ``` @@ -567,17 +567,17 @@ invoke css create app/assets/stylesheets/blorgh/comments.css ``` -The form will be making a `POST` request to `/posts/:post_id/comments`, which +The form will be making a `POST` request to `/articles/:article_id/comments`, which will correspond with the `create` action in `Blorgh::CommentsController`. This action needs to be created, which can be done by putting the following lines inside the class definition in `app/controllers/blorgh/comments_controller.rb`: ```ruby def create - @post = Post.find(params[:post_id]) - @comment = @post.comments.create(comment_params) + @article = Article.find(params[:article_id]) + @comment = @article.comments.create(comment_params) flash[:notice] = "Comment has been created!" - redirect_to posts_path + redirect_to articles_path end private @@ -590,11 +590,11 @@ This is the final step required to get the new comment form working. Displaying the comments, however, is not quite right yet. If you were to create a comment right now, you would see this error: -``` +``` Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * -"/Users/ryan/Sites/side_projects/blorgh/app/views" +"/Users/ryan/Sites/side_projects/blorgh/app/views" ``` The engine is unable to find the partial required for rendering the comments. @@ -612,7 +612,7 @@ line inside it: ``` The `comment_counter` local variable is given to us by the `<%= render -@post.comments %>` call, which will define it automatically and increment the +@article.comments %>` call, which will define it automatically and increment the counter as it iterates through each comment. It's used in this example to display a small number next to each comment when it's created. @@ -625,7 +625,7 @@ Hooking Into an Application Using an engine within an application is very easy. This section covers how to mount the engine into an application and the initial setup required, as well as linking the engine to a `User` class provided by the application to provide -ownership for posts and comments within the engine. +ownership for articles and comments within the engine. ### Mounting the Engine @@ -676,7 +676,7 @@ pre-defined path which may be customizable. ### Engine setup -The engine contains migrations for the `blorgh_posts` and `blorgh_comments` +The engine contains migrations for the `blorgh_articles` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the application use this command: @@ -698,7 +698,7 @@ haven't been copied over already. The first run for this command will output something such as this: ```bash -Copied migration [timestamp_1]_create_blorgh_posts.rb from blorgh +Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh ``` @@ -709,7 +709,7 @@ migrations in the application. To run these migrations within the context of the application, simply run `rake db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the -posts will be empty. This is because the table created inside the application is +articles will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the newly mounted engine. You'll find that it's the same as when it was only an engine. @@ -734,11 +734,11 @@ rake db:migrate SCOPE=blorgh VERSION=0 When an engine is created, it may want to use specific classes from an application to provide links between the pieces of the engine and the pieces of -the application. In the case of the `blorgh` engine, making posts and comments +the application. In the case of the `blorgh` engine, making articles and comments have authors would make a lot of sense. A typical application might have a `User` class that would be used to represent -authors for a post or a comment. But there could be a case where the application +authors for a article or a comment. But there could be a case where the application calls this class something different, such as `Person`. For this reason, the engine should not hardcode associations specifically for a `User` class. @@ -753,14 +753,14 @@ rails g model user name:string The `rake db:migrate` command needs to be run here to ensure that our application has the `users` table for future use. -Also, to keep it simple, the posts form will have a new text field called +Also, to keep it simple, the articles form will have a new text field called `author_name`, where users can elect to put their name. The engine will then take this name and either create a new `User` object from it, or find one that -already has that name. The engine will then associate the post with the found or +already has that name. The engine will then associate the article with the found or created `User` object. First, the `author_name` text field needs to be added to the -`app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be +`app/views/blorgh/articles/_form.html.erb` partial inside the engine. This can be added above the `title` field with this code: ```html+erb @@ -770,23 +770,23 @@ added above the `title` field with this code: </div> ``` -Next, we need to update our `Blorgh::PostController#post_params` method to +Next, we need to update our `Blorgh::ArticleController#article_params` method to permit the new form parameter: ```ruby -def post_params - params.require(:post).permit(:title, :text, :author_name) +def article_params + params.require(:article).permit(:title, :text, :author_name) end ``` -The `Blorgh::Post` model should then have some code to convert the `author_name` -field into an actual `User` object and associate it as that post's `author` -before the post is saved. It will also need to have an `attr_accessor` set up +The `Blorgh::Article` model should then have some code to convert the `author_name` +field into an actual `User` object and associate it as that article's `author` +before the article is saved. It will also need to have an `attr_accessor` set up for this field, so that the setter and getter methods are defined for it. To do all this, you'll need to add the `attr_accessor` for `author_name`, the association for the author and the `before_save` call into -`app/models/blorgh/post.rb`. The `author` association will be hard-coded to the +`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the `User` class for the time being. ```ruby @@ -803,14 +803,14 @@ private By representing the `author` association's object with the `User` class, a link is established between the engine and the application. There needs to be a way -of associating the records in the `blorgh_posts` table with the records in the +of associating the records in the `blorgh_articles` table with the records in the `users` table. Because the association is called `author`, there should be an -`author_id` column added to the `blorgh_posts` table. +`author_id` column added to the `blorgh_articles` table. To generate this new column, run this command within the engine: ```bash -$ bin/rails g migration add_author_id_to_blorgh_posts author_id:integer +$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer ``` NOTE: Due to the migration's name and the column specification after it, Rails @@ -828,12 +828,12 @@ $ bin/rake blorgh:install:migrations Notice that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. -``` -NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been +``` +NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been skipped. Migration with the same name already exists. NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists. Copied migration -[timestamp]_add_author_id_to_blorgh_posts.rb from blorgh +[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh ``` Run the migration using: @@ -843,20 +843,20 @@ $ bin/rake db:migrate ``` Now with all the pieces in place, an action will take place that will associate -an author - represented by a record in the `users` table - with a post, -represented by the `blorgh_posts` table from the engine. +an author - represented by a record in the `users` table - with an article, +represented by the `blorgh_articles` table from the engine. -Finally, the author's name should be displayed on the post's page. Add this code -above the "Title" output inside `app/views/blorgh/posts/show.html.erb`: +Finally, the author's name should be displayed on the article's page. Add this code +above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: ```html+erb <p> <b>Author:</b> - <%= @post.author %> + <%= @article.author %> </p> ``` -By outputting `@post.author` using the `<%=` tag, the `to_s` method will be +By outputting `@article.author` using the `<%=` tag, the `to_s` method will be called on the object. By default, this will look quite ugly: ``` @@ -925,15 +925,15 @@ This method works like its brothers, `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.author_class`. -The next step is to switch the `Blorgh::Post` model over to this new setting. +The next step is to switch the `Blorgh::Article` model over to this new setting. Change the `belongs_to` association inside this model -(`app/models/blorgh/post.rb`) to this: +(`app/models/blorgh/article.rb`) to this: ```ruby belongs_to :author, class_name: Blorgh.author_class ``` -The `set_author` method in the `Blorgh::Post` model should also use this class: +The `set_author` method in the `Blorgh::Article` model should also use this class: ```ruby self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name) @@ -960,7 +960,7 @@ Resulting in something a little shorter, and more implicit in its behavior. The `author_class` method should always return a `Class` object. Since we changed the `author_class` method to return a `Class` instead of a -`String`, we must also modify our `belongs_to` definition in the `Blorgh::Post` +`String`, we must also modify our `belongs_to` definition in the `Blorgh::Article` model: ```ruby @@ -985,14 +985,14 @@ to load that class and then reference the related table. This could lead to problems if the table wasn't already existing. Therefore, a `String` should be used and then converted to a class using `constantize` in the engine later on. -Go ahead and try to create a new post. You will see that it works exactly in the +Go ahead and try to create a new article. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in `config/initializers/blorgh.rb` to learn what the class is. There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by` method which returns an object of that class, to be -associated with a post when it's created. This object, of course, should have +associated with an article when it's created. This object, of course, should have some sort of identifier by which it can be referenced. #### General Engine Configuration @@ -1107,12 +1107,12 @@ that isn't referenced by your main application. #### Implementing Decorator Pattern Using Class#class_eval -**Adding** `Post#time_since_created`: +**Adding** `Article#time_since_created`: ```ruby -# MyApp/app/decorators/models/blorgh/post_decorator.rb +# MyApp/app/decorators/models/blorgh/article_decorator.rb -Blorgh::Post.class_eval do +Blorgh::Article.class_eval do def time_since_created Time.current - created_at end @@ -1120,20 +1120,20 @@ end ``` ```ruby -# Blorgh/app/models/post.rb +# Blorgh/app/models/article.rb -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments end ``` -**Overriding** `Post#summary`: +**Overriding** `Article#summary`: ```ruby -# MyApp/app/decorators/models/blorgh/post_decorator.rb +# MyApp/app/decorators/models/blorgh/article_decorator.rb -Blorgh::Post.class_eval do +Blorgh::Article.class_eval do def summary "#{title} - #{truncate(text)}" end @@ -1141,9 +1141,9 @@ end ``` ```ruby -# Blorgh/app/models/post.rb +# Blorgh/app/models/article.rb -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base has_many :comments def summary "#{title}" @@ -1159,13 +1159,13 @@ class modifications, you might want to consider using [`ActiveSupport::Concern`] ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code. -**Adding** `Post#time_since_created` and **Overriding** `Post#summary`: +**Adding** `Article#time_since_created` and **Overriding** `Article#summary`: ```ruby -# MyApp/app/models/blorgh/post.rb +# MyApp/app/models/blorgh/article.rb -class Blorgh::Post < ActiveRecord::Base - include Blorgh::Concerns::Models::Post +class Blorgh::Article < ActiveRecord::Base + include Blorgh::Concerns::Models::Article def time_since_created Time.current - created_at @@ -1178,22 +1178,22 @@ end ``` ```ruby -# Blorgh/app/models/post.rb +# Blorgh/app/models/article.rb -class Post < ActiveRecord::Base - include Blorgh::Concerns::Models::Post +class Article < ActiveRecord::Base + include Blorgh::Concerns::Models::Article end ``` ```ruby -# Blorgh/lib/concerns/models/post +# Blorgh/lib/concerns/models/article -module Blorgh::Concerns::Models::Post +module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern # 'included do' causes the included code to be evaluated in the - # context where it is included (post.rb), rather than being - # executed in the module's context (blorgh/concerns/models/post). + # context where it is included (article.rb), rather than being + # executed in the module's context (blorgh/concerns/models/article). included do attr_accessor :author_name belongs_to :author, class_name: "User" @@ -1224,25 +1224,25 @@ When Rails looks for a view to render, it will first look in the `app/views` directory of the application. If it cannot find the view there, it will check in the `app/views` directories of all engines that have this directory. -When the application is asked to render the view for `Blorgh::PostsController`'s +When the application is asked to render the view for `Blorgh::ArticlesController`'s index action, it will first look for the path -`app/views/blorgh/posts/index.html.erb` within the application. If it cannot +`app/views/blorgh/articles/index.html.erb` within the application. If it cannot find it, it will look inside the engine. You can override this view in the application by simply creating a new file at -`app/views/blorgh/posts/index.html.erb`. Then you can completely change what +`app/views/blorgh/articles/index.html.erb`. Then you can completely change what this view would normally output. -Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` +Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb` and put this content in it: ```html+erb -<h1>Posts</h1> -<%= link_to "New Post", new_post_path %> -<% @posts.each do |post| %> - <h2><%= post.title %></h2> - <small>By <%= post.author %></small> - <%= simple_format(post.text) %> +<h1>Articles</h1> +<%= link_to "New Article", new_article_path %> +<% @articles.each do |article| %> + <h2><%= article.title %></h2> + <small>By <%= article.author %></small> + <%= simple_format(article.text) %> <hr> <% end %> ``` @@ -1259,30 +1259,30 @@ Routes inside an engine are drawn on the `Engine` class within ```ruby Blorgh::Engine.routes.draw do - resources :posts + resources :articles end ``` By having isolated routes such as this, if you wish to link to an area of an engine from within an application, you will need to use the engine's routing -proxy method. Calls to normal routing methods such as `posts_path` may end up +proxy method. Calls to normal routing methods such as `articles_path` may end up going to undesired locations if both the application and the engine have such a helper defined. -For instance, the following example would go to the application's `posts_path` -if that template was rendered from the application, or the engine's `posts_path` +For instance, the following example would go to the application's `articles_path` +if that template was rendered from the application, or the engine's `articles_path` if it was rendered from the engine: ```erb -<%= link_to "Blog posts", posts_path %> +<%= link_to "Blog articles", articles_path %> ``` -To make this route always use the engine's `posts_path` routing helper method, +To make this route always use the engine's `articles_path` routing helper method, we must call the method on the routing proxy method that shares the same name as the engine. ```erb -<%= link_to "Blog posts", blorgh.posts_path %> +<%= link_to "Blog articles", blorgh.articles_path %> ``` If you wish to reference the application inside the engine in a similar way, use diff --git a/guides/source/generators.md b/guides/source/generators.md index bb9653e3f2..25c67de993 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -279,10 +279,10 @@ end and see it in action when invoking the generator: ```bash -$ bin/rails generate scaffold Post body:text +$ bin/rails generate scaffold Article body:text [...] invoke my_helper - create app/helpers/posts_helper.rb + create app/helpers/articles_helper.rb ``` We can notice on the output that our new helper was invoked instead of the Rails default. However one thing is missing, which is tests for our new generator and to do that, we are going to reuse old helpers test generators. diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index bd33c5a146..5b75540c05 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -506,33 +506,33 @@ Layout declarations cascade downward in the hierarchy, and more specific layout end ``` -* `posts_controller.rb` +* `articles_controller.rb` ```ruby - class PostsController < ApplicationController + class ArticlesController < ApplicationController end ``` -* `special_posts_controller.rb` +* `special_articles_controller.rb` ```ruby - class SpecialPostsController < PostsController + class SpecialArticlesController < ArticlesController layout "special" end ``` -* `old_posts_controller.rb` +* `old_articles_controller.rb` ```ruby - class OldPostsController < SpecialPostsController + class OldArticlesController < SpecialArticlesController layout false def show - @post = Post.find(params[:id]) + @article = Article.find(params[:id]) end def index - @old_posts = Post.older + @old_articles = Article.older render layout: "old" end # ... @@ -542,10 +542,10 @@ Layout declarations cascade downward in the hierarchy, and more specific layout In this application: * In general, views will be rendered in the `main` layout -* `PostsController#index` will use the `main` layout -* `SpecialPostsController#index` will use the `special` layout -* `OldPostsController#show` will use no layout at all -* `OldPostsController#index` will use the `old` layout +* `ArticlesController#index` will use the `main` layout +* `SpecialArticlesController#index` will use the `special` layout +* `OldArticlesController#show` will use no layout at all +* `OldArticlesController#index` will use the `old` layout #### Avoiding Double Render Errors diff --git a/guides/source/routing.md b/guides/source/routing.md index 314dbffae2..0ff13cb07d 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -183,61 +183,61 @@ You may wish to organize groups of controllers under a namespace. Most commonly, ```ruby namespace :admin do - resources :posts, :comments + resources :articles, :comments end ``` -This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create: +This will create a number of routes for each of the `articles` and `comments` controller. For `Admin::ArticlesController`, Rails will create: -| HTTP Verb | Path | Controller#Action | Named Helper | -| --------- | --------------------- | ------------------- | ------------------------- | -| GET | /admin/posts | admin/posts#index | admin_posts_path | -| GET | /admin/posts/new | admin/posts#new | new_admin_post_path | -| POST | /admin/posts | admin/posts#create | admin_posts_path | -| GET | /admin/posts/:id | admin/posts#show | admin_post_path(:id) | -| GET | /admin/posts/:id/edit | admin/posts#edit | edit_admin_post_path(:id) | -| PATCH/PUT | /admin/posts/:id | admin/posts#update | admin_post_path(:id) | -| DELETE | /admin/posts/:id | admin/posts#destroy | admin_post_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | ------------------------ | ---------------------- | ---------------------------- | +| GET | /admin/articles | admin/articles#index | admin_articles_path | +| GET | /admin/articles/new | admin/articles#new | new_admin_article_path | +| POST | /admin/articles | admin/articles#create | admin_articles_path | +| GET | /admin/articles/:id | admin/articles#show | admin_article_path(:id) | +| GET | /admin/articles/:id/edit | admin/articles#edit | edit_admin_article_path(:id) | +| PATCH/PUT | /admin/articles/:id | admin/articles#update | admin_article_path(:id) | +| DELETE | /admin/articles/:id | admin/articles#destroy | admin_article_path(:id) | -If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use: +If you want to route `/articles` (without the prefix `/admin`) to `Admin::ArticlesController`, you could use: ```ruby scope module: 'admin' do - resources :posts, :comments + resources :articles, :comments end ``` or, for a single case: ```ruby -resources :posts, module: 'admin' +resources :articles, module: 'admin' ``` -If you want to route `/admin/posts` to `PostsController` (without the `Admin::` module prefix), you could use: +If you want to route `/admin/articles` to `ArticlesController` (without the `Admin::` module prefix), you could use: ```ruby scope '/admin' do - resources :posts, :comments + resources :articles, :comments end ``` or, for a single case: ```ruby -resources :posts, path: '/admin/posts' +resources :articles, path: '/admin/articles' ``` In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`: -| HTTP Verb | Path | Controller#Action | Named Helper | -| --------- | --------------------- | ----------------- | ------------------- | -| GET | /admin/posts | posts#index | posts_path | -| GET | /admin/posts/new | posts#new | new_post_path | -| POST | /admin/posts | posts#create | posts_path | -| GET | /admin/posts/:id | posts#show | post_path(:id) | -| GET | /admin/posts/:id/edit | posts#edit | edit_post_path(:id) | -| PATCH/PUT | /admin/posts/:id | posts#update | post_path(:id) | -| DELETE | /admin/posts/:id | posts#destroy | post_path(:id) | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | ------------------------ | -------------------- | ---------------------- | +| GET | /admin/articles | articles#index | articles_path | +| GET | /admin/articles/new | articles#new | new_article_path | +| POST | /admin/articles | articles#create | articles_path | +| GET | /admin/articles/:id | articles#show | article_path(:id) | +| GET | /admin/articles/:id/edit | articles#edit | edit_article_path(:id) | +| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) | +| DELETE | /admin/articles/:id | articles#destroy | article_path(:id) | TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._ @@ -304,7 +304,7 @@ TIP: _Resources should never be nested more than 1 level deep._ One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource, like this: ```ruby -resources :posts do +resources :articles do resources :comments, only: [:index, :new, :create] end resources :comments, only: [:show, :edit, :update, :destroy] @@ -313,7 +313,7 @@ resources :comments, only: [:show, :edit, :update, :destroy] This idea strikes a balance between descriptive routes and deep nesting. There exists shorthand syntax to achieve just that, via the `:shallow` option: ```ruby -resources :posts do +resources :articles do resources :comments, shallow: true end ``` @@ -321,7 +321,7 @@ end This will generate the exact same routes as the first example. You can also specify the `:shallow` option in the parent resource, in which case all of the nested resources will be shallow: ```ruby -resources :posts, shallow: true do +resources :articles, shallow: true do resources :comments resources :quotes resources :drafts @@ -332,7 +332,7 @@ The `shallow` method of the DSL creates a scope inside of which every nesting is ```ruby shallow do - resources :posts do + resources :articles do resources :comments resources :quotes resources :drafts @@ -344,7 +344,7 @@ There exist two options for `scope` to customize shallow routes. `:shallow_path` ```ruby scope shallow_path: "sekret" do - resources :posts do + resources :articles do resources :comments, shallow: true end end @@ -352,21 +352,21 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Controller#Action | Named Helper | -| --------- | -------------------------------------- | ----------------- | --------------------- | -| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments_path | -| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments_path | -| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment_path | -| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path | -| GET | /sekret/comments/:id(.:format) | comments#show | comment_path | -| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path | -| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------------------------- | ----------------- | ------------------------ | +| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path | +| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path | +| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path | +| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path | +| GET | /sekret/comments/:id(.:format) | comments#show | comment_path | +| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path | +| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path | The `:shallow_prefix` option adds the specified parameter to the named helpers: ```ruby scope shallow_prefix: "sekret" do - resources :posts do + resources :articles do resources :comments, shallow: true end end @@ -374,15 +374,15 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Controller#Action | Named Helper | -| --------- | -------------------------------------- | ----------------- | ------------------------ | -| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments_path | -| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments_path | -| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment_path | -| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path | -| GET | /comments/:id(.:format) | comments#show | sekret_comment_path | -| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path | -| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path | +| HTTP Verb | Path | Controller#Action | Named Helper | +| --------- | -------------------------------------------- | ----------------- | --------------------------- | +| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path | +| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path | +| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path | +| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path | +| GET | /comments/:id(.:format) | comments#show | sekret_comment_path | +| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path | +| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path | ### Routing concerns @@ -403,7 +403,7 @@ These concerns can be used in resources to avoid code duplication and share beha ```ruby resources :messages, concerns: :commentable -resources :posts, concerns: [:commentable, :image_attachable] +resources :articles, concerns: [:commentable, :image_attachable] ``` The above is equivalent to: @@ -413,7 +413,7 @@ resources :messages do resources :comments end -resources :posts do +resources :articles do resources :comments resources :images, only: :index end @@ -422,7 +422,7 @@ end Also you can use them in any place that you want inside the routes, for example in a scope or namespace call: ```ruby -namespace :posts do +namespace :articles do concerns :commentable end ``` @@ -662,15 +662,15 @@ get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/ `:constraints` takes regular expressions with the restriction that regexp anchors can't be used. For example, the following route will not work: ```ruby -get '/:id', to: 'posts#show', constraints: { id: /^\d/ } +get '/:id', to: 'articles#show', constraints: { id: /^\d/ } ``` However, note that you don't need to use anchors because all routes are anchored at the start. -For example, the following routes would allow for `posts` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace: +For example, the following routes would allow for `articles` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace: ```ruby -get '/:id', to: 'posts#show', constraints: { id: /\d.+/ } +get '/:id', to: 'articles#show', constraints: { id: /\d.+/ } get '/:username', to: 'users#show' ``` @@ -771,20 +771,20 @@ get '*pages', to: 'pages#show', format: true You can redirect any path to another path using the `redirect` helper in your router: ```ruby -get '/stories', to: redirect('/posts') +get '/stories', to: redirect('/articles') ``` You can also reuse dynamic segments from the match in the path to redirect to: ```ruby -get '/stories/:name', to: redirect('/posts/%{name}') +get '/stories/:name', to: redirect('/articles/%{name}') ``` You can also provide a block to redirect, which receives the symbolized path parameters and the request object: ```ruby -get '/stories/:name', to: redirect { |path_params, req| "/posts/#{path_params[:name].pluralize}" } -get '/stories', to: redirect { |path_params, req| "/posts/#{req.subdomain}" } +get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" } +get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" } ``` Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. @@ -793,7 +793,7 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl ### Routing to Rack Applications -Instead of a String like `'posts#index'`, which corresponds to the `index` action in the `PostsController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher: +Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher: ```ruby match '/application.js', to: Sprockets, via: :all @@ -801,7 +801,7 @@ match '/application.js', to: Sprockets, via: :all As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate. -NOTE: For the curious, `'posts#index'` actually expands out to `PostsController.action(:index)`, which returns a valid Rack application. +NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. ### Using `root` @@ -837,7 +837,7 @@ get 'こんにちは', to: 'welcome#index' Customizing Resourceful Routes ------------------------------ -While the default routes and helpers generated by `resources :posts` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers. +While the default routes and helpers generated by `resources :articles` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers. ### Specifying a Controller to Use @@ -974,11 +974,11 @@ You can prefix routes with a named parameter also: ```ruby scope ':username' do - resources :posts + resources :articles end ``` -This will provide you with URLs such as `/bob/posts/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views. +This will provide you with URLs such as `/bob/articles/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views. ### Restricting the Routes Created diff --git a/guides/source/testing.md b/guides/source/testing.md index 9fc866fff3..e9a5e0d7ab 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -141,20 +141,20 @@ NOTE: For more information on Rails <i>scaffolding</i>, refer to [Getting Starte When you use `rails generate scaffold`, for a resource among other things it creates a test stub in the `test/models` folder: ```bash -$ bin/rails generate scaffold post title:string body:text +$ bin/rails generate scaffold article title:string body:text ... -create app/models/post.rb -create test/models/post_test.rb -create test/fixtures/posts.yml +create app/models/article.rb +create test/models/article_test.rb +create test/fixtures/articles.yml ... ``` -The default test stub in `test/models/post_test.rb` looks like this: +The default test stub in `test/models/article_test.rb` looks like this: ```ruby require 'test_helper' -class PostTest < ActiveSupport::TestCase +class ArticleTest < ActiveSupport::TestCase # test "the truth" do # assert true # end @@ -170,10 +170,10 @@ require 'test_helper' As you know by now, `test_helper.rb` specifies the default configuration to run our tests. This is included with all the tests, so any methods added to this file are available to all your tests. ```ruby -class PostTest < ActiveSupport::TestCase +class ArticleTest < ActiveSupport::TestCase ``` -The `PostTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `PostTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide. +The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide. Any method defined within a class inherited from `MiniTest::Unit::TestCase` (which is the superclass of `ActiveSupport::TestCase`) that begins with `test` (case sensitive) is simply called a test. So, `test_password`, `test_valid_password` and `testValidPassword` all are legal test names and are run automatically when the test case is run. @@ -220,7 +220,7 @@ In order to run your tests, your test database will need to have the current str Running a test is as simple as invoking the file containing the test cases through `rake test` command. ```bash -$ bin/rake test test/models/post_test.rb +$ bin/rake test test/models/article_test.rb . Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. @@ -231,7 +231,7 @@ Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. You can also run a particular test method from the test case by running the test and providing the `test method name`. ```bash -$ bin/rake test test/models/post_test.rb test_the_truth +$ bin/rake test test/models/article_test.rb test_the_truth . Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. @@ -243,25 +243,25 @@ This will run all test methods from the test case. Note that `test_helper.rb` is The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. -To see how a test failure is reported, you can add a failing test to the `post_test.rb` test case. +To see how a test failure is reported, you can add a failing test to the `article_test.rb` test case. ```ruby -test "should not save post without title" do - post = Post.new - assert_not post.save +test "should not save article without title" do + article = Article.new + assert_not article.save end ``` Let us run this newly added test. ```bash -$ bin/rake test test/models/post_test.rb test_should_not_save_post_without_title +$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title F Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. 1) Failure: -test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: +test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: Failed assertion, no message given. 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips @@ -270,9 +270,9 @@ Failed assertion, no message given. In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: ```ruby -test "should not save post without title" do - post = Post.new - assert_not post.save, "Saved the post without a title" +test "should not save article without title" do + article = Article.new + assert_not article.save, "Saved the article without a title" end ``` @@ -280,14 +280,14 @@ Running this test shows the friendlier assertion message: ```bash 1) Failure: -test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: -Saved the post without a title +test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: +Saved the article without a title ``` Now to get this test to pass we can add a model level validation for the _title_ field. ```ruby -class Post < ActiveRecord::Base +class Article < ActiveRecord::Base validates :title, presence: true end ``` @@ -295,7 +295,7 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ bin/rake test test/models/post_test.rb test_should_not_save_post_without_title +$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title . Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. @@ -320,15 +320,15 @@ end Now you can see even more output in the console from running the tests: ```bash -$ bin/rake test test/models/post_test.rb test_should_report_error +$ bin/rake test test/models/article_test.rb test_should_report_error E Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. 1) Error: -test_should_report_error(PostTest): -NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0> - test/models/post_test.rb:10:in `block in <class:PostTest>' +test_should_report_error(ArticleTest): +NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0> + test/models/article_test.rb:10:in `block in <class:ArticleTest>' 1 tests, 0 assertions, 0 failures, 1 errors, 0 skips ``` @@ -345,7 +345,7 @@ backtrace. simply set the `BACKTRACE` environment variable to enable this behavior: ```bash -$ BACKTRACE=1 bin/rake test test/models/post_test.rb +$ BACKTRACE=1 bin/rake test test/models/article_test.rb ``` ### What to Include in Your Unit Tests @@ -422,26 +422,26 @@ You should test for things such as: * was the correct object stored in the response template? * was the appropriate message displayed to the user in the view? -Now that we have used Rails scaffold generator for our `Post` resource, it has already created the controller code and tests. You can take look at the file `posts_controller_test.rb` in the `test/controllers` directory. +Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. -Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`. +Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`. ```ruby -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success - assert_not_nil assigns(:posts) + assert_not_nil assigns(:articles) end end ``` -In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `posts` instance variable. +In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `articles` instance variable. The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments: * The action of the controller you are requesting. This can be in the form of a string or a symbol. -* An optional hash of request parameters to pass into the action (eg. query string parameters or post variables). +* An optional hash of request parameters to pass into the action (eg. query string parameters or article variables). * An optional hash of session variables to pass along with the request. * An optional hash of flash values. @@ -457,17 +457,17 @@ Another example: Calling the `:view` action, passing an `id` of 12 as the `param get(:view, {'id' => '12'}, nil, {'message' => 'booya!'}) ``` -NOTE: If you try running `test_should_create_post` test from `posts_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. +NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. -Let us modify `test_should_create_post` test in `posts_controller_test.rb` so that all our test pass: +Let us modify `test_should_create_article` test in `articles_controller_test.rb` so that all our test pass: ```ruby -test "should create post" do - assert_difference('Post.count') do - post :create, post: {title: 'Some title'} +test "should create article" do + assert_difference('Article.count') do + post :create, article: {title: 'Some title'} end - assert_redirected_to post_path(assigns(:post)) + assert_redirected_to article_path(assigns(:article)) end ``` @@ -576,12 +576,12 @@ is the correct way to assert for the layout when the view renders a partial with Here's another example that uses `flash`, `assert_redirected_to`, and `assert_difference`: ```ruby -test "should create post" do - assert_difference('Post.count') do - post :create, post: {title: 'Hi', body: 'This is my first post.'} +test "should create article" do + assert_difference('article.count') do + post :create, article: {title: 'Hi', body: 'This is my first article.'} end - assert_redirected_to post_path(assigns(:post)) - assert_equal 'Post was successfully created.', flash[:notice] + assert_redirected_to article_path(assigns(:article)) + assert_equal 'Article was successfully created.', flash[:notice] end ``` @@ -712,7 +712,7 @@ class UserFlowsTest < ActionDispatch::IntegrationTest assert_equal 'Welcome david!', flash[:notice] https!(false) - get "/posts/all" + get "/articles/all" assert_response :success assert assigns(:products) end @@ -807,43 +807,43 @@ For more information on `MiniTest`, refer to [Minitest](http://www.ruby-doc.org/ Setup and Teardown ------------------ -If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Posts` controller: +If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Articles` controller: ```ruby require 'test_helper' -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase # called before every single test def setup - @post = posts(:one) + @article = articles(:one) end # called after every single test def teardown - # as we are re-initializing @post before every test + # as we are re-initializing @article before every test # setting it to nil here is not essential but I hope # you understand how you can use the teardown method - @post = nil + @article = nil end - test "should show post" do - get :show, id: @post.id + test "should show article" do + get :show, id: @article.id assert_response :success end - test "should destroy post" do - assert_difference('Post.count', -1) do - delete :destroy, id: @post.id + test "should destroy article" do + assert_difference('Article.count', -1) do + delete :destroy, id: @article.id end - assert_redirected_to posts_path + assert_redirected_to articles_path end end ``` -Above, the `setup` method is called before each test and so `@post` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using: +Above, the `setup` method is called before each test and so `@article` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using: * a block * a method (like in the earlier example) @@ -855,38 +855,38 @@ Let's see the earlier example by specifying `setup` callback by specifying a met ```ruby require 'test_helper' -class PostsControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionController::TestCase # called before every single test - setup :initialize_post + setup :initialize_article # called after every single test def teardown - @post = nil + @article = nil end - test "should show post" do - get :show, id: @post.id + test "should show article" do + get :show, id: @article.id assert_response :success end - test "should update post" do - patch :update, id: @post.id, post: {} - assert_redirected_to post_path(assigns(:post)) + test "should update article" do + patch :update, id: @article.id, article: {} + assert_redirected_to article_path(assigns(:article)) end - test "should destroy post" do - assert_difference('Post.count', -1) do - delete :destroy, id: @post.id + test "should destroy article" do + assert_difference('Article.count', -1) do + delete :destroy, id: @article.id end - assert_redirected_to posts_path + assert_redirected_to articles_path end private - def initialize_post - @post = posts(:one) + def initialize_article + @article = articles(:one) end end ``` @@ -894,11 +894,11 @@ end Testing Routes -------------- -Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Posts` controller above should look like: +Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Articles` controller above should look like: ```ruby -test "should route to post" do - assert_routing '/posts/1', {controller: "posts", action: "show", id: "1"} +test "should route to article" do + assert_routing '/articles/1', {controller: "articles", action: "show", id: "1"} end ``` diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index b0543d8d20..eab5779533 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -488,7 +488,7 @@ def update respond_to do |format| format.json do # perform a partial update - @post.update params[:post] + @article.update params[:article] end format.json_patch do diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index aba3c9ed61..7c3fd9f69d 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -158,7 +158,7 @@ is a helper that assists with writing forms. `form_for` takes a `:remote` option. It works like this: ```erb -<%= form_for(@post, remote: true) do |f| %> +<%= form_for(@article, remote: true) do |f| %> ... <% end %> ``` @@ -166,7 +166,7 @@ option. It works like this: This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/posts" class="new_post" data-remote="true" id="new_post" method="post"> +<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post"> ... </form> ``` @@ -180,10 +180,10 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out: ```coffeescript $(document).ready -> - $("#new_post").on("ajax:success", (e, data, status, xhr) -> - $("#new_post").append xhr.responseText + $("#new_article").on("ajax:success", (e, data, status, xhr) -> + $("#new_article").append xhr.responseText ).on "ajax:error", (e, xhr, status, error) -> - $("#new_post").append "<p>ERROR</p>" + $("#new_article").append "<p>ERROR</p>" ``` Obviously, you'll want to be a bit more sophisticated than that, but it's a @@ -196,7 +196,7 @@ is very similar to `form_for`. It has a `:remote` option that you can use like this: ```erb -<%= form_tag('/posts', remote: true) do %> +<%= form_tag('/articles', remote: true) do %> ... <% end %> ``` @@ -204,7 +204,7 @@ this: This will generate the following HTML: ```html -<form accept-charset="UTF-8" action="/posts" data-remote="true" method="post"> +<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post"> ... </form> ``` @@ -219,21 +219,21 @@ is a helper that assists with generating links. It has a `:remote` option you can use like this: ```erb -<%= link_to "a post", @post, remote: true %> +<%= link_to "an article", @article, remote: true %> ``` which generates ```html -<a href="/posts/1" data-remote="true">a post</a> +<a href="/articles/1" data-remote="true">an article</a> ``` You can bind to the same Ajax events as `form_for`. Here's an example. Let's -assume that we have a list of posts that can be deleted with just one +assume that we have a list of articles that can be deleted with just one click. We would generate some HTML like this: ```erb -<%= link_to "Delete post", @post, remote: true, method: :delete %> +<%= link_to "Delete article", @article, remote: true, method: :delete %> ``` and write some CoffeeScript like this: @@ -241,7 +241,7 @@ and write some CoffeeScript like this: ```coffeescript $ -> $("a[data-remote]").on "ajax:success", (e, data, status, xhr) -> - alert "The post was deleted." + alert "The article was deleted." ``` ### button_to @@ -249,14 +249,14 @@ $ -> [`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this: ```erb -<%= button_to "A post", @post, remote: true %> +<%= button_to "An article", @article, remote: true %> ``` this generates ```html -<form action="/posts/1" class="button_to" data-remote="true" method="post"> - <div><input type="submit" value="A post"></div> +<form action="/articles/1" class="button_to" data-remote="true" method="post"> + <div><input type="submit" value="An article"></div> </form> ``` diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index ba6a0feeef..9e7569465f 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -2,8 +2,7 @@ *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* |