##// END OF EJS Templates
acts_as_versioned use old style (Rails 2.x) of method call for #all (#24348)....
Jean-Philippe Lang -
r15568:2b70d977052a
parent child
Show More
@@ -1,568 +1,569
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 219 @earliest ||= order('version').first
220 220 end
221 221
222 222 # find latest version of this record
223 223 def latest
224 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 order('version desc').
252 252 where("#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version).
253 253 first
254 254 end
255 255
256 256 # find first version after the given version.
257 257 def self.after(version)
258 258 order('version').
259 259 where("#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version).
260 260 first
261 261 end
262 262
263 263 def previous
264 264 self.class.before(self)
265 265 end
266 266
267 267 def next
268 268 self.class.after(self)
269 269 end
270 270
271 271 def versions_count
272 272 page.version
273 273 end
274 274 end
275 275
276 276 versioned_class.cattr_accessor :original_class
277 277 versioned_class.original_class = self
278 278 versioned_class.table_name = versioned_table_name
279 279 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
280 280 :class_name => "::#{self.to_s}",
281 281 :foreign_key => versioned_foreign_key
282 282 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
283 283 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
284 284 end
285 285 end
286 286
287 287 module ActMethods
288 288 def self.included(base) # :nodoc:
289 289 base.extend ClassMethods
290 290 end
291 291
292 292 # Finds a specific version of this record
293 293 def find_version(version = nil)
294 294 self.class.find_version(id, version)
295 295 end
296 296
297 297 # Saves a version of the model if applicable
298 298 def save_version
299 299 save_version_on_create if save_version?
300 300 end
301 301
302 302 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
303 303 def save_version_on_create
304 304 rev = self.class.versioned_class.new
305 305 self.clone_versioned_model(self, rev)
306 306 rev.version = send(self.class.version_column)
307 307 rev.send("#{self.class.versioned_foreign_key}=", self.id)
308 308 rev.save
309 309 end
310 310
311 311 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
312 312 # Override this method to set your own criteria for clearing old versions.
313 313 def clear_old_versions
314 314 return if self.class.max_version_limit == 0
315 315 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
316 316 if excess_baggage > 0
317 317 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
318 318 self.class.versioned_class.connection.execute sql
319 319 end
320 320 end
321 321
322 322 def versions_count
323 323 version
324 324 end
325 325
326 326 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
327 327 def revert_to(version)
328 328 if version.is_a?(self.class.versioned_class)
329 329 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
330 330 else
331 331 return false unless version = versions.find_by_version(version)
332 332 end
333 333 self.clone_versioned_model(version, self)
334 334 self.send("#{self.class.version_column}=", version.version)
335 335 true
336 336 end
337 337
338 338 # Reverts a model to a given version and saves the model.
339 339 # Takes either a version number or an instance of the versioned model
340 340 def revert_to!(version)
341 341 revert_to(version) ? save_without_revision : false
342 342 end
343 343
344 344 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
345 345 def save_without_revision
346 346 save_without_revision!
347 347 true
348 348 rescue
349 349 false
350 350 end
351 351
352 352 def save_without_revision!
353 353 without_locking do
354 354 without_revision do
355 355 save!
356 356 end
357 357 end
358 358 end
359 359
360 360 # Returns an array of attribute keys that are versioned. See non_versioned_columns
361 361 def versioned_attributes
362 362 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
363 363 end
364 364
365 365 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
366 366 # If called with a single parameter, gets whether the parameter has changed.
367 367 def changed?(attr_name = nil)
368 368 attr_name.nil? ?
369 369 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
370 370 (altered_attributes && altered_attributes.include?(attr_name.to_s))
371 371 end
372 372
373 373 # keep old dirty? method
374 374 alias_method :dirty?, :changed?
375 375
376 376 # Clones a model. Used when saving a new version or reverting a model's version.
377 377 def clone_versioned_model(orig_model, new_model)
378 378 self.versioned_attributes.each do |key|
379 379 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
380 380 end
381 381
382 382 if self.class.columns_hash.include?(self.class.inheritance_column)
383 383 if orig_model.is_a?(self.class.versioned_class)
384 384 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
385 385 elsif new_model.is_a?(self.class.versioned_class)
386 386 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
387 387 end
388 388 end
389 389 end
390 390
391 391 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
392 392 def save_version?
393 393 version_condition_met? && changed?
394 394 end
395 395
396 396 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
397 397 # custom version condition checking.
398 398 def version_condition_met?
399 399 case
400 400 when version_condition.is_a?(Symbol)
401 401 send(version_condition)
402 402 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
403 403 version_condition.call(self)
404 404 else
405 405 version_condition
406 406 end
407 407 end
408 408
409 409 # Executes the block with the versioning callbacks disabled.
410 410 #
411 411 # @foo.without_revision do
412 412 # @foo.save
413 413 # end
414 414 #
415 415 def without_revision(&block)
416 416 self.class.without_revision(&block)
417 417 end
418 418
419 419 # Turns off optimistic locking for the duration of the block
420 420 #
421 421 # @foo.without_locking do
422 422 # @foo.save
423 423 # end
424 424 #
425 425 def without_locking(&block)
426 426 self.class.without_locking(&block)
427 427 end
428 428
429 429 def empty_callback() end #:nodoc:
430 430
431 431 protected
432 432 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
433 433 def set_new_version
434 434 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
435 435 end
436 436
437 437 # Gets the next available version for the current record, or 1 for a new record
438 438 def next_version
439 439 return 1 if new_record?
440 440 (versions.maximum('version') || 0) + 1
441 441 end
442 442
443 443 # clears current changed attributes. Called after save.
444 444 def clear_altered_attributes
445 445 self.altered_attributes = []
446 446 end
447 447
448 448 def write_changed_attribute(attr_name, attr_value)
449 449 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
450 450 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast_from_database(attr_value)
451 451 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
452 452 write_attribute(attr_name, attr_value_for_db)
453 453 end
454 454
455 455 module ClassMethods
456 456 # Finds a specific version of a specific row of this model
457 457 def find_version(id, version = nil)
458 458 return find(id) unless version
459 459
460 460 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
461 461 options = { :conditions => conditions, :limit => 1 }
462 462
463 463 if result = find_versions(id, options).first
464 464 result
465 465 else
466 466 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
467 467 end
468 468 end
469 469
470 470 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
471 471 def find_versions(id, options = {})
472 versioned_class.all({
473 :conditions => ["#{versioned_foreign_key} = ?", id],
474 :order => 'version' }.merge(options))
472 versioned_class.
473 where(options[:conditions] || {versioned_foreign_key => id}).
474 limit(options[:limit]).
475 order('version')
475 476 end
476 477
477 478 # Returns an array of columns that are versioned. See non_versioned_columns
478 479 def versioned_columns
479 480 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
480 481 end
481 482
482 483 # Returns an instance of the dynamic versioned model
483 484 def versioned_class
484 485 const_get versioned_class_name
485 486 end
486 487
487 488 # Rake migration task to create the versioned table using options passed to acts_as_versioned
488 489 def create_versioned_table(create_table_options = {})
489 490 # create version column in main table if it does not exist
490 491 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
491 492 self.connection.add_column table_name, :version, :integer
492 493 end
493 494
494 495 self.connection.create_table(versioned_table_name, create_table_options) do |t|
495 496 t.column versioned_foreign_key, :integer
496 497 t.column :version, :integer
497 498 end
498 499
499 500 updated_col = nil
500 501 self.versioned_columns.each do |col|
501 502 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
502 503 self.connection.add_column versioned_table_name, col.name, col.type,
503 504 :limit => col.limit,
504 505 :default => col.default,
505 506 :scale => col.scale,
506 507 :precision => col.precision
507 508 end
508 509
509 510 if type_col = self.columns_hash[inheritance_column]
510 511 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
511 512 :limit => type_col.limit,
512 513 :default => type_col.default,
513 514 :scale => type_col.scale,
514 515 :precision => type_col.precision
515 516 end
516 517
517 518 if updated_col.nil?
518 519 self.connection.add_column versioned_table_name, :updated_at, :timestamp
519 520 end
520 521 end
521 522
522 523 # Rake migration task to drop the versioned table
523 524 def drop_versioned_table
524 525 self.connection.drop_table versioned_table_name
525 526 end
526 527
527 528 # Executes the block with the versioning callbacks disabled.
528 529 #
529 530 # Foo.without_revision do
530 531 # @foo.save
531 532 # end
532 533 #
533 534 def without_revision(&block)
534 535 class_eval do
535 536 CALLBACKS.each do |attr_name|
536 537 alias_method "orig_#{attr_name}".to_sym, attr_name
537 538 alias_method attr_name, :empty_callback
538 539 end
539 540 end
540 541 block.call
541 542 ensure
542 543 class_eval do
543 544 CALLBACKS.each do |attr_name|
544 545 alias_method attr_name, "orig_#{attr_name}".to_sym
545 546 end
546 547 end
547 548 end
548 549
549 550 # Turns off optimistic locking for the duration of the block
550 551 #
551 552 # Foo.without_locking do
552 553 # @foo.save
553 554 # end
554 555 #
555 556 def without_locking(&block)
556 557 current = ActiveRecord::Base.lock_optimistically
557 558 ActiveRecord::Base.lock_optimistically = false if current
558 559 result = block.call
559 560 ActiveRecord::Base.lock_optimistically = true if current
560 561 result
561 562 end
562 563 end
563 564 end
564 565 end
565 566 end
566 567 end
567 568
568 569 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now