CanCan and polymorphic associations (Eager Loading error)
Asked Answered
T

2

6

I'm trying to define a user's ability to access something based on a column on an associated model (so something like can :read, Step, 'steppable' => {published: true}), the problem is that it's a polymorphic association so it can't find the steppable table because it doesn't exist.

I've got steps, and each step has a steppable (either a lecture, a quiz, or some other action). I need an activerecord query that will work. I've tried:

Step.includes(:steppable).where('steppable' => {published: true})

and

Step.joins(:steppable).where('steppable' => {published: true})

But both result in ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :steppable

Models look like this:

class Step < ActiveRecord::Base
   ...
   belongs_to :steppable, polymorphic: true, dependent: :destroy
   ...
end

and

class Lecture
   ...
   has_one :step, as: :steppable, dependent: :destroy
   ...
end

Note: I'd like to be agnostic regarding the associated model, and in order for it to work for fetching records with CanCan, it has to be done using database columns (see github.com/ryanb/cancan/wiki/defining-abilities)

Theurich answered 19/9, 2013 at 14:3 Comment(2)
Did you try Step.includes(:steppable).all, just to check? What does your models look like? We need more information...Timeserver
Yeah, Step.includes(:steppable).all works but Step.includes(:steppable).where('steppable' => {published: true}).all leads to the same ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :steppable error. I updated the question with info on the model.Theurich
V
10

You should be able to do this:

can :read, Step, steppable_type: 'Lecture', steppable_id: Lecture.published.pluck(:id)
can :read, Step, steppable_type: 'OtherThing', steppable_id: OtherThing.published.pluck(:id)

You have to do it for each Steppable class, but it gets around the eager loading polymorphic associations problem. To dry this up a bit:

[Lecture, OtherThing].each do |klass|
  can :read, Step, steppable_type: klass.to_s, steppable_id: klass.published.pluck(:id)
end

In this case, as long as each steppable class has a scope published, you just add any steppable class into that array, even if published is defined differently in each class.

Venditti answered 19/9, 2013 at 23:8 Comment(1)
This example is too simple, you may want to define hash conditions on the steppable record which differ depending on the model and it will crash because CanCanCan will try to call an attribute which does not exist for that model.Crosshead
T
0

You can do it this way:

lectures = Lecture.where(published: true)
steps = Step.where(steppable_type: 'Lecture', steppable_id: lectures)

Or in the case you really want to be agnostic regarding the associated model:

Step.all.select { |s| s.steppable.published? }
Timeserver answered 19/9, 2013 at 14:58 Comment(4)
Neither of those will work. I'd like to be agnostic regarding the associated model, and in order for it to work for fetching records with CanCan, it has to be done using database columns (see github.com/ryanb/cancan/wiki/defining-abilities)Theurich
Could you be more precise about what you're trying to do with CanCan?Timeserver
Yep, I'm trying to set something like: can :read, Step, steppable: {published: true} but that leads to the error above. I could do it with can :read, Step do |step| step.steppable.published? end but that doesn't allow for fetching items using CanCan's accessible_by method.Theurich
Then I guess I won't be able to help you more than I did ; )Timeserver

© 2022 - 2024 — McMap. All rights reserved.