public
Description: Ruby on Rails
Homepage: http://rubyonrails.org
Clone URL: git://github.com/rails/rails.git
Introduce dynamic scopes for ActiveRecord: you can now use class methods like 
scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, 
password) that will use the scoped method with attributes you supply. [#1648 
state:committed]

Signed-off-by: David Heinemeier Hansson <david@loudthinking.com>
yaroslav (author)
Sun Dec 28 11:25:55 -0800 2008
dhh (committer)
Sun Dec 28 11:52:46 -0800 2008
commit  66ee5890c5f21995b7fe0c486547f1287afe2b55
tree    81b44888712ff3ea7093209b3235858948354d25
parent  1fb275541a58e6a2100261c6117e96e6c014cc6c
...
1
2
 
 
3
4
5
...
1
2
3
4
5
6
7
0
@@ -1,5 +1,7 @@
0
 *2.3.0/3.0*
0
 
0
+* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
0
+
0
 * Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
0
 
0
 * I18n the word separator for error messages. Introduces the activerecord.errors.format.separator translation key.  #1294 [Akira Matsuda]
...
51
52
53
 
54
55
56
...
51
52
53
54
55
56
57
0
@@ -51,6 +51,7 @@ module ActiveRecord
0
   autoload :Callbacks, 'active_record/callbacks'
0
   autoload :Dirty, 'active_record/dirty'
0
   autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
0
+  autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
0
   autoload :Migration, 'active_record/migration'
0
   autoload :Migrator, 'active_record/migration'
0
   autoload :NamedScope, 'active_record/named_scope'
...
1456
1457
1458
 
 
1459
 
1460
1461
1462
...
1809
1810
1811
1812
 
 
 
 
 
1813
1814
1815
...
1868
1869
1870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1871
1872
1873
...
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
...
1812
1813
1814
 
1815
1816
1817
1818
1819
1820
1821
1822
...
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
0
@@ -1456,7 +1456,10 @@ module ActiveRecord #:nodoc:
0
       def respond_to?(method_id, include_private = false)
0
         if match = DynamicFinderMatch.match(method_id)
0
           return true if all_attributes_exists?(match.attribute_names)
0
+        elsif match = DynamicScopeMatch.match(method_id)
0
+          return true if all_attributes_exists?(match.attribute_names)
0
         end
0
+        
0
         super
0
       end
0
 
0
@@ -1809,7 +1812,11 @@ module ActiveRecord #:nodoc:
0
         # This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
0
         # or find_or_create_by_user_and_password(user, password).
0
         #
0
-        # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
0
+        # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
0
+        # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
0
+        # respectively.
0
+        #
0
+        # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
0
         # attempts to use it do not run through method_missing.
0
         def method_missing(method_id, *arguments, &block)
0
           if match = DynamicFinderMatch.match(method_id)
0
@@ -1868,6 +1875,22 @@ module ActiveRecord #:nodoc:
0
               }, __FILE__, __LINE__
0
               send(method_id, *arguments, &block)
0
             end
0
+          elsif match = DynamicScopeMatch.match(method_id)
0
+            attribute_names = match.attribute_names
0
+            super unless all_attributes_exists?(attribute_names)
0
+            if match.scope?
0
+              self.class_eval %{
0
+                def self.#{method_id}(*args)                        # def self.scoped_by_user_name_and_password(*args)
0
+                  options = args.extract_options!                   #   options = args.extract_options!
0
+                  attributes = construct_attributes_from_arguments( #   attributes = construct_attributes_from_arguments(
0
+                    [:#{attribute_names.join(',:')}], args          #     [:user_name, :password], args
0
+                  )                                                 #   )
0
+                                                                    # 
0
+                  scoped(:conditions => attributes)                 #   scoped(:conditions => attributes)
0
+                end                                                 # end
0
+              }, __FILE__, __LINE__
0
+              send(method_id, *arguments)
0
+            end
0
           else
0
             super
0
           end
...
278
279
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
0
@@ -278,3 +278,23 @@ class NamedScopeTest < ActiveRecord::TestCase
0
     assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
0
   end
0
 end
0
+
0
+class DynamicScopeMatchTest < ActiveRecord::TestCase  
0
+  def test_scoped_by_no_match
0
+    assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
0
+  end
0
+
0
+  def test_scoped_by
0
+    match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location")
0
+    assert_not_nil match
0
+    assert match.scope?
0
+    assert_equal %w(age sex location), match.attribute_names
0
+  end
0
+end
0
+
0
+class DynamicScopeTest < ActiveRecord::TestCase
0
+  def test_dynamic_scope
1
+    assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)
0
+    assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
0
+  end
0
+end

Comments

shingara Mon Dec 29 00:26:02 -0800 2008 at activerecord/test/cases/named_scope_test.rb L297

it’s not better with assert on :

Post.find(:first, :conditions => { :author_id => 1})

because we can’t assume that Post(1) is allways to author_id(1).

yaroslav Mon Dec 29 00:33:16 -0800 2008

@shingara well, actually we can since we use fixtures :)

fabiokung Fri Feb 20 07:57:56 -0800 2009

I would suggest dropping the “scoped_” preffix, as it IMO improves readability:

User.by_name_and_password.find(:all)
User.by_name.by_password.find(:all, :order => 'created_at')
mereghost Fri Feb 20 10:45:23 -0800 2009

Still, the “scoped_” prefix allows for clarity of what’s happening.

joshpencheon Fri Feb 20 11:12:58 -0800 2009

I prefer it with the “scoped_” prefix, as it’s more in keeping with related declarations (e.g. “named_scope”, “default_scope” etc.)

Using fabiokungs examples, I think it actually rolls of the tongue worse without the prefixes!

iaccoand Fri Feb 20 21:48:51 -0800 2009

I am ok with “scoped_” prefix

henrik Sat Feb 21 00:30:40 -0800 2009

I’d agree with fabiokung except that I tend to use “by_x” for named scopes that add an :order. E.g. “by_created”, so having “by_name” use conditions instead of order could get weird, for me at least. “with_” might make more sense for conditions anyway.

To be clear, I don’t mind “scoped_by” or “find_by” since the “by” is prefixed.