rails - how to make nested route with ancestry - ruby-on-rails

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

Related

Rails - Multiple Models with root sluggable routes

I'm looking for advice to find the ideal solution that fits within Rails' best practises.
My app has 5 models:
Category
Place
Division
Subdivision
Item (polymorphic)
A Place belongs to a Category. A Division belongs to a Place. A Subdivision belongs to a Division, and an Item can belong to a Place, Division OR Subdivision.
Here is my routes.rb
resources :categories, path: ''
get ':category/:place', to: 'places#show', as: :place
get ':category/:place/:item', to: 'items#show', as: :place_item
get ':category/:place/:division', to: 'divisions#show', as: :division
get ':category/:place/:division/:item', to: 'items#show', as: :division_item
get ':category/:place/:division/:subdivision', to: 'subdivisions#show', as: :subdivision
get ':category/:place/:division/:subdivision/:item', to: 'items#show', as: :subdivision_item
The issue is that if I type in the URL:
/category/place/divison
The error is "could not find ITEM".
I am currently using friendly_id for the slugs, is there a best method for introducing a new model "Slug" that would handle the slugs for all 5 of these models?
Potentially something like this? (Haven't tested)
get '/*category_id', :controller => 'slugs'
Here's item_controller:
def show
end
def show_place_item
render "show"
end
def show_division_item
render "show"
end
def show_subdivision_item
render "show"
end
private
def set_item
#item = Item.friendly.find(params[:id])
end
def item_params
params.require(:item).permit(:name, :slug, :category_id)
end

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!

Rails 4 - routing actions for contact form

I have two actions in the controller:
def report
#user = User.find_by_slug(params[:slug])
end
def reportForm
#user = User.find_by_slug(params[:slug])
Thread.new do
mail = ...
end
#message = 'Thanks!'
end
and in routes:
# User report form
get "/user/:slug/report", to: "users#report"
# Catch report form and action
post "/user/:slug/report", to: 'users#reportForm'
And the view:
<form method="POST" action="/user/<%= #user.slug %>/reportForm">
...
But the problem is, that when I send the form, the action reportForm is not called and instead of that is only refresh the current page with the form.
What's wrong here?
Thank you guys.
Form Helpers
The first thing that's wrong is you're not using the form helpers that Rails provides - this is a problem because you'll end up with niggly little problems like the one you're receiving:
#config/routes.rb
resources :users do
get :report #-> domain.com/users/:id/report
post :reportForm #-> domain.com/users/:id/reportForm
end
#view
<%= form_tag user_reportForm_path(#user) do %>
...
<% end %>
Routes
The second issue you have is to do with your routes
You've set the following routes:
get "/user/:slug/report", to: "users#report"
post "/user/:slug/report", to: 'users#reportForm'
This means you've got to send the request to domain.com/user/user_slug/report. Your form sends the URL to reportForm...
You should see my routes above for the solution to this problem
But more importantly, you should read up on nested resources:
#config/routes.rb
resources :users do
match :report, action: "reportForm", via: [:get, :post] #-> domain.com/users/:id/report
end
Slug
Finally, you're trying to use params[:slug] in your controller
With the resourceful routes you should be using in Rails, you'll be passing params[:id] most of the time. This should not be an issue (what is contained in params[:id] can be anything).
I would highly recommend looking at a gem called friendly_id, which makes including slugs in your application a lot simpler:
#app/models/user.rb
Class User < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: [:slugged, :finders]
end
This will allow you to call:
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def reportForm
User.find params[:id] #-> will use either `id` or `slug`
end
end

Rails routes remove characters from URL

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

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

Resources