diff options
184 files changed, 2597 insertions, 1013 deletions
diff --git a/README.rdoc b/README.rdoc index 0b209cf56f..216a122c66 100644 --- a/README.rdoc +++ b/README.rdoc @@ -48,7 +48,7 @@ more separate. Each of these packages can be used independently outside of "Welcome aboard: You're riding Ruby on Rails!" -5. Follow the guidelines to start developing your application. You can find the following resources handy: +5. Follow the guidelines to start developing your application. You may find the following resources handy: * The README file created within your application. * The {Getting Started with Rails}[http://guides.rubyonrails.org/getting_started.html]. diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 0fa18d751b..9b206fbcc7 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -32,7 +32,7 @@ This can be as simple as: end The body of the email is created by using an Action View template (regular -ERb) that has the instance variables that are declared in the mailer action. +ERB) that has the instance variables that are declared in the mailer action. So the corresponding body template for the method above could look like this: @@ -72,6 +72,19 @@ Or you can just chain the methods together like: Notifier.welcome.deliver # Creates the email and sends it immediately +== Setting defaults + +It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you wont need to worry about that. Finally it is also possible to pass in a Proc that will get evaluated when it is needed. + +Note that every value you set with this method will get over written if you use the same key in your mailer method. + +Example: + + class Authenticationmailer < ActionMailer::Base + default :from => "awesome@application.com", :subject => Proc.new { "E-mail was generated at #{Time.now}" } + ..... + end + == Receiving emails To receive emails, you need to implement a public instance method called <tt>receive</tt> that takes an diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 2ae85f8b57..ee02cf6945 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -17,8 +17,6 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.requirements << 'none' - s.has_rdoc = true - s.add_dependency('actionpack', version) s.add_dependency('mail', '~> 2.2.15') end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 16fcf112b7..cd76383931 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -4,6 +4,7 @@ require 'action_mailer/collector' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/proc' +require 'active_support/core_ext/string/inflections' require 'action_mailer/log_subscriber' module ActionMailer #:nodoc: @@ -349,9 +350,6 @@ module ActionMailer #:nodoc: helper ActionMailer::MailHelper include ActionMailer::OldApi - delegate :register_observer, :to => Mail - delegate :register_interceptor, :to => Mail - private_class_method :new #:nodoc: class_attribute :default_params @@ -363,6 +361,32 @@ module ActionMailer #:nodoc: }.freeze class << self + # Register one or more Observers which will be notified when mail is delivered. + def register_observers(*observers) + observers.flatten.compact.each { |observer| register_observer(observer) } + end + + # Register one or more Interceptors which will be called before mail is sent. + def register_interceptors(*interceptors) + interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) } + end + + # Register an Observer which will be notified when mail is delivered. + # Either a class or a string can be passed in as the Observer. If a string is passed in + # it will be <tt>constantize</tt>d. + def register_observer(observer) + delivery_observer = (observer.is_a?(String) ? observer.constantize : observer) + Mail.register_observer(delivery_observer) + end + + # Register an Inteceptor which will be called before mail is sent. + # Either a class or a string can be passed in as the Observer. If a string is passed in + # it will be <tt>constantize</tt>d. + def register_interceptor(interceptor) + delivery_interceptor = (interceptor.is_a?(String) ? interceptor.constantize : interceptor) + Mail.register_interceptor(delivery_interceptor) + end + def mailer_name @mailer_name ||= name.underscore end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 4ec478067f..444754d0e9 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -19,13 +19,17 @@ module ActionMailer options.stylesheets_dir ||= paths["public/stylesheets"].first # make sure readers methods get compiled - options.asset_path ||= app.config.asset_path - options.asset_host ||= app.config.asset_host + options.asset_path ||= app.config.asset_path + options.asset_host ||= app.config.asset_host ActiveSupport.on_load(:action_mailer) do include AbstractController::UrlFor extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) include app.routes.mounted_helpers + + register_interceptors(options.delete(:interceptors)) + register_observers(options.delete(:observers)) + options.each { |k,v| send("#{k}=", v) } end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 6a7931da8c..9fdd0e1ced 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -478,6 +478,11 @@ class BaseTest < ActiveSupport::TestCase end end + class MySecondObserver + def self.delivered_email(mail) + end + end + test "you can register an observer to the mail object that gets informed on email delivery" do ActionMailer::Base.register_observer(MyObserver) mail = BaseMailer.welcome @@ -485,11 +490,31 @@ class BaseTest < ActiveSupport::TestCase mail.deliver end + test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do + ActionMailer::Base.register_observer("BaseTest::MyObserver") + mail = BaseMailer.welcome + MyObserver.expects(:delivered_email).with(mail) + mail.deliver + end + + test "you can register multiple observers to the mail object that both get informed on email delivery" do + ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver) + mail = BaseMailer.welcome + MyObserver.expects(:delivered_email).with(mail) + MySecondObserver.expects(:delivered_email).with(mail) + mail.deliver + end + class MyInterceptor def self.delivering_email(mail) end end + class MySecondInterceptor + def self.delivering_email(mail) + end + end + test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do ActionMailer::Base.register_interceptor(MyInterceptor) mail = BaseMailer.welcome @@ -497,6 +522,21 @@ class BaseTest < ActiveSupport::TestCase mail.deliver end + test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do + ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor") + mail = BaseMailer.welcome + MyInterceptor.expects(:delivering_email).with(mail) + mail.deliver + end + + test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do + ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) + mail = BaseMailer.welcome + MyInterceptor.expects(:delivering_email).with(mail) + MySecondInterceptor.expects(:delivering_email).with(mail) + mail.deliver + end + test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do mail1 = ProcMailer.welcome yesterday = 1.day.ago diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 3eba2281c4..25e2d27a01 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.1.0 (unreleased)* +* Implicit actions named not_implemented can be rendered [Santiago Pastorino] + * Wildcard route will always matching the optional format segment by default. For example if you have this route: map '*pages' => 'pages#show' diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 3661d27d51..b900962d32 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -19,7 +19,7 @@ It consists of several modules: * Action View, which handles view template lookup and rendering, and provides view helpers that assist when building HTML forms, Atom feeds and more. - Template formats that Action View handles are ERb (embedded Ruby, typically + Template formats that Action View handles are ERB (embedded Ruby, typically used to inline short Ruby snippets inside HTML), XML Builder and RJS (dynamically generated JavaScript from Ruby code). @@ -57,7 +57,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActionController/Base.html] -* ERb templates (static content mixed with dynamic output from ruby) +* ERB templates (static content mixed with dynamic output from ruby) <% for post in @posts %> Title: <%= post.title %> diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 651f3b164a..d3c66800d9 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -17,8 +17,6 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.requirements << 'none' - s.has_rdoc = true - s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) s.add_dependency('rack-cache', '~> 1.0.0') @@ -28,5 +26,5 @@ Gem::Specification.new do |s| s.add_dependency('rack-test', '~> 0.5.7') s.add_dependency('rack-mount', '~> 0.7.1') s.add_dependency('tzinfo', '~> 0.3.23') - s.add_dependency('erubis', '~> 2.6.6') + s.add_dependency('erubis', '~> 2.7.0') end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 07ff5ad9f3..0951267fea 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,3 +1,4 @@ +require 'erubis' require 'active_support/configurable' require 'active_support/descendants_tracker' require 'active_support/core_ext/module/anonymous' @@ -18,6 +19,7 @@ module AbstractController include ActiveSupport::Configurable extend ActiveSupport::DescendantsTracker + undef_method :not_implemented class << self attr_reader :abstract alias_method :abstract?, :abstract diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 95992c2698..f7b2b7ff53 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -29,7 +29,7 @@ module AbstractController # # ==== Options # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except<tt> - The callback should be run for all actions except this action + # * <tt>except</tt> - The callback should be run for all actions except this action def _normalize_callback_options(options) if only = options[:only] only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index e6523e56d2..5f9e082cd3 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -105,7 +105,7 @@ module ActionController # == Renders # # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering - # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured. + # of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured. # The controller passes objects to the view by assigning instance variables: # # def show @@ -128,7 +128,7 @@ module ActionController # end # end # - # Read more about writing ERb and Builder templates in ActionView::Base. + # Read more about writing ERB and Builder templates in ActionView::Base. # # == Redirects # diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index e5db31061b..585bd5e5ab 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -201,19 +201,23 @@ module ActionController class_attribute :middleware_stack self.middleware_stack = ActionController::MiddlewareStack.new - def self.inherited(base) + def self.inherited(base) #nodoc: base.middleware_stack = self.middleware_stack.dup super end + # Adds given middleware class and its args to bottom of middleware_stack def self.use(*args, &block) middleware_stack.use(*args, &block) end + # Alias for middleware_stack def self.middleware middleware_stack end + # Makes the controller a rack endpoint that points to the action in + # the given env's action_dispatch.request.path_parameters key. def self.call(env) action(env['action_dispatch.request.path_parameters'][:action]).call(env) end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index b98429792d..1d6df89007 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -67,7 +67,7 @@ module ActionController # class PostsController < ApplicationController # REALM = "SuperSecret" # USERS = {"dhh" => "secret", #plain text password - # "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password + # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password # # before_filter :authenticate, :except => [:index] # diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 678f4ca763..3ec0c4c6a4 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -10,8 +10,8 @@ module ActionController end end - def default_render - render + def default_render(*args) + render(*args) end def action_method?(action_name) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index a2e06fe0a6..618a6cdb7d 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,8 +1,9 @@ require 'abstract_controller/collector' require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/object/inclusion' module ActionController #:nodoc: - module MimeResponds #:nodoc: + module MimeResponds extend ActiveSupport::Concern included do @@ -189,7 +190,7 @@ module ActionController #:nodoc: raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? if response = retrieve_response_from_mimes(mimes, &block) - response.call + response.call(nil) end end @@ -222,6 +223,9 @@ module ActionController #:nodoc: # is quite simple (it just needs to respond to call), you can even give # a proc to it. # + # In order to use respond_with, first you need to declare the formats your + # controller responds to in the class level with a call to <tt>respond_to</tt>. + # def respond_with(*resources, &block) raise "In order to use respond_with, first you need to declare the formats your " << "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? @@ -245,9 +249,9 @@ module ActionController #:nodoc: config = self.class.mimes_for_respond_to[mime] if config[:except] - !config[:except].include?(action) + !action.in?(config[:except]) elsif config[:only] - config[:only].include?(action) + action.in?(config[:only]) else true end @@ -257,9 +261,9 @@ module ActionController #:nodoc: # Collects mimes and return the response for the negotiated format. Returns # nil if :not_acceptable was sent to the client. # - def retrieve_response_from_mimes(mimes=nil, &block) + def retrieve_response_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) { default_render } + collector = Collector.new(mimes) { |options| default_render(options || {}) } block.call(collector) if block_given? if format = request.negotiate_mime(collector.order) diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 4b45413cf8..59a3621f72 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -77,6 +77,37 @@ module ActionController #:nodoc: # # respond_with(@project, :manager, @task) # + # === Custom options + # + # <code>respond_with</code> also allow you to pass options that are forwarded + # to the underlying render call. Those options are only applied success + # scenarios. For instance, you can do the following in the create method above: + # + # def create + # @project = Project.find(params[:project_id]) + # @task = @project.comments.build(params[:task]) + # flash[:notice] = 'Task was successfully created.' if @task.save + # respond_with(@project, @task, :status => 201) + # end + # + # This will return status 201 if the task was saved with success. If not, + # it will simply ignore the given options and return status 422 and the + # resource errors. To customize the failure scenario, you can pass a + # a block to <code>respond_with</code>: + # + # def create + # @project = Project.find(params[:project_id]) + # @task = @project.comments.build(params[:task]) + # respond_with(@project, @task, :status => 201) do |format| + # if @task.save + # flash[:notice] = 'Task was successfully created.' + # else + # format.html { render "some_special_template" } + # end + # end + # end + # + # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>. class Responder attr_reader :controller, :request, :format, :resource, :resources, :options @@ -131,7 +162,11 @@ module ActionController #:nodoc: # responds to :to_format and display it. # def to_format - default_render + if get? || !has_errors? + default_render + else + display_errors + end rescue ActionView::MissingTemplate => e api_behavior(e) end @@ -155,8 +190,6 @@ module ActionController #:nodoc: if get? display resource - elsif has_errors? - display resource.errors, :status => :unprocessable_entity elsif post? display resource, :status => :created, :location => api_location elsif has_empty_resource_definition? @@ -185,7 +218,7 @@ module ActionController #:nodoc: # controller. # def default_render - @default_response.call + @default_response.call(options) end # Display is just a shortcut to render a resource with the current format. @@ -209,6 +242,10 @@ module ActionController #:nodoc: controller.render given_options.merge!(options).merge!(format => resource) end + def display_errors + controller.render format => resource.errors, :status => :unprocessable_entity + end + # Check whether the resource has errors. # def has_errors? diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 49971fc9f8..7f972fc281 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -60,6 +60,7 @@ module ActionDispatch autoload :Static end + autoload :ClosedError, 'action_dispatch/middleware/closed_error' autoload :MiddlewareStack, 'action_dispatch/middleware/stack' autoload :Routing diff --git a/actionpack/lib/action_dispatch/middleware/closed_error.rb b/actionpack/lib/action_dispatch/middleware/closed_error.rb new file mode 100644 index 0000000000..0a4db47f4b --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/closed_error.rb @@ -0,0 +1,7 @@ +module ActionDispatch + class ClosedError < StandardError #:nodoc: + def initialize(kind) + super "Cannot modify #{kind} because it was closed. This means it was already streamed back to the client or converted to HTTP headers." + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 7ac608f0a8..24ebb8fed7 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -83,7 +83,7 @@ module ActionDispatch # Raised when storing more than 4K of session data. class CookieOverflow < StandardError; end - class CookieJar < Hash #:nodoc: + class CookieJar #:nodoc: # This regular expression is used to split the levels of a domain. # The top level domain can be any string without a period or @@ -115,13 +115,22 @@ module ActionDispatch @delete_cookies = {} @host = host @secure = secure - - super() + @closed = false + @cookies = {} end + attr_reader :closed + alias :closed? :closed + def close!; @closed = true end + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. def [](name) - super(name.to_s) + @cookies[name.to_s] + end + + def update(other_hash) + @cookies.update other_hash + self end def handle_options(options) #:nodoc: @@ -145,6 +154,7 @@ module ActionDispatch # Sets the cookie named +name+. The second argument may be the very cookie # value, or a hash of options as documented above. def []=(key, options) + raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! value = options[:value] @@ -153,7 +163,7 @@ module ActionDispatch options = { :value => value } end - value = super(key.to_s, value) + value = @cookies[key.to_s] = value handle_options(options) @@ -170,7 +180,7 @@ module ActionDispatch handle_options(options) - value = super(key.to_s) + value = @cookies.delete(key.to_s) @delete_cookies[key] = options value end @@ -225,6 +235,7 @@ module ActionDispatch end def []=(key, options) + raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! else @@ -263,6 +274,7 @@ module ActionDispatch end def []=(key, options) + raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! options[:value] = @verifier.generate(options[:value]) @@ -305,6 +317,7 @@ module ActionDispatch end def call(env) + cookie_jar = nil status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] @@ -315,6 +328,9 @@ module ActionDispatch end [status, headers, body] + ensure + cookie_jar = ActionDispatch::Request.new(env).cookie_jar unless cookie_jar + cookie_jar.close! end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 21aeeb217a..027ff7f8ac 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -43,9 +43,15 @@ module ActionDispatch class FlashNow #:nodoc: def initialize(flash) @flash = flash + @closed = false end + attr_reader :closed + alias :closed? :closed + def close!; @closed = true end + def []=(k, v) + raise ClosedError, :flash if closed? @flash[k] = v @flash.discard(k) v @@ -66,27 +72,70 @@ module ActionDispatch end end - class FlashHash < Hash + class FlashHash + include Enumerable + def initialize #:nodoc: - super - @used = Set.new + @used = Set.new + @closed = false + @flashes = {} end + attr_reader :closed + alias :closed? :closed + def close!; @closed = true end + def []=(k, v) #:nodoc: + raise ClosedError, :flash if closed? keep(k) - super + @flashes[k] = v + end + + def [](k) + @flashes[k] end def update(h) #:nodoc: h.keys.each { |k| keep(k) } - super + @flashes.update h + self + end + + def keys + @flashes.keys + end + + def key?(name) + @flashes.key? name + end + + def delete(key) + @flashes.delete key + self + end + + def to_hash + @flashes.dup + end + + def empty? + @flashes.empty? + end + + def clear + @flashes.clear + end + + def each(&block) + @flashes.each(&block) end alias :merge! :update def replace(h) #:nodoc: @used = Set.new - super + @flashes.replace h + self end # Sets a flash that will not be available to the next action, only to the current. @@ -100,7 +149,7 @@ module ActionDispatch # # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>. def now - FlashNow.new(self) + @now ||= FlashNow.new(self) end # Keeps either the entire current flash or a specific flash entry available for the next action: @@ -184,8 +233,11 @@ module ActionDispatch session = env['rack.session'] || {} flash_hash = env['action_dispatch.request.flash_hash'] - if flash_hash && (!flash_hash.empty? || session.key?('flash')) + if flash_hash + if !flash_hash.empty? || session.key?('flash') session["flash"] = flash_hash + end + flash_hash.close! end if session.key?('flash') && session['flash'].empty? diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 35be0b3a27..13aef18ee1 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,6 +1,7 @@ require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/inclusion' require 'active_support/inflector' require 'action_dispatch/routing/redirection' @@ -1345,11 +1346,11 @@ module ActionDispatch end def resource_scope? #:nodoc: - [:resource, :resources].include?(@scope[:scope_level]) + @scope[:scope_level].among?(:resource, :resources) end def resource_method_scope? #:nodoc: - [:collection, :member, :new].include?(@scope[:scope_level]) + @scope[:scope_level].among?(:collection, :member, :new) end def with_exclusive_scope diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 77a15f3e97..16a6b93ce8 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActionDispatch module Assertions # A small suite of assertions that test responses from \Rails applications. @@ -33,7 +35,7 @@ module ActionDispatch def assert_response(type, message = nil) validate_request! - if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?") + if type.among?(:success, :missing, :redirect, :error) && @response.send("#{type}?") assert_block("") { true } # to count the assertion elsif type.is_a?(Fixnum) && @response.response_code == type assert_block("") { true } # to count the assertion diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 2b862fb7d6..f41d3e5ddb 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -1,4 +1,5 @@ require 'action_controller/vendor/html-scanner' +require 'active_support/core_ext/object/inclusion' #-- # Copyright (c) 2006 Assaf Arkin (http://labnotes.org) @@ -441,7 +442,7 @@ module ActionDispatch if matches assert_block("") { true } # to count the assertion - if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) + if block_given? && !rjs_type.among?(:remove, :show, :hide, :toggle) begin @selected ||= nil in_scope, @selected = @selected, matches diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 5c6416a19e..174babb9aa 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -1,6 +1,7 @@ require 'stringio' require 'uri' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/try' require 'rack/test' require 'test/unit/assertions' @@ -26,31 +27,31 @@ module ActionDispatch # object's <tt>@response</tt> instance variable will point to the same # response object. # - # You can also perform POST, PUT, DELETE, and HEAD requests with +post+, - # +put+, +delete+, and +head+. + # You can also perform POST, PUT, DELETE, and HEAD requests with +#post+, + # +#put+, +#delete+, and +#head+. def get(path, parameters = nil, headers = nil) process :get, path, parameters, headers end - # Performs a POST request with the given parameters. See get() for more + # Performs a POST request with the given parameters. See +#get+ for more # details. def post(path, parameters = nil, headers = nil) process :post, path, parameters, headers end - # Performs a PUT request with the given parameters. See get() for more + # Performs a PUT request with the given parameters. See +#get+ for more # details. def put(path, parameters = nil, headers = nil) process :put, path, parameters, headers end - # Performs a DELETE request with the given parameters. See get() for + # Performs a DELETE request with the given parameters. See +#get+ for # more details. def delete(path, parameters = nil, headers = nil) process :delete, path, parameters, headers end - # Performs a HEAD request with the given parameters. See get() for more + # Performs a HEAD request with the given parameters. See +#get+ for more # details. def head(path, parameters = nil, headers = nil) process :head, path, parameters, headers @@ -59,7 +60,7 @@ module ActionDispatch # Performs an XMLHttpRequest request with the given parameters, mirroring # a request from the Prototype library. # - # The request_method is :get, :post, :put, :delete or :head; the + # The request_method is +:get+, +:post+, +:put+, +:delete+ or +:head+; the # parameters are +nil+, a hash, or a url-encoded or multipart string; # the headers are a hash. Keys are automatically upcased and prefixed # with 'HTTP_' if not already. @@ -243,7 +244,8 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, parameters = nil, rack_environment = nil) + def process(method, path, parameters = nil, env = nil) + env ||= {} if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -259,7 +261,7 @@ module ActionDispatch hostname, port = host.split(':') - env = { + default_env = { :method => method, :params => parameters, @@ -277,9 +279,7 @@ module ActionDispatch session = Rack::Test::Session.new(_mock_session) - (rack_environment || {}).each do |key, value| - env[key] = value - end + env.reverse_merge!(default_env) # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri @@ -321,7 +321,7 @@ module ActionDispatch define_method(method) do |*args| reset! unless integration_session # reset the html_document variable, but only for new get/post calls - @html_document = nil unless %w(cookies assigns).include?(method) + @html_document = nil unless method.among?("cookies", "assigns") integration_session.__send__(method, *args).tap do copy_session_variables! end @@ -384,7 +384,7 @@ module ActionDispatch end end - # An test that spans multiple controllers and actions, + # An integration test spans multiple controllers and actions, # tying them all together to ensure they work together as expected. It tests # more completely than either unit or functional tests do, exercising the # entire stack, from the dispatcher to the database. diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index ab8c6259c5..5519103627 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -8,13 +8,13 @@ require 'action_view/log_subscriber' module ActionView #:nodoc: # = Action View Base # - # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb + # Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERB # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. # If the template file has a <tt>.rjs</tt> extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator. # - # == ERb + # == ERB # - # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the # following loop for names: # # <b>Names of all the people</b> @@ -23,7 +23,7 @@ module ActionView #:nodoc: # <% end %> # # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this - # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong: + # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong: # # <%# WRONG %> # Hi, Mr. <% puts "Frodo" %> @@ -81,7 +81,7 @@ module ActionView #:nodoc: # # == Builder # - # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An XmlMarkup object + # Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension. # # Here are some basic examples: diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index db9d7a08ff..96e5722252 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -4,7 +4,7 @@ module ActionView # = Action View Atom Feed Helpers module Helpers #:nodoc: module AtomFeedHelper - # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other + # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other # template languages). # # Full usage example: diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index c88bd1efd5..9ac7dff1ec 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -14,7 +14,7 @@ module ActionView # variable. You can then use this variable anywhere in your templates or layout. # # ==== Examples - # The capture method can be used in ERb templates... + # The capture method can be used in ERB templates... # # <% @greeting = capture do %> # Welcome to my shiny new web page! The date and time is diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb index 65c8debc76..1f2bc28cac 100644 --- a/actionpack/lib/action_view/helpers/csrf_helper.rb +++ b/actionpack/lib/action_view/helpers/csrf_helper.rb @@ -17,10 +17,12 @@ module ActionView # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted, # so they do not use these tags. def csrf_meta_tags - <<-METAS.strip_heredoc.chomp.html_safe if protect_against_forgery? - <meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/> - <meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/> - METAS + if protect_against_forgery? + [ + tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token), + tag('meta', :name => 'csrf-token', :content => form_authenticity_token) + ].join("\n").html_safe + end end # For backwards compatibility. diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 18e303778c..506db24dc2 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -584,7 +584,7 @@ module ActionView # Works like update_page but wraps the generated JavaScript in a # <tt>\<script></tt> tag. Use this to include generated JavaScript in an - # ERb template. See JavaScriptGenerator for more information. + # ERB template. See JavaScriptGenerator for more information. # # +html_options+ may be a hash of <tt>\<script></tt> attributes to be # passed to ActionView::Helpers::JavaScriptHelper#javascript_tag. diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 2d3c5fe7e7..bdda1df437 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -303,7 +303,7 @@ module ActionView # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>. # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." def auto_link(text, *args, &block)#link = :all, html = {}, &block) - return ''.html_safe if text.blank? + return '' if text.blank? options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter unless args.empty? @@ -507,7 +507,7 @@ module ActionView end content_tag(:a, link_text, link_attributes.merge('href' => href), !!options[:sanitize]) + punctuation.reverse.join('') end - end.html_safe + end end # Turns all email addresses into clickable links. If a block is given, diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 2cd2dca711..de75488e72 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -81,9 +81,12 @@ module ActionView # # => /workshops # # <%= url_for(@workshop) %> - # # calls @workshop.to_s + # # calls @workshop.to_param which by default returns the id # # => /workshops/5 # + # # to_param can be re-defined in a model to provide different URL names: + # # => /workshops/1-workshop-name + # # <%= url_for("http://www.example.com") %> # # => http://www.example.com # @@ -183,7 +186,7 @@ module ActionView # link_to "Profiles", :controller => "profiles" # # => <a href="/profiles">Profiles</a> # - # You can use a block as well if your link target is hard to fit into the name parameter. ERb example: + # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: # # <%= link_to(@profile) do %> # <strong><%= @profile.name %></strong> -- <span>Check it out!</span> diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index a36837afc8..0443b1b0f5 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -55,7 +55,7 @@ module ActionView class ERB # Specify trim mode for the ERB compiler. Defaults to '-'. - # See ERb documentation for suitable values. + # See ERB documentation for suitable values. class_attribute :erb_trim_mode self.erb_trim_mode = '-' diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb new file mode 100644 index 0000000000..9b69a2648e --- /dev/null +++ b/actionpack/test/controller/flash_hash_test.rb @@ -0,0 +1,90 @@ +require 'abstract_unit' + +module ActionDispatch + class FlashHashTest < ActiveSupport::TestCase + def setup + @hash = Flash::FlashHash.new + end + + def test_set_get + @hash[:foo] = 'zomg' + assert_equal 'zomg', @hash[:foo] + end + + def test_keys + assert_equal [], @hash.keys + + @hash['foo'] = 'zomg' + assert_equal ['foo'], @hash.keys + + @hash['bar'] = 'zomg' + assert_equal ['foo', 'bar'].sort, @hash.keys.sort + end + + def test_update + @hash['foo'] = 'bar' + @hash.update('foo' => 'baz', 'hello' => 'world') + + assert_equal 'baz', @hash['foo'] + assert_equal 'world', @hash['hello'] + end + + def test_delete + @hash['foo'] = 'bar' + @hash.delete 'foo' + + assert !@hash.key?('foo') + assert_nil @hash['foo'] + end + + def test_to_hash + @hash['foo'] = 'bar' + assert_equal({'foo' => 'bar'}, @hash.to_hash) + + @hash.to_hash['zomg'] = 'aaron' + assert !@hash.key?('zomg') + assert_equal({'foo' => 'bar'}, @hash.to_hash) + end + + def test_empty? + assert @hash.empty? + @hash['zomg'] = 'bears' + assert !@hash.empty? + @hash.clear + assert @hash.empty? + end + + def test_each + @hash['hello'] = 'world' + @hash['foo'] = 'bar' + + things = [] + @hash.each do |k,v| + things << [k,v] + end + + assert_equal([%w{ hello world }, %w{ foo bar }].sort, things.sort) + end + + def test_replace + @hash['hello'] = 'world' + @hash.replace('omg' => 'aaron') + assert_equal({'omg' => 'aaron'}, @hash.to_hash) + end + + def test_discard_no_args + @hash['hello'] = 'world' + @hash.discard + @hash.sweep + assert_equal({}, @hash.to_hash) + end + + def test_discard_one_arg + @hash['hello'] = 'world' + @hash['omg'] = 'world' + @hash.discard 'hello' + @hash.sweep + assert_equal({'omg' => 'world'}, @hash.to_hash) + end + end +end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 3569a2f213..9c89f1334d 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -174,13 +174,13 @@ class FlashTest < ActionController::TestCase assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed assert_nil flash.discard(:unknown) # non existant key passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard()) # nothing passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil)) # nothing passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed assert_nil flash.keep(:unknown) # non existant key passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep()) # nothing passed - assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil)) # nothing passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed + assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed end def test_redirect_to_with_alert @@ -214,11 +214,20 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' class TestController < ActionController::Base + def dont_set_flash + head :ok + end + def set_flash flash["that"] = "hello" head :ok end + def set_flash_now + flash.now["that"] = "hello" + head :ok + end + def use_flash render :inline => "flash: #{flash["that"]}" end @@ -245,6 +254,47 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end + def test_setting_flash_raises_after_stream_back_to_client + with_test_route_set do + env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } + get '/set_flash', nil, env + assert_raise(ActionDispatch::ClosedError) { + @request.flash['alert'] = 'alert' + } + end + end + + def test_setting_flash_raises_after_stream_back_to_client_even_with_an_empty_flash + with_test_route_set do + env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } + get '/dont_set_flash', nil, env + assert_raise(ActionDispatch::ClosedError) { + @request.flash['alert'] = 'alert' + } + end + end + + def test_setting_flash_now_raises_after_stream_back_to_client + with_test_route_set do + env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } + get '/set_flash_now', nil, env + assert_raise(ActionDispatch::ClosedError) { + @request.flash.now['alert'] = 'alert' + } + end + end + + def test_setting_flash_now_raises_after_stream_back_to_client_even_with_an_empty_flash + with_test_route_set do + env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new } + get '/dont_set_flash', nil, env + assert_raise(ActionDispatch::ClosedError) { + @request.flash.now['alert'] = 'alert' + } + end + end + + private # Overwrite get to send SessionSecret in env hash diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index f0d62b0b13..01dc2f2091 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -521,4 +521,12 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest get '/foo' assert_raise(NameError) { missing_path } end + + test "process reuse the env we pass as argument" do + env = { :SERVER_NAME => 'server', 'action_dispatch.custom' => 'custom' } + get '/foo', nil, env + assert_equal :get, env[:method] + assert_equal 'server', env[:SERVER_NAME] + assert_equal 'custom', env['action_dispatch.custom'] + end end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index 5debf96232..be5866e5aa 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'controller/fake_models' require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/object/inclusion' class StarStarMimeController < ActionController::Base layout nil @@ -158,7 +159,7 @@ class RespondToController < ActionController::Base protected def set_layout - if ["all_types_with_layout", "iphone_with_html_response_type"].include?(action_name) + if action_name.among?("all_types_with_layout", "iphone_with_html_response_type") "respond_to/layouts/standard" elsif action_name == "iphone_with_html_response_type_without_layout" "respond_to/layouts/missing" @@ -558,6 +559,15 @@ class RespondWithController < ActionController::Base respond_with(resource, :location => "http://test.host/", :status => :created) end + def using_invalid_resource_with_template + respond_with(resource) + end + + def using_options_with_template + @customer = resource + respond_with(@customer, :status => 123, :location => "http://test.host/") + end + def using_resource_with_responder responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" } respond_with(resource, :responder => responder) @@ -953,6 +963,54 @@ class RespondWithControllerTest < ActionController::TestCase assert_equal 201, @response.status end + def test_using_resource_with_status_and_location_with_invalid_resource + errors = { :name => :invalid } + Customer.any_instance.stubs(:errors).returns(errors) + + @request.accept = "text/xml" + + post :using_resource_with_status_and_location + assert_equal errors.to_xml, @response.body + assert_equal 422, @response.status + assert_equal nil, @response.location + + put :using_resource_with_status_and_location + assert_equal errors.to_xml, @response.body + assert_equal 422, @response.status + assert_equal nil, @response.location + end + + def test_using_invalid_resource_with_template + errors = { :name => :invalid } + Customer.any_instance.stubs(:errors).returns(errors) + + @request.accept = "text/xml" + + post :using_invalid_resource_with_template + assert_equal errors.to_xml, @response.body + assert_equal 422, @response.status + assert_equal nil, @response.location + + put :using_invalid_resource_with_template + assert_equal errors.to_xml, @response.body + assert_equal 422, @response.status + assert_equal nil, @response.location + end + + def test_using_options_with_template + @request.accept = "text/xml" + + post :using_options_with_template + assert_equal "<customer-name>david</customer-name>", @response.body + assert_equal 123, @response.status + assert_equal "http://test.host/", @response.location + + put :using_options_with_template + assert_equal "<customer-name>david</customer-name>", @response.body + assert_equal 123, @response.status + assert_equal "http://test.host/", @response.location + end + def test_using_resource_with_responder get :using_resource_with_responder assert_equal "Resource name is david", @response.body diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb index 667a9021be..3bb3016fdb 100644 --- a/actionpack/test/controller/new_base/render_implicit_action_test.rb +++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb @@ -3,8 +3,9 @@ require 'abstract_unit' module RenderImplicitAction class SimpleController < ::ApplicationController self.view_paths = [ActionView::FixtureResolver.new( - "render_implicit_action/simple/hello_world.html.erb" => "Hello world!", - "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!" + "render_implicit_action/simple/hello_world.html.erb" => "Hello world!", + "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!", + "render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented" )] def hello_world() end @@ -25,9 +26,17 @@ module RenderImplicitAction assert_status 200 end + test "render an action called not_implemented" do + get "/render_implicit_action/simple/not_implemented" + + assert_body "Not Implemented" + assert_status 200 + end + test "action_method? returns true for implicit actions" do assert SimpleController.new.action_method?(:hello_world) assert SimpleController.new.action_method?(:"hyphen-ated") + assert SimpleController.new.action_method?(:not_implemented) end end end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index d520b5e512..31f4bf3a76 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -172,13 +172,11 @@ end class RequestForgeryProtectionControllerTest < ActionController::TestCase include RequestForgeryProtectionTests - test 'should emit a csrf-token meta tag' do + test 'should emit a csrf-param meta tag and a csrf-token meta tag' do ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?') get :meta - assert_equal <<-METAS.strip_heredoc.chomp, @response.body - <meta name="csrf-param" content="authenticity_token"/> - <meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/> - METAS + assert_select 'meta[name=?][content=?]', 'csrf-param', 'authenticity_token' + assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae<=?' end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index acb4617a60..6ea492cf8b 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/with_options' +require 'active_support/core_ext/object/inclusion' class ResourcesController < ActionController::Base def index() render :nothing => true end @@ -1292,7 +1293,7 @@ class ResourcesTest < ActionController::TestCase def assert_resource_methods(expected, resource, action_method, method) assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}" expected.each do |action| - assert resource.send("#{action_method}_methods")[method].include?(action), + assert action.in?(resource.send("#{action_method}_methods")[method]) "#{method} not in #{action_method} methods: #{resource.send("#{action_method}_methods")[method].inspect}" end end @@ -1329,9 +1330,9 @@ class ResourcesTest < ActionController::TestCase options = options.merge(:action => action.to_s) path_options = { :path => path, :method => method } - if Array(allowed).include?(action) + if action.in?(Array(allowed)) assert_recognizes options, path_options - elsif Array(not_allowed).include?(action) + elsif action.in?(Array(not_allowed)) assert_not_recognizes options, path_options end end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 39159fd629..ebc16694db 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -495,3 +495,99 @@ class CookiesTest < ActionController::TestCase end end end + +class CookiesIntegrationTest < ActionDispatch::IntegrationTest + SessionKey = '_myapp_session' + SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' + + class TestController < ActionController::Base + def dont_set_cookies + head :ok + end + + def set_cookies + cookies["that"] = "hello" + head :ok + end + end + + def test_setting_cookies_raises_after_stream_back_to_client + with_test_route_set do + get '/set_cookies' + assert_raise(ActionDispatch::ClosedError) { + request.cookie_jar['alert'] = 'alert' + cookies['alert'] = 'alert' + } + end + end + + def test_setting_cookies_raises_after_stream_back_to_client_even_without_cookies + with_test_route_set do + get '/dont_set_cookies' + assert_raise(ActionDispatch::ClosedError) { + request.cookie_jar['alert'] = 'alert' + } + end + end + + def test_setting_permanent_cookies_raises_after_stream_back_to_client + with_test_route_set do + get '/set_cookies' + assert_raise(ActionDispatch::ClosedError) { + request.cookie_jar.permanent['alert'] = 'alert' + cookies['alert'] = 'alert' + } + end + end + + def test_setting_permanent_cookies_raises_after_stream_back_to_client_even_without_cookies + with_test_route_set do + get '/dont_set_cookies' + assert_raise(ActionDispatch::ClosedError) { + request.cookie_jar.permanent['alert'] = 'alert' + } + end + end + + def test_setting_signed_cookies_raises_after_stream_back_to_client + with_test_route_set do + get '/set_cookies' + assert_raise(ActionDispatch::ClosedError) { + request.cookie_jar.signed['alert'] = 'alert' + cookies['alert'] = 'alert' + } + end + end + + def test_setting_signed_cookies_raises_after_stream_back_to_client_even_without_cookies + with_test_route_set do + get '/dont_set_cookies' + assert_raise(ActionDispatch::ClosedError) { + request.cookie_jar.signed['alert'] = 'alert' + } + end + end + + private + + # Overwrite get to send SessionSecret in env hash + def get(path, parameters = nil, env = {}) + env["action_dispatch.secret_token"] ||= SessionSecret + super + end + + def with_test_route_set + with_routing do |set| + set.draw do + match ':action', :to => CookiesIntegrationTest::TestController + end + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::Cookies + middleware.delete "ActionDispatch::ShowExceptions" + end + + yield + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 5e5758a60e..cf7a5aaa3b 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1,6 +1,7 @@ require 'erb' require 'abstract_unit' require 'controller/fake_controllers' +require 'active_support/core_ext/object/inclusion' class TestRoutingMapper < ActionDispatch::IntegrationTest SprocketsApp = lambda { |env| @@ -495,7 +496,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :todos, :id => /\d+/ end - scope '/countries/:country', :constraints => lambda { |params, req| %[all France].include?(params[:country]) } do + scope '/countries/:country', :constraints => lambda { |params, req| params[:country].among?("all", "France") } do match '/', :to => 'countries#index' match '/cities', :to => 'countries#cities' end diff --git a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb new file mode 100644 index 0000000000..bf5869ed22 --- /dev/null +++ b/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb @@ -0,0 +1 @@ +<content>I should not be displayed</content>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb new file mode 100644 index 0000000000..b313017913 --- /dev/null +++ b/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb @@ -0,0 +1 @@ +<customer-name><%= @customer.name %></customer-name>
\ No newline at end of file diff --git a/actionpack/test/template/erb_util_test.rb b/actionpack/test/template/erb_util_test.rb index d1891094e8..30f6d1a213 100644 --- a/actionpack/test/template/erb_util_test.rb +++ b/actionpack/test/template/erb_util_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/core_ext/object/inclusion' class ErbUtilTest < Test::Unit::TestCase include ERB::Util @@ -29,7 +30,7 @@ class ErbUtilTest < Test::Unit::TestCase def test_rest_in_ascii (0..127).to_a.map {|int| int.chr }.each do |chr| - next if %w(& " < >).include?(chr) + next if chr.in?('&"<>') assert_equal chr, html_escape(chr) end end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index ff183d097d..435ce81238 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'controller/fake_models' +require 'active_support/core_ext/object/inclusion' class FormHelperTest < ActionView::TestCase tests ActionView::Helpers::FormHelper @@ -1743,7 +1744,7 @@ class FormHelperTest < ActionView::TestCase def snowman(method = nil) txt = %{<div style="margin:0;padding:0;display:inline">} txt << %{<input name="utf8" type="hidden" value="✓" />} - if (method && !['get','post'].include?(method.to_s)) + if method && !method.to_s.among?('get', 'post') txt << %{<input name="_method" type="hidden" value="#{method}" />} end txt << %{</div>} diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 93ff7ba0fd..76236aee41 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'tzinfo' +require 'active_support/core_ext/object/inclusion' class Map < Hash def category @@ -82,7 +83,7 @@ class FormOptionsHelperTest < ActionView::TestCase def test_collection_options_with_proc_for_disabled assert_dom_equal( "<option value=\"<Abe>\"><Abe> went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>", - options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda{|p| %w(Babe Cabe).include? p.author_name }) + options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda{|p| p.author_name.among?("Babe", "Cabe") }) ) end diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index f8671f2980..aa2114ff79 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/core_ext/object/inclusion' class FormTagHelperTest < ActionView::TestCase tests ActionView::Helpers::FormTagHelper @@ -13,7 +14,7 @@ class FormTagHelperTest < ActionView::TestCase txt = %{<div style="margin:0;padding:0;display:inline">} txt << %{<input name="utf8" type="hidden" value="✓" />} - if (method && !['get','post'].include?(method.to_s)) + if method && !method.to_s.among?('get','post') txt << %{<input name="_method" type="hidden" value="#{method}" />} end txt << %{</div>} diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index d0d4286393..a4fcff5167 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -315,14 +315,20 @@ class TextHelperTest < ActionView::TestCase end end - def test_auto_link_should_be_html_safe + def test_auto_link_should_not_be_html_safe email_raw = 'santiago@wyeworks.com' link_raw = 'http://www.rubyonrails.org' - assert auto_link(nil).html_safe? - assert auto_link('').html_safe? - assert auto_link("#{link_raw} #{link_raw} #{link_raw}").html_safe? - assert auto_link("hello #{email_raw}").html_safe? + assert !auto_link(nil).html_safe?, 'should not be html safe' + assert !auto_link('').html_safe?, 'should not be html safe' + assert !auto_link("#{link_raw} #{link_raw} #{link_raw}").html_safe?, 'should not be html safe' + assert !auto_link("hello #{email_raw}").html_safe?, 'should not be html safe' + end + + def test_auto_link_email_address + email_raw = 'aaron@tenderlovemaking.com' + email_result = %{<a href="mailto:#{email_raw}">#{email_raw}</a>} + assert !auto_link_email_addresses(email_result).html_safe?, 'should not be html safe' end def test_auto_link diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 3082c7186a..03287fac7a 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,5 +1,14 @@ *Rails 3.1.0 (unreleased)* +* Add support for proc or lambda as an option for InclusionValidator, + ExclusionValidator, and FormatValidator [Prem Sichanugrist] + + You can now supply Proc, lambda, or anything that respond to #call in those + validations, and it will be called with current record as an argument. + That given proc or lambda must returns an object which respond to #include? for + InclusionValidator and ExclusionValidator, and returns a regular expression + object for FormatValidator. + * Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH] * ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov] diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index fec9c7ff8b..9f80673bb8 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -17,8 +17,6 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' - s.has_rdoc = true - s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') s.add_dependency('i18n', '~> 0.5.0') diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index a479795d51..5ede78617a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -8,7 +8,7 @@ module ActiveModel # Provides a way to track changes in your object in the same way as # Active Record does. # - # The requirements to implement ActiveModel::Dirty are to: + # The requirements for implementing ActiveModel::Dirty are: # # * <tt>include ActiveModel::Dirty</tt> in your object # * Call <tt>define_attribute_methods</tt> passing each method you want to diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 5e3cf510b0..22ca3efa2b 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -278,19 +278,18 @@ module ActiveModel # When using inheritance in your models, it will check all the inherited # models too, but only if the model itself hasn't been found. Say you have # <tt>class Admin < User; end</tt> and you wanted the translation for - # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, + # the <tt>:blank</tt> error message for the <tt>title</tt> attribute, # it looks for these translations: # - # <ol> - # <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li> - # <li><tt>activemodel.errors.models.admin.blank</tt></li> - # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li> - # <li><tt>activemodel.errors.models.user.blank</tt></li> - # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li> - # <li><tt>activemodel.errors.messages.blank</tt></li> - # <li><tt>errors.attributes.title.blank</tt></li> - # <li><tt>errors.messages.blank</tt></li> - # </ol> + # * <tt>activemodel.errors.models.admin.attributes.title.blank</tt> + # * <tt>activemodel.errors.models.admin.blank</tt> + # * <tt>activemodel.errors.models.user.attributes.title.blank</tt> + # * <tt>activemodel.errors.models.user.blank</tt> + # * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope) + # * <tt>activemodel.errors.messages.blank</tt> + # * <tt>errors.attributes.title.blank</tt> + # * <tt>errors.messages.blank</tt> + # def generate_message(attribute, type = :invalid, options = {}) type = options.delete(:message) if options[:message].is_a?(Symbol) diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index e38e565d09..a85c23f725 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,18 +1,35 @@ +require 'active_support/core_ext/range.rb' + module ActiveModel # == Active Model Exclusion Validator module Validations class ExclusionValidator < EachValidator + ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << + "and must be supplied as the :in option of the configuration hash" + def check_validity! - raise ArgumentError, "An object with the method include? is required must be supplied as the " << - ":in option of the configuration hash" unless options[:in].respond_to?(:include?) + unless [:include?, :call].any? { |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end end def validate_each(record, attribute, value) - if options[:in].include?(value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + if exclusions.send(inclusion_method(exclusions), value) record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value)) end end + + private + + # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the + # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> + # uses the previous logic of comparing a value with the range endpoints. + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? + end end module HelperMethods @@ -22,10 +39,14 @@ module ActiveModel # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed" + # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, :message => "should not be the same as your username or first name" # end # # Configuration options: # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. + # This can be supplied as a proc or lambda which returns an enumerable. If the enumerable + # is a range the test is performed with <tt>Range#cover?</tt> + # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 541f53a834..6f23d492eb 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -4,10 +4,12 @@ module ActiveModel module Validations class FormatValidator < EachValidator def validate_each(record, attribute, value) - if options[:with] && value.to_s !~ options[:with] - record.errors.add(attribute, :invalid, options.except(:with).merge!(:value => value)) - elsif options[:without] && value.to_s =~ options[:without] - record.errors.add(attribute, :invalid, options.except(:without).merge!(:value => value)) + if options[:with] + regexp = option_call(record, :with) + record_error(record, attribute, :with, value) if value.to_s !~ regexp + elsif options[:without] + regexp = option_call(record, :without) + record_error(record, attribute, :without, value) if value.to_s =~ regexp end end @@ -16,12 +18,25 @@ module ActiveModel raise ArgumentError, "Either :with or :without must be supplied (but not both)" end - if options[:with] && !options[:with].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" - end + check_options_validity(options, :with) + check_options_validity(options, :without) + end + + private - if options[:without] && !options[:without].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" + def option_call(record, name) + option = options[name] + option.respond_to?(:call) ? option.call(record) : option + end + + def record_error(record, attribute, name, value) + record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value)) + end + + def check_options_validity(options, name) + option = options[name] + if option && !option.is_a?(Regexp) && !option.respond_to?(:call) + raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" end end end @@ -40,17 +55,26 @@ module ActiveModel # validates_format_of :email, :without => /NOSPAM/ # end # + # You can also provide a proc or lambda which will determine the regular expression that will be used to validate the attribute + # + # class Person < ActiveRecord::Base + # # Admin can have number as a first letter in their screen name + # validates_format_of :screen_name, :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i } + # end + # # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # - # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression, - # or else an exception will be raised. + # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression + # or a proc or lambda, or else an exception will be raised. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation. + # This can be provided as a proc or lambda returning regular expression which will be called at runtime. # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. + # This can be provided as a proc or lambda returning regular expression which will be called at runtime. # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 92ac940f36..d32aebeb88 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -5,20 +5,30 @@ module ActiveModel # == Active Model Inclusion Validator module Validations class InclusionValidator < EachValidator + ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << + "and must be supplied as the :in option of the configuration hash" + def check_validity! - raise ArgumentError, "An object with the method include? is required must be supplied as the " << - ":in option of the configuration hash" unless options[:in].respond_to?(:include?) + unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end end def validate_each(record, attribute, value) - record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) unless options[:in].send(include?, value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + unless exclusions.send(inclusion_method(exclusions), value) + record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) + end end + private + # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> # uses the previous logic of comparing a value with the range endpoints. - def include? - options[:in].is_a?(Range) ? :cover? : :include? + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? end end @@ -29,11 +39,13 @@ module ActiveModel # validates_inclusion_of :gender, :in => %w( m f ) # validates_inclusion_of :age, :in => 0..99 # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list" + # validates_inclusion_of :states, :in => lambda{ |person| STATES[person.country] } # end # # Configuration options: - # * <tt>:in</tt> - An enumerable object of available items. - # If the enumerable is a range the test is performed with <tt>Range#cover?</tt> + # * <tt>:in</tt> - An enumerable object of available items. This can be + # supplied as a proc or lambda which returns an enumerable. If the enumerable + # is a range the test is performed with <tt>Range#cover?</tt> # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index c5ed8d22d3..5f7f7a6ad6 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/array/wrap' require "active_support/core_ext/module/anonymous" require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/inclusion' module ActiveModel #:nodoc: @@ -67,7 +68,7 @@ module ActiveModel #:nodoc: # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) - # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value) + # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless value.among?('Mr.', 'Mrs.', 'Dr.') # end # end # diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 015153ec7c..9a73a5ad91 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'logger' +require 'active_support/core_ext/object/inclusion' class SanitizerTest < ActiveModel::TestCase @@ -9,7 +10,7 @@ class SanitizerTest < ActiveModel::TestCase attr_accessor :logger def deny?(key) - [ 'admin' ].include?(key) + key.in?(['admin']) end end diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 500a5c575f..500a5c575f 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index b6a2f88667..b6a2f88667 100644 --- a/activemodel/test/cases/serializeration/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index be9d98d644..72a383f128 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -42,4 +42,16 @@ class ExclusionValidationTest < ActiveModel::TestCase ensure Person.reset_callbacks(:validate) end + + def test_validates_exclusion_of_with_lambda + Topic.validates_exclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) } + + p = Topic.new + p.title = "elephant" + p.author_name = "sikachu" + assert p.invalid? + + p.title = "wasabi" + assert p.valid? + end end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 6c4fb36d52..73647efea5 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -98,6 +98,30 @@ class PresenceValidationTest < ActiveModel::TestCase assert_raise(ArgumentError) { Topic.validates_format_of(:title, :without => "clearly not a regexp") } end + def test_validates_format_of_with_lambda + Topic.validates_format_of :content, :with => lambda{ |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ } + + p = Topic.new + p.title = "digit" + p.content = "Pixies" + assert p.invalid? + + p.content = "1234" + assert p.valid? + end + + def test_validates_format_of_without_lambda + Topic.validates_format_of :content, :without => lambda{ |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ } + + p = Topic.new + p.title = "characters" + p.content = "1234" + assert p.invalid? + + p.content = "Pixies" + assert p.valid? + end + def test_validates_format_of_for_ruby_class Person.validates_format_of :karma, :with => /\A\d+\Z/ diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 62f2ec785d..92c473de0e 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -74,4 +74,16 @@ class InclusionValidationTest < ActiveModel::TestCase ensure Person.reset_callbacks(:validate) end + + def test_validates_inclusion_of_with_lambda + Topic.validates_inclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) } + + p = Topic.new + p.title = "wasabi" + p.author_name = "sikachu" + assert p.invalid? + + p.title = "elephant" + assert p.valid? + end end diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e536d2b408..93eb42a52c 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,57 @@ *Rails 3.1.0 (unreleased)* +* Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your + scope to be lazily evaluated, or takes parameters, please define it as a normal class method + instead. For example, change this: + + class Post < ActiveRecord::Base + scope :unpublished, lambda { where('published_at > ?', Time.now) } + end + + To this: + + class Post < ActiveRecord::Base + def self.unpublished + where('published_at > ?', Time.now) + end + end + + [Jon Leighton] + +* Default scopes are now evaluated at the latest possible moment, to avoid problems where + scopes would be created which would implicitly contain the default scope, which would then + be impossible to get rid of via Model.unscoped. + + Note that this means that if you are inspecting the internal structure of an + ActiveRecord::Relation, it will *not* contain the default scope, though the resulting + query will do. You can get a relation containing the default scope by calling + ActiveRecord#with_default_scope, though this is not part of the public API. + + [Jon Leighton] + +* Deprecated support for passing hashes and relations to 'default_scope'. Please create a class + method for your scope instead. For example, change this: + + class Post < ActiveRecord::Base + default_scope where(:published => true) + end + + To this: + + class Post < ActiveRecord::Base + def self.default_scope + where(:published => true) + end + end + + Rationale: It will make the implementation simpler because we can simply use inheritance to + handle inheritance scenarios, rather than trying to make up our own rules about what should + happen when you call default_scope multiple times or in subclasses. + + [Jon Leighton] + +* PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. + * ConnectionManagement middleware is changed to clean up the connection pool after the rack body has been flushed. diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index b1df24844a..c3cd76a714 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -17,7 +17,6 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*'] s.require_path = 'lib' - s.has_rdoc = true s.extra_rdoc_files = %w( README.rdoc ) s.rdoc_options.concat ['--main', 'README.rdoc'] diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 27c446b12c..66f6477ec5 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/object/inclusion' module ActiveRecord module Associations @@ -163,7 +164,7 @@ module ActiveRecord def creation_attributes attributes = {} - if [:has_one, :has_many].include?(reflection.macro) && !options[:through] + if reflection.macro.among?(:has_one, :has_many) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 964e7fddc8..763b12f708 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: self.macro = :belongs_to @@ -65,7 +67,7 @@ module ActiveRecord::Associations::Builder def configure_dependency if options[:dependent] - unless [:destroy, :delete].include?(options[:dependent]) + unless options[:dependent].among?(:destroy, :delete) raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 77bb66228d..67ce339380 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: self.macro = :has_many @@ -14,7 +16,7 @@ module ActiveRecord::Associations::Builder def configure_dependency if options[:dependent] - unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent]) + unless options[:dependent].among?(:destroy, :delete_all, :nullify, :restrict) raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ ":nullify or :restrict (#{options[:dependent].inspect})" end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 07ba5d088e..18d7117bb7 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: self.macro = :has_one @@ -27,7 +29,7 @@ module ActiveRecord::Associations::Builder def configure_dependency if options[:dependent] - unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent]) + unless options[:dependent].among?(:destroy, :delete, :nullify, :restrict) raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ ":nullify or :restrict (#{options[:dependent].inspect})" end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 9f4fc44cc6..33a184d48d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -21,14 +21,7 @@ module ActiveRecord attr_reader :proxy def initialize(owner, reflection) - # When scopes are created via method_missing on the proxy, they are stored so that - # any records fetched from the database are kept around for future use. - @scopes_cache = Hash.new do |hash, method| - hash[method] = { } - end - super - @proxy = CollectionProxy.new(self) end @@ -74,7 +67,6 @@ module ActiveRecord def reset @loaded = false @target = [] - @scopes_cache.clear end def select(select = nil) @@ -327,10 +319,6 @@ module ActiveRecord end end - def cached_scope(method, args) - @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) - end - def load_target if find_target? targets = [] diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index cf77d770c9..388173c1fb 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -82,8 +82,6 @@ module ActiveRecord end end - elsif @association.klass.scopes[method] - @association.cached_scope(method, args) else scoped.readonly(nil).send(method, *args, &block) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 1d2e8667e4..aaa8b97389 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord # = Active Record Belongs To Has One Association module Associations @@ -50,7 +52,7 @@ module ActiveRecord end def remove_target!(method) - if [:delete, :destroy].include?(method) + if method.among?(:delete, :destroy) target.send(method) else nullify_owner_attributes(target) diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 6aac96df6f..67b0e77a25 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/object/inclusion' module ActiveRecord module AttributeMethods @@ -58,7 +59,7 @@ module ActiveRecord private def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.among?(:datetime, :timestamp) end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index fe81c7dc2f..08a41e2d8b 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -425,8 +425,8 @@ module ActiveRecord #:nodoc: self.store_full_sti_class = true # Stores the default scope for the class - class_attribute :default_scoping, :instance_writer => false - self.default_scoping = [] + class_attribute :default_scopes, :instance_writer => false + self.default_scopes = [] # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. @@ -870,7 +870,9 @@ module ActiveRecord #:nodoc: # Returns a scope for this class without taking into account the default_scope. # # class Post < ActiveRecord::Base - # default_scope :published => true + # def self.default_scope + # where :published => true + # end # end # # Post.all # Fires "SELECT * FROM posts WHERE published = true" @@ -892,13 +894,8 @@ module ActiveRecord #:nodoc: block_given? ? relation.scoping { yield } : relation end - def scoped_methods #:nodoc: - key = :"#{self}_scoped_methods" - Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup - end - def before_remove_const #:nodoc: - reset_scoped_methods + self.current_scope = nil end # Specifies how the record is loaded by +Marshal+. @@ -1020,7 +1017,7 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) if match.finder? options = arguments.extract_options! - relation = options.any? ? construct_finder_arel(options, current_scoped_methods) : scoped + relation = options.any? ? scoped(options) : scoped relation.send :find_by_attributes, match, attribute_names, *arguments elsif match.instantiator? scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block @@ -1109,43 +1106,47 @@ module ActiveRecord #:nodoc: # end # # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(method_scoping = {}, action = :merge, &block) - method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) + def with_scope(scope = {}, action = :merge, &block) + # If another Active Record class has been passed in, get its current scope + scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) + + previous_scope = self.current_scope - if method_scoping.is_a?(Hash) + if scope.is_a?(Hash) # Dup first and second level of hash (method and params). - method_scoping = method_scoping.dup - method_scoping.each do |method, params| - method_scoping[method] = params.dup unless params == true + scope = scope.dup + scope.each do |method, params| + scope[method] = params.dup unless params == true end - method_scoping.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(method_scoping[:find] || {}) + scope.assert_valid_keys([ :find, :create ]) + relation = construct_finder_arel(scope[:find] || {}) + relation.default_scoped = true unless action == :overwrite - if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create] + if previous_scope && previous_scope.create_with_value && scope[:create] scope_for_create = if action == :merge - current_scoped_methods.create_with_value.merge(method_scoping[:create]) + previous_scope.create_with_value.merge(scope[:create]) else - method_scoping[:create] + scope[:create] end relation = relation.create_with(scope_for_create) else - scope_for_create = method_scoping[:create] - scope_for_create ||= current_scoped_methods.create_with_value if current_scoped_methods + scope_for_create = scope[:create] + scope_for_create ||= previous_scope.create_with_value if previous_scope relation = relation.create_with(scope_for_create) if scope_for_create end - method_scoping = relation + scope = relation end - method_scoping = current_scoped_methods.merge(method_scoping) if current_scoped_methods && action == :merge + scope = previous_scope.merge(scope) if previous_scope && action == :merge - self.scoped_methods << method_scoping + self.current_scope = scope begin yield ensure - self.scoped_methods.pop + self.current_scope = previous_scope end end @@ -1168,39 +1169,80 @@ MSG with_scope(method_scoping, :overwrite, &block) end - # Sets the default options for the model. The format of the - # <tt>options</tt> argument is the same as in find. + def current_scope #:nodoc: + Thread.current[:"#{self}_current_scope"] + end + + def current_scope=(scope) #:nodoc: + Thread.current[:"#{self}_current_scope"] = scope + end + + # Implement this method in your model to set a default scope for all operations on + # the model. # # class Person < ActiveRecord::Base - # default_scope order('last_name, first_name') + # def self.default_scope + # order('last_name, first_name') + # end # end # - # <tt>default_scope</tt> is also applied while creating/building a record. It is not + # Person.all # => SELECT * FROM people ORDER BY last_name, first_name + # + # The <tt>default_scope</tt> is also applied while creating/building a record. It is not # applied while updating a record. # # class Article < ActiveRecord::Base - # default_scope where(:published => true) + # def self.default_scope + # where(:published => true) + # end # end # # Article.new.published # => true # Article.create.published # => true - def default_scope(options = {}) - reset_scoped_methods - default_scoping = self.default_scoping.dup - self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop) - end + # + # === Deprecation warning + # + # There is an alternative syntax as follows: + # + # class Person < ActiveRecord::Base + # default_scope order('last_name, first_name') + # end + # + # This is now deprecated and will be removed in Rails 3.2. + def default_scope(scope = {}) + ActiveSupport::Deprecation.warn <<-WARN +Passing a hash or scope to default_scope is deprecated and will be removed in Rails 3.2. You should create a class method for your scope instead. For example, change this: - def current_scoped_methods #:nodoc: - method = scoped_methods.last - if method.respond_to?(:call) - relation.scoping { method.call } - else - method - end +class Post < ActiveRecord::Base + default_scope where(:published => true) +end + +To this: + +class Post < ActiveRecord::Base + def self.default_scope + where(:published => true) + end +end +WARN + + self.default_scopes = default_scopes.dup << scope end - def reset_scoped_methods #:nodoc: - Thread.current[:"#{self}_scoped_methods"] = nil + def build_default_scope #:nodoc: + if method(:default_scope).owner != Base.singleton_class + # Use relation.scoping to ensure we ignore whatever the current value of + # self.current_scope may be. + relation.scoping { default_scope } + elsif default_scopes.any? + default_scopes.inject(relation) do |default_scope, scope| + if scope.is_a?(Hash) + default_scope.apply_finder_options(scope) + else + default_scope.merge(scope) + end + end + end end # Returns the class type of the record using the current module as a prefix. So descendants of @@ -1916,11 +1958,8 @@ MSG end def populate_with_current_scope_attributes - if scope = self.class.send(:current_scoped_methods) - create_with = scope.scope_for_create - create_with.each { |att,value| - respond_to?("#{att}=") && send("#{att}=", value) - } + self.class.scoped.scope_for_create.each do |att,value| + respond_to?("#{att}=") && send("#{att}=", value) end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 86d58df99b..a175bf003c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -214,6 +214,24 @@ module ActiveRecord # needs to be aware of it because an ordinary +save+ will raise such exception # instead of quietly returning +false+. # + # == Debugging callbacks + # + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property + # defines what part of the chain the callback runs in. + # + # To find all callbacks in the before_save callback chain: + # + # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) } + # + # Returns an array of callback objects that form the before_save chain. + # + # To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object: + # + # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead) + # + # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. + # module Callbacks extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0f44baa2fe..b0e6136e12 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -203,6 +203,10 @@ module ActiveRecord def release_savepoint end + def case_sensitive_modifier(node) + node + end + def current_savepoint_name "active_record_#{open_transactions}" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 7bad511c64..1f7e527eeb 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -528,6 +528,11 @@ module ActiveRecord def case_sensitive_equality_operator "= BINARY" end + deprecate :case_sensitive_equality_operator + + def case_sensitive_modifier(node) + Arel::Nodes::Bin.new(node) + end def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) where_sql @@ -587,8 +592,26 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) - execute(sql, name).each(:as => :hash) + def select(sql, name = nil, binds = []) + exec_query(sql, name, binds).to_a + end + + def exec_query(sql, name = 'SQL', binds = []) + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + + log(sql, name, binds) do + begin + result = @connection.query(sql) + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + ActiveRecord::Result.new(result.fields, result.to_a) + end end def supports_views? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e1186209d3..d88075fdea 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -196,6 +196,7 @@ module ActiveRecord @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} @statements = {} + @client_encoding = nil connect end @@ -330,6 +331,63 @@ module ActiveRecord @statements.clear end + if "<3".respond_to?(:encode) + # Taken from here: + # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb + # Author: TOMITA Masahiro <tommy@tmtm.org> + ENCODINGS = { + "armscii8" => nil, + "ascii" => Encoding::US_ASCII, + "big5" => Encoding::Big5, + "binary" => Encoding::ASCII_8BIT, + "cp1250" => Encoding::Windows_1250, + "cp1251" => Encoding::Windows_1251, + "cp1256" => Encoding::Windows_1256, + "cp1257" => Encoding::Windows_1257, + "cp850" => Encoding::CP850, + "cp852" => Encoding::CP852, + "cp866" => Encoding::IBM866, + "cp932" => Encoding::Windows_31J, + "dec8" => nil, + "eucjpms" => Encoding::EucJP_ms, + "euckr" => Encoding::EUC_KR, + "gb2312" => Encoding::EUC_CN, + "gbk" => Encoding::GBK, + "geostd8" => nil, + "greek" => Encoding::ISO_8859_7, + "hebrew" => Encoding::ISO_8859_8, + "hp8" => nil, + "keybcs2" => nil, + "koi8r" => Encoding::KOI8_R, + "koi8u" => Encoding::KOI8_U, + "latin1" => Encoding::ISO_8859_1, + "latin2" => Encoding::ISO_8859_2, + "latin5" => Encoding::ISO_8859_9, + "latin7" => Encoding::ISO_8859_13, + "macce" => Encoding::MacCentEuro, + "macroman" => Encoding::MacRoman, + "sjis" => Encoding::SHIFT_JIS, + "swe7" => nil, + "tis620" => Encoding::TIS_620, + "ucs2" => Encoding::UTF_16BE, + "ujis" => Encoding::EucJP_ms, + "utf8" => Encoding::UTF_8, + "utf8mb4" => Encoding::UTF_8, + } + else + ENCODINGS = Hash.new { |h,k| h[k] = k } + end + + # Get the client encoding for this database + def client_encoding + return @client_encoding if @client_encoding + + result = exec_query( + "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", + 'SCHEMA') + @client_encoding = ENCODINGS[result.rows.last.last] + end + def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do result = nil @@ -363,6 +421,10 @@ module ActiveRecord end end + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + def exec_without_stmt(sql, name = 'SQL') # :nodoc: # Some queries, like SHOW CREATE TABLE don't work through the prepared # statement API. For those queries, we need to use this method. :'( @@ -506,7 +568,7 @@ module ActiveRecord def tables(name = nil, database = nil) #:nodoc: tables = [] - result = execute(["SHOW TABLES", database].compact.join(' IN '), name) + result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA') result.each { |field| tables << field[0] } result.free tables @@ -551,7 +613,7 @@ module ActiveRecord def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql) + result = execute(sql, 'SCHEMA') result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } result.free columns @@ -638,7 +700,7 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) #:nodoc: keys = [] - result = execute("describe #{quote_table_name(table)}") + result = execute("describe #{quote_table_name(table)}", 'SCHEMA') result.each_hash do |h| keys << h["Field"]if h["Key"] == "PRI" end @@ -655,6 +717,11 @@ module ActiveRecord def case_sensitive_equality_operator "= BINARY" end + deprecate :case_sensitive_equality_operator + + def case_sensitive_modifier(node) + Arel::Nodes::Bin.new(node) + end def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) where_sql diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5a830a50fb..1c2cc7d5fa 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -229,7 +229,12 @@ module ActiveRecord @statements = {} connect - @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] + + if postgresql_version < 80200 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" + end + + @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end def clear_cache! @@ -293,13 +298,13 @@ module ActiveRecord # Enable standard-conforming strings if available. def set_standard_conforming_strings old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on') rescue nil + execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil ensure self.client_min_messages = old end def supports_insert_with_returning? - postgresql_version >= 80200 + true end def supports_ddl_transactions? @@ -310,10 +315,9 @@ module ActiveRecord true end - # Returns the configured supported identifier length supported by PostgreSQL, - # or report the default of 63 on PostgreSQL 7.x. + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63) + @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i end # QUOTING ================================================== @@ -404,7 +408,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== def supports_disable_referential_integrity?() #:nodoc: - postgresql_version >= 80100 + true end def disable_referential_integrity #:nodoc: @@ -427,34 +431,16 @@ module ActiveRecord end # Executes an INSERT query and returns the new record's ID - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # Extract the table from the insert sql. Yuck. - table = sql.split(" ", 4)[2].gsub('"', '') - - # Try an insert with 'returning id' if available (PG >= 8.2) - if supports_insert_with_returning? - pk, sequence_name = *pk_and_sequence_for(table) unless pk - if pk - id = select_value("#{sql} RETURNING #{quote_column_name(pk)}") - clear_query_cache - return id - end - end + _, table = extract_schema_and_table(sql.split(" ", 4)[2]) - # Otherwise, insert then grab last_insert_id. - if insert_id = super - insert_id - else - # If neither pk nor sequence name is given, look them up. - unless pk || sequence_name - pk, sequence_name = *pk_and_sequence_for(table) - end + pk ||= primary_key(table) - # If a pk is given, fallback to default sequence name. - # Don't fetch last insert id for a table without a pk. - if pk && sequence_name ||= default_sequence_name(table, pk) - last_insert_id(sequence_name) - end + if pk + select_value("#{sql} RETURNING #{quote_column_name(pk)}") + else + super end end alias :create :insert @@ -554,6 +540,10 @@ module ActiveRecord end end + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -632,20 +622,12 @@ module ActiveRecord # Example: # drop_database 'matt_development' def drop_database(name) #:nodoc: - if postgresql_version >= 80200 - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" - else - begin - execute "DROP DATABASE #{quote_table_name(name)}" - rescue ActiveRecord::StatementInvalid - @logger.warn "#{name} database doesn't exist." if @logger - end - end + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end # Returns the list of all tables in the schema search path or a specified schema. def tables(name = nil) - query(<<-SQL, name).map { |row| row[0] } + query(<<-SQL, 'SCHEMA').map { |row| row[0] } SELECT tablename FROM pg_tables WHERE schemaname = ANY (current_schemas(false)) @@ -653,7 +635,21 @@ module ActiveRecord end def table_exists?(name) - name = name.to_s + schema, table = extract_schema_and_table(name.to_s) + + binds = [[nil, table.gsub(/(^"|"$)/,'')]] + binds << [nil, schema] if schema + + exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_tables + WHERE tablename = $1 + #{schema ? "AND schemaname = $2" : ''} + SQL + end + + # Extracts the table and schema name from +name+ + def extract_schema_and_table(name) schema, table = name.split('.', 2) unless table # A table was provided without a schema @@ -665,13 +661,7 @@ module ActiveRecord table = name schema = nil end - - query(<<-SQL).first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_tables - WHERE tablename = '#{table.gsub(/(^"|"$)/,'')}' - #{schema ? "AND schemaname = '#{schema}'" : ''} - SQL + [schema, table] end # Returns the list of all indexes for a table. @@ -748,37 +738,47 @@ module ActiveRecord # Returns the current client message level. def client_min_messages - query('SHOW client_min_messages')[0][0] + query('SHOW client_min_messages', 'SCHEMA')[0][0] end # Set the client message level. def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'") + execute("SET client_min_messages TO '#{level}'", 'SCHEMA') end # Returns the sequence name for a table's primary key or some other specified key. def default_sequence_name(table_name, pk = nil) #:nodoc: - default_pk, default_seq = pk_and_sequence_for(table_name) - default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq" + serial_sequence(table_name, pk || 'id').split('.').last + rescue ActiveRecord::StatementInvalid + "#{table_name}_#{pk || 'id'}_seq" + end + + def serial_sequence(table, column) + result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]]) + SELECT pg_get_serial_sequence($1, $2) + eosql + result.rows.first.first end # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: unless pk and sequence default_pk, default_sequence = pk_and_sequence_for(table) + pk ||= default_pk sequence ||= default_sequence end - if pk - if sequence - quoted_sequence = quote_column_name(sequence) - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) - end_sql - else - @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger - end + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_column_name(sequence) + + select_value <<-end_sql, 'Reset sequence' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql end end @@ -786,7 +786,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first + result = exec_query(<<-end_sql, 'SCHEMA').rows.first SELECT attr.attname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -803,28 +803,6 @@ module ActiveRecord AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql - if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). - result = query(<<-end_sql, 'PK and custom sequence')[0] - SELECT attr.attname, - CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) - END - FROM pg_class t - JOIN pg_attribute attr ON (t.oid = attrelid) - JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) - JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) - WHERE t.oid = '#{quote_table_name(table)}'::regclass - AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' - end_sql - end - # [primary_key, sequence] [result.first, result.last] rescue @@ -833,8 +811,21 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first + row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first + SELECT DISTINCT(attr.attname) + FROM pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = $1::regclass + end_sql + + row && row.first end # Renames a table. @@ -848,38 +839,14 @@ module ActiveRecord add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(add_column_sql, options) - begin - execute add_column_sql - rescue ActiveRecord::StatementInvalid => e - raise e if postgresql_version > 80000 - - execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - end + execute add_column_sql end # Changes the column of a table. def change_column(table_name, column_name, type, options = {}) quoted_table_name = quote_table_name(table_name) - begin - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - rescue ActiveRecord::StatementInvalid => e - raise e if postgresql_version > 80000 - # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it. - begin - begin_db_transaction - tmp_column_name = "#{column_name}_ar_tmp" - add_column(table_name, tmp_column_name, type, options) - execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})" - remove_column(table_name, column_name) - rename_column(table_name, tmp_column_name, column_name) - commit_db_transaction - rescue - rollback_db_transaction - end - end + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -1031,9 +998,9 @@ module ActiveRecord # If using Active Record's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. if ActiveRecord::Base.default_timezone == :utc - execute("SET time zone 'UTC'") + execute("SET time zone 'UTC'", 'SCHEMA') elsif @local_tz - execute("SET time zone '#{@local_tz}'") + execute("SET time zone '#{@local_tz}'", 'SCHEMA') end end @@ -1076,7 +1043,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: - exec_query(<<-end_sql).rows + exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 32229a8410..8ef286b473 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -173,6 +173,10 @@ module ActiveRecord end end + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.execute(sql) } end @@ -188,7 +192,8 @@ module ActiveRecord end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super || @connection.last_insert_row_id + super + id_value || @connection.last_insert_row_id end alias :create :insert_sql diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index d523c643ba..0939ec2626 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -173,10 +173,10 @@ class FixturesFileNotFound < StandardError; end # traversed in the database to create the fixture hash and/or instance variables. This is expensive for # large sets of fixtured data. # -# = Dynamic fixtures with ERb +# = Dynamic fixtures with ERB # # Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can -# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like: +# mix ERB in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like: # # <% for i in 1..1000 %> # fix_<%= i %>: @@ -186,7 +186,7 @@ class FixturesFileNotFound < StandardError; end # # This will create 1000 very simple YAML fixtures. # -# Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. +# Using ERB, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. # This is however a feature to be used with some caution. The point of fixtures are that they're # stable units of predictable sample data. If you feel that you need to inject dynamic values, then # perhaps you should reexamine whether your application is properly testable. Hence, dynamic values diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb index d18b2b0a54..95a8e5cff7 100644 --- a/activerecord/lib/active_record/identity_map.rb +++ b/activerecord/lib/active_record/identity_map.rb @@ -50,7 +50,14 @@ module ActiveRecord def get(klass, primary_key) obj = repository[klass.symbolized_base_class][primary_key] - obj.is_a?(klass) ? obj : nil + if obj.is_a?(klass) + if ActiveRecord::Base.logger + ActiveRecord::Base.logger.debug "#{klass} with ID = #{primary_key} loaded from Identity Map" + end + obj + else + nil + end end def add(record) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index d291632260..f1df04950b 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -9,11 +9,6 @@ module ActiveRecord module NamedScope extend ActiveSupport::Concern - included do - class_attribute :scopes - self.scopes = {} - end - module ClassMethods # Returns an anonymous \scope. # @@ -35,7 +30,13 @@ module ActiveRecord if options scoped.apply_finder_options(options) else - current_scoped_methods ? relation.merge(current_scoped_methods) : relation.clone + if current_scope + current_scope.clone + else + scope = relation.clone + scope.default_scoped = true + scope + end end end @@ -50,6 +51,14 @@ module ActiveRecord # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. # + # Note that this is simply 'syntactic sugar' for defining an actual class method: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red') + # end + # end + # # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. @@ -73,14 +82,34 @@ module ActiveRecord # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean # only shirts. # - # Named \scopes can also be procedural: + # If you need to pass parameters to a scope, define it as a normal method: # # class Shirt < ActiveRecord::Base - # scope :colored, lambda {|color| where(:color => color) } + # def self.colored(color) + # where(:color => color) + # end # end # # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. # + # Note that scopes defined with \scope will be evaluated when they are defined, rather than + # when they are used. For example, the following would be incorrect: + # + # class Post < ActiveRecord::Base + # scope :recent, where('published_at >= ?', Time.now - 1.week) + # end + # + # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> + # class was defined, and so the resultant SQL query would always be the same. The correct + # way to do this would be via a class method, which will re-evaluate the scope each time + # it is called: + # + # class Post < ActiveRecord::Base + # def self.recent + # where('published_at >= ?', Time.now - 1.week) + # end + # end + # # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: # # class Shirt < ActiveRecord::Base @@ -91,6 +120,18 @@ module ActiveRecord # end # end # + # The above could also be written as a class method like so: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red').extending do + # def dom_id + # 'red_shirts' + # end + # end + # end + # end + # # Scopes can also be used while creating/building a record. # # class Article < ActiveRecord::Base @@ -99,34 +140,68 @@ module ActiveRecord # # Article.published.new.published # => true # Article.published.create.published # => true + # + # Class methods on your model are automatically available + # on scopes. Assuming the following setup: + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # scope :featured, where(:featured => true) + # + # def self.latest_article + # order('published_at desc').first + # end + # + # def self.titles + # map(&:title) + # end + # + # end + # + # We are able to call the methods like this: + # + # Article.published.featured.latest_article + # Article.featured.titles + def scope(name, scope_options = {}) name = name.to_sym valid_scope_name?(name) extension = Module.new(&Proc.new) if block_given? + if !scope_options.is_a?(Relation) && scope_options.respond_to?(:call) + ActiveSupport::Deprecation.warn <<-WARN +Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your scope to be lazily evaluated, or takes parameters, please define it as a normal class method instead. For example, change this: + +class Post < ActiveRecord::Base + scope :unpublished, lambda { where('published_at > ?', Time.now) } +end + +To this: + +class Post < ActiveRecord::Base + def self.unpublished + where('published_at > ?', Time.now) + end +end + WARN + end + scope_proc = lambda do |*args| options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options + options = scoped.apply_finder_options(options) if options.is_a?(Hash) - relation = if options.is_a?(Hash) - scoped.apply_finder_options(options) - elsif options - scoped.merge(options) - else - scoped - end + relation = scoped.merge(options) extension ? relation.extending(extension) : relation end - self.scopes = self.scopes.merge name => scope_proc - - singleton_class.send(:redefine_method, name, &scopes[name]) + singleton_class.send(:redefine_method, name, &scope_proc) end protected def valid_scope_name?(name) - if !scopes[name] && respond_to?(name, true) + if respond_to?(name, true) logger.warn "Creating scope :#{name}. " \ "Overwriting existing method #{self.name}.#{name}." end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 522c0cfc9f..08b27b6a8e 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -403,12 +403,6 @@ module ActiveRecord unless reject_new_record?(association_name, attributes) association.build(attributes.except(*UNASSIGNABLE_KEYS)) end - elsif existing_records.count == 0 #Existing record but not yet associated - existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) - if !call_reject_if(association_name, attributes) - association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } unless association.loaded? || call_reject_if(association_name, attributes) # Make sure we are operating on the actual object which is in the association's @@ -452,6 +446,7 @@ module ActiveRecord end def call_reject_if(association_name, attributes) + return false if has_destroy_flag?(attributes) case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ff36814684..944a29a3e6 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + db_namespace = namespace :db do task :load_config => :rails_env do require 'active_record' @@ -135,7 +137,7 @@ db_namespace = namespace :db do end def local_database?(config, &block) - if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank? + if config['host'].among?("127.0.0.1", "localhost") || config['host'].blank? yield else $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host." diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e801bc4afa..eac6a5dcad 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/module/deprecation' +require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Reflection @@ -163,7 +164,7 @@ module ActiveRecord def initialize(macro, name, options, active_record) super - @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @collection = macro.among?(:has_many, :has_and_belongs_to_many) end # Returns a new, unsaved instance of the associated class. +options+ will diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 896daf516e..490360ccb5 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,7 +6,7 @@ module ActiveRecord JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches @@ -15,14 +15,16 @@ module ActiveRecord delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :to => :klass attr_reader :table, :klass, :loaded - attr_accessor :extensions + attr_accessor :extensions, :default_scoped alias :loaded? :loaded + alias :default_scoped? :default_scoped def initialize(klass, table) @klass, @table = klass, table @implicit_readonly = nil @loaded = false + @default_scoped = false SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} @@ -154,12 +156,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.scoped_methods << self - begin - yield - ensure - @klass.scoped_methods.pop - end + @klass.send(:with_scope, self, :overwrite) { yield } end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -194,6 +191,7 @@ module ActiveRecord # # The same idea applies to limit and order # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') def update_all(updates, conditions = nil, options = {}) + IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled? if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else @@ -322,6 +320,7 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) + IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled? if conditions where(conditions).delete_all else @@ -353,6 +352,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) + IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled? where(primary_key => id_or_array).delete_all end @@ -374,7 +374,7 @@ module ActiveRecord end def where_values_hash - equalities = @where_values.grep(Arel::Nodes::Equality).find_all { |node| + equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name } @@ -402,13 +402,20 @@ module ActiveRecord to_a.inspect end + def with_default_scope #:nodoc: + if default_scoped? + default_scope = @klass.send(:build_default_scope) + default_scope ? default_scope.merge(self) : self + else + self + end + end + protected def method_missing(method, *args, &block) if Array.method_defined?(method) to_a.send(method, *args, &block) - elsif @klass.scopes[method] - merge(@klass.send(method, *args, &block)) elsif @klass.respond_to?(method) scoping { @klass.send(method, *args, &block) } elsif arel.respond_to?(method) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8fa315bdf3..673e47942b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -309,6 +309,15 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id + if IdentityMap.enabled? && where_values.blank? && + limit_value.blank? && order_values.blank? && + includes_values.blank? && preload_values.blank? && + readonly_value.nil? && joins_values.blank? && + !@klass.locking_enabled? && + record = IdentityMap.get(@klass, id) + return record + end + column = columns_hash[primary_key] substitute = connection.substitute_for(column, @bind_values) @@ -343,8 +352,8 @@ module ActiveRecord if result.size == expected_size result else - conditions = arel.wheres.map { |x| x.value }.join(', ') - conditions = " [WHERE #{conditions}]" if conditions.present? + conditions = arel.where_sql + conditions = " [#{conditions}]" if conditions error = "Couldn't find all #{@klass.name.pluralize} with IDs " error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})" diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 02b7056989..94aa999715 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -8,7 +8,8 @@ module ActiveRecord attr_accessor :includes_values, :eager_load_values, :preload_values, :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :bind_values, - :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value + :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, + :from_value, :reorder_value def includes(*args) args.reject! {|a| a.blank? } @@ -63,7 +64,11 @@ module ActiveRecord end def reorder(*args) - except(:order).order(args) + return self if args.blank? + + relation = clone + relation.reorder_value = args.flatten + relation end def joins(*args) @@ -163,7 +168,7 @@ module ActiveRecord end def arel - @arel ||= build_arel + @arel ||= with_default_scope.build_arel end def build_arel @@ -180,7 +185,8 @@ module ActiveRecord arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? - arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty? + order = @reorder_value ? @reorder_value : @order_values + arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? build_select(arel, @select_values.uniq) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 128e0fbd86..69706b5ead 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -8,6 +8,8 @@ module ActiveRecord merged_relation = clone + r = r.with_default_scope if r.default_scoped? && r.klass != klass + Relation::ASSOCIATION_METHODS.each do |method| value = r.send(:"#{method}_values") @@ -70,6 +72,7 @@ module ActiveRecord # def except(*skips) result = self.class.new(@klass, table) + result.default_scoped = default_scoped ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| result.send(:"#{method}_values=", send(:"#{method}_values")) @@ -94,6 +97,7 @@ module ActiveRecord # def only(*onlies) result = self.class.new(@klass, table) + result.default_scoped = default_scoped ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method| result.send(:"#{method}_values=", send(:"#{method}_values")) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 0465b21e88..243012f88c 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,9 +1,9 @@ module ActiveRecord ### - # This class encapsulates a Result returned from calling +exec+ on any + # This class encapsulates a Result returned from calling +exec_query+ on any # database connection adapter. For example: # - # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo') + # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo') # x # => #<ActiveRecord::Result:0xdeadbeef> class Result include Enumerable diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 60d4c256c4..d363f36108 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -270,6 +270,7 @@ module ActiveRecord def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure + IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state(force_restore_state) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9cd6c26322..d1225a9ed9 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,6 +14,7 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) + table = finder_class.arel_table coder = record.class.serialized_attributes[attribute.to_s] @@ -21,21 +22,15 @@ module ActiveRecord value = coder.dump value end - sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) - - relation = finder_class.unscoped.where(sql, *params) + relation = build_relation(finder_class, table, attribute, value) + relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array.wrap(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) - relation = relation.where(scope_item => scope_value) - end - - if record.persisted? - # TODO : This should be in Arel - relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id)) + relation = relation.and(table[scope_item].eq(scope_value)) end - if relation.exists? + if finder_class.unscoped.where(relation).exists? record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) end end @@ -57,27 +52,18 @@ module ActiveRecord class_hierarchy.detect { |klass| !klass.abstract_class? } end - def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc: + def build_relation(klass, table, attribute, value) #:nodoc: column = klass.columns_hash[attribute.to_s] + value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? - operator = if value.nil? - "IS ?" - elsif column.text? - value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s - "#{klass.connection.case_sensitive_equality_operator} ?" - else - "= ?" - end - - sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}" - - if value.nil? || (options[:case_sensitive] || !column.text?) - sql = "#{sql_attribute} #{operator}" + if !options[:case_sensitive] && column.text? + relation = table[attribute].matches(value) else - sql = "LOWER(#{sql_attribute}) = LOWER(?)" + value = klass.connection.case_sensitive_modifier(value) + relation = table[attribute].eq(value) end - [sql, [value]] + relation end end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb index afcda2a98a..d80c8ba996 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb @@ -1,4 +1,5 @@ require 'rails/generators/active_record' +require 'active_support/core_ext/object/inclusion' module ActiveRecord module Generators @@ -13,7 +14,7 @@ module ActiveRecord def session_table_name current_table_name = ActiveRecord::SessionStore::Session.table_name - if ["sessions", "session"].include?(current_table_name) + if current_table_name.among?("sessions", "session") current_table_name = (ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session') end current_table_name diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb new file mode 100644 index 0000000000..146b77a95c --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -0,0 +1,69 @@ +# encoding: utf-8 + +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class MysqlAdapterTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + @conn.exec_query('drop table if exists ex') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex` ( + `id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `number` integer, + `data` varchar(255)) + eosql + end + + def test_client_encoding + if "<3".respond_to?(:encoding) + assert_equal Encoding::UTF_8, @conn.client_encoding + else + assert_equal 'utf8', @conn.client_encoding + end + end + + def test_exec_insert_number + insert(@conn, 'number' => 10) + + result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') + + assert_equal 1, result.rows.length + assert_equal 10, result.rows.last.last + end + + def test_exec_insert_string + str = 'いただきます!' + insert(@conn, 'number' => 10, 'data' => str) + + result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') + + value = result.rows.last.last + + if "<3".respond_to?(:encoding) + # FIXME: this should probably be inside the mysql AR adapter? + value.force_encoding(@conn.client_encoding) + + # The strings in this file are utf-8, so transcode to utf-8 + value.encode!(Encoding::UTF_8) + end + + assert_equal str, value + end + + private + def insert(ctx, data) + binds = data.map { |name, value| + [ctx.columns('ex').find { |x| x.name == name }, value] + } + columns = binds.map(&:first).map(&:name) + + sql = "INSERT INTO ex (#{columns.join(", ")}) + VALUES (#{(['?'] * columns.length).join(', ')})" + + ctx.exec_insert(sql, 'SQL', binds) + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index b0a4a4e39d..2d412a6e2a 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require "cases/helper" module ActiveRecord @@ -6,7 +7,52 @@ module ActiveRecord def setup @connection = ActiveRecord::Base.connection @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(id serial primary key, data character varying(255))') + @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))') + end + + def test_serial_sequence + assert_equal 'public.accounts_id_seq', + @connection.serial_sequence('accounts', 'id') + + assert_raises(ActiveRecord::StatementInvalid) do + @connection.serial_sequence('zomg', 'id') + end + end + + def test_default_sequence_name + assert_equal 'accounts_id_seq', + @connection.default_sequence_name('accounts', 'id') + + assert_equal 'accounts_id_seq', + @connection.default_sequence_name('accounts') + end + + def test_default_sequence_name_bad_table + assert_equal 'zomg_id_seq', + @connection.default_sequence_name('zomg', 'id') + + assert_equal 'zomg_id_seq', + @connection.default_sequence_name('zomg') + end + + def test_exec_insert_number + insert(@connection, 'number' => 10) + + result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') + + assert_equal 1, result.rows.length + assert_equal "10", result.rows.last.last + end + + def test_exec_insert_string + str = 'いただきます!' + insert(@connection, 'number' => 10, 'data' => str) + + result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10') + + value = result.rows.last.last + + assert_equal str, value end def test_table_alias_length @@ -63,6 +109,21 @@ module ActiveRecord bind = @connection.substitute_for(nil, [nil]) assert_equal Arel.sql('$2'), bind end + + private + def insert(ctx, data) + binds = data.map { |name, value| + [ctx.columns('ex').find { |x| x.name == name }, value] + } + columns = binds.map(&:first).map(&:name) + + bind_subs = columns.length.times.map { |x| "$#{x + 1}" } + + sql = "INSERT INTO ex (#{columns.join(", ")}) + VALUES (#{bind_subs.join(', ')})" + + ctx.exec_insert(sql, 'SQL', binds) + end end end end diff --git a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb deleted file mode 100644 index d1fc470907..0000000000 --- a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb +++ /dev/null @@ -1,229 +0,0 @@ -# encoding: utf-8 -require "cases/helper" -require 'models/binary' - -module ActiveRecord - module ConnectionAdapters - class SQLiteAdapterTest < ActiveRecord::TestCase - class DualEncoding < ActiveRecord::Base - end - - def setup - @ctx = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => nil - @ctx.execute <<-eosql - CREATE TABLE items ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer - ) - eosql - end - - def test_quote_binary_column_escapes_it - DualEncoding.connection.execute(<<-eosql) - CREATE TABLE dual_encodings ( - id integer PRIMARY KEY AUTOINCREMENT, - name string, - data binary - ) - eosql - str = "\x80".force_encoding("ASCII-8BIT") - binary = DualEncoding.new :name => 'いただきます!', :data => str - binary.save! - assert_equal str, binary.data - end - - def test_execute - @ctx.execute "INSERT INTO items (number) VALUES (10)" - records = @ctx.execute "SELECT * FROM items" - assert_equal 1, records.length - - record = records.first - assert_equal 10, record['number'] - assert_equal 1, record['id'] - end - - def test_quote_string - assert_equal "''", @ctx.quote_string("'") - end - - def test_insert_sql - 2.times do |i| - rv = @ctx.insert_sql "INSERT INTO items (number) VALUES (#{i})" - assert_equal(i + 1, rv) - end - - records = @ctx.execute "SELECT * FROM items" - assert_equal 2, records.length - end - - def test_insert_sql_logged - sql = "INSERT INTO items (number) VALUES (10)" - name = "foo" - - assert_logged([[sql, name]]) do - @ctx.insert_sql sql, name - end - end - - def test_insert_id_value_returned - sql = "INSERT INTO items (number) VALUES (10)" - idval = 'vuvuzela' - id = @ctx.insert_sql sql, nil, nil, idval - assert_equal idval, id - end - - def test_select_rows - 2.times do |i| - @ctx.create "INSERT INTO items (number) VALUES (#{i})" - end - rows = @ctx.select_rows 'select number, id from items' - assert_equal [[0, 1], [1, 2]], rows - end - - def test_select_rows_logged - sql = "select * from items" - name = "foo" - - assert_logged([[sql, name]]) do - @ctx.select_rows sql, name - end - end - - def test_transaction - count_sql = 'select count(*) from items' - - @ctx.begin_db_transaction - @ctx.create "INSERT INTO items (number) VALUES (10)" - - assert_equal 1, @ctx.select_rows(count_sql).first.first - @ctx.rollback_db_transaction - assert_equal 0, @ctx.select_rows(count_sql).first.first - end - - def test_tables - assert_equal %w{ items }, @ctx.tables - - @ctx.execute <<-eosql - CREATE TABLE people ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer - ) - eosql - assert_equal %w{ items people }.sort, @ctx.tables.sort - end - - def test_tables_logs_name - name = "hello" - assert_logged [[name]] do - @ctx.tables(name) - assert_not_nil @ctx.logged.first.shift - end - end - - def test_columns - columns = @ctx.columns('items').sort_by { |x| x.name } - assert_equal 2, columns.length - assert_equal %w{ id number }.sort, columns.map { |x| x.name } - assert_equal [nil, nil], columns.map { |x| x.default } - assert_equal [true, true], columns.map { |x| x.null } - end - - def test_columns_with_default - @ctx.execute <<-eosql - CREATE TABLE columns_with_default ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer default 10 - ) - eosql - column = @ctx.columns('columns_with_default').find { |x| - x.name == 'number' - } - assert_equal 10, column.default - end - - def test_columns_with_not_null - @ctx.execute <<-eosql - CREATE TABLE columns_with_default ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer not null - ) - eosql - column = @ctx.columns('columns_with_default').find { |x| - x.name == 'number' - } - assert !column.null, "column should not be null" - end - - def test_indexes_logs - intercept_logs_on @ctx - assert_difference('@ctx.logged.length') do - @ctx.indexes('items') - end - assert_match(/items/, @ctx.logged.last.first) - end - - def test_no_indexes - assert_equal [], @ctx.indexes('items') - end - - def test_index - @ctx.add_index 'items', 'id', :unique => true, :name => 'fun' - index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } - - assert_equal 'items', index.table - assert index.unique, 'index is unique' - assert_equal ['id'], index.columns - end - - def test_non_unique_index - @ctx.add_index 'items', 'id', :name => 'fun' - index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } - assert !index.unique, 'index is not unique' - end - - def test_compound_index - @ctx.add_index 'items', %w{ id number }, :name => 'fun' - index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } - assert_equal %w{ id number }.sort, index.columns.sort - end - - def test_primary_key - assert_equal 'id', @ctx.primary_key('items') - - @ctx.execute <<-eosql - CREATE TABLE foos ( - internet integer PRIMARY KEY AUTOINCREMENT, - number integer not null - ) - eosql - assert_equal 'internet', @ctx.primary_key('foos') - end - - def test_no_primary_key - @ctx.execute 'CREATE TABLE failboat (number integer not null)' - assert_nil @ctx.primary_key('failboat') - end - - private - - def assert_logged logs - intercept_logs_on @ctx - yield - assert_equal logs, @ctx.logged - end - - def intercept_logs_on ctx - @ctx.extend(Module.new { - attr_accessor :logged - def log sql, name - @logged << [sql, name] - yield - end - }) - @ctx.logged = [] - end - end - end -end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index b8abdface4..0e2f468908 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,12 +1,34 @@ +# encoding: utf-8 require "cases/helper" module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + class DualEncoding < ActiveRecord::Base + end + def setup @conn = Base.sqlite3_connection :database => ':memory:', :adapter => 'sqlite3', :timeout => 100 + @conn.execute <<-eosql + CREATE TABLE items ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer + ) + eosql + end + + def test_exec_insert + column = @conn.columns('items').find { |col| col.name == 'number' } + vals = [[column, 10]] + @conn.exec_insert('insert into items (number) VALUES (?)', 'SQL', vals) + + result = @conn.exec_query( + 'select number from items where number = ?', 'SQL', vals) + + assert_equal 1, result.rows.length + assert_equal 10, result.rows.first.first end def test_primary_key_returns_nil_for_no_pk @@ -102,6 +124,213 @@ module ActiveRecord assert_equal [[1, 'foo']], result.rows end + + def test_quote_binary_column_escapes_it + return unless "<3".respond_to?(:encode) + + DualEncoding.connection.execute(<<-eosql) + CREATE TABLE dual_encodings ( + id integer PRIMARY KEY AUTOINCREMENT, + name string, + data binary + ) + eosql + str = "\x80".force_encoding("ASCII-8BIT") + binary = DualEncoding.new :name => 'いただきます!', :data => str + binary.save! + assert_equal str, binary.data + end + + def test_execute + @conn.execute "INSERT INTO items (number) VALUES (10)" + records = @conn.execute "SELECT * FROM items" + assert_equal 1, records.length + + record = records.first + assert_equal 10, record['number'] + assert_equal 1, record['id'] + end + + def test_quote_string + assert_equal "''", @conn.quote_string("'") + end + + def test_insert_sql + 2.times do |i| + rv = @conn.insert_sql "INSERT INTO items (number) VALUES (#{i})" + assert_equal(i + 1, rv) + end + + records = @conn.execute "SELECT * FROM items" + assert_equal 2, records.length + end + + def test_insert_sql_logged + sql = "INSERT INTO items (number) VALUES (10)" + name = "foo" + + assert_logged([[sql, name, []]]) do + @conn.insert_sql sql, name + end + end + + def test_insert_id_value_returned + sql = "INSERT INTO items (number) VALUES (10)" + idval = 'vuvuzela' + id = @conn.insert_sql sql, nil, nil, idval + assert_equal idval, id + end + + def test_select_rows + 2.times do |i| + @conn.create "INSERT INTO items (number) VALUES (#{i})" + end + rows = @conn.select_rows 'select number, id from items' + assert_equal [[0, 1], [1, 2]], rows + end + + def test_select_rows_logged + sql = "select * from items" + name = "foo" + + assert_logged([[sql, name, []]]) do + @conn.select_rows sql, name + end + end + + def test_transaction + count_sql = 'select count(*) from items' + + @conn.begin_db_transaction + @conn.create "INSERT INTO items (number) VALUES (10)" + + assert_equal 1, @conn.select_rows(count_sql).first.first + @conn.rollback_db_transaction + assert_equal 0, @conn.select_rows(count_sql).first.first + end + + def test_tables + assert_equal %w{ items }, @conn.tables + + @conn.execute <<-eosql + CREATE TABLE people ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer + ) + eosql + assert_equal %w{ items people }.sort, @conn.tables.sort + end + + def test_tables_logs_name + name = "hello" + assert_logged [[name, []]] do + @conn.tables(name) + assert_not_nil @conn.logged.first.shift + end + end + + def test_columns + columns = @conn.columns('items').sort_by { |x| x.name } + assert_equal 2, columns.length + assert_equal %w{ id number }.sort, columns.map { |x| x.name } + assert_equal [nil, nil], columns.map { |x| x.default } + assert_equal [true, true], columns.map { |x| x.null } + end + + def test_columns_with_default + @conn.execute <<-eosql + CREATE TABLE columns_with_default ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer default 10 + ) + eosql + column = @conn.columns('columns_with_default').find { |x| + x.name == 'number' + } + assert_equal 10, column.default + end + + def test_columns_with_not_null + @conn.execute <<-eosql + CREATE TABLE columns_with_default ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer not null + ) + eosql + column = @conn.columns('columns_with_default').find { |x| + x.name == 'number' + } + assert !column.null, "column should not be null" + end + + def test_indexes_logs + intercept_logs_on @conn + assert_difference('@conn.logged.length') do + @conn.indexes('items') + end + assert_match(/items/, @conn.logged.last.first) + end + + def test_no_indexes + assert_equal [], @conn.indexes('items') + end + + def test_index + @conn.add_index 'items', 'id', :unique => true, :name => 'fun' + index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + + assert_equal 'items', index.table + assert index.unique, 'index is unique' + assert_equal ['id'], index.columns + end + + def test_non_unique_index + @conn.add_index 'items', 'id', :name => 'fun' + index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + assert !index.unique, 'index is not unique' + end + + def test_compound_index + @conn.add_index 'items', %w{ id number }, :name => 'fun' + index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + assert_equal %w{ id number }.sort, index.columns.sort + end + + def test_primary_key + assert_equal 'id', @conn.primary_key('items') + + @conn.execute <<-eosql + CREATE TABLE foos ( + internet integer PRIMARY KEY AUTOINCREMENT, + number integer not null + ) + eosql + assert_equal 'internet', @conn.primary_key('foos') + end + + def test_no_primary_key + @conn.execute 'CREATE TABLE failboat (number integer not null)' + assert_nil @conn.primary_key('failboat') + end + + private + + def assert_logged logs + intercept_logs_on @conn + yield + assert_equal logs, @conn.logged + end + + def intercept_logs_on ctx + @conn.extend(Module.new { + attr_accessor :logged + def log sql, name, binds = [] + @logged << [sql, name, binds] + yield + end + }) + @conn.logged = [] + end end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 16d4877fe8..007f11b535 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -70,16 +70,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope car = cars(:honda) - original_scoped_methods = Bulb.scoped_methods + scoped_count = car.foo_bulbs.scoped.where_values.count - bulb = car.bulbs.build - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = car.foo_bulbs.build + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = car.bulbs.create - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = car.foo_bulbs.create + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = car.bulbs.create! - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = car.foo_bulbs.create! + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count end def test_no_sql_should_be_fired_if_association_already_loaded diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index c1dad5e246..f3c96ccbe6 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -165,16 +165,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_and_create_should_not_happen_within_scope pirate = pirates(:blackbeard) - original_scoped_methods = Bulb.scoped_methods.dup + scoped_count = pirate.association(:foo_bulb).scoped.where_values.count - bulb = pirate.build_bulb - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = pirate.build_foo_bulb + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = pirate.create_bulb - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = pirate.create_foo_bulb + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = pirate.create_bulb! - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = pirate.create_foo_bulb! + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count end def test_create_association diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 5a7b139030..afc830cae9 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' require 'models/tag' require 'models/tagging' require 'models/post' @@ -453,7 +454,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.tags.include?(new_tag) assert new_tag.persisted? - assert saved_post.reload.tags(true).include?(new_tag) + assert new_tag.in?(saved_post.reload.tags(true)) new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.") @@ -466,7 +467,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.save! assert new_post.persisted? - assert new_post.reload.tags(true).include?(saved_tag) + assert saved_tag.in?(new_post.reload.tags(true)) assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index d0a9028264..3641031d12 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' module ActiveRecord module AttributeMethods @@ -41,13 +42,13 @@ module ActiveRecord instance = @klass.new @klass.column_names.each do |name| - assert ! instance.methods.map(&:to_s).include?(name) + assert !name.in?(instance.methods.map(&:to_s)) end @klass.define_attribute_methods @klass.column_names.each do |name| - assert(instance.methods.map(&:to_s).include?(name), "#{name} is not defined") + assert name.in?(instance.methods.map(&:to_s)), "#{name} is not defined" end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 84f75cc803..a3c5c14758 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' require 'models/minimalistic' require 'models/developer' require 'models/auto_id' @@ -638,7 +639,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def time_related_columns_on_topic - Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) } + Topic.columns.select { |c| c.type.among?(:time, :date, :datetime, :timestamp) } end def serialized_columns_on_topic diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index aeb0b28bab..e57c5b3b87 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -172,7 +172,7 @@ class BasicsTest < ActiveRecord::TestCase with_active_record_default_timezone :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a @@ -186,7 +186,7 @@ class BasicsTest < ActiveRecord::TestCase Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a assert_equal [0, 0, 6, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a @@ -199,7 +199,7 @@ class BasicsTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do time = Time.utc(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a @@ -212,7 +212,7 @@ class BasicsTest < ActiveRecord::TestCase Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a assert_equal [0, 0, 1, 1, 1, 2000, 6, 1, false, "EST"], saved_time.to_a @@ -1048,7 +1048,7 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.new(:content => myobj) assert topic.save Topic.serialize(:content, Hash) - assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content } + assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).reload.content } ensure Topic.serialize(:content) end @@ -1626,18 +1626,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal c1, c2 end - def test_default_scope_is_reset + def test_current_scope_is_reset Object.const_set :UnloadablePost, Class.new(ActiveRecord::Base) - UnloadablePost.table_name = 'posts' - UnloadablePost.class_eval do - default_scope order('posts.comments_count ASC') - end - UnloadablePost.scoped_methods # make Thread.current[:UnloadablePost_scoped_methods] not nil + UnloadablePost.send(:current_scope=, UnloadablePost.scoped) UnloadablePost.unloadable - assert_not_nil Thread.current[:UnloadablePost_scoped_methods] + assert_not_nil Thread.current[:UnloadablePost_current_scope] ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil Thread.current[:UnloadablePost_scoped_methods] + assert_nil Thread.current[:UnloadablePost_current_scope] ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index deaf5252db..42b14bd25c 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' require 'models/default' require 'models/entrant' @@ -94,7 +95,7 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_equal 0, klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null # 0 in MySQL 4, nil in 5. - assert [0, nil].include?(klass.columns_hash['omit'].default) + assert klass.columns_hash['omit'].default.among?(0, nil) assert !klass.columns_hash['omit'].null assert_raise(ActiveRecord::StatementInvalid) { klass.create! } diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index 89f7b92d09..199e59657d 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -90,6 +90,13 @@ class IdentityMapTest < ActiveRecord::TestCase ) end + def test_queries_are_not_executed_when_finding_by_id + Post.find 1 + assert_no_queries do + Post.find 1 + end + end + ############################################################################## # Tests checking if IM is functioning properly on more advanced finds # # and associations # @@ -144,7 +151,7 @@ class IdentityMapTest < ActiveRecord::TestCase s = Subscriber.find('swistak') - assert_equal({'name' => ["Raczkowski Marcin", "Swistak Sreberkowiec"]}, swistak.changes) + assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) assert_equal("Swistak Sreberkowiec", swistak.name) end @@ -159,8 +166,8 @@ class IdentityMapTest < ActiveRecord::TestCase s = Subscriber.find('swistak') assert_equal("Swistak Sreberkowiec", swistak.name) - assert_equal({}, swistak.changes) - assert !swistak.name_changed? + assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) + assert swistak.name_changed? end def test_has_many_associations @@ -381,6 +388,15 @@ class IdentityMapTest < ActiveRecord::TestCase assert_not_nil post.title end + def test_log + log = StringIO.new + ActiveRecord::Base.logger = Logger.new(log) + ActiveRecord::Base.logger.level = Logger::DEBUG + Post.find 1 + Post.find 1 + assert_match(/Post with ID = 1 loaded from Identity Map/, log.string) + end + # Currently AR is not allowing changing primary key (see Persistence#update) # So we ignore it. If this changes, this test needs to be uncommented. # def test_updating_of_pkey diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 8ebde933b4..5f55299065 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -8,6 +8,8 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger + @using_identity_map = ActiveRecord::IdentityMap.enabled? + ActiveRecord::IdentityMap.enabled = false super ActiveRecord::LogSubscriber.attach_to(:active_record) end @@ -16,6 +18,7 @@ class LogSubscriberTest < ActiveRecord::TestCase super ActiveRecord::LogSubscriber.log_subscribers.pop ActiveRecord::Base.logger = @old_logger + ActiveRecord::IdentityMap.enabled = @using_identity_map end def set_logger(logger) diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 7e8383da9e..7f0f007a70 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -249,22 +249,21 @@ class MethodScopingTest < ActiveRecord::TestCase end def test_scoped_with_duck_typing - scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] }) + scoping = Struct.new(:current_scope).new(:find => { :conditions => ["name = ?", 'David'] }) Developer.send(:with_scope, scoping) do assert_equal %w(David), Developer.find(:all).map { |d| d.name } end end def test_ensure_that_method_scoping_is_correctly_restored - scoped_methods = Developer.instance_eval('current_scoped_methods') - begin Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do raise "an exception" end rescue end - assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + + assert !Developer.scoped.where_values.include?("name = 'Jamis'") end end @@ -509,14 +508,15 @@ class NestedScopingTest < ActiveRecord::TestCase def test_ensure_that_method_scoping_is_correctly_restored Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - scoped_methods = Developer.instance_eval('current_scoped_methods') begin Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do raise "an exception" end rescue end - assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + + assert Developer.scoped.where_values.include?("name = 'David'") + assert !Developer.scoped.where_values.include?("name = 'Maiha'") end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 9b20ea08de..2880fdc651 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -58,17 +58,6 @@ class NamedScopeTest < ActiveRecord::TestCase assert Topic.approved.respond_to?(:tables_in_string, true) end - def test_subclasses_inherit_scopes - assert Topic.scopes.include?(:base) - - assert Reply.scopes.include?(:base) - assert_equal Reply.find(:all), Reply.base - end - - def test_classes_dont_inherit_subclasses_scopes - assert !ActiveRecord::Base.scopes.include?(:base) - end - def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified assert !Topic.find(:all, :conditions => {:approved => true}).empty? @@ -440,26 +429,31 @@ class NamedScopeTest < ActiveRecord::TestCase end end + # Note: these next two are kinda odd because they are essentially just testing that the + # query cache works as it should, but they are here for legacy reasons as they was previously + # a separate cache on association proxies, and these show that that is not necessary. def test_scopes_are_cached_on_associations post = posts(:welcome) - assert_equal post.comments.containing_the_letter_e.object_id, post.comments.containing_the_letter_e.object_id - - post.comments.containing_the_letter_e.all # force load - assert_no_queries { post.comments.containing_the_letter_e.all } + Post.cache do + assert_queries(1) { post.comments.containing_the_letter_e.all } + assert_no_queries { post.comments.containing_the_letter_e.all } + end end def test_scopes_with_arguments_are_cached_on_associations post = posts(:welcome) - one = post.comments.limit_by(1).all - assert_equal 1, one.size + Post.cache do + one = assert_queries(1) { post.comments.limit_by(1).all } + assert_equal 1, one.size - two = post.comments.limit_by(2).all - assert_equal 2, two.size + two = assert_queries(1) { post.comments.limit_by(2).all } + assert_equal 2, two.size - assert_no_queries { post.comments.limit_by(1).all } - assert_no_queries { post.comments.limit_by(2).all } + assert_no_queries { post.comments.limit_by(1).all } + assert_no_queries { post.comments.limit_by(2).all } + end end def test_scopes_are_reset_on_association_reload @@ -477,6 +471,12 @@ class NamedScopeTest < ActiveRecord::TestCase require "models/without_table" end end + + def test_scopes_with_callables_are_deprecated + assert_deprecated do + Post.scope :WE_SO_EXCITED, lambda { |partyingpartyingpartying, yeah| fun!.fun!.fun! } + end + end end class DynamicScopeMatchTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index c57ab7ed28..6568eb1d18 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -131,6 +131,14 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal 'photography', interest.reload.topic end + def test_destroy_works_independent_of_reject_if + Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }, :allow_destroy => true + man = Man.create(:name => "Jon") + interest = man.interests.create(:topic => 'the ladies') + man.update_attributes({:interests_attributes => { :_destroy => "1", :id => interest.id } }) + assert man.reload.interests.empty? + end + def test_has_many_association_updating_a_single_record Man.accepts_nested_attributes_for(:interests) man = Man.create(:name => 'John') diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 53aefc7b58..287f7e255b 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -13,7 +13,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_find_queries - assert_queries(2) { Task.find(1); Task.find(1) } + assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Task.find(1); Task.find(1) } end def test_find_queries_with_cache diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 30a783d5a2..5079aec9ba 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -132,8 +132,6 @@ class RelationScopingTest < ActiveRecord::TestCase end def test_ensure_that_method_scoping_is_correctly_restored - scoped_methods = Developer.send(:current_scoped_methods) - begin Developer.where("name = 'Jamis'").scoping do raise "an exception" @@ -141,7 +139,7 @@ class RelationScopingTest < ActiveRecord::TestCase rescue end - assert_equal scoped_methods, Developer.send(:current_scoped_methods) + assert !Developer.scoped.where_values.include?("name = 'Jamis'") end end @@ -310,72 +308,178 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end - def test_default_scope_with_lambda - expected = Post.find_all_by_author_id(2) - PostForAuthor.selected_author = 2 - received = PostForAuthor.all - assert_equal expected, received - expected = Post.find_all_by_author_id(1) - PostForAuthor.selected_author = 1 - received = PostForAuthor.all + def test_default_scope_is_unscoped_on_find + assert_equal 1, DeveloperCalledDavid.count + assert_equal 11, DeveloperCalledDavid.unscoped.count + end + + def test_default_scope_is_unscoped_on_create + assert_nil DeveloperCalledJamis.unscoped.create!.name + end + + def test_default_scope_with_conditions_string + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal nil, DeveloperCalledDavid.create!.name + end + + def test_default_scope_with_conditions_hash + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal 'Jamis', DeveloperCalledJamis.create!.name + end + + def test_default_scoping_with_threads + 2.times do + Thread.new { assert DeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join + end + end + + def test_default_scope_with_inheritance + wheres = InheritedPoorDeveloperCalledJamis.scoped.where_values_hash + assert_equal "Jamis", wheres[:name] + assert_equal 50000, wheres[:salary] + end + + def test_method_scope + expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } assert_equal expected, received end - def test_default_scope_with_thing_that_responds_to_call - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'posts' + def test_nested_scope + expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do + DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end + assert_equal expected, received + end - klass.class_eval do - default_scope Class.new(Struct.new(:klass)) { - def call - klass.where(:author_id => 2) - end - }.new(self) + def test_scope_overwrites_default + expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_reorder_overrides_default_scope_order + expected = Developer.order('name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + assert_equal expected, received + end + + def test_nested_exclusive_scope + expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do + DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end + assert_equal expected, received + end + + def test_order_in_default_scope_should_prevail + expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + assert_equal expected, received + end + + def test_create_attribute_overwrites_default_scoping + assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name + assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary + end + + def test_create_attribute_overwrites_default_values + assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary + assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary + end + + def test_default_scope_attribute + jamis = PoorDeveloperCalledJamis.new(:name => 'David') + assert_equal 50000, jamis.salary + end - records = klass.all - assert_equal 3, records.length - assert_equal 2, records.first.author_id + def test_where_attribute + aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + end + + def test_where_attribute_merge + aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') + assert_equal 'Aaron', aaron.name + end + + def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit + posts_limit_offset = Post.limit(3).offset(2) + posts_offset_limit = Post.offset(2).limit(3) + assert_equal posts_limit_offset, posts_offset_limit + end + + def test_create_with_merge + aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( + PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + + aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). + create_with(:name => 'Aaron').new + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + end + + def test_create_with_reset + jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new + assert_equal 'Jamis', jamis.name + end + + def test_unscoped_with_named_scope_should_not_have_default_scope + assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor + + assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) + assert_equal 10, DeveloperCalledJamis.unscoped.poor.length + end +end + +class DeprecatedDefaultScopingTest < ActiveRecord::TestCase + fixtures :developers, :posts + + def test_default_scope + expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + assert_equal expected, received end def test_default_scope_is_unscoped_on_find - assert_equal 1, DeveloperCalledDavid.count - assert_equal 11, DeveloperCalledDavid.unscoped.count + assert_equal 1, DeprecatedDeveloperCalledDavid.count + assert_equal 11, DeprecatedDeveloperCalledDavid.unscoped.count end def test_default_scope_is_unscoped_on_create - assert_nil DeveloperCalledJamis.unscoped.create!.name + assert_nil DeprecatedDeveloperCalledJamis.unscoped.create!.name end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort - assert_equal nil, DeveloperCalledDavid.create!.name + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeprecatedDeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal nil, DeprecatedDeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort - assert_equal 'Jamis', DeveloperCalledJamis.create!.name + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeprecatedDeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal 'Jamis', DeprecatedDeveloperCalledJamis.create!.name end def test_default_scoping_with_threads 2.times do - Thread.new { assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values }.join + Thread.new { assert DeprecatedDeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join end end def test_default_scoping_with_inheritance # Inherit a class having a default scope and define a new default scope - klass = Class.new(DeveloperOrderedBySalary) - klass.send :default_scope, :limit => 1 + klass = Class.new(DeprecatedDeveloperOrderedBySalary) + ActiveSupport::Deprecation.silence { klass.send :default_scope, :limit => 1 } # Scopes added on children should append to parent scope - assert_equal 1, klass.scoped.limit_value - assert_equal ['salary DESC'], klass.scoped.order_values + assert_equal [developers(:jamis).id], klass.all.map(&:id) # Parent should still have the original scope - assert_nil DeveloperOrderedBySalary.scoped.limit_value - assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values + assert_equal Developer.order('salary DESC').map(&:id), DeprecatedDeveloperOrderedBySalary.all.map(&:id) end def test_default_scope_called_twice_merges_conditions @@ -385,8 +489,10 @@ class DefaultScopingTest < ActiveRecord::TestCase Developer.create!(:name => "Brian", :salary => 100000) klass = Class.new(Developer) - klass.__send__ :default_scope, :conditions => { :name => "David" } - klass.__send__ :default_scope, :conditions => { :salary => 100000 } + ActiveSupport::Deprecation.silence do + klass.__send__ :default_scope, :conditions => { :name => "David" } + klass.__send__ :default_scope, :conditions => { :salary => 100000 } + end assert_equal 1, klass.count assert_equal "David", klass.first.name assert_equal 100000, klass.first.salary @@ -399,9 +505,11 @@ class DefaultScopingTest < ActiveRecord::TestCase Developer.create!(:name => "Brian", :salary => 100000) klass = Class.new(Developer) - klass.class_eval do - default_scope where("name = 'David'") - default_scope where("salary = 100000") + ActiveSupport::Deprecation.silence do + klass.class_eval do + default_scope where("name = 'David'") + default_scope where("salary = 100000") + end end assert_equal 1, klass.count @@ -411,96 +519,90 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_method_scope expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } assert_equal expected, received end def test_nested_scope expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do + DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end assert_equal expected, received end def test_scope_overwrites_default expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + received = DeprecatedDeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } assert_equal expected, received end def test_reorder_overrides_default_scope_order expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + received = DeprecatedDeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } assert_equal expected, received end def test_nested_exclusive_scope expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do + DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end assert_equal expected, received end def test_order_in_default_scope_should_prevail expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } assert_equal expected, received end def test_default_scope_using_relation - posts = PostWithComment.scoped - assert_equal 2, posts.count + posts = DeprecatedPostWithComment.scoped + assert_equal 2, posts.to_a.length assert_equal posts(:thinking), posts.first end def test_create_attribute_overwrites_default_scoping - assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name - assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary + assert_equal 'David', DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').name + assert_equal 200000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary end def test_create_attribute_overwrites_default_values - assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary - assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary + assert_equal nil, DeprecatedPoorDeveloperCalledJamis.create!(:salary => nil).salary + assert_equal 50000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').salary end def test_default_scope_attribute - jamis = PoorDeveloperCalledJamis.new(:name => 'David') + jamis = DeprecatedPoorDeveloperCalledJamis.new(:name => 'David') assert_equal 50000, jamis.salary end def test_where_attribute - aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') + aaron = DeprecatedPoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name end def test_where_attribute_merge - aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') + aaron = DeprecatedPoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') assert_equal 'Aaron', aaron.name end - def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit - posts_limit_offset = Post.limit(3).offset(2) - posts_offset_limit = Post.offset(2).limit(3) - assert_equal posts_limit_offset, posts_offset_limit - end - def test_create_with_merge - aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( - PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new + aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( + DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name - aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). + aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). create_with(:name => 'Aaron').new assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name end def test_create_with_reset - jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new + jamis = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new assert_equal 'Jamis', jamis.name end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 7bdbd773b6..6874bd18f8 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from].map(&:to_s).sort, + assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder].map(&:to_s).sort, Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 7178bb0d00..89ee5416bf 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,14 +1,15 @@ class Bulb < ActiveRecord::Base - - default_scope :conditions => {:name => 'defaulty' } + def self.default_scope + where :name => 'defaulty' + end belongs_to :car - attr_reader :scoped_methods_after_initialize + attr_reader :scope_after_initialize - after_initialize :record_scoped_methods_after_initialize - def record_scoped_methods_after_initialize - @scoped_methods_after_initialize = self.class.scoped_methods.dup + after_initialize :record_scope_after_initialize + def record_scope_after_initialize + @scope_after_initialize = self.class.scoped end end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index e7db3d3423..a978debb58 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,6 +1,7 @@ class Car < ActiveRecord::Base has_many :bulbs + has_many :foo_bulbs, :class_name => "Bulb", :conditions => { :name => 'foo' } has_many :tyres has_many :engines has_many :wheels, :as => :wheelable @@ -14,9 +15,13 @@ class Car < ActiveRecord::Base end class CoolCar < Car - default_scope :order => 'name desc' + def self.default_scope + order 'name desc' + end end class FastCar < Car - default_scope order('name desc') + def self.default_scope + order 'name desc' + end end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 09489b8ea4..39441e8610 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -13,7 +13,9 @@ end class SpecialCategorization < ActiveRecord::Base self.table_name = 'categorizations' - default_scope where(:special => true) + def self.default_scope + where(:special => true) + end belongs_to :author belongs_to :category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 2a4c37089a..3bd7db7834 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,5 +1,8 @@ class Comment < ActiveRecord::Base - scope :limit_by, lambda {|l| limit(l) } + def self.limit_by(l) + limit(l) + end + scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'" scope :not_again, where("comments.body NOT LIKE '%again%'") scope :for_first_post, :conditions => { :post_id => 1 } diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 32d060cf09..10385ba899 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -86,7 +86,11 @@ end class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' - default_scope :order => 'salary DESC' + + def self.default_scope + order('salary DESC') + end + scope :by_name, order('name DESC') def self.all_ordered_by_name @@ -98,15 +102,74 @@ end class DeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - default_scope :conditions => "name = 'David'" + + def self.default_scope + where "name = 'David'" + end end class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis' } + + def self.default_scope + where :name => 'Jamis' + end + + scope :poor, where('salary < 150000') +end + +class AbstractDeveloperCalledJamis < ActiveRecord::Base + self.abstract_class = true + + def self.default_scope + where :name => 'Jamis' + end end class PoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis', :salary => 50000 } + + def self.default_scope + where :name => 'Jamis', :salary => 50000 + end +end + +class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis + self.table_name = 'developers' + + def self.default_scope + super.where :salary => 50000 + end +end + +ActiveSupport::Deprecation.silence do + class DeprecatedDeveloperOrderedBySalary < ActiveRecord::Base + self.table_name = 'developers' + default_scope :order => 'salary DESC' + + def self.by_name + order('name DESC') + end + + def self.all_ordered_by_name + with_scope(:find => { :order => 'name DESC' }) do + find(:all) + end + end + end + + class DeprecatedDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + default_scope :conditions => "name = 'David'" + end + + class DeprecatedDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope :conditions => { :name => 'Jamis' } + end + + class DeprecatedPoorDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope :conditions => { :name => 'Jamis', :salary => 50000 } + end end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 0d3f62bb33..5e0f5323e6 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -34,7 +34,7 @@ class Pirate < ActiveRecord::Base :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"} has_many :birds_with_reject_all_blank, :class_name => "Bird" - has_one :bulb, :foreign_key => :car_id + has_one :foo_bulb, :foreign_key => :car_id, :class_name => "Bulb", :conditions => { :name => 'foo' } accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 82894a3d57..7055380d85 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -7,12 +7,15 @@ class Post < ActiveRecord::Base scope :containing_the_letter_a, where("body LIKE '%a%'") scope :ranked_by_comments, order("comments_count DESC") - scope :limit_by, lambda {|l| limit(l) } - scope :with_authors_at_address, lambda { |address| { - :conditions => [ 'authors.author_address_id = ?', address.id ], - :joins => 'JOIN authors ON authors.id = posts.author_id' - } - } + + def self.limit_by(l) + limit(l) + end + + def self.with_authors_at_address(address) + where('authors.author_address_id = ?', address.id) + .joins('JOIN authors ON authors.id = posts.author_id') + end belongs_to :author do def greeting @@ -27,9 +30,10 @@ class Post < ActiveRecord::Base scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } scope :with_very_special_comments, joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) - scope :with_post, lambda {|post_id| - { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } - } + + def self.with_post(post_id) + joins(:comments).where(:comments => { :post_id => post_id }) + end has_many :comments do def find_most_recent @@ -142,20 +146,25 @@ class SubStiPost < StiPost self.table_name = Post.table_name end -class PostWithComment < ActiveRecord::Base - self.table_name = 'posts' - default_scope where("posts.comments_count > 0").order("posts.comments_count ASC") +ActiveSupport::Deprecation.silence do + class DeprecatedPostWithComment < ActiveRecord::Base + self.table_name = 'posts' + default_scope where("posts.comments_count > 0").order("posts.comments_count ASC") + end end class PostForAuthor < ActiveRecord::Base self.table_name = 'posts' cattr_accessor :selected_author - default_scope lambda { where(:author_id => PostForAuthor.selected_author) } end class FirstPost < ActiveRecord::Base self.table_name = 'posts' - default_scope where(:id => 1) + + def self.default_scope + where(:id => 1) + end + has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index e33a0f2acc..76c0a1a32e 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -18,6 +18,9 @@ class Reference < ActiveRecord::Base end class BadReference < ActiveRecord::Base - self.table_name ='references' - default_scope :conditions => {:favourite => false } + self.table_name = 'references' + + def self.default_scope + where :favourite => false + end end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 6440dbe8ab..60e750e6c4 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,10 +1,20 @@ class Topic < ActiveRecord::Base scope :base - scope :written_before, lambda { |time| - if time - { :conditions => ['written_on < ?', time] } - end - } + + ActiveSupport::Deprecation.silence do + scope :written_before, lambda { |time| + if time + { :conditions => ['written_on < ?', time] } + end + } + + scope :with_object, Class.new(Struct.new(:klass)) { + def call + klass.where(:approved => true) + end + }.new(self) + end + scope :approved, :conditions => {:approved => true} scope :rejected, :conditions => {:approved => false} @@ -19,12 +29,6 @@ class Topic < ActiveRecord::Base end end - scope :with_object, Class.new(Struct.new(:klass)) { - def call - klass.where(:approved => true) - end - }.new(self) - module NamedExtension def two 2 diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb index 87f80911e1..1a63d6ceb6 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,3 +1,5 @@ class WithoutTable < ActiveRecord::Base - default_scope where(:published => true) -end
\ No newline at end of file + def self.default_scope + where(:published => true) + end +end diff --git a/activeresource/activeresource.gemspec b/activeresource/activeresource.gemspec index a71168722b..c2fd707e9b 100644 --- a/activeresource/activeresource.gemspec +++ b/activeresource/activeresource.gemspec @@ -17,7 +17,6 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*'] s.require_path = 'lib' - s.has_rdoc = true s.extra_rdoc_files = %w( README.rdoc ) s.rdoc_options.concat ['--main', 'README.rdoc'] diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index 480f2fbecb..e286362f87 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/benchmark' require 'active_support/core_ext/uri' +require 'active_support/core_ext/object/inclusion' require 'net/https' require 'date' require 'time' @@ -277,7 +278,7 @@ module ActiveResource def legitimize_auth_type(auth_type) return :basic if auth_type.nil? auth_type = auth_type.to_sym - [:basic, :digest].include?(auth_type) ? auth_type : :basic + auth_type.among?(:basic, :digest) ? auth_type : :basic end end end diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 75649053d0..e085a05f6d 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/kernel/reporting' +require 'active_support/core_ext/object/inclusion' module ActiveResource class InvalidRequestError < StandardError; end #:nodoc: @@ -299,7 +300,7 @@ module ActiveResource end def success? - (200..299).include?(code) + code.in?(200..299) end def [](key) diff --git a/activeresource/test/cases/http_mock_test.rb b/activeresource/test/cases/http_mock_test.rb index 43cf5f5ef0..dfb097315e 100644 --- a/activeresource/test/cases/http_mock_test.rb +++ b/activeresource/test/cases/http_mock_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/core_ext/object/inclusion' class HttpMockTest < ActiveSupport::TestCase setup do @@ -192,7 +193,7 @@ class HttpMockTest < ActiveSupport::TestCase end def request(method, path, headers = {}, body = nil) - if [:put, :post].include? method + if method.among?(:put, :post) @http.send(method, path, body, headers) else @http.send(method, path, headers) diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 373236ce9a..eb027795a8 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.1.0 (unreleased)* +* Add Object#in? to test if an object is included in another object, and Object#among? to test if an object is included in a list of objects which will be passed as arguments. [Prem Sichanugrist, Brian Morearty, John Reitano] + * LocalCache strategy is now a real middleware class, not an anonymous class posing for pictures. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index df7f68fecf..eaecb30090 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -16,6 +16,4 @@ Gem::Specification.new do |s| s.files = Dir['CHANGELOG', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' - - s.has_rdoc = true end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 18182bbb40..bdef47a237 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/file/atomic' require 'active_support/core_ext/string/conversions' +require 'active_support/core_ext/object/inclusion' require 'rack/utils' module ActiveSupport @@ -20,7 +21,7 @@ module ActiveSupport end def clear(options = nil) - root_dirs = Dir.entries(cache_path).reject{|f| ['.', '..'].include?(f)} + root_dirs = Dir.entries(cache_path).reject{|f| f.among?('.', '..')} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end @@ -161,7 +162,7 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) return if dir == cache_path - if Dir.entries(dir).reject{|f| ['.', '..'].include?(f)}.empty? + if Dir.entries(dir).reject{|f| f.among?('.', '..')}.empty? File.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 418102352f..6a0bffb6d7 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/object/inclusion' module ActiveSupport # \Callbacks are code hooks that are run at key points in an object's lifecycle. @@ -412,7 +413,7 @@ module ActiveSupport # CallbackChain. # def __update_callbacks(name, filters = [], block = nil) #:nodoc: - type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before + type = filters.first.among?(:before, :after, :around) ? filters.shift : :before options = filters.last.is_a?(Hash) ? filters.pop : {} filters.unshift(block) if block diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb index 82a4f7ac5a..6fa55a9255 100644 --- a/activesupport/lib/active_support/core_ext/date_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb @@ -16,6 +16,6 @@ class DateTime def in_time_zone(zone = ::Time.zone) return self unless zone - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.__send__(:get_zone, zone)) + ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) end end diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 790a26f5c1..9ad1e12699 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/try' +require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb new file mode 100644 index 0000000000..cf89288aed --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -0,0 +1,20 @@ +class Object + # Returns true if this object is included in the argument. Argument must be + # any object which respond to +#include?+. Usage: + # + # characters = ["Konata", "Kagami", "Tsukasa"] + # "Konata".in?(characters) # => true + # + def in?(another_object) + another_object.include?(self) + end + + # Returns true if this object is included in the argument list. Usage: + # + # username = "sikachu" + # username.among?("josevalim", "dhh", "wycats") # => false + # + def among?(*objects) + objects.include?(self) + end +end diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index addd4dab95..c27cbc37c5 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -9,7 +9,7 @@ class ERB # A utility method for escaping HTML tag characters. # This method is also aliased as <tt>h</tt>. # - # In your ERb templates, use this method to escape any unsafe content. For example: + # In your ERB templates, use this method to escape any unsafe content. For example: # <%=h @person.name %> # # ==== Example: diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index ff90d7ca58..0c5962858e 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -34,29 +34,36 @@ class Time # end # end def zone=(time_zone) - Thread.current[:time_zone] = get_zone(time_zone) + Thread.current[:time_zone] = find_zone!(time_zone) end # Allows override of <tt>Time.zone</tt> locally inside supplied block; resets <tt>Time.zone</tt> to existing value when done. def use_zone(time_zone) - old_zone, ::Time.zone = ::Time.zone, get_zone(time_zone) - yield - ensure - ::Time.zone = old_zone + new_zone = find_zone!(time_zone) + begin + old_zone, ::Time.zone = ::Time.zone, new_zone + yield + ensure + ::Time.zone = old_zone + end end - private - def get_zone(time_zone) - return time_zone if time_zone.nil? || time_zone.is_a?(ActiveSupport::TimeZone) - # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) - unless time_zone.respond_to?(:period_for_local) - time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) rescue nil - end - # Return if a TimeZone instance, or wrap in a TimeZone instance if a TZInfo::Timezone - if time_zone - time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone.create(time_zone.name, nil, time_zone) - end + # Returns a TimeZone instance or nil, or raises an ArgumentError for invalid timezones. + def find_zone!(time_zone) + return time_zone if time_zone.nil? || time_zone.is_a?(ActiveSupport::TimeZone) + # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) + unless time_zone.respond_to?(:period_for_local) + time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) end + # Return if a TimeZone instance, or wrap in a TimeZone instance if a TZInfo::Timezone + time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone.create(time_zone.name, nil, time_zone) + rescue TZInfo::InvalidTimezoneIdentifier + raise ArgumentError, "Invalid Timezone: #{time_zone}" + end + + def find_zone(time_zone) + find_zone!(time_zone) rescue nil + end end # Returns the simultaneous time in <tt>Time.zone</tt>. @@ -74,6 +81,6 @@ class Time def in_time_zone(zone = ::Time.zone) return self unless zone - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.__send__(:get_zone, zone)) + ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) end end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index c2deba3b1b..04df2ea562 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -46,7 +46,7 @@ module ActiveSupport # If assigned value cannot be matched to a TimeZone, an exception will be raised. initializer "active_support.initialize_time_zone" do |app| require 'active_support/core_ext/time/zones' - zone_default = Time.__send__(:get_zone, app.config.time_zone) + zone_default = Time.find_zone!(app.config.time_zone) unless zone_default raise \ diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index fb52fc7083..8d6c27e381 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -5,16 +5,9 @@ require 'active_support/testing/deprecation' require 'active_support/testing/declarative' require 'active_support/testing/pending' require 'active_support/testing/isolation' +require 'active_support/testing/mochaing' require 'active_support/core_ext/kernel/reporting' -begin - silence_warnings { require 'mocha' } -rescue LoadError - # Fake Mocha::ExpectationError so we can rescue it in #run. Bleh. - Object.const_set :Mocha, Module.new - Mocha.const_set :ExpectationError, Class.new(StandardError) -end - module ActiveSupport class TestCase < ::Test::Unit::TestCase if defined? MiniTest diff --git a/activesupport/lib/active_support/testing/mochaing.rb b/activesupport/lib/active_support/testing/mochaing.rb new file mode 100644 index 0000000000..4ad75a6681 --- /dev/null +++ b/activesupport/lib/active_support/testing/mochaing.rb @@ -0,0 +1,7 @@ +begin + silence_warnings { require 'mocha' } +rescue LoadError + # Fake Mocha::ExpectationError so we can rescue it in #run. Bleh. + Object.const_set :Mocha, Module.new + Mocha.const_set :ExpectationError, Class.new(StandardError) +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/testing/pending.rb b/activesupport/lib/active_support/testing/pending.rb index 3d119e2fba..feac7bc347 100644 --- a/activesupport/lib/active_support/testing/pending.rb +++ b/activesupport/lib/active_support/testing/pending.rb @@ -11,32 +11,36 @@ module ActiveSupport @@at_exit = false def pending(description = "", &block) - if description.is_a?(Symbol) - is_pending = $tags[description] - return block.call unless is_pending - end + if defined?(::MiniTest) + skip(description.blank? ? nil : description) + else + if description.is_a?(Symbol) + is_pending = $tags[description] + return block.call unless is_pending + end - if block_given? - failed = false + if block_given? + failed = false - begin - block.call - rescue Exception - failed = true - end + begin + block.call + rescue Exception + failed = true + end - flunk("<#{description}> did not fail.") unless failed - end + flunk("<#{description}> did not fail.") unless failed + end - caller[0] =~ (/(.*):(.*):in `(.*)'/) - @@pending_cases << "#{$3} at #{$1}, line #{$2}" - print "P" + caller[0] =~ (/(.*):(.*):in `(.*)'/) + @@pending_cases << "#{$3} at #{$1}, line #{$2}" + print "P" - @@at_exit ||= begin - at_exit do - puts "\nPending Cases:" - @@pending_cases.each do |test_case| - puts test_case + @@at_exit ||= begin + at_exit do + puts "\nPending Cases:" + @@pending_cases.each do |test_case| + puts test_case + end end end end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index c66aa78ce8..6b9120e51f 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,5 +1,6 @@ require "active_support/values/time_zone" require 'active_support/core_ext/object/acts_like' +require 'active_support/core_ext/object/inclusion' module ActiveSupport # A Time-like class that can represent a time in any time zone. Necessary because standard Ruby Time instances are @@ -309,7 +310,7 @@ module ActiveSupport end def marshal_load(variables) - initialize(variables[0].utc, ::Time.__send__(:get_zone, variables[1]), variables[2].utc) + initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc) end # Ensure proxy class responds to all methods that underlying time instance responds to. @@ -344,7 +345,7 @@ module ActiveSupport end def duration_of_variable_length?(obj) - ActiveSupport::Duration === obj && obj.parts.any? {|p| [:years, :months, :days].include? p[0] } + ActiveSupport::Duration === obj && obj.parts.any? {|p| p[0].among?(:years, :months, :days) } end end end diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb new file mode 100644 index 0000000000..9f2ace0ef7 --- /dev/null +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -0,0 +1,52 @@ +require 'abstract_unit' +require 'active_support/core_ext/object/inclusion' + +class InTest < Test::Unit::TestCase + def test_in_array + assert 1.in?([1,2]) + assert !3.in?([1,2]) + end + + def test_in_hash + h = { "a" => 100, "b" => 200 } + assert "a".in?(h) + assert !"z".in?(h) + end + + def test_in_string + assert "lo".in?("hello") + assert !"ol".in?("hello") + assert ?h.in?("hello") + end + + def test_in_range + assert 25.in?(1..50) + assert !75.in?(1..50) + end + + def test_in_set + s = Set.new([1,2]) + assert 1.in?(s) + assert !3.in?(s) + end + + def test_among + assert 1.among?(1,2,3) + assert !5.among?(1,2,3) + assert [1,2,3].among?([1,2,3], 2, [3,4,5]) + end + + module A + end + class B + include A + end + class C < B + end + + def test_in_module + assert A.in?(B) + assert A.in?(C) + assert !A.in?(A) + end +end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index bafa335a09..72b55183ba 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -36,6 +36,12 @@ class TimeWithZoneTest < Test::Unit::TestCase assert_equal @twz.object_id, @twz.in_time_zone(ActiveSupport::TimeZone['Eastern Time (US & Canada)']).object_id end + def test_in_time_zone_with_bad_argument + assert_raise(ArgumentError) { @twz.in_time_zone('No such timezone exists') } + assert_raise(ArgumentError) { @twz.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @twz.in_time_zone(Object.new) } + end + def test_localtime assert_equal @twz.localtime, @twz.utc.getlocal end @@ -760,6 +766,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase end end + def test_in_time_zone_with_invalid_argument + assert_raise(ArgumentError) { @t.in_time_zone("No such timezone exists") } + assert_raise(ArgumentError) { @dt.in_time_zone("No such timezone exists") } + assert_raise(ArgumentError) { @t.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @dt.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @t.in_time_zone(Object.new) } + assert_raise(ArgumentError) { @dt.in_time_zone(Object.new) } + end + def test_in_time_zone_with_time_local_instance with_env_tz 'US/Eastern' do time = Time.local(1999, 12, 31, 19) # == Time.utc(2000) @@ -790,6 +805,14 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone end + def test_use_zone_raises_on_invalid_timezone + Time.zone = 'Alaska' + assert_raise ArgumentError do + Time.use_zone("No such timezone exists") { } + end + assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone + end + def test_time_zone_getter_and_setter Time.zone = ActiveSupport::TimeZone['Alaska'] assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone @@ -843,11 +866,27 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase end def test_time_zone_setter_with_invalid_zone - Time.zone = 'foo' - assert_nil Time.zone + assert_raise(ArgumentError){ Time.zone = "No such timezone exists" } + assert_raise(ArgumentError){ Time.zone = -15.hours } + assert_raise(ArgumentError){ Time.zone = Object.new } + end + + def test_find_zone_without_bang_returns_nil_if_time_zone_can_not_be_found + assert_nil Time.find_zone('No such timezone exists') + assert_nil Time.find_zone(-15.hours) + assert_nil Time.find_zone(Object.new) + end + + def test_find_zone_with_bang_raises_if_time_zone_can_not_be_found + assert_raise(ArgumentError) { Time.find_zone!('No such timezone exists') } + assert_raise(ArgumentError) { Time.find_zone!(-15.hours) } + assert_raise(ArgumentError) { Time.find_zone!(Object.new) } + end - Time.zone = -15.hours - assert_nil Time.zone + def test_time_zone_setter_with_find_zone_without_bang + assert_nil Time.zone = Time.find_zone('No such timezone exists') + assert_nil Time.zone = Time.find_zone(-15.hours) + assert_nil Time.zone = Time.find_zone(Object.new) end def test_current_returns_time_now_when_zone_not_set diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index b054855d08..90b40b5478 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -1,6 +1,7 @@ # encoding: utf-8 require 'abstract_unit' require 'active_support/inflector/transliterate' +require 'active_support/core_ext/object/inclusion' class TransliterateTest < Test::Unit::TestCase @@ -15,7 +16,7 @@ class TransliterateTest < Test::Unit::TestCase # create string with range of Unicode"s western characters with # diacritics, excluding the division and multiplication signs which for # some reason or other are floating in the middle of all the letters. - string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include? c}.pack("U*") + string = (0xC0..0x17E).to_a.reject {|c| c.among?(0xD7, 0xF7)}.pack("U*") string.each_char do |char| assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(string) end @@ -1,9 +1,7 @@ #!/usr/bin/env ruby -begin - require "rails/cli" -rescue LoadError +if File.exists?(File.join(File.expand_path('../..', __FILE__), '.git')) railties_path = File.expand_path('../../railties/lib', __FILE__) $:.unshift(railties_path) - require "rails/cli" end +require "rails/cli" diff --git a/rails.gemspec b/rails.gemspec index 98b5f46554..4ee814c507 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -17,7 +17,6 @@ Gem::Specification.new do |s| s.bindir = 'bin' s.executables = ['rails'] - s.default_executable = 'rails' s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) diff --git a/railties/CHANGELOG b/railties/CHANGELOG index c1e0a214d2..90faad6131 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,23 @@ *Rails 3.1.0 (unreleased)* +* Changed scaffold and app generator to create Ruby 1.9 style hash when running on Ruby 1.9 [Prem Sichanugrist] + + So instead of creating something like: + + redirect_to users_path, :notice => "User has been created" + + it will now be like this: + + redirect_to users_path, notice: "User has been created" + + You can also passing `--old-style-hash` to make Rails generate old style hash even you're on Ruby 1.9 + +* Changed scaffold_controller generator to create format block for JSON instead of XML [Prem Sichanugrist] + +* Add using Turn with natural language test case names for test_help.rb when running with minitest (Ruby 1.9.2+) [DHH] + +* Direct logging of Active Record to STDOUT so it's shown inline with the results in the console [DHH] + * Added `config.force_ssl` configuration which loads Rack::SSL middleware and force all requests to be under HTTPS protocol [DHH, Prem Sichanugrist, and Josh Peek] * Added `rails plugin new` command which generates rails plugin with gemspec, tests and dummy application for testing [Piotr Sarnacki] diff --git a/railties/guides/rails_guides/textile_extensions.rb b/railties/guides/rails_guides/textile_extensions.rb index affef1ccb0..9beeefb551 100644 --- a/railties/guides/rails_guides/textile_extensions.rb +++ b/railties/guides/rails_guides/textile_extensions.rb @@ -1,9 +1,11 @@ +require 'active_support/core_ext/object/inclusion' + module RailsGuides module TextileExtensions def notestuff(body) body.gsub!(/^(IMPORTANT|CAUTION|WARNING|NOTE|INFO)[.:](.*)$/) do |m| css_class = $1.downcase - css_class = 'warning' if ['caution', 'important'].include?(css_class) + css_class = 'warning' if css_class.among?('caution', 'important') result = "<div class='#{css_class}'><p>" result << $2.strip @@ -33,7 +35,7 @@ module RailsGuides def code(body) body.gsub!(%r{<(yaml|shell|ruby|erb|html|sql|plain)>(.*?)</\1>}m) do |m| es = ERB::Util.h($2) - css_class = ['erb', 'shell'].include?($1) ? 'html' : $1 + css_class = $1.among?('erb', 'shell') ? 'html' : $1 %{<notextile><div class="code_container"><code class="#{css_class}">#{es}</code></div></notextile>} end end diff --git a/railties/guides/source/2_3_release_notes.textile b/railties/guides/source/2_3_release_notes.textile index 67743a4797..15abba66ab 100644 --- a/railties/guides/source/2_3_release_notes.textile +++ b/railties/guides/source/2_3_release_notes.textile @@ -410,7 +410,7 @@ You're likely familiar with Rails' practice of adding timestamps to static asset h4. Asset Hosts as Objects -Asset hosts get more flexible in edge Rails with the ability to declare an asset host as a specific object that responds to a call. This allows you to to implement any complex logic you need in your asset hosting. +Asset hosts get more flexible in edge Rails with the ability to declare an asset host as a specific object that responds to a call. This allows you to implement any complex logic you need in your asset hosting. * More Information: "asset-hosting-with-minimum-ssl":http://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index 178d98c2d6..496dc7224b 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -615,26 +615,15 @@ Rails comes with two built-in HTTP authentication mechanisms: h4. HTTP Basic Authentication -HTTP basic authentication is an authentication scheme that is supported by the majority of browsers and other HTTP clients. As an example, consider an administration section which will only be available by entering a username and a password into the browser's HTTP basic dialog window. Using the built-in authentication is quite easy and only requires you to use one method, +authenticate_or_request_with_http_basic+. +HTTP basic authentication is an authentication scheme that is supported by the majority of browsers and other HTTP clients. As an example, consider an administration section which will only be available by entering a username and a password into the browser's HTTP basic dialog window. Using the built-in authentication is quite easy and only requires you to use one method, +http_basic_authenticate_with+. <ruby> class AdminController < ApplicationController - USERNAME, PASSWORD = "humbaba", "5baa61e4" - - before_filter :authenticate - - private - - def authenticate - authenticate_or_request_with_http_basic do |username, password| - username == USERNAME && - Digest::SHA1.hexdigest(password) == PASSWORD - end - end + http_basic_authenticate_with :name => "humbaba", :password => "5baa61e4" end </ruby> -With this in place, you can create namespaced controllers that inherit from +AdminController+. The before filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication. +With this in place, you can create namespaced controllers that inherit from +AdminController+. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication. h4. HTTP Digest Authentication diff --git a/railties/guides/source/action_view_overview.textile b/railties/guides/source/action_view_overview.textile index cfd71ad287..d0b3ee6bfc 100644 --- a/railties/guides/source/action_view_overview.textile +++ b/railties/guides/source/action_view_overview.textile @@ -1295,7 +1295,7 @@ end h5. update_page_tag -Works like update_page but wraps the generated JavaScript in a +script+ tag. Use this to include generated JavaScript in an ERb template. +Works like update_page but wraps the generated JavaScript in a +script+ tag. Use this to include generated JavaScript in an ERB template. h4. PrototypeHelper::JavaScriptGenerator::GeneratorMethods diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index f3a10b8b92..df8e35ed33 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -790,7 +790,7 @@ Post.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) + 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) </ruby> If there was no +where+ condition, this would generate the normal set of two queries. diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 788f528654..3ba840c044 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -442,6 +442,28 @@ require_library_or_gem('mysql') NOTE: Defined in +active_support/core_ext/kernel/requires.rb+. +h4. +in?+ and +either?+ + +The predicate +in?+ tests if an object is included in another object, and the predicate +either?+ tests if an object is included in a list of objects which will be passed as arguments. + +Examples of +in?+: + +<ruby> + 1.in?([1,2]) # => true + "lo".in?("hello") # => true + 25.in?(30..50) # => false +</ruby> + +Examples of +either?+: + +<ruby> + 1.either?(1,2,3) # => true + 5.either?(1,2,3) # => false + [1,2,3].either?([1,2,3], 2, [3,4,5]) # => true +</ruby> + +NOTE: Defined in +active_support/core_ext/object/inclusion.rb+. + h3. Extensions to +Module+ h4. +alias_method_chain+ diff --git a/railties/guides/source/api_documentation_guidelines.textile b/railties/guides/source/api_documentation_guidelines.textile index 7433507866..e22ffa4c04 100644 --- a/railties/guides/source/api_documentation_guidelines.textile +++ b/railties/guides/source/api_documentation_guidelines.textile @@ -29,7 +29,7 @@ Documentation has to be concise but comprehensive. Explore and document edge cas The proper names of Rails components have a space in between the words, like "Active Support". +ActiveRecord+ is a Ruby module, whereas Active Record is an ORM. All Rails documentation should consistently refer to Rails components by their proper name, and if in your next blog post or presentation you remember this tidbit and take it into account that'd be phenomenal. -Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERb. When in doubt, please have a look at some authoritative source like their official documentation. +Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. When in doubt, please have a look at some authoritative source like their official documentation. Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile index 581fece1ab..f3e8d880df 100644 --- a/railties/guides/source/command_line.textile +++ b/railties/guides/source/command_line.textile @@ -484,7 +484,7 @@ end We take whatever args are supplied, save them to an instance variable, and literally copying from the Rails source, implement a +manifest+ method, which calls +record+ with a block, and we: * Check there's a *public* directory. You bet there is. -* Run the ERb template called "tutorial.erb". +* Run the ERB template called "tutorial.erb". * Save it into "Rails.root/public/tutorial.txt". * Pass in the arguments we saved through the +:assigns+ parameter. diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 298335d484..9ca567129b 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -149,7 +149,7 @@ h4. Configuring Middleware Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: -* +Rack::SSL+ Will force every requests to be under HTTPS protocol. Will be available if +config.force_ssl+ is set to _true_. +* +Rack::SSL+ Will force every request to be under HTTPS protocol. Will be available if +config.force_ssl+ is set to _true_. * +ActionDispatch::Static+ is used to serve static assets. Disabled if +config.serve_static_assets+ is _true_. * +Rack::Lock+ Will wrap the app in mutex so it can only be called by a single thread at a time. Only enabled if +config.action_controller.allow_concurrency+ is set to _false_, which it is by default. * +ActiveSupport::Cache::Strategy::LocalCache+ Serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index 0661549644..5906f953bf 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -501,8 +501,8 @@ def index @posts = Post.all respond_to do |format| - format.html # index.html.erb - format.xml { render :xml => @posts } + format.html # index.html.erb + format.json { render :json => @posts } end end </ruby> @@ -511,7 +511,7 @@ end TIP: For more information on finding records with Active Record, see "Active Record Query Interface":active_record_querying.html. -The +respond_to+ block handles both HTML and XML calls to this action. If you browse to "http://localhost:3000/posts.xml":http://localhost:3000/posts.xml, you'll see all of the posts in XML format. The HTML format looks for a view in +app/views/posts/+ with a name that corresponds to the action name. Rails makes all of the instance variables from the action available to the view. Here's +app/views/posts/index.html.erb+: +The +respond_to+ block handles both HTML and JSON calls to this action. If you browse to "http://localhost:3000/posts.json":http://localhost:3000/posts.json, you'll see a JSON containing all of the posts. The HTML format looks for a view in +app/views/posts/+ with a name that corresponds to the action name. Rails makes all of the instance variables from the action available to the view. Here's +app/views/posts/index.html.erb+: <erb> <h1>Listing posts</h1> @@ -584,8 +584,8 @@ def new @post = Post.new respond_to do |format| - format.html # new.html.erb - format.xml { render :xml => @post } + format.html # new.html.erb + format.json { render :json => @post } end end </ruby> @@ -653,13 +653,13 @@ def create respond_to do |format| if @post.save - format.html { redirect_to(@post, + format.html { redirect_to(@post, :notice => 'Post was successfully created.') } - format.xml { render :xml => @post, + format.json { render :json => @post, :status => :created, :location => @post } else - format.html { render :action => "new" } - format.xml { render :xml => @post.errors, + format.html { render :action => "new" } + format.json { render :json => @post.errors, :status => :unprocessable_entity } end end @@ -681,8 +681,8 @@ def show @post = Post.find(params[:id]) respond_to do |format| - format.html # show.html.erb - format.xml { render :xml => @post } + format.html # show.html.erb + format.json { render :json => @post } end end </ruby> @@ -743,12 +743,12 @@ def update respond_to do |format| if @post.update_attributes(params[:post]) - format.html { redirect_to(@post, + format.html { redirect_to(@post, :notice => 'Post was successfully updated.') } - format.xml { head :ok } + format.json { render :json => {}, :status => :ok } else - format.html { render :action => "edit" } - format.xml { render :xml => @post.errors, + format.html { render :action => "edit" } + format.json { render :json => @post.errors, :status => :unprocessable_entity } end end @@ -767,8 +767,8 @@ def destroy @post.destroy respond_to do |format| - format.html { redirect_to(posts_url) } - format.xml { head :ok } + format.html { redirect_to(posts_url) } + format.json { render :json => {}, :status => :ok } end end </ruby> @@ -1201,36 +1201,19 @@ h3. Security If you were to publish your blog online, anybody would be able to add, edit and delete posts or delete comments. -Rails provides a very simple HTTP authentication system that will work nicely in this situation. First, we enable simple HTTP based authentication in our <tt>app/controllers/application_controller.rb</tt>: +Rails provides a very simple HTTP authentication system that will work nicely in this situation. -<ruby> -class ApplicationController < ActionController::Base - protect_from_forgery - - private - - def authenticate - authenticate_or_request_with_http_basic do |user_name, password| - user_name == 'admin' && password == 'password' - end - end - -end -</ruby> - -You can obviously change the username and password to whatever you want. We put this method inside of +ApplicationController+ so that it is available to all of our controllers. - -Then in the +PostsController+ we need to have a way to block access to the various actions if the person is not authenticated, here we can use the Rails <tt>before_filter</tt> method, which allows us to specify that Rails must run a method and only then allow access to the requested action if that method allows it. +In the +PostsController+ we need to have a way to block access to the various actions if the person is not authenticated, here we can use the Rails <tt>http_basic_authenticate_with</tt> method, allowing access to the requested action if that method allows it. -To use the before filter, we specify it at the top of our +PostsController+, in this case, we want the user to be authenticated on every action, except for +index+ and +show+, so we write that: +To use the authentication system, we specify it at the top of our +PostsController+, in this case, we want the user to be authenticated on every action, except for +index+ and +show+, so we write that: <ruby> class PostsController < ApplicationController - before_filter :authenticate, :except => [:index, :show] + http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index # GET /posts - # GET /posts.xml + # GET /posts.json def index @posts = Post.all respond_to do |format| @@ -1242,7 +1225,7 @@ We also only want to allow authenticated users to delete comments, so in the +Co <ruby> class CommentsController < ApplicationController - before_filter :authenticate, :only => :destroy + http_basic_authenticate_with :name => "dhh", :password => "secret", :only => :destroy def create @post = Post.find(params[:post_id]) @@ -1475,6 +1458,7 @@ Two very common sources of data that are not UTF-8: h3. Changelog +* April 11, 2011: Changed scaffold_controller generator to create format block for JSON instead of XML "Sebastian Martinez":http://www.wyeworks.com * August 30, 2010: Minor editing after Rails 3 release by "Joost Baaij":http://www.spacebabies.nl * July 12, 2010: Fixes, editing and updating of code samples by "Jaime Iniesta":http://jaimeiniesta.com * May 16, 2010: Added a section on configuration gotchas to address common encoding problems that people might have by "Yehuda Katz":http://www.yehudakatz.com diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 0cbbe1f389..7c01f01b24 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -478,8 +478,7 @@ The next line in +config/application.rb+ is: require 'rails/all' </ruby> -h4 +railties/lib/rails/all.rb+ - +h4. +railties/lib/rails/all.rb+ This file is responsible for requiring all the individual parts of Rails like so: @@ -591,7 +590,7 @@ h4. +activesupport/lib/active_support/deprecation/behaviors.rb+ This file defines the behavior of the +ActiveSupport::Deprecation+ module, setting up the +DEFAULT_BEHAVIORS+ hash constant which contains the three defaults to outputting deprecation warnings: +:stderr+, +:log+ and +:notify+. This file begins by requiring +activesupport/notifications+ and +activesupport/core_ext/array/wrap+. -h4 +activesupport/lib/active_support/notifications.rb+ +h4. +activesupport/lib/active_support/notifications.rb+ TODO: document +ActiveSupport::Notifications+. diff --git a/railties/guides/source/layout.html.erb b/railties/guides/source/layout.html.erb index f2681c6461..911655e0f4 100644 --- a/railties/guides/source/layout.html.erb +++ b/railties/guides/source/layout.html.erb @@ -111,7 +111,7 @@ <h3>Feedback</h3> <p> - You're encouraged to help in keeping the quality of this guide. + You're encouraged to help improve the quality of this guide. </p> <p> If you see any typos or factual errors you are confident to diff --git a/railties/guides/source/ruby_on_rails_guides_guidelines.textile b/railties/guides/source/ruby_on_rails_guides_guidelines.textile index 6576758856..8e55780dca 100644 --- a/railties/guides/source/ruby_on_rails_guides_guidelines.textile +++ b/railties/guides/source/ruby_on_rails_guides_guidelines.textile @@ -10,10 +10,10 @@ Guides are written in "Textile":http://www.textism.com/tools/textile/. There's c h3. Prologue -Each guide should start with motivational text at the top. That's the little introduction in the blue area. The prologue should tell the readers what's the guide about, and what will they learn. See for example the "Routing Guide":routing.html. +Each guide should start with motivational text at the top (that's the little introduction in the blue area.) The prologue should tell the reader what the guide is about, and what they will learn. See for example the "Routing Guide":routing.html. h3. Titles - + The title of every guide uses +h2+, guide sections use +h3+, subsections +h4+, etc. Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be: @@ -23,7 +23,7 @@ h5. Middleware Stack is an Array h5. When are Objects Saved? </plain> -Use same typography as in regular text: +Use the same typography as in regular text: <plain> h6. The +:content_type+ Option @@ -42,13 +42,13 @@ Those guidelines apply also to guides. h3. HTML Generation -To generate all the guides just cd into the +railties+ directory and execute +To generate all the guides, just +cd+ into the +railties+ directory and execute: <plain> bundle exec rake generate_guides </plain> -You'll need the gems erubis, i18n, and RedCloth. +(You may need to run +bundle install+ first to install the required gems.) To process +my_guide.textile+ and nothing else use the +ONLY+ environment variable: @@ -56,13 +56,13 @@ To process +my_guide.textile+ and nothing else use the +ONLY+ environment variab bundle exec rake generate_guides ONLY=my_guide </plain> -Although by default guides that have not been modified are not processed, so +ONLY+ is rarely needed in practice. +By default, guides that have not been modified are not processed, so +ONLY+ is rarely needed in practice. To force process of all the guides, pass +ALL=1+. -It is also recommended that you work with +WARNINGS=1+, this detects duplicate IDs and warns about broken internal links. +It is also recommended that you work with +WARNINGS=1+. This detects duplicate IDs and warns about broken internal links. -If you want to generate guides in languages other than English, you can keep them in a separate directory under +source+ (eg. <tt>source/es</tt>) and use the +LANGUAGE+ environment variable. +If you want to generate guides in languages other than English, you can keep them in a separate directory under +source+ (eg. <tt>source/es</tt>) and use the +LANGUAGE+ environment variable: <plain> rake generate_guides LANGUAGE=es @@ -70,7 +70,7 @@ rake generate_guides LANGUAGE=es h3. HTML Validation -Please do validate the generated HTML with +Please validate the generated HTML with: <plain> rake validate_guides @@ -80,4 +80,5 @@ Particularly, titles get an ID generated from their content and this often leads h3. Changelog +* March 31, 2011: grammar tweaks by "Josiah Ivey":http://twitter.com/josiahivey * October 5, 2010: ported from the docrails wiki and revised by "Xavier Noria":credits.html#fxn diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile index d3f72509c6..2809c6d076 100644 --- a/railties/guides/source/testing.textile +++ b/railties/guides/source/testing.textile @@ -79,9 +79,9 @@ steve: Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are separated by a blank space. You can place comments in a fixture file by using the # character in the first column. -h5. ERb'in It Up +h5. ERB'in It Up -ERb allows you to embed ruby code within templates. Both the YAML and CSV fixture formats are pre-processed with ERb when you load fixtures. This allows you to use Ruby to help you generate some sample data. +ERB allows you to embed ruby code within templates. Both the YAML and CSV fixture formats are pre-processed with ERB when you load fixtures. This allows you to use Ruby to help you generate some sample data. <erb> <% earth_size = 20 %> @@ -391,7 +391,7 @@ There are a bunch of different types of assertions you can use. Here's the compl |+assert_nil( obj, [msg] )+ |Ensures that +obj.nil?+ is true.| |+assert_not_nil( obj, [msg] )+ |Ensures that +obj.nil?+ is false.| |+assert_match( regexp, string, [msg] )+ |Ensures that a string matches the regular expression.| -|+assert_no_match( regexp, string, [msg] )+ |Ensures that a string doesn't matches the regular expression.| +|+assert_no_match( regexp, string, [msg] )+ |Ensures that a string doesn't match the regular expression.| |+assert_in_delta( expecting, actual, delta, [msg] )+ |Ensures that the numbers +expecting+ and +actual+ are within +delta+ of each other.| |+assert_throws( symbol, [msg] ) { block }+ |Ensures that the given block throws the symbol.| |+assert_raise( exception1, exception2, ... ) { block }+ |Ensures that the given block raises one of the given exceptions.| @@ -748,7 +748,8 @@ You don't need to set up and run your tests by hand on a test-by-test basis. Rai h3. Brief Note About +Test::Unit+ -Ruby ships with a boat load of libraries. One little gem of a library is +Test::Unit+, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in +Test::Unit::Assertions+. The class +ActiveSupport::TestCase+ which we have been using in our unit and functional tests extends +Test::Unit::TestCase+ that it is how we can use all the basic assertions in our tests. +Ruby ships with a boat load of libraries. One little gem of a library is +Test::Unit+, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in +Test::Unit::Assertions+. The class +ActiveSupport::TestCase+ which we have been using in our unit and functional tests extends +Test::Unit::TestCase+, allowing +us to use all of the basic assertions in our tests. NOTE: For more information on +Test::Unit+, refer to "test/unit Documentation":http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/ diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 02ccdf8060..7108d4157e 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + ARGV << '--help' if ARGV.empty? aliases = { @@ -69,7 +71,7 @@ when '--version', '-v' require 'rails/commands/application' else - puts "Error: Command not recognized" unless %w(-h --help).include?(command) + puts "Error: Command not recognized" unless command.among?('-h', '--help') puts <<-EOT Usage: rails COMMAND [ARGS] diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index 3b57b925ba..f3fa1fd54d 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -1,5 +1,6 @@ require 'rails/version' -if %w(--version -v).include? ARGV.first + +if ['--version', '-v'].include?(ARGV.first) puts "Rails #{Rails::VERSION::STRING}" exit(0) end diff --git a/railties/lib/rails/commands/benchmarker.rb b/railties/lib/rails/commands/benchmarker.rb index 0432261802..e10ec8fd38 100644 --- a/railties/lib/rails/commands/benchmarker.rb +++ b/railties/lib/rails/commands/benchmarker.rb @@ -1,4 +1,6 @@ -if [nil, "-h", "--help"].include?(ARGV.first) +require 'active_support/core_ext/object/inclusion' + +if ARGV.first.among?(nil, "-h", "--help") puts "Usage: rails benchmarker [times] 'Person.expensive_way' 'Person.another_expensive_way' ..." exit 1 end diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index de2f190ad5..2b7faf9715 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -34,6 +34,10 @@ module Rails exit end end + + if defined?(ActiveRecord) + ActiveRecord::Base.logger = Logger.new(STDERR) + end if options[:sandbox] puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})" diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb index db59cd8ad9..d082ba592c 100644 --- a/railties/lib/rails/commands/destroy.rb +++ b/railties/lib/rails/commands/destroy.rb @@ -1,7 +1,9 @@ require 'rails/generators' +require 'active_support/core_ext/object/inclusion' + Rails::Generators.configure! -if [nil, "-h", "--help"].include?(ARGV.first) +if ARGV.first.among?(nil, "-h", "--help") Rails::Generators.help 'destroy' exit end diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb index 1b3eef504a..789e5ebedb 100644 --- a/railties/lib/rails/commands/generate.rb +++ b/railties/lib/rails/commands/generate.rb @@ -1,7 +1,9 @@ require 'rails/generators' +require 'active_support/core_ext/object/inclusion' + Rails::Generators.configure! -if [nil, "-h", "--help"].include?(ARGV.first) +if ARGV.first.among?(nil, "-h", "--help") Rails::Generators.help 'generate' exit end diff --git a/railties/lib/rails/commands/profiler.rb b/railties/lib/rails/commands/profiler.rb index 6d9717b5cd..d3195a204e 100644 --- a/railties/lib/rails/commands/profiler.rb +++ b/railties/lib/rails/commands/profiler.rb @@ -1,4 +1,6 @@ -if [nil, "-h", "--help"].include?(ARGV.first) +require 'active_support/core_ext/object/inclusion' + +if ARGV.first.among?(nil, "-h", "--help") $stderr.puts "Usage: rails profiler 'Person.expensive_method(10)' [times] [flat|graph|graph_html]" exit(1) end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index a2eaf7a6fb..93db5106ce 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -53,6 +53,9 @@ module Rails class_option :help, :type => :boolean, :aliases => "-h", :group => :rails, :desc => "Show this help message and quit" + + class_option :old_style_hash, :type => :boolean, :default => false, + :desc => "Force using old style hash (:foo => 'bar') on Ruby >= 1.9" end def initialize(*args) @@ -174,6 +177,15 @@ module Rails create_file("#{destination}/.gitkeep") unless options[:skip_git] end + # Returns Ruby 1.9 style key-value pair if current code is running on + # Ruby 1.9.x. Returns the old-style (with hash rocket) otherwise. + def key_value(key, value) + if options[:old_style_hash] || RUBY_VERSION < '1.9' + ":#{key} => #{value}" + else + "#{key}: #{value}" + end + end end end end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index dfecd2a6e4..2b0eaea3a4 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -8,6 +8,7 @@ rescue LoadError end require 'rails/generators/actions' +require 'active_support/core_ext/object/inclusion' module Rails module Generators @@ -164,7 +165,7 @@ module Rails names.each do |name| defaults = if options[:type] == :boolean { } - elsif [true, false].include?(default_value_for_option(name, options)) + elsif default_value_for_option(name, options).among?(true, false) { :banner => "" } else { :desc => "#{name.to_s.humanize} to be invoked", :banner => "NAME" } diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 4c46db4d67..a7393cfe18 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -17,7 +17,7 @@ <% end -%> <td><%%= link_to 'Show', <%= singular_table_name %> %></td> <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td> - <td><%%= link_to 'Destroy', <%= singular_table_name %>, :confirm => 'Are you sure?', :method => :delete %></td> + <td><%%= link_to 'Destroy', <%= singular_table_name %>, <%= key_value :confirm, "'Are you sure?'" %>, <%= key_value :method, ":delete" %> %></td> </tr> <%% end %> </table> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 64273e4ca4..f85375b2a3 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -1,4 +1,5 @@ require 'active_support/time' +require 'active_support/core_ext/object/inclusion' module Rails module Generators @@ -44,7 +45,7 @@ module Rails end def reference? - [ :references, :belongs_to ].include?(self.type) + self.type.among?(:references, :belongs_to) end end end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 2af7f85463..36bc9e055c 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -8,6 +8,9 @@ module Rails class_option :skip_namespace, :type => :boolean, :default => false, :desc => "Skip namespace (affects only isolated applications)" + class_option :old_style_hash, :type => :boolean, :default => false, + :desc => "Force using old style hash (:foo => 'bar') on Ruby >= 1.9" + def initialize(args, *options) #:nodoc: # Unfreeze name in case it's given as a frozen string args[0] = args[0].dup if args[0].is_a?(String) && args[0].frozen? @@ -181,6 +184,16 @@ module Rails class_collisions "#{options[:prefix]}#{name}#{options[:suffix]}" end end + + # Returns Ruby 1.9 style key-value pair if current code is running on + # Ruby 1.9.x. Returns the old-style (with hash rocket) otherwise. + def key_value(key, value) + if options[:old_style_hash] || RUBY_VERSION < '1.9' + ":#{key} => #{value}" + else + "#{key}: #{value}" + end + end end end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index c383d4842f..61daefef90 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -23,9 +23,13 @@ source 'http://rubygems.org' # Bundle gems for the local environment. Make sure to # put test-only gems in this group so their generators # and rake tasks are available in development mode: -# group :development, :test do -# gem 'webrat' -# end + +group :development, :test do + # Depend "turn" for pretty printing test output, but disable autorequire. + gem 'turn', :require => false + + # gem 'webrat' +end # Needed for guides generation # gem "RedCloth", "~> 4.2" diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt index 62aa06dc3e..ddfe4ba1e1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt @@ -1,6 +1,6 @@ # Be sure to restart your server when you modify this file. -<%= app_const %>.config.session_store :cookie_store, :key => '_<%= app_name %>_session' +<%= app_const %>.config.session_store :cookie_store, <%= key_value :key, "'_#{app_name}_session'" %> # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information diff --git a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb index 664d8c74c8..9a2efa68a7 100644 --- a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb +++ b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb @@ -3,5 +3,5 @@ # # Examples: # -# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) -# Mayor.create(:name => 'Daley', :city => cities.first) +# cities = City.create([{ <%= key_value :name, "'Chicago'" %> }, { <%= key_value :name, "'Copenhagen'" %> }]) +# Mayor.create(<%= key_value :name, "'Daley'" %>, <%= key_value :city, "cities.first" %>) diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index b5317a055b..32b961d9fc 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -1,35 +1,35 @@ <% module_namespacing do -%> class <%= controller_class_name %>Controller < ApplicationController # GET <%= route_url %> - # GET <%= route_url %>.xml + # GET <%= route_url %>.json def index @<%= plural_table_name %> = <%= orm_class.all(class_name) %> respond_to do |format| format.html # index.html.erb - format.xml { render :xml => @<%= plural_table_name %> } + format.json { render <%= key_value :json, "@#{plural_table_name}" %> } end end # GET <%= route_url %>/1 - # GET <%= route_url %>/1.xml + # GET <%= route_url %>/1.json def show @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> respond_to do |format| format.html # show.html.erb - format.xml { render :xml => @<%= singular_table_name %> } + format.json { render <%= key_value :json, "@#{singular_table_name}" %> } end end # GET <%= route_url %>/new - # GET <%= route_url %>/new.xml + # GET <%= route_url %>/new.json def new @<%= singular_table_name %> = <%= orm_class.build(class_name) %> respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @<%= singular_table_name %> } + format.json { render <%= key_value :json, "@#{singular_table_name}" %> } end end @@ -39,46 +39,46 @@ class <%= controller_class_name %>Controller < ApplicationController end # POST <%= route_url %> - # POST <%= route_url %>.xml + # POST <%= route_url %>.json def create @<%= singular_table_name %> = <%= orm_class.build(class_name, "params[:#{singular_table_name}]") %> respond_to do |format| if @<%= orm_instance.save %> - format.html { redirect_to(@<%= singular_table_name %>, :notice => '<%= human_name %> was successfully created.') } - format.xml { render :xml => @<%= singular_table_name %>, :status => :created, :location => @<%= singular_table_name %> } + format.html { redirect_to @<%= singular_table_name %>, <%= key_value :notice, "'#{human_name} was successfully created.'" %> } + format.json { render <%= key_value :json, "@#{singular_table_name}" %>, <%= key_value :status, ':created' %>, <%= key_value :location, "@#{singular_table_name}" %> } else - format.html { render :action => "new" } - format.xml { render :xml => @<%= orm_instance.errors %>, :status => :unprocessable_entity } + format.html { render <%= key_value :action, '"new"' %> } + format.json { render <%= key_value :json, "@#{orm_instance.errors}" %>, <%= key_value :status, ':unprocessable_entity' %> } end end end # PUT <%= route_url %>/1 - # PUT <%= route_url %>/1.xml + # PUT <%= route_url %>/1.json def update @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> respond_to do |format| if @<%= orm_instance.update_attributes("params[:#{singular_table_name}]") %> - format.html { redirect_to(@<%= singular_table_name %>, :notice => '<%= human_name %> was successfully updated.') } - format.xml { head :ok } + format.html { redirect_to @<%= singular_table_name %>, <%= key_value :notice, "'#{human_name} was successfully updated.'" %> } + format.json { head :ok } else - format.html { render :action => "edit" } - format.xml { render :xml => @<%= orm_instance.errors %>, :status => :unprocessable_entity } + format.html { render <%= key_value :action, '"edit"' %> } + format.json { render <%= key_value :json, "@#{orm_instance.errors}" %>, <%= key_value :status, ':unprocessable_entity' %> } end end end # DELETE <%= route_url %>/1 - # DELETE <%= route_url %>/1.xml + # DELETE <%= route_url %>/1.json def destroy @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> @<%= orm_instance.destroy %> respond_to do |format| - format.html { redirect_to(<%= index_helper %>_url) } - format.xml { head :ok } + format.html { redirect_to <%= index_helper %>_url } + format.json { head :ok } end end end diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb index 964d59d84c..01fe6dda7a 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb @@ -19,30 +19,30 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase test "should create <%= singular_table_name %>" do assert_difference('<%= class_name %>.count') do - post :create, :<%= singular_table_name %> => @<%= singular_table_name %>.attributes + post :create, <%= key_value singular_table_name, "@#{singular_table_name}.attributes" %> end assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) end test "should show <%= singular_table_name %>" do - get :show, :id => @<%= singular_table_name %>.to_param + get :show, <%= key_value :id, "@#{singular_table_name}.to_param" %> assert_response :success end test "should get edit" do - get :edit, :id => @<%= singular_table_name %>.to_param + get :edit, <%= key_value :id, "@#{singular_table_name}.to_param" %> assert_response :success end test "should update <%= singular_table_name %>" do - put :update, :id => @<%= singular_table_name %>.to_param, :<%= singular_table_name %> => @<%= singular_table_name %>.attributes + put :update, <%= key_value :id, "@#{singular_table_name}.to_param" %>, <%= key_value singular_table_name, "@#{singular_table_name}.attributes" %> assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) end test "should destroy <%= singular_table_name %>" do assert_difference('<%= class_name %>.count', -1) do - delete :destroy, :id => @<%= singular_table_name %>.to_param + delete :destroy, <%= key_value :id, "@#{singular_table_name}.to_param" %> end assert_redirected_to <%= index_helper %>_path diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 591fd6f6bd..302206bbcd 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -11,7 +11,7 @@ # # Annotations are looked for in comments and modulus whitespace they have to # start with the tag optionally followed by a colon. Everything up to the end -# of the line (or closing ERb comment tag) is considered to be their text. +# of the line (or closing ERB comment tag) is considered to be their text. class SourceAnnotationExtractor class Annotation < Struct.new(:line, :tag, :text) @@ -99,4 +99,4 @@ class SourceAnnotationExtractor puts end end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 00029e627e..41485c8bac 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -13,6 +13,18 @@ if defined?(Test::Unit::Util::BacktraceFilter) && ENV['BACKTRACE'].nil? Test::Unit::Util::BacktraceFilter.module_eval { include Rails::BacktraceFilterForTestUnit } end +if defined?(MiniTest) + # Enable turn if it is available + begin + require 'turn' + + if MiniTest::Unit.respond_to?(:use_natural_language_case_names=) + MiniTest::Unit.use_natural_language_case_names = true + end + rescue LoadError + end +end + if defined?(ActiveRecord) require 'active_record/test_case' diff --git a/railties/railties.gemspec b/railties/railties.gemspec index c51fe856be..cd0646b8ed 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -17,7 +17,6 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.rdoc_options << '--exclude' << '.' - s.has_rdoc = false s.add_dependency('rake', '>= 0.8.7') s.add_dependency('thor', '~> 0.14.4') diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 044fd2a278..62697b1bf9 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1,5 +1,18 @@ require "isolation/abstract_unit" +class ::MyMailInterceptor + def self.delivering_email(email); email; end +end + +class ::MyOtherMailInterceptor < ::MyMailInterceptor; end + +class ::MyMailObserver + def self.delivered_email(email); email; end +end + +class ::MyOtherMailObserver < ::MyMailObserver; end + + module ApplicationTests class ConfigurationTest < Test::Unit::TestCase include ActiveSupport::Testing::Isolation @@ -245,6 +258,80 @@ module ApplicationTests assert_equal res, last_response.body # value should be unchanged end + test "registers interceptors with ActionMailer" do + add_to_config <<-RUBY + config.action_mailer.interceptors = MyMailInterceptor + RUBY + + require "#{app_path}/config/environment" + require "mail" + + ActionMailer::Base + + assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") + end + + test "registers multiple interceptors with ActionMailer" do + add_to_config <<-RUBY + config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"] + RUBY + + require "#{app_path}/config/environment" + require "mail" + + ActionMailer::Base + + assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") + end + + test "registers observers with ActionMailer" do + add_to_config <<-RUBY + config.action_mailer.observers = MyMailObserver + RUBY + + require "#{app_path}/config/environment" + require "mail" + + ActionMailer::Base + + assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") + end + + test "registers multiple observers with ActionMailer" do + add_to_config <<-RUBY + config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"] + RUBY + + require "#{app_path}/config/environment" + require "mail" + + ActionMailer::Base + + assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") + end + + test "valid timezone is setup correctly" do + add_to_config <<-RUBY + config.root = "#{app_path}" + config.time_zone = "Wellington" + RUBY + + require "#{app_path}/config/environment" + + assert_equal "Wellington", Rails.application.config.time_zone + end + + test "raises when an invalid timezone is defined in the config" do + add_to_config <<-RUBY + config.root = "#{app_path}" + config.time_zone = "That big hill over yonder hill" + RUBY + + assert_raise(ArgumentError) do + require "#{app_path}/config/environment" + end + end + test "config.action_controller.perform_caching = false" do make_basic_app do |app| app.config.action_controller.perform_caching = false diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 018c2fa6bf..43f2fbd71c 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -216,6 +216,24 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_new_hash_style + run_generator [destination_root] + assert_file "config/initializers/session_store.rb" do |file| + if RUBY_VERSION < "1.9" + assert_match /config.session_store :cookie_store, :key => '_.+_session'/, file + else + assert_match /config.session_store :cookie_store, key: '_.+_session'/, file + end + end + end + + def test_force_old_style_hash + run_generator [destination_root, "--old-style-hash"] + assert_file "config/initializers/session_store.rb" do |file| + assert_match /config.session_store :cookie_store, :key => '_.+_session'/, file + end + end + protected def action(*args, &block) diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index d55ed22975..c7f45a807d 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -122,4 +122,22 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase ensure Unknown::Generators.send :remove_const, :ActiveModel end + + def test_new_hash_style + run_generator + assert_file "app/controllers/users_controller.rb" do |content| + if RUBY_VERSION < "1.9" + assert_match /\{ render :action => "new" \}/, content + else + assert_match /\{ render action: "new" \}/, content + end + end + end + + def test_force_old_style_hash + run_generator ["User", "--old-style-hash"] + assert_file "app/controllers/users_controller.rb" do |content| + assert_match /\{ render :action => "new" \}/, content + end + end end |