Dynamic Custom Routing in Rails - ruby-on-rails

I don't want to use Rails routing conventions, as I want my urls to look a certain way. My model structure hierarchy is dynamic.
If I have these three paths referencing different books:
/book/chapter/page
/book/chapter/page/sentence
/book/page/sentence
Different books I'm storing in the DB (mongodb) have different hierarchies, but they all START with Book and END with Sentence, it's just the middle part that varies.
My current logic for Routes is to handle it all in a RoutesController:
Routes
get '*path', to: "routes#route"
Routes#route
def route
path = params[:path].split("/")
## Look up the book
book = Book.find_by(name: path[0])
## Get the book specific hierarchy, like ["books", "pages", "sentences"]
## or ["books", "chapters", "pages", "sentences"]
hierarchy = book.hierarchy
if path.length == hierarchy.length
## Since end of hierarchy is always sentence
## Here is want to redirect_to Sentence#Show
else
## Here I want to look up based on model specific hierarchy
## LOOKUP CONTROLLER: hierarchy[path.length-1]}", ACTION: Show
## eg: Chapter#show, Subchapter#show, Page#show, etc.
end
end
I cant do a simple redirect_to because I'm not using the config/routes file so it throws an error:
No route matches {:action=>"show", :controller=>"chapters", :path=>"books/chapters"}

I know you don't want to use the default routes, but you may be able to make them work.
scope ':bookName' do
scope '(:chapter)', :chapter => /c\d+/ do #we need to know if it's a chapter
scope '(:page)', :page => /p\d+/ do #or a page, c1 = chapter, p1 = page
resources :sentence
end
resources :page
end
resources :chapter
end
The () make a part of the path optional.

Related

Rails getting routes name in Route.rb with controller actions

I'm a real beginner of rails.
Can I get multiple routes from one controller + many actions?
For example,
resources :something
get "something#index", "something#show", "something#update"...etc.
I'm just curious if there is a command to get route name from the actions.
For example, in a controller named "pledges",
class PledgesController < ApplicationController
def home
end
def abc
end
def defg
end
def hijk
end
end
Can any commands get "pledges#home", "pledges#abc", "pledges#defg","pledges#hijk" ?
To add custom, "non-RESTful" routes to a resource, you could do the following:
resources :pledges do
collection do
get :foo
end
member do
put :bar
end
end
collection-defined routes will produce results against Pledge as a whole – think the index route.
member-defined routes will produce results against an instance of Pledge – think the show route.
This would produce the following routes for you:
foo_pledges GET /pledges/foo(.:format pledges#foo
bar_pledge PUT /pledges/:id/bar(.:format) pledges#bar
pledges GET /pledges(.:format) pledges#index
POST /pledges(.:format) pledges#create
new_pledge GET /pledges/new(.:format) pledges#new
edit_pledge GET /pledges/:id/edit(.:format) pledges#edit
pledge GET /pledges/:id(.:format) pledges#show
PATCH /pledges/:id(.:format) pledges#update
PUT /pledges/:id(.:format) pledges#update
DELETE /pledges/:id(.:format) pledges#destroy
You will have to define all of the custom actions, if there are not restful (but I would highly recommend that you follow the rest conventions). For example:
get 'pledges' => 'abc'
post 'pledges' => 'defg'
put 'pledges' => 'hijk

Rails 4. Different routing for few models (relatd and not)

I am now working on website SEO optimization and what I am required to do is proper routing for links to be very seo friendly. I have read lots of information about routing, but it messed up in my head and I stuck.
So I have Store model which belongs to StoreType model, to City model and District model + District belongs_to :city.
I need to have routes like this:
/stores/store_type_name/ - store_type 'show' action(list of stores by type)
/stores/city_name/store_type_name/ - store_type 'show' action(list of stores by city&type)
/stores/city_name/district_name/store_type_name/ - store_type 'show' action(list of stores by city&district&type)
/stores/city_name/store_type_name/store_name - store 'show' action
The only solution I came up with for now is:
Routes.rb
namespace :stores do
get ':transliterated', to: 'store_types#show'
get ':transliterated/:name_en', to: 'store_types#city'
get ':transliterated/:name_en/:id', to: 'store_types#district'
end
With controller like this:
def district
#store_type = StorerType.find_by_transliterated(params[:transliterated])
#city = City.find_by_name_en(params[:name_en])
#district = District.find_by_id(params[:id])
if #store_type && #city && #district
stores = #store_type.stores.where(city_id:#city.id)
#stores = stores.where(district_id:#district.id)
else
redirect_to root_path
end
end
That works well but 1) I can not now add route for last example(store show page) as route is looking for :transliterated params in that namespace and redirects if record is not found. 2) I understand that this solution is bad and can be done much better, I just do not know how. Give me an advice please.
PS. Actually there is routing implemented on the site already so I am looking for the solution for those 4 urls listed above only, without touching anything else there.
Resourceful
Firstly, let me define the basis of all your routing for you...
Rails' routing structure is known as being resourceful - meaning based around resources / objects. As with Ruby being an object-orientated language, Rails is an object-orientated framework; the routes are no exception to this:
This means anything you do with your routes has to be resource-based, as follows:
#config/routes.rb
namespace :stores do
resources :store_types, only: [:show], path: "" do #-> domain.com/stores/:id -> store_types#show
get :name_en, action: :city #-> domain.com/stores/:store_type_id/:name_en
get :name_en/:id, action: :district #-> domain.com/stores/:store_type_id/:name_en/:id
end
end
This will give you the ability to send the traffic directly to your store_types controller without having all sorts of crazy routes all over the place
--
friendly_id
Something else to consider is a gem called friendly_id
friendly_id basically allows you to define / call routes with slugs, rather than ids. The difference is that the routes remain the same - it's the data, and the handling of that data, which changes
Typically in Rails, you'll create routes like this: domain.com/controller/:id
When you send people to links, they'll hit domain.com/controller/1 for example. Friendly_ID basically facilities the ability to send people to domain.com/controller/your_name, handling it in exactly the same way as you would with an ID:
#app/models/your_model.rb
Class YourModel < ActiveRecord::Base
friendly_id :name, use: [:slugged, :finders]
end
This will allow you to call:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
def show
#model = Model.find params[:id]
end
end
You can use some static strings in the urls to help to identify the actions, for examples:
namespace :stores do
get 'type/:transliterated', to: 'store_types#show'
get 'type/:transliterated/city/:name_en', to: 'store_types#city'
get ':transliterated/:name_en/:id', to: 'store_types#district'
end

Controller Best Practice

I have a Cards Controller where i need to set up categories. Because the views for this Controller would get pretty heavy to oversee i divided everything in folders.
routes.rb
resources :cards do
collection do
get 'druid'
get 'hunter'
get 'mage'
get 'paladin'
get 'priest'
get 'rogue'
get 'shaman'
get 'warlock'
get 'warrior'
get 'free'
get 'common'
get 'rare'
get 'epic'
get 'legendary'
get 'spell'
get 'minion'
get 'weapon'
get 'beast'
get 'deamon'
get 'dragon'
get 'murloc'
get 'pirate'
get 'totem'
end
end
View Folders:
Views ->
cards ->
class ->
druid.html.erb
hunter.html.erb
mage.html.erb
paladin.html.erb
priest.html.erb
rogue.html.erb
shaman.html.erb
warlock.html.erb
warrior.html.erb
rarity ->
free.html.erb
common.html.erb
rare.html.erb
epic.html.erb
legendary.html.erb
type ->
spell.html.erb
minion.html.erb
weapon.html.erb
race ->
beast.html.erb
deamon.html.erb
dragon.html.erb
murloc.html.erb
priate.html.erb
totem.html.erb
Now i don't think this is such a good Idea, but as for now i don't know any better way of doing it..
My messy controller will look like this:
def druid
render 'cards/class/druid'
end
def hunter
render 'cards/class/hunter'
end
def mage
render 'cards/class/mage'
end
def paladin
render 'cards/class/paladin'
end
etc...
Now... This list will get pretty long...
Is there a better way of dealing with this ???
A remark first: in your example (which I suppose is a simplified version of your application), your controller is just firing up the view. If this is correct, the pages could as well be totally static (pure HTML) and served statically.
Now, I think you should have more resources there: class, rarity, type and race could be resources by themselves, with the different "values" being the pages. After all (for what I can infer with my RPG knowledge), those are the elements of your domain model, so they should work great as resources.
The folders are already like that, so this could give something like: (warning, pseudo code out of my head)
resources :classes do # ClassesController
collection do
get 'druid'
get 'mage'
end
end
resources :rarity do # RarityController
collection do
get 'rare'
get 'common'
end
Finally, never forget that controllers & the routing file are just ruby code. You can make loops there:
cards_list = ['rogue', 'druid', ...]
resources :cards do
collection do
cards_list.each do |card_name|
get card_name
end
end
end
end
This would work for the version by resource above too.
Some metaprogramming could achieve the same on your controller (if you have nothing different between the various action methods).
UPDATE: 04/24/2014
Based on your absolute need to maintain separate views I would define the routes like this:
resources :cards do
collection do
get :cards, path: '/cards/:arg1'
end
end
OR
resources :cards do
collection do
get :cards_by_class, path: '/cards/class/:class'
get :cards_by_race, path: '/cards/race/:race'
#etc...
end
end
In your controller:
def index
template, #cards = get_cards
# you need to determine the path to the views based on params[:arg1]
render template
end
private
# These constants could be defined in the Card model itself
CLASS_TYPES = %w(druid hunter mage paladin) # etc.
RARITIES = %w(free common rare epic legendary)
WEAPON_TYPES = %w(spell minion weapon)
RACE_TYPES = %w(beast demon dragon) # etc.
def get_cards
path = ""
cards = nil
case params[:arg1]
when CLASS_TYPES
# Substitute 'classtype' with the proper column name
cards = Card.where("classtype = '#{params[:arg1]}'")
path = "/cards/class/#{params[:arg1]}"
when RARITIES
# Substitute 'rarity' with the proper column name
cards = Card.where("rarity = '#{params[:arg1]}'")
path = "/cards/rarity/#{params[:arg1]}"
when WEAPON_TYPES
# Substitute 'weapon_type' with the proper column name
cards = Card.where("weapon_type = '#{params[:arg1]}'")
path = "/cards/type/#{params[:arg1]}"
when RACE_TYPES
... # you get the idea
end
return path, cards
end
To be more accurate, I would need to see your schema for the Card model.
Although Martin's solution would surely work just fine, I would probably go in this direction, and avoid hard-coding the different types, classes, etc:
# This sets up routes for :new, :create, :show, :update, :delete, :edit, :index
resources :cards
If you want to get a list of 'druid' cards in your spec/test:
get :index, {kind: 'druid'}
My controller method would look similar to this:
def index
#cards = Card.where("kind = #{params[:kind]}")
render "cards/class/#{params[:kind]}"
end
You could even make the view a single ERB or HAML template that may be generic enough to handle the different types of attributes. In that case you would get rid of, or change, the 'render' line above and let it default to 'index' or 'cards/class/index.html.erb' or whatever.
This is just off the top of my head, but I would tend to avoid concrete-coding the attributes, and instead treat them like properties of a 'card'. After all, if the 'card' is the resource, then all of the races, rarity, class, etc. are just attributes of a card.
Hopefully this all makes sense. Let me know if I can clarify further. The 'kind' attribute maybe something different in your DB schema model of the 'Card'.

Rails route scope default based on current_user

I have a route something like:
scope ":department", department: /admin|english|math/, defaults: { department: 'admin' }
Is it possible to make the default department for this route to be based on the current_user.department.name?
If this is not possible, what is another way of solving my problem. The problem is that I want all links to default to the current scope unless otherwise noted. I'm currently doing the following in LOTS of places:
# links to /math/students
link_to 'students', students_path(department: current_user.department.name.downcase)
If I understand correctly, what you want is to be able to write:
link_to 'students', students_path
and have the department option automatically set based on the current user.
Here's a solution that's like some of the others that have been offered: define helpers for each route that requires a department. However, we can do this programmatically.
Here we go:
app/helpers/url_helper.rb
module UrlHelper
Rails.application.routes.routes.named_routes.values.
select{ |route| route.parts.include?(:department) }.each do |route|
define_method :"department_#{route.name}_path" do |*args|
opts = args.extract_options!
if args.size > 0
keys = route.parts - [:department]
opts.merge!(Hash[*keys.zip(args).flatten])
end
opts.reverse_merge!(department: current_user.department.name.downcase)
Rails.application.routes.url_helpers.send(:"#{route.name}_path", opts)
end
end
end
You now have helper methods like department_students_path for every route that has a :department path segment. These will work just like students_path -- you can pass in opts, you can even set the :department explicitly and it will override the default. And they stay up to date with changes to your routes.rb without you having to maintain it.
You might even be able to name them the same as the original helpers, i.e.,
define_method :"#{route.name}_path"
without having to prefix them with department_--I didn't do that because I'd rather avoid naming collisions like that. I'm not sure how that would work (which method would win the method lookup when calling it from a view template), but you might look into it.
You can of course repeat this block for the _url helper methods, so you'll have those in addition to the _path ones.
To make the helpers available on controllers as well as views, just include UrlHelper in your ApplicationController.
So I think this satisfies your criteria:
You can call a helper method for paths scoped to :department which will default to the current_user's department, so that you don't have to explicitly specify this every time.
The helpers are generated via metaprogramming based on the actually defined named routes that have a :department segment, so you don't have to maintain them.
Like the built-in rails url_helpers, these guys can take positional args for other path segments, like, department_student_path(#student). However, one limitation is that if you want to override the department, you need to do so in the final opts hash (department_student_path(#student, department: 'math')). Then again, in that case you could always just do student_path('math', #student), so I don't think it's much of a limitation.
Route contraints can accept a proc, so you could solve this problem with the following "hack":
concern :student_routes do
# all your routes where you want this functionality
end
# repeat this for all departments
scope ":department", constraints: lambda{|req| req.session[:user_id] && User.find(req.session[:user_id]).department.name.downcase == "english"}, defaults: { department: 'english' } do
concerns :student_routes
end
Route concerns is a feature of rails 4. If you don't use rails 4, can you get the feature with this gem: https://github.com/rails/routing_concerns.
You can also just copy all the routes for students to all of the scopes.
You could use
link_to 'students', polymorphic_path([current_user.department.name.downcase, :students])
This will call
math_students_path
So to work I suppose you should add in your routes
scope ':department', as: 'department' do
...
as key generates helpers of the form: department_something_path
As that line is too long you could extract that to a helper
# students path taking into account department
def students_path!(other_params = {}, user = current_user)
polymorphic_path([user.department.name.downcase, :students], other_params)
Then you will use in your views
link_to 'students', students_path!
What about using nested routes? You could implement this with:
resources :departments do
resources :students
end
If you want to use a friendly url (like "math" instead of an id) can you add the following to the department model:
# in models/department.rb
def to_param
name.downcase
end
Remember name have to be unique for each department. Ryan B have made a railscast about this. You could also use a gem like friendly id.
You can now add the following to your views:
# links to /math/students
link_to 'students', department_students_path(current_user.department)
If you use this often, then create a helper:
# helpers/student_helpers.rb
def students_path_for_current_user
department_students_path(current_user.department)
end
# in view
link_to 'students', students_path_for_current_user
I ended up specifying it in the ApplicationController with:
def default_url_options(options={})
{department: current_user.department.name}
end
The path helpers will fill have the :department parameter filled in automatically then...

Accessing a resource in routes.rb by using attributes other than Id

I have the following in my routes.rb
map.resources :novels do |novel|
novel.resources :chapters
end
With the above defined route, I can access the chapters by using xxxxx.com/novels/:id/chapters/:id.
But this is not what I want, the Chapter model has another field called number (which corresponds to chapter number). I want to access each chapter through an URL which is something like
xxxx.com/novels/:novel_id/chapters/:chapter_number. How can I accomplish this without explicitly defining a named route?
Right now I'm doing this by using the following named route defined ABOVE map.resources :novels
map.chapter_no 'novels/:novel_id/chapters/:chapter_no', :controller => 'chapters', :action => 'show'
Thanks.
:id can be almost anything you want. So, leave the routing config untouched and change your action from
class ChaptersControllers
def show
#chapter = Chapter.find(params[:id])
end
end
to (assuming the field you want to search for is called :chapter_no)
class ChaptersControllers
def show
#chapter = Chapter.find_by_chapter_no!(params[:id])
end
end
Also note:
I'm using the bang! finder version (find_by_chapter_no! instead of find_by_chapter_no) to simulate the default find behavior
The field you are searching should have a database index for better performances

Resources