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