I'm trying to setup slugs to have urls like this:
/artists/1/pink-floyd
My to_param method is:
def to_param
"#{self.id}/#{self.name.parameterize}"
end
Unfortunately url is /artists/1%2Fpink-floyd
How can I make this work the way I want?
You can do this a number of ways.
I think what would be the closest to what you want to achieve is something like:
def to_param
"#{self.id}-#{self.name.parameterize}"
end
Which makes:
/artists/1-pink-floyd
An alternative would be to change the routes:
get "/artists/:id/:parameterized_name" => "artists#show", as: :artist
This will give you params :parameterized_name in your controller action.
To use it in the view:
artist_path(artist, parameterized_name: artist.name.parameterize)
Related
I have a reports controller with several actions, one for each kind of report.
I was planning to have them route like this:
/reports/:report_type
I'd like the report_type string fragment to be used as the controller name so that I can have a single route to handle all of them, something like this:
get 'reports/:rpt_type' => "reports#:rpt_type"
...that would resolve to this, as an example:
get 'reports/song_performers' => 'reports#song_performers'
Is this possible, and if so, how?
Yes, it is. You could handle this in the controller, rather than the routes.rb file:
# reports_controller.rb
def show
send(params[:rpt_type])
end
private
def song_performers
# do stuff
end
def other_type
# do other stuff
end
# in routes.rb
get 'reports/:rpt_type', to: 'reports#show'
I use in my app to_param to create custom URL (this custom path contains slashes):
class Machine < ActiveRecord::Base
def to_param
MachinePrettyPath.show_path(self, cut_model_text: true)
end
end
The thing is, that since Rails 4.1.2 behaviour changed and Rails doesn't allow to use slashes in the URL (when use custom URL), so it escapes slashes.
I had such routes:
Rails.application.routes.draw do
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
resources :machines, except: :destroy do
collection do
get :search
get 'search/:ad_type(/:machine_type(/:machine_subtype(/:brand)))', action: 'search', as: :pretty_search
get ':subcategory/:brand(/:model)/:id', action: 'show', as: :pretty
patch ':subcategory/:brand(/:model)/:id', action: 'update' # To be able to update machines with new rich paths.
end
end
end
end
I tried by recommendation in the thread to use glob param just for show method to make sure it works:
resources :machines, except: :destroy do
#...
end
scope format: false do
get '/machines/*id', to: "machines#show"
end
But it absolutely doesn't work. I still have such broken links:
http://localhost:3000/machines/tractor%2Fminitractor%2Fmodel1%2F405
Of course, if I replace escaped slashes on myself:
http://localhost:3000/machines/tractor/minitractor/model1/405
And try to visit path, then page'll be opened.
Any ideas how can I fix that?
I've been having the same problem when using the auto-generated url helpers. I used a debugger to trace the new behavior to its source (somewhere around ActionDispatch::Journey::Visitors::Formatter), but didn't find any promising solutions. It looks like the parameterized model is now strictly treated as a single slash-delimited segment of the path and escaped accordingly, with no options to tell the formatter otherwise.
As far as I can tell, the only way to get the url helper to produce the old result is to use your original routes file and pass each segment separately, something like:
pretty_machine_path(machine.subcategory, machine.brand, machine.model, machine.id)
This is ugly as hell and obviously not something you'll want to do over and over. You could add a method to MachinePrettyPath to generate the segments as an array and explode the result for the helper (say, pretty_machine_path(*MachinePrettyPath.show_path_segments(machine))) but that's still pretty verbose.
Between the above headaches and the "You're Doing it Wrong" attitude from the devs in that Rails ticket you linked to, the simplest option for me was to bite the bullet and write a custom URL helper instead of using to_param. I've yet to find a good example of the "right" way to do that, but something like this bare-bones example should serve the purpose:
#app/helpers/urls_helper.rb
module UrlsHelper
def machine_path(machine, options = {})
pretty_machine_path(*MachinePrettyPath.show_path_segments(machine), options)
end
end
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper :urls #for the views
include UrlsHelper #for controllers
#...
end
If you're sure the returned url is safe you should add .html_safe to the returned string:
MachinePrettyPath.show_path(self, cut_model_text: true).html_safe
(didn't see anywhere else where it can be escaped but check all the flow in your app, maybe manually testing method by by method)
How about defining the url yourself?
def to_param
"#{ subcategory.title.parameterize }/#{ brand.name.parameterize }/#{ model.name.parameterize) }/#{ id }"
end
And then in your routes something like this:
get 'machines/*id', to: "machines#show"
You also have to split your params[:id] when you do a find on your model.
Machine.find( params[:id].split("/").last )
Lets say I have a Page resource, and a particular instance has id = 5 and permalink = foobar.
With resources :pages I can use <%= link_to #page.title, #page %> which outputs the url "/pages/5".
How would I make it output "/pages/foobar" instead? Likewise with the edit url... How do I make edit_page_path(#page) output "/pages/foobar/edit"?
UPDATE
Answers so far have said to override to_param in Page.rb which is a great start. +1 to each. But what if I want <%=link_to #page.title, #page%> to output "/:permalink" rather than "/pages/:permalink"?? I'll accept the answer that comes up with that.
You can override the to_param method in your model which will tell Rails what to use instead of your primary key for routing.
For example
class Page
def to_param
"#{self.id}-#{self.title.parameterize}"
end
end
The parameterize call makes your title URL friendly, you might also notice the use of self.id, this is recommended in case you have a duplicate title.
You need to overide to_param method in your model to return the field you want. Here's a blog post with some examples:
You want to use a permalink.
Add this to your model:
class Post
def to_param
"#{id}-{title}"
end
end
This assumes that you have a title.
Once you get this you want to look look up permalink-fu, or it's actually really simple to do your own with an after save:
class Post
before_save :manage_peramlink
def manage_peramlink
permalink = "#{name.gsub(/\s/, '_').gsub(/[^\w-]/, '').downcase}"
end
def to_param
"permalink"
end
end
Make sure you add peramlink as a field to your model.
I have models that looks like
search.rb:
id eg: 101
name eg: San Francisco
cars.rb
id
name
The search controller redirects user to cars.
search_controller.rb
if search.search_type=='cars'
redirect_to :controller=>'cars', :action=>'index', :id=>search
end
A query to find list of cars from San Francisco looks like:
http://localhost/cars?id=101
I overrode to_param method in search.rb like:
search.rb
def to_param
normalized_name = name.gsub(' ', '-').gsub(/[^a-zA-Z0-9\_\-\.]/, '')
"#{self.id}-#{normalized_name}"
end
This works to some extent:
It generates URLs that look like:
http://localhost/cars?id=101-San-Francisco
However, I would like is to generate a URL that looks like
http://localhost/cars/San-Francisco
How would I go about doing that?
Do I fix the routes?
Thanks for your help.
This can be tricky if you're not prepared for it. The method that controls how a model shows up in the params is pretty straightforward, as you've identified, but that's only the half of it. Later you'll have to retrieve it, so you need to set up for that:
class Search < ActiveRecord::Base
before_save :assign_slug
def self.from_param(id)
self.find_by_slug(id)
end
def to_param
self.slug
end
protected
def assign_slug
self.slug = self.name.gsub(' ', '-').gsub(/[^\w\-\.]/, '')
end
end
This requires adding a slug column to your Search model in order to make it possible to look up a search by it. It may be a good idea to add an index, possibly a unique one, to help retrieve these later in an efficient manner.
In your controller you don't use find but from_param instead to do the retrieval.
You'll also need to add a custom route that takes this parameter:
match '/cars/:search', :to => 'cars#index', :as => 'cars_search'
As a note you should use the route path generator methods whenever possible because multiple routes may match the same parameters. For instance:
redirect_to cars_search_path(:search => search)
I understand how to create a vanity URL in Rails in order to translate
http://mysite.com/forum/1 into http://mysite.com/some-forum-name
But I'd like to take it a step further and get the following working (if it is possible at all):
Instead of:
http://mysite.com/forum/1/board/99/thread/321
I'd like in the first step to get to something like this: http://mysite.com/1/99/321
and ultimately have it like http://mysite.com/some-forum-name/some-board-name/this-is-the-thread-subject.
Is this possible?
To have this work "nicely" with the Rails URL helpers you have to override to_param in your model:
def to_param
permalink
end
Where permalink is generated by perhaps a before_save
before_save :set_permalink
def set_permalink
self.permalink = title.parameterize
end
The reason you create a permalink is because, eventually, maybe, potentially, you'll have a title that is not URL friendly. That is where parameterize comes in.
Now, as for finding those posts based on what permalink is you can either go the easy route or the hard route.
Easy route
Define to_param slightly differently:
def to_param
id.to_s + permalink
end
Continue using Forum.find(params[:id]) where params[:id] would be something such as 1-my-awesome-forum. Why does this still work? Well, Rails will call to_i on the argument passed to find, and calling to_i on that string will return simply 1.
Hard route
Leave to_param the same. Resort to using find_by_permalink in your controllers, using params[:id] which is passed in form the routes:
Model.find_by_permalink(params[:id])
Now for the fun part
Now you want to take the resource out of the URL. Well, it's a Sisyphean approach. Sure you could stop using the routing helpers Ruby on Rails provides such as map.resources and define them using map.connect but is it really worth that much gain? What "special super powers" does it grant you? None, I'm afraid.
But still if you wanted to do that, here's a great place to start from:
get ':forum_id/:board_id/:topic_id', :to => "topics#show", :as => "forum_board_topic"
Take a look at the Rails Routing from the Outside In guide.
maybe try something like
map.my_thread ':forum_id/:board_od/:thread_id.:format', :controller => 'threads', :action => 'show'
And then in your controller have
#forum = Forum.find(params[:forum_id])
#board = #forum.find(params[:board_id])
#thread = #board.find(params[:thread_id])
Notice that you can have that model_id be anything (the name in this case)
In your view, you can use
<%= link_to my_thread_path(#forum, #board, #thread) %>
I hope this helps