Rails 3.1 nested forms validation - ruby-on-rails

I'm using Formtastic 2 for a nested form - I have a menus and a meals model, menus have many meals, every meal belongs to one menu.
I added the meal form to the menu show action, right below a list of already associated meals.
Creating meals works fine if validation succeeds, I forward to the menu show action again listing the created meal in the list.
But when the meals doesn't get validated and I forward to the menu show action again with an appropriate flash message, I would really like to fill the form with the data that was submitted before and rendering the errors next to it.
I tried with this redirect:
redirect_to(menu_path(menu,#meal), :alert => 'The meal was not created')
But I can't get at the meal variable and passing it back to the form this way, the request itself is a GET request with only the menu id.

You shouldn't redirect after validation errors, because you'll lose all state. Just the old template directly after a failed validation. A little gotcha is that you need to use flash.now[:alert], so it won't carry over to the next page.
Usually you'll have this structure:
def new
#meal = Meal.new
end
def create
#meal = Meal.new(params[:meal])
if #meal.save
flash[:notice] = "Meal was created"
redirect_to menu_path(menu, #meal)
else
flash.now[:alert] = "The meal was not created"
render :new
end
end

Related

How to render a batch action in ActiveAdmin including a form?

I display a list of trips in admin/trip, for the selected trips I have a batch action which generate a pattern of html code of a newsletter (completed with informations from the selected trips).
batch_action :batch_action, method: :post do |ids|
#my process
render "admin/trips/generate_newsletter.html.erb"
That works. However in the view, just before the html code I have a form_tag:
<%= form_tag({action: "batch_action"}, class: "formtastic") do %>
The form only contains a text_area where the admin can add a comment for the newsletter. The idea is that when we send the form, the page is refreshed, keeping the trips selected and adding the admin comment into the html code.
Now when I submit the form I get an error: ' Couldn't find batch action "" '.
The variable #_params contains:
{"utf8"=>"✓","authenticity_token"=>"pzqtPMxhB9G83UYad2olCF4O79cK4+mf+R0VBOR61uo=", "admin_text"=>"Admin comment", "commit"=>"Ajouter", "action"=>"batch_action", "controller"=>"admin/trips"}
I've tried multiple things but I don't see the answer, maybe what I want to do is impossible this way.
ActiveAdmin 1.0 provides a DSL for creating a custom form in a batch action.
In app/admin/trips.rb
batch_action :generate_newsletter, form: {
comment: :text,
send_now: :checkbox
} do |ids, inputs|
# load selected trips
trips = Trip.find(ids) # selected trips
# Generate Newsletter from trips array
newsletter = Newsletter.create_from(trips)
if inputs[:send_now] == 'on'
# send generated newsletter
NewsletterMailer.send_email(newsletter).deliver
# return to admin/trips#index
redirect_to collection_path, notice: 'Sent newsletter'
else
# show generated newsletter
redirect_to admin_newsletter_path(newsletter), notice: 'Generated newsletter'
end
end
However, I am not aware of a built-in method that keeps the IDs selected after the page refreshes. To compensate, I recommend creating a Newsletter populated with the selected trips (shown above). One could even make an association between Newsletter and Trips so that you can reorder or remove trips for a newsletter.
CITE: http://activeadmin.info/docs/9-batch-actions.html

How to render one association automatically with cocoon

I have cocoon working with nested form, if you click add field link it inserts input fields. How do I render first input automatically, and then insert additional inputs when "add field" is clicked ?
In your controller, use this code. In the code below, jobs is a model and profile accepts_nested_attributes_for jobs. Replace #profile with whatever your form is for. The 2nd line is what will build the form fields, unless form fields already exist.
def new
#profile = current_user.profile
1.times {#profile.jobs.build} unless current_user.profile.jobs.any?
end
You may need to change times to time since its singular. In fact, you may be able to get rid of the times method altogether and do:
def new
#profile = current_user.profile
#profile.jobs.build unless current_user.profile.jobs.any?
end
The quick and dirty solution is to just use jQuery (which Cocoon requires anyways) to click Cocoon's "add item" button when the page loads:
$(document).ready(function() { $(".add_fields").click() } );
I use this in my "new" views, but not in "edit" views, since there may already be some nested items and I don't want to make assumptions. But you could also use script to count the nested item forms and conditionally show the "new item" fields.

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: Prevent duplicate inserts due to pressing back button and save again

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| %>

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