Undefined method error for scope on STI subclass - ruby-on-rails

The Setup
I have an STI setup like so:
class Transaction < ActiveRecord::Base
belongs_to :account
scope :deposits, -> { where type: Deposit }
end
class Deposit < Transaction
scope :pending, -> { where state: :pending }
end
class Account < ActiveRecord::Base
has_many :transactions
end
If I call:
> a = Account.first
> a.transactions.deposits
...then I get what I expect, a collection of Deposit instances, however if I look at the class of what's returned:
> a.transactions.deposits.class
...then it's actually not a Deposit collection, it's still a Transaction collection, ie. it's a Transaction::ActiveRecord_AssociationRelation
The Problem
So, to the problem, if I then want to call one of the Deposit scopes on that collection it fails:
> a.transactions.deposits.pending
NoMethodError: undefined method `pending' for #<Transaction::ActiveRecord_Associations_CollectionProxy:0x007f8ac1252d00>
Things I've Checked
I've tried changing the scope to Deposit.where... which had no effect, and also to Deposit.unscoped.where... which actually returns the right collection object, but it strips all the scope, so I lose the account_id=123 part of the query so it fails on that side.
I've checked this and the problem exists for both Rails 4.1 and 4.2. Thanks for any pointers on how to make this work.
I know there's a workaround, but...
I know I could work around the issue by adding a has_many :deposits into Account, but I'm trying to avoid that (in reality I have many associated tables and many different transaction subclasses, and I'm trying to avoid adding the dozens of extra associations that would require).
Question
How can I get what's returned by the deposits scope to actually be a Deposit::ActiveRecord_Association... so that I can chain my scopes from Deposit class?

I created an isolated test for your issue here:https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope-rb and it has the error you mentioned.
I came across this post from pivotal http://pivotallabs.com/merging-scopes-with-sti-models/ about using were_values in a scope to get all the conditions. I then used them on unscope to force the expected class, basically this:
def self.deposits
conditions = where(nil).where_values.reduce(&:and)
Deposit.unscoped.where(conditions)
end
This test asserts that it returns a Deposit::ActiveRecord_Relation https://gist.github.com/aalvarado/4ce836699d0ffb8b3782#file-sti_scope2-rb
Update
You can also write this as a scope if you prefer:
scope :deposits, -> { Deposit.unscoped.where where(nil).where_values.reduce &:and }

As a quick workaround you can do > a.transactions.deposits.merge(Deposit.pending), but can't think of a different way of solving it. I'll think and try more options later and come back if I find anything.

You might want to say that an Account has_many :deposits
class Account < ActiveRecord::Base
has_many :transactions
has_many :deposits
end
Then you should be able to query
a.deposits.pending

Related

Scope associated element with join table

I'm trying to scope the main group of my user. This group is noted with a cat: which is 2.
So I thought of doing this with a scope like
class User < ApplicationRecord
has_many :users_group, dependent: :destroy
has_many :groups, through: :users_group
scope :my_group, -> { self.joins(:groups).where('groups.cat = 2').limit(1) }
end
But the command below is not working :
current_user.my_group
Can you lead me on the good way to achieve it ?
As Mario says, a scope works on a collection, not an instance.
If you want to keep the method in the User model you can use the following:
user.rb
def my_group
groups.find_by_cat(2)
end
Using find_by will return a single group, rather than using where / limit. If the group isn't found, it will return nil.
I'd suggest using a scope to return a single instance is a bit of an anti-pattern, and it would be better achieved using this method, or dropping the following method into Group and calling current_user.groups.my_group - although the name my_group sounds a bit out of place like that. For completeness, here it is regardless:
group.rb
def my_group
find_by_cat(2)
end
current_user doesn't return an ActiveRecord relation, it just returns the user so you can't chain it together with a scope (I'm assuming the error message you're getting is undefined method 'my_group' for #<User>?). Add the scope to your Group class and use it through your groups has_many relationship e.g.
current_user.groups.my_group

How to get scope with polymorphic association

I am building a Rails 5 app and in this app I got two models.
First one is called Timeoff and second one is called Approval.
I want to get all Timeoff objects that got no approvals.
The time off model
class Timeoff < ApplicationRecord
scope :not_approved, -> { self.approvals.size > 0 }
has_many :approvals, as: :approvable, dependent: :destroy
end
The Approval model
class Approval < ApplicationRecord
belongs_to :approvable, polymorphic: true
end
I am calling it like this
Timeoff.not_approved
I get the error
NoMethodError: undefined method `approvals' for #<Class:0x007f9698587830>
You're trying to call approvals in the class context, but it actually belongs to an instance of Timeoff. For example:
Timeoff.approvals # doesn't work
Timeoff.first.approvals # works
That's why you get the undefined method error.
But I think you want a database query here. You could go two ways - that I know of:
Make two queries: find the timeoffs that have approvals and then query for the other ones using NOT IN
timeoff_ids = Approval.where(approvable_type: 'Timeoff').pluck(:approvable_id)
Timeoff.where.not(id: timeoff_ids)
This may get really slow if your tables are big.
Or you could do a join on the approvals table and filter to where the id is null:
Timeoff.joins("LEFT JOIN approvals ON timeoffs.id = approvals.approvable_id AND approvals.approvable_type = 'Timeoff'").where("approvals.id IS NULL")
This should also work, and may be faster - but you should measure with your own data to be sure.
Also, take a look at this question: How to select rows with no matching entry in another table? there is a complete explanation of the second query and some other ways to solve it.

Ruby on Rails Association build and assign 2 related associations

So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly

Rails, trouble with scopes

I'm not sure what I'm doing wrong here but I want to create scope for a model I have, but I want it to evaluated a count on a related model... like say:
class Thing < ActiveRecord::Base
has_many :photos
scope :with_images, self.photo.count > 0
end
class Photo < ActiveRecord::Base
belongs_to :thing
end
I should then have a scope which would work like
Thing.where('some conditions').with_images
I get a NoMethodError on photos, why wouldn't this be available as a relation? I don't want to use it as a method.
There are two things going on here. First, you're trying to call photo, instead of photos.
However, the you're still going to get an error because at the time of execution, self refers to the constant Thing, and not an instance of Thing. The declaration has_many :photos defines a method photos for instances of Thing. Therefore, Thing (the constant) doesn't have a method called photos.
tl;dr Just use a :joins argument since it will only find records that have photos
scope :with_images, :joins => :photos
It should be:
self.photos.count > 0
or if you're using counter cache:
self.photos_count > 0

How can I access ActiveRecord Associations in class callbacks in rails?

Updated
Appears to be a precedence error and nothing to do with the question I originally asked. See discussion below.
Original question
Is it possible to use active record associations in callbacks? I've tested this code in the console and it works fine as long as it isn't in a callback. I'm trying to create callbacks that pull attributes from other associated models and I keep getting errors of nil.attribute.
If callbacks are not the correct approach to take, how would one do a similar action in rails? If the associations are simple, you could use create_association(attributes => ), but as associations get more complex this starts to get messy.
For example...
class User < ActiveRecord::Base
belongs_to :b
before_validation_on_create {|user| user.create_b} #note, other logic prevents creating multiple b
end
class B < ActiveRecord::Base
has_many :users, :dependent => destroy
after_create{ |b| b.create_c }
has_one :c
end
class C < ActiveRecord::Base
belongs_to :b
after_create :create_alert_email
private
def create_alert_email
self.alert_email = User.find_by_b_id(self.b_id).email #error, looks for nil.email
end
end
Off course associations are available in your callbacks. After all, the create_after_email is simply a method. You can even call it alone, without using a callback. ActiveRecord doesn't apply any special flag to callback methods to prevent them from working as any other method.
Also notice you are running a User#find query directly without taking advantage of any association method. An other reason why ActiveRecord association feature should not be the guilty in this case.
The reason why you are getting the error should probably searched somewhere else.
Be sure self.b_id is set and references a valid record. Perhaps it is nil or actually there's no User record with that value. In fact, you don't test whether the query returns a record or nil: you are assuming a record with that value always exists. Are you sure this assumption is always statisfied?

Resources