Wednesday, January 30, 2008

ActiveScaffold reverse associations to models in modules

I really enjoy using ActiveScaffold, and encourage everyone to use it to generate administration style interfaces. I came across an apparent defect today that I wanted to share.



I have a set of models that are contained in a module, and I'm using AS to do administration for them. Mostly fine. However there's a problem when I click a link on an element of a many relationship that's being displayed in a list. In this image,


Metalicus


ranges is the result of a has_many relationship, and when I click on the range 'Summer Selection', I get this:


Metalicus


and the following stacktrace:


[sourcecode language='ruby']
ActionView::TemplateError (undefined method `reflect_on_all_associations' for Range:Class) on line #19 of vendor/plugins/active_scaffold/frontends/default/views/_nested.rhtml:
16: # determine what constraints we need
17: if column.through_association?
18: @constraints = {
19: association.source_reflection.reverse => {
20: association.through_reflection.reverse => parent_id
21: }
22: }

vendor/plugins/active_scaffold/lib/extensions/reverse_associations.rb:26:in `reverse_matches_for'
vendor/plugins/active_scaffold/lib/extensions/reverse_associations.rb:12:in `reverse'
vendor/plugins/active_scaffold/frontends/default/views/_nested.rhtml:19:in `_run_erb_47vendor47plugins47active_scaffold47frontends47default47views47_nested46rhtml'
vendor/plugins/active_scaffold/frontends/default/views/_nested.rhtml:11:in `each'
vendor/plugins/active_scaffold/frontends/default/views/_nested.rhtml:11:in `_run_erb_47vendor47plugins47active_scaffold47frontends47default47views47_nested46rhtml'
vendor/rails/actionpack/lib/action_view/base.rb:637:in `send'
vendor/rails/actionpack/lib/action_view/base.rb:637:in `compile_and_render_template'
vendor/rails/actionpack/lib/action_view/base.rb:365:in `render_template'
vendor/rails/actionpack/lib/action_view/base.rb:316:in `render_file'
vendor/rails/actionpack/lib/action_view/base.rb:331:in `render_without_active_scaffold'
[/sourcecode]



The problem is that class should be MyModule::Range, not Range.



AS is looking up the class using code on ActiveRecord::Reflection::AssociationReflection, from the file activescaffold/lib/extensions/reverse_associations.rb. The fix (apparently - I haven't tested this exhaustively yet) is to change the code that sends "class_name.constantize" to the association to send "klass" instead (you need to do this in two places). The completed code is attached below. I hope this helps someone else!



[sourcecode language='ruby']
module ActiveRecord
module Reflection
class AssociationReflection #:nodoc:
def reverse_for?(klass)
reverse_matches_for(klass).empty? ? false : true
end

attr_writer :reverse
def reverse
unless @reverse
# Following line changed for compatibility with associations on classes in modules
# reverse_matches = reverse_matches_for(self.class_name.constantize)
reverse_matches = reverse_matches_for(self.klass)
# grab first association, or make a wild guess
@reverse = reverse_matches.empty? ? self.active_record.to_s.pluralize.underscore : reverse_matches.first.name
end
@reverse
end

protected

def reverse_matches_for(klass)
reverse_matches = []

# stage 1 filter: collect associations that point back to this model and use the same primary_key_name
klass.reflect_on_all_associations.each do |assoc|
# skip over has_many :through associations
next if assoc.options[:through]

## Following line changed for compatibility with associations on classes in modules
# next unless assoc.options[:polymorphic] or assoc.class_name.constantize == self.active_record
next unless assoc.options[:polymorphic] or assoc.klass == self.active_record
case [assoc.macro, self.macro].find_all{|m| m == :has_and_belongs_to_many}.length
# if both are a habtm, then match them based on the join table
when 2
next unless assoc.options[:join_table] == self.options[:join_table]

# if only one is a habtm, they do not match
when 1
next

# otherwise, match them based on the primary_key_name
when 0
next unless assoc.primary_key_name.to_sym == self.primary_key_name.to_sym
end

reverse_matches < 1

reverse_matches
end

end
end
end
[/sourcecode]

No comments:

Post a Comment