adding initial active record adapter

This commit is contained in:
Ryan Bates 2010-12-29 16:24:06 -08:00
parent 4c5ba09f4c
commit af9e77a79e
6 changed files with 115 additions and 12 deletions

View File

@ -6,6 +6,7 @@ require 'cancan/exceptions'
require 'cancan/query'
require 'cancan/inherited_resource'
require 'cancan/model_adapters/abstract_adapter'
require 'cancan/model_adapters/active_record_adapter' if defined? ActiveRecord
require 'cancan/model_adapters/data_mapper_adapter' if defined? DataMapper
require 'cancan/model_adapters/mongoid_adapter' if defined? Mongoid

View File

@ -186,11 +186,8 @@ module CanCan
@aliased_actions = {}
end
# Returns a CanCan::Query instance to help generate database queries based on the ability.
# If any relevant rules use a block then an exception will be raised because an
# SQL query cannot be generated from blocks of code.
def query(action, subject)
Query.new(subject, relevant_rules_for_query(action, subject))
def model_adapter(model_class, action)
ModelAdapters::ActiveRecordAdapter.new(model_class, relevant_rules_for_query(action, model_class))
end
# See ControllerAdditions#authorize! for documentation.

View File

@ -0,0 +1,10 @@
module CanCan
module ModelAdapters
class AbstractAdapter
def initialize(model_class, rules)
@model_class = model_class
@rules = rules
end
end
end
end

View File

@ -1,3 +1,103 @@
module CanCan
module ModelAdapters
class ActiveRecordAdapter < AbstractAdapter
# Returns conditions intended to be used inside a database query. Normally you will not call this
# method directly, but instead go through ActiveRecordAdditions#accessible_by.
#
# If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
#
# can :manage, User, :id => 1
# query(:manage, User).conditions # => { :id => 1 }
#
# If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
#
# can :manage, User, :id => 1
# can :manage, User, :manager_id => 1
# cannot :manage, User, :self_managed => true
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
#
def conditions
if @rules.size == 1 && @rules.first.base_behavior
# Return the conditions directly if there's just one definition
@rules.first.tableized_conditions.dup
else
@rules.reverse.inject(false_sql) do |sql, rule|
merge_conditions(sql, rule.tableized_conditions.dup, rule.base_behavior)
end
end
end
# Returns the associations used in conditions for the :joins option of a search.
# See ActiveRecordAdditions#accessible_by for use in Active Record.
def joins
joins_hash = {}
@rules.each do |rule|
merge_joins(joins_hash, rule.associations_hash)
end
clean_joins(joins_hash) unless joins_hash.empty?
end
def database_records
if @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
@model_class.where(conditions).joins(joins)
else
@model_class.scoped(:conditions => conditions, :joins => joins)
end
end
private
def merge_conditions(sql, conditions_hash, behavior)
if conditions_hash.blank?
behavior ? true_sql : false_sql
else
conditions = sanitize_sql(conditions_hash)
case sql
when true_sql
behavior ? true_sql : "not (#{conditions})"
when false_sql
behavior ? conditions : false_sql
else
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
end
end
end
def false_sql
sanitize_sql(['?=?', true, false])
end
def true_sql
sanitize_sql(['?=?', true, true])
end
def sanitize_sql(conditions)
@model_class.send(:sanitize_sql, conditions)
end
# Takes two hashes and does a deep merge.
def merge_joins(base, add)
add.each do |name, nested|
if base[name].is_a?(Hash) && !nested.empty?
merge_joins(base[name], nested)
else
base[name] = nested
end
end
end
# Removes empty hashes and moves everything into arrays.
def clean_joins(joins_hash)
joins = []
joins_hash.each do |name, nested|
joins << (nested.empty? ? name : {name => clean_joins(nested)})
end
joins
end
end
end
end
module CanCan
# This module is automatically included into all Active Record models.
module ActiveRecordAdditions
@ -20,12 +120,7 @@ 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)
query = ability.query(action, self)
if respond_to?(:where) && respond_to?(:joins)
where(query.conditions).joins(query.joins)
else
scoped(:conditions => query.conditions, :joins => query.joins)
end
ability.model_adapter(self, action).database_records
end
end

View File

@ -1,7 +1,7 @@
if ENV["MODEL_ADAPTER"].nil? || ENV["MODEL_ADAPTER"] == "active_record"
require "spec_helper"
describe CanCan::ActiveRecordAdditions do
describe CanCan::ModelAdapters::ActiveRecordAdapter do
before(:each) do
@model_class = Class.new(Project)
stub(@model_class).scoped { :scoped_stub }