Utilizing rails helper and controller methods - ruby-on-rails

Suppose I have a Rails app that deals with Posts and Comment objects. A Post has_many Comments and each Comment belongs_to a Post.
Each Comment has a word_count property. The Post object has an average_comment_word_count property which is an average of each of the Comment's word_count.
First question is if the Post object gets modified asynchronously (comments get added which affects the average word count), at what point should I recalculate the property? When the object is returned? Or each time a new comment is added? Does it go into the comment or post helper methods? Which controller function should call this method?
Also when I include the following Post helper method, I get a NULL value returned as JSON.
def average_word_count
#average_word_count = 0
# current_user returns the current user object
# user has_many posts and each post belongs_to a user
current_user.posts.find(params[:id]).comments.each do |comment|
#average_word_count += comment.word_count / current_user.posts.find(params[:id]).comments.count
end
#average_word_count
end

class Comment < ActiveRecord::Base
belongs_to :post
after_save :update_post_word_count
def update_post_word_count
average_wc = post.comments.average(:word_count)
post.update_attributes average_comment_word_count: average_wc
end
end
Or, derive it only when you need it:
class Post < ActiveRecord::Base
has_many :comments
def average_comment_word_count
comments.average :word_count
end
end
Or, if it's just used once somewhere with low traffic, brazenly flout the Law of Demeter and just calculate it as needed from a post object:
Average Comment Word Count: <%= #post.comments.average :word_count %>
Update: As #coreward notes, the first part of this answer isn't useful for asynchronous updates, but the rest of the answer may still be helpful.

You would be a lot better off just building a custom counter cache based on what's already in ActiveModel that keeps track of the total number of words, then just count comments to do math manually.
# you need a comments_count column and a words_count column in this table
class Post < ActiveRecord::Base
has_many :comments
def avg_words_per_comment
words_count / comments_count
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :counter_cache => true
after_save { update_counters(post.id, :words => word_count }
before_destroy { update_counters(post.id, :words => -word_count }
end
# And in your view:
<p>
The average comment for this post has <%= #post.avg_words_per_comment %> words.
</p>
Then you don't need to worry about asynchonicity and the calculation on view is minimal.
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/counter_cache.rb#L65

Related

Rails - Querying Noticed Notification with the params id

I have searched through the related questions for example this one and the solutions marked there doesn't work for me.
So here is my problem:
I want to get a list of notifications that are for a specific recipient, and the notifications have to be made on comments belonging to a specific plant.
Currently I am using ruby to filter but the database hit is not ideal.
Here is the state of my code.
Models:
class Plant < ApplicationRecord
has_many :comments
end
class User < ApplicationRecord
has_many :notifications, as: :recipient, dependent: :destroy
end
class Notification < ApplicationRecord
belongs_to :recipient, polymorphic: true
end
class Comment < ApplicationRecord
has_noticed_notifications
end
class CommentNotification < Noticed::Base
def comment
params[:comment]
end
end
This is the query I am currently using: #plant.comments.flat_map { |comment| comment.notifications_as_comment }.filter { |notification| notification.recipient == current_user }.map(&:mark_as_read!)
Any help is deeply appreciated...
I resolved this by making a comment method and then using that in my filter, it also killed all the excess dB hits I was getting.
def comment_id
self[:params][:comment].id
end
Then I used this query to arrive at the result, looks cleaner too. Note comment_ids = #plant.comments.ids
Notification.unread.where(recipient_id: current_user.id)
.filter { |notification| comment_ids.include?(notification.comment_id) }
.map(&:mark_as_read!)
Noticed has a built in helper method for finding notifications based on the params.
If you add has_noticed_notifications to the model which you want to search for in the params, Comment in your case.
You can then call #comment.notifications_as_comment and it will return all notifications where the #comment is params[:comment]
This is in the Noticed readme here. I definitely came here and found this question before I found the details in the readme!

Update Post attribute when new Rating is created

I have a Post model which has many ratings and I would like to store the average ratings figure for each post in the post ratings db column. I have a counter cache on the post model which counts the number of ratings.
I have the following code in my post.rb
def update_rating
if self.ratings_count_changed?
self.rating = self.ratings.average(:rating)
end
end
I had this as a before_save callback so whenever the post is edited and saved it updates the ratings, not very useful.
Is there a way I can call this method whenever a new rating is created?
One way of getting closer to your goal is to add after_create callback to Rating model:
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_attributes(:rating => self.post.ratings.average(:rating))
end
end
Or even more OO friendly version:
class Post < ActiveRecord::Base
has_many :ratings
# ...
def update_average_rating
update_attributes(:rating => self.ratings.average(:rating))
end
end
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_average_rating
end
end
Do you think about storing average rating in rating model instead post model? In that case you don't need any callback and you need recalc average by first request after changes.
Instead of before_save on post, do an after_create on the rating because it sounds like you need to update that rating score when a new rating is created, not before the post is saved.
How about putting this into Rating model:
after_create :update_post_average_rating
def update_post_average_rating
self.post.rating = #....
end

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Retrieve all posts where the given user has commented, Ruby on Rails

I have users, posts and comments. User can post only one comment to each post.
class User < ActiveRecord::Base
has_many :posts
has_many :comments
end
class Post < ActiveRecord::Base
has_many :comments
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
On userpage (http://host/users/1 for example) I want to show all posts where the given user has commented. Each post then will have all other comments.
I can do something like this in my User controller:
def show
#user = User.find(params[:user_id])
#posts = []
user.comments.each {|comment| #posts << comment.post}
end
This way I will find User, then all his comments, then corresponding post to each comment, and then (in my view) for each post I will render post.comments. I'm totally new in Rails, so I can do this =) But I think it's somehow bad and there is a better way to do this, maybe I should use scopes or named_scopes (don't know yet what this is, but looks scary).
So can you point me out to the right direction here?
You could define an association which retrieves all the posts with comments in a single query. Keeping it in the model reduces the complexity of your controllers, enables you to reuse the association and makes it easier to unit test.
class User < ActiveRecord::Base
has_many :posts_with_comments, :through => :comments, :source => :post
# ...
end
:through is an option for has_many to specify a join table through which to perform the query. We need to specify the :source as Rails wouldn't be able to infer the source from :post_with_comments.
Lastly, update your controller to use the association.
def show
#user = User.find(params[:user_id])
#posts = #user.posts_with_comments
end
To understand more about :through and :source take a look at the documentation.
When you got the user, you have the associations to his posts and each post has his comments.
You could write:
(I don't know the names of your table fields, so i named the text text)
# In Controller
#user = User.find(params[:user_id]).include([:posts, :comments])
# In View
#user.posts.each do |post|
post.text
# Comments to the Post
post.comments.each do |comment|
comment.text
end
end
I haven't tested the code, so there could be some errors.

Limiting a search to records from last_request_at

I am trying to figure out how to display a count for records that have been created in a table since the last_request_at of a user.
In my view I am counting the notes of a question with the following code:
<% unless #questions.empty? %>
<% #questions.each do |question| %>
<%= h(question.notes.count) %>
end
end
This is happening in the /views/users/show.html.erb file. Instead of counting all the notes for the question, I would only like to count the notes that have been created since the users last_request_at datetime. I don't neccessarily want to scope notes to display this 'new notes' count application wide, just simply in this one instance.
To accomplish I am assuming I need to create a variable in the User#show action and call it in the view but not really sure how to do that.
Other information you may need:
class Note < ActiveRecord::Base
belongs_to :user
belongs_to :question
end
class Question < ActiveRecord::Base
has_many :notes, :dependent => :destroy
belongs_to :user
end
Just create a named scope and then use it only when it applies:
class Note < ActiveRecord::Base
named_scope :added_since, lambda { |time| {
:conditions => time && [ 'created_at>=?', time ]
}}
end
This should only enforce conditions if a time is provided. If you submit a nil time, the default behavior is to scope all notes.
This way you can do something along the lines of:
#new_notes = #user.notes.added_since(#user.last_login_at)
Adding a named scope does not alter the default scope.

Resources