##// END OF EJS Templates
Replaces find(:first) calls....
Jean-Philippe Lang -
r10703:a7023dfa9b8e
parent child
Show More
@@ -1,279 +1,279
1 module ActiveRecord
1 module ActiveRecord
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module List #:nodoc:
3 module List #:nodoc:
4 def self.included(base)
4 def self.included(base)
5 base.extend(ClassMethods)
5 base.extend(ClassMethods)
6 end
6 end
7
7
8 # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
8 # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9 # The class that has this specified needs to have a +position+ column defined as an integer on
9 # The class that has this specified needs to have a +position+ column defined as an integer on
10 # the mapped database table.
10 # the mapped database table.
11 #
11 #
12 # Todo list example:
12 # Todo list example:
13 #
13 #
14 # class TodoList < ActiveRecord::Base
14 # class TodoList < ActiveRecord::Base
15 # has_many :todo_items, :order => "position"
15 # has_many :todo_items, :order => "position"
16 # end
16 # end
17 #
17 #
18 # class TodoItem < ActiveRecord::Base
18 # class TodoItem < ActiveRecord::Base
19 # belongs_to :todo_list
19 # belongs_to :todo_list
20 # acts_as_list :scope => :todo_list
20 # acts_as_list :scope => :todo_list
21 # end
21 # end
22 #
22 #
23 # todo_list.first.move_to_bottom
23 # todo_list.first.move_to_bottom
24 # todo_list.last.move_higher
24 # todo_list.last.move_higher
25 module ClassMethods
25 module ClassMethods
26 # Configuration options are:
26 # Configuration options are:
27 #
27 #
28 # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
28 # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29 # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
29 # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30 # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
30 # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31 # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
31 # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32 # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
32 # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33 def acts_as_list(options = {})
33 def acts_as_list(options = {})
34 configuration = { :column => "position", :scope => "1 = 1" }
34 configuration = { :column => "position", :scope => "1 = 1" }
35 configuration.update(options) if options.is_a?(Hash)
35 configuration.update(options) if options.is_a?(Hash)
36
36
37 configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
37 configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38
38
39 if configuration[:scope].is_a?(Symbol)
39 if configuration[:scope].is_a?(Symbol)
40 scope_condition_method = %(
40 scope_condition_method = %(
41 def scope_condition
41 def scope_condition
42 if #{configuration[:scope].to_s}.nil?
42 if #{configuration[:scope].to_s}.nil?
43 "#{configuration[:scope].to_s} IS NULL"
43 "#{configuration[:scope].to_s} IS NULL"
44 else
44 else
45 "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
45 "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46 end
46 end
47 end
47 end
48 )
48 )
49 else
49 else
50 scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
50 scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51 end
51 end
52
52
53 class_eval <<-EOV
53 class_eval <<-EOV
54 include ActiveRecord::Acts::List::InstanceMethods
54 include ActiveRecord::Acts::List::InstanceMethods
55
55
56 def acts_as_list_class
56 def acts_as_list_class
57 ::#{self.name}
57 ::#{self.name}
58 end
58 end
59
59
60 def position_column
60 def position_column
61 '#{configuration[:column]}'
61 '#{configuration[:column]}'
62 end
62 end
63
63
64 #{scope_condition_method}
64 #{scope_condition_method}
65
65
66 before_destroy :remove_from_list
66 before_destroy :remove_from_list
67 before_create :add_to_list_bottom
67 before_create :add_to_list_bottom
68 EOV
68 EOV
69 end
69 end
70 end
70 end
71
71
72 # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
72 # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73 # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
73 # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74 # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
74 # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75 # the first in the list of all chapters.
75 # the first in the list of all chapters.
76 module InstanceMethods
76 module InstanceMethods
77 # Insert the item at the given position (defaults to the top position of 1).
77 # Insert the item at the given position (defaults to the top position of 1).
78 def insert_at(position = 1)
78 def insert_at(position = 1)
79 insert_at_position(position)
79 insert_at_position(position)
80 end
80 end
81
81
82 # Swap positions with the next lower item, if one exists.
82 # Swap positions with the next lower item, if one exists.
83 def move_lower
83 def move_lower
84 return unless lower_item
84 return unless lower_item
85
85
86 acts_as_list_class.transaction do
86 acts_as_list_class.transaction do
87 lower_item.decrement_position
87 lower_item.decrement_position
88 increment_position
88 increment_position
89 end
89 end
90 end
90 end
91
91
92 # Swap positions with the next higher item, if one exists.
92 # Swap positions with the next higher item, if one exists.
93 def move_higher
93 def move_higher
94 return unless higher_item
94 return unless higher_item
95
95
96 acts_as_list_class.transaction do
96 acts_as_list_class.transaction do
97 higher_item.increment_position
97 higher_item.increment_position
98 decrement_position
98 decrement_position
99 end
99 end
100 end
100 end
101
101
102 # Move to the bottom of the list. If the item is already in the list, the items below it have their
102 # Move to the bottom of the list. If the item is already in the list, the items below it have their
103 # position adjusted accordingly.
103 # position adjusted accordingly.
104 def move_to_bottom
104 def move_to_bottom
105 return unless in_list?
105 return unless in_list?
106 acts_as_list_class.transaction do
106 acts_as_list_class.transaction do
107 decrement_positions_on_lower_items
107 decrement_positions_on_lower_items
108 assume_bottom_position
108 assume_bottom_position
109 end
109 end
110 end
110 end
111
111
112 # Move to the top of the list. If the item is already in the list, the items above it have their
112 # Move to the top of the list. If the item is already in the list, the items above it have their
113 # position adjusted accordingly.
113 # position adjusted accordingly.
114 def move_to_top
114 def move_to_top
115 return unless in_list?
115 return unless in_list?
116 acts_as_list_class.transaction do
116 acts_as_list_class.transaction do
117 increment_positions_on_higher_items
117 increment_positions_on_higher_items
118 assume_top_position
118 assume_top_position
119 end
119 end
120 end
120 end
121
121
122 # Move to the given position
122 # Move to the given position
123 def move_to=(pos)
123 def move_to=(pos)
124 case pos.to_s
124 case pos.to_s
125 when 'highest'
125 when 'highest'
126 move_to_top
126 move_to_top
127 when 'higher'
127 when 'higher'
128 move_higher
128 move_higher
129 when 'lower'
129 when 'lower'
130 move_lower
130 move_lower
131 when 'lowest'
131 when 'lowest'
132 move_to_bottom
132 move_to_bottom
133 end
133 end
134 reset_positions_in_list
134 reset_positions_in_list
135 end
135 end
136
136
137 def reset_positions_in_list
137 def reset_positions_in_list
138 acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
138 acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
139 unless item.send(position_column) == (i + 1)
139 unless item.send(position_column) == (i + 1)
140 acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id})
140 acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id})
141 end
141 end
142 end
142 end
143 end
143 end
144
144
145 # Removes the item from the list.
145 # Removes the item from the list.
146 def remove_from_list
146 def remove_from_list
147 if in_list?
147 if in_list?
148 decrement_positions_on_lower_items
148 decrement_positions_on_lower_items
149 update_attribute position_column, nil
149 update_attribute position_column, nil
150 end
150 end
151 end
151 end
152
152
153 # Increase the position of this item without adjusting the rest of the list.
153 # Increase the position of this item without adjusting the rest of the list.
154 def increment_position
154 def increment_position
155 return unless in_list?
155 return unless in_list?
156 update_attribute position_column, self.send(position_column).to_i + 1
156 update_attribute position_column, self.send(position_column).to_i + 1
157 end
157 end
158
158
159 # Decrease the position of this item without adjusting the rest of the list.
159 # Decrease the position of this item without adjusting the rest of the list.
160 def decrement_position
160 def decrement_position
161 return unless in_list?
161 return unless in_list?
162 update_attribute position_column, self.send(position_column).to_i - 1
162 update_attribute position_column, self.send(position_column).to_i - 1
163 end
163 end
164
164
165 # Return +true+ if this object is the first in the list.
165 # Return +true+ if this object is the first in the list.
166 def first?
166 def first?
167 return false unless in_list?
167 return false unless in_list?
168 self.send(position_column) == 1
168 self.send(position_column) == 1
169 end
169 end
170
170
171 # Return +true+ if this object is the last in the list.
171 # Return +true+ if this object is the last in the list.
172 def last?
172 def last?
173 return false unless in_list?
173 return false unless in_list?
174 self.send(position_column) == bottom_position_in_list
174 self.send(position_column) == bottom_position_in_list
175 end
175 end
176
176
177 # Return the next higher item in the list.
177 # Return the next higher item in the list.
178 def higher_item
178 def higher_item
179 return nil unless in_list?
179 return nil unless in_list?
180 acts_as_list_class.find(:first, :conditions =>
180 acts_as_list_class.where(
181 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
181 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
182 )
182 ).first
183 end
183 end
184
184
185 # Return the next lower item in the list.
185 # Return the next lower item in the list.
186 def lower_item
186 def lower_item
187 return nil unless in_list?
187 return nil unless in_list?
188 acts_as_list_class.find(:first, :conditions =>
188 acts_as_list_class.where(
189 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
189 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
190 )
190 ).first
191 end
191 end
192
192
193 # Test if this record is in a list
193 # Test if this record is in a list
194 def in_list?
194 def in_list?
195 !send(position_column).nil?
195 !send(position_column).nil?
196 end
196 end
197
197
198 private
198 private
199 def add_to_list_top
199 def add_to_list_top
200 increment_positions_on_all_items
200 increment_positions_on_all_items
201 end
201 end
202
202
203 def add_to_list_bottom
203 def add_to_list_bottom
204 self[position_column] = bottom_position_in_list.to_i + 1
204 self[position_column] = bottom_position_in_list.to_i + 1
205 end
205 end
206
206
207 # Overwrite this method to define the scope of the list changes
207 # Overwrite this method to define the scope of the list changes
208 def scope_condition() "1" end
208 def scope_condition() "1" end
209
209
210 # Returns the bottom position number in the list.
210 # Returns the bottom position number in the list.
211 # bottom_position_in_list # => 2
211 # bottom_position_in_list # => 2
212 def bottom_position_in_list(except = nil)
212 def bottom_position_in_list(except = nil)
213 item = bottom_item(except)
213 item = bottom_item(except)
214 item ? item.send(position_column) : 0
214 item ? item.send(position_column) : 0
215 end
215 end
216
216
217 # Returns the bottom item
217 # Returns the bottom item
218 def bottom_item(except = nil)
218 def bottom_item(except = nil)
219 conditions = scope_condition
219 conditions = scope_condition
220 conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
220 conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
221 acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
221 acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
222 end
222 end
223
223
224 # Forces item to assume the bottom position in the list.
224 # Forces item to assume the bottom position in the list.
225 def assume_bottom_position
225 def assume_bottom_position
226 update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
226 update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
227 end
227 end
228
228
229 # Forces item to assume the top position in the list.
229 # Forces item to assume the top position in the list.
230 def assume_top_position
230 def assume_top_position
231 update_attribute(position_column, 1)
231 update_attribute(position_column, 1)
232 end
232 end
233
233
234 # This has the effect of moving all the higher items up one.
234 # This has the effect of moving all the higher items up one.
235 def decrement_positions_on_higher_items(position)
235 def decrement_positions_on_higher_items(position)
236 acts_as_list_class.update_all(
236 acts_as_list_class.update_all(
237 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
237 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
238 )
238 )
239 end
239 end
240
240
241 # This has the effect of moving all the lower items up one.
241 # This has the effect of moving all the lower items up one.
242 def decrement_positions_on_lower_items
242 def decrement_positions_on_lower_items
243 return unless in_list?
243 return unless in_list?
244 acts_as_list_class.update_all(
244 acts_as_list_class.update_all(
245 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
245 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
246 )
246 )
247 end
247 end
248
248
249 # This has the effect of moving all the higher items down one.
249 # This has the effect of moving all the higher items down one.
250 def increment_positions_on_higher_items
250 def increment_positions_on_higher_items
251 return unless in_list?
251 return unless in_list?
252 acts_as_list_class.update_all(
252 acts_as_list_class.update_all(
253 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
253 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
254 )
254 )
255 end
255 end
256
256
257 # This has the effect of moving all the lower items down one.
257 # This has the effect of moving all the lower items down one.
258 def increment_positions_on_lower_items(position)
258 def increment_positions_on_lower_items(position)
259 acts_as_list_class.update_all(
259 acts_as_list_class.update_all(
260 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
260 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
261 )
261 )
262 end
262 end
263
263
264 # Increments position (<tt>position_column</tt>) of all items in the list.
264 # Increments position (<tt>position_column</tt>) of all items in the list.
265 def increment_positions_on_all_items
265 def increment_positions_on_all_items
266 acts_as_list_class.update_all(
266 acts_as_list_class.update_all(
267 "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
267 "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
268 )
268 )
269 end
269 end
270
270
271 def insert_at_position(position)
271 def insert_at_position(position)
272 remove_from_list
272 remove_from_list
273 increment_positions_on_lower_items(position)
273 increment_positions_on_lower_items(position)
274 self.update_attribute(position_column, position)
274 self.update_attribute(position_column, position)
275 end
275 end
276 end
276 end
277 end
277 end
278 end
278 end
279 end
279 end
@@ -1,566 +1,566
1 # Copyright (c) 2005 Rick Olson
1 # Copyright (c) 2005 Rick Olson
2 #
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining
3 # Permission is hereby granted, free of charge, to any person obtaining
4 # a copy of this software and associated documentation files (the
4 # a copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish,
6 # without limitation the rights to use, copy, modify, merge, publish,
7 # distribute, sublicense, and/or sell copies of the Software, and to
7 # distribute, sublicense, and/or sell copies of the Software, and to
8 # permit persons to whom the Software is furnished to do so, subject to
8 # permit persons to whom the Software is furnished to do so, subject to
9 # the following conditions:
9 # the following conditions:
10 #
10 #
11 # The above copyright notice and this permission notice shall be
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Software.
12 # included in all copies or substantial portions of the Software.
13 #
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
22 module ActiveRecord #:nodoc:
22 module ActiveRecord #:nodoc:
23 module Acts #:nodoc:
23 module Acts #:nodoc:
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
26 # column is present as well.
26 # column is present as well.
27 #
27 #
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30 #
30 #
31 # class Page < ActiveRecord::Base
31 # class Page < ActiveRecord::Base
32 # # assumes pages_versions table
32 # # assumes pages_versions table
33 # acts_as_versioned
33 # acts_as_versioned
34 # end
34 # end
35 #
35 #
36 # Example:
36 # Example:
37 #
37 #
38 # page = Page.create(:title => 'hello world!')
38 # page = Page.create(:title => 'hello world!')
39 # page.version # => 1
39 # page.version # => 1
40 #
40 #
41 # page.title = 'hello world'
41 # page.title = 'hello world'
42 # page.save
42 # page.save
43 # page.version # => 2
43 # page.version # => 2
44 # page.versions.size # => 2
44 # page.versions.size # => 2
45 #
45 #
46 # page.revert_to(1) # using version number
46 # page.revert_to(1) # using version number
47 # page.title # => 'hello world!'
47 # page.title # => 'hello world!'
48 #
48 #
49 # page.revert_to(page.versions.last) # using versioned instance
49 # page.revert_to(page.versions.last) # using versioned instance
50 # page.title # => 'hello world'
50 # page.title # => 'hello world'
51 #
51 #
52 # page.versions.earliest # efficient query to find the first version
52 # page.versions.earliest # efficient query to find the first version
53 # page.versions.latest # efficient query to find the most recently created version
53 # page.versions.latest # efficient query to find the most recently created version
54 #
54 #
55 #
55 #
56 # Simple Queries to page between versions
56 # Simple Queries to page between versions
57 #
57 #
58 # page.versions.before(version)
58 # page.versions.before(version)
59 # page.versions.after(version)
59 # page.versions.after(version)
60 #
60 #
61 # Access the previous/next versions from the versioned model itself
61 # Access the previous/next versions from the versioned model itself
62 #
62 #
63 # version = page.versions.latest
63 # version = page.versions.latest
64 # version.previous # go back one version
64 # version.previous # go back one version
65 # version.next # go forward one version
65 # version.next # go forward one version
66 #
66 #
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68 module Versioned
68 module Versioned
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
70 def self.included(base) # :nodoc:
70 def self.included(base) # :nodoc:
71 base.extend ClassMethods
71 base.extend ClassMethods
72 end
72 end
73
73
74 module ClassMethods
74 module ClassMethods
75 # == Configuration options
75 # == Configuration options
76 #
76 #
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
86 #
86 #
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
88 #
88 #
89 # or...
89 # or...
90 #
90 #
91 # class Auction
91 # class Auction
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
93 # !expired?
93 # !expired?
94 # end
94 # end
95 # end
95 # end
96 #
96 #
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
100 #
100 #
101 # def name=(new_name)
101 # def name=(new_name)
102 # write_changed_attribute :name, new_name
102 # write_changed_attribute :name, new_name
103 # end
103 # end
104 #
104 #
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
106 # to create an anonymous mixin:
106 # to create an anonymous mixin:
107 #
107 #
108 # class Auction
108 # class Auction
109 # acts_as_versioned do
109 # acts_as_versioned do
110 # def started?
110 # def started?
111 # !started_at.nil?
111 # !started_at.nil?
112 # end
112 # end
113 # end
113 # end
114 # end
114 # end
115 #
115 #
116 # or...
116 # or...
117 #
117 #
118 # module AuctionExtension
118 # module AuctionExtension
119 # def started?
119 # def started?
120 # !started_at.nil?
120 # !started_at.nil?
121 # end
121 # end
122 # end
122 # end
123 # class Auction
123 # class Auction
124 # acts_as_versioned :extend => AuctionExtension
124 # acts_as_versioned :extend => AuctionExtension
125 # end
125 # end
126 #
126 #
127 # Example code:
127 # Example code:
128 #
128 #
129 # @auction = Auction.find(1)
129 # @auction = Auction.find(1)
130 # @auction.started?
130 # @auction.started?
131 # @auction.versions.first.started?
131 # @auction.versions.first.started?
132 #
132 #
133 # == Database Schema
133 # == Database Schema
134 #
134 #
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
138 #
138 #
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
141 #
141 #
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
144 #
144 #
145 # class AddVersions < ActiveRecord::Migration
145 # class AddVersions < ActiveRecord::Migration
146 # def self.up
146 # def self.up
147 # # create_versioned_table takes the same options hash
147 # # create_versioned_table takes the same options hash
148 # # that create_table does
148 # # that create_table does
149 # Post.create_versioned_table
149 # Post.create_versioned_table
150 # end
150 # end
151 #
151 #
152 # def self.down
152 # def self.down
153 # Post.drop_versioned_table
153 # Post.drop_versioned_table
154 # end
154 # end
155 # end
155 # end
156 #
156 #
157 # == Changing What Fields Are Versioned
157 # == Changing What Fields Are Versioned
158 #
158 #
159 # By default, acts_as_versioned will version all but these fields:
159 # By default, acts_as_versioned will version all but these fields:
160 #
160 #
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
162 #
162 #
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
164 #
164 #
165 # class Post < ActiveRecord::Base
165 # class Post < ActiveRecord::Base
166 # acts_as_versioned
166 # acts_as_versioned
167 # self.non_versioned_columns << 'comments_count'
167 # self.non_versioned_columns << 'comments_count'
168 # end
168 # end
169 #
169 #
170 def acts_as_versioned(options = {}, &extension)
170 def acts_as_versioned(options = {}, &extension)
171 # don't allow multiple calls
171 # don't allow multiple calls
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
173
173
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
175
175
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
178 :version_association_options
178 :version_association_options
179
179
180 # legacy
180 # legacy
181 alias_method :non_versioned_fields, :non_versioned_columns
181 alias_method :non_versioned_fields, :non_versioned_columns
182 alias_method :non_versioned_fields=, :non_versioned_columns=
182 alias_method :non_versioned_fields=, :non_versioned_columns=
183
183
184 class << self
184 class << self
185 alias_method :non_versioned_fields, :non_versioned_columns
185 alias_method :non_versioned_fields, :non_versioned_columns
186 alias_method :non_versioned_fields=, :non_versioned_columns=
186 alias_method :non_versioned_fields=, :non_versioned_columns=
187 end
187 end
188
188
189 send :attr_accessor, :altered_attributes
189 send :attr_accessor, :altered_attributes
190
190
191 self.versioned_class_name = options[:class_name] || "Version"
191 self.versioned_class_name = options[:class_name] || "Version"
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
195 self.version_column = options[:version_column] || 'version'
195 self.version_column = options[:version_column] || 'version'
196 self.version_sequence_name = options[:sequence_name]
196 self.version_sequence_name = options[:sequence_name]
197 self.max_version_limit = options[:limit].to_i
197 self.max_version_limit = options[:limit].to_i
198 self.version_condition = options[:if] || true
198 self.version_condition = options[:if] || true
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
200 self.version_association_options = {
200 self.version_association_options = {
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
202 :foreign_key => versioned_foreign_key,
202 :foreign_key => versioned_foreign_key,
203 :dependent => :delete_all
203 :dependent => :delete_all
204 }.merge(options[:association_options] || {})
204 }.merge(options[:association_options] || {})
205
205
206 if block_given?
206 if block_given?
207 extension_module_name = "#{versioned_class_name}Extension"
207 extension_module_name = "#{versioned_class_name}Extension"
208 silence_warnings do
208 silence_warnings do
209 self.const_set(extension_module_name, Module.new(&extension))
209 self.const_set(extension_module_name, Module.new(&extension))
210 end
210 end
211
211
212 options[:extend] = self.const_get(extension_module_name)
212 options[:extend] = self.const_get(extension_module_name)
213 end
213 end
214
214
215 class_eval do
215 class_eval do
216 has_many :versions, version_association_options do
216 has_many :versions, version_association_options do
217 # finds earliest version of this record
217 # finds earliest version of this record
218 def earliest
218 def earliest
219 @earliest ||= find(:first, :order => 'version')
219 @earliest ||= order('version').first
220 end
220 end
221
221
222 # find latest version of this record
222 # find latest version of this record
223 def latest
223 def latest
224 @latest ||= find(:first, :order => 'version desc')
224 @latest ||= order('version desc').first
225 end
225 end
226 end
226 end
227 before_save :set_new_version
227 before_save :set_new_version
228 after_create :save_version_on_create
228 after_create :save_version_on_create
229 after_update :save_version
229 after_update :save_version
230 after_save :clear_old_versions
230 after_save :clear_old_versions
231 after_save :clear_altered_attributes
231 after_save :clear_altered_attributes
232
232
233 unless options[:if_changed].nil?
233 unless options[:if_changed].nil?
234 self.track_altered_attributes = true
234 self.track_altered_attributes = true
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
236 options[:if_changed].each do |attr_name|
236 options[:if_changed].each do |attr_name|
237 define_method("#{attr_name}=") do |value|
237 define_method("#{attr_name}=") do |value|
238 write_changed_attribute attr_name, value
238 write_changed_attribute attr_name, value
239 end
239 end
240 end
240 end
241 end
241 end
242
242
243 include options[:extend] if options[:extend].is_a?(Module)
243 include options[:extend] if options[:extend].is_a?(Module)
244 end
244 end
245
245
246 # create the dynamic versioned model
246 # create the dynamic versioned model
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
248 def self.reloadable? ; false ; end
248 def self.reloadable? ; false ; end
249 # find first version before the given version
249 # find first version before the given version
250 def self.before(version)
250 def self.before(version)
251 find :first, :order => 'version desc',
251 find :first, :order => 'version desc',
252 :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
252 :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
253 end
253 end
254
254
255 # find first version after the given version.
255 # find first version after the given version.
256 def self.after(version)
256 def self.after(version)
257 find :first, :order => 'version',
257 find :first, :order => 'version',
258 :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
258 :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
259 end
259 end
260
260
261 def previous
261 def previous
262 self.class.before(self)
262 self.class.before(self)
263 end
263 end
264
264
265 def next
265 def next
266 self.class.after(self)
266 self.class.after(self)
267 end
267 end
268
268
269 def versions_count
269 def versions_count
270 page.version
270 page.version
271 end
271 end
272 end
272 end
273
273
274 versioned_class.cattr_accessor :original_class
274 versioned_class.cattr_accessor :original_class
275 versioned_class.original_class = self
275 versioned_class.original_class = self
276 versioned_class.table_name = versioned_table_name
276 versioned_class.table_name = versioned_table_name
277 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
277 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
278 :class_name => "::#{self.to_s}",
278 :class_name => "::#{self.to_s}",
279 :foreign_key => versioned_foreign_key
279 :foreign_key => versioned_foreign_key
280 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
280 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
281 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
281 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
282 end
282 end
283 end
283 end
284
284
285 module ActMethods
285 module ActMethods
286 def self.included(base) # :nodoc:
286 def self.included(base) # :nodoc:
287 base.extend ClassMethods
287 base.extend ClassMethods
288 end
288 end
289
289
290 # Finds a specific version of this record
290 # Finds a specific version of this record
291 def find_version(version = nil)
291 def find_version(version = nil)
292 self.class.find_version(id, version)
292 self.class.find_version(id, version)
293 end
293 end
294
294
295 # Saves a version of the model if applicable
295 # Saves a version of the model if applicable
296 def save_version
296 def save_version
297 save_version_on_create if save_version?
297 save_version_on_create if save_version?
298 end
298 end
299
299
300 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
300 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
301 def save_version_on_create
301 def save_version_on_create
302 rev = self.class.versioned_class.new
302 rev = self.class.versioned_class.new
303 self.clone_versioned_model(self, rev)
303 self.clone_versioned_model(self, rev)
304 rev.version = send(self.class.version_column)
304 rev.version = send(self.class.version_column)
305 rev.send("#{self.class.versioned_foreign_key}=", self.id)
305 rev.send("#{self.class.versioned_foreign_key}=", self.id)
306 rev.save
306 rev.save
307 end
307 end
308
308
309 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
309 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
310 # Override this method to set your own criteria for clearing old versions.
310 # Override this method to set your own criteria for clearing old versions.
311 def clear_old_versions
311 def clear_old_versions
312 return if self.class.max_version_limit == 0
312 return if self.class.max_version_limit == 0
313 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
313 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
314 if excess_baggage > 0
314 if excess_baggage > 0
315 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
315 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
316 self.class.versioned_class.connection.execute sql
316 self.class.versioned_class.connection.execute sql
317 end
317 end
318 end
318 end
319
319
320 def versions_count
320 def versions_count
321 version
321 version
322 end
322 end
323
323
324 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
324 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
325 def revert_to(version)
325 def revert_to(version)
326 if version.is_a?(self.class.versioned_class)
326 if version.is_a?(self.class.versioned_class)
327 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
327 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
328 else
328 else
329 return false unless version = versions.find_by_version(version)
329 return false unless version = versions.find_by_version(version)
330 end
330 end
331 self.clone_versioned_model(version, self)
331 self.clone_versioned_model(version, self)
332 self.send("#{self.class.version_column}=", version.version)
332 self.send("#{self.class.version_column}=", version.version)
333 true
333 true
334 end
334 end
335
335
336 # Reverts a model to a given version and saves the model.
336 # Reverts a model to a given version and saves the model.
337 # Takes either a version number or an instance of the versioned model
337 # Takes either a version number or an instance of the versioned model
338 def revert_to!(version)
338 def revert_to!(version)
339 revert_to(version) ? save_without_revision : false
339 revert_to(version) ? save_without_revision : false
340 end
340 end
341
341
342 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
342 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
343 def save_without_revision
343 def save_without_revision
344 save_without_revision!
344 save_without_revision!
345 true
345 true
346 rescue
346 rescue
347 false
347 false
348 end
348 end
349
349
350 def save_without_revision!
350 def save_without_revision!
351 without_locking do
351 without_locking do
352 without_revision do
352 without_revision do
353 save!
353 save!
354 end
354 end
355 end
355 end
356 end
356 end
357
357
358 # Returns an array of attribute keys that are versioned. See non_versioned_columns
358 # Returns an array of attribute keys that are versioned. See non_versioned_columns
359 def versioned_attributes
359 def versioned_attributes
360 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
360 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
361 end
361 end
362
362
363 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
363 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
364 # If called with a single parameter, gets whether the parameter has changed.
364 # If called with a single parameter, gets whether the parameter has changed.
365 def changed?(attr_name = nil)
365 def changed?(attr_name = nil)
366 attr_name.nil? ?
366 attr_name.nil? ?
367 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
367 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
368 (altered_attributes && altered_attributes.include?(attr_name.to_s))
368 (altered_attributes && altered_attributes.include?(attr_name.to_s))
369 end
369 end
370
370
371 # keep old dirty? method
371 # keep old dirty? method
372 alias_method :dirty?, :changed?
372 alias_method :dirty?, :changed?
373
373
374 # Clones a model. Used when saving a new version or reverting a model's version.
374 # Clones a model. Used when saving a new version or reverting a model's version.
375 def clone_versioned_model(orig_model, new_model)
375 def clone_versioned_model(orig_model, new_model)
376 self.versioned_attributes.each do |key|
376 self.versioned_attributes.each do |key|
377 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
377 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
378 end
378 end
379
379
380 if self.class.columns_hash.include?(self.class.inheritance_column)
380 if self.class.columns_hash.include?(self.class.inheritance_column)
381 if orig_model.is_a?(self.class.versioned_class)
381 if orig_model.is_a?(self.class.versioned_class)
382 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
382 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
383 elsif new_model.is_a?(self.class.versioned_class)
383 elsif new_model.is_a?(self.class.versioned_class)
384 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
384 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
385 end
385 end
386 end
386 end
387 end
387 end
388
388
389 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
389 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
390 def save_version?
390 def save_version?
391 version_condition_met? && changed?
391 version_condition_met? && changed?
392 end
392 end
393
393
394 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
394 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
395 # custom version condition checking.
395 # custom version condition checking.
396 def version_condition_met?
396 def version_condition_met?
397 case
397 case
398 when version_condition.is_a?(Symbol)
398 when version_condition.is_a?(Symbol)
399 send(version_condition)
399 send(version_condition)
400 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
400 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
401 version_condition.call(self)
401 version_condition.call(self)
402 else
402 else
403 version_condition
403 version_condition
404 end
404 end
405 end
405 end
406
406
407 # Executes the block with the versioning callbacks disabled.
407 # Executes the block with the versioning callbacks disabled.
408 #
408 #
409 # @foo.without_revision do
409 # @foo.without_revision do
410 # @foo.save
410 # @foo.save
411 # end
411 # end
412 #
412 #
413 def without_revision(&block)
413 def without_revision(&block)
414 self.class.without_revision(&block)
414 self.class.without_revision(&block)
415 end
415 end
416
416
417 # Turns off optimistic locking for the duration of the block
417 # Turns off optimistic locking for the duration of the block
418 #
418 #
419 # @foo.without_locking do
419 # @foo.without_locking do
420 # @foo.save
420 # @foo.save
421 # end
421 # end
422 #
422 #
423 def without_locking(&block)
423 def without_locking(&block)
424 self.class.without_locking(&block)
424 self.class.without_locking(&block)
425 end
425 end
426
426
427 def empty_callback() end #:nodoc:
427 def empty_callback() end #:nodoc:
428
428
429 protected
429 protected
430 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
430 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
431 def set_new_version
431 def set_new_version
432 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
432 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
433 end
433 end
434
434
435 # Gets the next available version for the current record, or 1 for a new record
435 # Gets the next available version for the current record, or 1 for a new record
436 def next_version
436 def next_version
437 return 1 if new_record?
437 return 1 if new_record?
438 (versions.calculate(:max, :version) || 0) + 1
438 (versions.calculate(:max, :version) || 0) + 1
439 end
439 end
440
440
441 # clears current changed attributes. Called after save.
441 # clears current changed attributes. Called after save.
442 def clear_altered_attributes
442 def clear_altered_attributes
443 self.altered_attributes = []
443 self.altered_attributes = []
444 end
444 end
445
445
446 def write_changed_attribute(attr_name, attr_value)
446 def write_changed_attribute(attr_name, attr_value)
447 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
447 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
448 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
448 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
449 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
449 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
450 write_attribute(attr_name, attr_value_for_db)
450 write_attribute(attr_name, attr_value_for_db)
451 end
451 end
452
452
453 module ClassMethods
453 module ClassMethods
454 # Finds a specific version of a specific row of this model
454 # Finds a specific version of a specific row of this model
455 def find_version(id, version = nil)
455 def find_version(id, version = nil)
456 return find(id) unless version
456 return find(id) unless version
457
457
458 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
458 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
459 options = { :conditions => conditions, :limit => 1 }
459 options = { :conditions => conditions, :limit => 1 }
460
460
461 if result = find_versions(id, options).first
461 if result = find_versions(id, options).first
462 result
462 result
463 else
463 else
464 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
464 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
465 end
465 end
466 end
466 end
467
467
468 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
468 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
469 def find_versions(id, options = {})
469 def find_versions(id, options = {})
470 versioned_class.find :all, {
470 versioned_class.find :all, {
471 :conditions => ["#{versioned_foreign_key} = ?", id],
471 :conditions => ["#{versioned_foreign_key} = ?", id],
472 :order => 'version' }.merge(options)
472 :order => 'version' }.merge(options)
473 end
473 end
474
474
475 # Returns an array of columns that are versioned. See non_versioned_columns
475 # Returns an array of columns that are versioned. See non_versioned_columns
476 def versioned_columns
476 def versioned_columns
477 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
477 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
478 end
478 end
479
479
480 # Returns an instance of the dynamic versioned model
480 # Returns an instance of the dynamic versioned model
481 def versioned_class
481 def versioned_class
482 const_get versioned_class_name
482 const_get versioned_class_name
483 end
483 end
484
484
485 # Rake migration task to create the versioned table using options passed to acts_as_versioned
485 # Rake migration task to create the versioned table using options passed to acts_as_versioned
486 def create_versioned_table(create_table_options = {})
486 def create_versioned_table(create_table_options = {})
487 # create version column in main table if it does not exist
487 # create version column in main table if it does not exist
488 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
488 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
489 self.connection.add_column table_name, :version, :integer
489 self.connection.add_column table_name, :version, :integer
490 end
490 end
491
491
492 self.connection.create_table(versioned_table_name, create_table_options) do |t|
492 self.connection.create_table(versioned_table_name, create_table_options) do |t|
493 t.column versioned_foreign_key, :integer
493 t.column versioned_foreign_key, :integer
494 t.column :version, :integer
494 t.column :version, :integer
495 end
495 end
496
496
497 updated_col = nil
497 updated_col = nil
498 self.versioned_columns.each do |col|
498 self.versioned_columns.each do |col|
499 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
499 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
500 self.connection.add_column versioned_table_name, col.name, col.type,
500 self.connection.add_column versioned_table_name, col.name, col.type,
501 :limit => col.limit,
501 :limit => col.limit,
502 :default => col.default,
502 :default => col.default,
503 :scale => col.scale,
503 :scale => col.scale,
504 :precision => col.precision
504 :precision => col.precision
505 end
505 end
506
506
507 if type_col = self.columns_hash[inheritance_column]
507 if type_col = self.columns_hash[inheritance_column]
508 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
508 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
509 :limit => type_col.limit,
509 :limit => type_col.limit,
510 :default => type_col.default,
510 :default => type_col.default,
511 :scale => type_col.scale,
511 :scale => type_col.scale,
512 :precision => type_col.precision
512 :precision => type_col.precision
513 end
513 end
514
514
515 if updated_col.nil?
515 if updated_col.nil?
516 self.connection.add_column versioned_table_name, :updated_at, :timestamp
516 self.connection.add_column versioned_table_name, :updated_at, :timestamp
517 end
517 end
518 end
518 end
519
519
520 # Rake migration task to drop the versioned table
520 # Rake migration task to drop the versioned table
521 def drop_versioned_table
521 def drop_versioned_table
522 self.connection.drop_table versioned_table_name
522 self.connection.drop_table versioned_table_name
523 end
523 end
524
524
525 # Executes the block with the versioning callbacks disabled.
525 # Executes the block with the versioning callbacks disabled.
526 #
526 #
527 # Foo.without_revision do
527 # Foo.without_revision do
528 # @foo.save
528 # @foo.save
529 # end
529 # end
530 #
530 #
531 def without_revision(&block)
531 def without_revision(&block)
532 class_eval do
532 class_eval do
533 CALLBACKS.each do |attr_name|
533 CALLBACKS.each do |attr_name|
534 alias_method "orig_#{attr_name}".to_sym, attr_name
534 alias_method "orig_#{attr_name}".to_sym, attr_name
535 alias_method attr_name, :empty_callback
535 alias_method attr_name, :empty_callback
536 end
536 end
537 end
537 end
538 block.call
538 block.call
539 ensure
539 ensure
540 class_eval do
540 class_eval do
541 CALLBACKS.each do |attr_name|
541 CALLBACKS.each do |attr_name|
542 alias_method attr_name, "orig_#{attr_name}".to_sym
542 alias_method attr_name, "orig_#{attr_name}".to_sym
543 end
543 end
544 end
544 end
545 end
545 end
546
546
547 # Turns off optimistic locking for the duration of the block
547 # Turns off optimistic locking for the duration of the block
548 #
548 #
549 # Foo.without_locking do
549 # Foo.without_locking do
550 # @foo.save
550 # @foo.save
551 # end
551 # end
552 #
552 #
553 def without_locking(&block)
553 def without_locking(&block)
554 current = ActiveRecord::Base.lock_optimistically
554 current = ActiveRecord::Base.lock_optimistically
555 ActiveRecord::Base.lock_optimistically = false if current
555 ActiveRecord::Base.lock_optimistically = false if current
556 result = block.call
556 result = block.call
557 ActiveRecord::Base.lock_optimistically = true if current
557 ActiveRecord::Base.lock_optimistically = true if current
558 result
558 result
559 end
559 end
560 end
560 end
561 end
561 end
562 end
562 end
563 end
563 end
564 end
564 end
565
565
566 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
566 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
@@ -1,601 +1,601
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4
4
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
7 # descendants with a single query. The drawback is that insertion or move need some complex
7 # descendants with a single query. The drawback is that insertion or move need some complex
8 # sql queries. But everything is done here by this module!
8 # sql queries. But everything is done here by this module!
9 #
9 #
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 #
12 #
13 # == API
13 # == API
14 #
14 #
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 # by another easier.
16 # by another easier.
17 #
17 #
18 # item.children.create(:name => "child1")
18 # item.children.create(:name => "child1")
19 #
19 #
20
20
21 # Configuration options are:
21 # Configuration options are:
22 #
22 #
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 # * +:left_column+ - column name for left boundry data, default "lft"
24 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
26 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # (if it hasn't been already) and use that as the foreign key restriction. You
27 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # can also pass an array to scope by multiple attributes.
28 # can also pass an array to scope by multiple attributes.
29 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
29 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
30 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 # child objects are destroyed alongside this object by calling their destroy
31 # child objects are destroyed alongside this object by calling their destroy
32 # method. If set to :delete_all (default), all the child objects are deleted
32 # method. If set to :delete_all (default), all the child objects are deleted
33 # without calling their destroy method.
33 # without calling their destroy method.
34 # * +:counter_cache+ adds a counter cache for the number of children.
34 # * +:counter_cache+ adds a counter cache for the number of children.
35 # defaults to false.
35 # defaults to false.
36 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
36 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 #
37 #
38 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
38 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
39 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
39 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
40 # to acts_as_nested_set models
40 # to acts_as_nested_set models
41 def acts_as_nested_set(options = {})
41 def acts_as_nested_set(options = {})
42 options = {
42 options = {
43 :parent_column => 'parent_id',
43 :parent_column => 'parent_id',
44 :left_column => 'lft',
44 :left_column => 'lft',
45 :right_column => 'rgt',
45 :right_column => 'rgt',
46 :dependent => :delete_all, # or :destroy
46 :dependent => :delete_all, # or :destroy
47 :counter_cache => false,
47 :counter_cache => false,
48 :order => 'id'
48 :order => 'id'
49 }.merge(options)
49 }.merge(options)
50
50
51 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
51 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
52 options[:scope] = "#{options[:scope]}_id".intern
52 options[:scope] = "#{options[:scope]}_id".intern
53 end
53 end
54
54
55 class_attribute :acts_as_nested_set_options
55 class_attribute :acts_as_nested_set_options
56 self.acts_as_nested_set_options = options
56 self.acts_as_nested_set_options = options
57
57
58 include CollectiveIdea::Acts::NestedSet::Model
58 include CollectiveIdea::Acts::NestedSet::Model
59 include Columns
59 include Columns
60 extend Columns
60 extend Columns
61
61
62 belongs_to :parent, :class_name => self.base_class.to_s,
62 belongs_to :parent, :class_name => self.base_class.to_s,
63 :foreign_key => parent_column_name,
63 :foreign_key => parent_column_name,
64 :counter_cache => options[:counter_cache],
64 :counter_cache => options[:counter_cache],
65 :inverse_of => :children
65 :inverse_of => :children
66 has_many :children, :class_name => self.base_class.to_s,
66 has_many :children, :class_name => self.base_class.to_s,
67 :foreign_key => parent_column_name, :order => left_column_name,
67 :foreign_key => parent_column_name, :order => left_column_name,
68 :inverse_of => :parent,
68 :inverse_of => :parent,
69 :before_add => options[:before_add],
69 :before_add => options[:before_add],
70 :after_add => options[:after_add],
70 :after_add => options[:after_add],
71 :before_remove => options[:before_remove],
71 :before_remove => options[:before_remove],
72 :after_remove => options[:after_remove]
72 :after_remove => options[:after_remove]
73
73
74 attr_accessor :skip_before_destroy
74 attr_accessor :skip_before_destroy
75
75
76 before_create :set_default_left_and_right
76 before_create :set_default_left_and_right
77 before_save :store_new_parent
77 before_save :store_new_parent
78 after_save :move_to_new_parent
78 after_save :move_to_new_parent
79 before_destroy :destroy_descendants
79 before_destroy :destroy_descendants
80
80
81 # no assignment to structure fields
81 # no assignment to structure fields
82 [left_column_name, right_column_name].each do |column|
82 [left_column_name, right_column_name].each do |column|
83 module_eval <<-"end_eval", __FILE__, __LINE__
83 module_eval <<-"end_eval", __FILE__, __LINE__
84 def #{column}=(x)
84 def #{column}=(x)
85 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
85 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
86 end
86 end
87 end_eval
87 end_eval
88 end
88 end
89
89
90 define_model_callbacks :move
90 define_model_callbacks :move
91 end
91 end
92
92
93 module Model
93 module Model
94 extend ActiveSupport::Concern
94 extend ActiveSupport::Concern
95
95
96 module ClassMethods
96 module ClassMethods
97 # Returns the first root
97 # Returns the first root
98 def root
98 def root
99 roots.first
99 roots.first
100 end
100 end
101
101
102 def roots
102 def roots
103 where(parent_column_name => nil).order(quoted_left_column_name)
103 where(parent_column_name => nil).order(quoted_left_column_name)
104 end
104 end
105
105
106 def leaves
106 def leaves
107 where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
107 where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
108 end
108 end
109
109
110 def valid?
110 def valid?
111 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
111 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 end
112 end
113
113
114 def left_and_rights_valid?
114 def left_and_rights_valid?
115 joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
115 joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
116 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
116 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
117 where(
117 where(
118 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
118 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
119 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
119 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
120 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
121 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
122 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
122 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
123 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
123 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
124 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
124 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
125 ).count == 0
125 ).count == 0
126 end
126 end
127
127
128 def no_duplicates_for_columns?
128 def no_duplicates_for_columns?
129 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
129 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
130 connection.quote_column_name(c)
130 connection.quote_column_name(c)
131 end.push(nil).join(", ")
131 end.push(nil).join(", ")
132 [quoted_left_column_name, quoted_right_column_name].all? do |column|
132 [quoted_left_column_name, quoted_right_column_name].all? do |column|
133 # No duplicates
133 # No duplicates
134 select("#{scope_string}#{column}, COUNT(#{column})").
134 select("#{scope_string}#{column}, COUNT(#{column})").
135 group("#{scope_string}#{column}").
135 group("#{scope_string}#{column}").
136 having("COUNT(#{column}) > 1").
136 having("COUNT(#{column}) > 1").
137 first.nil?
137 first.nil?
138 end
138 end
139 end
139 end
140
140
141 # Wrapper for each_root_valid? that can deal with scope.
141 # Wrapper for each_root_valid? that can deal with scope.
142 def all_roots_valid?
142 def all_roots_valid?
143 if acts_as_nested_set_options[:scope]
143 if acts_as_nested_set_options[:scope]
144 roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
144 roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
145 each_root_valid?(grouped_roots)
145 each_root_valid?(grouped_roots)
146 end
146 end
147 else
147 else
148 each_root_valid?(roots)
148 each_root_valid?(roots)
149 end
149 end
150 end
150 end
151
151
152 def each_root_valid?(roots_to_validate)
152 def each_root_valid?(roots_to_validate)
153 left = right = 0
153 left = right = 0
154 roots_to_validate.all? do |root|
154 roots_to_validate.all? do |root|
155 (root.left > left && root.right > right).tap do
155 (root.left > left && root.right > right).tap do
156 left = root.left
156 left = root.left
157 right = root.right
157 right = root.right
158 end
158 end
159 end
159 end
160 end
160 end
161
161
162 # Rebuilds the left & rights if unset or invalid.
162 # Rebuilds the left & rights if unset or invalid.
163 # Also very useful for converting from acts_as_tree.
163 # Also very useful for converting from acts_as_tree.
164 def rebuild!(validate_nodes = true)
164 def rebuild!(validate_nodes = true)
165 # Don't rebuild a valid tree.
165 # Don't rebuild a valid tree.
166 return true if valid?
166 return true if valid?
167
167
168 scope = lambda{|node|}
168 scope = lambda{|node|}
169 if acts_as_nested_set_options[:scope]
169 if acts_as_nested_set_options[:scope]
170 scope = lambda{|node|
170 scope = lambda{|node|
171 scope_column_names.inject(""){|str, column_name|
171 scope_column_names.inject(""){|str, column_name|
172 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
172 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 }
173 }
174 }
174 }
175 end
175 end
176 indices = {}
176 indices = {}
177
177
178 set_left_and_rights = lambda do |node|
178 set_left_and_rights = lambda do |node|
179 # set left
179 # set left
180 node[left_column_name] = indices[scope.call(node)] += 1
180 node[left_column_name] = indices[scope.call(node)] += 1
181 # find
181 # find
182 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
182 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
183 # set right
183 # set right
184 node[right_column_name] = indices[scope.call(node)] += 1
184 node[right_column_name] = indices[scope.call(node)] += 1
185 node.save!(:validate => validate_nodes)
185 node.save!(:validate => validate_nodes)
186 end
186 end
187
187
188 # Find root node(s)
188 # Find root node(s)
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node|
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node|
190 # setup index for this scope
190 # setup index for this scope
191 indices[scope.call(root_node)] ||= 0
191 indices[scope.call(root_node)] ||= 0
192 set_left_and_rights.call(root_node)
192 set_left_and_rights.call(root_node)
193 end
193 end
194 end
194 end
195
195
196 # Iterates over tree elements and determines the current level in the tree.
196 # Iterates over tree elements and determines the current level in the tree.
197 # Only accepts default ordering, odering by an other column than lft
197 # Only accepts default ordering, odering by an other column than lft
198 # does not work. This method is much more efficent than calling level
198 # does not work. This method is much more efficent than calling level
199 # because it doesn't require any additional database queries.
199 # because it doesn't require any additional database queries.
200 #
200 #
201 # Example:
201 # Example:
202 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
202 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
203 #
203 #
204 def each_with_level(objects)
204 def each_with_level(objects)
205 path = [nil]
205 path = [nil]
206 objects.each do |o|
206 objects.each do |o|
207 if o.parent_id != path.last
207 if o.parent_id != path.last
208 # we are on a new level, did we decent or ascent?
208 # we are on a new level, did we decent or ascent?
209 if path.include?(o.parent_id)
209 if path.include?(o.parent_id)
210 # remove wrong wrong tailing paths elements
210 # remove wrong wrong tailing paths elements
211 path.pop while path.last != o.parent_id
211 path.pop while path.last != o.parent_id
212 else
212 else
213 path << o.parent_id
213 path << o.parent_id
214 end
214 end
215 end
215 end
216 yield(o, path.length - 1)
216 yield(o, path.length - 1)
217 end
217 end
218 end
218 end
219 end
219 end
220
220
221 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
221 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
222 #
222 #
223 # category.self_and_descendants.count
223 # category.self_and_descendants.count
224 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
224 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
225
225
226 # Value of the parent column
226 # Value of the parent column
227 def parent_id
227 def parent_id
228 self[parent_column_name]
228 self[parent_column_name]
229 end
229 end
230
230
231 # Value of the left column
231 # Value of the left column
232 def left
232 def left
233 self[left_column_name]
233 self[left_column_name]
234 end
234 end
235
235
236 # Value of the right column
236 # Value of the right column
237 def right
237 def right
238 self[right_column_name]
238 self[right_column_name]
239 end
239 end
240
240
241 # Returns true if this is a root node.
241 # Returns true if this is a root node.
242 def root?
242 def root?
243 parent_id.nil?
243 parent_id.nil?
244 end
244 end
245
245
246 def leaf?
246 def leaf?
247 new_record? || (right - left == 1)
247 new_record? || (right - left == 1)
248 end
248 end
249
249
250 # Returns true is this is a child node
250 # Returns true is this is a child node
251 def child?
251 def child?
252 !parent_id.nil?
252 !parent_id.nil?
253 end
253 end
254
254
255 # Returns root
255 # Returns root
256 def root
256 def root
257 self_and_ancestors.where(parent_column_name => nil).first
257 self_and_ancestors.where(parent_column_name => nil).first
258 end
258 end
259
259
260 # Returns the array of all parents and self
260 # Returns the array of all parents and self
261 def self_and_ancestors
261 def self_and_ancestors
262 nested_set_scope.where([
262 nested_set_scope.where([
263 "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
263 "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
264 ])
264 ])
265 end
265 end
266
266
267 # Returns an array of all parents
267 # Returns an array of all parents
268 def ancestors
268 def ancestors
269 without_self self_and_ancestors
269 without_self self_and_ancestors
270 end
270 end
271
271
272 # Returns the array of all children of the parent, including self
272 # Returns the array of all children of the parent, including self
273 def self_and_siblings
273 def self_and_siblings
274 nested_set_scope.where(parent_column_name => parent_id)
274 nested_set_scope.where(parent_column_name => parent_id)
275 end
275 end
276
276
277 # Returns the array of all children of the parent, except self
277 # Returns the array of all children of the parent, except self
278 def siblings
278 def siblings
279 without_self self_and_siblings
279 without_self self_and_siblings
280 end
280 end
281
281
282 # Returns a set of all of its nested children which do not have children
282 # Returns a set of all of its nested children which do not have children
283 def leaves
283 def leaves
284 descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
284 descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
285 end
285 end
286
286
287 # Returns the level of this object in the tree
287 # Returns the level of this object in the tree
288 # root level is 0
288 # root level is 0
289 def level
289 def level
290 parent_id.nil? ? 0 : ancestors.count
290 parent_id.nil? ? 0 : ancestors.count
291 end
291 end
292
292
293 # Returns a set of itself and all of its nested children
293 # Returns a set of itself and all of its nested children
294 def self_and_descendants
294 def self_and_descendants
295 nested_set_scope.where([
295 nested_set_scope.where([
296 "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
296 "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
297 ])
297 ])
298 end
298 end
299
299
300 # Returns a set of all of its children and nested children
300 # Returns a set of all of its children and nested children
301 def descendants
301 def descendants
302 without_self self_and_descendants
302 without_self self_and_descendants
303 end
303 end
304
304
305 def is_descendant_of?(other)
305 def is_descendant_of?(other)
306 other.left < self.left && self.left < other.right && same_scope?(other)
306 other.left < self.left && self.left < other.right && same_scope?(other)
307 end
307 end
308
308
309 def is_or_is_descendant_of?(other)
309 def is_or_is_descendant_of?(other)
310 other.left <= self.left && self.left < other.right && same_scope?(other)
310 other.left <= self.left && self.left < other.right && same_scope?(other)
311 end
311 end
312
312
313 def is_ancestor_of?(other)
313 def is_ancestor_of?(other)
314 self.left < other.left && other.left < self.right && same_scope?(other)
314 self.left < other.left && other.left < self.right && same_scope?(other)
315 end
315 end
316
316
317 def is_or_is_ancestor_of?(other)
317 def is_or_is_ancestor_of?(other)
318 self.left <= other.left && other.left < self.right && same_scope?(other)
318 self.left <= other.left && other.left < self.right && same_scope?(other)
319 end
319 end
320
320
321 # Check if other model is in the same scope
321 # Check if other model is in the same scope
322 def same_scope?(other)
322 def same_scope?(other)
323 Array(acts_as_nested_set_options[:scope]).all? do |attr|
323 Array(acts_as_nested_set_options[:scope]).all? do |attr|
324 self.send(attr) == other.send(attr)
324 self.send(attr) == other.send(attr)
325 end
325 end
326 end
326 end
327
327
328 # Find the first sibling to the left
328 # Find the first sibling to the left
329 def left_sibling
329 def left_sibling
330 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
330 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
331 order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
331 order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
332 end
332 end
333
333
334 # Find the first sibling to the right
334 # Find the first sibling to the right
335 def right_sibling
335 def right_sibling
336 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
336 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
337 end
337 end
338
338
339 # Shorthand method for finding the left sibling and moving to the left of it.
339 # Shorthand method for finding the left sibling and moving to the left of it.
340 def move_left
340 def move_left
341 move_to_left_of left_sibling
341 move_to_left_of left_sibling
342 end
342 end
343
343
344 # Shorthand method for finding the right sibling and moving to the right of it.
344 # Shorthand method for finding the right sibling and moving to the right of it.
345 def move_right
345 def move_right
346 move_to_right_of right_sibling
346 move_to_right_of right_sibling
347 end
347 end
348
348
349 # Move the node to the left of another node (you can pass id only)
349 # Move the node to the left of another node (you can pass id only)
350 def move_to_left_of(node)
350 def move_to_left_of(node)
351 move_to node, :left
351 move_to node, :left
352 end
352 end
353
353
354 # Move the node to the left of another node (you can pass id only)
354 # Move the node to the left of another node (you can pass id only)
355 def move_to_right_of(node)
355 def move_to_right_of(node)
356 move_to node, :right
356 move_to node, :right
357 end
357 end
358
358
359 # Move the node to the child of another node (you can pass id only)
359 # Move the node to the child of another node (you can pass id only)
360 def move_to_child_of(node)
360 def move_to_child_of(node)
361 move_to node, :child
361 move_to node, :child
362 end
362 end
363
363
364 # Move the node to root nodes
364 # Move the node to root nodes
365 def move_to_root
365 def move_to_root
366 move_to nil, :root
366 move_to nil, :root
367 end
367 end
368
368
369 def move_possible?(target)
369 def move_possible?(target)
370 self != target && # Can't target self
370 self != target && # Can't target self
371 same_scope?(target) && # can't be in different scopes
371 same_scope?(target) && # can't be in different scopes
372 # !(left..right).include?(target.left..target.right) # this needs tested more
372 # !(left..right).include?(target.left..target.right) # this needs tested more
373 # detect impossible move
373 # detect impossible move
374 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
374 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
375 end
375 end
376
376
377 def to_text
377 def to_text
378 self_and_descendants.map do |node|
378 self_and_descendants.map do |node|
379 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
379 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
380 end.join("\n")
380 end.join("\n")
381 end
381 end
382
382
383 protected
383 protected
384
384
385 def without_self(scope)
385 def without_self(scope)
386 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
386 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
387 end
387 end
388
388
389 # All nested set queries should use this nested_set_scope, which performs finds on
389 # All nested set queries should use this nested_set_scope, which performs finds on
390 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
390 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
391 # declaration.
391 # declaration.
392 def nested_set_scope(options = {})
392 def nested_set_scope(options = {})
393 options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
393 options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
394 scopes = Array(acts_as_nested_set_options[:scope])
394 scopes = Array(acts_as_nested_set_options[:scope])
395 options[:conditions] = scopes.inject({}) do |conditions,attr|
395 options[:conditions] = scopes.inject({}) do |conditions,attr|
396 conditions.merge attr => self[attr]
396 conditions.merge attr => self[attr]
397 end unless scopes.empty?
397 end unless scopes.empty?
398 self.class.base_class.scoped options
398 self.class.base_class.scoped options
399 end
399 end
400
400
401 def store_new_parent
401 def store_new_parent
402 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
402 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
403 true # force callback to return true
403 true # force callback to return true
404 end
404 end
405
405
406 def move_to_new_parent
406 def move_to_new_parent
407 if @move_to_new_parent_id.nil?
407 if @move_to_new_parent_id.nil?
408 move_to_root
408 move_to_root
409 elsif @move_to_new_parent_id
409 elsif @move_to_new_parent_id
410 move_to_child_of(@move_to_new_parent_id)
410 move_to_child_of(@move_to_new_parent_id)
411 end
411 end
412 end
412 end
413
413
414 # on creation, set automatically lft and rgt to the end of the tree
414 # on creation, set automatically lft and rgt to the end of the tree
415 def set_default_left_and_right
415 def set_default_left_and_right
416 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
416 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").limit(1).lock(true).first
417 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
417 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
418 # adds the new node to the right of all existing nodes
418 # adds the new node to the right of all existing nodes
419 self[left_column_name] = maxright + 1
419 self[left_column_name] = maxright + 1
420 self[right_column_name] = maxright + 2
420 self[right_column_name] = maxright + 2
421 end
421 end
422
422
423 def in_tenacious_transaction(&block)
423 def in_tenacious_transaction(&block)
424 retry_count = 0
424 retry_count = 0
425 begin
425 begin
426 transaction(&block)
426 transaction(&block)
427 rescue ActiveRecord::StatementInvalid => error
427 rescue ActiveRecord::StatementInvalid => error
428 raise unless connection.open_transactions.zero?
428 raise unless connection.open_transactions.zero?
429 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
429 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
430 raise unless retry_count < 10
430 raise unless retry_count < 10
431 retry_count += 1
431 retry_count += 1
432 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
432 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
433 sleep(rand(retry_count)*0.1) # Aloha protocol
433 sleep(rand(retry_count)*0.1) # Aloha protocol
434 retry
434 retry
435 end
435 end
436 end
436 end
437
437
438 # Prunes a branch off of the tree, shifting all of the elements on the right
438 # Prunes a branch off of the tree, shifting all of the elements on the right
439 # back to the left so the counts still work.
439 # back to the left so the counts still work.
440 def destroy_descendants
440 def destroy_descendants
441 return if right.nil? || left.nil? || skip_before_destroy
441 return if right.nil? || left.nil? || skip_before_destroy
442
442
443 in_tenacious_transaction do
443 in_tenacious_transaction do
444 reload_nested_set
444 reload_nested_set
445 # select the rows in the model that extend past the deletion point and apply a lock
445 # select the rows in the model that extend past the deletion point and apply a lock
446 self.class.base_class.find(:all,
446 self.class.base_class.
447 :select => "id",
447 select("id").
448 :conditions => ["#{quoted_left_column_name} >= ?", left],
448 where("#{quoted_left_column_name} >= ?", left).
449 :lock => true
449 lock(true).
450 )
450 all
451
451
452 if acts_as_nested_set_options[:dependent] == :destroy
452 if acts_as_nested_set_options[:dependent] == :destroy
453 descendants.each do |model|
453 descendants.each do |model|
454 model.skip_before_destroy = true
454 model.skip_before_destroy = true
455 model.destroy
455 model.destroy
456 end
456 end
457 else
457 else
458 nested_set_scope.delete_all(
458 nested_set_scope.delete_all(
459 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
459 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
460 left, right]
460 left, right]
461 )
461 )
462 end
462 end
463
463
464 # update lefts and rights for remaining nodes
464 # update lefts and rights for remaining nodes
465 diff = right - left + 1
465 diff = right - left + 1
466 nested_set_scope.update_all(
466 nested_set_scope.update_all(
467 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
467 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
468 ["#{quoted_left_column_name} > ?", right]
468 ["#{quoted_left_column_name} > ?", right]
469 )
469 )
470 nested_set_scope.update_all(
470 nested_set_scope.update_all(
471 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
471 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
472 ["#{quoted_right_column_name} > ?", right]
472 ["#{quoted_right_column_name} > ?", right]
473 )
473 )
474
474
475 reload
475 reload
476 # Don't allow multiple calls to destroy to corrupt the set
476 # Don't allow multiple calls to destroy to corrupt the set
477 self.skip_before_destroy = true
477 self.skip_before_destroy = true
478 end
478 end
479 end
479 end
480
480
481 # reload left, right, and parent
481 # reload left, right, and parent
482 def reload_nested_set
482 def reload_nested_set
483 reload(
483 reload(
484 :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
484 :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
485 :lock => true
485 :lock => true
486 )
486 )
487 end
487 end
488
488
489 def move_to(target, position)
489 def move_to(target, position)
490 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
490 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
491 run_callbacks :move do
491 run_callbacks :move do
492 in_tenacious_transaction do
492 in_tenacious_transaction do
493 if target.is_a? self.class.base_class
493 if target.is_a? self.class.base_class
494 target.reload_nested_set
494 target.reload_nested_set
495 elsif position != :root
495 elsif position != :root
496 # load object if node is not an object
496 # load object if node is not an object
497 target = nested_set_scope.find(target)
497 target = nested_set_scope.find(target)
498 end
498 end
499 self.reload_nested_set
499 self.reload_nested_set
500
500
501 unless position == :root || move_possible?(target)
501 unless position == :root || move_possible?(target)
502 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
502 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
503 end
503 end
504
504
505 bound = case position
505 bound = case position
506 when :child; target[right_column_name]
506 when :child; target[right_column_name]
507 when :left; target[left_column_name]
507 when :left; target[left_column_name]
508 when :right; target[right_column_name] + 1
508 when :right; target[right_column_name] + 1
509 when :root; 1
509 when :root; 1
510 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
510 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
511 end
511 end
512
512
513 if bound > self[right_column_name]
513 if bound > self[right_column_name]
514 bound = bound - 1
514 bound = bound - 1
515 other_bound = self[right_column_name] + 1
515 other_bound = self[right_column_name] + 1
516 else
516 else
517 other_bound = self[left_column_name] - 1
517 other_bound = self[left_column_name] - 1
518 end
518 end
519
519
520 # there would be no change
520 # there would be no change
521 return if bound == self[right_column_name] || bound == self[left_column_name]
521 return if bound == self[right_column_name] || bound == self[left_column_name]
522
522
523 # we have defined the boundaries of two non-overlapping intervals,
523 # we have defined the boundaries of two non-overlapping intervals,
524 # so sorting puts both the intervals and their boundaries in order
524 # so sorting puts both the intervals and their boundaries in order
525 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
525 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
526
526
527 # select the rows in the model between a and d, and apply a lock
527 # select the rows in the model between a and d, and apply a lock
528 self.class.base_class.select('id').lock(true).where(
528 self.class.base_class.select('id').lock(true).where(
529 ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
529 ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
530 )
530 )
531
531
532 new_parent = case position
532 new_parent = case position
533 when :child; target.id
533 when :child; target.id
534 when :root; nil
534 when :root; nil
535 else target[parent_column_name]
535 else target[parent_column_name]
536 end
536 end
537
537
538 self.nested_set_scope.update_all([
538 self.nested_set_scope.update_all([
539 "#{quoted_left_column_name} = CASE " +
539 "#{quoted_left_column_name} = CASE " +
540 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
540 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
541 "THEN #{quoted_left_column_name} + :d - :b " +
541 "THEN #{quoted_left_column_name} + :d - :b " +
542 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
542 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
543 "THEN #{quoted_left_column_name} + :a - :c " +
543 "THEN #{quoted_left_column_name} + :a - :c " +
544 "ELSE #{quoted_left_column_name} END, " +
544 "ELSE #{quoted_left_column_name} END, " +
545 "#{quoted_right_column_name} = CASE " +
545 "#{quoted_right_column_name} = CASE " +
546 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
546 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
547 "THEN #{quoted_right_column_name} + :d - :b " +
547 "THEN #{quoted_right_column_name} + :d - :b " +
548 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
548 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
549 "THEN #{quoted_right_column_name} + :a - :c " +
549 "THEN #{quoted_right_column_name} + :a - :c " +
550 "ELSE #{quoted_right_column_name} END, " +
550 "ELSE #{quoted_right_column_name} END, " +
551 "#{quoted_parent_column_name} = CASE " +
551 "#{quoted_parent_column_name} = CASE " +
552 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
552 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
553 "ELSE #{quoted_parent_column_name} END",
553 "ELSE #{quoted_parent_column_name} END",
554 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
554 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
555 ])
555 ])
556 end
556 end
557 target.reload_nested_set if target
557 target.reload_nested_set if target
558 self.reload_nested_set
558 self.reload_nested_set
559 end
559 end
560 end
560 end
561
561
562 end
562 end
563
563
564 # Mixed into both classes and instances to provide easy access to the column names
564 # Mixed into both classes and instances to provide easy access to the column names
565 module Columns
565 module Columns
566 def left_column_name
566 def left_column_name
567 acts_as_nested_set_options[:left_column]
567 acts_as_nested_set_options[:left_column]
568 end
568 end
569
569
570 def right_column_name
570 def right_column_name
571 acts_as_nested_set_options[:right_column]
571 acts_as_nested_set_options[:right_column]
572 end
572 end
573
573
574 def parent_column_name
574 def parent_column_name
575 acts_as_nested_set_options[:parent_column]
575 acts_as_nested_set_options[:parent_column]
576 end
576 end
577
577
578 def scope_column_names
578 def scope_column_names
579 Array(acts_as_nested_set_options[:scope])
579 Array(acts_as_nested_set_options[:scope])
580 end
580 end
581
581
582 def quoted_left_column_name
582 def quoted_left_column_name
583 connection.quote_column_name(left_column_name)
583 connection.quote_column_name(left_column_name)
584 end
584 end
585
585
586 def quoted_right_column_name
586 def quoted_right_column_name
587 connection.quote_column_name(right_column_name)
587 connection.quote_column_name(right_column_name)
588 end
588 end
589
589
590 def quoted_parent_column_name
590 def quoted_parent_column_name
591 connection.quote_column_name(parent_column_name)
591 connection.quote_column_name(parent_column_name)
592 end
592 end
593
593
594 def quoted_scope_column_names
594 def quoted_scope_column_names
595 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
595 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
596 end
596 end
597 end
597 end
598
598
599 end
599 end
600 end
600 end
601 end
601 end
General Comments 0
You need to be logged in to leave comments. Login now