How to render a batch action in ActiveAdmin including a form? - ruby-on-rails

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

Related

How to display create page/form within a separate controllers show page?

I would like to have the ability to create an order directly from the listings show page instead of having to be directed to a new orders page.
I have a Listing (listingcontroller show method) which can be purchased by clicking a button to go to an orders page (orderscontroller create method).
In what way can I have the order form directly on the listings show page?
I have tried adding the form, but I get error:
First argument in form cannot contain nil or be empty
<%= form_for([#listing, #order]) do |form| %>
When I take the Orders controller create method and put it in the Listings Controller Show method i get this error:
Couldn't find Listing without an ID
Here's the form_for I want within the Listings Show Page:
<%= form_for([#listing, #order]) do |form| %>
....
Orders Controller create:
#order = Order.new(order_params)
#listing = Listing.find(params[:listing_id])
#seller = #listing.user
#order.listing_id = #listing.id
#order.buyer_id = current_user.id
#order.seller_id = #seller.id
...
Routes:
resources :listings do
resources :orders
end
listing model:
has_many :orders
category model:
has_and_belongs_to_many :listings
I tried taking the orders create method and injecting it into the Listings show method with "def create" and without. I put "#listing = Listing.find(params[:listing_id])" ahead of the create method (when using "def create" and i would still get the error it needs an id. Even when I get that error, at the bottom of the webpage the request shows the listing ID is there.
I tried using a hidden field in the form but didn't work for me.
Do I need to do something to the controllers or is there a way to load the :listing_id into the form somehow. This is probably something very quick and simple for some of you but why won't it load on the listings show page, but loads fine in the orders create page?
Easy approach.
Your show action in listing_controller.rb should have the following code:
def show
#listing = Listing.find(params[:listing_id])
#order = #listing.orders.build
.
.
.
end
Your views/listings/show.erb should have the following code
<%= form_for(#order, url: listing_orders_path(#listing)) do |f| %>
.
.
.
<%= end %>
This way you create an order to the listing (in memory) before you submit the form. You can add the listing id as a hidden field.
After submit the order you modify your orders_controller.rb this way:
def create
#listing = Listing.find(params[:listing_id])
#order = #listing.orders.build(params[...]) #select the params you need for the order creation. Since you create the order directly to the listing you don't need to add the listing_id to the order.
if #order.save
#do something
else
#do something
end
end
Keep in mind that using params[] directly you have security problems, please check about mass assignment: https://guides.rubyonrails.org/v3.2.8/security.html
You can achieve that by using AJAX call, where you will pass the url of orders action and other params. There will be no reload of page and you'll get the functionality right on the listings page.
Here is the link to have a look - How AJAX calls work.

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.

Correct way to have multiple submit buttons on single form in rails

I have a page in my rails application that requires me to have multiple submit buttons on a single form. To give you an idea of what I am trying to do, I have a shopping cart, which is a table of items. Each item has an order button, and a remove button. There is also a checkbox associated with each item, so that multiple items can be selected, and then submitted for order by a Button at the bottom of my table.
Currently, each of these three buttons have a different "name" property associated with them. In my controller I have code similar to the following:
if params[:remove]
#do stuff to remove item from shopping cart, then re-direct back to shopping cart
elsif params[:order_one]
#do stuff to order item, then render a confirm page
else
#do stuff to order multiple items, then render confirm page
end
But this seems messy to me. Is there a better "railsy" way to implement this?
Also, I am currently not using any JS/Ajax in my pages, and I would like to eventually have the shopping cart be a div that is rendered via Ajax. When a user clicks the remove button, I would like the form to be submitted remotely, so that the shopping cart div can be re-rendered via Ajax once the item is removed from the shopping cart - will this be possible having one form that has a button submitting some forms via ajax, and some forms without ajax?
This is a vast subject, but for an ajax delete button you can start like this. In your table view, use :
<tr id="tr_<%= item.id %>">
...
<%= link_to 'delete', item, method: :delete, remote: true %>
</tr>
You must handle the ajax call on destroy route in your items_controller.rb:
def destroy
#item= Item.destroy(params[:id])
respond_to do |format|
format.html { redirect_to items_url }
format.js
end
end
Create a new destroy.js.erb file in your items views folder and put in it :
$('#your_table').remove('#tr_<%= #item.id %>');
Then when you click on the remove button, it will call the destroy action in your controller. As the button is defined as remote, rails will automagically perform an ajax request and won't reload the page. Your controller will render the according js view in which you then remove the row from your table.
As I said, this is only the beginning and it can be done much better on the javascript side. Also note that I didn't check the code.
Also have a look at http://railscasts.com/episodes/136-jquery-ajax-revised?view=asciicast for a more in depth explanation.

Rails 3.1 nested forms validation

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

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

Resources