views:

188

answers:

2

I posted a question awhile ago asking how I could limit the rate at which a form could be submitted from a rails application. I was helped by a very patient user and their solution works great. The code was for my comments controller, and now I find myself wanting to add this functionality to another controller, my Messages controller. I immediately tried reusing the working code from the comments controller but I couldn't get it to work. Instead of asking for the working code, could someone please help me understand my working comment controller code?

class CommentsController < ApplicationController
  #...

  before_filter :post_check

  def record_post_time
    cookies[:last_post_at] = Time.now.to_i
  end
  def last_post_time
    Time.at((cookies[:last_post_at].to_i rescue 0))       
  end    
  MIN_POST_TIME = 2.minutes    
  def post_check
    return true if  (Time.now - last_post_time) > MIN_POST_TIME
    flash[:warning] = "You are trying to reply too fast."
    @message = Message.find(params[:message_id])
    redirect_to(@message)
    return false
  end

  #...

  def create
    @message = Message.find(params[:message_id])
    @comment = @message.comments.build(params[:comment])
    if @comment.save
      record_post_time
      flash[:notice] = "Replied to \"#{@message.title}\""
      redirect_to(@message)
    else
      render :action => "new"
    end
  end

  def update
    @message = Message.find(params[:message_id])
    @comment = Comment.find(params[:id])
    if @comment.update_attributes(params[:comment])
      record_post_time
      redirect_to post_comment_url(@message, @comment)
    else
      render :action => "edit"
    end  
  end
#...
end

My Messages controller is pretty much a standard rails generated controller with a few before filters and associated private methods for DRYing up the code and a redirect for non existent pages.

I'll explain how much of the code I understand. When a comment is created, a cookie is created with a last_post_time value. If they try to post another comment, the cookie is checked if the last one was made in the last two minutes. If it was a flash warning is displayed and no comment is recorded. What I don't really understand is how the post_check method works and how I can adapt it for my simpler posts controller. I thought I could reuse all the code in the message controller with the exception of the line:

@message = Message.find(params[:message_id])
# (don't need the redirect code)

in the post_check method. I really want to understand this. Can someone explain why this doesn't work? I greatly appreciate you reading my lengthy question.

Create and update actions of the Messages controller:

def create
  @message = Message.new(params[:message])
  respond_to do |format|
    if @message.save
      record_post_time
      flash[:notice] = "Created \"#{@message.title}\""
      format.html { redirect_to(messages_url) }
    else
      format.html { render :action => "new" }
    end
  end
end

def update
  respond_to do |format|
    if @post.update_attributes(params[:post])
      record_post_time
      flash[:notice] = 'Post was successfully updated.'
      format.html { redirect_to(@post) }
    else
      format.html { render :action => "edit" }
    end
  end
end
+1  A: 

I will try this for the MessageController class, please read the comments inside post_check class. Oh I also make it as private methods, usually it is a best practice to put non accessible action as private methods.

class MessagesController < ApplicationController
  #...

  before_filter :post_check

  MIN_POST_TIME = 2.minutes

  def create
    @message = Message.new(params[:message])
    respond_to do |format|
      if @message.save
        record_post_time
        flash[:notice] = "Created \"#{@message.title}\""
        format.html { redirect_to(messages_url) }
      else
        format.html { render :action => "new" }
      end
    end
  end

  def update
    respond_to do |format|
      if @post.update_attributes(params[:post])
        record_post_time
        flash[:notice] = 'Post was successfully updated.'
        format.html { redirect_to(@post) }
      else
        format.html { render :action => "edit" }
      end
    end
  end

  private

  def record_post_time
    # Use different cookie value than comments
    cookies[:mesg_last_post_at] = Time.now.to_i
  end

  def last_post_time
    # Use different cookie value than comments
    Time.at((cookies[:mesg_last_post_at].to_i rescue 0))       
  end

  def post_check
    return true if  (Time.now - last_post_time) > MIN_POST_TIME
    flash[:warning] = "You are trying to reply too fast."

    # What we want to do here is to redirect back to the page 
    # where you are before trying to create a new message or
    # update an existing message.
    #
    # Dont use this:
    # @message = Message.find(params[:id])
    # redirect_to(@message)
    #
    # Use redirect_to :back so that you will be redirected
    # to the previous page before you invoke the create or update
    # action.  Most likely you will be at the new action or edit
    # action.
    redirect_to :back

    return false
  end

end

Hopefully it helps clarify your understanding in what post_check before filter method does.

SamChandra
Thank you for your help. Your commented explanations were really helpful. I tried your code, and in firefox the error is "This problem can sometimes be caused by disabling or refusing to accept cookies." I tried it again after deleting the cookie, same result. So I think it's an infinite redirect loop. Any ideas?
by combining both your answer and Ragmaanir's, I have reached a solution. Thank you very much!
+1  A: 

Hm, post_check should only be executed for the create method. Else the new/edit/show methods will call post_check too and redirect. That causes an infinite loop.

before_filter :post_check, :only => [:create]

By the way, i think you should put code and filters like that in the model: Fat Model - Skinny Controller. So the flooding-prevention will alwas apply to comments created in other controllers etc, not just when users create them inside your CommentsController.

Ragmaanir
I think thats what was needed! Thank you!