Rails 5: Search form not preserving locale url parameter - ruby-on-rails-5

I have a search form on my home page (root_path) that returns the results to the root_path as well. I am making a multilingual site using Ruby I18n and am using the url parameters option, so that the language is specified like so: localhost/?locale=en
For some reason, when submitting the search form, the response is not in the chosen language (it is in the default language), and the locale parameter is now gone from the URL. For example, submitting with localhost/?locale=ja will return with an English page and a URL of localhost/(other search from parameters but no locale)
All other parts of my application preserve the locale parameter correctly (e.g. link_to). Other forms in my application also preserve it correctly when submitted.
There are only two differences I can see between this form and the others.
This form uses method:get
This form is on the root_path
Here is the form:
<%= form_with(url: root_path, local: true, method: :get) do |form| %>
(various fields)
<%= form.submit t(:submitsearch), name: nil %>
The html that is generated:
<form action="/?locale=en" accept-charset="UTF-8" method="get">
<input type="submit" value="Search" data-disable-with="Search" />
application_controller
class ApplicationController < ActionController::Base
before_action :set_locale
def default_url_options
{ locale: I18n.locale }
end
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
end
However the url that is requested is:
http://localhost:3000/?utf8=✓&(other parameters - no locale)
Here's an example from my app that behaves as expected
<%= form_with model: #equipment_type, local: true do |form| %>
(various fields)
<%= form.submit %>
which has generated html of
<form action="/equipment_types/2?locale=en" accept-charset="UTF-8" method="post">
<input type="submit" name="commit" value="Update Equipment type" data-disable-with="Update Equipment type" />
I basically need the search form to preserve the user's chosen language, but at the moment it is always reverting to the fallback (and the locale parameter is disappearing from the URL). The form action seems to be getting generated correctly (it has the ?locale=en parameter), but it is not in the request URL or response.
Can anyone suggest what the problem might be here?

So after a bit more research, the link here suggests that this is default behaviour of browsers and that it is per HTML spec. That is, any query string parameters in the action of a form will be entirely replaced. For rails, that means if you have a form with action GET, even though rails puts the locale parameter in the form action, it will not get used and you will lose the chosen locale (and revert to the default).
As a solution, you can add the following tag to your form:
<%= hidden_field_tag "locale", params[:locale]%>
Which will add the parameter back into the form submission. It's not perfect, because the locale parameter now appears after the UTF8 one (e.g. ?utf8=✓&locale=en) whereas in all other cases, the locale parameter will appear first. Though this is only a minor point.

Related

Rails 5 - use collection_radio_buttons to open a partial with nested models?

(my first SO question, so please be kind!)
Each Schedule has only one System;
Each System has many Applications;
Each Application has many Users and Documents.
What I want to do to create a Schedule entry:
Generate a form that first shows multiple System names that can be clicked. When a System is clicked, it opens a partial that lists Applications associated with that System. Then, when clicking particular Applications, yet another partial opens that contains Users and Documents associated with that particular Application.
When editing this entry later, I want to be able to see everything I had entered before, with the correct System, Application(s), User(s), and Document(s) already pre-selected.
My question here is how to make a form element for choosing a System that will open another partial -- and, later, will be pre-selected when I view and/or edit the entry.
What kinda works right now is a <%= link_to %>, styled with Bootstrap, which opens its associated Applications partial when it's clicked. However, I'm not able to save the System from it, and I can't figure out whether it can display as already-selected later, such as in an Edit form.
We're trying to use radio buttons instead (because you can't pre-select a link_to, right?), so I've been throwing pasta at the wall with f.collection_radio_buttons, or iterating over f.radio_button, or a tag of <input type="radio">. However, I can't figure out how to make a radio button open a partial.
Since first posting this question, I've narrowed down to using f.radio_button within a loop. It shows as correctly "checked" when viewed while Editing the entry later, but it still doesn't open the partial.
Here's a snippet from /_schedule_form.html.erb:
<%= form_for #schedule do |f| %>
<% System.all.each do |a| %>
<!-- This link_to makes the Applications partial appear, but that's all it does -->
<%= link_to a.system_nm, system_applications_path(:system_id => a.id,
:schedule_id => params['id']), :title => 'Click to choose system',
:class -> 'btn btn-default btn-flat active', data: {:remote => true,
'data-target' => '#applicationList'} %>
</a> <!-- closes the link_to tag, I believe -->
<% end %>
<div id="applicationList">
<!-- the Applications partial renders here -->
</div>
<% end %>
Here's the system_applications.js.erb file that opens the _system_applications.html.erb partial:
$("#applicationList").html("<%= escape_javascript(render
'system_applications', locals: {applications: #applications,
schedule_applications_array: #schedule_applications_array})%>");
Here's an update with possible clue:
I'm now using this embedded Ruby code:
<% System.all.each do |rt| %>
<label>
<%= f.radio_button :system_id, rt.id, data:{:remote => true, 'data-target' =>
'#applicationList'}, href: system_applications_path(:system_id => rt.id,
:schedule_id => params['id']), class: 'remote-input', onclick:
"#applicationsList" %>
</label>
<% end %>
And, when I click on that radio button, I'm getting a JS error in the browser console Uncaught SyntaxError: Invalid or Unexpected Token which points to the rendered HTML, and specifically the > at the end of the line:
<input data-remote="true" data-data-target="#applicationList" href="/schedules/system_applications/24?schedule_id=122" class="remote-input" onclick="#applicationList" type="radio" value="24" checked="checked" name="schedule[system_id]" id="schedule_system_id_24" />
Just to make it more complicated:
When creating a NEW entry, and when I hover over one of the link_to buttons, I get a clean-looking path like /schedules/system_applications/24, and that's what gets sent to the server when clicked (which then reads the params as {"system_id"=>"24"}. But hovering over the f.radio_button labels shows no path, and clicking it sends "/schedules/new?schedule%5Bsystem_id%5D=24" with the params {"schedule"=>{"system_id"=>"24"}}. This triggers a 500 (Internal Server Error) (which makes sense, because there's no path for that URL). Do the params need to be the same? If so, how am I screwing it up?
Also, clicking the radio button sends the request to SchedulesController#new, while clicking the link_to sends it to SchedulesController#system_applications. I don't understand how I'm telling the radio button to send it to #new.
Where I'm stuck now is, the f.radio_button renders as "checked", which is correct; but it doesn't open the partial. The link_to version opens the partial, but I don't think it can render as "checked" later on.
Let me know if I'm asking clearly enough, too.
I think we made it work. One key change was to use url instead of href to use the system_applications_path to the controller, as shown here:
<% #systems.each do |a|
<label class="btn btn-sm btn-default">
<%= f.radio_button :system_id, a.id, :data => {url:system_applications_path(:system_id
=> a.id, :schedule_id => params['id']), 'data-target' => '#applicationList'+a.id.to_s,
:remote => true} %>
<%= a.system_nm %>
</label>
<% end %>
Now, when I click the radio button, it opens the correct partial, and it shows as selected later when I go to edit the entry. The Bootstrap styling (btn btn-sm btn-default) helps to show how much clickable area there is, but it's not required for basic functionality.
I suppose some of that [in]famous Rails Magic has radio_buttons treating href: differently than url. But it's "magic" only because I don't understand it (yet -- growth mindset!). What's the explanation behind it?

How do I render a reply into turbolinks 5 with rails 5

I have a controller action where I'd like to receive form data, execute some business logic, then refresh the form. This works Ok if I save the object in the database then use a redirect_to. I would prefer the controller edit the object in memory and render the response directly.
For example, take a standard rails 5.1.4 generated app generated like so:
rails new turbolinks_example
rails g scaffold Thing name
rails db:migrate
The form, slightly edited for brevity and to enable turbolinks:
<%= form_with(model: thing) do |form| %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :thing_name %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
A) Now we edit the controller to change the object. This uses a redirect to edit and works:
ThingsController < ApplicationController
...
def update
if #thing.update(thing_params)
#thing.update name: "#{#thing.name} is OK"
redirect_to edit_thing_path(#thing)
end
end
B) This uses a render and does not work:
class ThingsController < ApplicationController
...
def update
if #thing.update(thing_params)
#thing.name = "#{#thing.name} is OK"
render :edit
end
end
end
With A)
- Update request is received by controller
- Object is modified (& saved)
- Redirect is returned
- Rediredted url is rendered
- DOM is updated
With B)
- Update request is received by controller
- Object is modified (in memory)
- Response is rendered
- Response is received by browser, but ignored
The response received, looks correct. Full HTML, with the changes made to the object. How do I get turbolinks to notice it and replace document.body like normal?
The full project, including development.log is on Github
The problem is that, in Rails 5:
Forms are remote by default: they are sent via AJAX unless local: true provided
When rendering HTML as a response to an AJAX call, nothing happens unless, of course, there is custom javascript in the client handling the response
Turbolinks is enabled by default, which handles redirect_to properly, but does nothing for render
I think this is an inconsistency in Rails that causes much confusion, such as the problem you exposed with your code. I created a gem turbolinks_render to deal with this problem. I also wrote a little post on this very same problem.
I hope that, one way or other, this gets fixed in future Rails versions.

How do I set two different actions from a rails form?

I have a rails form that creates (and updates) materials. Obviously, by default, when the user is logged in and the materials is retrieved the form renders with the update action. However, I want to have a "save as" action in case the user wants to save another version of the material. The obvious way to do this is to have one button which sends to 'update' in the controller and another which sends to 'new', but I don't know how to do this since it seems to depend on the form_for parameters.
Update
To avoid using Javascript I tried changing the form_for url to:
<%= form_for #material, :url => choose_action_path do |f| %>
Then in the controller I have:
def choose_action
if params[:save_as] == "Save As"
redirect_to :action => "create"
else
redirect_to :action => "update"
end
end
but this is not working. Is this a strategy that could work or is this crazy?
You could probably do this with some jquery.
Let's say the model you are working on is called Email. When you edit an Email, the form tag that is created with have an action action='/emails/:id and a method method='post'. Hidden underneath the form tag is something like
<form accept-charset="UTF-8" action="/email/1" method="post">
<div style="margin:0;padding:0">
<input name="_method" type="hidden" value="put" />
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
</div>
more info can be found here: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
That extra chunk of code makes the form submit a put request instead of a post, which subsequently maps to the update action of your controller. To get it to send the form to the create action, you want to post the action "/email". In other words, you want to manipulate the code above into something like
<form accept-charset="UTF-8" action="/email" method="post">
Note the removal of the div tag.
So my suggestion would be to create a button "Save As", then add some jquery which will make those modifications when the "Save As" button is clicked.
This stackoverflow answer might help you with the jquery Jquery to change form action

Is there any way to avoid having the browser's address changed after using "render"?

I am currently following the Ruby on Rails Tutorial by Michael Hartl. And there is something that has been bugging me for quite some time. I looked it up but I still can't find a good answer.
Anyway, I've noticed is when you have a validation error in the signup page it renders the original signup page and changes the nav bar address. I've matched /signup to the action new, but if I use render it changes from /signup to /users (the default, because of the RESTful standard I guess).
I'll leave some lines of my code:
routes.rb
resources :users
match '/signup', :to => 'users#new'
users_controller.rb
def new
#user = User.new
#title = "Sign up"
end
def create
#user = User.new(params[:user])
if #user.save
sign_in #user
flash[:success] = "Welcome to the Sample App!"
redirect_to user_path(#user)
else
#title = "Sign up"
#user.password = ""
#user.password_confirmation = ""
render 'new'
end
end
So I've tried to work around this by not using the render method but redirect_to instead but I'm having trouble using it. As it is actually sending data to the path provided, #user.errors gets overwritten by creating a new instance of the model and the flash variable cannot show the errors.
_errors.html.erb
<% if #user.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#user.errors.count, "error") %>
prohibited the user from being saved:
</h2>
<p>There were problems with the following fields:</p>
<ul>
<% #user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
My question is: is there any way that by using render I can change the url displayed on the navbar? It's really frustrating if someone makes a mistake in the signup form, presses enter in the navbar and ends up in a totally different place.
The reason why the address changes is because you have performed a POST request to /users/ therefore the browser is doing the correct thing by displaying the different address.
There are a few of ways around this:
Store the invalid User object and redirect back to the Users.new action.
Change the URL of the Users.create action.
Use history.replaceState to change the user's address bar.
The first option keeps the controller more RESTful, however it will need use of the :session or flash to persist the data across the redirect.
The second option keeps the code simpler, but involves fiddling with the routes.rb file.
The third option relies on javascript and support for HTML5 to mess with the user's browser history.
Personally I would leave the URL as is, but if I had a client who insisted on doing this, I would go for the second option.

Rails 3.0.9 + Devise + Cucumber + Capybara the infamous "No route matches /users/sign_out"

I am using devise 1.4.2 with rails 3.0.9, cucumber-rails 1.0.2, capybara 1.0.0. I got No route matches "/users/sign_out" error when I clicked logout. I added :method => :delete to link_to tag after going through this so question ( no-route-matches-users-sign-out-devise-rails-3 ).
Since I replaced prototype with jquery, I also had to change
config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
to
config.action_view.javascript_expansions[:defaults] = %w(jquery jquery_ujs)
to get around rails.js not found error.
Although with above changes I am able to successfully sign out and redirected to root, when I look at response of localhost:3000/users/sign_out request in FireBug it shows the same routing error message click here to see the screenshot with notes
After successfully implementing authentication to rails 3 app through devise, When I added feature and specs using Cucumber + Capybara + RSpec following this tutorial (github.com/RailsApps/rails3-devise-rspec-cucumber/wiki/Tutorial), I got following error
When I sign in as "user#test.com/please" # features/step_definitions/user_steps.rb:41
Then I should be signed in # features/step_definitions/user_steps.rb:49
And I sign out # features/step_definitions/user_steps.rb:53
No route matches "/users/sign_out" (ActionController::RoutingError)
<internal:prelude>:10:in `synchronize'
./features/step_definitions/user_steps.rb:55:in `/^I sign out$/'
features/users/sign_out.feature:10:in `And I sign out'
And I should see "Signed out" # features/step_definitions/web_steps.rb:105
When I return next time # features/step_definitions/user_steps.rb:60
Then I should be signed out
with the following step_definition for 'I sign out'
Then /^I sign out$/ do
visit('/users/sign_out')
end
I searched a lot and found that this is because of unobrusive javascript in Rails 3 being used for 'data-method' attributes, but I also read somewhere that Capybara does check for data-method attributes and behaves accordingly. But it did not work for me, so following this post Capybara attack: rack-test, lost sessions and http request methods I changed my step definition to following:
Then /^I sign out$/ do
rack_test_session_wrapper = Capybara.current_session.driver
rack_test_session_wrapper.process :delete, '/users/sign_out'
end
but I got undefined method process for Capybara::RackTest::Driver (NoMethodError).
Following this lead I changed the above step definition as following:
Then /^I sign out$/ do
rack_test_session_wrapper = Capybara.current_session.driver
rack_test_session_wrapper.delete '/users/sign_out'
end
This at least passed the 'I sign out' step, but it did not redirected to the home page after signing out and the next step failed:
And I should see "Signed out" # features/step_definitions/web_steps.rb:105
expected there to be content "Signed out" in "YasPiktochart\n\n \n Signed in as user#test.com. Not you?\n Logout\n \n\n Signed in successfully.\n\n Home\n User: user#test.com\n\n\n\n" (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/web_steps.rb:107:in `/^(?:|I )should see "([^"]*)"$/'
features/users/sign_out.feature:11:in `And I should see "Signed out"'
After all this I had to resort to adding 'GET' method for logout in the routes file:
devise_for :users do get 'logout' => 'devise/sessions#destroy' end
modified my view from
<%= link_to "Logout", destroy_user_session_path, :method => :delete %>
to
<%= link_to "Logout", logout_path %>
and changed my step definition to following:
Then /^I sign out$/ do
visit('/logout')
end
This obviously solved all the problems, all the tests passed and firebug did not show any error on sign_out. But I know that using 'get' request for destroying sessions is not a good practice, because it's a state-changing behavior.
Could this be due to particular version or Rails, Devise, Cucumber-Rails, or Capybara I am using? I want to use Devise's default sign_out route instead of overriding it with get method and be able to do BDD using Cucumber and RSpec. I am new to using Cucumber+Capybara, does there exists another method to send POST request instead of using "visit('/users/sign_out')", which only uses GET method?
So I have found that
<%= link_to "Logout", destroy_user_session_path, :method => :delete %>
rails helper generates following html
<a rel="nofollow" data-method="delete" href="/users/sign_out">Sign out</a>
and jquery_ujs.js has following method to convert the links with data-method="delete" attribute to a form and submit at runtime:
// Handles "data-method" on links such as:
// Delete
handleMethod: function(link) {
var href = link.attr('href'),
method = link.data('method'),
csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content'),
form = $('<form method="post" action="' + href + '"></form>'),
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
if (csrf_param !== undefined && csrf_token !== undefined) {
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
}
form.hide().append(metadata_input).appendTo('body');
form.submit();
}
And Capybara helper visit('/users/sign_out') simply clicks the link and send a GET request to the server which does not have any route for this request.
As opposed to link_to helper the button_to helper adds the required form within the html when page is rendered instead of relying on javascript:
<%= button_to "Logout", destroy_user_session_path, :method => :delete %>
generates following html
<form class="button_to" action="/users/sign_out" method="post">
<div>
<input type="hidden" name="_method" value="delete">
<input type="submit" value="Logout">
<input type="hidden" name="authenticity_token" value="0Il8D+7hRcWYfl7A1MjNPenDixLYZUkMBL4OOoryeJs=">
</div>
</form>
with this I can easily use Capybara helper click_button('Logout') in my 'I sign out' step definition.
"link_to with a method anything other than GET is actually a bad idea, as links can be right clicked and opened in a new tab/window, and because this just copies the url (and not the method) it will break for non-get links..."
As Max Will explained right clicking and opening the link_to link with non-get data-method in new tab results in a broken link.
Some more useful discussion on link_to helper with ':method => :delete' and capybara issue can be found on this link
For now I would stick to simple link_to helper without :method attribute, and would prefer using button_to if I want to switch to non-get method for deleting.
At the same time I think there should be a capybara helper equivalent to Visit to cater for data-method attribute to send post request, so that one could avoid using javascript based driver for integration testing. Or may be there already is one which I am not aware of. Correct me if I am wrong.
The easiest way to correct this problem (albeit probably not the most correct one) is to modify your routes file to match the rest of the application. E.g. make the GET version of destroy_user_session_path work. You can do this by modifying the routes file as follows
Remove:
devise_for :users
Add:
devise_for :users do
get "/users/sign_out" => "devise/sessions#destroy", :as => :destroy_user_session
end
This is a bit dirty. I'm sure that Devise deprecated the GET route for good reason. However, fixing it any other way is beyond my Cucumber knowledge at this point, as every test in that suite ultimately relies on visit('/users/logout') which just isn't possible with the out-of-the-box Devise routes.
UPDATE
You can also fix this by commenting out the following in config/initialers/devise.rb
#config.sign_out_via = :delete
Devise 1.4.1 (27 June 2011) changed the default behavior for sign out requests:
https://github.com/plataformatec/devise/commit/adb127bb3e3b334cba903db2c21710e8c41c2b40
Jose Valim explained why: "GET requests should not change the state of the server. When sign out is a GET request, CSRF can be used to sign you out automatically and things that preload links can eventually sign you out by mistake as well."
Cucumber wants to test GET requests not DELETE requests for destroy_user_session_path. If you intend to use Cucumber with Devise, change the Devise default from DELETE to GET for the Rails test environment only with this change to config/initializers/devise.rb:
config.sign_out_via = Rails.env.test? ? :get : :delete
Don't try to tweak the routes.rb file to make the fix. It isn't necessary. If you're not going to use Cucumber, leave Devise's new default (DELETE) in place.
The example source code here:
https://github.com/RailsApps/rails3-devise-rspec-cucumber
now includes the change to the Devise initializer for Cucumber.
The application template here:
https://github.com/RailsApps/rails3-application-templates
now detects the collision between Devise and Cucumber and alters the Devise initializer as needed.
These changes were tested with with Rails 3.1.0.rc4 but the behavior should be the same with Rails 3.0.9. Please add comments here if the issue is unresolved or if you have more information.
The right way to solve this problem is explained in the devise's wiki page: https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara
Basically, once you have included on your user_step.rb file:
include Warden::Test::Helpers
Warden.test_mode!
You may replace visit '/users/sign_out' with logout(:user)
I'm actually having the same exact problem but with a Rails/Sinatra app. I've got Devise set up for Rails and the logout works. I've got a GuestApp Sinatra app running in lib, which works great except for the logout link. I'm trying to force data-method="delete" on the sinatra logout link, but nothing i do will make the request a delete request.
I think this might me a sinatra problem for me, but I thought what ever requests come in are processed by rails routes first until they reach my sinatra route. I'm about to manually add the GET route for logout, but I'd rather not have to do that.
Here's my devise routes:
devise_for :member, :path => '', :path_names => {
:sign_in => "login",
:sign_out => "logout",
:sign_up => "register" }
Here's my link:
%a{:href => '/logout', :"data-method" => 'delete', :rel => 'nofollow'}Log Out
Log Out
#- realized it should be method instead, but still not reaching routes.rb as delete
Log Out
When i need use something like this in test.env:
visit destroy_user_session_path
it's work for me, but maybe this is not right)
config/init/devise.rb
# The default HTTP method used to sign out a resource. Default is :delete.
if Rails.env.test?
config.sign_out_via = :get
else
config.sign_out_via = :delete
end