Rails: Prevent duplicate inserts due to pressing back button and save again - ruby-on-rails

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.

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
= .......

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

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.

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.

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

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

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!

Add html: { autocomplete: "off" } in your form_for like this:
<%= form_for #object, url: xxx_path, html: { autocomplete: "off" } do |f| %>

Related

How to add a controller variable to a database row in rails?

I'd like to add a variable in the controller to a database row using activerecord and rails.
To do this now I have to add a hidden input tag to my view which contains the username of the member who is adding the row. This tag then gets picked up by my controller and added into the database.
Here is my hidden input field:
<% f.hidden_field(:uploader, value: #current_user.username) %>
And this is the code which creates the building
def create # (post) Add new records
#bld = Building.new(params[:bld].permit(:name, :uploader, :description, :down_link))
if #bld.save
redirect_to my_buildings_url, :alert => 'Here\'s your new building!'
else
render :new
end
end
Is there any way to eliminate my hidden input tag and have the value of #current_user.username added to the database row in the controller (if you know what I mean)?
This would be done in the controller action as current user would be the person logged in. Therefore in your create action.
def create # (post) Add new records
#bld = Building.new(params[:bld].permit(:name, :description, :down_link))
#bld.uploader = current_user.username
if #bld.save
redirect_to my_buildings_url, :alert => 'Here\'s your new building!'
else
render :new
end
end
However you'd be better off defining an association called uploader which calls your user model. Then all you have to store is an integer. Also you don't open yourself to params trickery.

Rails - how to pass created record from the new form to a redirected page

I think this is a pretty simple question but nothing I've read has answered my question directly:
I have a new products page with a standard form. After successfully submitting the form, I redirect to a custom controller action and view called "thanks".
On the "thanks" page, I want to be able to print the name of the product just created and possibly some other attributes.
How do I pass the object just created into my new action? Right now the controller looks like this:
def create
#product = Product.new(params[:product])
if #product.save
flash[:notice] = "Successfully created Product."
redirect_to thanks_path
else
render :action => 'new'
end
end
def thanks
end
You can't send object through redirect.
There are three ways to solve your problem:
Render the 'thanks' template directly(not action #thanks)
render 'thanks' # thanks template
You can send whatever instance variable to this template directly. #thanks is no longer needed in this case.
Drawback: The url won't be changed.
Convey messages through session
If you want to show certain messages, you can prepare it in #create and send it through session or flash(part of session actually). flash is better as you don't need to clear it manually.
Note: You may want to use ActiveRecord as session storage if the message size is big, otherwise you'll meet CookiesOverflow by default setting.
Send very simple message through session say obj_id
Similar to #2 but I thinks this is better than #2. In #thanks, you can construct complex message according to if obj_id is present, what is the id and then find related data through db.
You have two fairly decent options.
First, you could adjust the thanks_path route to take an id parameter, and call it like redirect_to thanks_path(#product). Then you can call it up in your thank you method like any standard show method. It might be worth mentioning that if you are going to be displaying sensitive information on the thank you screen, you may want to use a random uuid, instead of an id, to look up the product.
A better way might be to not redirect at all, but rather adjust your view from simply drawing the form to something like this:
<% if #product && !#product.new_record %>
THANK YOU MESSAGE GOES HERE
<% else %>
EXISTING FORM GOES HERE
<% end %>

Confused, new action not rendered after validation failure

So i have the following code:
def new
#all_areas = Area.all
#area = Area.new
end
The reason i am passing in all_areas is it's required for a drop down box in the form, Using mongoid and an Area can be recursively embedded in another Area.
My form has the following code:
<% if #all_areas %>
<%= f.label :parent_area %>
<%= f.collection_select(:parent_area, #all_areas, :_id, :name, prompt: "Select a Parent...") %>
<% end %>
However when i submit 'invalid values, i.e blank name, the 'new' page does not render the select box to select a parent.
What is going on here? Is this a bug?
My create action is pretty simple, if it fails validation i just do the following:
else
render 'new'
Why is #all_areas not passed to the view the second time? i have actually fixed it by changing the code in my create action to the following:
else
#all_areas = Area.all
render 'new'
But this is quite surprising, unless i am missing something?
It's not a bug. The reason for the behaviour is that the create action is actually quite separate from the new action, so the instance variables you assigned in new don't get carried over. render 'new' only renders the view called "new", it doesn't actually call the new action.
new happens when you issue a GET request to /areas/new. create happens when you POST to /areas. Because they're separate requests, the server doesn't remember any state - in fact, you could call create without ever calling new (say if you used curl from the command line).
Basically, your approach is correct, you need to set the #all_areas instance variable in both actions. You might want to extract it out into a separate private method to avoid the duplication.

Rails - When calling NEW, create a record before displaying a form?

Here's what I'm trying to do.
when the user clicks new note.. I want the user to be taken to a page when they can start typing a note, and save it to the server all with AJAX..
Problem is, every time the page saves, it's making a new note.
This leads me to believe that when Rails gets the DEF NEW controller, some how I need rails to first make a NEW NOTE record and then redirect to the edit controller of that new note, where the user can create/edit the note all with AJAX.
Thoughts? Thanks.
I had the same problem once, creating the note first is probably a good idea.
Another way would be to send the user to the new action. When the first save occurs you send the new object back as a JSON object, and replace the form's action with the update url for that record as well as setting the form's method to put.
This way you don't end up with empty records in the database (with your use-case, you might want exactly that, so a User can continue a note later.)
Just my two cents.
Ok a way of implementing this could look like this:
Form
<%= form_for resource,
:remote => true,
:html => { 'id' => 'autosave' },
:url => resources_path(:format => :json) do |f| %>
...
<% end %>
Application JS
var $form = $('#autosave');
// bind to the first success event from the form
$form.one('ajax:success', function(data, status, xhr) {
// data contains our json object (your note as json)
// now we update the form
$form.attr('action', '/notes/' + data.id);
$form.attr('method', 'put');
$form.attr('data-method', 'put');
});
Controller
class ExampleController
...
def create
#
# respond with the json object if format = json,
# see the form above I'm passing the format with :url parameter in form_for
#
respond_with(resource) do |format|
format.json { render :json => resource }
end
end
end
If you really want use to use #new to create a note and save it, then you can simply do
def new
#note = Note.create # instead of Note.new
end
Rails will then display this note just like the #edit action, so the note id will be in a hidden field. Then when you send the Ajax calls, you'll be calling #edit. If you want to preserve the behavior of #new for when javascript is turned off, then you might want to create a different action.
def new
#note = Note.new
end
def new_js
#note = Note.create
end
When you load the page that has the link to new_note, include some javascript that changes the link to new_js_note. So when JS is off, you get the standard #new form. When JS is on, you get a form that is basically editing a preexisting blank note.

render/redirect to new action when validation fails (rails)

I have a form where users can enter an isbn and it will try to lookup book data and save it.
When validation fails for the isbn lookup (for example if somebody entered it incorrectly), I would like it to redirect to another form where users can enter data in manually if the isbn lookup fails (but not if other validations like numerical price fail).
Any ideas on how to do this? Thanks for the help!
Trying to understand what you're trying to do: please correct me if my assumption is wrong.
If you can't save the model because the ISBN failed validation and you want to display a form for just the ISBN since the other fields are OK, there's a couple things you can do to hold the other attributes in the meantime:
Output them as hidden fields when you render the form
Store them in session so you can redirect
If you can't save the model then there doesn't seem to be any reason for redirecting to another action: the user is still trying to complete the create action, except you want to render a different form for just the ISBN.
Here's how I'd do it using session, so you can adapt this for redirecting to another action if you need to:
def create
book = Book.new( params[:book].reverse_merge(session[:unsaved_book]) )
if book.save?
session.delete[:unsaved_book]
flash[:notice] = 'I love it!'
redirect_to book
else
if book.errors.on[:isbn] && book.errors.length == 1
session[:unsaved_book] = params[:book]
flash[:error] = 'Sorry, wrong ISBN number.'
render 'unknown_isbn'
else
flash[:error] = 'Check your inputs.'
render 'new'
end
end
end
I'd give them the option to re enter the isbn if the lookup failed, as it might just be a typo.
For the redirecting part:
redirect_to invalid_input_path and return unless model.valid?
redirect_to isbn_lookup_failed_path and return unless model.do_isbn_lookup
....

Resources