This project is archived and is in readonly mode.

#4615 ✓invalid
gucki

polymorphic_path not dry at all

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

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? :)

Comments and changes to this ticket

  • gucki

    gucki May 16th, 2010 @ 01:24 PM

    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
    
  • Andrew White

    Andrew White June 7th, 2010 @ 09:09 AM

    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.

  • José Valim

    José Valim June 7th, 2010 @ 09:32 AM

    • 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.

  • gucki

    gucki June 7th, 2010 @ 09:39 AM

    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.

  • Andrew White

    Andrew White June 7th, 2010 @ 10:19 AM

    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.

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

<h2 style="font-size: 14px">Tickets have moved to Github</h2>

The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>

Pages