Rails 3 Scope - Find users not in a specific group - ruby-on-rails

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

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

In Ruby on Rails, how can I create a scope for a has_many relationship?

Here is an example:
Let says I have a Student object which has a has_many relationship with ReportCard objects. ReportCard objects have a boolean field called "graded" that flags they have been graded. So it looks like:
class Student < ActiveRecord
has_many :report_cards
end
class ReportCard < ActiveRecord
# graded :boolean (comes from DB)
belongs_to :student
end
Now, let's say you want to create a default scope so that if a student has no graded ReportCards, you want to see all of them, but if they have at least one graded ReportCard, you only want to see the graded ones. Finally, let's say you order them by "semester_number".
Using this scope on ReportCard works correctly:
scope :only_graded_if_possible, ->(student) { where(graded: true, student: student).order(:semester_number).presence || order(:semester_number) }
But I want it to be the default scope for Student so I tried:
class Student < ActiveRecord
has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || order(:semester_number) }
end
but this does not work. It won't return any report_cards if there is a single graded report_card in the whole db. Looking at the queries that are run, first it runs something like:
SELECT report_cards.* FROM report_cards WHERE reports_cards.graded = t ORDER BY semester_number ASC
I think this must be the present? check part of the presence query and notice it does not filter on Student at all! So if there is a single report_card that is graded, the check passes and then it runs the following query to get what to return:
SELECT report_cards.* FROM reports_card WHERE report_card.student_id = 'student id here' AND report_card.graded = t ORDER BY semester_number
This query actually would be correct if the student had a graded report card but it is always empty if he does not.
I assume that possibly the filtering on Student is added afterwards. So I tried to somehow to get it to filter student right off the bat:
has_many :report_cards, ->{ where(graded: true, student: self).order(:semester_number).presence || order(:semester_number) }
This does not work either because it appears that "self" in this scope is not the Student object like I'd assume, but a list of all the report_card ids. Here is the query this one runs:
SELECT report_cards.* FROM report_cards WHERE report_cards.graded = t AND report_cards.student_id IN (SELECT report_cards.id FROM report_cards) ORDER BY semester_number ASC
That isn't even close to correct. How can I get this to work?
I think what it really comes down to is someone being able to pass "self" (meaning the current Student object) as a parameter into the scope being applied in the "has_many". Maybe that isn't possible.
You can pass object to has_many scope as a parameter to lambda
has_many :report_cards, -> (student) { ... }
Try this:
class Student < ActiveRecord::Base
has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || unscoped.order(:semester_number) }
end
I am using this in my project where I have a model which is associated with users:
has_many :users, -> { only_deleted }
And in the Users model create a scope of only_deleted, returning your users which are deleted.

Find user who has no post in Rails

This the the database relation:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
I come across a functionality where I want to query all the users who don't have any posts yet. I know that we can do this with something like this:
users = User.all
users.each do |user|
unless user.posts.any?
# do something when user don't have any post.
end
end
However, I wonder if there is any way to optimize this by using one query only.
Thanks!
This results in a single query which fetches all users who don't have posts yet:
User.includes(:posts).references(:posts).where('posts.id IS NULL')
Another solution is this:
User.where('NOT EXISTS(SELECT 1 FROM posts WHERE user_id = users.id)')
Since this is a rather complex query to use everywhere, you can place this inside a named scope in User:
class User < ActiveRecord::Base
scope :without_posts, -> { where('NOT EXISTS(SELECT 1 FROM posts WHERE user_id = users.id)') }
end
Now you can use this scope elsewhere in your application:
User.without_posts
I'd try something like
User.joins(posts).where("count(posts.id) = 0")
Which returns all users that have 0 posts.
with rails 6.1, even simpler:
User.where.missing(:posts)

HABTM relation find all records, excluding some based on association

I've looked at some of the similar SO posts relating to this but I'm struggling to get my head around it.
I have a habtm relation between Projects and Users. I'm trying to find all the Projects that a particular user does not belong to but I don't know how.
I've tried this sort of thing:
Project.where('project_id != ?', user.id)
But it's also obviously wrong.
I'm using rails 3.2.x
Many of the answers relating to this mention scopes but I haven't come across them before (I'm still very new to Rails).
I just found this post with one answer suggesting: Project.where('id not in (?)', user.projects)
which seems to work, except when user.projects is empty. I'm trying Project.where('id not in (?)', (d.projects.empty? ? '', d.projects))
as is suggested in JosephCastro's answer comment thread but it's giving me a syntax error on the second d.projects.
Edit
Project model snippet that relates to Users
class Project < ActiveRecord::Base
attr_accessible ...
has_and_belongs_to_many :users, :before_add => :validates_unique
and then
class User < ActiveRecord::Base
attr_accessible ...
has_and_belongs_to_many :projects
You can place a scope in your Project model like so:
scope :not_belonging_to, lambda {|user| joins(:projects_users).where('projects_users.user_id <> ?', user.id) }}
This assumes your join table name matches rails convention for HABTM associations
To then get projects that a user doesn't belong to, first find your user, then pass them to the scope like so:
#user = User.find(params[:id]) # example
#unowned_projects = Project.not_belonging_to(#user)
On reflection, that scope won't work as it will find projects that have more than one developer, if one of those is your guy.
Instead, use the following:
scope :not_belonging_to, lambda {|user| where('id NOT IN (?)', user.projects.empty? ? '' : user.projects) }
From Matt's reply above, which was extremely helpful.
I had trouble with this for a while. I attempted to use the following:
scope :not_belonging_to, lambda {|developer| where('id NOT IN (?)', developer.projects.empty? ? '' : developer.projects) }
But I received the following error:
SQLite3::SQLException: only a single result allowed for a SELECT that is part of an expression:
I found I needed to update the scope, adding .ids on the end. See below:
scope :not_belonging_to, lambda {|developer| where('id NOT IN (?)', developer.projects.empty? ? '' : developer.projects.ids) }

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