I am trying to develop ratings for my application, where a User is able to set a specific rating for a comment. I have followed the following tutorial in order to do so.
Here are my associations:
class Rating < ActiveRecord::Base
belongs_to :comment
belongs_to :user
end
class Comment < ActiveRecord::Base
has_many :ratings
belongs_to :user
end
class User < ActiveRecord::Base
has_many :ratings
has_many :comments
end
My problem here is that, in the index action of my comments controller, I need to include the rating that the user has done for that comment. In the tutorial is just shown how to select a particular rating by doing this:
#rating = Rating.where(comment_id: #comment.id, user_id: #current_user.id).first
unless #rating
#rating = Rating.create(comment_id: #comment.id, user_id: #current_user.id, score: 0)
end
However, I will have several ratings, because in my controller I have:
def index
#comments = #page.comments #Here each comment should have the associated rating for the current_user, or a newly created rating if it does not exist.
end
You want to find the comment's rating where the rating's user_id matches the current user.
<%= comment.ratings.where(user_id: current_user.id).first %>
However this sort of logic is pretty cumbersome in the views, a better strategy would be to define a scope in Rating that returns all ratings made by a specific user.
class Rating
scope :by_user, lambda { |user| where(user_id: user.id) }
end
class Comment
# this will return either the rating created by the given user, or nil
def rating_by_user(user)
ratings.by_user(user).first
end
end
Now in your view, you have access to the rating for the comment created by the current user:
<% #comments.each do |comment| %>
<%= comment.rating_by_user(current_user) %>
<% end %>
If you want to eager load all ratings in your index page, you can do the following:
def index
#comments = page.comments.includes(:ratings)
end
You can then find the correct rating with the following:
<% #comments.each do |comment| %>
<%= comment.ratings.find { |r| r.user_id == current_user.id } %>
<% end %>
This would return the correct rating without generating any extra SQL queries, at the expense of loading every associated rating for each comment.
I'm not aware of a way in ActiveRecord to eager load a subset of a has_many relationship. See this related StackOverflow question, as well as this blog post that contains more information about eager loading.
Related
I'm building e-commerce application in which you can set user specific prices for products. If price for specific product for specific user is not set, it will show default product price.
It works fine, but I'm looking for more efficient solution since I'm not happy with current one.
Tables:
users
products (all product info + regular_price)
prices (user_id, product_id, user_price)
Models:
class User < ApplicationRecord
has_many :prices
end
class Product < ApplicationRecord
has_many :prices
validates :name, presence: true
def self.with_user_prices(current_user)
Product.joins(
Product.sanitize_sql_array(['LEFT OUTER JOIN prices ON prices.user_id = ?
AND products.id = prices.product_id', current_user])
).select('products.*, prices.user_price')
end
end
class Price < ApplicationRecord
belongs_to :product
belongs_to :user
end
How I get all products with user specific prices in controller:
#products = Product.with_user_prices(current_user)
How I display them in view:
<% #products.each do |product| %>
<%= product.user_price ? product.user_price : product.regular_price %>
<% end %>
As you see I'm currently joining prices table and then in view I display user_price (prices table) if it exists, otherwise regular_price (products table).
I would love to solve everything in a single query keeping only one price column with appropriate value according to the current_user
You can make use of SQL COALESCE function:
class Product < ApplicationRecord
# ...
def self.with_user_prices(user)
includes(:prices).where(prices: { user_id: user.id }).select(
'products.*, COALESCE(prices.user_price, products.regular_price) as price'
)
end
end
Then you can use it simply by:
<%= product.price %>
Note that I simplified Product.with_user_prices method a little bit by using includes, which is gonna generate SQL LEFT JOIN query since there's condition on prices.
New Answer:
please do not mark this answer as correct because I just basically extended Marek's code and yours into mine, as after several attempts I came to what you've already done anyway, but I'm just putting it here in case it helps anyone:
app/models/product.rb
class Product < ApplicationRecord
def self.with_user_prices(user)
joins(
sanitize_sql_array([
"LEFT OUTER JOIN prices on prices.product_id = products.id AND prices.user_id = ?", user.id
])
).select(
'products.*',
'COALESCE(prices.user_price, products.regular_price) AS price_for_user'
)
end
end
controller:
#products = Product.with_user_prices(current_user)
view:
<% #products.each do |product| %>
<%= product.price_for_user %>
<% end %>
Old Answer (Inefficient Code):
Untested but can you try the following? (Not sure if this is more or less efficient than your approach)
app/models/product.rb
class Product < ApplicationRecord
has_many :prices
def price_for_user(user)
prices.includes(:user).where(
users: { id: user.id }
).first&.user_price || regular_price
end
end
controller:
# will perform LEFT OUTER JOIN (to eager load both `prices` and `prices -> user`) preventing N+1 queries
#products = Product.eager_load(prices: :user)
view:
<% #products.each do |product| %>
<%= product.price_for_user(current_user) %>
<% end %>
I am fairly new to RoR and trying to get a basic app to work - I have a 'books' model and a 'genre' model. I wish to create a page that randomly generates books of different genre's for a user to select.
I have created a 'random_book' controller, but am unsure on how to proceed with the random selection and display.
Any help/pointers would be appreciated.
Edit:
Here's the work I've been doing in the random_book model:
" load 'user.rb'
class random_book < ActiveRecord::Base
belongs_to :book
belongs_to :genre
def get_random_book
find(:all).sample(5)
end
"
Thank you.
Based on discussion
4 models
Book
Genre
UserBook
User
They will look something like this:
class User < ActiveRecord::Base
has_many :user_books
has_many :books, through: :user_books
end
class Genre < ActiveRecord::Base
has_many :books
def fetch_random_books(qty)
#you want to make sure you don't error out by requesting a sample of an empty list of books so check first. The qty argument lets you control the number of books for the search
unless self.books.empty?
self.books.limit(qty).sample
end
end
end
class Book < ActiveRecord::Base
belongs_to :genre
has_many :user_books
end
class UserBook < ActiveRecord::Base
belongs_to :book
end
I would most likely use a different route for the random book section, because it's not very url-friendly to say code-descriptive things like user_book.
There are 4 things to do
Create a new route to get a list of genre_ids that a user chooses.
Create an action that correlates to the route you created that renders a list of boos and adds those books to a users list
Create a form in a view (any view, like a sidebar in an existing books view, doesn't matter) this form will post the route and action you just made
Create a view to render the book list (the easy / DRY way is to add a few elements to the existing books index to let users know its a random generated list of books based on their genre pics)
Add the route
post "/random-books", to: "books#random_books", as: :random_books
Create the action in the books_controler
def random_books
if params[:genre_ids]
genres = Genre.where(id: params[:genre_ids])
#books = []
genres.each do |genre|
#books << genre.fetch_random_books(10)
end
else
#books = nil
end
respond_to do |format|
format.html { render action: :index }
end
end
Then create a form that makes a post request to the index action of the books_controller -- You can parse the form and update the UserBook model inside that action, and then display list of books all at the same time.
<%= form_tag(random_books_path, method: :post) do %>
<ul>
<% Genre.all.each do |genre| %>
<li>
<label class='genre-select'>
<%= check_box_tag 'genre_ids[]', genre.id -%>
<%= genre.name %>
</label>
</li>
<% end %>
</ul>
<%= submit_tag "Fetch Book List"%>
<% end %>
-- The last one I'm sure you can do, it returns a books object list so parse it however works best for you. In the controller action you can automatically add the ids for the books to the UserBook model by adding this inside the controller action:
#books.each{ |book| book.user_books.create(user: user)}
In my app, I have pins (posts), users and bookmarks. Each user can bookmark a post for later reading. This all works great.
However, I am stuck on how to show if the user has previously bookmarked the pin, on a list of all pins? For each pin in the list, I want to do a check in my view to see if they have bookmarked it.
pin.rb (model)
class Pin < ActiveRecord::Base
belongs_to :user
has_many :bookmarks, dependent: :destroy
end
user.rb (model)
class User < ActiveRecord::Base
has_many :pins, dependent: :destroy
has_many :bookmarks, through: :pins, dependent: :destroy
end
bookmark.rb (model)
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :pin
validates :pin_id, uniqueness: { scope: :user_id, :message => "has already been bookmarked" }
end
A user can bookmark a pin, which creates a single record in the bookmarks table. The bookmarks table stores the user_id and the pin_id.
I'm having a bit of a brain melt-down on how I would go about checking in the view, if a user has bookmarked a pin or not. I'd basically just like to show a yes or no flag to the user.
Any guidance or advise would be greatly appreciated.
Thanks,
Michael.
UPDATE:
I ended up modifying the anser by Lucas. In my Pin model I defined a boolean method:
def bookmarked_by?(user)
return true if bookmarks.any? {|b| b.user == user }
end
...that will return true if any bookmarks belong to the given user on this pin. Seems to work okay, but welcome any additional improvements.
Well I'd suggest that when you query Pins, include their Bookmarks that are associated with the current user. And then check if the Pin has any bookmarks.
pins = Pin.includes(:bookmarks).where("bookmarks.user_id" => current_user.id)
Now when looping these pins to view them, check whether the pin.bookmarks.length is 0 or more. And on that basis you can view yes or no.
<% pins.each do |p| %>
<%= "yes" if p.bookmarks.length > 0 %>
<% end %>
What I would do, to build slightly on #Tamer's answer is to create a method on the Pin model:
class Pin < ActiveRecord::Base
# ...
def bookmarked?
!bookmarks.empty?
end
end
Then you can simply do this in the view:
<% pins.each do |p| %>
<%= "yes" if p.bookmarked? %>
<% end %>
The main reason for this is it encapsulates the logic for understanding whether or not something is bookmarked to the thing that cares about that information, which is the pin. It also keeps logic out of the view, which is super important for clean and maintainable views.
What if, for example, you wanted to check and show pins as bookmarked only if bookmarked by the current user, you could do something like the following:
class Pin < ActiveRecord::Base
# ...
def bookmarked_by?(user)
return false unless bookmarked?
bookmarks.any? {|b| b.user == user }
end
end
I don't like the bookmarks.any? call as I think it will cause another db query, and I think there's a better way to do it, offhand, I'm unsure. I'll update once I find it.
However, in the view, you can now do:
<% pins.each do |p| %>
<%= "yes" if p.bookmarked_by? current_user %>
<% end %>
Update:
Was given an incredibly efficient way for distilling the query down into a COUNT query:
class Pin < ActiveRecord::Base
def bookmarked_by?(user)
bookmarks.for_user(user).any?
end
end
class Bookmark < ActiveRecord::Base
scope :for_user, ->(user) { where(user: user) }
end
pins = Pin.includes(:bookmarks) # ... any additional conditions
<% pins.each do |pin| %>
<%= 'yes' if pin.bookmarked_by? current_user %>
<% end %>
See the question I posted here with the brilliant answer from #pdobb: How does Rails handle a has_many when query uses includes?
Maybe this?
<%= "yes" if Bookmark.where(pin_id: pin.id, user_id: current_user.id).first %>
Probably cleaner to make a helper method, or maybe a Pin class method
class Pin
def bookmarked?(user)
!!Bookmark.where(pin_id: id, user_id: user.id).first
end
...
in the view...
<%= "yes" if pin.bookmarked?(current_user) %>
In my app, when a User makes a Comment in a Post, Notifications are generated that marks that comment as unread.
class Notification < ActiveRecord::Base
belongs_to :user
belongs_to :post
belongs_to :comment
class User < ActiveRecord::Base
has_many :notifications
class Post < ActiveRecord::Base
has_many :notifications
I’m making an index page that lists all the posts for a user and the notification count for each post for just that user.
# posts controller
#posts = Post.where(
:user_id => current_user.id
)
.includes(:notifications)
# posts view
#posts.each do |post|
<%= post.notifications.count %>
This doesn’t work because it counts notifications for all users. What’s an efficient way to do count notifications for a single user without running a separate query in each post?
Found a solution!
# posts controller
#posts = Post.where(…
#notifications = Notification.where(
:user_id => current_user.id,
:post_id => #posts.map(&:id),
:seen => false
).select(:post_id).count(group: :post_id)
# posts view
#posts.each do |post|
<%= #notifications[post.id] %>
Seems efficient enough.
You could do something like this:
#posts=Post.joins(:notifications).where('notification.user_id' => current_user.id)
Where notification.user_id is the id of the notification of current_user
I suggest creating a small class to encapsulate the logic for a collection of notifications:
class NotificationCollection
def self.for_user(user)
new(Notification.where(user_id: user.id))
end
def initialize(notifications)
#notifications = notifications
end
include Enumerable
def each(&block)
#notifications.each(&block)
end
def on_post(post)
select do |notification|
notification.post_id == post.id
end
end
end
Then, on your controller:
#user_notifications = NotificationCollection.for_user(current_user)
#posts = Post.where(user_id: current_user.id)
Finally, on your view:
#posts.each do |post|
<%= #user_notifications.on_post(post).count %>
end
That way, you only need to do a single notification query per user - not as performant as doing a COUNT() on the database, but should be enough if the notifications of a single user stay below the hundreds.
In my Rails 3.2 app, I have User, Comment, and Article models. In a view I'm trying to show all of a User's comments ordered by the article date (comments belong to articles). The problem is that in my view, when I try to retrieve the comment id, I get the article id.
***Models***
# user.rb
has_many :comments
# comment.rb
belongs_to :user
belongs_to :article
scope :by_article_date,
select("*, articles.date as article_date").
joins(:article).
order("article_date DESC")
# article.rb
has_many :comments
# users_controller
def comments
#user = current_user
#comments = #user.comments.by_article_date
end
# comments.html.erb
<% #comments.each do |comment| %>
<%= comment.id %> # shows the article id, but should be comment.id
<%= comment.article.id %> # shows article id correctly
<% end %>
Can someone please help me figure out what's going on?
Try this in your scope:
scope :by_article_date, -> { joins(:article).order("articles.date DESC") }