Created
December 30, 2020 09:00
-
-
Save tomasc/52652bd6fcacdf0d03ecc5ad3c6c298d to your computer and use it in GitHub Desktop.
CanCan custom ActiveRecord Adapter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
module CanCan | |
module ModelAdapters | |
class CustomActiveRecordAdapter < AbstractAdapter | |
def self.inherited(subclass) | |
@subclasses ||= [] | |
@subclasses.insert(0, subclass) | |
end | |
def self.adapter_class(model_class) | |
@subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter | |
end | |
# Used to determine if the given adapter should be used for the passed in class. | |
def self.for_class?(model_class) | |
model_class <= ActiveRecord::Base | |
end | |
# Override if you need custom find behavior | |
def self.find(model_class, id) | |
model_class.find(id) | |
end | |
# Used to determine if this model adapter will override the matching behavior for a hash of conditions. | |
# If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash | |
def self.override_conditions_hash_matching?(subject, conditions) | |
false | |
end | |
# Override if override_conditions_hash_matching? returns true | |
# TODO: override for conditions in locks (do not need that at the moment) | |
def self.matches_conditions_hash?(subject, conditions) | |
raise NotImplemented, "This model adapter does not support matching on a conditions hash." | |
end | |
# Used to determine if this model adapter will override the matching behavior for a specific condition. | |
# If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash | |
def self.override_condition_matching?(subject, name, value) | |
false | |
end | |
# Override if override_condition_matching? returns true | |
def self.matches_condition?(subject, name, value) | |
raise NotImplemented, "This model adapter does not support matching on a specific condition." | |
end | |
def initialize(model_class, rules) | |
@model_class = model_class | |
@rules = rules | |
end | |
def database_records | |
or_conditions = [subject_type_conditions, *open_conditions].compact.reduce(:or) | |
and_conditions = [or_conditions, *closed_conditions].compact.reduce(:and) | |
if sti_class? | |
if and_conditions.present? | |
# the and_conditions already include subject_type_conditions controlling | |
# whether class is accessible or not | |
@model_class.unscoped.where(and_conditions) | |
else | |
# the records are not accessible unless specified otherwise | |
@model_class.none | |
end | |
else | |
if and_conditions.present? | |
@model_class.unscoped.where(and_conditions) | |
else | |
open_subject_types.include?(@model_class) ? @model_class.unscoped : @model_class.none | |
end | |
end | |
end | |
private | |
def has_any_conditions? | |
subject_type_conditions.present? || | |
open_conditions.present? || | |
closed_conditions.present? | |
end | |
def subject_types | |
@subject_types ||= begin | |
root_cls = @model_class.base_class | |
[root_cls, *root_cls.descendants].compact | |
end | |
end | |
def open_subject_types | |
@open_subject_types ||= begin | |
subject_types.inject(Set[]) do |res, cls| | |
subject_type_rules_for(cls).each do |rule| | |
cls_list = [cls, *cls.descendants].compact | |
rule.base_behavior ? res += cls_list : res -= cls_list | |
end | |
res.to_a | |
end | |
end | |
end | |
def closed_subject_types | |
@closed_subject_types ||= subject_types - open_subject_types | |
end | |
def subject_type_conditions | |
return unless sti_class? | |
@model_class.arel_table[type_key].in(open_subject_types) | |
end | |
def open_conditions | |
@open_conditions ||= begin | |
condition_rules.select(&:base_behavior).each_with_object({}) do |rule, res| | |
rule.conditions.each do |k, v| | |
res[k] ||= [] | |
res[k] << v | |
end | |
end.map { |key, values| @model_class.arel_table[key].in(values) } | |
end | |
end | |
def closed_conditions | |
@closed_conditions ||= begin | |
condition_rules.reject(&:base_behavior).each_with_object({}) do |rule, res| | |
rule.conditions.each do |k, v| | |
res[k] ||= [] | |
res[k] << v | |
end | |
end.map { |key, values| @model_class.arel_table[key].not_in(values) } | |
end | |
end | |
def subject_type_rules_for(subject_type) | |
subject_type_rules.select do |rule| | |
rule.subjects.include?(subject_type) | |
end | |
end | |
def subject_type_rules | |
@rules.reject { |rule| rule.conditions.present? } | |
end | |
def condition_rules | |
@rules.select { |rule| rule.conditions.present? } | |
end | |
def id_key | |
@model_class.primary_key | |
end | |
def type_key | |
@model_class.inheritance_column | |
end | |
def sti_class? | |
@model_class.column_names.include?(type_key) | |
end | |
end | |
end | |
end | |
ActiveSupport.on_load(:active_record) do | |
send :include, CanCan::ModelAdditions | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In Ability class override the model adapter method: