How to mark a post as read, so that it won't show up again for a user?
I was looking at the unread gem but it seems a bit complicated and many seem to run into difficulty with it.
Instead, I was thinking of adding an 'already_read' column (boolean) to the post model.
But then I ran into two problems:
- How to ensure that the already_read column is only updated for the current user
- How to create the correct route/method so that when user clicks 'mark as read', it updates the database
Anyone have any thoughts or ideas? Thank you!
There are multiple ways to set this up, but the most up-to-date 'Rails way' is called the many-to-many association.
Create a table that you can call 'journal', and it will contain only two columns:
- post_id
- user_id
When you create a record in this table it records the fact that a certain post was read by a certain user.
And, of course the created_at, and updated_at will be added to the journal table by default. You can also add other columns here if you wanted to record something related to the event. For example you may want to record the IP of the user, or the browser they used.
You will need this in your models:
# user model
has_many :journals # kinda clunky but it means journal entries
has_many :posts, :through => :journals
# posts model
has_many :journals
has_many :users, :through => :journals
# journal model
belonds_to: user
belongs_to :post
When you display a post for a certain user, you will check if there is a record with that post_id and user_id in the journal table. So in controller you will have something like this:
def show
@post = Post.find(params[:id])
@read = Journal.where(post_id: @post.id, user_id: current_user.id).last
end
In your views you will have to check if the @read has any records. If nil you will display a button to create a new journal entry. If not nil, it means the journal entry has been created in the past and thus you can tell the user they already read it:
<% if @read == nil %>
<% form_for @journal do |f| %>
<% f.hidden_field :post_id, value: post.id %>
<% f.hidden_field :user_id, value: current_user.id %>
<% f.submit 'Mark as Read' %>
<% end %>
<% else %>
You read this entry <%= time_ago_in_words(@read.created_at) %> ago
<% end %>
You will have to create the new and create methods for the journal of course for the form to work.
Now, you can create a page where you list all the posts read by a user by doing a query like this:
@posts_read_by_user = current_user.posts
Or, you can list all the readers of a post:
@readers_of_post = @post.users
Ivan, this is wonderful! I am going to read through all this, and will get back to you with questions! Thanks for taking the time to provide such a thorough answer. I think it will help many others too!
Hi Ivan, a few questions, kindly:
1) Why would current_user.posts
load a list of read posts? Clearly I am missing something, but it seems like current_user.posts would list all posts (read or unread) by the current user.
2) We actually want to show only the posts that have not been marked as read. To provide context, it's a message board, and we have a section called 'Hot Topics' which shows posts that have comments within the last 10 days. As soon as you comment on a hot topic post, it is removed from the hot topics list. But sometimes, you just read it, and decide not to comment. Users should still have the option to remove it, to reduce their hot topics list. So we want to put a 'mark as read' button on each hot topic. Then we will check to see if the post is 'hot' (has a new comment) and also 'unread'. Only if both criteria are satisfied, will it display. And any time a message is marked 'unread', if a new comment is added thereafter, it will again be marked as 'unread' and 'hot', and therefore, will again be displayed.
3) How do we actually set up the routes for this? Do we have to modify the routes file? Do we have to add anything to the form itself, so it knows where to 'post' to?
1) In the Journal model you're only recording the association between users and posts where the user marked it as read. You're not recording unread posts. Therefore when you query the journal model for all records match the current user, you would only be pulling the read posts.
current_user.posts does the following, but in one step in an efficient database query:
- Collects all journal entries where user_id matches the current user's id
- Takes the post_id from the collected journal entries
- Pulls the post objects based on the post_ids and puts it into an ActiveRecord object
2) OK, the many-to-many will work for you just fine. Here is some pseudocode to create the condition for the display:
# Posts controller
@recently_commented_posts = Post... # You got this covered I assume
@recently_commented_posts.each do |post|
journal_entries_for_given_post = Journal.where( user_id: current_user.id, post_id: post.id )
# if there are no journal entries (count == 0), it means the post hasn't been marked as read
if journal_entries_for_given_post.count == 0
<p><%= link_to post.title, post %></p>
end
end
3) Nothing special, just the usual resources :journals
is sufficient in routes.
In your journals_controller.rb you do need to create a method for new at least to create the new journal entries. So it would be something like this:
class JournalsController < ApplicationController
before_action :set_journal
before_action :authenticate_user!
def create
@journal = Journal.new(journal_params)
respond_to do |format|
if @journal.save
@post = Post.find(@journal.post_id)
format.html { redirect_to posts_path(@post), notice: 'Successfully marked as read.' }
end
end
end
private
def set_journal
@journal = Journal.find(params[:id])
end
def journal_params
params.require(:journal).permit(:post_id, :user_id)
end
end
In your posts_controller to the show method (or to the index method depending on where you show the button) you need to add the following to initiate the form:
@journal = Journal.new
In summary:
- You would be initiating the form from your posts controller show or index methods,
- Display the form on your posts show or index view,
- And when you submit the form the params will be submitted to the journal controller to create the record.
Let me know if this makes sense! :)
Hi Ivan!
Thanks for this incredible assistance. I think I understand enough to try to get it working. I will report back how things go, and if I get stuck.
Monroe
Hi Ivan,
Above you wrote: "You will have to create the new and create methods for the journal of course for the form to work."
But later you said that "You would be initiating the form from your posts controller show or index methods."
Do I still need to create a new and create method for the journal? In a new journal controller, or in the posts controller? I think that's the last thing that is confusing to me.
Thanks very much!
-Monroe