Add the has_many_polymorphs plugin
[cs356-p2-videostore.git] / vendor / plugins / has_many_polymorphs / lib / has_many_polymorphs / association.rb
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         {@reflection.primary_key_name => @owner.id,
92           type_key=> (@owner.class.base_class.name if type_key)}
93       end
94
95       def construct_from #:nodoc:
96         # build the FROM part of the query, in this case, the polymorphic join table
97         @reflection.klass.table_name
98       end
99
100       def construct_owner #:nodoc:
101         # the table name for the owner object's class
102         @owner.class.table_name
103       end
104       
105       def construct_owner_key #:nodoc:
106         # the primary key field for the owner object
107         @owner.class.primary_key
108       end
109
110       def construct_select(custom_select = nil) #:nodoc:
111         # build the select query
112         selected = custom_select || @reflection.options[:select]
113       end
114
115       def construct_joins(custom_joins = nil) #:nodoc:
116         # build the string of default joins
117         "JOIN #{construct_owner} polymorphic_parent ON #{construct_from}.#{@reflection.options[:foreign_key]} = polymorphic_parent.#{construct_owner_key} " + 
118         @reflection.options[:from].map do |plural|
119           klass = plural._as_class
120           "LEFT JOIN #{klass.table_name} ON #{construct_from}.#{@reflection.options[:polymorphic_key]} = #{klass.table_name}.#{klass.primary_key} AND #{construct_from}.#{@reflection.options[:polymorphic_type_key]} = #{@reflection.klass.quote_value(klass.base_class.name)}"
121         end.uniq.join(" ") + " #{custom_joins}"
122       end
123
124       def construct_conditions #:nodoc:
125         # build the fully realized condition string
126         conditions = construct_quoted_owner_attributes.map do |field, value|
127           "#{construct_from}.#{field} = #{@reflection.klass.quote_value(value)}" if value
128         end
129         conditions << custom_conditions if custom_conditions
130         "(" + conditions.compact.join(') AND (') + ")"
131       end
132
133       def custom_conditions #:nodoc:
134         # custom conditions... not as messy as has_many :through because our joins are a little smarter
135         if @reflection.options[:conditions]
136           "(" + interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) + ")"
137         end
138       end
139
140       alias :construct_owner_attributes :construct_quoted_owner_attributes
141       alias :conditions :custom_conditions # XXX possibly not necessary
142       alias :sql_conditions :custom_conditions # XXX ditto      
143
144       # construct attributes for join for a particular record
145       def construct_join_attributes(record) #:nodoc:
146         {@reflection.options[:polymorphic_key] => record.id, 
147           @reflection.options[:polymorphic_type_key] => "#{record.class.base_class}",          
148           @reflection.options[:foreign_key] => @owner.id}.merge(@reflection.options[:foreign_type_key] ? 
149         {@reflection.options[:foreign_type_key] => "#{@owner.class.base_class}"} : {}) # for double-sided relationships
150       end
151                     
152       def build(attrs = nil) #:nodoc:
153         raise PolymorphicMethodNotSupportedError, "You can't associate new records."
154       end      
155
156     end   
157         
158   end
159 end