How to Rank in Rails After Grouping - ruby-on-rails

In my Rails 6 app where I'm using Postgres, I have a table called UserCategories.
| id | user_id | category_id | points| rank |
What I'm trying to do is:
Group the records by category_id
Sort the records for each category_id by points (desc)
Update the rank field based on the order the record is in for the category_id
Example (desired rank determined by the points per category_id):
| id | user_id | category_id | points| rank |
| 1 | 1 | 1 | 2 | | # I want rank to be 1
| 2 | 2 | 1 | 1 | | # I want rank to be 2
| 3 | 1 | 2 | 3 | | # I want rank to be 1
| 4 | 2 | 2 | 3 | | # I want rank to be 1
My model method:
def self.calculate_user_category_ranks
#user_categories = UserCategory.select(:id, :points, :user_id, :category_id, :rank).all.order(points: :desc).group_by(&:category_id)
# returns:
# {2=>[#<UserCategory:0x000000000de8be00 id: 2, user_id: 1, category_id: 2, points: 3, rank: 0>, #<UserLeague:0x000000000de8bce8 id: 4, user_id: 2, category_id: 2, points: 3, rank: 0>],
1=>[#<UserCategory:0x000000000de8bbf8 id: 1, user_id: 1, category_id: 1, points: 2, rank: 0>, <UserLeague:0x000000000de8bb30 id: 3, user_id: 2, category_id: 1, points: 1, rank: 0>]}
rank = 0
points_counter = 0
#user_categories.each do |id, points|
uc = UserCategory.find(id)
if points != point_counter
rank += 1
point_counter = points
end
uc.rank = rank
uc.save
end
end
After I execute this code:
| id | user_id | category_id | points| rank |
| 1 | 1 | 1 | 2 | 2 | # I want rank to be 1
| 2 | 2 | 1 | 1 | 0 | # I want rank to be 2
| 3 | 1 | 2 | 3 | 1 | # I want rank to be 1
| 4 | 2 | 2 | 3 | 0 | # I want rank to be 1
Can someone please help me determine what I'm doing wrong?

You may be choosing to put rank as a database column for reasons of efficiency, but the principles of database normalization would suggest that it is "bad practice" to have a column whose value is calculable from the other columns in the table. So, recognizing that you may not accept this solution for reasons of efficiency, let me suggest that, for any particular instance of UserCategory you can determine its rank in Ruby:
class UserCategory < ApplicationRecord
scope :in_the_same_category, ->(category_id) { where("category_id = ?", category_id }
def in_my_category
UserCategory.in_the_same_category(category_id)
end
def rank
in_my_category.
sort_by(&:points).
reverse.
map(&:points).
uniq.
index(points) + 1
end
end

Start with
UserCategory.group(:category_id)
Then build it up
If you really need to restrict the fields returned or add a where clause then
UserCategory.where(some_field: "some_value").select(:id, :points, :user_id, :category_id, :rank).group(:category_id).order(:points :desc)
https://apidock.com/rails/ActiveRecord/QueryMethods/group for more info

Related

How do I get 1 row from set of multiple rows based on a condition in PostgreSQL 10?

I have two tables, user and car with below mentioned rows and columns.
Table 1: user
id | name
---------
1 | ABC
2 | PQR
3 | XYZ
Table 2: car
id | user_id | is_serviced
--------------------------
1 | 1 | 0
2 | 1 | 1
3 | 2 | 0
4 | 2 | 0
User ABC has two cars - only one car has been serviced.
User PQR has two cars - none of the cars has been serviced.
User XYZ has no cars yet.
I want to fetch records as per below output where I want to display all users who have at least one serviced car.
The query I have (I do not understand what clause to use for my query to get the expected output):
SELECT u.user_name,
CASE WHEN c.is_serviced = true THEN 'YES' ELSE 'NO' END AS has_serviced_car
FROM "user" u
LEFT JOIN car c ON c.user_id = u.id;
The output of the above query:
user_name | has_serviced_car
-----------------------
ABC | No
ABC | Yes
PQR | No
PQR | No
XYZ | No
Expected output:
user_name | has_serviced_car
-----------------------
ABC | Yes
PQR | No
XYZ | No
Please note that user XYZ has no cars still I need it to be displayed.
Got it!
SELECT u.name AS user_name,
CASE
WHEN u.id IN (SELECT u.id
FROM "user" u
LEFT JOIN car c ON u.id = c.user_id
WHERE c.is_serviced = true) THEN 'Yes'
ELSE 'No' END AS has_serviced_car
FROM "user" u
LEFT JOIN car c ON u.id = c.user_id
GROUP BY user_name, u.id
ORDER BY u.id;

Rails query through 2 different associated models

I'm having a little trouble trying to get a query to work the way I want it, I'm not getting all the results I'm hoping for.
I have 3 models Post, Comment and Tag. Both the posts and the comments can contain tags, and both have a has_and_belongs_to_many relationship with tags. I want to be able to get all the posts that either have a specified tag or have comments with that tag, I've been doing it in the following scope on posts like so:
scope :tag, -> (tag_id) { joins(:tags, :comment_tags).where("tags_posts.tag_id = :tag_id OR comments_tags.tag_id = :tag_id", tag_id: tag_id) }
But that doesn't return all the posts, just a subset of them, seems like its only the ones regarding the comments, this is the query it generates:
SELECT COUNT(*) FROM "posts"
INNER JOIN "tags_posts" ON "tags_posts"."post_id" = "posts"."id"
INNER JOIN "tags" ON "tags"."id" = "tags_posts"."tag_id"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
INNER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id"
INNER JOIN "tags" "comment_tags_posts" ON "comment_tags_posts"."id" = "comments_tags"."tag_id"
WHERE (tags_posts.tag_id = 1 OR comments_tags.tag_id = 1)
These are the models:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
has_many :comment_tags, through: :comments, source: :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
has_and_belongs_to_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
has_and_belongs_to_many :tags
end
I'm not certain whether you've already figured this out, but in case you haven't, here is a possible solution:
In plain SQL, mainly for illustration purposes:
SELECT
DISTINCT posts.*
FROM
posts
INNER JOIN
tags_posts ON tags_posts.post_id = posts.id
LEFT JOIN
comments ON comments.post_id = posts.id
LEFT JOIN
comments_tags ON comments_tags.comment_id = comments.id
INNER JOIN
tags ON (tags.id = tags_posts.tag_id OR tags.id = comments_tags.tag_id)
WHERE tags.id = 1
The primary issue in your original version was that you were making an INNER JOIN with comments and comments_tags. As a result you were probably cutting out every Post which did not have any comments. So the solution is to LEFT JOIN everything related to the comments. And then, because we are left joining, we can INNER JOIN tags on either the tag posts or comment posts.
Converting to Active Record is not very pretty, but necessary:
Post.joins("INNER JOIN posts_tags ON posts_tags.post_id = posts.id")
.joins("LEFT JOIN comments ON comments.post_id = posts.id")
.joins("LEFT JOIN comments_tags ON comments_tags.comment_id = comments.id")
.joins("INNER JOIN tags ON (posts_tags.tag_id = tags.id OR comments_tags.tag_id = tags.id)")
.where(tags: {id: 1})
.uniq
Note the necessity of DISTINCT and uniq, as you will get duplicates because of the LEFT JOIN.
Edit
In case there's some misunderstanding of the dataset or structure, this is an example of the data I used in my test to create the above query.
posts
+----+--------------------------+
| id | text |
+----+--------------------------+
| 1 | Post about programming 1 |
| 2 | Post about programming 2 |
| 3 | Post about programming 3 |
| 4 | Post about cooking 1 |
| 5 | Post about cooking 2 |
+----+--------------------------+
tags
+----+-------------+
| id | name |
+----+-------------+
| 1 | programming |
| 2 | cooking |
| 3 | woodworking |
+----+-------------+
tags_posts
+--------+---------+
| tag_id | post_id |
+--------+---------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 4 |
| 2 | 5 |
+--------+---------+
comments
+----+----------------------------------------------+---------+
| id | comment_text | post_id |
+----+----------------------------------------------+---------+
| 1 | comment - programming on programming post 1a | 1 |
| 2 | comment - programming on programming post 1b | 1 |
| 3 | comment - programming on programming post 2a | 2 |
| 4 | comment - cooking on programming post 3a | 3 |
| 5 | comment - programming on cooking post 4a | 4 |
| 6 | comment - cooking on cooking post 4b | 4 |
| 7 | comment - cooking on cooking post 5a | 5 |
+----+----------------------------------------------+---------+
comments_tags
+--------+------------+
| tag_id | comment_id |
+--------+------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 5 |
| 2 | 4 |
| 2 | 6 |
| 2 | 7 |
+--------+------------+
If I want to search for "programming", the above query will yield this result set:
+----+--------------------------+
| id | text |
+----+--------------------------+
| 1 | Post about programming 1 |
| 2 | Post about programming 2 |
| 4 | Post about cooking 1 |
| 3 | Post about programming 3 |
+----+--------------------------+
since we have 3 posts specifically tagged with "programming", and one comment tagged as "programming" on a differently tagged post.
I am not sure to understand what's a yum, is it a post ?
From your SQL query it seems it will count only the yum that have both a specific tag AND comment with this specific tag. What you want is to count yum that have a specific tag OR comments with this specific tag.
I would do either 2 queries one to count the yum with specific tag + one to count the yum with specific commented tags and add them both to get the total or make one query with an UNION condition.
scope :yums_tagged, -> (tag_id) { joins(:tags).where("tags_yums.tag_id = :tag_id", tag_id: tag_id) }
scope :comments_taged, -> (tag_id) { joins(:comment_tags).where("comments_tags.tag_id = :tag_id", tag_id: tag_id) }

Display latest messages from messages table, group by user

I'm trying to create an inbox for messaging between users.
Here are the following tables:
Messsages
Id | Message_from | message_to | message
1 | 2 | 1 | Hi
2 | 2 | 1 | How are you
3 | 1 | 3 | Hola
4 | 4 | 1 | Whats up
5 | 1 | 4 | Just Chilling
6 | 5 | 1 | Bonjour
Users
Id | Name
1 | Paul
2 | John
3 | Tim
4 | Rob
5 | Sarah
6 | Jeff
I'd like to display an inbox showing the list of users that the person has communicated and the last_message from either users
Paul's Inbox:
Name | user_id | last_message
Sarah| 5 | bonjour
Rob | 4 | Just Chilling
Tim | 3 | Hola
John | 2 | How are you
How do I do this with Active Records?
This should be rather efficient:
SELECT u.name, sub.*
FROM (
SELECT DISTINCT ON (1)
m.message_from AS user_id
, m.message AS last_message
FROM users u
JOIN messages m ON m.message_to = u.id
WHERE u.name = 'Paul' -- must be unique
ORDER BY 1, m.id DESC
) sub
JOIN users u ON sub.user_id = u.id;
Compute all users with the latest message in the subquery sub using DISTINCT ON. Then join to
table users a second time to resolve the name.
Details for DISTINCT ON:
Select first row in each GROUP BY group?
Aside: Using "id" and "name" as column names is not a very helpful naming convention.
How about this:
#received_messages = current_user.messages_to.order(created_at: :desc).uniq
If you want to include messages from the user as well, you might have to do a union query, or two queries, then merge and join them. I'm just guessing with some pseudocode, here, but this should set you on your way.
received_messages = current_user.messages_to
sent_messages = current_user.messages_from
(received_messages + sent_messages).sort_by { |message| message[:created_at] }.reverse
This type of logic is belongs to a model, not the controller, so perhaps you can add this to the message model.
scope :ids_of_latest_per_user, -> { pluck('MAX(id)').group(:user_id) }
scope :latest_per_user, -> { where(:id => Message.latest_by_user) }
Message.latest_per_user

Mutiple selection select boxes with Ruby on Rails

I'm using 4 tables to fill a chart, similar to these:
#colors #types #fuels
----------------- --------------------- -------------------
| ID | Color | | ID | Type | | ID | Fuel |
----------------- --------------------- -------------------
| 1 | 'Red' | | 1 | 'SUV' | | 1 | 'Gasoline' |
| 2 | 'Green' | | 2 | 'Truck' | | 2 | 'Diesel' |
| 3 | 'Blue' | | 3 | 'Sports Car' | | 3 | 'Electric' |
| 4 | 'Yellow' | | 4 | 'Compact' | | 4 | 'Hybrid' |
----------------- --------------------- -------------------
The last table is an orders table, similar to this:
#orders
--------------------------------------------------------
| order_id | color_id | type_id | fueld_id | purchases |
--------------------------------------------------------
| 1 | 2 | 1 | 4 | 2 |
| 2 | 1 | 4 | 1 | 4 |
| 3 | 2 | 2 | 2 | 6 |
| 4 | 4 | 1 | 4 | 2 |
| 5 | 1 | 4 | 2 | 1 |
| 6 | 3 | 3 | 3 | 1 |
| 7 | 3 | 3 | 3 | 2 |
| 8 | 2 | 1 | 1 | 2 |
--------------------------------------------------------
I have a controller that polls data from them all to make the chart. So far so good. Now, I want to let the user pick one or more attributes from each table, to make a dynamic orders page.
My approach is to show 3 select boxes (listboxes) that could allow the user to make a selection, and based on this, the #orders table would be modified. The modifications would land on different action, say:
def newtable
...
end
I know how to do this via SQL, but I'm not too sure how to properly show these listboxes using RoR. My idea is to pick one, several, or ALL the elements of each table.
Would form_for do the trick? I was trying to use it, but I don't have a model to base the query on, and I'm not sure how to create one (or if that approach is actually viable).
Thanks.
Well I'm Not Familiar with Rails 4 yet,but i can provide an answer as per Rails 3.
First Make Necessary Associations.
And In your Orders controller define one new method like
def create
----your stuff---
end
In your Order Form page:
<%= form_tag :action => 'create' %>
<%= collection_select :order,:color_id, #colors,'id','Color',{:label=> 'Select a Color',:prompt=> 'Select'} %>
<%= collection_select :order,:type_id, #types,'id','Type',{:label=> 'Select a Type',:prompt=> 'Select'} %>
<%= collection_select :order,:fuel_id, #fuels,'id','Fuel',{:label=> 'Select a Fuel',:prompt=> 'Select'} %>
<%= submit_tag "Create" %>
Hope this works.
Note: This is just a sample code for your understanding.
<%= f.select(:color, #color.collect {|p| [ p.name, p.id ] },
{ :prompt => "Please select"},
{ :multiple => true, :size => 4 }) %>
To configure your controller to accept multiple parameters: Click Me
You can create a separate model as a ruby class to handle this if you wish, but only if you find the other models getting too cramped. Hope that helps

Select minimum value from a group in activerecord

I need to fetch like the winner bids, and a bid can be for a different date (don't ask why), so I need to select the bid with minimum bid price for each day.
Here are my models
Leilao
has_many :bids
Bid
belongs_to :leilao
#winner_name
#price
#date
I tried a solution already and got close to what I need. The problem is that, in some cases, when I create a new bid with lower price, I don't know why the results do not change.
Leilao.find(1).bids.having('MIN("bids"."price")').group('date').all
This seems to work, but as I said, it does not work in some cases when I create a new bid. But it worked properly once. So, if you do know what might be happening, please tell me.
I then searched for some way for doing this and I got the following
Leilao.find(1).bids.minimum(:price, :group => :date)
which works properly, but with this, I just fetch the dates and prices and I need all the bid data.
I could get it by doing this, but it feels really bad to me
winner_bids = Leilao.find(1).bids.minimum(:price, :group => :date)
winners_data = []
winner_bids.each do |date, price|
winners_data << Leilao.find(1).bids.where(price: price, date: date).first
end
winners_data
Any idea a better way to do this? Or what's wrong with my first approach?
Performance is not an issue, since this is just for academic propose but it just feels nasty for me
Also those Leilao.find(1) is just for explaining it here, I'm not using it allover the place, no.
Thanks in advance
see this
mysql> select * from bids;
+----+-------------+-------+------------+-----------+
| id | winner_name | price | date | leilao_id |
+----+-------------+-------+------------+-----------+
| 1 | A | 1.1 | 2012-06-01 | 1 |
| 2 | A | 2.2 | 2012-06-01 | 1 |
| 3 | A | 3.3 | 2012-05-31 | 1 |
| 4 | A | 4.4 | 2012-05-31 | 1 |
+----+-------------+-------+------------+-----------+
4 rows in set (0.00 sec)
mysql> select * from bids where leilao_id = 1 group by date order by price asc;
+----+-------------+-------+------------+-----------+
| id | winner_name | price | date | leilao_id |
+----+-------------+-------+------------+-----------+
| 1 | A | 1.1 | 2012-06-01 | 1 |
| 3 | A | 3.3 | 2012-05-31 | 1 |
+----+-------------+-------+------------+-----------+
2 rows in set (0.00 sec)
in rails
1.9.2-p290 :013 > Leilao.find(1).bids.order(:price).group(:date).all
[
[0] #<Bid:0x00000003553838> {
:id => 1,
:winner_name => "A",
:price => 1.1,
:date => Fri, 01 Jun 2012,
:leilao_id => 1
},
[1] #<Bid:0x00000003553518> {
:id => 3,
:winner_name => "A",
:price => 3.3,
:date => Thu, 31 May 2012,
:leilao_id => 1
}
]
As Amol said its not going to work, the SQL looks like
SELECT "whatever".* FROM "whatever" GROUP BY date ORDER BY price
But the group_by will be applied before order_by
I had this trouble when i had market table where is stored different types of records, i solved it by writing a class method, maybe not the best approach but working
def self.zobraz_trh(user)
markets = []
area = self.group(:area).all.map &:area
area.each do |market|
markets << self.where(["area = ? and user_id != ?", market,user]).order(:price).first
end
markets
end
where self is Market class

Resources