I want to show the list of people who are reading a post.
Before attempting to implement the function, I wanted to ask for some advice. I would start by checking users who invoke the show action in the PostsController; every time a person views a post, he will be recorded onto the list of people who are reading the post. However, I am not sure how to remove the person from the list when he navigates away from reading the post. Is there a way to use cookies or sessions to notice if the user navigates away from a certain page? A little bit of help would help me get started right away!
I appreciate any advice. Thank you!
To be more accurate
in routes.rb
post "/posts/:id/being_read" => "posts#being_read", :defaults => { :format => "json"}
in controller
PostsController < ApplicationController
def being_read
current_user && #post.being_read_by current_user
head :ok
end
end
on show page
function beingRead(){
$.post("/posts/<%=#post.id%>/being_read", function(data) {
setTimeout(beingRead,10000);
});
}
in post.rb
def being_read_by user=nil
# making use of rails low level cache
readers_ids = Rails.cache.fetch("Post #{id} being read", :expires_in => 5.minutes) || []
readers_ids = Rails.cache.fetch("Post #{id} being read", :expires_in => 5.minutes) (readers_ids << user.id) if user
readers_ids
end
Usually, a system of timeout is used.
Rather than store a boolean "is reading post", we can store the date on which the user has read the post.
No, you can easily set a timeout (for example 5 minutes) to know who are reading the post. This is an approximation, but this is almost realistic.
Advantages :
you can delete the finished dates when you want (with a cron, each day, or something else), with a single query rather than one a each view.
you resolve the problem of the user who close its browser (when no next page is open)
Related
I am new to rails, I have one model named "Topic", I want to get users those are currently viewing topic index page.
How do I get those users, can anybody help me?
Thanks in advance
Here is article which describe how to track active/online users use Redis. You can use this approach to track users who are currently viewing your page.
I would do this like so:
Similar to Jack Hardcastle's suggestion, you could have a column in the users table in your db which stores the path part of a user's url, and another column called "last_seen_at"
:string last_viewed_page
:datetime last_seen_at
Then, in your ApplicationController you could have a before_filter like this:
#in ApplicationController
before_action :set_last_seen_at_and_last_viewed_page
def set_last_seen_at_and_last_viewed_page
if current_user && request.method == :get
current_user.update_attributes(:last_seen_at => Time.now, :last_viewed_page => request.path)
end
end
The reason i test that request.method == :get is that this will only include pages that they actually load in the browser, and see the address in their address bar, and not form submission urls which are usually hidden from the user.
So, now you have the data, it's easy to query. We can never know who is on a page at this exact moment, since we don't know what they are actually doing - they might have gone to the toilet or closed their computer. We can only say that "this page is the last page these people looked at, and they did so within the last 15 minutes" or whatever.
So, if we wanted to use that criteria, of the last 15 minutes, we could say
#current_path = request.path
#users_on_current_path = User.where(["last_seen_at > ? and last_viewed_page = ?", 15.minutes.ago, #current_path])
Any user can 'like' photos on my site. But if he presses the button many times (10 for example), the 10 post requests will be sent to the server.
I tried to solve with the help of sessions.
I thought this code would take likes only each 5 seconds. But it doesnt! There are my action and before_filter:
def like
photo.create_like if session[:voted].nil?
session[:voted] = Time.now.to_a.first(3).reverse.join
redirect_to root_path
end
def check_session
a = Time.now.to_a.first(3).reverse.join
b = a.to_i - session[:voted].to_i
session[:voted] = nil if b >= 5
end
You can't do this with sessions (at least not with the default session store): by default rails stores the session in a cookie.
Cookies are sent by the browser as part of the request, and the response from your server can optionally update them. If you click on your like button several times in quick succession then you'll fire off several requests, each containing cookie data representing the current state of the session. Your response updates the session, but it's too late for the requests that have already been sent - their session data has been already been sent to the server and won't include any changes made by the responses.
As others have said, a bandaid is to use Javascript to restrict multiple submission but the only robust way to deal with this is at the database level ( with a unique index on the likes table).
You forgot reverse in your like method, so the comparison doesn't work. It should be:
session[:voted] = Time.now.to_a.first(3).reverse.join
Also in like you're using session[:voted] and in check_session you're using session[:time].
Although this won't work perfectly either. It would be better to use a Unix timestamp for this and let check_session return a boolean. Something like this:
def like
photo.create_like if check_session
session[:voted] = Time.now.to_i
redirect_to root_path
end
def check_session
session[:voted].blank? || (Time.now.to_i - session[:voted]) > 5
end
disable the button with
<%= submit_tag "Login", 'data-disable-with' => "Please wait.." %>
You could add a conditional in your views to show voted or want to vote? if the user voted already
I haven't touched a scrap of code yet, but here's my thoughts on how to do this:
Create a :interactions entry in my session hash. This will contain an array of time stamps. Every time a user goes through any action, the time they did this will be appended to the :interactions entry. The array will be initialized in my sessions controller, and timestamps appended to it via a filter in my application controller:
class SessionsController < ApplicationController
def create
create_session
session[:interactions] = []
end
end
class ApplicationController < ActionController::Base
after_action :log_time
private
def log_time
session[:interactions] << Time.now.to_i
end
end
Then, create another action in my application controller, the one tasked with launching the recaptcha if the user's behaviour is suspicious. All it does is see when we have 20 entries in our session[:interactions] array, find out the time elapsed between each pair of consecutive entries, and then find the average time elapsed between these interactions. If the average time is under two minutes, the recaptcha is launched. The session[interactions] array is then reset.
class ApplicationController < ActionController::Base
after_action :log_time
before_action :launch_captcha
private
def launch_captcha
if session[:interactions].length == 20
elapsed = []
session[:interactions].each_slice(2) do |a, b|
elapsed << b - a
end
total = elapsed.inject(:+)
average = total / 20
if total < 120
# this a part I'm really not sure how to do:
# various instance variables should be populated here
redirect_to 'application/launch_captcha.html.erb'
end
session[:interactions] = []
end
end
def log_time
session[:interactions] << Time.now
end
end
Now, the fact the session[:interactions] is reset may be a bit of a weakness; all bets are off for those twenty interactions. But I want to build on the above logic, maybe add session[:captchas_sent], to the session hash (or even have captchas_sent as a column and save it to the user's record), and if the session[:captchas_sent] is x amount or y amount, warnings or temporary bans could come into effect.
What are your thoughts on the above way of monitoring user behaviour?
Here's where my knowledge of rails is starting to break down though. Once I've redirected the user to the recaptcha page, how should I proceed? I have two tables, questions and answers with a has_many belongs_to relationship between them respectively.
So a random question will come from my questions table, and then I'll have a form that pertains to an answer. It will be an ajax form, and have just one field, a text field for the answer. The action the form links to, human test, will see if the answer given is equal to one of the question's answers. But how should the question's id be passed into this action? It couldn't be a simple hidden field, because once the spammer knows the answer to one question, his script could always set the id to that one question. So the params hash or the sessions hash maybe? I need some advice here guys.
I also don't really know how the human test method should proceed once the it finds the user's answer is equal to one of the question's answers:
Let's say the user is submitting a comment. They fill in the comment, hit the submit button, the launch_captcha action kicks in, and redirects them to 'application/launch_captcha.html.erb'. What has happened to the data in the comment create form? Once they've answered the captcha correctly, how should the human_test method proceed? How could it go on to submit their comment as usual? I just don't know how to do that...I need to create an action that is called before the create action of a form...and..argh I just don't know. Help guys!
I fully understand captcha, and already implemented to my app. It's working fine so far.
Obviously, it'll be annoying if it asks you captcha code to type in every time you wanna post:(
Now I'm thinking of the way comparing current time with the time the user posed last time.
If it's Post model, and it has its created_at(Timestamp)
How can I write in my controller? I'd like to use 'before filter' checking when it does create.
and I want user to wait at least 1 min to post next post.
should it be something like this below? I'm so newbie to Rails. Please help!
def spam_check
#post = Post.find_by_user_id(current_user.id)
lasttime = #post.created_at.last
end
In your Post model :
validate :spam_check?, :on => :create
def spam_check?
#post = Post.find_all_by_user_id(current_user.id).last
last_time = #post.created_at
Time.now - last_time > 1.minute
end
You can use JS to fill some automatically fill some hidden fields in your form before send. And if someone has disabled JS then simply view the captcha.
EDIT: It is good idea in addition to #sub_stantial's post.
Good morning,
A bit of background might help. I'm creating a very simple app to be used as an attendance tracking solution - it will sit on a running computer in a gym, and people can enter their names and click a button based on what type of workout they did.
The people running the app are not overly technical - but I wanted to give them a way to change the basic text on the home page, as well as on the 'help' page. I created a Meta model which has columns for "help text" "main page text" etc., which they can update via the ActiveAdmin interface.
I want to do page caching on both the home and help pages (more so just to learn Rails caching for my own more complicated projects) and only want to expire the 'help' page if the Meta -> "help_text" attribute has changed, but expire the home page if any of the other Meta -> attributes have changed.
Is this possible?
My caching code is pretty basic at this point:
class MetaSweeper < ActionController::Caching::Sweeper
observe Meta
def after_create(meta)
expire_cache_for(meta)
puts "[CACHE] Expiring cached pages"
end
def after_update(meta)
expire_cache_for(meta)
puts "[CACHE] Expiring cached pages"
end
def after_destroy(meta)
expire_cache_for(meta)
puts "[CACHE] Expiring cached pages"
end
private
def expire_cache_for(meta)
expire_page(:controller => 'static_pages', :action => 'home')
expire_page(:controller => 'static_pages', :action => 'help')
# Expire a fragment
#expire_fragment('all_available_products')
end
end
And in the application_controller:
cache_sweeper :meta_sweeper
Thanks!
EDIT 1
From the first answer, I've tried to set a virtual attribute on the "Meta" model to try to capture if the help_content attribute has changed, so that I can determine if I should expire the /help page.
meta.rb
attr_accessor :help_changed
before_update :set_cache_expiry_checker
private
def set_cache_expiry_checker
help_changed = "you bet its changed!"
end
meta_sweeper.rb
def after_update(meta)
puts "should be doing something here about expiring...."
if meta.help_changed
puts "should expire help page"
else
puts "should NOT expire help page"
end
When I run my application, I see the output of the first puts, but not the second. meta.help_changed appears to be nil.... even though I do have the virtual attribute on the model. If I do a "puts meta.inspect" at the top of after_update, I see all of the meta attributes EXCEPT the virtual one. Is there something going on in sweepers that only pulls the database stuff?
Is there another place inside the sweeper I can set a variable to do this? I've tried doing a:
expire_help_cache = meta.help_changed
At the very top of meta_sweeper.rb, but that throws errors as there is no "meta" variable when you are outside of the after_update method.
Rails provides you with afoo_changed? method that tells you if the foo attribute has changed and a changes method that lists all the changes. By the time your sweeper is called I suspect these will have already been reset.
You could however add a before_update callback that would check whether the columns you are interested in are dirty. Set an instance variable based on that and then check the value of the instance variable in your sweeper.