Rails ActiveRecord: Find All Users Except Current User - ruby-on-rails

I feel this should be very simple but my brain is short-circuiting on it. If I have an object representing the current user, and want to query for all users except the current user, how can I do this, taking into account that the current user can sometimes be nil?
This is what I am doing right now:
def index
#users = User.all
#users.delete current_user
end
What I don't like is that I am doing post-processing on the query result. Besides feeling a little wrong, I don't think this will work nicely if I convert the query over to be run with will_paginate. Any suggestions for how to do this with a query? Thanks.

It is possible to do the following in Rails 4 and up:
User.where.not(id: id)
You can wrap it in a nice scope.
scope :all_except, ->(user) { where.not(id: user) }
#users = User.all_except(current_user)
Or use a class method if you prefer:
def self.all_except(user)
where.not(id: user)
end
Both methods will return an AR relation object. This means you can chain method calls:
#users = User.all_except(current_user).paginate
You can exclude any number of users because where() also accepts an array.
#users = User.all_except([1,2,3])
For example:
#users = User.all_except(User.unverified)
And even through other associations:
class Post < ActiveRecord::Base
has_many :comments
has_many :commenters, -> { uniq }, through: :comments
end
#commenters = #post.commenters.all_except(#post.author)
See where.not() in the API Docs.

#users = (current_user.blank? ? User.all : User.find(:all, :conditions => ["id != ?", current_user.id]))

You can also create named_scope, e.g. in your model:
named_scope :without_user, lambda{|user| user ? {:conditions => ["id != ?", user.id]} : {} }
and in controller:
def index
#users = User.without_user(current_user).paginate
end
This scope will return all users when called with nil and all users except given in param in other case. The advantage of this solution is that you are free to chain this call with other named scopes or will_paginate paginate method.

Here is a shorter version:
User.all :conditions => (current_user ? ["id != ?", current_user.id] : [])

One note on GhandaL's answer - at least in Rails 3, it's worth modifying to
scope :without_user, lambda{|user| user ? {:conditions => ["users.id != ?", user.id]} : {} }
(the primary change here is from 'id != ...' to 'users.id !=...'; also scope instead of named_scope for Rails 3)
The original version works fine when simply scoping the Users table. When applying the scope to an association (e.g. team.members.without_user(current_user).... ), this change was required to clarify which table we're using for the id comparison. I saw a SQL error (using SQLite) without it.
Apologies for the separate answer...i don't yet have the reputation to comment directly on GhandaL's answer.

Very easy solution I used
#users = User.all.where("id != ?", current_user.id)

User.all.where("id NOT IN(?)", current_user.id) will through exception
undefined method where for #<Array:0x0000000aef08f8>
User.where("id NOT IN (?)", current_user.id)

Another easy way you could do it:
#users = User.all.where("id NOT IN(?)", current_user.id)

an array would be more helpful
arrayID[0]=1
arrayID[1]=3
User.where.not(id: arrayID)

User.where(:id.ne=> current_user.id)

ActiveRecord::QueryMethods#excluding (Rails 7+)
Starting from Rails 7, there is a new method ActiveRecord::QueryMethods#excluding.
A quote right from the official Rails docs:
excluding(*records)
Excludes the specified record (or collection of records) from the resulting relation. For example:
Post.excluding(post)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1
Post.excluding(post_one, post_two)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)
This can also be called on associations. As with the above example, either a single record of collection thereof may be specified:
post = Post.find(1)
comment = Comment.find(2)
post.comments.excluding(comment)
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2
This is short-hand for .where.not(id: post.id) and .where.not(id: [post_one.id, post_two.id]).
An ArgumentError will be raised if either no records are specified, or if any of the records in the collection (if a collection is passed in) are not instances of the same model that the relation is scoping.
Also aliased as: without
Sources:
Official docs - ActiveRecord::QueryMethods#excluding
PR - Add #excluding to ActiveRecord::Relation to exclude a record (or collection of records) from the resulting relation.
What's Cooking in Rails 7?

What you are doing is deleting the current_user from the #users Array. This won't work since there isn't a delete method for arrays. What you probably want to do is this
def index
#users = User.all
#users - [current_user]
end
This will return a copy of the #users array, but with the current_user object removed (it it was contained in the array in the first place.
Note: This may not work if array subtraction is based on exact matches of objects and not the content. But it worked with strings when I tried it. Remember to enclose current_user in [] to force it into an Array.

Related

how to join named scopes in rails

I want to join my named scopes as I generate them with an array.
how wouldI do that , i can't join named scopes, is there a better way to do this?
scope :search, ->(attrs_for_search,params) do
if attrs_for_search.present?
params_to_search_on = params.keys & attrs_for_search
params_to_search_on.collect{|x| where("#{x} ILIKE ?", "%#{params[x]}%") }
else
none
end
end
Contact.search(%w[email],{'email => 'jason'})
I think that you can create a scope and use the 'send' method to join the scopes.
scope :search, ->(field, value) do
where("#{field} LIKE ?", "%#{value}%")
end
def self.multi_search(params)
result = nil
params_to_search_on = params.keys
params_to_search_on.each do |k|
if result.nil?
result = Message.send(:search, k, params[k])
else
result = result.send(:search, k, params[k])
end
end
result
end
Hopes this help you.
You can't chain scopes when you return an array.
You can:
Try returning a relation:
results = params_to_search_on.collect{|x| where("#{x} ILIKE ?", "%#{params[x]}%") }
where(id: results.flatten.map(&:id))
I have not tested this, but I think you will need flatten because results is an array of relations [rel_1,_rel_2]
Then you can use/chain/attach-a-bunch-of scopes like this:
Contact.search(attrs, params).other_scope.another_scope(with_params)
You may want to read about full text search, to expand this topic, I would need to know the DB you are using.

How to pull the newest object from an array without using first or last?

I want to pull out the newest post from an array of posts. But using .first is messing it up for some reason. For some it is pulling the newest, while for some it is pulling the oldest.
I have: default_scope order: posts.created_at DESC
so first should give me the latest post but sometimes it doesn't. Is there a better way to get the newest post?
Update
Here is my controller code:
#users = User.includes(:ideas).order('users.updated_at DESC')
I want to order users by last updated DESC and then for each user show his most recent post. I think the default_scope order: posts.created_at DESC is clashing with .order('users.updated_at DESC')
In my view I am doing:
#users.each do |user|
post = user.posts.first
end
It's not working right. HOw do I fix this without N+1 problem?
You can sort by id if it's incremental:
scope :latest, -> { order('id DESC') }
Post.latest.first
Post.latest.limit(10)
One more benefit for relational databases here is that there is an index on id already.
For some reason, the order clause in the default scope is being overwritten.
You can force the ordering with reorder:
Post.reorder(created_at: :desc).first
Use it in a named scope:
class Post < ActiveRecord::Base
scope :latest, -> { reorder(created_at: :desc).first }
end
References: reorder on Rails API
Edit for the updated question
With the default scope defined as below in post.rb:
default_scope { order(created_at: :desc) }
You can simply write:
#posts = Post.includes(:user).group('posts.user_id')
You will get the latest posts (one per user)
If some posts don't have a user:
#posts = Post.joins(:user).select('posts.*, users.username AS user_username, etc........').group('posts.user_id')
Each post of #posts will get a user_username method without the n+1 problem. You can of course define more custom methods depending of what you need.
If
Post.first(:order => 'created_at DESC')
won't work (or are you interested in the most recently updated) there's something broken somewhere...
It should be like :
Post.order('created_at DESC').first

Sort posts by number of votes within certain time frame

I currently have a voting system implemented in my app and I'm sorting the posts by number of votes with this code in my view:
<%= render #posts.sort_by { |post| post.votes.count }.reverse %>
I want to sort by number of votes for each post by also don't want the post to be any more than lets say 5 days old. How can I sort the posts by both number of votes and date simultaneously.
This is wrong. You should do all sorting operation on the database side.
For this example consider using Arel for creating complex queries or consider create counter cache column.
You could just add a scope to your posts model, something like:
scope :five_days_ago, lambda { where("created_at >= :date", :date => 5.days.ago) }
Then just adjust your render method to the following:
<%= render #posts.five_days_ago.sort_by { |post| post.votes.count }.reverse %>
This assumes you want to keep the structure you are using. Obviously, as other suggested, doing it all in the database is the best course of action.
luacassus is right. It's better do delegate the sorting to the database for at least two reasons:
Performance
You can chain more query methods onto it (necessary for pagination, for example).
The counter cache is probably the best idea, but for the complex query, let me give it a shot. In your Post model:
class << self
def votes_descending
select('posts.*, count(votes.id) as vote_count').joins('LEFT OUTER JOIN votes on votes.post_id = posts.id').group_by('posts.id').order('votes_count desc')
end
def since(date)
where('created_at >= ?', date)
end
end
So...
#posts = Post.votes_descending.since(5.days.ago)
Indeed it will be better to let the db do the sorting. I would do something like
class Post < ActiveRecord::Base
default_scope :order => 'created_at DESC'
end
then you will always have your posts sorted and if I'm not mistaken the last one should be the first you get, so that substitutes your 'reverse' call. Then you can use the scope posted above to get only 5 days old posts. Also check that there is an index on the created_at column in the db. You can do this with
SHOW INDEX FROM posts
The db will do this much faster than ruby.
I figured out another way to do it although I appreciate your help it may not be the cleanest way but I did
def most
range = "created_at #{(7.days.ago.utc...Time.now.utc).to_s(:db)}"
#posts = Post.all(:conditions => range)
#title = "All Posts"
#vote = Vote.new(params[:vote])
respond_to do |format|
format.html
format.json { render :json => #users }
end
end
for my controller
created a route of /most :to => 'posts#most'
and made a view with the original code I had in my view.
I know its not the best way but I am still new to coding so its the best way I could figure out how.

Find and display nearest date in RoR

I am new to ruby on rails and I'm not sure where to start with this. I have a model for users, and one for projects. Users have many projects, and projects have one user. There is an end_date column in the projects table (as well as a name column).
What I want to do is find the project with the nearest end_date and display it's name and end date on the user's show page.
I tried putting this code in the projects controller, but I do not know if it is working, because I don't know how to access it and display the project name in the view.
def next_deadline(after = DateTime.now, limit = 1)
find(:all, :conditions => ['end_date > ?', after], :limit => limit)
end
Any help would be appreciated. Let me know if more information is needed.
As #Dan mentioned, you do need the :order clause to get the first one, but you should add it to your query and not replace the :conditions (otherwise you'll get the project with the earliest end_date irrespective of your after argument). The way you're defining this method is a bit off though. It should be defined in your Project model (and definitely not the controller) as a class method, or, what I think is a better approach, as a scope. In Rails < 3 (which it seems that you're using):
class Project < ActiveRecord::Base
named_scope :next_deadline, Proc.new { |after = DateTime.now, limit = 1| {:conditions => ['end_date > ?', after], :order => "end_date ASC", :limit => limit} }
...
end
Or in Rails >= 3:
class Project < ActiveRecord::Base
scope :next_deadline, Proc.new { |after = DateTime.now, limit = 1| where('end_date > ?', after).order("end_date ASC").limit(limit) }
...
end
Also, you can always test this kind of code using the Rails console: script/console in Rails < 3, rails c in Rails >= 3.
#projects = Project.find_by_sql("SELECT projects.* FROM projects
JOIN users ON users.id = projects.user_id AND projects.user_id = " + #user.id.to_s + "
WHERE projects.end_date > now()
ORDER BY projects.end_date ASC
LIMIT " + limit)
or
#projects = Project.where(:user_id => #user.id)
.where("end_date > ?", DateTime.now)
.order("end_date ASC")
You want to use :order, not :conditions.
Model.find(:all , :order => "end_date ASC")
Then the first result will be the item with the closest end_date
As Dan said, the condition you wrote won't get the nearest end date, but the dates that are greater than today, or the date passed in as a parameter.
In your User model you could write
def next_deadline_project
self.projects.first
end
as long as you give projects a default scope that orders records by end_date
In order to show information on the view you must set it in an instance variable in the User's controller show method. Instance variables are passed to views and you can access them to display the data.
#project = next_deadline_project
And in your show.html.erb you can use something like:
<%= #project.name %> - <%= #project.end_date %>

ActiveRecord Query Union

I've written a couple of complex queries (at least to me) with Ruby on Rail's query interface:
watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})
Both of these queries work fine by themselves. Both return Post objects. I would like to combine these posts into a single ActiveRelation. Since there could be hundreds of thousands of posts at some point, this needs to be done at the database level. If it were a MySQL query, I could simply user the UNION operator. Does anybody know if I can do something similar with RoR's query interface?
Here's a quick little module I wrote that allows you to UNION multiple scopes. It also returns the results as an instance of ActiveRecord::Relation.
module ActiveRecord::UnionScope
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def union_scope(*scopes)
id_column = "#{table_name}.id"
sub_query = scopes.map { |s| s.select(id_column).to_sql }.join(" UNION ")
where "#{id_column} IN (#{sub_query})"
end
end
end
Here's the gist: https://gist.github.com/tlowrimore/5162327
Edit:
As requested, here's an example of how UnionScope works:
class Property < ActiveRecord::Base
include ActiveRecord::UnionScope
# some silly, contrived scopes
scope :active_nearby, -> { where(active: true).where('distance <= 25') }
scope :inactive_distant, -> { where(active: false).where('distance >= 200') }
# A union of the aforementioned scopes
scope :active_near_and_inactive_distant, -> { union_scope(active_nearby, inactive_distant) }
end
I also have encountered this problem, and now my go-to strategy is to generate SQL (by hand or using to_sql on an existing scope) and then stick it in the from clause. I can't guarantee it's any more efficient than your accepted method, but it's relatively easy on the eyes and gives you a normal ARel object back.
watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})
Post.from("(#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}) AS posts")
You can do this with two different models as well, but you need to make sure they both "look the same" inside the UNION -- you can use select on both queries to make sure they will produce the same columns.
topics = Topic.select('user_id AS author_id, description AS body, created_at')
comments = Comment.select('author_id, body, created_at')
Comment.from("(#{comments.to_sql} UNION #{topics.to_sql}) AS comments")
Based on Olives' answer, I did come up with another solution to this problem. It feels a little bit like a hack, but it returns an instance of ActiveRelation, which is what I was after in the first place.
Post.where('posts.id IN
(
SELECT post_topic_relationships.post_id FROM post_topic_relationships
INNER JOIN "watched" ON "watched"."watched_item_id" = "post_topic_relationships"."topic_id" AND "watched"."watched_item_type" = "Topic" WHERE "watched"."user_id" = ?
)
OR posts.id IN
(
SELECT "posts"."id" FROM "posts" INNER JOIN "news" ON "news"."id" = "posts"."news_id"
INNER JOIN "watched" ON "watched"."watched_item_id" = "news"."id" AND "watched"."watched_item_type" = "News" WHERE "watched"."user_id" = ?
)', id, id)
I'd still appreciate it if anybody has any suggestions to optimize this or improve the performance, because it's essentially executing three queries and feels a little redundant.
You could also use Brian Hempel's active_record_union gem that extends ActiveRecord with an union method for scopes.
Your query would be like this:
Post.joins(:news => :watched).
where(:watched => {:user_id => id}).
union(Post.joins(:post_topic_relationships => {:topic => :watched}
.where(:watched => {:user_id => id}))
Hopefully this will be eventually merged into ActiveRecord some day.
Could you use an OR instead of a UNION?
Then you could do something like:
Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)
(Since you are joins the watched table twice I'm not too sure what the names of the tables will be for the query)
Since there are a lot of joins, it might also be quite heavy on the database, but it might be able to be optimized.
How about...
def union(scope1, scope2)
ids = scope1.pluck(:id) + scope2.pluck(:id)
where(id: ids.uniq)
end
Arguably, this improves readability, but not necessarily performance:
def my_posts
Post.where <<-SQL, self.id, self.id
posts.id IN
(SELECT post_topic_relationships.post_id FROM post_topic_relationships
INNER JOIN watched ON watched.watched_item_id = post_topic_relationships.topic_id
AND watched.watched_item_type = "Topic"
AND watched.user_id = ?
UNION
SELECT posts.id FROM posts
INNER JOIN news ON news.id = posts.news_id
INNER JOIN watched ON watched.watched_item_id = news.id
AND watched.watched_item_type = "News"
AND watched.user_id = ?)
SQL
end
This method returns an ActiveRecord::Relation, so you could call it like this:
my_posts.order("watched_item_type, post.id DESC")
There is an active_record_union gem.
Might be helpful
https://github.com/brianhempel/active_record_union
With ActiveRecordUnion, we can do:
the current user's (draft) posts and all published posts from anyone
current_user.posts.union(Post.published)
Which is equivalent to the following SQL:
SELECT "posts".* FROM (
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
UNION
SELECT "posts".* FROM "posts" WHERE (published_at < '2014-07-19 16:04:21.918366')
) posts
In a similar case I summed two arrays and used Kaminari:paginate_array(). Very nice and working solution. I was unable to use where(), because I need to sum two results with different order() on the same table.
Heres how I joined SQL queries using UNION on my own ruby on rails application.
You can use the below as inspiration on your own code.
class Preference < ApplicationRecord
scope :for, ->(object) { where(preferenceable: object) }
end
Below is the UNION where i joined the scopes together.
def zone_preferences
zone = Zone.find params[:zone_id]
zone_sql = Preference.for(zone).to_sql
region_sql = Preference.for(zone.region).to_sql
operator_sql = Preference.for(Operator.current).to_sql
Preference.from("(#{zone_sql} UNION #{region_sql} UNION #{operator_sql}) AS preferences")
end
Less problems and easier to follow:
def union_scope(*scopes)
scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) }
end
So in the end:
union_scope(watched_news_posts, watched_topic_posts)
gem 'active_record_extended'
Also has a set of union helpers among many others.
I would just run the two queries you need and combine the arrays of records that are returned:
#posts = watched_news_posts + watched_topics_posts
Or, at the least test it out. Do you think the array combination in ruby will be far too slow? Looking at the suggested queries to get around the problem, I'm not convinced that there will be that significant of a performance difference.
Elliot Nelson answered good, except the case where some of the relations are empty. I would do something like that:
def union_2_relations(relation1,relation2)
sql = ""
if relation1.any? && relation2.any?
sql = "(#{relation1.to_sql}) UNION (#{relation2.to_sql}) as #{relation1.klass.table_name}"
elsif relation1.any?
sql = relation1.to_sql
elsif relation2.any?
sql = relation2.to_sql
end
relation1.klass.from(sql)
end
When we add UNION to the scopes, it breaks at time due to order_by clause added before the UNION.
So I changed it in a way to give it a UNION effect.
module UnionScope
def self.included(base)
base.send(:extend, ClassMethods)
end
module ClassMethods
def union_scope(*scopes)
id_column = "#{table_name}.id"
sub_query = scopes.map { |s| s.pluck(:id) }.flatten
where("#{id_column} IN (?)", sub_query)
end
end
end
And then use it like this in any model
class Model
include UnionScope
scope :union_of_scopeA_scopeB, -> { union_scope(scopeA, scopeB) }
end
Tim's answer is great. It uses the ids of the scopes in the WHERE clause. As shosti reports, this method is problematic in terms of performance because all ids need to be generated during query execution. This is why, I prefer joeyk16 answer. Here a generalized module:
module ActiveRecord::UnionScope
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def self.union(*scopes)
self.from("(#{scopes.map(&:to_sql).join(' UNION ')}) AS #{self.table_name}")
end
end
end
If you don't want to use SQL syntax inside your code, here's solution with arel
watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id}).arel
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id}).arel
results = Arel::Nodes::Union.new(watched_news_posts, watched_topic_posts)
from(Post.arel_table.create_table_alias(results, :posts))

Resources