#-- # Copyright (C) 2006 Dimitrij Denissenko # Please read LICENSE document for more information. #++ require 'active_record' module ActiveRecord #:nodoc: class Base class << self # This method works exactly the same way as the original with_scope # method, but it applies globally and not just within a block. # Arguments: # # * reference_id - ID reference to scope # * method_scoping - please see with_scope method documentation for further details # * options: # * :override - set this option to 'true' if you want to override an already existing global_scope (calling global_scope with the same reference_id twice leads to an exception otherwise) # # Example: # # class Article < ActiveRecord::Base # global_scope(:find_with_blog_id_one_only, :find => { :conditions => "blog_id = 1" }) # global_scope(:find_with_posts_and_limit, :find => { :conditions => "posts > 0", :limit => 1 }) # global_scope(:create_with_blog_id_one_by_default, :create => {:blog_id => 1}) # end # # Article.find(1) # => SELECT * from articles # # WHERE blog_id = 1 AND posts > 0 AND id = 1 # # LIMIT 1 # a = Article.create(2) # # a.blog_id # => 1 # # ATTENTION: This method is not meant to be used in a usual application # development process. General scopes are almost never a good idea, so # please review your data and application structure before you try to # implement it. def global_scope(reference_id, method_scoping = {}, options = {}) if self.global_scope_hash.has_key?(reference_id) && !options[:override] raise ActiveRecordError, "Global scope reference id '#{reference_id}' already exists!" end @@subclasses[self].each do |subclass| if !subclass.global_scope_hash.has_key?(reference_id) || options[:override] subclass.global_scope(reference_id, method_scoping, options) end end unless @@subclasses[self].blank? self.global_scope_hash[reference_id] = method_scoping; end # This method allows to skip a global scope directive. Example: # # class Article < ActiveRecord::Base # global_scope(:find_with_blog_id_one_only, :find => { :conditions => "blog_id = 1" }) # global_scope(:find_with_author_id_one_only, :find => { :conditions => "author_id = 1" }) # end # # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 1 # # Article.without_global_scope(:find_with_blog_id_one_only) do # Article.find(:all) # => SELECT * from articles WHERE author_id = 1 # end # def without_global_scope(reference_id, &block) unless self.global_scope_hash.has_key?(reference_id) raise ActiveRecordError, "Invalid global scope: '#{reference_id}' for class '#{self.name}'. Defined global scopes: #{self.global_scope_hash.keys.inspect}" end method_scoping = self.global_scope_hash.delete(reference_id) begin yield ensure self.global_scope_hash[reference_id] = method_scoping; end end # This is the original with_scope method with a small extension. :find parameters may now also include # :order and :group options. # # === Original documentation # # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. # method_name may be :find or :create. :find parameters may include the :conditions, :joins, # :include, :offset, :limit, :order, :group, and :readonly options. :create parameters are an attributes hash. # # Article.with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do # Article.find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 # a = Article.create(1) # a.blog_id # => 1 # end # # In nested scopings, all previous parameters are overwritten by inner rule # except :conditions in :find, that are merged as hash. # # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do # Article.with_scope(:find => { :limit => 10}) # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 # end # Article.with_scope(:find => { :conditions => "author_id = 3" }) # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 # end # end # # You can ignore any previous scopings by using with_exclusive_scope method. # # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do # Article.with_exclusive_scope(:find => { :limit => 10 }) # Article.find(:all) # => SELECT * from articles LIMIT 10 # end # end def with_scope(method_scoping = {}, action = :merge, &block) method_scoping = preprocess_method_scoping(method_scoping) # Merge scopings if action == :merge && scoped_methods.last method_scoping = merge_method_scoping(scoped_methods.last, method_scoping) end self.scoped_methods << method_scoping begin yield ensure self.scoped_methods.pop end end protected def global_scope_hash#:nodoc: write_inheritable_attribute(:global_scopes, {}) unless read_inheritable_attribute(:global_scopes) read_inheritable_attribute(:global_scopes) end def current_scoped_methods #:nodoc: current_scoped_methods = scoped_methods.last || {} global_scope_hash.values.each do |method_scoping| method_scoping = preprocess_method_scoping(method_scoping) evaluate_proc_conditions!(method_scoping) current_scoped_methods = merge_method_scoping(current_scoped_methods, method_scoping) end current_scoped_methods end private def evaluate_proc_conditions!(method_scoping) method_scoping.each_pair do |methods, params| params.each_pair do |key, value| method_scoping[methods][key] = value.call if value.is_a?(Proc) end end end def preprocess_method_scoping(method_scoping) #:nodoc: method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) # Dup first and second level of hash (method and params). method_scoping = method_scoping.inject({}) do |hash, (method, params)| hash[method] = (params == true) ? params : params.dup hash end method_scoping.assert_valid_keys([ :find, :create ]) if f = method_scoping[:find] f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :order, :readonly, :lock ]) f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly) end method_scoping end def merge_method_scoping(original_scope, additional_scope) #:nodoc: original_scope.inject(additional_scope) do |hash, (method, params)| case hash[method] when Hash if method == :find (hash[method].keys + params.keys).uniq.each do |key| merge = hash[method][key] && params[key] # merge if both scopes have the same key if key == :conditions && merge hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ") elsif key == :include && merge hash[method][key] = merge_includes(hash[method][key], params[key]).uniq else hash[method][key] = hash[method][key] || params[key] end end else hash[method] = params.merge(hash[method]) end else hash[method] = params end hash end end end end end