Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

polymorphic_path not dry at all

#4615

Hi there,

please consider the following simple example of nested resources:

Models:
team.rb (Team)
team/member.rb (Team::Member)
team/member/job.rb (Team::Member::Job)

Routes:
Test::Application.routes.draw do
resources :team do

resources :members do
  resources :jobs
end

end end

Setup some data:
t = Team.create
3.times { t.members.create }
m = t.members.find(3)
5.times { m.jobs.create }

Testing polymorphic routes:
t = Team.find(1)
m = t.members.find(3)
j = m.jobs.find(5)

app.polymorphic_path(m)
=> ActionController::RoutingError: No route matches {:action=>"destroy", :controller=>"members", :team_id=>#<Team::Member id: 3, team_id: 1, created_at: "2010-05-16 09:29:29", updated_at: "2010-05-16 09:29:29">}

app.polymorphic_path([t, m])
=> NoMethodError: undefined method team_team_member_path' for #<ActionDispatch::Integration::Session:0x00000003cc2bf0>

The only way to solve this error is to make the routes much more complex by using namespaces in them.

But most important the usage of polymorphic_path doesn't really look DRY to me because you always have to specify all parents of a nested resource in each and every polymorphic_path.

To make polymorphic_path DRY and solve the problem with the needed routing complexity (namespaces), I put the following few lines into polymorphic_routes.rb at line 112 (inside polymorphic_url).

  namespaces = record.class.name.split('::')
  if namespaces.size>1
    chain = [args.shift]
    namespaces.pop
    namespaces.reverse.each do |namespace|
      chain << chain.last.send(namespace.downcase)
    end      
    args.unshift *chain.reverse
  end

This simply assembles all the parents of the object passed. It should also be very backwards compatible as most people don't put their models into subfolders using namespaces (I guess?).

So after monkey patching the calls which failed before now work just fine and are really dry:

app.polymorphic_path(m)
=> "/team/1/members/3"

app.polymorphic_path(j)
=> "/team/1/members/3/jobs/5"

What do you think? :)

Reported by gucki · May 16th, 2010 @ 12:30 PM

State: invalid
Milestone: none
Assigned to: nobody
Importance: none

Activity

  1. gucki
    gucki

    Here's a little optimized version. Better performance and only invoke if a single record is passed to polymorphic_path. This improve backwards compatibility even more :)

      unless record_or_hash_or_array.is_a?(Array)
        namespaces = record.class.name.split('::')
        if namespaces.size>1
          data = args.first
          namespaces.pop
          namespaces.reverse.each do |namespace|
            data = data.send(namespace.downcase)
            args.unshift data
          end
        end
      end
    

    May 16th, 2010 @ 01:24 PM

  2. Andrew White
    Andrew White

    Have you tried overriding model_name in the member and job models? e.g:

    class Team::Member < ActiveRecord::Base
      def self.model_name
        ActiveSupport::ModelName.new("Member")
      end
    end
    

    After doing this app.polymorphic_path([t, m]) should work.

    June 7th, 2010 @ 09:09 AM

  3. José Valim
    José Valim
    • State changed from new to invalid

    Good solution! But, as you said, this is specific to your application so it does not make sense in Rails itself.

    June 7th, 2010 @ 09:32 AM

  4. gucki
    gucki

    Thanks Andrew. But this is only a solution to the error but not for the problem that the whole thing is still not DRY.

    Specifying a path with app.polymorphic_path([t, m]) seems not DRY to me. One should only have to specify app.polymorphic_path(m) as the parent (here team) could be resolved from the namespace, as my code above does.

    June 7th, 2010 @ 09:39 AM

  5. Andrew White
    Andrew White

    Gucki, your solution will only work where the associations are given the default names. I'd argue that trying to tie knowledge of the model into the router is likely to end up making things worse. For example I use the model_name override method to have a generic controller for some STI models - your patch would break that by using the full class name and not the STI base class. Also I have STI models inside a collection module (e.g. Page::Components::MenuComponent) which would also break with your patch.

    June 7th, 2010 @ 10:19 AM