Scope associated element with join table - ruby-on-rails

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

Related

Includes with a related element

I would like to use the includes method with the related element of my Post
My Post can be associated with different type of element. And I use a value :cat to knows witch kind of element is associated.
The value work as this (cat: (1 => Message, 2=>Question, 3=>Task, 4=>Event) with the association has_one
Example : If post.cat == 3, I can call the task related with a method post.task
Now, I would like to optimize the SQL requests of my Post/Index with the method includes. But is not working for the moment. Can you help me to find the error of my code ?
Post_controller :
def index
#posts = current_user.posts
#posts.each do |post|
if post.cat == 3
#task = post.task.includes(:users)
elsif post.cat == 4
#event = post.event.includes(:reminds)
end
end
end
Error: undefined method `includes'
Edit :
Post_model:
class Post < ApplicationRecord
has_one :post_message, dependent: :destroy
has_one :question, dependent: :destroy
has_one :task, dependent: :destroy
has_one :event, dependent: :destroy
end
Task_model :
class Task < ApplicationRecord
belongs_to :post
has_many :users_task, dependent: :destroy
has_many :users, through: :users_task
end
Why are you using #posts.each ?
For me, the best solution for that is to find all the posts whith the defined cat to run the includes method. In your case, it would be like that :
#posts.where(cat: 1).includes(:message)
#posts.where(cat: 2).includes(:question)
#posts.where(cat: 3).includes(task: :users)
#posts.where(cat: 4).includes(event: :reminds)
Well, after many tries, I opted for a scope method to run the includes method. It's not a really elegant solution, but I think it's the best in my case.
So I'm preparing the scopes in my Post_Model:
scope :with_tasks, -> { where(cat: 3).includes(:user).includes(task: :users) }
scope :with_events, -> { where(cat: 4).includes(:user).includes(event: :reminds) }
And after, I render them in my index action like this :
#posts = current_user.posts.with_tasks + current_user.posts.with_events
So the code is generating 2 SQL Requests to find the posts (one for each category).
I think there is a way to join all that directly into a new global scope, but I don't know how. So if there is anyone knows that, he can edit the answer
Enjoy !
If you're getting an undefined method: 'includes' error, it means that either post.task or post.event are not returning ActiveRecord objects like your code is expecting. Are you sure there will always be values set for .task or .event at that point in execution? Are there any cases where that value might be nil or blank?
By the way, have you heard about 'polymorphic associations'? Defining an association as polymorphic allows you to associate records of arbitrary types with a specific column (by storing both object ID and class name on each record behind the scenes). It seems like this exactly matches your use case. It would be much easier to use the built-in mechanism than trying to do all the if-then switching based on category in your code.

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

Undefined method error for scope on STI subclass

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

Rails 3 Scope - Find users not in a specific group

Here is my problem (a bit simplified) :
I've got the following models :
class User
has_many :group_users
has_many :groups, through: :group_users
class Group
has_many :group_users
has_many :users, through: :group_users
class GroupUser
belongs_to :group
belongs_to :user
scope :belonging_to_group ->(group) {where(group_id = group.id)}
I would like to scope users that are not in one specific group, let's say veggies for examples, something that would begin like this :
scope :not_in_group, ->(group)
I've tried stuffs with having clauses like that:
scope :not_in_group, ->(group) {joins(:group_users).merge(::GroupUser.belonging_to_group(group)).group(:title).having('count(group_users.user_id) = 0')
but nothing seems to work
EDIT : I've got another problem now, you may want to check this if you're calling your scope from an other class' class method : Rails - Use a class method from a another class method with one attribute of the second class
Try This
scope :not_in_group, -> group_id {joins(:group_users).where('group_users.group_id != ?', group_id)}
For left join, try this:
scope :not_in_group, -> group_id {joins("left join group_users on users.id = group_users.user_id").where('group_users.group_id != ?', group_id)}
According to the rails documentation:
Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects. source.
class User
#...
# Retrieve all users not in a specific group
# example: User.not_in_group(Group.find(5))
def self.not_in_group(group)
includes(:group_users).where("group_users.group_id != ?", group.id)
end
end
If you are determined to use a scope, here it is:
scope :not_in_group, ->(group) {includes(:group_users).where("group_users.group_id != ?", group.id)}
EDIT : I realised that it was not working fine if a user had 2 groups, as he would appear as not in both. I believe that MySQL found an other group_users with the user's id and the other group_id.
So I changed to this, which seems to work :
scope :not_in_group, ->(group){
in_group = User.joins(:group_users).where("group_users.group_id = ?", group.id)
where(arel_table[:id].not_in in_group.map(&:id))
}
In two times but working.
Previous solution : only works if your user has one group.
Finally I found a solution, I had to pass raw SQL in joins() and where() as #Himesh suggested me to do. Thanks a lot to you and #lightswitch05 for your help.
Both of your propositions did not select the users without any group, here is a solution that works :
def not_in_group(group)
joins('LEFT JOIN group_users ON group_users.user_id = users.id').where("group_users.group_id != ? OR group_users.group_id is null", group.id)
end
Found some held there : Rails 3 ActiveRecord where clause where id is set or null

Traversing HABTM relationships on ActiveRecord

I'm working on a project for my school on rails (don't worry this is not graded on code) and I'm looking for a clean way to traverse relationships in ActiveRecord.
I have ActiveRecord classes called Users, Groups and Assignments. Users and Groups have a HABTM relationship as well as Groups and Assignments. Now what I need is a User function get_group(aid) where "given a user, find its group given an assignment".
The easy route would be:
def get_group(aid)
group = nil
groups.each { |g| group = g if g.assignment.find(aid).id == aid }
return group
end
Is there a cleaner implementation that takes advantage of the HABTM relationship between Groups and Assignments rather than just iterating? One thing I've also tried is the :include option for find(), like this:
def get_group(aid)
user.groups.find(:first,
:include => :assignments,
:conditions => ["assignments.id = ?", aid])
end
But this doesn't seem to work. Any ideas?
First off, be careful. Since you are using has_and_belongs_to_many for both relationships, then there might be more than one Group for a given User and Assignment. So I'm going to implement a method that returns an array of Groups.
Second, the name of the method User#get_group that takes an assignment id is pretty misleading and un-Ruby-like.
Here is a clean way to get all of the common groups using Ruby's Array#&, the intersection operator. I gave the method a much more revealing name and put it on Group since it is returning Group instances. Note, however, that it loads Groups that are related to one but not the other:
class Group < ActiveRecord::Base
has_and_belongs_to_many :assignments
has_and_belongs_to_many :users
# Use the array intersection operator to find all groups associated with both the User and Assignment
# instances that were passed in
def self.find_all_by_user_and_assignment(user, assignment)
user.groups & assignment.groups
end
end
Then if you really needed a User#get_groups method, you could define it like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
def get_groups(assignment_id)
Group.find_all_by_user_and_assignment(self, Assignment.find(assignment_id))
end
end
Although I'd probably name it User#groups_by_assignment_id instead.
My Assignment model is simply:
class Assignment < ActiveRecord::Base
has_and_belongs_to_many :groups
end

Resources