Rails routes remove characters from URL - ruby-on-rails

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

Related

Rails 4 Route Parameters Defined by Keywords

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!

Custom parameters in URL for show action

I'm working on implementing a SEO-hiarchy, which means that I need to prepend parameters for a show action.
The use-case is a search site where the URL-structure is:
/cars/(:brand)/ => a list page
/cars/(:brand)/(:model_name)?s=query_params => a search action
/cars/:brand/:model_name/:variant/:id => a car show action
My problem is to make the show action URLs work without having to provide :brand, :model_name and :variant as individual arguments. They are always available from as values on the resource.
What I have:
/cars/19330-Audi-A4-3.0-TDI
What I want
/cars/Audi/A4/3.0-TDI/19330
Previously, this was how the routes.rb looked like:
# Before
resources :cars. only: [:show] do
member do
get 'favourize'
get 'unfavourize'
end
Following was my first attempt:
# First attempt
scope '/cars/:brand/:model_name/:variant' do
match ":id" => 'cars_controller#show'
match ":car_id/favourize" => 'cars_controller#favourize', as: :favourize_car
match ":car_id/unfavourize" => 'cars_controller#unfavourize', as: :unfavourize_car
end
This makes it possible to do:
cars_path(car, brand: car.brand, model_name: car.model_name, variant: car.variant)
But that is obviously not really ideal.
How is it possible to setup the routes (and perhaps the .to_param method?) in a way that doesn't make it a tedious task to change all link_to calls?
Thanks in advance!
-- UPDATE --
With #tharrisson's suggestion, this is what I tried:
# routes.rb
match '/:brand/:model_name/:variant/:id' => 'cars#show', as: :car
# car.rb
def to_param
# Replace all non-alphanumeric chars with - , then merge adjacent dashes into one
"#{brand}/#{model_name}/#{variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-')}/#{id}"
end
The route works fine, e.g. /cars/Audi/A4/3.0-TDI/19930 displays the correct page. Generating the link with to_param, however, doesn't work. Example:
link_to "car link", car_path(#car)
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>#<Car id: 487143, (...)>})
link_to "car link 2", car_path(#car, brand: "Audi")
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>"Audi", :model_name=>#<Car id: 487143, (...)>})
Rails doesn't seem to know how to translate the to_param into a valid link.
I do not see any way to do this with Rails without tweaking either the URL recognition or the URL generation.
With your first attempt, you got the URL recognition working but not the generation. The solution I can see to make the generation working would be to override the car_path helper method.
Another solution could be, like you did in the UPDATE, to override the to_param method of Car. Notice that your problem is not in the to_param method but in the route definition : you need to give :brand,:model_name and :variant parameters when you want to generate the route. To deal with that, you may want to use a Wildcard segment in your route.
Finally you can also use the routing-filter gem which make you able to add logic before and after the url recognition / generation.
For me, it looks like all theses solutions are a bit heavy and not as easy as it should be but I believe this came from your need as you want to add some levels in the URL without strictly following the rails behavior which will give you URL like /brands/audi/models/A3/variants/19930
OK, so here's what I've got. This works in my little test case. Obviously some fixups needed, and I am sure could be more concise and elegant, but my motto is: "make it work, make it pretty, make it fast" :-)
In routes.rb
controller :cars do
match 'cars', :to => "cars#index"
match 'cars/:brand', :to => "cars#list_brand", :as => :brand
match 'cars/:brand/:model', :to => "cars#list_model_name", :as => :model_name
match 'cars/:brand/:model/:variant', :to => "cars#list_variant", :as => :variant
end
In the Car model
def to_param
"#{brand}/#{model_name}/#{variant}"
end
And obviously fragile and non-DRY, in cars_controller.rb
def index
#cars = Car.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #cars }
end
end
def list_brand
#cars = Car.where("brand = ?", params[:brand])
respond_to do |format|
format.html { render :index }
end
end
def list_model_name
#cars = Car.where("brand = ? and model_name = ?", params[:brand], params[:model])
respond_to do |format|
format.html { render :index }
end
end
def list_variant
#cars = Car.where("brand = ? and model_name = ? and variant = ?", params[:brand], params[:model], params[:variant])
respond_to do |format|
format.html { render :index }
end
end
You just need to create two routes, one for recognition, one for generation.
Updated: use the routes in question.
# config/routes.rb
# this one is used for path generation
resources :cars, :only => [:index, :show] do
member do
get 'favourize'
get 'unfavourize'
end
end
# this one is used for path recognition
scope '/cars/:brand/:model_name/:variant' do
match ':id(/:action)' => 'cars#show', :via => :get
end
And customize to_param
# app/models/car.rb
require 'cgi'
class Car < ActiveRecord::Base
def to_param
parts = [brand,
model_name,
variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-'),
id]
parts.collect {|p| p.present? ? CGI.escape(p.to_s) : '-'}.join('/')
end
end
Sample of path helpers:
link_to 'Show', car_path(#car)
link_to 'Edit', edit_car_path(#car)
link_to 'Favourize', favourize_car_path(#car)
link_to 'Unfavourize', unfavourize_car_path(#car)
link_to 'Cars', cars_path
form_for(#car) # if resources :cars is not
# restricted to :index and :show
You want bounded parameters to be passed to url of which some parameters are optional and some of them strictly needs to be present.
Rails guides shows you can have strict as well as optional parameters and also you can give name to particular route in-order to simplify its usage.
Guide on rails routing
bound parameters
Example usage -
In below route,
brand is optional parameter as its surrounded by circular bracket
Also please note there can be optional parameters inside route but they needs to added at last /cars(/:brand)(/:make)(/:model)
match '/cars/(:brand)', :to => 'cars#index', :as => cars
here cars_url will map to index action of cars controller..
again cars_url("Totoya") will route index action of cars controller along-with params[:brand] as Toyota
Show url route can be as below where id is mandatory and others can be optional
match '/cars/:id(/:brand(/:model_name/)(/:variant)', :to => "cars#show", :as => car
In above case, id is mandatory field. Other parameters are optional.
so you can access it like car_url(car.id) or car_url(12, 'toyota') or car_url(12, 'toyota', 'fortuner') or car_url(12, 'toyota', 'fortuner', 'something else)

Simplest route with a variable and a string? -- Rails 3.1

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

Advanced Routing with Rails3

I want to use regular expressions inside my routes. I have an Products controller, but I want a different URL structure to access the products
http://host/this/
http://host/that/
http://host/andthat/
These URLs should call a action in my controller (Products:show_category(:category))
Is something like this possible?
match "(this|that|andthat)" => "products#show_category", :category => $1
the action should look like this
def show_category
puts params[:category] # <-- "this" if http://host/this/ is called
# ...
end
I haven't actually tested it, but try out:
match ':category' => 'products#show_category', :constraints => { :category => /this|that|andthat/ }
I'm not too sure if this answers your question, but you could add a collection to routes.rb:
resources :products do
collection do
get :category1
get :category2
get :category3
end
end
If you then run rake routes, you'll see that you have urls like /products/category1 and products/category2. Category1, 2 and 3 can be defined in your controller as usual:
def category1
#custom code here
end
def category2
#custom code here
end
def category3
#custom code here
end
As I said, I'm not too sure if that's what you're looking to do, but hope that helps a bit!

Adding extra params to rails route resources

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

Resources