Custom params in route resources

Anyone who knows me will tell you I’m a huge rails fanboy. There are times when you get stuck. A name can’t be changed because it’s something the framework decided on. I ran into this recently when working with nested routes.

The problem

Working at The Flatiron School has been awesome. I’ve gotten to work on some great projects. While working on a Progress Dashboard I came across this code for creating nested routes. WALL OF CODE COMING.

In my routes file

1
2
3
4
resources :dashboards, only: [:show, :index] do
  resources :assignments, only: [:show]
  resources :students, only: [:index, :show]
end

Generated routes

1
2
3
4
5
dashboard_assignment GET        /dashboards/:dashboard_id/assignments/:id(.:format) assignments#show
  dashboard_students GET        /dashboards/:dashboard_id/students(.:format)        students#index
   dashboard_student GET        /dashboards/:dashboard_id/students/:id(.:format)    students#show
          dashboards GET        /dashboards(.:format)                               dashboards#index
           dashboard GET        /dashboards/:id(.:format)                           dashboards#show

Controller action

1
2
3
4
def show
  @student_group = StudentGroup.find_by(name: params[:id])
  @assignments = @student_group.assignments.order('created_at ASC')
end

The problem was in an miscommunication between my routes and my controller. I was asking for the :id parameter, but was fetching the :name from the database. This became even MORE apparent when I worked on the nested controllers.

1
2
3
4
def show
  @student_group = StudentGroup.find_by name: params[:dashboard_id]
  @assignment = Assignment.find params[:id]
end

There had to be a way to change that parameter to something better. Did the framework betray me?! Off to the docs

The Docs

So many options for routes! Each one deserves its own post. The docs are incredibly great, until they’re not. I searched and searched. and found NOTHING! Time to go code diving :-)

How to Spelunk

My preferred method of diving through code is using Rubymine’s Command + B feature. I knew I needed to pass some param to resources in my routes file so I started there.

1
2
3
4
5
6
7
8
 def resources(*resources, &block)
  options = resources.extract_options!.dup
  ...
  resource_scope(:resources, Resource.new(resources.pop, options)) do
    ...
  end
  ...
end

Here I noticed we were creating a new Instance of the resource class with the options we passed in.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Resource #:nodoc:
  attr_reader :controller, :path, :options, :param

  def initialize(entities, options = {})
    @name       = entities.to_s
    @path       = (options[:path] || @name).to_s
    @controller = (options[:controller] || @name).to_s
    @as         = options[:as]
    @param      = (options[:param] || :id).to_sym
    @options    = options
  end
  ...
end

LOOK AT THAT! You can pass in :param to the the resource declaration!

The solution

Back in our routes file, we add the param option to the resource.

1
2
3
4
resources :dashboards, only: [:show, :index], param: :name do
  resources :assignments, only: [:show]
  resources :students, only: [:index, :show]
end

This generates these routes:

1
2
3
4
5
dashboard_assignment GET        /dashboards/:dashboard_name/assignments/:id(.:format) assignments#show
  dashboard_students GET        /dashboards/:dashboard_name/students(.:format)        students#index
   dashboard_student GET        /dashboards/:dashboard_name/students/:id(.:format)    students#show
          dashboards GET        /dashboards(.:format)                                 dashboards#index
           dashboard GET        /dashboards/:name(.:format)                           dashboards#show

And in our controllers:

1
2
3
4
def show
  @student_group = StudentGroup.find_by name: params[:dashboard_name]
  @assignment = Assignment.find params[:id]
end

So much better! I hope you found this useful. It was definitely fun to figure out.

comments powered by Disqus