Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Introduce dynamic scopes for ActiveRecord: you can now use class meth…

…ods 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>
  • Loading branch information...
commit 66ee5890c5f21995b7fe0c486547f1287afe2b55 1 parent 1fb2755
Yaroslav Markin yaroslav authored dhh committed
2  activerecord/CHANGELOG
View
@@ -1,5 +1,7 @@
*2.3.0/3.0*
+* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
+
* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
* I18n the word separator for error messages. Introduces the activerecord.errors.format.separator translation key. #1294 [Akira Matsuda]
1  activerecord/lib/active_record.rb
View
@@ -51,6 +51,7 @@ def self.load_all!
autoload :Callbacks, 'active_record/callbacks'
autoload :Dirty, 'active_record/dirty'
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
+ autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
autoload :Migration, 'active_record/migration'
autoload :Migrator, 'active_record/migration'
autoload :NamedScope, 'active_record/named_scope'
25 activerecord/lib/active_record/base.rb
View
@@ -1456,7 +1456,10 @@ def abstract_class?
def respond_to?(method_id, include_private = false)
if match = DynamicFinderMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
+ elsif match = DynamicScopeMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
end
+
super
end
@@ -1809,7 +1812,11 @@ def undecorated_table_name(class_name = base_class.name)
# This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
# or find_or_create_by_user_and_password(user, password).
#
- # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
+ # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
+ # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
+ # respectively.
+ #
+ # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
# attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments, &block)
if match = DynamicFinderMatch.match(method_id)
@@ -1868,6 +1875,22 @@ def self.#{method_id}(*args)
}, __FILE__, __LINE__
send(method_id, *arguments, &block)
end
+ elsif match = DynamicScopeMatch.match(method_id)
+ attribute_names = match.attribute_names
+ super unless all_attributes_exists?(attribute_names)
+ if match.scope?
+ self.class_eval %{
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
+ options = args.extract_options! # options = args.extract_options!
+ attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
+ [:#{attribute_names.join(',:')}], args # [:user_name, :password], args
+ ) # )
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ end # end
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ end
else
super
end
25 activerecord/lib/active_record/dynamic_scope_match.rb
View
@@ -0,0 +1,25 @@
+module ActiveRecord
+ class DynamicScopeMatch
+ def self.match(method)
+ ds_match = self.new(method)
+ ds_match.scope ? ds_match : nil
+ end
+
+ def initialize(method)
+ @scope = true
+ case method.to_s
+ when /^scoped_by_([_a-zA-Z]\w*)$/
+ names = $1
+ else
+ @scope = nil
+ end
+ @attribute_names = names && names.split('_and_')
+ end
+
+ attr_reader :scope, :attribute_names
+
+ def scope?
+ !@scope.nil?
+ end
+ end
+end
20 activerecord/test/cases/named_scope_test.rb
View
@@ -278,3 +278,23 @@ def test_chaining_with_duplicate_joins
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
end
end
+
+class DynamicScopeMatchTest < ActiveRecord::TestCase
+ def test_scoped_by_no_match
+ assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
+ end
+
+ def test_scoped_by
+ match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.scope?
+ assert_equal %w(age sex location), match.attribute_names
+ end
+end
+
+class DynamicScopeTest < ActiveRecord::TestCase
+ def test_dynamic_scope
+ assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ 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"})
+ end
+end

6 comments on commit 66ee589

Cyril Mougel

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 Markin

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

Fabio Kung

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')
Marcello Rocha

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

Josh Pencheon

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!

Andrew Iacco

I am ok with “scoped_” prefix

Henrik Nyh

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.

Please sign in to comment.
Something went wrong with that request. Please try again.