Ruby on Rails: Pass Parameters to View - ruby-on-rails

When a user makes a request to the url /mobile in my Rails app, I would like a parameter to automatically be appended to the URL that gets loaded after the request (something like /mobile?tree_width=5)
I have tried a few things, all of which have not worked.
The closest I have gotten is this in my controller:
def mobile
respond_to do |format|
format.html{
# pass tree width here
render mobile_project_steps_path(#project, :tree_width => #project.tree_width)
}
end
end
I am getting the error
Missing template /projects/8/steps/mobile?tree_width=5
But I think this path should exist according to my rake routes:
mobile_project_steps GET /projects/:project_id/steps/mobile(.:format) steps#mobile
How do I add a param to the URL from a controller?

You need to check if the param is missing and if it is redirect to current action with extra param. I would squeeze it with in before_action:
before_action :check_tree_width, only: :mobile
def mobile
# Your actual logic
end
private
def check_tree_width
redirect_to(tree_width: #project.tree_width) unless params[:tree_width].present?
end

Related

How to get Rails "new" path after failed form-input

I am implementing a simple language switcher in the common HTML menu-bar on a website in Rails, where the locale is path-based with the Rails-standard I18n implementation, e.g., /en/articles for Index for the Article model in English (in the following example, I consistently use pages for the model Article).
The language switcher is a simple text link (<a> in HTML) to the same page in another language. For example, the French page for /en/article/7 should be /fr/article/7.
Here is the most simplified code for the language-switcher link, which preserves all the GET query parameters for the current page:
str_link = link_to("fr", url_for(locale: "fr", params: request.query_parameters.except("locale")))
This code is included inside the common layout /app/views/layouts/application.html.erb so that it is applied to all pages on the website.
This works most of time but fails on the page after a user-input to a new page turns out to be invalid (status: :unprocessable_entity), according to the Rails standard CRUD action.
In the new page, the required link for the language switcher for French should be /fr/article/new; the above code-snippet works fine when a user freshly opens the new page in English (/en/article/new). However, once a user's input turns out to be invalid, the given URL is /en/articles, whose contents are equivalent to the new page.
How can I obtain the path /en/article/new in such cases so as to make the language switcher provide the correct link?
Obviously, when a user just requests the Index page /en/articles, the counterpart French page is /fr/articles, the URL of which is identical to the failed new page. So, they must be distinguished, that is, the path cannot be guessed thoroughly from the current URL and it depends on whether it is a fresh request or unprocessable_entity.
An answer to the question "Ruby on Rails Link to Previous Page on Form Failing After Invalid Input" suggests implementing hidden_field that contains the new page URL and the algorithm uses it. However, the suggestion does not work well in this case because the hidden_field for action create is POST and not a GET parameter. In the present case of the common language switcher, I need to deal with an arbitrary number of models inside the common layout (application.html.erb), meaning which parameter in params to permit cannot be pre-determined and hence to access hidden_field in params is tricky.
Here are the routes and Article Controller generated according to the Rails standard:
% bin/rails g scaffold Article title:string content:text
Routes:
# config/routes.rb
Rails.application.routes.draw do
filter :locale
resources :articles
end
Article Controller:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def create
#article = Article.new(article_params)
respond_to do |format|
if #article.save
format.html { redirect_to article_url(#article), notice: "Article was successfully created." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
private
def article_params
params.require(:article).permit(:title, :content)
end
end
I am using Rails 7.0. But I think the Rails version is almost irrelevant.
The language needs to be defined as a variable in your routes:
# config/routes.rb
scope "/:locale" do
resources :articles
end
This puts a language parameter in all your routes. Now you have to make sure that it's well-defined everywhere, as described here
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
end
And you need to define the default locale somewhere, e.g. in an intializer:
# config/initializers/locale.rb
Rails.application.config.i18n.default_locale = I18n.default_locale = :en

(Rails) How to get 'id' out of edit url

I have a model called studies.
After action redirect redirect_to edit_study_path(#new_study),
URL: http://localhost:3000/studies/2/edit.
Is there anyway to customize an url after passing id ?
For example, http://localhost:3000/study
(still going to the edit path, and still with the :id in the params)
I guess what you want is to edit the current study?
In this case, it's possible, using ressource instead of ressources in the routes.
Let's have an example:
#in routes.rb
resources :studies
resource :study
Both of them will by default link to the StudiesController and call the same actions (eg. edit in your case) but in two different routes
get "/studies/:id/edit" => "studies#edit"
get "/study/edit" => "studies#edit"
in your edit action, you should then setup to handle correctly the parameters:
def edit
#study = params[:id].nil? ? current_study : Study.find(params[:id])
end
Note you need a current_study method somewhere, and store the current_study in cookies/sessions to make it works.
Example:
# In application_controller.rb
def current_study
#current_study ||= Study.find_by(id: session[:current_study_id]) #using find_by doesn't raise exception if doesn't exists
end
def current_study= x
#current_study = x
session[:current_study_id] = x.id
end
#... And back to study controller
def create
#...
#Eg. setup current_study and go to edit after creation
if study.save
self.current_study = study
redirect_to study_edit_path #easy peesy
end
end
Happy coding,
Yacine.

How to redirect from /:id to /:friendly_id

Is is possible to force a 301 redirect when someone attempts to browse to a page using the old /:id URL, rather than than the preferred /:friendly_id link?
Apparently such redirections help to tell Google that you have updated the link.. so it stops displaying the old non-friendly link.
With the latest version of friendly_id (5.0.3 at the time of writing this answer) and Rails 4, I do this in the controller:
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]
...
private
def set_item
#item = Item.friendly.find(params[:id])
redirect_to action: action_name, id: #item.friendly_id, status: 301 unless #item.friendly_id == params[:id]
end
end
Here's a description of the redirect_to line, broken down piece by piece:
action: action_name retains the action that you're connecting to (which can be show, edit, update, or destroy based on the before_action that's in place) so that if you're accessing /items/1/edit you will be redirected to /items/pretty-url/edit
id: #item.friendly_id ensures that the URL you're being redirected to is the pretty URL
status: 301 sets the redirect to the status of 301, for SEO
unless #item.friendly_id == params[:id] makes sure that we're not redirecting people who access #item through its pretty URL
just defined the redirection inside the routes file
get '/:old_id', to: redirect {|params, req| "/#{X.find(params[:old_id]).friendly_id}" }
While James Chevalier's answer is correct, you can extract this method to the ApplicationController in order to use with any model that uses FriendlyId:
def redirect_resource_if_not_latest_friendly_id(resource)
# This informs search engines with a 301 Moved Permanently status code that
# the show should now be accessed at the new slug. Otherwise FriendlyId
# would make the show accessible at all previous slugs.
if resource.friendly_id != params[:id]
redirect_to resource, status: 301
end
end
As you can see it's also unnecessary to pass a specific action key to redirect_to. Passing a Rails model to redirect_to will automatically attempt to access the show action on the associated collection resource route (assuming it's set up that way). That also means it's unnecessary to pass an id key since FriendlyId always returns the latest slug in the model's #to_param.
Not being a huge fan of unless (confusing semantics) I tend to shy away from it but that's more my personal preference.
Routes
I don't think your routes are the problem here
The problem is the backend handling of the route (I.E whether it uses friendly_id or not). All Google will see is this:
domain.com/users/45
domain.com/users/your_user
If both of those routes work, Google will be happy. I think you're alluding to the idea that if you change the routes to only handle your_user, you'll need to be able to get Google to appreciate the redirects
Redirects
Considering you can handle both id and slug in the backend (we have code for this if you want), I'd handle redirects using the ActionDispatch::Routing::Redirection class:
#config/routes.rb
begin
User.all.each do |u|
begin
get "#{u.id}" => redirect("#{u.slug}")
rescue
end
end
rescue
end
Yes it is possible, you need to define both routes on your config/routes.rb
get 'path/:id' => 'controller#action'
get 'path/:friendly_id' => 'controller#action_2'
then in your legacy action method you need to provide a
return redirect_to controller_action_2_path(friendly_id: friendly_id),
status: :moved_permanently
this will generate a 301 response code. Which will eventually make bots start hitting your new pattern, without losing any of your traffic or indexing (SEO).

Rerouting model contents using to_param

I am wanting to expand the URLs associated with the contents of a model called Product, at the moment, I can view a specific product by going to products/ID.
I would like to extend the product URL so it includes some more descriptive information, such as the product name.
I have previously been advised to adjust the to_param function (in Product.rb) as below:
def to_param
"#{id}-#{product_name.parameterize}"
end
However, this doesn't currently work. The URL associated with each product appears correctly when you hover over it / click it, but there is no matching product found. I get the error no match for ID=ID-specific-product-name
If i visit /products/id i can still successfully view the specific item
Can anyone guide me as to how I could generate this longer URL containing the product name (:product_name)?
EDIT
The show controller action in my controller is:
def show
#uniqueturbo = Uniqueturbo.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #uniqueturbo }
end
end
If you're trying to make some SEO friendly urls
http://www.yourdomain.com/products/123123-My-Little-PonyBook
I think that the easiest way is to change the routes, like this
get '/products/:title/:id' => "products#show"
and then you'll get seo-friendly url's like:
http://www.yourdomain.com/products/My-Little-PonyBook/123123
To generate this url, create helper
def url_for_product(product)
"/products/#{product.title}/#{product.id}"
end
The other way is to leave the normal RESTful route, and reparse 'id' parameter, like:
def show
product_id = params[:id].split('_')[0] # :-)
# ...
end
and still you need the helper method, this time, sth like:
def url_for_product(product)
product_path(product) + "_#{product.title.tableize}"
end

Ruby on Rails controller design

When I look at examples of Rails controllers, I usually see something like this:
class WidgetController < ActionController::Base
def new
#widget = Widget.new
end
def create
#widget = Widget.new(params[:id])
if #widget.save
redirect_to #widget
else
render 'new'
end
end
end
This works, but there's a couple problems:
Routes
If I add widgets to my routes.rb file:
Example::Application.routes.draw do
resources :widgets
end
GET /widgets/new will route to new and POST /widgets will route to create.
If the user enters incorrect information on the new widget page and submits it, their browser will display a URL with /widgets, but the new template will be rendered. If the user bookmarks the page and returns later or refreshes the page, the index action will be called instead of the new action, which isn't what the user expects. If there's no index action or if the user doesn't have permission to view it, the response will be a 404.
Duplication of code
As a contrived example, let's say I had some tricky logic in my new method:
def new
#widget = Widget.new
do_something_tricky()
end
Using the current approach, I'd duplicate that logic in new and create. I could call new from create, but then I'd have to modify new to check if #widget is defined:
def new
#widget ||= Widget.new
do_something_tricky()
end
Plus, this feels wrong because it reduces the orthogonality of the controller actions.
What to do?
So what's the Rails way of resolving this problem? Should I redirect to new instead of rendering the new template? Should I call new inside of create? Should I just live with it? Is there a better way?
I don't think this is a problem in "the rails way" and there is no builtin functionality to allow this without getting your hands dirty. What does a user expects when bookmarking a form they just submitted and had errors? Users don't know better, and they shouldn't bookmark a failed form.
I think redirecting to new_widget_path is the cleanest solution. Yet, you should keep the errors and display them on the form. For this I recommend you keep the params in session (which I expect to be smaller than a serialized Widget object).
def new
#widget = widget_from_session || Widget.new
end
def widget_from_session
Widget.new(session.delete(:widget_params)) if session[:widget_params].present?
end
private :widget_from_session
# Before the redirect
session[:widget_params] = params
The code is self explanatory, Widget.new will only be called when widget_from_session returns nil, this is when session[:widget_params] is present. Calling delete on a hash will return de deleted value and delete it from the original hash.
UPDATE Option 2
What about submitting the form using ajax? Your controller could benefit from:
respond_to :html, :json
...
def create
#widget = Widget.new params[:widget]
#widget
respond_with #widget, location: nil
end
Based on the response code (which is set by Rails: 201 Created or 422 Unprocessable Entity), you could show the errors (available in the body of the response when validations fail) or redirect the user to #widget
This is how StackOverflow does it: https://stackoverflow.com/questions/ask. They submit the form asynchronously.
In general, I think the Rails way of solving the problem would be to put the tricky method onto the model or as a helper method, so the controller stays "thin" and you don't have to make sure to add custom behavior to both #new and #create.
EDIT: For further reading, I'd recommend the "Rails AntiPatterns" book, as they go through a lot of these common design issues and give potential solutions.
you put do_something_tricky() in its own method and call it inside the create action (but only when you're rendering the new template, ie when validation fails).
As for the bookmark issue, I don't know a good way to prevent that but to modify the routes and set the create action to the new action but using POST
get '/users/new' => 'users#new'
post '/users/new' => 'users#create'
UPDATE: using resources
resources :platos, except: :create do
post '/new' => 'plates#create', on: :collection, as: :create
end
then you can use create_platos_path in your forms
You don't need to write same function in two action , use before_filter instead.
If you want to have "widget_new_url" after incorrect submission then in your form add url of new widget path something like :url => widget_new_path .
Rails takes the url from Form .
I have this problem before, so I use edit action instead.
Here is my code.
Routes:
resources :wines do
collection do
get :create_wine, as: :create_wine
end
end
Controller:
def create_wine
#wine = Wine.find_uncomplete_or_create_without_validation(current_user)
redirect_to edit_wine_path(#wine)
end
def edit
#wine = Wine.find(params[:id])
end
def update
#wine = Wine.find(params[:id])
if #wine.update_attributes(params[:wine])
redirect_to #wine, notice: "#{#wine.name} updated"
else
render :edit
end
end
Model:
def self.find_uncomplete_or_create_without_validation(user)
wine = user.wines.uncomplete.first || self.create_without_validation(user)
end
def self.create_without_validation(user)
wine = user.wines.build
wine.save(validate: false)
wine
end
View:
= simple_form_for #wine, html: { class: 'form-horizontal' } do |f|
= f.input :complete, as: :hidden, input_html: { value: 'true' }
What I did is create a new action 'create_wine' with get action.
If user request 'create_wine', it will create a new wine without validation and redirect to edit action with a update form for attributes and a hidden field for compele .
If user has create before but gave up saving the wine it will return the last uncompleted wine.
Which means whether use save it or not, the url will be the same to /wines/:id.
Not really good for RESTful design, but solve my problem. If there is any better solution please let me know.

Resources