determining viability of routes generated with polymorphic_url - ruby-on-rails

I've got a generic partial that includes a link if the object is linkable.
The URL is generated with polymorphic_url. If polymorphic_url raises NoMethodError, then I don't include the link.
This mostly works great, except where a resource has an update or destroy action but not a show action. In that case polymorphic_url generates a valid URL, it's just not valid with GET.
Is there any way to restrict polymorphic_url to a specific request method, or a nice way to determine if the returned URL is valid for GET?

I took a second look at recognize_path, and got it to work.
begin
url = polymorphic_url(...)
# validate URL, since polymorphic URL may return a URL for a
# different method
Rails.application.routes.recognize_path(url, {:method => 'GET')
url
rescue NoMethodError => e # raised if polymorphic_url fails
nil
rescue ActionController::RoutingError => e # raised if recognize_path fails
nil
end

This might depend on your setting, but can't you find a path to the controller in question and thus find out, whether he knows the show action?

Related

No route matches [GET] on a post request

I started Ruby on Rails few days ago, and I'm struggling with routing.
Indeed, I would like to make a post request through my routes.rb, but I keep having a
No route matches [GET] "/orders/refresh"
error.
Here is my routes.rb :
# frozen_string_literal: true
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get '/orders', to: 'orders#index'
get '/orders/active(/:q)', to: 'orders#active'
post '/orders/refresh', to: 'orders#refresh'
end
and here is my controller (orders_controller.rb) :
# frozen_string_literal: true
class OrdersController < ApplicationController
def index
#orders = Order.order(:departure_date).all
render json: #orders.to_json
end
def active
if !params[:q]
#orders = Order.order(:departure_date).where(active: true)
else
#orders = Order.order(:departure_date).where("reference = ? OR client_name = ? OR departure_city = ? OR arrival_city = ?",
params[:q], params[:q], params[:q], params[:q])
.where(active: true)
end
render json: #orders.to_json
end
def refresh
response = RestClient.get 'https://wakeo-technical-test.s3.eu-west-3.amazonaws.com/api.json'
json = JSON.parse response
if !json.nil?
json.each do |order|
old_order = Order.find_by(reference: order["client_number"])
if !old_order.nil?
old_order.update(departure_date: order["dep_time"])
old_order.update(arrival_date: order["arr_time"])
old_order.update(client_name: order["company"])
old_order.update(departure_city: order["dep_city"])
old_order.update(arrival_city: order["arr_city"])
end
end
else
puts "error seeding external API"
end
end
end
From what I have understood, it seems like RoR will try to find a GET request for that specific URL, and since it won't find any, it will throw that error. How could I make that request be a POST for Rails ?
Also, I would appreciate any suggestion about how I should use ActiveRecord Querying, I'm pretty sure I could do it better here.
Thanks, have a great day !
EDIT : Here is the list of different routes my app seems to be capable of, including my POST.
Routes and error
The most common reason you unexpectly get GET requests instead of PUT, PATCH, POST or DELETE is that you are using link_to 'Something', '/some_path', method: :post and you broke the Rails Unobtrusive Javascript Driver (Rails UJS):
Because submitting forms with HTTP methods other than GET and POST
isn't widely supported across browsers, all other HTTP methods are
actually sent over POST with the intended method indicated in the
_method parameter. Rails automatically detects and compensates for this.
Rails does that with a JavaScript event handler attached to any link with the data-method attribute. But if you broke that functionality the browser will just perform its default action which is sending a GET request when the user clicks a link.
This problem usually boils down to one or more of:
Your javascript is throwing an error which halts script execution (use the browser console to find the error, make it suck less).
Rails UJS is not included in your assets pipeline or webpacker packs and thus not in the page.
The quick and easy solution to sidestep the problem is by using button_to which actually creates a form and does not require any JavaScript trickery. After all forms can send POST requests. And by just passing a _METHOD hidden field Rack will treat the request as any other HTTP verb.
button_to 'Something', '/some_path', method: :post
But in the long run you should probably fix the problem if you want to use any of the features of Rails UJS.
Your routes.rb is expecting a POST request to /orders/refresh routes, but apparently you are testing with a GET request.
Try changing your routes.rb:
Rails.application.routes.draw do
# ...
get '/orders/refresh', to: 'orders#refresh'
end
... or change your request to a POST request. If you are using Rails forms, you must do something like this:
form_with(url: "/orders/refresh", method: "post")
Ok, I think I figured it out.
It might be because when I hit /orders/refresh directly in my web browser, it will try to find a GET corresponding to the request.
I managed to make POST using a client like Postman, and everything works fine.
Thank you for your help !

Get current path name in Rails

I would like to be able to get the name of the current route in a request in Ruby on Rails. I've found ways I can access the controller and action of the request, but I would like to access a string or symbol of the name.
For example, if I have a users resource;
If I go to /users/1 I would like to be able to get users_path
If I go to /users/1/edit I would like to be able to get edit_users_path
I simply want to retrieve the name of the current route on a given request.
The following code will give it to you.
Rails.application.routes.recognize_path(request.request_uri)
Note that there are a couple of exceptions that can get thrown in ActionDispatch::Routing::RouteSet (which is what is returned from Rails.application.routes) so be careful about those. You can find the implementation of the method here.
You can use the following methods to get the current url in your action but none of them will give you the name of the route like users_path
request.original_url # => www.mysite.com/users/1
request.request_uri # = > /users/1
I had to use request.original_fullpath (Rails 5.2)
You can use the current_page? method to achieve this.
current_page?(user_path(#user))
current_page?(edit_user_path(#user))
Here's a link:
current_page? method

URL helper method uses what default :id?

I'm new to Rails and had a doubt regarding the link_to method. The second argument should be the url of the link, which one can generate using the url helper methods. However, one may need to pass :id as an argument of the helper method, which can be done by passing an object (which has :id as one of its attributes).
Well, in one case I did not pass the object to the method (in one of the views). However, the url was still able to obtain the correct :id (presumably using an instance variable defined earlier).
How did Rails choose a value for :id when I didn't even pass in any object?
Thanks a lot!
Edit
Here's the relevant code:
link_to 'Find Movies With Same Director', same_dir_path
Here, I am on a "show" page with url /movies/1. The same_dir_path is the helper method for the URL /movies/same_dir/:id where :id would be that of the passed object and movie#same_dir is the controller#action. Note I did not pass any object to the helper method link_to and yet, it takes the :id from the previous url ('1' in this case). The URL isn't even relative to the previous one (the path is different).
This is the controller method (same_dir):
def same_dir
#movies = Movie.find(params[:id])
if (#movies.director.nil? || #movies.director == '')
flash.keep
redirect_to movies_path
flash[:warning]="'#{#movies.title}' has no director info"
return
end
#otherMovies = Movie.find_all_by_director(#movies.director)
end
This is the routes.rb code:
match 'movies/same_dir/:id'=> 'movies#same_dir', :as => :same_dir
resources :movies
After reading your updated question I can provide you with a better answer:
Rails controllers can have default url options via the url_options method. (Doesn't seem to be a very documented feature. (here and here))
By default this method returns the parameters from the current request and that is where the id is coming from.
You can override it, too:
def url_options
{ my_parameter: my_value }.merge(super)
end
Original answer (might still be useful):
What you are witnessing is most likely a browser feature. For example this code:
link_to "Show", ""
generates this HTML code:
Show
If you click that link in a browser it navigates to the empty url relative to the current url, which is in fact equal to the current url.
Another example:
link_to "Publish", :publish
generates:
Publish
Now if your current url is http://localhost/articles/1/edit that link will take you to http://localhost/articles/1/publish. (Notice that the final url contains the model ID even though you are not having it in the HTML source)
In both cases your current model ID is preserved by the browser because you are using relative urls.
This behaviour might give you the illusion of some magical model ID detection, especially because browsers preview the final (=absolute) url when hovering over the link.
Have a look at the source, I'll bet your generated links do not contain any model IDs.

Custom path generator

I thought I knew how to override a path helper, but I'm not getting my expected behavior.
I tried adding something like this to my ApplicationHelper:
def post_path(post)
"/posts/#{post.id}/#{post.url}"
end
But for some reason, in one of my controllers when I try to use post_path(#post) it just returns the full url, something like /posts/4faddb375d9a1e045e000068/asdf (which is the current url in the browser) rather than /posts/4faddb375d9a1e045e000068/post-title-here.
In my routes file:
get '/posts/:id/:slug' => 'posts#show', :as => 'post'
The strange thing is if I use post_path(#post, #post.url), it works correctly. And if in a view I use post_path(#post) it works correctly. (#post.url returns the url friendly title)
In case you can't tell, I'm trying to eventually get the behavior similar to stackoverflow's where the url contains the id and a slug at the end and if the slug doesn't match the model with the given id, it'll redirect to the correct url.
What I'd try would be to put the whole def post_path in the application_controller.rb and make it a helper with helper_method :post_path. You'll get the best of both worlds.

How do can you make redirect_to use a different HTTP request?

At the end of one of my controller actions I need to redirect to a page that only accepts put requests. I have been trying to figure out how to get redirect_to to use a put request but to no success.
Is this possible? Or is there another way to accomplish this?
I don't think you are able to do this, and I suspect that the limitation is part of HTTP itself.
When using redirect_to - the redirection happens as a "302 Moved" header unless otherwise specified in the parameters.
Having a look at the HTTP Spec itself doesn't reveal any way to change the type of request the browser makes via redirect.
HTTP Redirects:
This class of status code indicates
that further action needs to be taken
by the user agent in order to fulfill
the request. The action required MAY
be carried out by the user agent
without interaction with the user if
and only if the method used in the
second request is GET or HEAD.
I think you may need to use JavaScript to achieve this functionality, or perhaps rethink the flow of control in your application.
If the action is in the same controller as where you're trying to redirect from, simply call the action and render the template like so:
def show
index
render :action => "index"
end
If it's not, then I don't know how you do that.
Ok, so I found a solution to my problem. I found a very good write up on the situation here. My implementation looks like this:
private
def redirect_post(redirect_post_params)
controller_name = redirect_post_params[:controller]
controller = "#{controller_name.camelize}Controller".constantize
# Throw out existing params and merge the stored ones
request.parameters.reject! { true }
request.parameters.merge!(redirect_post_params)
controller.process(request, response)
if response.redirected_to
#performed_redirect = true
else
#performed_render = true
end
end
Then I called this method like this:
redirect_post :controller => 'registrations', :action => 'order', :_method => 'put', :authenticity_token => params[:authenticity_token]
So I was able to 'fake' a put request by making a post request (using redirect_post) and then assigning 'put' to a _method param. If you look at a normal put request all it is a post from a form with a _method param. So its a bit hackish but it gets the job done.
Also, you have to make sure that when you call redirect_post the values of your hash are strings otherwise errors will be thrown.
You could redirect to a different page that issues the put request from the client, using Javascript.

Resources