Passing a model object to a link generated by a named route - ruby-on-rails

In my routes.rb file I have:
get 'search' => 'movies#search', as: :search_directors
And the search action in the movies controller looks like this:
def search
#movie = Movie.find(params[:id])
# other code
end
One of the views contains the following link:
= link_to 'Find Movies With Same Director', search_directors_path(#movie)
I was hoping that when this link is clicked, the ID of the #movie object will be available through params[:id] in the movies#search action. But instead, when I click on it Rails gives me this error:
ActiveRecord::RecordNotFound in MoviesController#search
Couldn't find Movie with 'id'=
The #movie object that I'm passing as a parameter to the route IS valid because other parts of the view is working properly (it is the show.html.haml view for the Movie URI).

You need to specify the id in your route, or pass it as an additional parameter (in that case it will be appended to the url as a GET param).
get 'search/:id' => 'movies#search', as: :search_directors
Or (for GET param):
= link_to 'Find Movies With Same Director', search_directors_path(id: #movie.id)
In the last case, you would have to add a condition in your search method to test if params[:id] is present at all.

Hope this will work for you...
def search
#movie = Movie.find(params[:id]) # this will raise ActiveRecord::RecordNotFound if id is not available in data base.
# you should use where instead of find
#movie = Movie.where(id: params[:id]).first # this will return nil if not found but not raise ActiveRecord::RecordNotFound Exceptaion.
# other code
end

Related

Action show to find value from hash instead of id

I am still learning rails and have done a lot of readings, but I am not very clear about how params, 'show' actions work yet.
For example we have UsersController, 'index' action is showing all the users with the code #user = User.all, and 'show' action is looking into each users, by using the code #user = User.find(params[:id])
I understand that they are all from the database, where User is a model.
However in my scenario, what if the data I am showing in views, doesn't go through database, instead in the 'index' action it is something like this -
#user = [{name => "alex"}, {name => "peter"}, {name => "john"}]
and in my 'show' action, how can I write the code so that it finds the users by name?
In your Rails app, the data that you show in your views, do not necessarily have to come from/through the database. You can always show any data you want in your views.
For example, in your index action, if you have this:
#users = [{name => "alex"}, {name => "peter"}, {name => "john"}]
Then, in your index view, you can show only those users by looping through the #users instance variable.
Same for show page as well.
If you want to show the users by name in your show page, you have to set the users by name in an instance variable e.g. #users_by_name:
#users_by_name = User.find_by(name: user_name)
# or you can hard code the values if you want like index action
and then this #users_by_name instance variable will be available in your show view so that you can loop through that and show the user names.
Originally, the show page is designed for showing a particular user related information, but you can show whatever information you want going against the conventions.
To be able to have a route like this: localhost:3000/users/alex that will show the user alex's information, you can add a route in your routes.rb file:
get 'users/:name', to: "users#show"
And, in your controller's show action, something like this:
def show
#user = User.find(params[:name])
end
Then, show the #user information in your view page.
P.S. This is not a good idea to find user by name as there might be more than one user with same name in the database and it will create conflict/ or give wrong data in such situations.
In show action , we search the user specific record not all.
So , we have to provide some unique identifiers as parameters to find the specific record.
For eg. Your view should be similar to the params we are passing as below:
<% #user.each do |user| %><br>
<%= link_to user.name, user_show_path+"?name="+user.name %><br>
<% end %><br>
In show action , write the code
def show
#user = User.find_by(:name => params[:name])
end
Also in routes.rb , write the below code:
get 'users/:name', to: "users#show"
For the above solution, make sure that name field will be unique.
My original question is that if it is possible for 'show' action not to go through database
Sure.
Your show action can be the following if you wanted it to:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
#user = "me"
end
end
You really don't have to do anything specific in your application, Rails is just a framework and has certain conventions if you want it to work efficiently.
What you're asking is if you can populate your #user object from a third party set of data...
... Yes you can ...
The way to do it would be in the model, not the controller:
#app/models/user.rb
class User < ActiveRecord::Base
# populates from Hash
end
You'd then be able to populate the data in the controller from the model again:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
#user = User.__________ #-> pull from your hash
end
end
finds the users by name
That's simple - just pass the name through the url: url.com/users/marine_lorphelin
This will set the :id parameter to marine_lorphelin, with which you'll be able to look up the name through your model:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
#user = User._______
end
end
If you were using a database with your user model, you'd be able to use the following:
def show
#user = User.find_by name: params[:id]
end
Since you're not, you'll have to attach your XML hash to your model somehow. This, I don't know without specifics such as where you're getting your data from, how you're accessing it, and which routes you're going to send to invoke it.

Passing params using redirect to different action in controller

This is first action in controller:
def investor_following
#investor = params[:user][:investor_id]
# blah
end
def change_amount
investor = "xyz"
redirect to :action => :investor_following, :user[:investor_id] => investor
end
I am getting error how can I redirect to action investor following, what would be right syntax to do with params.
You should create a named route for your action in your routes.rb. I'm not sure what you investor_following function will do, so I am not certain if it should be a GET, POST, or PATCH. If you intend to modify your model, use a POST/PATCH, if not, use a get.
Once you have a named route, you will get a path helper like investor_following_path which you can send parameters as ruby objects:
#routes.rb
get '/investor_following', to: 'controllername#investor_following', as: 'investor_following'
#in your controller
redirect_to investor_following_path(user: {investor_id: investor})
This is untested but in general what you should do.
Here is info on redirect_to:
http://api.rubyonrails.org/classes/ActionController/Redirecting.html
Here is the info on routing for your named path:
http://guides.rubyonrails.org/routing.html

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.

Rails 3: Check for routing error in rails code

I have a lot of courses in my website. I have a "go to course" form. When user inputs a valid course name, I successfully redirect that user to that course page.
def courses
#title = "Courses"
#path = "#{root_path}"+"course/" + "#{params[:course]}"
if #path == "/course/"
#title = "Courses"
render 'courses'
else
redirect_to #path
end
end
This works for all valid inputs that is if the route exists. I want to take care of the error case when the user inputs invalid course name which would cause an invalid route. I want to check for routing error in the code and let the user know with an error message. Is this possible?"
Instead of building out custom routes in your controller, use Rails convention and place them in config/routes.rb:
resources :courses
This creates the route /courses/:id for you. Then your controller would simplify to:
def show
#title = "Courses"
end
It would help if you included your model, controller and routes.rb.
You can use this to check if your route is valid:
def check_route(route)
#route_valid = true
begin
ActionController::Routing::Routes.recognize_path(#route, :method => :get)
rescue
# error means that your route is not valid, so do something to remember that here
#route_valid = false
end
end
But in general, I do not like your idea of creating a route based on a course. Why not just use a simple finder?

Resources