extracting out Query class for generating sql conditions and association joins
This commit is contained in:
@@ -5,3 +5,4 @@ require 'cancan/resource_authorization'
|
||||
require 'cancan/controller_additions'
|
||||
require 'cancan/active_record_additions'
|
||||
require 'cancan/exceptions'
|
||||
require 'cancan/query'
|
||||
|
||||
@@ -182,86 +182,9 @@ module CanCan
|
||||
@aliased_actions = {}
|
||||
end
|
||||
|
||||
# Returns an array of arrays composing from desired action and hash of conditions which match the given ability.
|
||||
# This is useful if you need to generate a database query based on the current ability.
|
||||
#
|
||||
# can :read, Article, :visible => true
|
||||
# conditions :read, Article # returns [ [ true, { :visible => true } ] ]
|
||||
#
|
||||
# can :read, Article, :visible => true
|
||||
# cannot :read, Article, :blocked => true
|
||||
# conditions :read, Article # returns [ [ false, { :blocked => true } ], [ true, { :visible => true } ] ]
|
||||
#
|
||||
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
|
||||
#
|
||||
# If the ability is not defined then false is returned so be sure to take that into consideration.
|
||||
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
|
||||
# determined from that.
|
||||
def conditions(action, subject, options = {})
|
||||
relevant = relevant_can_definitions(action, subject)
|
||||
unless relevant.empty?
|
||||
if relevant.any?{|can_definition| can_definition.only_block? }
|
||||
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}"
|
||||
end
|
||||
relevant.map{|can_definition|
|
||||
[can_definition.base_behavior, can_definition.conditions(options)]
|
||||
}
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Returns sql conditions for object, which responds to :sanitize_sql .
|
||||
# This is useful if you need to generate a database query based on the current ability.
|
||||
#
|
||||
# can :manage, User, :id => 1
|
||||
# can :manage, User, :manager_id => 1
|
||||
# cannot :manage, User, :self_managed => true
|
||||
# sql_conditions :manage, User # returns not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))
|
||||
#
|
||||
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
|
||||
#
|
||||
# If the ability is not defined then false is returned so be sure to take that into consideration.
|
||||
# If there is just one :can ability, it conditions returned untouched.
|
||||
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
|
||||
# determined from that.
|
||||
def sql_conditions(action, subject, options = {})
|
||||
conds = conditions(action, subject, options)
|
||||
return false if conds == false
|
||||
return (conds[0][1] || {}) if conds.size==1 && conds[0][0] == true # to match previous spec
|
||||
|
||||
true_cond = subject.send(:sanitize_sql, ['?=?', true, true])
|
||||
false_cond = subject.send(:sanitize_sql, ['?=?', true, false])
|
||||
conds.reverse.inject(false_cond) do |sql, action|
|
||||
behavior, condition = action
|
||||
if condition && condition != {}
|
||||
condition = subject.send(:sanitize_sql, condition)
|
||||
case sql
|
||||
when true_cond
|
||||
behavior ? true_cond : "not (#{condition})"
|
||||
when false_cond
|
||||
behavior ? condition : false_cond
|
||||
else
|
||||
behavior ? "(#{condition}) OR (#{sql})" : "not (#{condition}) AND (#{sql})"
|
||||
end
|
||||
else
|
||||
behavior ? true_cond : false_cond
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the associations used in conditions. This is usually used in the :joins option for a search.
|
||||
# See ActiveRecordAdditions#accessible_by for use in Active Record.
|
||||
def association_joins(action, subject)
|
||||
can_definitions = relevant_can_definitions(action, subject)
|
||||
unless can_definitions.empty?
|
||||
if can_definitions.any?{|can_definition| can_definition.only_block? }
|
||||
raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}"
|
||||
end
|
||||
collect_association_joins(can_definitions)
|
||||
else
|
||||
nil
|
||||
end
|
||||
# Returns a CanCan::Query instance to help generate database queries based on the ability.
|
||||
def query(action, subject, options = {})
|
||||
Query.new(relevant_can_definitions_without_block(action, subject), subject, options)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -288,6 +211,14 @@ module CanCan
|
||||
end
|
||||
end
|
||||
|
||||
def relevant_can_definitions_without_block(action, subject)
|
||||
relevant_can_definitions(action, subject).each do |can_definition|
|
||||
if can_definition.only_block?
|
||||
raise Error, "Cannot determine SQL conditions or joins from block for #{action.inspect} #{subject.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def default_alias_actions
|
||||
{
|
||||
:read => [:index, :show],
|
||||
@@ -295,33 +226,5 @@ module CanCan
|
||||
:update => [:edit],
|
||||
}
|
||||
end
|
||||
|
||||
def collect_association_joins(can_definitions)
|
||||
joins = []
|
||||
can_definitions.each do |can_definition|
|
||||
merge_association_joins(joins, can_definition.association_joins || [])
|
||||
end
|
||||
joins = clear_association_joins(joins)
|
||||
joins unless joins.empty?
|
||||
end
|
||||
|
||||
def merge_association_joins(what, with)
|
||||
with.each do |join|
|
||||
name, nested = join.each_pair.first
|
||||
if at = what.detect{|h| h.has_key?(name) }
|
||||
at[name] = merge_association_joins(at[name], nested)
|
||||
else
|
||||
what << join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def clear_association_joins(joins)
|
||||
joins.map do |join|
|
||||
name, nested = join.each_pair.first
|
||||
nested.empty? ? name : {name => clear_association_joins(nested)}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,12 +20,12 @@ module CanCan
|
||||
# Here only the articles which the user can update are returned. This
|
||||
# internally uses Ability#conditions method, see that for more information.
|
||||
def accessible_by(ability, action = :read)
|
||||
conditions = ability.sql_conditions(action, self, :tableize => true) || {:id => nil}
|
||||
joins = ability.association_joins(action, self)
|
||||
query = ability.query(action, self, :tableize => true)
|
||||
conditions = query.sql_conditions || {:id => nil}
|
||||
if respond_to? :where
|
||||
where(conditions).joins(joins)
|
||||
where(conditions).joins(query.association_joins)
|
||||
else
|
||||
scoped(:conditions => conditions, :joins => joins)
|
||||
scoped(:conditions => conditions, :joins => query.association_joins)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
119
lib/cancan/query.rb
Normal file
119
lib/cancan/query.rb
Normal file
@@ -0,0 +1,119 @@
|
||||
module CanCan
|
||||
|
||||
# Generates the sql conditions and association joins for use in ActiveRecord queries.
|
||||
# Normally you will not use this class directly, but instead through ActiveRecordAdditions#accessible_by.
|
||||
class Query
|
||||
def initialize(can_definitions, sanitizer, options)
|
||||
@can_definitions = can_definitions
|
||||
@sanitizer = sanitizer
|
||||
@options = options
|
||||
end
|
||||
|
||||
# Returns an array of arrays composing from desired action and hash of conditions which match the given ability.
|
||||
# This is useful if you need to generate a database query based on the current ability.
|
||||
#
|
||||
# can :read, Article, :visible => true
|
||||
# conditions :read, Article # returns [ [ true, { :visible => true } ] ]
|
||||
#
|
||||
# can :read, Article, :visible => true
|
||||
# cannot :read, Article, :blocked => true
|
||||
# conditions :read, Article # returns [ [ false, { :blocked => true } ], [ true, { :visible => true } ] ]
|
||||
#
|
||||
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
|
||||
#
|
||||
# If the ability is not defined then false is returned so be sure to take that into consideration.
|
||||
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
|
||||
# determined from that.
|
||||
def conditions
|
||||
unless @can_definitions.empty?
|
||||
@can_definitions.map do |can_definition|
|
||||
[can_definition.base_behavior, can_definition.conditions(@options)]
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Returns sql conditions for object, which responds to :sanitize_sql .
|
||||
# This is useful if you need to generate a database query based on the current ability.
|
||||
#
|
||||
# can :manage, User, :id => 1
|
||||
# can :manage, User, :manager_id => 1
|
||||
# cannot :manage, User, :self_managed => true
|
||||
# sql_conditions :manage, User # returns not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))
|
||||
#
|
||||
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
|
||||
#
|
||||
# If the ability is not defined then false is returned so be sure to take that into consideration.
|
||||
# If there is just one :can ability, it conditions returned untouched.
|
||||
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
|
||||
# determined from that.
|
||||
def sql_conditions
|
||||
conds = conditions
|
||||
return false if conds == false
|
||||
return (conds[0][1] || {}) if conds.size==1 && conds[0][0] == true # to match previous spec
|
||||
|
||||
true_cond = sanitize_sql(['?=?', true, true])
|
||||
false_cond = sanitize_sql(['?=?', true, false])
|
||||
conds.reverse.inject(false_cond) do |sql, action|
|
||||
behavior, condition = action
|
||||
if condition && condition != {}
|
||||
condition = sanitize_sql(condition)
|
||||
case sql
|
||||
when true_cond
|
||||
behavior ? true_cond : "not (#{condition})"
|
||||
when false_cond
|
||||
behavior ? condition : false_cond
|
||||
else
|
||||
behavior ? "(#{condition}) OR (#{sql})" : "not (#{condition}) AND (#{sql})"
|
||||
end
|
||||
else
|
||||
behavior ? true_cond : false_cond
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the associations used in conditions. This is usually used in the :joins option for a search.
|
||||
# See ActiveRecordAdditions#accessible_by for use in Active Record.
|
||||
def association_joins
|
||||
unless @can_definitions.empty?
|
||||
collect_association_joins(@can_definitions)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sanitize_sql(conditions)
|
||||
@sanitizer.sanitize_sql(conditions)
|
||||
end
|
||||
|
||||
def collect_association_joins(can_definitions)
|
||||
joins = []
|
||||
@can_definitions.each do |can_definition|
|
||||
merge_association_joins(joins, can_definition.association_joins || [])
|
||||
end
|
||||
joins = clear_association_joins(joins)
|
||||
joins unless joins.empty?
|
||||
end
|
||||
|
||||
def merge_association_joins(what, with)
|
||||
with.each do |join|
|
||||
name, nested = join.each_pair.first
|
||||
if at = what.detect{|h| h.has_key?(name) }
|
||||
at[name] = merge_association_joins(at[name], nested)
|
||||
else
|
||||
what << join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def clear_association_joins(joins)
|
||||
joins.map do |join|
|
||||
name, nested = join.each_pair.first
|
||||
nested.empty? ? name : {name => clear_association_joins(nested)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user