I have the following route:
get "/:user_name/things/:thing_name" => "things#show", :as => "show_user_thing"
Thing belongs to user. So with just an instance of thing I have both parameters. However, when using the route helpers, I'm forced to specify each segment separately like so:
show_user_thing_path(#thing.user, #thing)
This sucks. I'd much rather do just this:
show_user_thing_path(#thing)
But how do I do this the 'route helper way'? I'd love to still use all the rails goodies for route's like these. Any ideas?
I feel your pain. In the cases where I uses the url helper a lot, I just write my own helper.
def show_thing_by_user_path(thing)
show_user_thing_path(thing.user, thing)
end
Of course you'd have to modify it slightly to include any options and formatting, but I think you get the idea of what I'm saying.
Related
Ruby on Rails 4.2+ only, please!
I've been looking all over for tips on how to make URLs pretty in Rails, and I'm struggling to see a solution I like.
What I want:
Hypothetical example: given Topic, Course, etc. models that have a bunch of fields (including URL-friendly slugs), I want to be able to
# ../routes.rb
# Match urls of the form /edu/material-engineering. These are read-only
# public URLs, not resources.
get 'edu/:slug', to: 'education#topic', as: :learn_topic
get 'edu/course/:id/slug', to: 'education#course', as: :learn_course
...
# I also have admin-only resource-oriented controllers for managing
# the content, but that's separate.
namespace :admin do
resource :topic
resource :course
...
end
# ../some_view.html.erb
# Generate URLS like this:
<%= link_to topic.name, learn_topic_path(topic) %>
<%= link_to course.name, learn_course_path(course) %>
What I don't want:
Messing with to_param. That's a dirty hack and completely breaks separation of concerns.
Resource/RESTful routing. There are no CRUD operations here other than "read."
link_to 'text', course_path(id: course.id, slug: course.slug). This completely defeats the purpose of not requiring views to know what params are required to generate a URL for a course.
EDIT: I know FriendlyId exists, but I'm precisely trying to understand how this sort of thing can be done and what the mechanics are, so that's not a solution for now.
There has to be a way to tell the named route helper topic_path(topic) to take the required parameters in the route (e.g, :slug, :id, whatever else) from the topic model object.
Anybody know? Thanks!
The best I've been able to come up with: just override the *_path helpers with my own implementation.
If you know a way to make the default helpers work, please chime in!
This problem boils down to one issue: the auto-generated *_path and *_url helpers don't give me the flexibility I want. What I want them to do is trivial, so without another option, I can just write my own:
module ApplicationHelper
def learn_topic_path(topic)
"/edu/#{topic.slug}"
end
...
end
Writing a few _path/_url helper overrides avoids all kinds of complication, and allows you to keep out of to_param, avoid including new plugins, etc.
One could probably go another step forward and generate the static components of the route from known routing rules, as well as infer what attributes one needed to extract from a model if the dynamic segment names line up to the model attribute names, but that starts to break down once you do more complicated things or add multiple models (e.g., 'edu/:topic_slug/:course_slug').
The big downside to doing this is that you now have to update routes in two places every time you change them: the route definition itself in routes.rb as well as the corresponding route helper in application_helper.rb. I can live with that for now.
You can use FriendlyId gem to achieve that.
Here's the link:
https://github.com/norman/friendly_id/blob/master/README.md
Let me know if you have questions.
I've got a model called ImplicitTest. It's called this as having a Ruby object called Test just breaks a lot of things in Rails.
However, I still want to expose it as a RESTful resource as test (e.g. /tests, /test/1/edit and so forth). Furthermore, it would be great to keep the controller as TestsController, although that's less important.
I was doing this by having simple resources :tests line in my routes.rb file, but this fails for the RESTful forms (e.g. <%= form_for #test ... > - this picks up that the #test object is of type ImplciitTest, and tries to lookup implicit_test_path which does not exist.
I tried adding form_for options, but came to the conclusion that to have the form work for both new and edit actions, there was no one single, unified way of asking form_for() to use a different prefix for the path name lookup.
So I've been trying to approach the problem from the routing side. Is there a line I can add to the routes file that will allow me to:
Have a model called ImplicitTest
Have the path as /test
Use the <%= form_for #test ... %> tag still
Keep the controller as TestsController (optional)
I know I am departing the Golden Path to do this, but Rails isn't letting me use Test as a model name, but this is the name users will expect to see in the URL for this resource, so I'm hoping there is are simple routing option(s) that enable this.
All you need to do is set the :path option on your route:
resources :implicit_tests, :path => '/test'
You would still use the standard implicit_tests_path helper this way, too, so your code doesn't have to diverge to alter the URL scheme.
Whilst looking at coreyward's answer, I stumbled across a shorter, but less intuitive method of getting what I need:
resources :tests, :as => "implicit_tests"
Are these essentially doing the same thing (given the extra :controller switch I added in the comments) ? Or is one preferred ?
I'd like to have a URL like this:
/payroll/region/1
and I'd like it to map to the Tasks Controller's payroll_list function. I'd also like to use REST. What's the best way to do this?
Many thanks!
Well I'd suggest you better go with the convention how Rails handles this. If you still insist on using such "strange" URLs and want to ignore the problems/headaches this can create during further development, then try to use Refraction.
I don't want to be rude but currently it seems to me that you did not understand why restful URLs are the way they are. Please do understand the design behind this first, then rethink your application/controller and routing design. I bet you will be enlighted.
In this example, your URL should probably be /regions/1/payrolls with map.resources :regions, :has_many => :payrolls. Then your payroll list would be rendered by the PayrollsController having a params[:region_id] - and that actually makes sense (and probably what you tried to achieve with your URL layout). Code snippet:
def index
if params[:region_id]
#region = Region.find(params[:region_id])
#payrolls = #region.payrolls
else
#payrolls = Payroll.all
end
end
If you still want to have a resource under a different named URL, use the following:
map.resources :regions do |regions|
regions.resources :tasks, :as => :payrolls
end
This will map the nested resources to the tasks controller using the named URL part "payrolls." But this probably does not work as you might expect because restful logic means you should handle the payroll model in the PayrollsController. Otherwise you might run into strange looking code. Maybe your design of the TasksController is just wrong? Rails will probably expect tasks to be handled over to your tasks controller although you name it payrolls. This can be confusing at least (however, it does not actually expect these being task models, so it will probably work).
BTW - Keep in mind: "restful" also means your application should answer to standard verbs on a resource, not just using "resourceful" routes. It's also about the GET, PUT, DELETE and POST http verbs, and of course the "edit", "new" etc default actions. Do not try to make your controllers big and complicated. Follow the motto "skinny controllers - fat models".
OK, so a better question, then might be this:
How can I get it so that I use your suggestion:
/regions/1/payroll
and have that map RESTfully to:
Tasks controller with index, new, etc that are prefixed by "payroll_"?
Like this: TasksController#payroll_index or TasksController#payroll_new
I'm using Rails 2.
I have resources nested like this:
- university_categories
- universities
- studies
- professors
- comments
I wish to use RESTful routes, but I don't want all that clutter in my URL. For example instead of:
/universities/:university_id/studies/:study_id/professors/:professor_id
I want:
/professors/:university_id/:study_id/:professor_id
(I don't map professors seperately so there shouldn't be a confusion between this and /professors/:professor_id since that route shouldn't exist).
Again, I want to use RESTful resources/routes...
Also note, I am using slugs instead of IDs. Slugs for studies are NOT unique, while other are. Also, there are no many-to-many relationships (so if I know the slug of a professor, which is unique, I also know which study and university and category it belongs to, however I still wish this information to be in the URI if possible for SEO, and also it is necessary when adding a new professor). I do however want to use shallow nesting for "administrator" URIs like edit, destroy (note the problem here with Study since it's slug is not unique, though)...
I would also like some tips on how to use the url helpers so that I don't have too much to fix if I change the routes in the future...
Thank you.
It doesn't seem like map.resources will provide you with this functionality, but you could use something like (untested)
map.show_professor "/professors/:university_id/:study_id/:professor_id", :controller => "professors", :action => "show"
and then similar routes for the other actions.
There might be a better solution, but this is the only way I can find that would work, since it seems map.resources assumes it is in the form of /resources/(:resource_id)
You can use REST this way, you just have to do all the actions yourself instead of using the shortcut.
As an example of an edit, you can just use
map.edit_professor "/professors/:id/edit", :controller => "professors", :action => "edit"
My application is in RoR
I have an action/view called showsummary where the ID has been passed into the URL, and the controller has used that to instantiate #vendor where #vendor.name is the name of a company.
I would like the URL to be, rather than showsummary/1/ to have /vendor-name in the URL instead.
How do I do that?
All of these solutions use find_by_name, which would definitely require having an index on that column and require they are unique. A better solution that we have used, sacrificing a small amount of beauty, is to use prefix the vendor name with its ID. This means that you dont have to have an index on your name column and/or require uniqueness.
vendor.rb
def to_param
normalized_name = name.gsub(' ', '-').gsub(/[^a-zA-Z0-9\_\-\.]/, '')
"#{self.id}-#{normalized_name}"
end
So this would give you URLs like
/1-Acme
/19-Safeway
etc
Then in your show action you can still use
Vendor.find(params[:id])
as that method will implicitly call .to_i on its argument, and calling to_i on such a string will always return the numerical prefix and drop the remaining text- its all fluff at that point.
The above assumes you are using the default route of /:controller/:action/:id, which would make your URLs look like
/vendors/show/1-Acme
But if you want them to just look
/1-Acme
Then have a route like
map.show_vendor '/:id', :controller => 'vendors', :action => 'show'
This would imply that that it would pretty much swallow alot of URLs that you probably wouldnt want it too. Take warning.
I thought I'd mention String#parameterize, as a supplement to the tagged answer.
def to_param
"#{id}-#{name.parameterize}"
end
It'll filter out hyphenated characters, replace spaces with dashes etc.
Ryan Bates has a great screencast on this very subject.
Basically you overload the to_param method in the Vendor model.
def to_param
permalink
end
Then when you look up the resource in your controller you do something like this:
#vender = Vender.find_by_name(params[:id])
But the problem with this is that you'll have to make sure that the vendors' names are unique. If they can't be then do the other solution that Ryan suggests where he prepends the the id to the name and then parses the resulting uri to find the item id.
You do this by modifying the routes that are used to access those URL's and changing them to use :name, rather than :id. This will probably mean that you have to write the routes yourself rather than relying on resources.
For instance add this to the routes.rb file:
map.with_options :controller => "vendor" do |vendor|
vendor.connect "/vendor/:name", :action => "show"
# more routes here for update, delete, new, etc as required
end
The other change that will be required is that now you'll have to find the vendor object in the database by the name not the id, so:
#vendor = Vendor.find_by_name(params[:name])
Internally (at least to my knowledge/experimentation) whatever parameter name is not specified in the URL part of the route (i.e. not within the "/Controller/Action/:id" part of it) is tacked on to the end as a parameter.
Friendly ID
http://github.com/norman/friendly_id/blob/26b373414eba639a773e61ac595bb9c1424f6c0b/README.rdoc
I'd have to experiment a bit to get it right, but there's two primary parts to the solution.
1) Add a route.
in config/routes, add a line that sends requests of the form baseurl/controller/:vendor-name to the action showsummary, (or maybe a new action, show_summary_by_vendor_name)
[also, if you planned on using baseurl/:vendorname, that's fine too]
For convenience, make sure the parameter is something like :vendor-name, not the default :id
2) Write the controller action.
In the controller file, either edit your showsummary action to differentiate based on whether it's called with an id or with a vendorname, or just write a show_summary_by_vendor_name. (depending on best practices, and what route you wrote in 1. I don't know off the top of my head which is preferable)
You can then do
#vendor = Vendors.find_by_name(params[:vendor_name])
or something like that, and then render it the way you would in regular showsummary.
3) Use that as the link.
Once you confirm that baseurl[/controller?]/vendor-name works, and shows the summary, make sure all the links in your application, and elsewhere, use that link. Off the top of my head, I can't remember how difficult it is to integrate a custom route into link_to, but I think it's doable. Most search engines [google] rely heavily on links, so good SEO will have you using those named links, not the numbered ones. I think. I don't know much about SEO.
Take a look also at this quck start to SEO for Rails