diff options
18 files changed, 450 insertions, 75 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index da89a7a43c..89c92730cd 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,8 +1,8 @@ ## Rails 4.0.0 (unreleased) ## -* Remove `:confirm` in favor of `:data => { :confirm => "Text" }` option for `button_to`, `button_tag`, `image_submit_tag`, `link_to` and `submit_tag` helpers. +* Deprecate `:confirm` in favor of `:data => { :confirm => "Text" }` option for `button_to`, `button_tag`, `image_submit_tag`, `link_to` and `submit_tag` helpers. - *Carlos Galdino* + *Carlos Galdino + Rafael Mendonça França* * Show routes in exception page while debugging a `RoutingError` in development. *Richard Schneeman and Mattt Thompson* @@ -95,7 +95,7 @@ * Templates without a handler extension now raises a deprecation warning but still defaults to ERb. In future releases, it will simply return the template contents. *Steve Klabnik* -* Remove `:disable_with` in favor of `'data-disable-with'` option from `submit_tag`, `button_tag` and `button_to` helpers. +* Deprecate `:disable_with` in favor of `:data => { :disable_with => "Text" }` option from `submit_tag`, `button_tag` and `button_to` helpers. *Carlos Galdino + Rafael Mendonça França* diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 4b8484bfb5..d7d9c45120 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -386,6 +386,15 @@ module ActionView # * <tt>:disabled</tt> - If true, the user will not be able to use this input. # * Any other key creates standard HTML options for the tag. # + # ==== Data attributes + # + # * <tt>:confirm => 'question?'</tt> - If present the unobtrusive JavaScript + # drivers will provide a prompt with the question specified. If the user accepts, + # the form is processed normally, otherwise no action is taken. + # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a + # disabled version of the submit button when the form is submitted. This feature is + # provided by the unobtrusive JavaScript driver. + # # ==== Examples # submit_tag # # => <input name="commit" type="submit" value="Save changes" /> @@ -411,6 +420,18 @@ module ActionView def submit_tag(value = "Save changes", options = {}) options = options.stringify_keys + if disable_with = options.delete("disable_with") + ActiveSupport::Deprecation.warn ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" + + options["data-disable-with"] = disable_with + end + + if confirm = options.delete("confirm") + ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead'" + + options["data-confirm"] = confirm + end + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options) end @@ -427,6 +448,17 @@ module ActionView # use this input. # * Any other key creates standard HTML options for the tag. # + # ==== Data attributes + # + # * <tt>:confirm => 'question?'</tt> - If present, the + # unobtrusive JavaScript drivers will provide a prompt with + # the question specified. If the user accepts, the form is + # processed normally, otherwise no action is taken. + # * <tt>:disable_with</tt> - Value of this parameter will be + # used as the value for a disabled version of the submit + # button when the form is submitted. This feature is provided + # by the unobtrusive JavaScript driver. + # # ==== Examples # button_tag # # => <button name="button" type="submit">Button</button> @@ -438,11 +470,26 @@ module ActionView # # <strong>Ask me!</strong> # # </button> # + # button_tag "Checkout", :data => { disable_with => "Please wait..." } + # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> + # def button_tag(content_or_options = nil, options = nil, &block) options = content_or_options if block_given? && content_or_options.is_a?(Hash) options ||= {} options = options.stringify_keys + if disable_with = options.delete("disable_with") + ActiveSupport::Deprecation.warn ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" + + options["data-disable-with"] = disable_with + end + + if confirm = options.delete("confirm") + ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead'" + + options["data-confirm"] = confirm + end + options.reverse_merge! 'name' => 'button', 'type' => 'submit' content_tag :button, content_or_options || 'Button', options, &block @@ -457,6 +504,12 @@ module ActionView # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * Any other key creates standard HTML options for the tag. # + # ==== Data attributes + # + # * <tt>:confirm => 'question?'</tt> - This will add a JavaScript confirm + # prompt with the question specified. If the user accepts, the form is + # processed normally, otherwise no action is taken. + # # ==== Examples # image_submit_tag("login.png") # # => <input src="/images/login.png" type="image" /> @@ -475,6 +528,12 @@ module ActionView def image_submit_tag(source, options = {}) options = options.stringify_keys + if confirm = options.delete("confirm") + ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead'" + + options["data-confirm"] = confirm + end + tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options) end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index b4eb3d4826..3d86790a8f 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -160,6 +160,16 @@ module ActionView # completion of the Ajax request and performing JavaScript operations once # they're complete # + # ==== Data attributes + # + # * <tt>:confirm => 'question?'</tt> - This will allow the unobtrusive JavaScript + # driver to prompt with the question specified. If the user accepts, the link is + # processed normally, otherwise no action is taken. + # * <tt>:disable_with</tt> - Value of this parameter will be + # used as the value for a disabled version of the submit + # button when the form is submitted. This feature is provided + # by the unobtrusive JavaScript driver. + # # ==== Examples # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base @@ -274,6 +284,16 @@ module ActionView # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will # be placed # + # ==== Data attributes + # + # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to + # prompt with the question specified. If the user accepts, the link is + # processed normally, otherwise no action is taken. + # * <tt>:disable_with</tt> - Value of this parameter will be + # used as the value for a disabled version of the submit + # button when the form is submitted. This feature is provided + # by the unobtrusive JavaScript driver. + # # ==== Examples # <%= button_to "New", :action => "new" %> # # => "<form method="post" action="/controller/new" class="button_to"> @@ -318,11 +338,11 @@ module ActionView # # # <%= button_to('Destroy', 'http://www.example.com', - # :method => "delete", :remote => true, :data => { :confirm' => 'Are you sure?' }) %> + # :method => "delete", :remote => true, :data => { :confirm' => 'Are you sure?', :disable_with => 'loading...' }) %> # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'> # # <div> # # <input name='_method' value='delete' type='hidden' /> - # # <input value='Destroy' type='submit' data-confirm='Are you sure?' /> + # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' /> # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </div> # # </form>" @@ -623,10 +643,24 @@ module ActionView html_options = html_options.stringify_keys html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options) + disable_with = html_options.delete("disable_with") + confirm = html_options.delete('confirm') method = html_options.delete('method') + if confirm + ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" + + html_options["data-confirm"] = confirm + end + add_method_to_attributes!(html_options, method) if method + if disable_with + ActiveSupport::Deprecation.warn ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" + + html_options["data-disable-with"] = disable_with + end + html_options else link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} diff --git a/actionpack/test/fixtures/project.rb b/actionpack/test/fixtures/project.rb index 2b53d39ed5..c124a9e605 100644 --- a/actionpack/test/fixtures/project.rb +++ b/actionpack/test/fixtures/project.rb @@ -1,3 +1,3 @@ class Project < ActiveRecord::Base - has_and_belongs_to_many :developers, :uniq => true + has_and_belongs_to_many :developers, -> { uniq } end diff --git a/actionpack/test/fixtures/reply.rb b/actionpack/test/fixtures/reply.rb index 0d3b0a7c98..16b53be18a 100644 --- a/actionpack/test/fixtures/reply.rb +++ b/actionpack/test/fixtures/reply.rb @@ -1,6 +1,6 @@ class Reply < ActiveRecord::Base scope :base, -> { scoped } - belongs_to :topic, :include => [:replies] + belongs_to :topic, -> { includes(:replies) } belongs_to :developer validates_presence_of :content diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 05baaa5a85..9afa4a2927 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -375,7 +375,14 @@ class FormTagHelperTest < ActionView::TestCase def test_submit_tag assert_dom_equal( %(<input name='commit' data-disable-with="Saving..." onclick="alert('hello!')" type="submit" value="Save" />), - submit_tag("Save", 'data-disable-with' => "Saving...", :onclick => "alert('hello!')") + submit_tag("Save", :onclick => "alert('hello!')", :data => { :disable_with => "Saving..." }) + ) + end + + def test_submit_tag_with_no_onclick_options + assert_dom_equal( + %(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />), + submit_tag("Save", :data => { :disable_with => "Saving..." }) ) end @@ -386,6 +393,15 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_submit_tag_with_deprecated_confirmation + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />), + submit_tag("Save", :confirm => "Are you sure?") + ) + end + end + def test_button_tag assert_dom_equal( %(<button name="button" type="submit">Button</button>), @@ -444,6 +460,15 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_button_tag_with_deprecated_confirmation + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + %(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>), + button_tag("Save", :type => "submit", :confirm => "Are you sure?") + ) + end + end + def test_image_submit_tag_with_confirmation assert_dom_equal( %(<input type="image" src="/images/save.gif" data-confirm="Are you sure?" />), @@ -451,6 +476,16 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_image_submit_tag_with_deprecated_confirmation + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + %(<input type="image" src="/images/save.gif" data-confirm="Are you sure?" />), + image_submit_tag("save.gif", :confirm => "Are you sure?") + ) + end + end + + def test_color_field_tag expected = %{<input id="car" name="car" type="color" />} assert_dom_equal(expected, color_field_tag("car")) diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index a09409635d..cb6f378ecb 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -94,13 +94,31 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_with_deprecated_confirm + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input data-confirm=\"Are you sure?\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :confirm => "Are you sure?") + ) + end + end + def test_button_to_with_javascript_disable_with assert_dom_equal( "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input data-disable-with=\"Greeting...\" type=\"submit\" value=\"Hello\" /></div></form>", - button_to("Hello", "http://www.example.com", 'data-disable-with' => "Greeting...") + button_to("Hello", "http://www.example.com", :data => { :disable_with => "Greeting..." }) ) end + def test_button_to_with_javascript_deprecated_disable_with + assert_deprecated ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" do + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input data-disable-with=\"Greeting...\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :disable_with => "Greeting...") + ) + end + end + def test_button_to_with_remote_and_form_options assert_dom_equal "<form method=\"post\" action=\"http://www.example.com\" class=\"custom-class\" data-remote=\"true\" data-type=\"json\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", button_to("Hello", "http://www.example.com", :remote => true, :form => { :class => "custom-class", "data-type" => "json" } ) end @@ -112,6 +130,31 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_with_remote_and_javascript_with_deprecated_confirm + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\" data-remote=\"true\"><div><input data-confirm=\"Are you sure?\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :remote => true, :confirm => "Are you sure?") + ) + end + end + + def test_button_to_with_remote_and_javascript_disable_with + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\" data-remote=\"true\"><div><input data-disable-with=\"Greeting...\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :remote => true, :data => { :disable_with => "Greeting..." }) + ) + end + + def test_button_to_with_remote_and_javascript_deprecated_disable_with + assert_deprecated ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" do + assert_dom_equal( + "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\" data-remote=\"true\"><div><input data-disable-with=\"Greeting...\" type=\"submit\" value=\"Hello\" /></div></form>", + button_to("Hello", "http://www.example.com", :remote => true, :disable_with => "Greeting...") + ) + end + end + def test_button_to_with_remote_false assert_dom_equal( "<form method=\"post\" action=\"http://www.example.com\" class=\"button_to\"><div><input type=\"submit\" value=\"Hello\" /></div></form>", @@ -220,6 +263,27 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_link_tag_with_deprecated_confirm + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<a href=\"http://www.example.com\" data-confirm=\"Are you sure?\">Hello</a>", + link_to("Hello", "http://www.example.com", :confirm => "Are you sure?") + ) + end + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<a href=\"http://www.example.com\" data-confirm=\"You can't possibly be sure, can you?\">Hello</a>", + link_to("Hello", "http://www.example.com", :confirm => "You can't possibly be sure, can you?") + ) + end + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<a href=\"http://www.example.com\" data-confirm=\"You can't possibly be sure,\n can you?\">Hello</a>", + link_to("Hello", "http://www.example.com", :confirm => "You can't possibly be sure,\n can you?") + ) + end + end + def test_link_to_with_remote assert_dom_equal( "<a href=\"http://www.example.com\" data-remote=\"true\">Hello</a>", @@ -269,6 +333,15 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_link_tag_using_post_javascript_and_with_deprecated_confirm + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<a href=\"http://www.example.com\" data-method=\"post\" rel=\"nofollow\" data-confirm=\"Are you serious?\">Hello</a>", + link_to("Hello", "http://www.example.com", :method => :post, :confirm => "Are you serious?") + ) + end + end + def test_link_tag_using_delete_javascript_and_href_and_confirm assert_dom_equal( "<a href='\#' rel=\"nofollow\" data-confirm=\"Are you serious?\" data-method=\"delete\">Destroy</a>", @@ -277,6 +350,16 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_link_tag_using_delete_javascript_and_href_and_with_deprecated_confirm + assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do + assert_dom_equal( + "<a href='\#' rel=\"nofollow\" data-confirm=\"Are you serious?\" data-method=\"delete\">Destroy</a>", + link_to("Destroy", "http://www.example.com", :method => :delete, :href => '#', :confirm => "Are you serious?"), + "When specifying url, form should be generated with it, but not this.href" + ) + end + end + def test_link_tag_with_block assert_dom_equal '<a href="/"><span>Example site</span></a>', link_to('/') { content_tag(:span, 'Example site') } diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index aca8291d75..4c1c91e3df 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- module ActiveRecord - # = Active Record Null Relation - module NullRelation + module NullRelation # :nodoc: def exec_queries @records = [] end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index dd1f77e925..3821c6122a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -75,6 +75,18 @@ module ActiveRecord binds) end + # Initializes new record from relation while maintaining the current + # scope. + # + # Expects arguments in the same format as +Base.new+. + # + # users = User.where(name: 'DHH') + # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil> + # + # You can also pass a block to new with the new record as argument: + # + # user = users.new { |user| user.name = 'Oscar' } + # user.name # => Oscar def new(*args, &block) scoping { @klass.new(*args, &block) } end @@ -87,17 +99,38 @@ module ActiveRecord alias build new + # Tries to create a new record with the same scoped attributes + # defined in the relation. Returns the initialized object if validation fails. + # + # Expects arguments in the same format as +Base.create+. + # + # ==== Examples + # users = User.where(name: 'Oscar') + # users.create # #<User id: 3, name: "oscar", ...> + # + # users.create(name: 'fxn') + # users.create # #<User id: 4, name: "fxn", ...> + # + # users.create { |user| user.name = 'tenderlove' } + # # #<User id: 5, name: "tenderlove", ...> + # + # users.create(name: nil) # validation on name + # # #<User id: nil, name: nil, ...> def create(*args, &block) scoping { @klass.create(*args, &block) } end + # Similar to #create, but calls +create!+ on the base class. Raises + # an exception if a validation error occurs. + # + # Expects arguments in the same format as <tt>Base.create!</tt>. def create!(*args, &block) scoping { @klass.create!(*args, &block) } end # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. # - # Expects arguments in the same format as <tt>Base.create</tt>. + # Expects arguments in the same format as +Base.create+. # # ==== Examples # # Find the first user named Penélope or create a new one. @@ -145,12 +178,13 @@ module ActiveRecord # are needed by the next ones when eager loading is going on. # # Please see further details in the - # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. + # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain _, queries = collecting_queries_for_explain { exec_queries } exec_explain(queries) end + # Converts relation objects to Array. def to_a # We monitor here the entire execution rather than individual SELECTs # because from the point of view of the user fetching the records of a @@ -209,6 +243,7 @@ module ActiveRecord c.respond_to?(:zero?) ? c.zero? : c.empty? end + # Returns true if there are any records. def any? if block_given? to_a.any? { |*block_args| yield(*block_args) } @@ -217,6 +252,7 @@ module ActiveRecord end end + # Returns true if there is more than one record. def many? if block_given? to_a.many? { |*block_args| yield(*block_args) } @@ -227,8 +263,6 @@ module ActiveRecord # Scope all queries to the current scope. # - # ==== Example - # # Comment.where(:post_id => 1).scoping do # Comment.first # SELECT * FROM comments WHERE post_id = 1 # end @@ -250,17 +284,14 @@ module ActiveRecord # ==== Parameters # # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. - # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. - # See conditions in the intro. - # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # # ==== Examples # # # Update all customers with the given attributes - # Customer.update_all :wants_email => true + # Customer.update_all wants_email: true # # # Update all books with 'Rails' in their title - # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David') + # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David') # # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') @@ -293,7 +324,7 @@ module ActiveRecord # ==== Examples # # # Updates one record - # Person.update(15, :user_name => 'Samuel', :group => 'expert') + # Person.update(15, user_name: 'Samuel', group: 'expert') # # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } @@ -333,7 +364,7 @@ module ActiveRecord # ==== Examples # # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(:status => "inactive") + # Person.destroy_all(status: "inactive") # Person.where(:age => 0..18).destroy_all def destroy_all(conditions = nil) if conditions @@ -435,6 +466,7 @@ module ActiveRecord where(primary_key => id_or_array).delete_all end + # Forces reloading of relation. def reload reset to_a # force reload @@ -448,10 +480,18 @@ module ActiveRecord self end + # Returns sql statement for the relation. + # + # Users.where(name: 'Oscar').to_sql + # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end + # Returns a hash of where conditions + # + # Users.where(name: 'Oscar').where_values_hash + # # => {:name=>"oscar"} def where_values_hash equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name @@ -469,6 +509,7 @@ module ActiveRecord @scope_for_create ||= where_values_hash.merge(create_with_value) end + # Returns true if relation needs eager loading. def eager_loading? @should_eager_load ||= eager_load_values.any? || @@ -483,6 +524,7 @@ module ActiveRecord includes_values & joins_values end + # Compares two relations for equality. def ==(other) case other when Relation @@ -506,6 +548,7 @@ module ActiveRecord end end + # Returns true if relation is blank. def blank? to_a.blank? end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index fb4388d4b2..5b78b246ab 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -9,8 +9,8 @@ module ActiveRecord # In that case, batch processing methods allow you to work # with the records in batches, thereby greatly reducing memory consumption. # - # The <tt>find_each</tt> method uses <tt>find_in_batches</tt> with a batch size of 1000 (or as - # specified by the <tt>:batch_size</tt> option). + # The #find_each method uses #find_in_batches with a batch size of 1000 (or as + # specified by the +:batch_size+ option). # # Person.all.find_each do |person| # person.do_awesome_stuff @@ -20,7 +20,7 @@ module ActiveRecord # person.party_all_night! # end # - # You can also pass the <tt>:start</tt> option to specify + # You can also pass the +:start+ option to specify # an offset to control the starting point. def find_each(options = {}) find_in_batches(options) do |records| @@ -29,14 +29,14 @@ module ActiveRecord end # Yields each batch of records that was found by the find +options+ as - # an array. The size of each batch is set by the <tt>:batch_size</tt> + # an array. The size of each batch is set by the +:batch_size+ # option; the default is 1000. # # You can control the starting point for the batch processing by - # supplying the <tt>:start</tt> option. This is especially useful if you + # supplying the +:start+ option. This is especially useful if you # want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and - # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt> + # worker 2 handle from 10,000 and beyond (by setting the +:start+ # option on that worker). # # It's not possible to set the order. That is automatically set to diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 64dda4f35a..a1c7e5b549 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/module/delegation' module ActiveRecord - module Delegation + module Delegation # :nodoc: # Set up common delegations for performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 36f98c6480..b04dd7c6a7 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/hash/keys' module ActiveRecord class Relation - class HashMerger + class HashMerger # :nodoc: attr_reader :relation, :hash def initialize(relation, hash) @@ -28,7 +28,7 @@ module ActiveRecord end end - class Merger + class Merger # :nodoc: attr_reader :relation, :values def initialize(relation, other) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1271b74ead..9df63d5485 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -35,7 +35,7 @@ module ActiveRecord CODE end - def create_with_value + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -67,6 +67,7 @@ module ActiveRecord args.empty? ? self : spawn.includes!(*args) end + # Like #includes, but modifies the relation in place. def includes!(*args) args.reject! {|a| a.blank? } @@ -84,6 +85,7 @@ module ActiveRecord args.blank? ? self : spawn.eager_load!(*args) end + # Like #eager_load, but modifies relation in place. def eager_load!(*args) self.eager_load_values += args self @@ -97,6 +99,7 @@ module ActiveRecord args.blank? ? self : spawn.preload!(*args) end + # Like #preload, but modifies relation in place. def preload!(*args) self.preload_values += args self @@ -114,6 +117,7 @@ module ActiveRecord args.blank? ? self : spawn.references!(*args) end + # Like #references, but modifies relation in place. def references!(*args) args.flatten! @@ -158,6 +162,7 @@ module ActiveRecord end end + # Like #select, but modifies relation in place. def select!(value) self.select_values += Array.wrap(value) self @@ -179,6 +184,7 @@ module ActiveRecord args.blank? ? self : spawn.group!(*args) end + # Like #group, but modifies relation in place. def group!(*args) args.flatten! @@ -200,6 +206,7 @@ module ActiveRecord args.blank? ? self : spawn.order!(*args) end + # Like #order, but modifies relation in place. def order!(*args) args.flatten! @@ -224,6 +231,7 @@ module ActiveRecord args.blank? ? self : spawn.reorder!(*args) end + # Like #reorder, but modifies relation in place. def reorder!(*args) args.flatten! @@ -240,6 +248,7 @@ module ActiveRecord args.compact.blank? ? self : spawn.joins!(*args) end + # Like #joins, but modifies relation in place. def joins!(*args) args.flatten! @@ -367,6 +376,7 @@ module ActiveRecord opts.blank? ? self : spawn.having!(opts, *rest) end + # Like #having, but modifies relation in place. def having!(opts, *rest) references!(PredicateBuilder.references(opts)) if Hash === opts @@ -383,6 +393,7 @@ module ActiveRecord spawn.limit!(value) end + # Like #limit, but modifies relation in place. def limit!(value) self.limit_value = value self @@ -399,6 +410,7 @@ module ActiveRecord spawn.offset!(value) end + # Like #offset, but modifies relation in place. def offset!(value) self.offset_value = value self @@ -410,6 +422,7 @@ module ActiveRecord spawn.lock!(locks) end + # Like #lock, but modifies relation in place. def lock!(locks = true) case locks when String, TrueClass, NilClass @@ -422,11 +435,11 @@ module ActiveRecord end # Returns a chainable relation with zero records, specifically an - # instance of the NullRelation class. + # instance of the <tt>ActiveRecord::NullRelation</tt> class. # - # The returned NullRelation inherits from Relation and implements the - # Null Object pattern so it is an object with defined null behavior: - # it always returns an empty array of records and does not query the database. + # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the + # Null Object pattern. It is an object with defined null behavior and always returns an empty + # array of records without quering the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -464,15 +477,34 @@ module ActiveRecord spawn.readonly!(value) end + # Like #readonly, but modifies relation in place. def readonly!(value = true) self.readonly_value = value self end + # Sets attributes to be used when creating new records from a + # relation object. + # + # users = User.where(name: 'Oscar') + # users.new.name # => 'Oscar' + # + # users = users.create_with(name: 'DHH') + # users.new.name # => 'DHH' + # + # You can pass +nil+ to +create_with+ to reset attributes: + # + # users = users.create_with(nil) + # users.new.name # => 'Oscar' def create_with(value) spawn.create_with!(value) end + # Like #create_with but modifies the relation in place. Raises + # +ImmutableRelation+ if the relation has already been loaded. + # + # users = User.scoped.create_with!(name: 'Oscar') + # users.new.name # => 'Oscar' def create_with!(value) self.create_with_value = value ? create_with_value.merge(value) : {} self @@ -495,6 +527,7 @@ module ActiveRecord spawn.from!(value, subquery_name) end + # Like #from, but modifies relation in place. def from!(value, subquery_name = nil) self.from_value = [value, subquery_name] self @@ -514,6 +547,7 @@ module ActiveRecord spawn.uniq!(value) end + # Like #uniq, but modifies relation in place. def uniq!(value = true) self.uniq_value = value self @@ -563,6 +597,7 @@ module ActiveRecord end end + # Like #extending, but modifies relation in place. def extending!(*modules, &block) modules << Module.new(&block) if block_given? @@ -579,15 +614,18 @@ module ActiveRecord spawn.reverse_order! end + # Like #reverse_order, but modifies relation in place. def reverse_order! self.reverse_order_value = !reverse_order_value self end + # Returns the Arel object associated with the relation. def arel @arel ||= with_default_scope.build_arel end + # Like #arel, but ignores the default scope of the model. def build_arel arel = Arel::SelectManager.new(table.engine, table) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 80d087a9ea..d21f02cd5f 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -34,6 +34,7 @@ module ActiveRecord end end + # Like #merge, but applies changes in place. def merge!(other) klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger klass.new(self, other).merge diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index bb559f4b4a..5fd20673d8 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,12 +1,46 @@ require 'active_support/core_ext/hash/keys' module ActiveSupport - # This class has dubious semantics and we only have it so that - # people can write <tt>params[:key]</tt> instead of <tt>params['key']</tt> - # and they get the same value for both keys. + # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered to be the same. + # + # rgb = ActiveSupport::HashWithIndifferentAccess.new + # + # rgb[:black] = '#000000' + # rgb[:black] # => '#000000' + # rgb['black'] # => '#000000' + # + # rgb['white'] = '#FFFFFF' + # rgb[:white] # => '#FFFFFF' + # rgb['white'] # => '#FFFFFF' + # + # Internally symbols are mapped to strings when used as keys in the entire + # writing interface (calling <tt>[]=</tt>, <tt>merge</tt>, etc). This + # mapping belongs to the public interface. For example, given + # + # hash = ActiveSupport::HashWithIndifferentAccess.new(:a => 1) + # + # you are guaranteed that the key is returned as a string: + # + # hash.keys # => ["a"] + # + # Technically other types of keys are accepted: + # + # hash = ActiveSupport::HashWithIndifferentAccess.new(:a => 1) + # hash[0] = 0 + # hash # => {"a"=>1, 0=>0} + # + # but this class is intended for use cases where strings or symbols are the + # expected keys and it is convenient to understand both as the same. For + # example the +params+ hash in Ruby on Rails. + # + # Note that core extensions define <tt>Hash#with_indifferent_access</tt>: + # + # rgb = {:black => '#000000', :white => '#FFFFFF'}.with_indifferent_access + # + # which may be handy. class HashWithIndifferentAccess < Hash - - # Always returns true, so that <tt>Array#extract_options!</tt> finds members of this class. + # Returns true so that <tt>Array#extract_options!</tt> finds members of + # this class. def extractable_options? true end @@ -51,25 +85,32 @@ module ActiveSupport # Assigns a new value to the hash: # - # hash = HashWithIndifferentAccess.new + # hash = ActiveSupport::HashWithIndifferentAccess.new # hash[:key] = "value" # + # This value can be later fetched using either +:key+ or +"key"+. def []=(key, value) regular_writer(convert_key(key), convert_value(value)) end alias_method :store, :[]= - # Updates the instantized hash with values from the second: + # Updates the receiver in-place merging in the hash passed as argument: # - # hash_1 = HashWithIndifferentAccess.new - # hash_1[:key] = "value" + # hash_1 = ActiveSupport::HashWithIndifferentAccess.new + # hash_2[:key] = "value" # - # hash_2 = HashWithIndifferentAccess.new + # hash_2 = ActiveSupport::HashWithIndifferentAccess.new # hash_2[:key] = "New Value!" # # hash_1.update(hash_2) # => {"key"=>"New Value!"} # + # The argument can be either an + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> or a regular +Hash+. + # In either case the merge respects the semantics of indifferent access. + # + # If the argument is a regular hash with keys +:key+ and +"key"+ only one + # of the values end up in the receiver, but which was is unespecified. def update(other_hash) if other_hash.is_a? HashWithIndifferentAccess super(other_hash) @@ -83,10 +124,10 @@ module ActiveSupport # Checks the hash for a key matching the argument passed in: # - # hash = HashWithIndifferentAccess.new + # hash = ActiveSupport::HashWithIndifferentAccess.new # hash["key"] = "value" - # hash.key? :key # => true - # hash.key? "key" # => true + # hash.key?(:key) # => true + # hash.key?("key") # => true # def key?(key) super(convert_key(key)) @@ -99,7 +140,7 @@ module ActiveSupport # Same as <tt>Hash#fetch</tt> where the key passed as argument can be # either a string or a symbol: # - # counters = HashWithIndifferentAccess.new + # counters = ActiveSupport::HashWithIndifferentAccess.new # counters[:foo] = 1 # # counters.fetch("foo") # => 1 @@ -113,7 +154,7 @@ module ActiveSupport # Returns an array of the values at the specified indices: # - # hash = HashWithIndifferentAccess.new + # hash = ActiveSupport::HashWithIndifferentAccess.new # hash[:a] = "x" # hash[:b] = "y" # hash.values_at("a", "b") # => ["x", "y"] @@ -129,23 +170,30 @@ module ActiveSupport end end - # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash. - # Does not overwrite the existing hash. + # This method has the same semantics of +update+, except it does not + # modify the receiver but rather returns a new hash with indifferent + # access with the result of the merge. def merge(hash) self.dup.update(hash) end - # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second. - # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a <tt>HashWithDifferentAccess</tt>. + # Like +merge+ but the other way around: Merges the receiver into the + # argument and returns a new hash with indifferent access as result: + # + # hash = ActiveSupport::HashWithIndifferentAccess.new + # hash['a'] = nil + # hash.reverse_merge(:a => 0, :b => 1) # => {"a"=>nil, "b"=>1} + # def reverse_merge(other_hash) - super self.class.new_from_hash_copying_default(other_hash) + super(self.class.new_from_hash_copying_default(other_hash)) end + # Same semantics as +reverse_merge+ but modifies the receiver in-place. def reverse_merge!(other_hash) replace(reverse_merge( other_hash )) end - # Removes a specified key from the hash. + # Removes the specified key from the hash. def delete(key) super(convert_key(key)) end @@ -160,7 +208,7 @@ module ActiveSupport def deep_symbolize_keys; to_hash.deep_symbolize_keys end def to_options!; self end - # Convert to a Hash with String keys. + # Convert to a regular hash with string keys. def to_hash Hash.new(default).merge!(self) end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index c319e94bc6..389df58ec4 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -161,38 +161,67 @@ class Struct #:nodoc: end class TrueClass - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) to_s end #:nodoc: + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end end class FalseClass - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) to_s end #:nodoc: + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end end class NilClass - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) 'null' end #:nodoc: + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + 'null' + end end class String - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) encoder.escape(self) end #:nodoc: + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + encoder.escape(self) + end end class Symbol - def as_json(options = nil) to_s end #:nodoc: + def as_json(options = nil) #:nodoc: + to_s + end end class Numeric - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) to_s end #:nodoc: + def as_json(options = nil) #:nodoc: + self + end + + def encode_json(encoder) #:nodoc: + to_s + end end class Float # Encoding Infinity or NaN to JSON should return "null". The default returns # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]'). - def as_json(options = nil) finite? ? self : nil end #:nodoc: + def as_json(options = nil) #:nodoc: + finite? ? self : nil + end end class BigDecimal @@ -216,7 +245,9 @@ class BigDecimal end class Regexp - def as_json(options = nil) to_s end #:nodoc: + def as_json(options = nil) #:nodoc: + to_s + end end module Enumerable @@ -226,7 +257,9 @@ module Enumerable end class Range - def as_json(options = nil) to_s end #:nodoc: + def as_json(options = nil) #:nodoc: + to_s + end end class Array @@ -262,7 +295,7 @@ class Hash Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }] end - def encode_json(encoder) + def encode_json(encoder) #:nodoc: # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields); diff --git a/guides/source/4_0_release_notes.textile b/guides/source/4_0_release_notes.textile index 895372ba63..d545798f6f 100644 --- a/guides/source/4_0_release_notes.textile +++ b/guides/source/4_0_release_notes.textile @@ -699,6 +699,8 @@ where(...).remove_conditions # => still has conditions * New rails application would be generated with the config.active_record.dependent_restrict_raises = false in the application config. +* The migration generator now creates a join table with (commented) indexes every time the migration name contains the word "join_table". + h3. Active Model * Changed <tt>AM::Serializers::JSON.include_root_in_json</tt> default value to false. Now, AM Serializers and AR objects have the same default behaviour. diff --git a/railties/lib/rails/queueing.rb b/railties/lib/rails/queueing.rb index 892cf8e894..baf6811d3e 100644 --- a/railties/lib/rails/queueing.rb +++ b/railties/lib/rails/queueing.rb @@ -3,8 +3,8 @@ require 'delegate' module Rails module Queueing - # A container for multiple queues. This class delegates to a default Queue - # so that Rails.queue.push and friends will Just Work. To use this class + # A container for multiple queues. This class delegates to a default Queue + # so that <tt>Rails.queue.push</tt> and friends will Just Work. To use this class # with multiple queues: # # # In your configuration: |