Rails: Prevent duplicate inserts due to pressing back button and save again
Asked Answered
B

8

11

Think about a simple Rails scaffold application with a "new" action containing a form to add records to a database with a "save" button. After the "create" action the controller redirects to the "show" action, where the user can use the "edit" link to edit the just inserted record. So far, so simple.

But if the user instead uses the browser's back button after creating a record to get back to the "new" action, the browser shows the form with the values the user just has entered. Now he changes some values and presses "save" again. He thinks that this would change the record, but of course this creates a new record.

What is the preferred way to prevent such duplicate entries? I'm looking for a general solution, maybe based on cookies or JavaScript.

Brana answered 11/1, 2011 at 12:47 Comment(2)
What is the data that is being submitted? is there any unique identifier at all that we can grab a hold of?Aplenty
I'm thinking about a general solution, so it should work for any kind of data. And so, it should also work if there is NO value which has to be unique.Brana
B
7

After some investigations I found a suitable solution based on cookies. Here it is:

In the controller's "new" action, a timestamp with the current time is generated and rendered in the form as hidden field. When the user submits the form, this timestamps gets back to the controller's "create" action. After creating the record, this timestamp is stored in the session cookie. If the user goes back to the "new" form via browser's back button, he gets a stale form, which means its timestamp is older than the one stored in the cookie. This is checked before creating the record and results in an error message.

Here is the controller code:

def new
  @post = Post.new
  @stale_form_check_timestamp = Time.now.to_i
end

def create
  @post = Post.new(params[:post])

  if session[:last_created_at].to_i > params[:timestamp].to_i
    flash[:error] = 'This form is stale!'
    render 'new'
  else
    @post.save!
    @stale_form_check_timestamp = Time.now.to_i
    session[:last_created_at] = @stale_form_check_timestamp
  end
end

And here the form code:

- form_for @post do |f|
  = tag :input, :type => 'hidden', :name => 'timestamp', :value => @stale_form_check_timestamp
  = f.input :some_field
  = .......
Brana answered 13/1, 2011 at 17:32 Comment(1)
this solution will not permit create new Post later until the session where erase, i tried to implement a solution reloading that, but just manage to work one time(releasing the :last_created_at after the error and fill it in new with ||= Time.now.to_i). Sadly this solution have a critic fail if you came back one more time, then the variable never is released and is the same error that before :/Advocation
C
6

When I had that same problem I created this little gem that solves it. When the user hits back, he's redirected to the edit_path of the record, instead of going back to the new_path.

https://github.com/yossi-shasho/redirect_on_back

You can do something like:

def create
  @user = User.new(params[:user])
  if result = @user.save
    redirect_on_back_to edit_user_path(@user) # If user hits 'back' he'll be redirected to edit_user_path
    redirect_to @user
  end
end
Classmate answered 6/7, 2013 at 15:7 Comment(4)
it looks nice, i tried to use it with activeadmin, but i just can't get redirect_on_back_to working :(, next time i have this problem without AAdmin, i will give it another shot, thanks!Advocation
@Advocation sorry to hear, can you describe the problem or open an issue here github.com/yossi-shasho/redirect_on_back/issues please?Classmate
i'm not sure if it was an issue, in my case i tried to override the activeadmin create action and implements the function, i'm guess is just a little tricky, activeadmin use inherent resources and i am not completely familiar with those, another idea come across that time was maybe it wasn't implementing the functions there, but my knowledge don't let me try more on this idea, by the way, i use it later in normal controllers and works like a charm, regards!Advocation
awesome, I'm happy to hear :)Classmate
G
3

Your model validations will ensure things like email addresses are unique, but I think this is more about usability and experience than anything else.

Say you are talking about an account creation form. First of all, your form submit button should say something like "Create Account", instead of just "Submit". Then depending on whether it was successful or not, show a message like either "Account successfully created" or "There were errors creating your account". If the user sees this message, they will know what happened.

Sure you can't prevent someone from hitting the back button and hitting enter again, but you should design for the majority of use cases. If they happen to hit back, they will see the button that says "Create Account". You should probably have some other text on the page that says "Please sign up for a new account to get started".

Just my $0.02.

Governor answered 11/1, 2011 at 13:2 Comment(3)
Absolutely: uxmovement.com/forms/…Scopoline
Yes, a sophisticated button label can prevent the user from clicking "create account" a second time. I agree with you that it should not be a simple "Submit". But IMHO this won't do it. There are always users ignoring the button text and think they can change the existing record. I'm looking for a way to recognize such things and display a message like "You are trying to add the same record twice!" instead, or, even better, doing an update instead of an create.Brana
What if the user actually wanted to enter the same record again?Elsyelton
J
2

Session or cookie may result in sides effects.

I totally agree : if there is a way to validate with your model, it's the safest way to prevent duplicate records.

Still you can do 2 things. Prevent browser caching : fields will appear empty in the form when the user clicks on the back button. And disable the "Create" button when clicked.

= f.submit "Create", :disable_with => "Processing..."

When your user will press the back button the button will be disabled.

Jameyjami answered 24/8, 2012 at 15:6 Comment(0)
F
0

You can use validators to make sure that no duplicate values are inserted. In this case validates_uniqueness_of :field

If you for example want to prevent users from having the same email address you could put the following code in your user model.

validates_uniqueness_of :email

This checks the column for any previous entries that are the same as the one your trying to inert. Good luck

Faso answered 11/1, 2011 at 12:56 Comment(1)
You are right, but this works only if there is a field which has to be unique. I'm looking for a more general solution.Brana
A
0

base on @Georg Ledermann answer i make this little snip of code for redirect to edit path if the user hits back and then hits create.

#objects_controller.rb
def new
    @object = Object.new
    @stale_form_check = Time.now.to_i
end

def create
    @object = Object.new(object_params)
    #function defined in application_controller.rb
    redirect_to_on_back_and_create(@object)
end

#application_controller.rb
private
def redirect_to_on_back_and_create(object)
    if session[:last_stale].present? and session[:last_stale_id].present? and session[:last_stale].to_i == params[:stale_form_check].to_i 
        redirect_to edit_polymorphic_path(object.class.find(session[:last_stale_id].to_i)), alert: "Este #{object.model_name.human} ya ha sido creado, puedes editarlo a continuación"
    else 
        if object.save
            session[:last_stale] = params[:stale_form_check].to_i
            session[:last_stale_id] = object.id
            redirect_to object, notice: "#{object.model_name.human} Creado con éxito"
        else
            render :new 
        end
    end
end

And finally add the @stale_form_check param to your form

<%= hidden_field_tag :stale_form_check, @stale_form_check %>

You could always abstracts this method where you need it, but in this way you could avoid lots of repetition in your project if you need this behavior in many parts

Hope it helps the next one, i used to use redirect_on_back gem, but it didn't work for me this time, the _usec param that this gem uses, was always been reset, so it can't compare in every time when it was need

Advocation answered 13/4, 2015 at 23:35 Comment(0)
S
0

Here's something that worked for me.

You will need to do 2 things: Create a method in your controller and add a conditional statement in that same controller under your 'create' method.

1) Your method should return the total count of that object from that user.

EX:

def user current_user.object.count end

2) Add conditional statement in your 'create' method.

EXAMPLE:

def create @object = Object.create(object_params) @object.save if user == 0 redirect_to x_path end

I hope this helps!

Svetlana answered 6/10, 2015 at 3:27 Comment(0)
A
0

Add html: { autocomplete: "off" } in your form_for like this:

<%= form_for @object, url: xxx_path, html: { autocomplete: "off" } do |f| %>
Alkyl answered 27/6, 2019 at 9:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.