# SimpleScope # # See the README for a description of the plugin. module SimpleScope def self.included(base) base.extend(ClassMethods) end module ClassMethods # Takes a model, some arguments and an optional block to create an around filter for adding # and removing our shorthand scoping hash. def scope_filter(model, *args, &block) filter_options, scope_options = extract_scope_options(args.last.is_a?(Hash) ? args.pop : {}) around_filter SimpleScopeFilter.new(model, *(args << scope_options), &block), filter_options end protected def extract_scope_options(options) collector = Proc.new { |k,v| [:cache, :class].include?(k) } [options.reject(&collector), options.reject { |k,v| !collector.call(k,v) }] end end class SimpleScopeFilter # Prepare the filter with the model (ActiveRecord class) to scope, the class for the scoping # which defaults to the model, and ensure that there is a source for the scoping hash. def initialize(model, *args, &block) raise TypeError, "No class provided" unless (@model = model).is_a?(Class) @method = args.first.is_a?(Symbol) ? args.shift : nil @block = block_given? ? block : nil raise ArgumentError, "Missing block or method for scoping" unless @block || @method @options = args.last.is_a?(Hash) ? args.pop : {} @klass = (@options[:class] || @model) end # Before an action is executed, retrieve the scoping (whether by method or block) and # perform some sanitisation on the hash it returns. Push the hash, in a rather bruteforce # way, on to the present scopings of the model in question. Tell the development log we are # doing all sorts of horrible things. def before(controller) scoping = @method ? controller.send(@method) : @block.call(controller) raise TypeError, "Scoping must be a hash of attributes" unless scoping.is_a?(Hash) constraints = prepare_constraints(scoping) scoped_methods << @model.with_scope(constraints) { current_scoped_methods } @model.logger.debug("[%s] %s#before:\n %s scoping for %s:\n %s" % [self.class.parent, controller.class, @klass, @model, scoped_methods.inspect]) end # After the action has executed, pop the last scoping (the one we created) and tell the # development log that we have stopped doing horrible things. def after(controller) @model.send(:scoped_methods).pop @model.logger.debug("[%s] %s#after:\n %s scoping for %s:\n %s" % [self.class.parent, controller.class, @klass, @model, scoped_methods.inspect]) end protected # Gets all the scopings of the model, using a send hack def scoped_methods @model.send(:scoped_methods) end # Gets the current scoping of the model, using a send hack def current_scoped_methods @model.send(:current_scoped_methods) end # Gets the column names from the klass (i.e. the table in question) def column_names @column_names ||= @klass.column_names.map(&:to_sym) end # Gets the conditional based on the value's class, using a send hack def attribute_condition(value) @klass.send(:attribute_condition, value) end # Generates the scoping hash for with_scope # Uses reject instead of select/partition because they return arrays. # Note: Performs no generation if :find or :create are already present def prepare_constraints(scoping) return scoping if scoping[:find] || scoping[:create] attributes = scoping.reject { |k,v| !column_names.include?(k) } options = scoping.reject { |k,v| column_names.include?(k) } { :find => construct_find(attributes, options), :create => construct_create(attributes) } end # Generates the create values out of all valid attributes for klass. def construct_create(attributes) attributes.reject { |k,v| !v.is_a?(@klass.columns_hash[k.to_s].klass) } end # Generates the find values, merging conditions with the other options. def construct_find(attributes, options) conditions = construct_conditions(attributes, options) conditions.empty? ? options : options.merge({:conditions => conditions}) end # Generates the conditions array, merging :conditions with the autogenerated one if it exists. def construct_conditions(attributes, options) conditions = attributes_to_conditions(attributes) options[:conditions] ? merge_conditions([options[:conditions], conditions]) : conditions end # This is totally unnecessary if you have Rails Edge (1.2 or higher), since hashes are # dealt with. It just converts hashes to the array format (the only one supported in 1.1.4). def attributes_to_conditions(hash) conditions = [] hash.each do |k,v| if @klass.columns_hash[k.to_s] conditions << ["%s.%s %s" % [@klass.table_name, k, attribute_condition(v)], v] end end merge_conditions(conditions) end # Takes an array of condition fragments and puts them together using AND def merge_conditions(fragments) return [] if fragments.empty? fragments, statements, values = fragments.dup, [], [] fragments.each do |fragment| statements << fragment and next if fragment.is_a?(String) statements << fragment.shift fragment.each { |v| values << v } end values.unshift statements.join(' AND ') end end end