kronn-has_many_polymorphs 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +86 -0
- data/LICENSE +184 -0
- data/README +214 -0
- data/lib/has_many_polymorphs.rb +19 -0
- data/lib/has_many_polymorphs/association.rb +165 -0
- data/lib/has_many_polymorphs/autoload.rb +58 -0
- data/lib/has_many_polymorphs/base.rb +71 -0
- data/lib/has_many_polymorphs/class_methods.rb +598 -0
- data/lib/has_many_polymorphs/debugging_tools.rb +104 -0
- data/lib/has_many_polymorphs/railtie.rb +8 -0
- data/lib/has_many_polymorphs/rake_task_redefine_task.rb +35 -0
- data/lib/has_many_polymorphs/reflection.rb +58 -0
- data/lib/has_many_polymorphs/support_methods.rb +88 -0
- metadata +96 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'has_many_polymorphs/reflection'
|
3
|
+
require 'has_many_polymorphs/association'
|
4
|
+
require 'has_many_polymorphs/class_methods'
|
5
|
+
|
6
|
+
require 'has_many_polymorphs/support_methods'
|
7
|
+
require 'has_many_polymorphs/base'
|
8
|
+
|
9
|
+
class ActiveRecord::Base
|
10
|
+
extend ActiveRecord::Associations::PolymorphicClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
if ENV['HMP_DEBUG'] && (Rails.env.development? || Rails.env.test?)
|
14
|
+
require 'has_many_polymorphs/debugging_tools'
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'has_many_polymorphs/railtie'
|
18
|
+
|
19
|
+
_logger_debug "loaded ok"
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
module Associations #:nodoc:
|
3
|
+
|
4
|
+
class PolymorphicError < ActiveRecordError #:nodoc:
|
5
|
+
end
|
6
|
+
|
7
|
+
class PolymorphicMethodNotSupportedError < ActiveRecordError #:nodoc:
|
8
|
+
end
|
9
|
+
|
10
|
+
# The association class for a <tt>has_many_polymorphs</tt> association.
|
11
|
+
class PolymorphicAssociation < HasManyThroughAssociation
|
12
|
+
|
13
|
+
# Push a record onto the association. Triggers a database load for a uniqueness check only if <tt>:skip_duplicates</tt> is <tt>true</tt>. Return value is undefined.
|
14
|
+
def <<(*records)
|
15
|
+
return if records.empty?
|
16
|
+
|
17
|
+
if @reflection.options[:skip_duplicates]
|
18
|
+
_logger_debug "Loading instances for polymorphic duplicate push check; use :skip_duplicates => false and perhaps a database constraint to avoid this possible performance issue"
|
19
|
+
load_target
|
20
|
+
end
|
21
|
+
|
22
|
+
@reflection.klass.transaction do
|
23
|
+
flatten_deeper(records).each do |record|
|
24
|
+
if @owner.new_record? or not record.respond_to?(:new_record?) or record.new_record?
|
25
|
+
raise PolymorphicError, "You can't associate unsaved records."
|
26
|
+
end
|
27
|
+
next if @reflection.options[:skip_duplicates] and @target.include? record
|
28
|
+
@owner.send(@reflection.through_reflection.name).proxy_target << @reflection.klass.create!(construct_join_attributes(record))
|
29
|
+
@target << record if loaded?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :push :<<
|
37
|
+
alias :concat :<<
|
38
|
+
|
39
|
+
# Runs a <tt>find</tt> against the association contents, returning the matched records. All regular <tt>find</tt> options except <tt>:include</tt> are supported.
|
40
|
+
def find(*args)
|
41
|
+
opts = args._extract_options!
|
42
|
+
opts.delete :include
|
43
|
+
super(*(args + [opts]))
|
44
|
+
end
|
45
|
+
|
46
|
+
def construct_scope
|
47
|
+
_logger_warn "Warning; not all usage scenarios for polymorphic scopes are supported yet."
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
# Deletes a record from the association. Return value is undefined.
|
52
|
+
def delete(*records)
|
53
|
+
records = flatten_deeper(records)
|
54
|
+
records.reject! {|record| @target.delete(record) if record.new_record?}
|
55
|
+
return if records.empty?
|
56
|
+
|
57
|
+
@reflection.klass.transaction do
|
58
|
+
records.each do |record|
|
59
|
+
joins = @reflection.through_reflection.name
|
60
|
+
@owner.send(joins).delete(@owner.send(joins).select do |join|
|
61
|
+
join.send(@reflection.options[:polymorphic_key]) == record.id and
|
62
|
+
join.send(@reflection.options[:polymorphic_type_key]) == "#{record.class.base_class}"
|
63
|
+
end)
|
64
|
+
@target.delete(record)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Clears all records from the association. Returns an empty array.
|
70
|
+
def clear(klass = nil)
|
71
|
+
load_target
|
72
|
+
return if @target.empty?
|
73
|
+
|
74
|
+
if klass
|
75
|
+
delete(@target.select {|r| r.is_a? klass })
|
76
|
+
else
|
77
|
+
@owner.send(@reflection.through_reflection.name).clear
|
78
|
+
@target.clear
|
79
|
+
end
|
80
|
+
[]
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# undef :sum
|
86
|
+
# undef :create!
|
87
|
+
|
88
|
+
def construct_quoted_owner_attributes(*args) #:nodoc:
|
89
|
+
# no access to returning() here? why not?
|
90
|
+
type_key = @reflection.options[:foreign_type_key]
|
91
|
+
h = {@reflection.primary_key_name => @owner.id}
|
92
|
+
h[type_key] = @owner.class.base_class.name if type_key
|
93
|
+
h
|
94
|
+
end
|
95
|
+
|
96
|
+
def construct_from #:nodoc:
|
97
|
+
# build the FROM part of the query, in this case, the polymorphic join table
|
98
|
+
@reflection.klass.quoted_table_name
|
99
|
+
end
|
100
|
+
|
101
|
+
def construct_owner #:nodoc:
|
102
|
+
# the table name for the owner object's class
|
103
|
+
@owner.class.quoted_table_name
|
104
|
+
end
|
105
|
+
|
106
|
+
def construct_owner_key #:nodoc:
|
107
|
+
# the primary key field for the owner object
|
108
|
+
@owner.class.primary_key
|
109
|
+
end
|
110
|
+
|
111
|
+
def construct_select(custom_select = nil) #:nodoc:
|
112
|
+
# build the select query
|
113
|
+
selected = custom_select || @reflection.options[:select]
|
114
|
+
end
|
115
|
+
|
116
|
+
def construct_joins(custom_joins = nil) #:nodoc:
|
117
|
+
# build the string of default joins
|
118
|
+
"JOIN #{construct_owner} AS polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " +
|
119
|
+
@reflection.options[:from].map do |plural|
|
120
|
+
klass = plural._as_class
|
121
|
+
"LEFT JOIN #{klass.quoted_table_name} ON #{construct_from}.#{@reflection.options[:polymorphic_key]} = #{klass.quoted_table_name}.#{klass.primary_key} AND #{construct_from}.#{@reflection.options[:polymorphic_type_key]} = #{@reflection.klass.quote_value(klass.base_class.name)}"
|
122
|
+
end.uniq.join(" ") + " #{custom_joins}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def construct_conditions #:nodoc:
|
126
|
+
# build the fully realized condition string
|
127
|
+
conditions = construct_quoted_owner_attributes.map do |field, value|
|
128
|
+
"#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value
|
129
|
+
end
|
130
|
+
conditions << custom_conditions if custom_conditions
|
131
|
+
"(" + conditions.compact.join(') AND (') + ")"
|
132
|
+
end
|
133
|
+
|
134
|
+
def custom_conditions #:nodoc:
|
135
|
+
# custom conditions... not as messy as has_many :through because our joins are a little smarter
|
136
|
+
if @reflection.options[:conditions]
|
137
|
+
"(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
alias :construct_owner_attributes :construct_quoted_owner_attributes
|
142
|
+
alias :conditions :custom_conditions # XXX possibly not necessary
|
143
|
+
alias :sql_conditions :custom_conditions # XXX ditto
|
144
|
+
|
145
|
+
# construct attributes for join for a particular record
|
146
|
+
def construct_join_attributes(record) #:nodoc:
|
147
|
+
{
|
148
|
+
@reflection.options[:polymorphic_key] => record.id,
|
149
|
+
@reflection.options[:polymorphic_type_key] => "#{record.class.base_class}",
|
150
|
+
@reflection.options[:foreign_key] => @owner.id
|
151
|
+
}.merge(
|
152
|
+
@reflection.options[:foreign_type_key] ?
|
153
|
+
{ @reflection.options[:foreign_type_key] => "#{@owner.class.base_class}" } :
|
154
|
+
{}
|
155
|
+
) # for double-sided relationships
|
156
|
+
end
|
157
|
+
|
158
|
+
def build(attrs = nil) #:nodoc:
|
159
|
+
raise PolymorphicMethodNotSupportedError, "You can't associate new records."
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "#{Rails.root}/config/initializers/inflections"
|
2
|
+
|
3
|
+
module HasManyPolymorphs
|
4
|
+
|
5
|
+
=begin rdoc
|
6
|
+
Searches for models that use <tt>has_many_polymorphs</tt> or <tt>acts_as_double_polymorphic_join</tt> and makes sure that they get loaded during app initialization. This ensures that helper methods are injected into the target classes.
|
7
|
+
|
8
|
+
Note that you can override DEFAULT_OPTIONS via Rails::Configuration#has_many_polymorphs_options. For example, if you need an application extension to be required before has_many_polymorphs loads your models, add an <tt>after_initialize</tt> block in <tt>config/environment.rb</tt> that appends to the <tt>'requirements'</tt> key:
|
9
|
+
Rails::Initializer.run do |config|
|
10
|
+
# your other configuration here
|
11
|
+
|
12
|
+
config.after_initialize do
|
13
|
+
config.has_many_polymorphs.options['requirements'] << 'lib/my_extension'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
=end
|
18
|
+
|
19
|
+
MODELS_ROOT = Rails.root.join('app', 'models')
|
20
|
+
|
21
|
+
DEFAULT_OPTIONS = {
|
22
|
+
:file_pattern => "#{MODELS_ROOT}/**/*.rb",
|
23
|
+
:file_exclusions => ['svn', 'CVS', 'bzr'],
|
24
|
+
:methods => ['has_many_polymorphs', 'acts_as_double_polymorphic_join'],
|
25
|
+
:requirements => []}
|
26
|
+
|
27
|
+
mattr_accessor :options
|
28
|
+
@@options = HashWithIndifferentAccess.new(DEFAULT_OPTIONS)
|
29
|
+
|
30
|
+
# Dispatcher callback to load polymorphic relationships from the top down.
|
31
|
+
def self.setup
|
32
|
+
|
33
|
+
_logger_debug "autoload hook invoked"
|
34
|
+
|
35
|
+
options[:requirements].each do |requirement|
|
36
|
+
_logger_warn "forcing requirement load of #{requirement}"
|
37
|
+
require requirement
|
38
|
+
end
|
39
|
+
|
40
|
+
Dir.glob(options[:file_pattern]).each do |filename|
|
41
|
+
next if filename =~ /#{options[:file_exclusions].join("|")}/
|
42
|
+
open(filename) do |file|
|
43
|
+
if file.grep(/#{options[:methods].join("|")}/).any?
|
44
|
+
begin
|
45
|
+
# determines the modelname by the directory - this allows the autoload of namespaced models
|
46
|
+
modelname = filename[0..-4].gsub("#{MODELS_ROOT.to_s}/", "")
|
47
|
+
model = modelname.camelize
|
48
|
+
_logger_warn "preloading parent model #{model}"
|
49
|
+
model.constantize
|
50
|
+
rescue Object => e
|
51
|
+
_logger_warn "#{model} could not be preloaded: #{e.inspect} #{e.backtrace}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
# Interprets a polymorphic row from a unified SELECT, returning the
|
5
|
+
# appropriate ActiveRecord instance. Overrides
|
6
|
+
# ActiveRecord::Base.instantiate_without_callbacks.
|
7
|
+
def instantiate_with_polymorphic_checks(record)
|
8
|
+
if record['polymorphic_parent_class']
|
9
|
+
reflection = record['polymorphic_parent_class'].constantize.reflect_on_association(
|
10
|
+
record['polymorphic_association_id'].to_sym
|
11
|
+
)
|
12
|
+
# _logger_debug "Instantiating a polymorphic row for #{
|
13
|
+
# record['polymorphic_parent_class']
|
14
|
+
# }.reflect_on_association(:#{
|
15
|
+
# record['polymorphic_association_id']
|
16
|
+
# })"
|
17
|
+
|
18
|
+
# rewrite the record with the right column names
|
19
|
+
table_aliases = reflection.options[:table_aliases].dup
|
20
|
+
record = Hash[*table_aliases.keys.map {|key| [key, record[table_aliases[key]]] }.flatten]
|
21
|
+
|
22
|
+
# find the real child class
|
23
|
+
klass = record["#{self.table_name}.#{reflection.options[:polymorphic_type_key]}"].constantize
|
24
|
+
if sti_klass = record["#{klass.table_name}.#{klass.inheritance_column}"]
|
25
|
+
klass = klass.class_eval do compute_type(sti_klass) end # in case of namespaced STI models
|
26
|
+
|
27
|
+
# copy the data over to the right structure
|
28
|
+
klass.columns.map(&:name).each do |col|
|
29
|
+
record["#{klass.table_name}.#{col}"] = record["#{klass.base_class.table_name}.#{col}"]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# check that the join actually joined to something
|
34
|
+
child_id = record["#{self.table_name}.#{reflection.options[:polymorphic_key]}"]
|
35
|
+
unless child_id == record["#{klass.table_name}.#{klass.primary_key}"]
|
36
|
+
msg = []
|
37
|
+
msg << "Referential integrity violation"
|
38
|
+
msg << "child <#{klass.name}:#{child_id}> was not found for #{reflection.name.inspect}"
|
39
|
+
raise ActiveRecord::Associations::PolymorphicError, msg.join('; ')
|
40
|
+
end
|
41
|
+
|
42
|
+
# eject the join keys
|
43
|
+
# XXX not very readable
|
44
|
+
record = Hash[*record._select do |column, value|
|
45
|
+
column[/^#{klass.table_name}/]
|
46
|
+
end.map do |column, value|
|
47
|
+
[column[/\.(.*)/, 1], value]
|
48
|
+
end.flatten]
|
49
|
+
|
50
|
+
# allocate and assign values
|
51
|
+
klass.allocate.tap do |obj|
|
52
|
+
obj.instance_variable_set("@attributes", record)
|
53
|
+
obj.instance_variable_set("@attributes_cache", Hash.new)
|
54
|
+
|
55
|
+
if obj.respond_to_without_attributes?(:after_find)
|
56
|
+
obj.send(:callback, :after_find)
|
57
|
+
end
|
58
|
+
|
59
|
+
if obj.respond_to_without_attributes?(:after_initialize)
|
60
|
+
obj.send(:callback, :after_initialize)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
else
|
65
|
+
instantiate_without_polymorphic_checks(record)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias_method_chain :instantiate, :polymorphic_checks
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,598 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
module Associations #:nodoc:
|
3
|
+
# Class methods added to ActiveRecord::Base for setting up
|
4
|
+
# polymorphic associations
|
5
|
+
#
|
6
|
+
# == Notes
|
7
|
+
#
|
8
|
+
# STI association targets must enumerated and named. For example,
|
9
|
+
# if Dog and Cat both inherit from Animal, you still need to say
|
10
|
+
# <tt>[:dogs, :cats]</tt>, and not <tt>[:animals]</tt>.
|
11
|
+
#
|
12
|
+
# Namespaced models follow the Rails <tt>underscore</tt> convention.
|
13
|
+
# ZooAnimal::Lion becomes <tt>:'zoo_animal/lion'</tt>.
|
14
|
+
#
|
15
|
+
# You do not need to set up any other associations other than for
|
16
|
+
# either the regular method or the double. The join associations and
|
17
|
+
# all individual and reverse associations are generated for you.
|
18
|
+
# However, a join model and table are required.
|
19
|
+
#
|
20
|
+
# There is a tentative report that you can make the parent model be
|
21
|
+
# its own join model, but this is untested.
|
22
|
+
module PolymorphicClassMethods
|
23
|
+
RESERVED_DOUBLES_KEYS = [
|
24
|
+
:conditions, :order, :limit, :offset, :extend,
|
25
|
+
:skip_duplicates, :join_extend, :dependent,
|
26
|
+
:rename_individual_collections, :namespace
|
27
|
+
] #:nodoc:
|
28
|
+
|
29
|
+
# This method creates a doubled-sided polymorphic relationship. It must be called on the join model:
|
30
|
+
#
|
31
|
+
# class Devouring < ActiveRecord::Base
|
32
|
+
# belongs_to :eater, :polymorphic => true
|
33
|
+
# belongs_to :eaten, :polymorphic => true
|
34
|
+
#
|
35
|
+
# acts_as_double_polymorphic_join(
|
36
|
+
# :eaters => [:dogs, :cats],
|
37
|
+
# :eatens => [:cats, :birds]
|
38
|
+
# )
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# The method works by defining one or more special
|
42
|
+
# <tt>has_many_polymorphs</tt> association on every model in the target
|
43
|
+
# lists, depending on which side of the association it is on. Double
|
44
|
+
# self-references will work.
|
45
|
+
#
|
46
|
+
# The two association names and their value arrays are the only required
|
47
|
+
# parameters.
|
48
|
+
#
|
49
|
+
# == Available options
|
50
|
+
#
|
51
|
+
# These options are passed through to targets on both sides of the association. If you want to affect only one side, prepend the key with the name of that side. For example, <tt>:eaters_extend</tt>.
|
52
|
+
#
|
53
|
+
# <tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, or <tt>:delete_all</tt>. Controls how the join record gets treated on any association delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
|
54
|
+
# <tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
|
55
|
+
# <tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with <tt>"\\_of_#{association_name}"</tt>.
|
56
|
+
# <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
|
57
|
+
# <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
|
58
|
+
# <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
|
59
|
+
# <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
|
60
|
+
# <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
|
61
|
+
# <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
|
62
|
+
# <tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
|
63
|
+
def acts_as_double_polymorphic_join options={}, &extension
|
64
|
+
collections, options = extract_double_collections(options)
|
65
|
+
|
66
|
+
# handle the block
|
67
|
+
options[:extend] = (if options[:extend]
|
68
|
+
Array(options[:extend]) + [extension]
|
69
|
+
else
|
70
|
+
extension
|
71
|
+
end) if extension
|
72
|
+
|
73
|
+
collection_option_keys = make_general_option_keys_specific!(options, collections)
|
74
|
+
|
75
|
+
join_name = self.name.tableize.to_sym
|
76
|
+
collections.each do |association_id, children|
|
77
|
+
parent_hash_key = (collections.keys - [association_id]).first # parents are the entries in the _other_ children array
|
78
|
+
|
79
|
+
begin
|
80
|
+
parent_foreign_key = self.reflect_on_association(parent_hash_key._singularize).primary_key_name
|
81
|
+
rescue NoMethodError
|
82
|
+
unless parent_foreign_key
|
83
|
+
msg = "Couldn't find 'belongs_to' association for :#{parent_hash_key._singularize} in #{self.name}."
|
84
|
+
raise PolymorphicError, msg
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
parents = collections[parent_hash_key]
|
89
|
+
conflicts = (children & parents) # set intersection
|
90
|
+
parents.each do |plural_parent_name|
|
91
|
+
|
92
|
+
parent_class = plural_parent_name._as_class
|
93
|
+
singular_reverse_association_id = parent_hash_key._singularize
|
94
|
+
|
95
|
+
internal_options = {
|
96
|
+
:is_double => true,
|
97
|
+
:from => children,
|
98
|
+
:as => singular_reverse_association_id,
|
99
|
+
:through => join_name.to_sym,
|
100
|
+
:foreign_key => parent_foreign_key,
|
101
|
+
:foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
|
102
|
+
:singular_reverse_association_id => singular_reverse_association_id,
|
103
|
+
:conflicts => conflicts
|
104
|
+
}
|
105
|
+
|
106
|
+
general_options = Hash[*options._select do |key, value|
|
107
|
+
collection_option_keys[association_id].include? key and !value.nil?
|
108
|
+
end.map do |key, value|
|
109
|
+
[key.to_s[association_id.to_s.length+1..-1].to_sym, value]
|
110
|
+
end._flatten_once] # rename side-specific options to general names
|
111
|
+
|
112
|
+
general_options.each do |key, value|
|
113
|
+
# avoid clobbering keys that appear in both option sets
|
114
|
+
if internal_options[key]
|
115
|
+
general_options[key] = Array(value) + Array(internal_options[key])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
parent_class.send(:has_many_polymorphs, association_id, internal_options.merge(general_options))
|
120
|
+
|
121
|
+
if conflicts.include? plural_parent_name
|
122
|
+
# unify the alternate sides of the conflicting children
|
123
|
+
(conflicts).each do |method_name|
|
124
|
+
unless parent_class.instance_methods.include?(method_name)
|
125
|
+
parent_class.send(:define_method, method_name) do
|
126
|
+
(self.send("#{singular_reverse_association_id}_#{method_name}") +
|
127
|
+
self.send("#{association_id._singularize}_#{method_name}")).freeze
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# unify the join model... join model is always renamed for doubles, unlike child associations
|
133
|
+
unless parent_class.instance_methods.include?(join_name)
|
134
|
+
parent_class.send(:define_method, join_name) do
|
135
|
+
(self.send("#{join_name}_as_#{singular_reverse_association_id}") +
|
136
|
+
self.send("#{join_name}_as_#{association_id._singularize}")).freeze
|
137
|
+
end
|
138
|
+
end
|
139
|
+
else
|
140
|
+
unless parent_class.instance_methods.include?(join_name)
|
141
|
+
# ensure there are no forward slashes in the aliased join_name_method (occurs when namespaces are used)
|
142
|
+
join_name_method = join_name.to_s.gsub('/', '_').to_sym
|
143
|
+
parent_class.send(:alias_method, join_name_method, "#{join_name_method}_as_#{singular_reverse_association_id}")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# This method createds a single-sided polymorphic relationship
|
151
|
+
#
|
152
|
+
# class Petfood < ActiveRecord::Base has_many_polymorphs :eaters,
|
153
|
+
# :from => [:dogs, :cats, :birds] end
|
154
|
+
#
|
155
|
+
# The only required parameter, aside from the association name, is
|
156
|
+
# <tt>:from</tt>.
|
157
|
+
#
|
158
|
+
# The method generates a number of associations aside from the
|
159
|
+
# polymorphic one. In this example Petfood also gets <tt>dogs</tt>,
|
160
|
+
# <tt>cats</tt>, and <tt>birds</tt>, and Dog, Cat, and Bird get
|
161
|
+
# <tt>petfoods</tt>. (The reverse association to the parents is
|
162
|
+
# always plural.)
|
163
|
+
#
|
164
|
+
# == Available options
|
165
|
+
#
|
166
|
+
# <tt>:from</tt>:: An array of symbols representing the target models. Required.
|
167
|
+
# <tt>:as</tt>:: A symbol for the parent's interface in the join--what the parent 'acts as'.
|
168
|
+
# <tt>:through</tt>:: A symbol representing the class of the join model. Follows Rails defaults if not supplied (the parent and the association names, alphabetized, concatenated with an underscore, and singularized).
|
169
|
+
# <tt>:dependent</tt>:: Accepts <tt>:destroy</tt>, <tt>:nullify</tt>, <tt>:delete_all</tt>. Controls how the join record gets treated on any associate delete (whether from the polymorph or from an individual collection); defaults to <tt>:destroy</tt>.
|
170
|
+
# <tt>:skip_duplicates</tt>:: If <tt>true</tt>, will check to avoid pushing already associated records (but also triggering a database load). Defaults to <tt>true</tt>.
|
171
|
+
# <tt>:rename_individual_collections</tt>:: If <tt>true</tt>, all individual collections are prepended with the polymorph name, and the children's parent collection is appended with "_of_#{association_name}"</tt>. For example, <tt>zoos</tt> becomes <tt>zoos_of_animals</tt>. This is to help avoid method name collisions in crowded classes.
|
172
|
+
# <tt>:extend</tt>:: One or an array of mixed modules and procs, which are applied to the polymorphic association (usually to define custom methods).
|
173
|
+
# <tt>:join_extend</tt>:: One or an array of mixed modules and procs, which are applied to the join association.
|
174
|
+
# <tt>:parent_extend</tt>:: One or an array of mixed modules and procs, which are applied to the target models' association to the parents.
|
175
|
+
# <tt>:conditions</tt>:: An array or string of conditions for the SQL <tt>WHERE</tt> clause.
|
176
|
+
# <tt>:parent_conditions</tt>:: An array or string of conditions which are applied to the target models' association to the parents.
|
177
|
+
# <tt>:order</tt>:: A string for the SQL <tt>ORDER BY</tt> clause.
|
178
|
+
# <tt>:parent_order</tt>:: A string for the SQL <tt>ORDER BY</tt> which is applied to the target models' association to the parents.
|
179
|
+
# <tt>:group</tt>:: An array or string of conditions for the SQL <tt>GROUP BY</tt> clause. Affects the polymorphic and individual associations.
|
180
|
+
# <tt>:limit</tt>:: An integer. Affects the polymorphic and individual associations.
|
181
|
+
# <tt>:offset</tt>:: An integer. Only affects the polymorphic association.
|
182
|
+
# <tt>:namespace</tt>:: A symbol. Prepended to all the models in the <tt>:from</tt> and <tt>:through</tt> keys. This is especially useful for Camping, which namespaces models by default.
|
183
|
+
# <tt>:uniq</tt>:: If <tt>true</tt>, the records returned are passed through a pure-Ruby <tt>uniq</tt> before they are returned. Rarely needed.
|
184
|
+
# <tt>:foreign_key</tt>:: The column name for the parent's id in the join.
|
185
|
+
# <tt>:foreign_type_key</tt>:: The column name for the parent's class name in the join, if the parent itself is polymorphic. Rarely needed--if you're thinking about using this, you almost certainly want to use <tt>acts_as_double_polymorphic_join()</tt> instead.
|
186
|
+
# <tt>:polymorphic_key</tt>:: The column name for the child's id in the join.
|
187
|
+
# <tt>:polymorphic_type_key</tt>:: The column name for the child's class name in the join.
|
188
|
+
#
|
189
|
+
# If you pass a block, it gets converted to a Proc and added to <tt>:extend</tt>.
|
190
|
+
#
|
191
|
+
# == On condition nullification
|
192
|
+
#
|
193
|
+
# When you request an individual association, non-applicable but fully-qualified fields in the polymorphic association's <tt>:conditions</tt>, <tt>:order</tt>, and <tt>:group</tt> options get changed to <tt>NULL</tt>. For example, if you set <tt>:conditions => "dogs.name != 'Spot'"</tt>, when you request <tt>.cats</tt>, the conditions string is changed to <tt>NULL != 'Spot'</tt>.
|
194
|
+
#
|
195
|
+
# Be aware, however, that <tt>NULL != 'Spot'</tt> returns <tt>false</tt> due to SQL's 3-value logic. Instead, you need to use the <tt>:conditions</tt> string <tt>"dogs.name IS NULL OR dogs.name != 'Spot'"</tt> to get the behavior you probably expect for negative matches.
|
196
|
+
def has_many_polymorphs(association_id, options = {}, &extension)
|
197
|
+
_logger_debug "associating #{self}.#{association_id}"
|
198
|
+
reflection = create_has_many_polymorphs_reflection(association_id, options, &extension)
|
199
|
+
# puts "Created reflection #{reflection.inspect}"
|
200
|
+
# configure_dependency_for_has_many(reflection)
|
201
|
+
collection_reader_method(reflection, PolymorphicAssociation)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Composed method that assigns option defaults, builds the
|
205
|
+
# reflection object, and sets up all the related associations on
|
206
|
+
# the parent, join, and targets.
|
207
|
+
def create_has_many_polymorphs_reflection(association_id, options, &extension) #:nodoc:
|
208
|
+
options.assert_valid_keys(
|
209
|
+
:from,
|
210
|
+
:as,
|
211
|
+
:through,
|
212
|
+
:foreign_key,
|
213
|
+
:foreign_type_key,
|
214
|
+
:polymorphic_key, # same as :association_foreign_key
|
215
|
+
:polymorphic_type_key,
|
216
|
+
:dependent, # default :destroy, only affects the join table
|
217
|
+
:skip_duplicates, # default true, only affects the polymorphic collection
|
218
|
+
:ignore_duplicates, # deprecated
|
219
|
+
:is_double,
|
220
|
+
:rename_individual_collections,
|
221
|
+
:reverse_association_id, # not used
|
222
|
+
:singular_reverse_association_id,
|
223
|
+
:conflicts,
|
224
|
+
:extend,
|
225
|
+
:join_class_name,
|
226
|
+
:join_extend,
|
227
|
+
:parent_extend,
|
228
|
+
:table_aliases,
|
229
|
+
:select, # applies to the polymorphic relationship
|
230
|
+
:conditions, # applies to the polymorphic relationship, the children, and the join
|
231
|
+
# :include,
|
232
|
+
:parent_conditions,
|
233
|
+
:parent_order,
|
234
|
+
:order, # applies to the polymorphic relationship, the children, and the join
|
235
|
+
:group, # only applies to the polymorphic relationship and the children
|
236
|
+
:limit, # only applies to the polymorphic relationship and the children
|
237
|
+
:offset, # only applies to the polymorphic relationship
|
238
|
+
:parent_order,
|
239
|
+
:parent_group,
|
240
|
+
:parent_limit,
|
241
|
+
:parent_offset,
|
242
|
+
# :source,
|
243
|
+
:namespace,
|
244
|
+
:uniq, # XXX untested, only applies to the polymorphic relationship
|
245
|
+
# :finder_sql,
|
246
|
+
# :counter_sql,
|
247
|
+
# :before_add,
|
248
|
+
# :after_add,
|
249
|
+
# :before_remove,
|
250
|
+
# :after_remove
|
251
|
+
:dummy
|
252
|
+
)
|
253
|
+
|
254
|
+
# validate against the most frequent configuration mistakes
|
255
|
+
verify_pluralization_of(association_id)
|
256
|
+
raise PolymorphicError, ":from option must be an array" unless options[:from].is_a? Array
|
257
|
+
|
258
|
+
# if an association with this name is already defined, we recreate it with
|
259
|
+
# the new and old :from-options combined
|
260
|
+
if self.reflections[association_id]
|
261
|
+
options[:from] += self.reflections[association_id].options[:from]
|
262
|
+
options[:from].uniq!
|
263
|
+
end
|
264
|
+
options[:from].each { |plural| verify_pluralization_of(plural) }
|
265
|
+
|
266
|
+
options[:as] ||= self.name.demodulize.underscore.to_sym
|
267
|
+
options[:conflicts] = Array(options[:conflicts])
|
268
|
+
options[:foreign_key] ||= "#{options[:as]}_id"
|
269
|
+
|
270
|
+
options[:association_foreign_key] =
|
271
|
+
options[:polymorphic_key] ||= "#{association_id._singularize}_id"
|
272
|
+
options[:polymorphic_type_key] ||= "#{association_id._singularize}_type"
|
273
|
+
|
274
|
+
if options.has_key? :ignore_duplicates
|
275
|
+
_logger_warn "DEPRECATION WARNING: please use :skip_duplicates instead of :ignore_duplicates"
|
276
|
+
options[:skip_duplicates] = options[:ignore_duplicates]
|
277
|
+
end
|
278
|
+
options[:skip_duplicates] = true unless options.has_key? :skip_duplicates
|
279
|
+
options[:dependent] = :destroy unless options.has_key? :dependent
|
280
|
+
options[:conditions] = sanitize_sql(options[:conditions])
|
281
|
+
|
282
|
+
# options[:finder_sql] ||= "(options[:polymorphic_key]
|
283
|
+
|
284
|
+
options[:through] ||= build_join_table_symbol(association_id, (options[:as]._pluralize or self.table_name))
|
285
|
+
|
286
|
+
# set up namespaces if we have a namespace key
|
287
|
+
# XXX needs test coverage
|
288
|
+
if options[:namespace]
|
289
|
+
namespace = options[:namespace].to_s.chomp("/") + "/"
|
290
|
+
options[:from].map! do |child|
|
291
|
+
"#{namespace}#{child}".to_sym
|
292
|
+
end
|
293
|
+
options[:through] = "#{namespace}#{options[:through]}".to_sym
|
294
|
+
end
|
295
|
+
|
296
|
+
options[:join_class_name] ||= options[:through]._classify
|
297
|
+
options[:table_aliases] ||= build_table_aliases([options[:through]] + options[:from])
|
298
|
+
options[:select] ||= build_select(association_id, options[:table_aliases])
|
299
|
+
|
300
|
+
options[:through] = "#{options[:through]}_as_#{options[:singular_reverse_association_id]}" if options[:singular_reverse_association_id]
|
301
|
+
options[:through] = demodulate(options[:through]).to_sym
|
302
|
+
|
303
|
+
options[:extend] = spiked_create_extension_module(association_id, Array(options[:extend]) + Array(extension))
|
304
|
+
options[:join_extend] = spiked_create_extension_module(association_id, Array(options[:join_extend]), "Join")
|
305
|
+
options[:parent_extend] = spiked_create_extension_module(association_id, Array(options[:parent_extend]), "Parent")
|
306
|
+
|
307
|
+
# create the reflection object
|
308
|
+
create_reflection(:has_many_polymorphs, association_id, options, self).tap do |reflection|
|
309
|
+
# set up the other related associations
|
310
|
+
create_join_association(association_id, reflection)
|
311
|
+
create_has_many_through_associations_for_parent_to_children(association_id, reflection)
|
312
|
+
create_has_many_through_associations_for_children_to_parent(association_id, reflection)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
def extract_double_collections(options)
|
319
|
+
collections = options._select do |key, value|
|
320
|
+
value.is_a? Array and key.to_s !~ /(#{RESERVED_DOUBLES_KEYS.map(&:to_s).join('|')})$/
|
321
|
+
end
|
322
|
+
|
323
|
+
raise PolymorphicError, "Couldn't understand options in acts_as_double_polymorphic_join. Valid parameters are your two class collections, and then #{RESERVED_DOUBLES_KEYS.inspect[1..-2]}, with optionally your collection names prepended and joined with an underscore." unless collections.size == 2
|
324
|
+
|
325
|
+
options = options._select do |key, value|
|
326
|
+
!collections[key]
|
327
|
+
end
|
328
|
+
|
329
|
+
[collections, options]
|
330
|
+
end
|
331
|
+
|
332
|
+
def make_general_option_keys_specific!(options, collections)
|
333
|
+
collection_option_keys = Hash[*collections.keys.map do |key|
|
334
|
+
[key, RESERVED_DOUBLES_KEYS.map{|option| "#{key}_#{option}".to_sym}]
|
335
|
+
end._flatten_once]
|
336
|
+
|
337
|
+
collections.keys.each do |collection|
|
338
|
+
options.each do |key, value|
|
339
|
+
next if collection_option_keys.values.flatten.include? key
|
340
|
+
# shift the general options to the individual sides
|
341
|
+
collection_key = "#{collection}_#{key}".to_sym
|
342
|
+
collection_value = options[collection_key]
|
343
|
+
case key
|
344
|
+
when :conditions
|
345
|
+
collection_value, value = sanitize_sql(collection_value), sanitize_sql(value)
|
346
|
+
options[collection_key] = (collection_value ? "(#{collection_value}) AND (#{value})" : value)
|
347
|
+
when :order
|
348
|
+
options[collection_key] = (collection_value ? "#{collection_value}, #{value}" : value)
|
349
|
+
when :extend, :join_extend
|
350
|
+
options[collection_key] = Array(collection_value) + Array(value)
|
351
|
+
else
|
352
|
+
options[collection_key] ||= value
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
collection_option_keys
|
358
|
+
end
|
359
|
+
|
360
|
+
# table mapping for use at the instantiation point
|
361
|
+
|
362
|
+
def build_table_aliases(from)
|
363
|
+
# for the targets
|
364
|
+
{}.tap do |aliases|
|
365
|
+
from.map(&:to_s).sort.map(&:to_sym).each_with_index do |plural, t_index|
|
366
|
+
begin
|
367
|
+
table = plural._as_class.table_name
|
368
|
+
rescue NameError => e
|
369
|
+
raise PolymorphicError, "Could not find a valid class for #{plural.inspect} (tried #{plural.to_s._classify}). If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead."
|
370
|
+
end
|
371
|
+
begin
|
372
|
+
plural._as_class.columns.map(&:name).each_with_index do |field, f_index|
|
373
|
+
aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}"
|
374
|
+
end
|
375
|
+
rescue ActiveRecord::StatementInvalid => e
|
376
|
+
_logger_warn "Looks like your table doesn't exist for #{plural.to_s._classify}.\nError #{e}\nSkipping..."
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def build_select(association_id, aliases)
|
383
|
+
# <tt>instantiate</tt> has to know which reflection the results are coming from
|
384
|
+
(["\'#{self.name}\' AS polymorphic_parent_class",
|
385
|
+
"\'#{association_id}\' AS polymorphic_association_id"] +
|
386
|
+
aliases.map do |table, _alias|
|
387
|
+
"#{table} AS #{_alias}"
|
388
|
+
end.sort).join(", ")
|
389
|
+
end
|
390
|
+
|
391
|
+
# method sub-builders
|
392
|
+
|
393
|
+
def create_join_association(association_id, reflection)
|
394
|
+
|
395
|
+
options = {
|
396
|
+
:foreign_key => reflection.options[:foreign_key],
|
397
|
+
:dependent => reflection.options[:dependent],
|
398
|
+
:class_name => reflection.klass.name,
|
399
|
+
:extend => reflection.options[:join_extend]
|
400
|
+
# :limit => reflection.options[:limit],
|
401
|
+
# :offset => reflection.options[:offset],
|
402
|
+
# :order => devolve(association_id, reflection, reflection.options[:order], reflection.klass, true),
|
403
|
+
# :conditions => devolve(association_id, reflection, reflection.options[:conditions], reflection.klass, true)
|
404
|
+
}
|
405
|
+
|
406
|
+
if reflection.options[:foreign_type_key]
|
407
|
+
type_check = "#{reflection.options[:join_class_name].constantize.quoted_table_name}.#{reflection.options[:foreign_type_key]} = #{quote_value(self.base_class.name)}"
|
408
|
+
conjunction = options[:conditions] ? " AND " : nil
|
409
|
+
options[:conditions] = "#{options[:conditions]}#{conjunction}#{type_check}"
|
410
|
+
options[:as] = reflection.options[:as]
|
411
|
+
end
|
412
|
+
|
413
|
+
has_many(reflection.options[:through], options)
|
414
|
+
|
415
|
+
inject_before_save_into_join_table(association_id, reflection)
|
416
|
+
end
|
417
|
+
|
418
|
+
def inject_before_save_into_join_table(association_id, reflection)
|
419
|
+
sti_hook = "sti_class_rewrite"
|
420
|
+
polymorphic_type_key = reflection.options[:polymorphic_type_key]
|
421
|
+
|
422
|
+
reflection.klass.class_eval %{
|
423
|
+
unless [self._save_callbacks.map(&:raw_filter)].flatten.include?(:#{sti_hook})
|
424
|
+
before_save :#{sti_hook}
|
425
|
+
|
426
|
+
def #{sti_hook}
|
427
|
+
self.send(:#{polymorphic_type_key}=, self.#{polymorphic_type_key}.constantize.base_class.name)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
}
|
431
|
+
end
|
432
|
+
|
433
|
+
def create_has_many_through_associations_for_children_to_parent(association_id, reflection)
|
434
|
+
|
435
|
+
child_pluralization_map(association_id, reflection).each do |plural, singular|
|
436
|
+
if singular == reflection.options[:as]
|
437
|
+
raise PolymorphicError, if reflection.options[:is_double]
|
438
|
+
"You can't give either of the sides in a double-polymorphic join the same name as any of the individual target classes."
|
439
|
+
else
|
440
|
+
"You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model."
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
parent = self
|
445
|
+
plural._as_class.instance_eval do
|
446
|
+
# this shouldn't be called at all during doubles; there is
|
447
|
+
# no way to traverse to a double polymorphic parent
|
448
|
+
# (TODO is that right?)
|
449
|
+
unless reflection.options[:is_double] or reflection.options[:conflicts].include? self.name.tableize.to_sym
|
450
|
+
|
451
|
+
# the join table
|
452
|
+
through = "#{reflection.options[:through]}#{'_as_child' if parent == self}".to_sym
|
453
|
+
has_many(through,
|
454
|
+
:as => association_id._singularize,
|
455
|
+
# :source => association_id._singularize,
|
456
|
+
# :source_type => reflection.options[:polymorphic_type_key],
|
457
|
+
:class_name => reflection.klass.name,
|
458
|
+
:dependent => reflection.options[:dependent],
|
459
|
+
:extend => reflection.options[:join_extend],
|
460
|
+
# :limit => reflection.options[:limit],
|
461
|
+
# :offset => reflection.options[:offset],
|
462
|
+
:order => devolve(association_id, reflection, reflection.options[:parent_order], reflection.klass),
|
463
|
+
:conditions => devolve(association_id, reflection, reflection.options[:parent_conditions], reflection.klass)
|
464
|
+
)
|
465
|
+
|
466
|
+
# the association to the target's parents
|
467
|
+
association = "#{reflection.options[:as]._pluralize}#{"_of_#{association_id}" if reflection.options[:rename_individual_collections]}".to_sym
|
468
|
+
has_many(association,
|
469
|
+
:through => through,
|
470
|
+
:class_name => parent.name,
|
471
|
+
:source => reflection.options[:as],
|
472
|
+
:foreign_key => reflection.options[:foreign_key],
|
473
|
+
:extend => reflection.options[:parent_extend],
|
474
|
+
:conditions => reflection.options[:parent_conditions],
|
475
|
+
:order => reflection.options[:parent_order],
|
476
|
+
:offset => reflection.options[:parent_offset],
|
477
|
+
:limit => reflection.options[:parent_limit],
|
478
|
+
:group => reflection.options[:parent_group]
|
479
|
+
)
|
480
|
+
|
481
|
+
# debugger if association == :parents
|
482
|
+
# nil
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def create_has_many_through_associations_for_parent_to_children(association_id, reflection)
|
489
|
+
child_pluralization_map(association_id, reflection).each do |plural, singular|
|
490
|
+
current_association = demodulate(child_association_map(association_id, reflection)[plural])
|
491
|
+
source = demodulate(singular)
|
492
|
+
|
493
|
+
if reflection.options[:conflicts].include? plural
|
494
|
+
# XXX check this
|
495
|
+
current_association = "#{association_id._singularize}_#{current_association}" if reflection.options[:conflicts].include? self.name.tableize.to_sym
|
496
|
+
source = "#{source}_as_#{association_id._singularize}".to_sym
|
497
|
+
end
|
498
|
+
|
499
|
+
# make push/delete accessible from the individual collections but still operate via the general collection
|
500
|
+
extension_module = self.class_eval %[
|
501
|
+
module #{self.name + current_association._classify + "PolymorphicChildAssociationExtension"}
|
502
|
+
def push *args; proxy_owner.send(:#{association_id}).send(:push, *args); self; end
|
503
|
+
alias :<< :push
|
504
|
+
def delete *args; proxy_owner.send(:#{association_id}).send(:delete, *args); end
|
505
|
+
def clear; proxy_owner.send(:#{association_id}).send(:clear, #{singular._classify}); end
|
506
|
+
self
|
507
|
+
end
|
508
|
+
]
|
509
|
+
|
510
|
+
has_many(
|
511
|
+
current_association.to_sym,
|
512
|
+
:through => reflection.options[:through],
|
513
|
+
:source => association_id._singularize,
|
514
|
+
:source_type => plural._as_class.base_class.name,
|
515
|
+
:class_name => plural._as_class.name, # make STI not conflate subtypes
|
516
|
+
:extend => (Array(extension_module) + reflection.options[:extend]),
|
517
|
+
:limit => reflection.options[:limit],
|
518
|
+
# :offset => reflection.options[:offset],
|
519
|
+
:order => devolve(association_id, reflection, reflection.options[:order], plural._as_class),
|
520
|
+
:conditions => devolve(association_id, reflection, reflection.options[:conditions], plural._as_class),
|
521
|
+
:group => devolve(association_id, reflection, reflection.options[:group], plural._as_class)
|
522
|
+
)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# some support methods
|
527
|
+
|
528
|
+
def child_pluralization_map(association_id, reflection)
|
529
|
+
Hash[*reflection.options[:from].map do |plural|
|
530
|
+
[plural, plural._singularize]
|
531
|
+
end.flatten]
|
532
|
+
end
|
533
|
+
|
534
|
+
def child_association_map(association_id, reflection)
|
535
|
+
Hash[*reflection.options[:from].map do |plural|
|
536
|
+
[plural, "#{association_id._singularize.to_s + "_" if reflection.options[:rename_individual_collections]}#{plural}".to_sym]
|
537
|
+
end.flatten]
|
538
|
+
end
|
539
|
+
|
540
|
+
def demodulate(s)
|
541
|
+
s.to_s.gsub('/', '_').to_sym
|
542
|
+
end
|
543
|
+
|
544
|
+
def build_join_table_symbol(association_id, name)
|
545
|
+
[name.to_s, association_id.to_s].sort.join("_").to_sym
|
546
|
+
end
|
547
|
+
|
548
|
+
def all_classes_for(association_id, reflection)
|
549
|
+
klasses = [self, reflection.klass, *child_pluralization_map(association_id, reflection).keys.map(&:_as_class)]
|
550
|
+
klasses += klasses.map(&:base_class)
|
551
|
+
klasses.uniq
|
552
|
+
end
|
553
|
+
|
554
|
+
def devolve(association_id, reflection, string, klass, remove_inappropriate_clauses = false)
|
555
|
+
# XXX remove_inappropriate_clauses is not implemented; we'll wait until someone actually needs it
|
556
|
+
return unless string
|
557
|
+
string = string.dup
|
558
|
+
# _logger_debug "devolving #{string} for #{klass}"
|
559
|
+
inappropriate_classes = (all_classes_for(association_id, reflection) - # the join class must always be preserved
|
560
|
+
[klass, klass.base_class, reflection.klass, reflection.klass.base_class])
|
561
|
+
inappropriate_classes.map do |klass|
|
562
|
+
klass.columns.map do |column|
|
563
|
+
[klass.table_name, column.name]
|
564
|
+
end.map do |table, column|
|
565
|
+
["#{table}.#{column}", "`#{table}`.#{column}", "#{table}.`#{column}`", "`#{table}`.`#{column}`"]
|
566
|
+
end
|
567
|
+
end.flatten.sort_by(&:size).reverse.each do |quoted_reference|
|
568
|
+
# _logger_debug "devolved #{quoted_reference} to NULL"
|
569
|
+
# XXX clause removal would go here
|
570
|
+
string.gsub!(quoted_reference, "NULL")
|
571
|
+
end
|
572
|
+
# _logger_debug "altered to #{string}"
|
573
|
+
string
|
574
|
+
end
|
575
|
+
|
576
|
+
def verify_pluralization_of(sym)
|
577
|
+
sym = sym.to_s
|
578
|
+
singular = sym.singularize
|
579
|
+
plural = singular.pluralize
|
580
|
+
raise PolymorphicError, "Pluralization rules not set up correctly. You passed :#{sym}, which singularizes to :#{singular}, but that pluralizes to :#{plural}, which is different. Maybe you meant :#{plural} to begin with?" unless sym == plural
|
581
|
+
end
|
582
|
+
|
583
|
+
def spiked_create_extension_module(association_id, extensions, identifier = nil)
|
584
|
+
module_extensions = extensions.select{|e| e.is_a? Module}
|
585
|
+
proc_extensions = extensions.select{|e| e.is_a? Proc }
|
586
|
+
|
587
|
+
# support namespaced anonymous blocks as well as multiple procs
|
588
|
+
proc_extensions.each_with_index do |proc_extension, index|
|
589
|
+
module_name = "#{self.to_s}#{association_id._classify}Polymorphic#{identifier}AssociationExtension#{index}"
|
590
|
+
the_module = self.class_eval "module #{module_name}; self; end" # XXX hrm
|
591
|
+
the_module.class_eval &proc_extension
|
592
|
+
module_extensions << the_module
|
593
|
+
end
|
594
|
+
module_extensions
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|