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!
Related
I'm trying to install the contact page on my Ruby on Rails app. It seems straight forward enough, but after installing the mailer gems, and creating my controller with:
$ rails generate controller contact_form new create
I navigate to my contact URL (/contact_form/new), and it says
"Unable to autoload constant ContactFormController, expected
/home/ubuntu/workspace/app/controllers/contact_form_controller.rb to
define it"
Routes and controller are as follows:
routes.rb
get 'contact_form/new'
get 'contact_form/create'
resources :contact_forms
contact_form_controller.rb
class ContactFormsController < ApplicationController
def new
#contact_form = ContactForm.new
end
def create
begin
#contact_form = ContactForm.new(params[:contact_form])
#contact_form.request = request
if #contact_form.deliver
flash.now[:notice] = 'Thank you for your message!'
else
render :new
end
rescue ScriptError
flash[:error] = 'Sorry, this message appears to be spam and was not delivered.'
end
end
end
contact_form.rb
class ContactForm < MailForm::Base
attribute :name, :validate => true
attribute :email, :validate => /\A([\w\.%\+\-]+)#([\w\-]+\.)+([\w]{2,})\z/i
attribute :message
attribute :nickname, :captcha => true
# Declare the e-mail headers. It accepts anything the mail method
# in ActionMailer accepts.
def headers
{
:subject => "My Contact Form",
:to => "your_email#example.org",
:from => %("#{name}" <#{email}>)
}
end
end
Note that your class is named ContactFormsController and Rails is looking for ContactFormController. You need to pay careful attention to the pluralization in Rails.
Controllers are plural.
Models are singular.
Routes are plural in most cases.
The pluralization of classes must always match the file name.
So why is Rails looking for ContactFormController? Because your routes are not defined properly:
get 'contact_form/new'
get 'contact_form/create'
get 'contact_forms/new' is the proper route for a form to create a new resource. You don't create resources with GET so get rid of get 'contact_form/create'.
resources :contact_forms
Is actually all that you need.
So to fix this error you should:
rename contact_form_controller.rb -> contact_forms_controller.rb.
change your route definition.
request /contact_forms/new instead.
How can one create a url in this form /:category/:product/:item/ from a link_to method.
I have tried this:
parameters: {category: #category, product: #product, item: #item}
= link_to "list", url_for(controller: "items", action: "list", params: parameters),
class: "button radius #{ additional_classes }", rel: "canonical"
But it gives me this url: /list?product=a&category=b&item=c. I am really interested in getting the other format
PS: I am using slim but the way not HTML
I managed to solve this issue by doing a merge:
parameters: {category: #category, product: #product, item: #item}
= link_to "list", url_for(controller: "items", action: "list").merge(parameters),
class: "button radius #{ additional_classes }", rel: "canonical"
Do this:
#config/routes.rb
resources :categories, path: "" do
resources :product, path: "" do
resources :item, path: ""
end
end
This will create the following url: url.com/:category_id/:product_id/:id
You'll be able to use the following link_to helper:
<%= link_to #item.title, [#category, #product, #item] %>
--
Although it should work, it's not recommended:
Resources should never be nested more than 1 level deep.
Therefore, you may wish to refactor your routes/infrastructure so that you're calling item_id above all -- product & categories acting as index methods:
#config/routes.rb
resources :categories, path: "", only: :index
resources :products, only: :index do
resources :items
end
This would allow you to to call <%= link_to "Products", products_path %> to get a list of products, and then <%= link_to #item.id, products_item_path(#product, #item) %> to get a specific item.
You may also wish to look at friendly_id
This will do nothing to your routes (it resides in your model) -- it basically allows you to use slugs to identify your resources (rather than id's).
I can update the answer if you'd like to know more about it; basically it will allow you to have urls like:
url.com/:category_id/:product_id/:item
url.com/category-name/product-name/item-name
Friendly ID
Friendly ID just replaces the :id parameter of your routes / find methods with the slug param. To set up friendly_id, you basically need to add a slug attribute to your model (with a migration), and then include the FriendlyID class on the model itself.
Here's how do you'd set it up:
# config/routes.rb
resources :categories, path: "" do
resources :subcategories, path: "" do
resources :products, path: "" # => http://url.com/:category_id/:subcategory_id/:product_id
end
end
It must be noted that nesting the resourceful routes that deeply is not recommended (I'll leave it to demonstrate what to do).
Rails will take the above and create a route helper such as category_subcategory_product_path(:category, :subcategory, :product).
By passing the objects for each of those values, "naked" Rails will just infer the id from them. FriendlyID (if set up correctly) should replace the id with the slug column in all of those models.
You need to set it up like the following:
# app/models/category.rb
class Category < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:finders, :slugged]
end
# app/models/subcategory.rb
class Subcategory < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:finders, :slugged]
end
# app/models/product.rb
class Product < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:finders, :slugged]
end
This - if you migrate each of those models to include a slug column - will allow Rails to populate the route helper with the slug object. Of course, that doesn't matter if you just use the naked values anyway:
category_subcategory_product_path(#category.slug, #subcat.slug, #product.slug)
The trick is to handle the slug in the controller.
You can pass any value you want through the route helpers (there is no validation). The difference lies in how you manage the passed data in the controllers:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def show
#product = Product.find params[:id] #-> FriendlyID will automatically look up the slug if present
end
end
Try change your routes like this :
get '/:category/:product/:item/', 'items#list', :as => 'item_list'
So you will get item_list_path and you can use it in link like this.
= link_to "list", item_list_path(#category, #product, #item)
But be sure to pass params in same order as they are in url.
I have a single model:
class Page < ActiveRecord::Base
has_ancestry
validates :slug, :name, uniqueness: true, presence: true
before_validation :generate_slug
def to_param
slug
end
def generate_slug
self.slug = Russian.translit(name).parameterize
end
end
and I'm using ancestry gem to create tree of pages and subpages, i.e. page can have multiple sub-pages and sub-pages can also have multiple sub-pages, and so on to infinity.
But my problem is that I can't make something is /page-1/page-1-2/page-1-2-1. All sub-pages have a URL is: /page-1-2 or /page-1-3-1.
My routes.rb:
Rails.application.routes.draw do
get '/pages' => 'pages#index'
resources :pages, path: "", path_names: { new: 'add' }
root 'pages#index'
end
How to make nested URL?
Thanks!
As far as I know there's no neat way of capturing nested tree structured routes with dynamic permalinks, you can create a named route to capture pretty nested pages path:
get '/p/*id', :to => 'pages#show', :as => :nested_pages
Also, make sure you update slug of your page object to have nested urls, i.e.: append parent pages' slug to it. For example:
page1.slug = '/page-1'
page2.slug = '/page-1/page-2' # page2 is a child of page1
page3.slug = '/page-1/page-2/page-3' # page3 is a child of page2
So, to make this work, you can probably change generate_slug method in your Page model class:
def generate_slug
name_as_slug = Russian.translit(name).parameterize
if parent.present?
self.slug = [parent.slug, (slug.blank? ? name_as_slug : slug.split('/').last)].join('/')
else
self.slug = name_as_slug if slug.blank?
end
end
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 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