I have a store model with the following:
def to_param
slug + "-info"
end
The urls will be like:
/dell-info
/ibm-info
/apple-info
My route for this is clearly wrong:
match '/:slug-info' => 'stores#info', :as => :stores
How can I fix this? If I use match '/:slug(-info)' as the route it works but matches BOTH /dell and /dell-info
You could add some constraints to the route and then strip off the "-info" in your controller:
match '/:slug' => 'stores#info', :as => :stores, :constraints => { :slug => /-info$/ }
and then, in your controller:
def info
slug = params[:slug].sub(/-info$/, '')
#...
end
Or better, have a method on your model that can remove the "-info" suffix while it looks up an object based on the slug:
# In the model
def self.for_slug(slug)
slug = slug.sub(/-info$/, '')
find_by_slug(slug)
end
# In the controller
def info
thing = Thing.for_slug(params[:slug])
#...
end
Related
We're trying to set up rails routes with the parameters separated by more then just forward-slash symbols.
As an example:
someexample.com/SOME-ITEM-for-sale/SOME-PLACE
For the following path we'd like to extract SOME-ITEM and SOME-PLACE strings as parameters whilst identifying which controller to run it all against with the "-for-sale/" part.
I've been playing with variations on :constraints => {:item => /[^\/]+/} constructs but without any success. Am I looking in the right place? Thanks!
UPDATE
In the end I went with this solution:
get ':type/*place' => 'places#index', as: :place , :constraints => {:type => /[^\/]+-for-sale/}
And then recovered the full "SOME-ITEM-for-sale" sting for parsing in the controller using
params[:type]
Hope that helps someone!
friendly_id is what you want:
#Gemfile
gem 'friendly_id', '~> 5.1.0'
$ rails generate friendly_id
$ rails generate scaffold item name:string slug:string:uniq
$ rake db:migrate
#app/models/item.rb
class Item < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
end
The above will give you a slug column, which FriendlyId will look up any requests you send to the app:
#config/routes.rb
resources :items, path: "" do
resources :places, path: "" #-> url.com/:item_id/:id
end
Although the params will still be id (unless you use the param option of resources, but FriendlyId will override both your routes and model to use the slug instead:
<%= link_to "Item Place", items_place_path(#item, #place) %> #-> url.com/item-name-information/place-name-information
Update
If you wanted to have a "dynamic" routing structure, you'll be able to use the following (this requires the history module of FriendlyId):
#config/routes.rb
#...
get '/:item_id/:place_id', to: SlugDispatcher.new(self), as: :item #-> this has to go at the bottom
#lib/slug_dispatcher.rb
class SlugDispatcher
#http://blog.arkency.com/2014/01/short-urls-for-every-route-in-your-rails-app/
##########################################
#Init
def initialize(router)
#router = router
end
#Env
def call(env)
id = env["action_dispatch.request.path_parameters"][:item_id]
slug = Slug.find_by slug: id
if slug
strategy(slug).call(#router, env)
else
raise ActiveRecord::RecordNotFound
end
end
##########################################
private
#Strategy
def strategy(url)
Render.new(url)
end
####################
#Render
class Render
def initialize(url)
#url = url
end
def call(router, env)
item = #url.sluggable_type.constantize.find #url.sluggable_id
controller = (#url.sluggable_type.downcase.pluralize + "_controller").classify.constantize
action = "show"
controller.action(action).call(env)
end
end
####################
end
This won't work out the box (we haven't adapted it for nested routes yet), but will provide you the ability to route to the appropriate controllers.
In the end we went with this solution:
get ':type/*place' => 'places#index', as: :place , :constraints => {:type => /[^\/]+-for-sale/}
The router command only gets activated if the :type parameter contains "-for-sale" in the string
And then we recovered the full "SOME-ITEM-for-sale" sting for parsing in the controller using
params[:type]
Hope that helps someone!
I have URLs like this
arizona/AZ12
colorado/CO470
I added the AZ and CO because friendly id wanted unique ids. Arizona and Colorado could have a unit 12.
I'd like to have URLs like
arizona/unit12
colorado/unit470
Seems like you could write something that removes the first two characters and replaces them. Would that be in the routes or controller?
My routes
resources :states, :except => [:index ], :path => '/' do
resources :units, :except => [:index ], :path => '/'
end
My controller
def show
#units = Unit.all
#states = State.with_units.group('states.id')
#state = State.all
#unit = Unit.friendly.find(params[:id])
end
Implement to_param method on your model. Rails will call to_param to convert the object to a slug for the URL. If your model does not define this method then it will use the implementation in ActiveRecord::Base which just returns the id.
class SomeModel
def to_param
"unit#{id}"
end
end
You can refer https://gist.github.com/agnellvj/1209733 for example
I have model looks like this :
class Listdomain < ActiveRecord::Base
attr_accessible :domainname, :permalink
before_create :make_it_permalink
validates_presence_of :domainname
private
def make_it_permalink
self.permalink = SecureRandom.base64(8)
end
end
Try to join id and permalink on params localhost:3000/1-oKPgT0s0S+U= and it's works
#routes.rb
match ':id' => 'homes#whoisresult', :as => :whois
#controller
def whoisresult
#listdomain = Listdomain.find(params[:id])
#domaintld = #listdomain.domainname
end
#model
def to_param
[id, permalink].join("-")
end
Try to use domainname on params localhost:3000/domainuser.com and it's works
#routes.rb
match ':domainname' => 'homes#whoisresult', :as => :whois, :constraints => { :domainname => /[^\/]+/ }
#controller
def whoisresult
#listdomain = Listdomain.find_by_domainname(params[:domainname])
#domaintld = #listdomain.domainname
end
I want join domain name and permalink using to_param like this :
localhost:3000/permalink-domainname
permalink : oKPgT0s0S+U=
domainame : domainuser.com
join with -
#routes.rb
match ':permalink' => 'homes#whoisresult', :as => :whois, :constraints => { :permalink => /[^\/]+/ }
#controller
def whoisresult
#listdomain = Listdomain.find_by_permalink(params[:permalink])
#domaintld = #listdomain.domainname # line 34
end
#model
def to_param
[permalink, domainname.downcase].join("-")
end
And I tried get => localhost:3000/oKPgT0s0S+U=-domainuser.com
I got an error message like this
undefined method `domainname' for nil:NilClass
app/controllers/homes_controller.rb:34:in `whoisresult'
How can I join domain name and permalink on to_param ?
#listdomain = Listdomain.find_by_permalink(params[:permalink])
Is returning an empty result. So check params[:permalink] to make sure you are passing the right permalink, as well check to see if you have a Listdomain in your database with that permalink.
I'd like my website to have URLs looking like this:
example.com/2010/02/my-first-post
I have my Post model with slug field ('my-first-post') and published_on field (from which we will deduct the year and month parts in the url).
I want my Post model to be RESTful, so things like url_for(#post) work like they should, ie: it should generate the aforementioned url.
Is there a way to do this? I know you need to override to_param and have map.resources :posts with :requirements option set, but I cannot get it all to work.
I have it almost done, I'm 90% there. Using resource_hacks plugin I can achieve this:
map.resources :posts, :member_path => '/:year/:month/:slug',
:member_path_requirements => {:year => /[\d]{4}/, :month => /[\d]{2}/, :slug => /[a-z0-9\-]+/}
rake routes
(...)
post GET /:year/:month/:slug(.:format) {:controller=>"posts", :action=>"show"}
and in the view:
<%= link_to 'post', post_path(:slug => #post.slug, :year => '2010', :month => '02') %>
generates proper example.com/2010/02/my-first-post link.
I would like this to work too:
<%= link_to 'post', post_path(#post) %>
But it needs overriding the to_param method in the model. Should be fairly easy, except for the fact, that to_param must return String, not Hash as I'd like it.
class Post < ActiveRecord::Base
def to_param
{:slug => 'my-first-post', :year => '2010', :month => '02'}
end
end
Results in can't convert Hash into String error.
This seems to be ignored:
def to_param
'2010/02/my-first-post'
end
as it results in error: post_url failed to generate from {:action=>"show", :year=>#<Post id: 1, title: (...) (it wrongly assigns #post object to the :year key). I'm kind of clueless at how to hack it.
Pretty URLs for Rails 3.x and Rails 2.x without the need for any external plugin, but with a little hack, unfortunately.
routes.rb
map.resources :posts, :except => [:show]
map.post '/:year/:month/:slug', :controller => :posts, :action => :show, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
application_controller.rb
def default_url_options(options = {})
# resource hack so that url_for(#post) works like it should
if options[:controller] == 'posts' && options[:action] == 'show'
options[:year] = #post.year
options[:month] = #post.month
end
options
end
post.rb
def to_param # optional
slug
end
def year
published_on.year
end
def month
published_on.strftime('%m')
end
view
<%= link_to 'post', #post %>
Note, for Rails 3.x you might want to use this route definition:
resources :posts
match '/:year/:month/:slug', :to => "posts#show", :as => :post, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
Is there any badge for answering your own question? ;)
Btw: the routing_test file is a good place to see what you can do with Rails routing.
Update: Using default_url_options is a dead end. The posted solution works only when there is #post variable defined in the controller. If there is, for example, #posts variable with Array of posts, we are out of luck (becase default_url_options doesn't have access to view variables, like p in #posts.each do |p|.
So this is still an open problem. Somebody help?
It's still a hack, but the following works:
In application_controller.rb:
def url_for(options = {})
if options[:year].class.to_s == 'Post'
post = options[:year]
options[:year] = post.year
options[:month] = post.month
options[:slug] = post.slug
end
super(options)
end
And the following will work (both in Rails 2.3.x and 3.0.0):
url_for(#post)
post_path(#post)
link_to #post.title, #post
etc.
This is the answer from some nice soul for a similar question of mine, url_for of a custom RESTful resource (composite key; not just id).
Ryan Bates talked about it in his screen cast "how to add custom routes, make some parameters optional, and add requirements for other parameters."
http://railscasts.com/episodes/70-custom-routes
This might be helpful. You can define a default_url_options method in your ApplicationController that receives a Hash of options that were passed to the url helper and returns a Hash of additional options that you want to use for those urls.
If a post is given as a parameter to post_path, it will be assigned to the first (unnassigned) parameter of the route. Haven't tested it, but it might work:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:year].is_a?Post
post = options[:year]
{
:year => post.created_at.year,
:month => post.created_at.month,
:slug => post.slug
}
else
{}
end
end
I'm in the similar situation, where a post has a language parameter and slug parameter. Writing post_path(#post) sends this hash to the default_url_options method:
{:language=>#<Post id: 1, ...>, :controller=>"posts", :action=>"show"}
UPDATE: There's a problem that you can't override url parameters from that method. The parameters passed to the url helper take precedence. So you could do something like:
post_path(:slug => #post)
and:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:slug].is_a?Post
{
:year => options[:slug].created_at.year,
:month => options[:slug].created_at.month
}
else
{}
end
end
This would work if Post.to_param returned the slug. You would only need to add the year and month to the hash.
You could just save yourself the stress and use friendly_id. Its awesome, does the job and you could look at a screencast by Ryan Bates to get started.
What I want to do seems simple, but might not be "proper"
let's say I have an image resource, and I manipulate the image based on the url. In the url I want to specify it's size and whether it's grayed, colored, or dimmed or some other condition.
currently I have a number of named routes that look like this.
map.gray_product_image "images/:product/:image/gray/:size.:format", :controller => 'images', :action => 'gray_product_image'
for me the trick is that if I created this useing Rails resources, I don't know how I would specify the :size, :format, or it's "color type".
I guess I would like to add a member route and specify my params like the following.
map.resources :products do |products|
products.resources :images, :member => {:gray_product_image => {':image/:size.:format' => :get}}
end
There are other times where I have wanted to added extra info to a resource route but didn't know how.
Any help would be greatly appreciated,
Thanks.
There's no good way to remove the controller/id part of a resource. The closest you're going to get through tricking ActionController with something like this:
map.resources :gray, :path_prefix => "/images/:product/:image_id/",
:controller => 'images', :requirements => {:colour => "gray"}
Which will produce routes like www.site.com/images/product/4/gray/1234.html with the following params hash:
params => {
:image_id => 4,
:id => 1234,
:colour => "gray",
:product => "product"
}
The format won't be passed explicitly but it will be available in the controller through the usually respond_to means.
Next you'll have to work some magic in controller to trick rails into doing what you want.
class ImagesController < ApplicationController
def show
#size = params[:id]
#image = Image.find(params[:image_id])
...
end
end
This actually works better as a filter so:
class ImagesController < ApplicationController
def initialize_colour
unless params[:colour].nil?
#size = params[:id]
#colour = params[:colour]
#image = Image.find(params[:image_id])
end
end
before_filter :initialize_colour, :except => [:index, :new, :create]
...
end
However to make good use of these routes, you're going to have to pass all those extra parameters to your url for calls. Like this:
gray_url(size, :image_id => #image.id, :product => product)
But helpers make that easy.
module ApplicationHelper
def easy_gray_url(image, size, product)
gray_url(size, :image_id => image.id, :product => product)
end
end
Check out the documentation for Resources. You'll find this:
The resources method accepts the
following options to customize the
resulting routes:
:requirements - Set custom routing parameter requirements; this is a hash of either regular expressions (which must match for the route to match) or extra parameters. For example:
map.resource :profile,
:path_prefix => ':name',
:requirements => { :name => /[a-zA-Z]+/, :extra => 'value' }
will only match if the first part is alphabetic, and will pass the parameter :extra to the controller.
I have realized that the way I want to represent my resources simply falls outside of the normal Rails resources, and that's ok. The problem I was really having was that each time added anther action and named route to get to what I wanted it felt wrong, I was repeating myself, both in my routes and in my actions.
I went back to simply creating my named routes, and spent a little more time in the controller so that I could keep my routes simple. Below is what I have now, and I am ok with it.
#routes.rb
map.with_options :controller => 'sketched_images', :action => 'show', :path_prefix => '/sketches', :name_prefix => 'sketched_', :color => 'grey' do |m|
m.style "styles/:style/:color/:size.:format"
m.design "designs/:design/:color/:size.:format"
m.product "products/:product/:color/:size.:format"
m.color_combo "colored_products/:color_combo/:size.:format"
end
class SketchedImagesController < ApplicationController
caches_page :show
before_filter :load_data
def show
#size = params[:size] || 100
respond_to do |wants|
wants.png
wants.jpg
end
end
private
def load_data
case
when params[:design]
#image = ClothingDesign.from_param(params[:design]).sketched_image
greyed
when params[:style]
#image = ClothingStyle.from_param(params[:style]).sketched_image
greyed
when params[:product]
#image = Product.from_param(params[:product]).sketched_images.first
greyed
when params[:color_combo]
#color_combo = ColorCombo.find_by_id(params[:color_combo])
#object = #color_combo.colorable
if #object.active? && !#object.sketched_images.blank?
#image = #object.sketched_images.first
colored
else
#image = #product.style.sketched_image
dimmed
end
end
end
def greyed
#blank = "#FFF"
#print = "#000"
#highlight = "#666"
end
def colored
#blank = "##{#color_combo.blank_color.value}"
#print = "##{#color_combo.design_color.value}"
#highlight = "##{#color_combo.highlight_color.value}" unless #color_combo.highlight_color.blank?
end
def dimmed
#blank = "#BBB"
#print = "#000"
#highlight = "#444"
end
end