Teemu Leisti
Teemu Leisti

Reputation: 3770

helper path generated by Rails based on a custom REST method fails

wanted to define custom routes into certain resources in addition to the ones Rails defines by default. To do this, the relevant parts of my routes.rb file look something like this:

resource :top, only: [:show]
scope module: :top do
  resource :reso, only: [:show]
end

get 'foo_reso' => 'top/reso#foo'
get 'bar_reso' => 'top/reso#bar'

As you can see, I only want routes to ResoController's methods show, foo, and bar. This works, as rake routes gives:

    reso GET    /reso(.:format)           top/reso#show
foo_reso GET    /foo_reso(.:format)       top/reso#foo
bar_reso GET    /bar_reso(.:format)       top/reso#bar

This works: clicking on a link in the application that takes you to the route foo_reso does result in a call ResoController#foo, and the subsequent display of the associated view.

However, I thought that the route definition was just slightly ugly, and instead of defining the routes explicity, wanted Rails to generate them automatically by telling it that the resource has two additional REST methods, foo and bar (while still restricting the standard methods by means of the only: argument).

I followed the advice in this answer, and changed routes.rb to this:

resource :top, only: [:show]
scope module: :top do
  resource :reso, only: [:show] do
    member do
      get :foo
      get :bar
    end
  end
end

Now, rake routes gives:

    reso GET    /reso(.:format)           top/reso#show
foo_reso GET    /reso/foo(.:format)       top/reso#foo
bar_reso GET    /reso/bar(.:format)       top/reso#bar

Note the difference between the paths in the two cases: while the helper path and the controller#action are the same, the path has changed from /foo_reso(.:format) to /reso/foo(.:format).

(I have all the resource names defined as uncountable in config/initializers/inflections.rb, so I don't get automatic pluralization of the names, because in my application, each controller is associated with a particular screen, not with a model, so pluralization doesn't fit the picture. In this app, the REST methods are really more like function calls than operations on a resource, which is why I need a different set than the standard.)

Now, clicking on a link to foo_reso in the application results in a Rails routing error page that says:

No route matches [GET] "/foo_reso"

Any ideas on what I could do to fix the situation, aside from just using my original solution?

Added on edit, 2013-07-12:

As I note in a comment below, according to the output of rake routes, the helper route foo_reso matches the controller and method I want to call, top/reso#foo, and when I manually enter the matching URL (reso/foo) into the URL bar, that works as intended. However, trying to open the route foo_reso from within the application results in a No route matches [GET] "/foo_reso". foo_reso_path and foo_reso_url result in the same error.

What on earth can the problem be? Surely not a bug in Rails?

Added on edit, 2013-07-22:

To make the use case a bit clearer, the idea is that when the user presses a "Reset" button on a page, the controller's reset method is called, which clears the page's inputs and outputs. I have been abstracting the question by changing the actual identifier reset to foo. (reso and and a couple of other identifiers are also the result of "obfuscation".) I'll continue to use the same translations for consistency's sake, but the code below should be clearer when you bear in mind that foo is actually reset. (bar is something else, but it doesn't matter, since the routing problem is the same.)

To answer Thong Kuah's and John Hinnegan's questions, this is how I use foo_reso_path. The relevant parts of file reso_controller:

class Top::ResoController < Top::ResoSuperController

  # GET /reso
  def show
    ...
    @reset_path = "foo_reso"
    ...
  end # show

  # GET /foo_reso
  def foo
    perform_foo_action
    redirect_to reso_path
  end
  ...
end

The relevant parts of file app/views/top/reso/show.html.erb:

<%= render partial: "top/resosuper_inputs", locals: { the_form_path: reso_path } %>
...
<input type="hidden" id="reset_path" name="reset_path" value="<%= @reset_path %>">

The form includes the partial _resosuper_inputs.html.erb, whose relevant parts are:

<!-- For using the "Reset" button. -->
<%= javascript_include_tag "my_reset_form.js" %>
<%= simple_form_for :filtering_criteria, url: the_form_path, method: :get do |f| %>
  ... inputs elided ...
  <%= f.button :submit, value: "Search" %>
  <%= f.button :submit, type: 'button', value: "Reset"), id: 'reset_button' %>
<% end %>

resosuper is the superclass of two different resources, but I don't think that has any effect on the case. Anyway, here's the Javascript file app/assets/javascripts/my_reset_form.js:

$(document).ready(function() {
  /* Hang functionality on the "Reset" button. */
  $('#reset_button').click(function () {
      var reset_path = $('#reset_path').val();
      window.open(reset_path, "_self")
  });
})

It's in jQuery, and as the comment says, makes the "Reset" button open the path whose value has been stored in the hidden input variable with the id reset_path. That value is what was given to the form by Top::ResoController in the variable @reset_path, namely, "foo_reso".

Keep in mind that all this worked fine when I defined foo_reso in routes.rb like this:

get 'foo_reso' => 'top/reso#foo'

Also keep in mind that replacing foo_reso with foo_reso_path or with foo_reso_url in the assignment statement in ResoController made no difference.

The places where I use foo_reso are in a controller and in a view, so that shouldn't be a problem.

Upvotes: 0

Views: 2336

Answers (3)

Thong Kuah
Thong Kuah

Reputation: 3283

As posted in the description, here is the "usage".

# GET /reso
def show
...
  @reset_path = "foo_reso"
...
end # show

However, all the above is doing is just setting the @reset_path instance variable into a literal string `"foo_reso" (which coincidentally matches the old route)

What the poster really wants is :

 @reset_path = foo_reso_path

which will generate the right path /reso/foo into the instance variable @reset_path

Sidenote: The poster has done all the right things here to debug route problems. Most of the time, you can trust rake routes. Checking that one can access the route directly is good, and so is checking the usage of the route helper is correct too is crucial

Upvotes: 1

Teemu Leisti
Teemu Leisti

Reputation: 3770

OK, I found out the problem. As often turns out to be the case after digging into a seemingly mysterious bug, the solution was very simple. My reso_controller.rb was:

class Top::ResoController < Top::ResoSuperController

# GET /reso
def show
  ...
  @reset_path = "foo_reso"
  ...
end # show

I replaced the assignment line by this:

  @reset_path = foo_reso_path

without quotes around foo_reso_path. Now the "Reset" button works as intended.

In summary, the following don't work:

  @reset_path = "foo_reso"
  @reset_path = "foo_reso_path"
  @reset_path = "foo_reso_url"
  @reset_path = foo_reso

And the following do work:

  @reset_path = foo_reso_path
  @reset_path = foo_reso_url

Don't I feel silly now. Sorry for the waste of time.

Upvotes: 0

Benjamin Bouchet
Benjamin Bouchet

Reputation: 13181

Short answer: There is no automatic way in Rails to do so.

The reason is that RESTul URL relies basically on the format /controller/action. Meaning if your controller ResoController has 3 methods show, foo, and bar, a correct restful implementation will result in the URLS:

/reso
/reso/foo
/reso/bar

Rails can help you do achieve this, and the solution is using members in your routes, exactly the way you described in your question.

Using URLs such as /foo_reso or /bar_reso - in other words /action_controller is not Rails standard, and Rails base philosophy is "convention over configuration". So if you really need those you must declare them by hand

Upvotes: 0

Related Questions