ruby / rails ordering of result set - ruby-on-rails

I am trying to produce a high score chart, with a list of users who have been given a "thumbs" ordered by the number of thumbs descending. I have it almost working, but cannot think how to get the records to order correctly.
So we have two models, being User and Thumb.
In the controller, we have
def thumbs_highscores
user_ids = current_account.thumbs.from_this_month.map(&:user_id)
#users = current_account.users.where(:id =>emp_ids)
end
In the view I am then displaying the high scores likes this...
%table.center{:style => "max-width: 600px"}
%tr
%th Rank
%th Name
%th.right # Thumbs received
- #users.each_with_index do |user, i|
%tr
%td= i+1
%td= user.name
%td.right
- user.thumbs.from_this_month.each do |thumb|
%i.fa.fa-star.orange
This works nicely, but is not displaying the users in the correct order. I would like to amend the controller code to list the users in the order of how many thumbs they have had in the last month but cannot get my head round it. Can anyone help?
thanks

change
#users.each_with_index do |user, i|
to
#users.sort_by{|u| u.thumbs_from_this_month.size}.reverse.each_with_index do |user, i|
You could eager load the thumbs associations in the controller to make this more efficient, and do the ordering there.

From what I understand from your code you should have a scope named from_this_month so my suggestion is to add order in the query, so you don't need to care about extra code and complexity to order your data.
Check the order documentation

I guess you could use the help of scope here. I'm not very skilled with lambdas, but I guess something like this in the user-model would work:
scope :thumbs_desc_list, lambda { User.all.map {|u| u.thumbs_this_month}.sort { |x,y| y <=> x } }
This ofcourse assumes you have an instance method in your user-model called thumbs_this_month which gives you the amount of thumbs received this month.
After that in the controller:
#users = User.thumbs_desc_list
... and now you should be able to iterate over #users in your view wherever you want, having them ordered by thumbs amount descendingly.
Using scope will allow you to use the properly order list wherever you need afterwards too.

You can change your code into below:
User.select("users.*, thumbs.*, count(thumbs.id) AS thumbs_count").joins(:thumbs).from_this_month.group("users.id").order("thumbs_count DESC")
Let me know when it works.

Related

How can I move this logic into controller and is there need for that?

Ok guys I need to ask how can I move this into controller
- if UserGroup.where("group_id = ? AND user_id = ?", annoucement.group_id, current_user.id).exists? and annoucement.exp_date >= Date.today
It is part of my view and I do not know how to pass values from each to it
#Part of index.html.haml
- #annoucements.each do |annoucement|
- if UserGroup.where("group_id = ? AND user_id = ?", annoucement.group_id, current_user.id).exists? and annoucement.exp_date >= Date.today
%tr{id: "annoucement_#{annoucement.id}"}
%td= annoucement.title
%td= annoucement.description
You're absolutely on the right track. Logic like this doesn't belong in the view, and possibly not even in the controller. You want to push the logic 'back', as far as you can, into the controller, maybe into the model. Logic in the view is code smell :D
I believe what you want is a left_outer_join. It looks like UserGroup is a join table between Users and Groups, right? If that's the case, then the naming convention would be UsersGroup. I'm going to work on this assumption. My code may not match what you've got, so adjust to taste.
This is using Rails 5 left_outer_join
https://blog.bigbinary.com/2016/03/24/support-for-left-outer-joins-in-rails-5.html
But the same thing can be done before Rails 5, using joins
https://apidock.com/rails/ActiveRecord/QueryMethods/joins
Announcement.left_outer_joins(:users_groups).where(users_groups: { user_id: current_user.id }).uniq
This will get all the announcements that have user groups, and then filter them with the current user ID.
You could put this in the controller and assign it to a variable, or you could make a scope in your model, or a myriad of other options. Good luck!

Rails sort users by method

I'm trying to rank my user's in order of an integer. The integer I'm getting is in my User Model.
def rating_number
Impression.where("impressionable_id = ?", self).count
end
This gives each User on the site a number (in integer form). Now, on the homepage, I want to show an ordered list that places these user's in order with the user with the highest number first and lowest number second. How can I accomplish this in the controller???
#users = User....???
Any help would be appreciated!
UPDATE
Using this in the controller
#users = User.all.map(&:rating_number)
and this for the view
<% #users.each do |user| %>
<li><%= user %></li>
<% end %>
shows the user's count. Unfortunately, the variable user is acting as the integer not the user, so attaching user.name doesn't work. Also, the list isn't in order based on the integer..
The advice here is still all kinds of wrong; all other answers will perform terribly. Trying to do this via a nested select count(*) is almost as bad an idea as using User.all and sorting in memory.
The correct way to do this if you want it to work on a reasonably large data set is to use counter caches and stop trying to order by the count of a related record.
Add a rating_number column to the users table, and make sure it has an index defined on it
Add a counter cache to your belongs_to:
class Impression < ActiveRecord::Base
belongs_to :user, counter_cache: :rating_number
end
Now creating/deleting impressions will modify the associated user's rating_number.
Order your results by rating_number, dead simple:
User.order(:rating_number)
The advice here is just all kinds of wrong. First of model your associations correctly. Secondly you dont ever want to do User.all and then sort it in-memory based on anything. How do you think it will perform with lots of records?
What you want to do is query your user rows and sort them based on a subquery that counts impressions for that user.
User.order("(SELECT COUNT(impressions.id) FROM impressions WHERE impressionable_id = users.id) DESC")
While this is not terribly efficient, it is still much more efficient than operating with data sets in memory. The next step is to cache the impressions count on the user itself (a la counter cache), and then use that for sorting.
It just pains me that doing User.all is the first suggestion...
If impressions is a column in your users table, you can do
User.order('impressions desc')
Edit
Since it's not a column in your users table, you can do this:
User.all.each(&:rating_number).sort {|x,y| y <=> x }
Edit
Sorry, you want this:
User.all.sort { |x, y| y.rating_number <=> x.rating_number }

Order Users with most Songs Desc

Hopefully a simple question. I have several models, two of them, :users and :songs, interact to get data from the database.
A User has_many :songs.
I'm trying to find the users with the most songs in the USER INDEX action, i.e list the 10 users with the most songs, user with the most songs at the top, descending.
So far I have in the users_controller in index;
#users = User.all
And so far all I can do in the view, users/index;
<% #users.each do |user| %>
<%= user.name %><%= user.songs.count %>
<% end %>
Works, it counts the songs but how do I order by users with most songs?
I have been looking at sort_by inside the block and I guess it could be done by listing all songs and grouping them by the user, but I feel that is not efficient enough. -as you can see I'm not an advanced developer.
Please tell me what you guys think and the answer to my solution if possible. The thing that is confusing me is that I am ordering a list generating from a table by data generated by another table. Must be simple but I haven't done it before so I can't get my head around it.
Thanks in Advance.
You should use something like this:
User.includes(:songs).order{|x| x.songs.size}
This does everything in one query so it should be more efficient than for example
User.all.sort{|x| user.songs.size}
Which would perform a query for each user
How about
User.all(:limit => 10, :order => "(select count(user_id) from songs where user_id = users.id) DESC")

Using will_paginate with multiple models (Rails)

Pretty sure that I'm missing something really simple here:
I'm trying to display a series of pages that contain instances of two different models - Profiles and Groups. I need them ordering by their name attribute. I could select all of the instances for each model, then sort and paginate them, but this feels sloppy and inefficient.
I'm using mislav-will_paginate, and was wondering if there is any better way of achieving this? Something like:
[Profile, Group].paginate(...)
would be ideal!
Good question, I ran into the same problem a couple of times. Each time, I ended it up by writing my own sql query based on sql unions (it works fine with sqlite and mysql). Then, you may use will paginate by passing the results (http://www.pathf.com/blogs/2008/06/how-to-use-will_paginate-with-non-activerecord-collectionarray/). Do not forget to perform the query to count all the rows.
Some lines of code (not tested)
my_query = "(select posts.title from posts) UNIONS (select profiles.name from profiles)"
total_entries = ActiveRecord::Base.connection.execute("select count(*) as count from (#{my_query})").first['count'].to_i
results = ActiveRecord::Base.connection.select_rows("select * from (#{my_query}) limit #{limit} offset #{offset}")
Is it overkilled ? Maybe but you've got the minimal number of queries and results are consistent.
Hope it helps.
Note: If you get the offset value from a http param, you should use sanitize_sql_for_conditions (ie: sql injection ....)
You can get close doing something like:
#profiles, #groups = [Profile, Group].map do |clazz|
clazz.paginate(:page => params[clazz.to_s.downcase + "_page"], :order => 'name')
end
That will then paginate using page parameters profile_page and group_page. You can get the will_paginate call in the view to use the correct page using:
<%= will_paginate #profiles, :page_param => 'profile_page' %>
....
<%= will_paginate #groups, :page_param => 'group_page' %>
Still, I'm not sure there's a huge benefit over setting up #groups and #profiles individually.
in my last project i stuck into a problem, i had to paginate multiple models with single pagination in my search functionality.
it should work in a way that the first model should appear first when the results of the first model a second model should continue the results and the third and so on as one single search feed, just like facebook feeds.
this is the function i created to do this functionality
def multi_paginate(models, page, per_page)
WillPaginate::Collection.create(page, per_page) do |pager|
# set total entries
pager.total_entries = 0
counts = [0]
offsets = []
for model in models
pager.total_entries += model.count
counts << model.count
offset = pager.offset-(offsets[-1] || 0)
offset = offset>model.count ? model.count : offset
offsets << (offset<0 ? 0 : offset)
end
result = []
for i in 0...models.count
result += models[i].limit(pager.per_page-result.length).offset(offsets[i]).to_a
end
pager.replace(result)
end
end
try it and let me know if you have any problem with it, i also posted it as an issue to will_paginate repository, if everyone confirmed that it works correctly i'll fork and commit it to the library.
https://github.com/mislav/will_paginate/issues/351
Have you tried displaying two different sets of results with their own paginators and update them via AJAX? It is not exactly what you want, but the result is similar.

ActiveRecord and SELECT AS SQL statements

I am developing in Rails an app where I would like to rank a list of users based on their current points. The table looks like this: user_id:string, points:integer.
Since I can't figure out how to do this "The Rails Way", I've written the following SQL code:
self.find_by_sql ['SELECT t1.user_id, t1.points, COUNT(t2.points) as user_rank FROM registrations as t1, registrations as t2 WHERE t1.points <= t2.points OR (t1.points = t2.points AND t1.user_id = t2.user_id) GROUP BY t1.user_id, t1.points ORDER BY t1.points DESC, t1.user_id DESC']
The thing is this: the only way to access the aliased column "user_rank" is by doing ranking[0].user_rank, which brinks me lots of headaches if I wanted to easily display the resulting table.
Is there a better option?
how about:
#ranked_users = User.all :order => 'users.points'
then in your view you can say
<% #ranked_users.each_with_index do |user, index| %>
<%= "User ##{index}, #{user.name} with #{user.points} points %>
<% end %>
if for some reason you need to keep that numeric index in the database, you'll need to add an after_save callback to update the full list of users whenever the # of points anyone has changes. You might look into using the acts_as_list plugin to help out with that, or that might be total overkill.
Try adding user_rank to your model.
class User < ActiveRecord::Base
def rank
#determine rank based on self.points (switch statement returning a rank name?)
end
end
Then you can access it with #user.rank.
What if you did:
SELECT t1.user_id, COUNT(t1.points)
FROM registrations t1
GROUP BY t1.user_id
ORDER BY COUNT(t1.points) DESC
If you want to get all rails-y, then do
cool_users = self.find_by_sql ['(sql above)']
cool_users.each do |cool_user|
puts "#{cool_user[0]} scores #{cool_user[1]}"
end

Resources