From 32744256ef9adfc96e2b2475a73b2d1a74c2b26d Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 18 Oct 2008 12:43:01 -0400 Subject: [PATCH] Add NamedScope plugin v0.3 from metaskills. --- lib/will_paginate.rb | 15 +--- lib/will_paginate/named_scope.rb | 170 -------------------------------- lib/will_paginate/named_scope_patch.rb | 37 ------- 3 files changed, 3 insertions(+), 219 deletions(-) delete mode 100644 lib/will_paginate/named_scope.rb delete mode 100644 lib/will_paginate/named_scope_patch.rb diff --git a/lib/will_paginate.rb b/lib/will_paginate.rb index 1b633f1..56c083f 100644 --- a/lib/will_paginate.rb +++ b/lib/will_paginate.rb @@ -47,19 +47,10 @@ module WillPaginate end # Enable named_scope, a feature of Rails 2.1, even if you have older Rails - # (tested on Rails 2.0.2 and 1.2.6). - # - # You can pass +false+ for +patch+ parameter to skip monkeypatching - # *associations*. Use this if you feel that named_scope broke - # has_many, has_many :through or has_and_belongs_to_many associations in - # your app. By passing +false+, you can still use named_scope in - # your models, but not through associations. + # (tested on Rails 2.0.4 and 1.2.6). def enable_named_scope(patch = true) - return if defined? ActiveRecord::NamedScope - require 'will_paginate/named_scope' - require 'will_paginate/named_scope_patch' if patch - - ActiveRecord::Base.send :include, WillPaginate::NamedScope + WillPaginate::Deprecation::warn("The patch false option to enable_named_scope is no longer used.") unless patch + require "will_paginate/named_scope" end end diff --git a/lib/will_paginate/named_scope.rb b/lib/will_paginate/named_scope.rb deleted file mode 100644 index 21fc168..0000000 --- a/lib/will_paginate/named_scope.rb +++ /dev/null @@ -1,170 +0,0 @@ -module WillPaginate - # This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate, - # but in other aspects when managing complex conditions that you want to be reusable. - module NamedScope - # All subclasses of ActiveRecord::Base have two named_scopes: - # * all, which is similar to a find(:all) query, and - # * scoped, which allows for the creation of anonymous scopes, on the fly: Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions) - # - # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing - # intermediate values (scopes) around as first-class objects is convenient. - def self.included(base) - base.class_eval do - extend ClassMethods - named_scope :scoped, lambda { |scope| scope } - end - end - - module ClassMethods - def scopes - read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) - end - - # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query, - # such as :conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions. - # - # class Shirt < ActiveRecord::Base - # named_scope :red, :conditions => {:color => 'red'} - # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true] - # end - # - # The above calls to named_scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query Shirt.find(:all, :conditions => {:color => 'red'}). - # - # Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object - # constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count, - # Shirt.red.find(:all, :conditions => {:size => 'small'}). Also, just - # as with the association objects, name scopes acts like an Array, implementing Enumerable; Shirt.red.each(&block), - # Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really were an Array. - # - # These named scopes are composable. For instance, Shirt.red.dry_clean_only will produce all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count returns the number of garments - # for which these criteria obtain. Similarly with Shirt.red.dry_clean_only.average(:thread_count). - # - # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to - # has_many associations. If, - # - # class Person < ActiveRecord::Base - # has_many :shirts - # end - # - # then elton.shirts.red.dry_clean_only will return all of Elton's red, dry clean - # only shirts. - # - # Named scopes can also be procedural. - # - # class Shirt < ActiveRecord::Base - # named_scope :colored, lambda { |color| - # { :conditions => { :color => color } } - # } - # end - # - # In this example, Shirt.colored('puce') finds all puce shirts. - # - # Named scopes can also have extensions, just as with has_many declarations: - # - # class Shirt < ActiveRecord::Base - # named_scope :red, :conditions => {:color => 'red'} do - # def dom_id - # 'red_shirts' - # end - # end - # end - # - # - # For testing complex named scopes, you can examine the scoping options using the - # proxy_options method on the proxy itself. - # - # class Shirt < ActiveRecord::Base - # named_scope :colored, lambda { |color| - # { :conditions => { :color => color } } - # } - # end - # - # expected_options = { :conditions => { :colored => 'red' } } - # assert_equal expected_options, Shirt.colored('red').proxy_options - def named_scope(name, options = {}, &block) - name = name.to_sym - scopes[name] = lambda do |parent_scope, *args| - Scope.new(parent_scope, case options - when Hash - options - when Proc - options.call(*args) - end, &block) - end - (class << self; self end).instance_eval do - define_method name do |*args| - scopes[name].call(self, *args) - end - end - end - end - - class Scope - attr_reader :proxy_scope, :proxy_options - - [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/ - delegate m, :to => :proxy_found - end - end - - delegate :scopes, :with_scope, :to => :proxy_scope - - def initialize(proxy_scope, options, &block) - [options[:extend]].flatten.each { |extension| extend extension } if options[:extend] - extend Module.new(&block) if block_given? - @proxy_scope, @proxy_options = proxy_scope, options.except(:extend) - end - - def reload - load_found; self - end - - def first(*args) - if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash)) - proxy_found.first(*args) - else - find(:first, *args) - end - end - - def last(*args) - if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash)) - proxy_found.last(*args) - else - find(:last, *args) - end - end - - def empty? - @found ? @found.empty? : count.zero? - end - - def respond_to?(method, include_private = false) - super || @proxy_scope.respond_to?(method, include_private) - end - - protected - def proxy_found - @found || load_found - end - - private - def method_missing(method, *args, &block) - if scopes.include?(method) - scopes[method].call(self, *args) - else - with_scope :find => proxy_options do - proxy_scope.send(method, *args, &block) - end - end - end - - def load_found - @found = find(:all) - end - end - end -end diff --git a/lib/will_paginate/named_scope_patch.rb b/lib/will_paginate/named_scope_patch.rb deleted file mode 100644 index 685bb57..0000000 --- a/lib/will_paginate/named_scope_patch.rb +++ /dev/null @@ -1,37 +0,0 @@ -ActiveRecord::Associations::AssociationProxy.class_eval do - protected - def with_scope(*args, &block) - @reflection.klass.send :with_scope, *args, &block - end -end - -[ ActiveRecord::Associations::AssociationCollection, - ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass| - klass.class_eval do - protected - alias :method_missing_without_scopes :method_missing_without_paginate - def method_missing_without_paginate(method, *args, &block) - if @reflection.klass.scopes.include?(method) - @reflection.klass.scopes[method].call(self, *args, &block) - else - method_missing_without_scopes(method, *args, &block) - end - end - end -end - -# Rails 1.2.6 -ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do - protected - def method_missing(method, *args, &block) - if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - super - elsif @reflection.klass.scopes.include?(method) - @reflection.klass.scopes[method].call(self, *args) - else - @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do - @reflection.klass.send(method, *args, &block) - end - end - end -end if ActiveRecord::Base.respond_to? :find_first -- 1.6.0 From a435ad7773da5174077e809df046b89eb44aa3b3 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 18 Oct 2008 12:44:09 -0400 Subject: [PATCH] Add NamedScope plugin v0.3 from metaskills. --- lib/will_paginate/named_scope.rb | 7 + lib/will_paginate/named_scope/core_ext.rb | 91 +++++++++++ lib/will_paginate/named_scope/named_scope.rb | 172 ++++++++++++++++++++ .../named_scope/named_scope_patch_1.2.rb | 85 ++++++++++ .../named_scope/named_scope_patch_2.0.rb | 55 +++++++ 5 files changed, 410 insertions(+), 0 deletions(-) create mode 100755 lib/will_paginate/named_scope.rb create mode 100755 lib/will_paginate/named_scope/core_ext.rb create mode 100755 lib/will_paginate/named_scope/named_scope.rb create mode 100755 lib/will_paginate/named_scope/named_scope_patch_1.2.rb create mode 100755 lib/will_paginate/named_scope/named_scope_patch_2.0.rb diff --git a/lib/will_paginate/named_scope.rb b/lib/will_paginate/named_scope.rb new file mode 100755 index 0000000..8c119fa --- /dev/null +++ b/lib/will_paginate/named_scope.rb @@ -0,0 +1,7 @@ +unless defined? ActiveRecord::NamedScope + require "will_paginate/named_scope/core_ext" + require "will_paginate/named_scope/named_scope" + require "will_paginate/named_scope/named_scope_patch_#{ActiveRecord::Base.respond_to?(:find_first) ? '1.2' : '2.0'}" + ActiveRecord::Base.send :include, ActiveRecord::NamedScope +end + diff --git a/lib/will_paginate/named_scope/core_ext.rb b/lib/will_paginate/named_scope/core_ext.rb new file mode 100755 index 0000000..136abf3 --- /dev/null +++ b/lib/will_paginate/named_scope/core_ext.rb @@ -0,0 +1,91 @@ + +unless Hash.instance_methods.include? 'except' + Hash.class_eval do + + # Returns a new hash without the given keys. + def except(*keys) + rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys) + reject { |key,| rejected.include?(key) } + end + + # Replaces the hash without only the given keys. + def except!(*keys) + replace(except(*keys)) + end + + end +end + +class ActiveRecord::Base + class << self + + def first(*args) + find(:first, *args) + end + + def last(*args) + find(:last, *args) + end + + def all(*args) + find(:all, *args) + end + + private + + def find_last(options) + order = options[:order] + if order + order = reverse_sql_order(order) + elsif !scoped?(:find, :order) + order = "#{table_name}.#{primary_key} DESC" + end + if scoped?(:find, :order) + scoped_order = reverse_sql_order(scope(:find, :order)) + scoped_methods.select { |s| s[:find].update(:order => scoped_order) } + end + find_initial(options.merge({ :order => order })) + end + + def reverse_sql_order(order_query) + reversed_query = order_query.split(/,/).each { |s| + if s.match(/\s(asc|ASC)$/) + s.gsub!(/\s(asc|ASC)$/, ' DESC') + elsif s.match(/\s(desc|DESC)$/) + s.gsub!(/\s(desc|DESC)$/, ' ASC') + elsif !s.match(/\s(asc|ASC|desc|DESC)$/) + s.concat(' DESC') + end + }.join(',') + end + + end +end + +ActiveRecord::Associations::AssociationCollection.class_eval do + + def first(*args) + if fetch_first_or_last_using_find? args + find(:first, *args) + else + load_target unless loaded? + @target.first(*args) + end + end + + def last(*args) + if fetch_first_or_last_using_find? args + find(:last, *args) + else + load_target unless loaded? + @target.last(*args) + end + end + + private + + def fetch_first_or_last_using_find?(args) + args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || !@target.blank? || args.first.kind_of?(Integer)) + end + +end diff --git a/lib/will_paginate/named_scope/named_scope.rb b/lib/will_paginate/named_scope/named_scope.rb new file mode 100755 index 0000000..ee3802d --- /dev/null +++ b/lib/will_paginate/named_scope/named_scope.rb @@ -0,0 +1,172 @@ +module ActiveRecord + module NamedScope + # All subclasses of ActiveRecord::Base have two named_scopes: + # * all, which is similar to a find(:all) query, and + # * scoped, which allows for the creation of anonymous scopes, on the fly: Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions) + # + # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing + # intermediate values (scopes) around as first-class objects is convenient. + def self.included(base) + base.class_eval do + extend ClassMethods + named_scope :scoped, lambda { |scope| scope } + end + end + + module ClassMethods + def scopes + read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) + end + + # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query, + # such as :conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions. + # + # class Shirt < ActiveRecord::Base + # named_scope :red, :conditions => {:color => 'red'} + # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true] + # end + # + # The above calls to named_scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, + # in effect, represents the query Shirt.find(:all, :conditions => {:color => 'red'}). + # + # Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object + # constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count, + # Shirt.red.find(:all, :conditions => {:size => 'small'}). Also, just + # as with the association objects, name scopes acts like an Array, implementing Enumerable; Shirt.red.each(&block), + # Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really were an Array. + # + # These named scopes are composable. For instance, Shirt.red.dry_clean_only will produce all shirts that are both red and dry clean only. + # Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count returns the number of garments + # for which these criteria obtain. Similarly with Shirt.red.dry_clean_only.average(:thread_count). + # + # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to + # has_many associations. If, + # + # class Person < ActiveRecord::Base + # has_many :shirts + # end + # + # then elton.shirts.red.dry_clean_only will return all of Elton's red, dry clean + # only shirts. + # + # Named scopes can also be procedural. + # + # class Shirt < ActiveRecord::Base + # named_scope :colored, lambda { |color| + # { :conditions => { :color => color } } + # } + # end + # + # In this example, Shirt.colored('puce') finds all puce shirts. + # + # Named scopes can also have extensions, just as with has_many declarations: + # + # class Shirt < ActiveRecord::Base + # named_scope :red, :conditions => {:color => 'red'} do + # def dom_id + # 'red_shirts' + # end + # end + # end + # + # + # For testing complex named scopes, you can examine the scoping options using the + # proxy_options method on the proxy itself. + # + # class Shirt < ActiveRecord::Base + # named_scope :colored, lambda { |color| + # { :conditions => { :color => color } } + # } + # end + # + # expected_options = { :conditions => { :colored => 'red' } } + # assert_equal expected_options, Shirt.colored('red').proxy_options + def named_scope(name, options = {}, &block) + name = name.to_sym + scopes[name] = lambda do |parent_scope, *args| + Scope.new(parent_scope, case options + when Hash + options + when Proc + options.call(*args) + end, &block) + end + (class << self; self end).instance_eval do + define_method name do |*args| + scopes[name].call(self, *args) + end + end + end + end + + class Scope + attr_reader :proxy_scope, :proxy_options + + [].methods.each do |m| + unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/ + delegate m, :to => :proxy_found + end + end + + delegate :scopes, :with_scope, :to => :proxy_scope + + def initialize(proxy_scope, options, &block) + [options[:extend]].flatten.each { |extension| extend extension } if options[:extend] + extend Module.new(&block) if block_given? + @proxy_scope, @proxy_options = proxy_scope, options.except(:extend) + end + + def reload + load_found; self + end + + def first(*args) + if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash)) + proxy_found.first(*args) + else + find(:first, *args) + end + end + + def last(*args) + if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash)) + proxy_found.last(*args) + else + find(:last, *args) + end + end + + def sum(*args) + super || 0 + end + + def empty? + @found ? @found.empty? : count.zero? + end + + def respond_to?(method, include_private = false) + super || @proxy_scope.respond_to?(method, include_private) + end + + protected + def proxy_found + @found || load_found + end + + private + def method_missing(method, *args, &block) + if scopes.include?(method) + scopes[method].call(self, *args) + else + with_scope :find => proxy_options do + proxy_scope.send(method, *args, &block) + end + end + end + + def load_found + @found = find(:all) + end + end + end +end diff --git a/lib/will_paginate/named_scope/named_scope_patch_1.2.rb b/lib/will_paginate/named_scope/named_scope_patch_1.2.rb new file mode 100755 index 0000000..01fc507 --- /dev/null +++ b/lib/will_paginate/named_scope/named_scope_patch_1.2.rb @@ -0,0 +1,85 @@ + +ActiveRecord::Associations::AssociationProxy.class_eval do + protected + def with_scope(*args, &block) + @reflection.klass.send :with_scope, *args, &block + end +end + +class ActiveRecord::Base + class << self + def find(*args) + options = extract_options_from_args!(args) + validate_find_options(options) + set_readonly_option!(options) + case args.first + when :first then find_initial(options) + when :last then find_last(options) + when :all then find_every(options) + else find_from_ids(args, options) + end + end + private + def attribute_condition_with_named_scope(argument) + case argument + when ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)" + else attribute_condition_without_named_scope(argument) + end + end + alias_method_chain :attribute_condition, :named_scope + end +end + +ActiveRecord::Associations::HasManyAssociation.class_eval do + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + elsif @reflection.klass.scopes.include?(method) + @reflection.klass.scopes[method].call(self, *args) + else + create_scoping = {} + set_belongs_to_association_for(create_scoping) + + @reflection.klass.with_scope( + :create => create_scoping, + :find => { + :conditions => @finder_sql, + :joins => @join_sql, + :readonly => false + } + ) do + @reflection.klass.send(method, *args, &block) + end + end + end +end + +ActiveRecord::Associations::HasManyThroughAssociation.class_eval do + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + elsif @reflection.klass.scopes.include?(method) + @reflection.klass.scopes[method].call(self, *args) + else + @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) } + end + end +end + +ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do + protected + def method_missing(method, *args, &block) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + elsif @reflection.klass.scopes.include?(method) + @reflection.klass.scopes[method].call(self, *args) + else + @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do + @reflection.klass.send(method, *args, &block) + end + end + end +end + diff --git a/lib/will_paginate/named_scope/named_scope_patch_2.0.rb b/lib/will_paginate/named_scope/named_scope_patch_2.0.rb new file mode 100755 index 0000000..ae780c2 --- /dev/null +++ b/lib/will_paginate/named_scope/named_scope_patch_2.0.rb @@ -0,0 +1,55 @@ + +ActiveRecord::Associations::AssociationProxy.class_eval do + protected + def with_scope(*args, &block) + @reflection.klass.send :with_scope, *args, &block + end +end + +class ActiveRecord::Base + class << self + def find(*args) + options = args.extract_options! + validate_find_options(options) + set_readonly_option!(options) + case args.first + when :first then find_initial(options) + when :last then find_last(options) + when :all then find_every(options) + else find_from_ids(args, options) + end + end + private + def attribute_condition_with_named_scope(argument) + case argument + when ActiveRecord::NamedScope::Scope then "IN (?)" + else attribute_condition_without_named_scope(argument) + end + end + alias_method_chain :attribute_condition, :named_scope + end +end + +ActiveRecord::Associations::AssociationCollection.class_eval do + protected + def method_missing(method, *args) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + if block_given? + super { |*block_args| yield(*block_args) } + else + super + end + elsif @reflection.klass.scopes.include?(method) + @reflection.klass.scopes[method].call(self, *args) + else + with_scope(construct_scope) do + if block_given? + @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } + else + @reflection.klass.send(method, *args) + end + end + end + end +end + -- 1.6.0 From 9c1620a30876a8d7533cbee0b009b5c8dd394291 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 18 Oct 2008 13:20:04 -0400 Subject: [PATCH] Enable named_scope before active record. --- lib/will_paginate.rb | 1 + test/finder_test.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletions(-) diff --git a/lib/will_paginate.rb b/lib/will_paginate.rb index 56c083f..945fc82 100644 --- a/lib/will_paginate.rb +++ b/lib/will_paginate.rb @@ -69,5 +69,6 @@ module WillPaginate end if defined?(Rails) and defined?(ActiveRecord) and defined?(ActionController) + # WillPaginate.enable_named_scope # Must be called before .enable_activerecord WillPaginate.enable end diff --git a/test/finder_test.rb b/test/finder_test.rb index 38ef565..d895d80 100644 --- a/test/finder_test.rb +++ b/test/finder_test.rb @@ -2,8 +2,8 @@ require 'helper' require 'lib/activerecord_test_case' require 'will_paginate' -WillPaginate.enable_activerecord WillPaginate.enable_named_scope +WillPaginate.enable_activerecord class FinderTest < ActiveRecordTestCase fixtures :topics, :replies, :users, :projects, :developers_projects -- 1.6.0