Disclaimer: I'm a complete Rails n00b.
I need to present my forum users with a list of posts, where some might be 'unread'. By unread I mean that the post has a newer :updated_at timestamp than the user's last view timestamp of that post. I can't figure out the best approach for this - unread posts would obviously be unique to every user.
I tried using the 'Unread' gem but the documentation is above my comprehension, I can't get it to work (and I'm not even sure it does what I want it to do).
What's the leanest most Rails-y way to do this?
I'm on Rails 4.1.6
I currently have a user model and a post model:
class User
has_many :posts
end
class Post
belongs_to :user
end
See below for edits.
EDIT 1:
I tried following the example below of adding a Looks model, and I think I'm much closer to a solution (although not all the way there). Here's what I did:
1) rails g model Look post:references user:references + rake db:migrate. This obviously generated the model needed:
class Look
belongs_to :post
belongs_to :user
end
2) Edited my User and Post models:
class User
has_many :posts, through: :looks
has_many :looks
end
class Post
belongs_to :user
has_many :looks
has_many :users, through: :looks
end
3) Went into rails console:
user1 = User.first
post1 = Post.first
post2 = Post.last
look = Look.create(user: user1, post: post1)
look = Look.create(user: user1, post: post2)
4) I now tried to spit out the results:
seen = user1.posts
seen.map(&:title)
This works fine, it gives me the result of user1 having seen those two posts.
5) Then I tried just spitting out the IDs of the seen posts:
ids = Look.where(user: user1).pluck(:post_id)
This also works fine, I get a map of seen post ids => [2, 30]
6) I then managed to get around duplicate IDs by putting a .uniqat the end of User.first.posts.map(&:id).uniq
7) This is where I get stuck!:
Applying has_many :posts, through: :looks ruins the current relationship between a user and a post (the user_id is not included when creating a post). Here is my PostsController:
def create
#post = current_user.posts.build(post_params)
if #post.save
redirect_to #post
else
render 'new'
end
end
private
def post_params
params.require(:post).permit(:title, :content, :sticky, :ama_post, :post_url)
end
This is the last hurdle. I just need to make sure the user_id is included when creating a post.
Presuming you have a typical setup with many users and many posts, and you want to keep track whether any user has looked at any post, then you need to associate the users and posts by using a third model that joins a specific user to a specific post.
Start with the Rails guide: http://guides.rubyonrails.org/association_basics.html
Example models:
class User < ActiveRecord::Base
has_many :looks
has_many :posts, through: looks
end
class Post < ActiveRecord::Base
has_many :looks
has_many :users, through: looks
end
class Look < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Let's set up some sample data using the rails console:
alice = User.create(name: "Alice")
post1 = Post.create(name: "Post 1")
post2 = Post.create(name: "Post 2")
post3 = Post.create(name: "Post 3")
post4 = Post.create(name: "Post 4")
look = Look.create(user: alice, post: post1)
look = Look.create(user: alice, post: post2)
To find the posts that a user has seen:
seen = alice.posts
seen.map(&:name)
=> ["Post 1", "Post 2"]
To find the posts that a user has not seen:
ids = seen.map(&:id)
unseen = Post.where("id not in (?)", ids)
unseen.map(&:name)
=> ["Post 3", "Post 4"]
This is a simple way to get the records you want.
After you get this working, there are better ways to get the records by using query optimizations.
For example, you can retrieve much less data by getting just the id numbers. Here are a few ways.
# Basic: traverses two joins, then loads all columns
ids = Post.joins(:looks => :user).where("users.id = ?", alice.id).map(&:id)
# Faster: traverses two joins, then loads one column
ids = Post.joins(:looks => :user).where("users.id = ?", alice.id).pluck(:id)
# Fastest: no joins, and only loads one column
ids = Look.where(user: alice).pluck(:post_id)
Take a look at the unread gem for guidance and example code.
You'll probably need some sort of historical log. It can either log individual instances, or merely log the latest visit to the forum.
Here's a hypothetical scenario:
Your forum has only one forum group.
You have a User model for each user.
You have a Topic/Thread model that contains the individual topic, with a has_many relationship to:
The Post model which contains a specific post authored by a specific user (not relevant to answer but part of scenario)
Your forum has a Topic controller with the standard RESTful resource routes assigned.
What you want to do is separate/highlight/designate the unread topics from the read topics, right? Breaking that down, you need to:
Figure out when the user last visited the forum's index page.
Get a list of topics.
When rendering each topic in the list, determine whether the topic is more "recent" than the user's last visit. Regardless of whether it was created after the last visit, or had a post after the last visit.
Render it differently (or whatever) as per your requirements.
Turning that into a very simple implementation, you would:
Add a "last index visit" attribute to the User.
Load that value when visiting the index.
Render the topics accordingly.
Update the last index visit attribute on the current user (best implemented as an after_action)
Now this implementation assumes that you only want to keep track of it in one place and are OK with the limitations associated with that choice (reading an individual topic won't mark it as read).
If you wanted to, you could eliminate that limitation by creating a table that belongs to both the user and the topic, and then updating that table whenever the user 'reads' the topic (depending on whether you want them to view the topic or just see it in the index).
EDIT
To ultimately answer this question, you need to break it down into more details. "Identify unread topics" means you need to answer:
When do I consider a topic read? Is it when I view the individual posts since it is 'new'? Is it when I see the topic in a list after it is 'new'?
What do I consider a new/updated topic? Is it when it's first posted? what happens if someone posts a reply?
EDIT 2
Then I'd do the following:
Add a touch: true statement to the Post model's association to the Topic to ensure the updated_at attribute on the topic is modified whenever another post is made/edited.
Create a view helper method to determine whether the current user's last reading (if any) is later than the Topic's updated_at timestamp.
Create an after_action only on Posts#show that creates/updates the Reading for that user & topic.
Related
I have two ActiveRecords Users and Posts:
class User < ActiveRecord::Base
has_many :post
end
class Post < MyBase
belongs_to :user
end
I'm trying to get list of all users and all their posts.
However when running the following code, I get the users who has posts with id 1 or 2, but only these posts. I want to get these users with all their posts.
Any ideas how to do that?
users = User.sorted.references([:posts]).includes([:posts]).where('posts.id=1' OR posts.id=2).all
This is one way to do it, first get the user ids of posts with id 1 or 2, and then get all those users. There are other ways of course, but this at least should work for you.
user_ids = Post.where('id = 1 OR id = 2').pluck(:user_id)
users = User.where(id: user_ids)
I have two models: User and Message that are connected by a has_many relationship. I want to get a list of users sorted by the timestamp on their last message.
class User < ActiveRecord::Base
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :user
end
When I do this:
#users = User.includes(:messages).order('messages.created_at DESC').limit(5)
It seems to order the messages, grab the newest 5 messages, and then return the users associated with those. So the number of users can be less than 5. I want to make sure I get the 5 users.
I want the query to get the newest message for each request, order the last messages, and then return the users with the newest messages. So I want something like:
#users = User.includes(:messages).order( <messages.last.created_at DESC> )
Ideally, the solution would do the sorting on the database, because I want to use paginate. If it's relevant I'm using Postgres.
I would probably be preferential to the solution mentioned by phoet of adding an attribute to User such as last_message_posted_at and using touch: true to update that on message creation. That simplifies the query that has to be performed when you need to pull your list of users. This also allows a much more readable (IMO) chain for your application:
#users = User.all.order(:last_message_posted_at)
=> "SELECT \"users\".* FROM \"users\" ORDER BY \"users\".\"last_message_posted_at\" ASC"
This also allows you to add a nice and simple scope to your User model
scope: :by_recent_message, ->{ order(:last_message_posted_at) }
User.by_recent_message.limit(5)
It also depends when and how often this #users scope is being used. Adding a few ms to the message post time is preferable, to me, than a complicated SQL query each time the list of users is pulled.
-- Edit for those who aren't familiar with the touch syntax --
Documentation for touch: http://apidock.com/rails/v4.2.1/ActiveRecord/Persistence/touch
class User < ActiveRecord::Base
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :user, touch: true
end
And then make my query (to the user with most recent last message):
#user = User.includes(:messages).order(updated_at: :desc )
You can try something along the lines of
Message.group(:user_id).joins(:users).order('max(messages.created_at) desc')
you can use left join instead of includes
#users = User.joins("LEFT JOIN messages on messages.user_id = users.id").order('messages.created_at').limit(5)
I'm learning Rails and I'm trying to connect the dots between Ruby and what's going on when creating associations. For example:
class Post < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts
end
I've read an explanation online that relates the use of belongs_to and has_many here to attr_accessor in Ruby. This is a tad confusing ... how is that so? I understand this sets up the 1:M association between Post and User, specifically Post has a foreign key containing a user id. In the rails console, I can do something like:
user = User.first
user.posts
user2 = User.create(username: 'some guy').save
post2 = Post.new(title: 'stuff', body: 'some more stuff')
user2.posts << post2
So are these kind of like 'getter' and 'setter' methods where an object of each class corresponds to a specific row in the database and I can use these methods because of their association/relationship?
To answer your exact question, the answer is "kinda yes, kinda no".
What rails does internally to set up the association is pretty complicated. yes, getter/setter methods are involved, yes, pointing at table rows are involved... but this isn't exactly what Active Record does, and it isn't only what Active Record does.
If you really want to know what Active Record does inside: you can just go and look at the source code on github.
If you have a more specific question... I recommend you update your question to ask that :)
My app has simple user/post/vote structure, user can up-vote/down-vote on each post. I want to let user to see "their own unvoted posts" at the index page, I tried where where.not but can't combine it to work.
I tried to do this in the posts_controller and my best try is to find all unvoted posts but don't know how to get related to users.
def index
if logged_in?
#posts = Post.where.not(id: Vote.all.map(&:voteable_id))
else
#posts = Post.all.sort_by{|x| x.total_votes}.reverse
end
end
The vote is polymorphic association, below are the 3 models:
Post: has_many :votes, as: :voteable,
Vote: belongs_to :voteable, polymorphic: true,
User: has_many :votes
I have logged_in? to judge if #current_user exists.
Here is my repo: https://github.com/Tim-Feng/ad-judge
I am not sure what you mean by "their own unvoted posts" since the example code you have is kind of confusing.
If you need to find the current user's posts which have no votes you should do the following:
current_user.posts.where.not(id: Vote.pluck(:voteable_id).uniq)
If on the other hand you need to find the posts that have not been voted by the current user, you should be using only the current user's votes like so:
Post.where.not(id: current_user.votes.pluck(:voteable_id).uniq)
Hope it helps
TIP: if you need to make your code more efficient you can make it run all in one query by using left joins instead of searching for the ids to except.
I am new to Rails so go easy. I have created a blog and also created the ability for users to indicate they "like" a particular post. The way I am implementing this is by using the post table and a separate table 'vote'. When the user clicks the "like" button it sends the record to the 'vote' table with a value of '1' and the particular post id.
I would like to display the "most liked" posts in the sidebar. How do I go about calling such a thing. I would like to display the post_title and the number of 'votes', that is, I would like to somehow query the 'vote' table for the post_id's that have the most records and display them in descending order.
I hope this is an easy question.
There are several ways to accomplish this, but probably the most versatile and Rails-ish would be to create a module with a method to do the ranking, and then have any classes or associations that can be "liked" extend that module.
# lib/likable.rb
#
module Likable
def most_liked (limit = 10)
# This may be possible without a find_by_sql... see the API docs.
find_by_sql("SELECT Posts.*, SUM(votes.count) AS num_votes FROM Posts, Votes WHERE Posts.id = post_id GROUP BY post_id ORDER BY num_votes DESC LIMIT #{limit}")
end
end
# app/models/post.rb
#
require 'likable'
class Post < ActiveRecord::Base
extend Likable
# ...whatever else you've got in here
end
# app/models/user.rb (or any other model with things that can be "liked")
#
require 'likable'
class User < ActiveRecord::Base
has_many :posts, :extend => Likable
# ...the rest of the User class
end
This lets you do things like...
Post.most_liked # => an array of the 10 most liked posts
#some_user.posts.most_liked(5) # => that user's 5 most liked posts
If you needed to later, you could add methods to the module to see, eg, how many votes a particular Post has. You could also change the post_id to a target_id in Vote and make it a polymorphic association, and then you could use your Likable module to vote for anything, not just posts (you would need to generalize the call to find in most_liked if you did that).
This is actually best done by adding a counter cache to the post model, avoiding the database count on every load.
This railscast episode explains how to setup the counter cache.
Assuming you named your counter cache votes_count, you can do this to get the 10 most popular posts from your controller.
#popular_posts = Post.find(:all, :limit => 10, :order => "votes_count DESC")