show only posts created in last week - ruby-on-rails

I want to be able to show posts and have them sorted by a couple criteria, first by the amount of votes they have on them and second by the date at which they were created. I don't want posts that are more than a week old being displayed so only posts in the last week. I tried doing this:
<%= render #posts.sort_by { |post| post.votes.count if post.created_at < 1.week.ago.utc }.reverse %>
but it gave me an error of comparison of NilClass with 2 failed
I know the code works by just sorting posts by vote count but I also want to limit time so could someone tell me how this can be done. I'm still new so sorry for the simplicity.

Solution by #Salil is ok, but I would suggest adding counter_cache column ( http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html ) and changing recent_post code (from this comment: https://stackoverflow.com/a/11498634/1392074 ) into:
def self.recent_posts
Post.where("created_at >= ?", 1.week.ago.utc).order("votes_count DESC, created_at DESC")
end

The code to find posts should be in Model and not on Views.
There is always a good idea that you should fetch the records which we need to display instead fetching the records and showing some of it on views.
You should do something like following
in your post.rb
def self.recent_posts
Post.select("p.*, COUNT(v.id) AS count").where("post.created_at >= 1.week.ago.utc").joins("p LEFT JOIN votes v on p.id=v.post_id").order("count, created_at DESC")
end

Related

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 }

How to sort through an associated attribute on two levels at once?

This is a simple ruby question I believe. In my app, I have Product model that has_many Reviews. Each Review has an attribute of an "overall" rating which is an integer.
What I want to do is display the top ten Products based on the average of their overall ratings. I've already gotten this to work, BUT, I also want to sort Products that have the SAME overall rating by a secondary aggregate attribute, which would be how MANY reviews that Product has. Right now, if I have 3 products with the same average overall rating, they seem to be displayed in random order.
So far my code is:
Controller
#best = Product.has_reviews.get_best_products(10)
Product Model
scope :has_reviews, joins{reviews.outer}.where{reviews.id != nil}
def self.get_best_products(number)
sorted = self.uniq
sorted = sorted.sort { |x, y| y.reviews.average("overall").to_f <=> x.reviews.average("overall").to_f }
sorted.first(number)
end
I've tried this for my model code:
def self.get_best_products(number)
sorted = self.uniq.sort! { |x, y| x.reviews.count.to_f <=> y.reviews.count.to_f }
sorted = sorted.sort { |x, y| y.reviews.average("overall").to_f <=> x.reviews.average("overall").to_f }
sorted.first(number)
end
...but it does not do what I want it to do. I am just iterating through the #best array using each in my view.
---UPDATE
OK now I am trying this:
Controller:
#best = Product.get_best_products(6)
Model:
def self.get_best_products(number)
self.joins{reviews}.order{'AVG(reviews.overall), COUNT(reviews)'}.limit(number)
end
But I am getting this error:
PGError: ERROR: column "products.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT "products".* FROM "products" INNER JOIN "reviews" ...
I am using the Squeel gem btw to avoid having direct SQL code in the model.
----UPDATE 2
Now I added the 'group' part to my method but I am still getting an error:
def self.get_best_products(number)
self.joins{reviews}.group('product.id').order{'AVG(reviews.overall), COUNT(reviews)'}.limit(number)
end
I get this error:
PGError: ERROR: missing FROM-clause entry for table "product"
LINE 1: ...eviews"."product_id" = "products"."id" GROUP BY product.i...
product.rb
scope :best_products, (lambda do |number|
joins(:reviews).order('AVG(reviews.overall), COUNT(reviews)').limit(number)
)
products_controller.rb
Product.best_products(10)
This makes sure everything happens in the database, so you won't get records you don't need.
If I got it right here is my idea of how I would do it:
As products has many reviews and reviews has an overall attribute I would add a reviews_counter column to the products table that will increment with each added review, this way you'll be able to gain a little more db performance as you don't have to count all the products reviews to get the most reviewed one.
Now you'll get the products ordered by reviews_counter:
#best_products = Products.order("reviews_counter desc")
and next you'll get the reviews for each product ordered by overall:
<% for prod in #best_products %>
<%= prod.reviews.order("overall desc") %> # can do all this or more in helper
<% end %>
also ordering this way, if you have 3 reviews with the same overall you can one more order() statement and sort it by name or id or whatever you like so they don't display in random order.
This is just my idea of how I would do it, I worked recently on an app that required something similar and we just added a counter_field to our model, it's not illegal to do so :)
p.s. it's not very clear for me how many records you would want to display for each so you'll just need to add .limit(5) for exemple to get only the first 5 reviews of a product.

get most discussed forums in rails

I got two classes: Forum and Topic. Forum has_many Topics.
How do you get the (lets suppose) 5 most disscused forums?
How do you limit it by Date? for example, most disscused this day/week etc.
thanks
1. How do you get the (lets suppose) 5 most disscused forums?
You will probably be able to do something like this:
Forum.where('id IN (?)', Topic.limit(5).count(:group => 'forum_id').map {|key, value| key })
2. How do you limit it by Date? for example, most disscused this day/week etc.
Forum.where('id IN (?)', Topic.where('date > ', 1.week.ago.to_s(:db)).limit(5).count(:group => 'forum_id').map {|key, value| key })
If the most discussed forum is the one with the most topics, the query would be:
Forum.joins(:topics).group("forums.id").order("COUNT(*) DESC").limit(5)
The five forums with the largest number of opened topics in the last week:
Forum.joins(:topics)
.where("topics.created_at > ?", 1.week.ago)
.group("forums.id")
.order("COUNT(*) DESC")
.limit(5)
The best way is to keep timestamps (created_at and updated_at) in both tables. And update them whenever some activity occurs on the topic.
Steps:
Keep timestamps in both the tables.
Add an after_save callback
in Topic model, to update updated_at of the parent forum.
Get the list of forums order by updated_at desc.
class Topic
def after_save
self.forum.touch
end
end
Then run a query like
Forum.order("updated_at desc").limit(5)

How to count "days with records" in Rails?

Rails adds and populates a created_at column for new records.
How can I use the to count the number of days that have records within a specified timeframe? (note: counting days, not counting records).
For example, say I have a Post model, how can I calculate how many days in the last year have a Post?
Since you asked for the ruby way, here it is:
Post.where('created_at >= ?', 1.year.ago).map { |p| p.created_at.beginning_of_day }.uniq.size
Update
You can put the following in your Post model
def self.number_of_days
where('created_at >= ?', 1.year.ago).map { |p| p.created_at.beginning_of_day }.uniq.size
end
Then in your controller you can do stuff like
#user.posts.number_of_days
Here's a more efficient way that delegates most of the work to the database (MySQL, not sure if it'll work on others):
Post.where('created_at >= ?', 1.year.ago).group('DATE(created_at)').length

Sorting by a virtual attribute in Rails 3

BACKGROUND: I have a set of Posts that can be voted on. I'd like to sort Posts according to their "vote score" which is determined by the following equation:
( (#post.votes.count) / ( (Time.now - #post.created_at) ** 1 ) )
I am currently defining the vote score as such:
def vote_score(x)
( (x.votes.count) / ( (Time.now - x.created_at) ** 1 ) )
end
And sorting them as such:
#posts = #posts.sort! { |a,b| vote_score((b) <=> vote_score((a) }
OBJECTIVE: This method takes a tremendous toll on my apps load times. Is there a better, more efficient way to accomplish this kind of sorting?
If you are using MySQL you can do the entire thing using a query:
SELECT posts.id,
(COUNT(votes.id)/(TIME_TO_SEC(NOW()) - TIME_TO_SEC(posts.created_at))) as score
FROM posts INNER JOIN votes ON votes.post_id = posts.id
GROUP BY posts.id
ORDER BY score DESC
Or:
class Post
scope :with_score, select('posts.*')
.select('(COUNT(votes.id)/(TIME_TO_SEC(NOW()) - TIME_TO_SEC(posts.created_at))) as score')
.joins(:votes)
.group('posts.id')
.order('score DESC')
end
Which would make your entire query:
#posts = Post.with_score.all
P.S: You can then modify your Post class to use the SQL version of score if it is present. You can also make the score function cached in an instance so you don't have to re-calculate it every time you ask for a post's score:
class Post
def score
#score ||= self[:score] || (votes.count/(Time.now.utc - x.created_at.utc)
end
end
P.S: The SQLLite3 equivalent is:
strftime('%s','now') - strftime('%s',posts.created_at)
You shouldn't use sort! if you are going to assign to the same variable (it is wrong in this case), you should change the sort to:
#posts.sort!{|a, b| vote_score(b) <=> vote_score(a) }
It looks like you are counting the votes for Post each time you call another Post which is hitting the database quite a bit and probably the source of the toll on your load times, you can use a counter_cache to count each time a vote is made and store that in the posts table. This will make it so you only do one db query to load from the posts table.
http://guides.rubyonrails.org/association_basics.html

Resources