modified_acts_as_versioned 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG +82 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README +28 -0
  5. data/RUNNING_UNIT_TESTS +41 -0
  6. data/Rakefile +50 -0
  7. data/VERSION.yml +4 -0
  8. data/acts_as_versioned.gemspec +29 -0
  9. data/init.rb +1 -0
  10. data/lib/acts_as_versioned.rb +488 -0
  11. data/rdoc/classes/ActiveRecord/Acts/Versioned/ActMethods/ClassMethods.html +336 -0
  12. data/rdoc/classes/ActiveRecord/Acts/Versioned/ActMethods.html +581 -0
  13. data/rdoc/classes/ActiveRecord/Acts/Versioned/ClassMethods.html +506 -0
  14. data/rdoc/classes/ActiveRecord/Acts/Versioned.html +187 -0
  15. data/rdoc/created.rid +1 -0
  16. data/rdoc/files/CHANGELOG.html +288 -0
  17. data/rdoc/files/README.html +158 -0
  18. data/rdoc/files/RUNNING_UNIT_TESTS.html +158 -0
  19. data/rdoc/files/lib/acts_as_versioned_rb.html +129 -0
  20. data/rdoc/fr_class_index.html +30 -0
  21. data/rdoc/fr_file_index.html +30 -0
  22. data/rdoc/fr_method_index.html +54 -0
  23. data/rdoc/index.html +24 -0
  24. data/rdoc/rdoc-style.css +208 -0
  25. data/test/abstract_unit.rb +60 -0
  26. data/test/database.yml +18 -0
  27. data/test/fixtures/authors.yml +6 -0
  28. data/test/fixtures/landmark.rb +3 -0
  29. data/test/fixtures/landmark_versions.yml +7 -0
  30. data/test/fixtures/landmarks.yml +7 -0
  31. data/test/fixtures/locked_pages.yml +10 -0
  32. data/test/fixtures/locked_pages_revisions.yml +27 -0
  33. data/test/fixtures/migrations/1_add_versioned_tables.rb +15 -0
  34. data/test/fixtures/page.rb +48 -0
  35. data/test/fixtures/page_versions.yml +16 -0
  36. data/test/fixtures/pages.yml +8 -0
  37. data/test/fixtures/widget.rb +6 -0
  38. data/test/migration_test.rb +47 -0
  39. data/test/schema.rb +82 -0
  40. data/test/versioned_test.rb +379 -0
  41. metadata +114 -0
@@ -0,0 +1,488 @@
1
+ # Copyright (c) 2005 Rick Olson
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
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
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
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
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.
21
+
22
+ module ActiveRecord #: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
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.
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
29
+ # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30
+ #
31
+ # class Page < ActiveRecord::Base
32
+ # # assumes pages_versions table
33
+ # acts_as_versioned
34
+ # end
35
+ #
36
+ # Example:
37
+ #
38
+ # page = Page.create(:title => 'hello world!')
39
+ # page.version # => 1
40
+ #
41
+ # page.title = 'hello world'
42
+ # page.save
43
+ # page.version # => 2
44
+ # page.versions.size # => 2
45
+ #
46
+ # page.revert_to(1) # using version number
47
+ # page.title # => 'hello world!'
48
+ #
49
+ # page.revert_to(page.versions.last) # using versioned instance
50
+ # page.title # => 'hello world'
51
+ #
52
+ # page.versions.earliest # efficient query to find the first version
53
+ # page.versions.latest # efficient query to find the most recently created version
54
+ #
55
+ #
56
+ # Simple Queries to page between versions
57
+ #
58
+ # page.versions.before(version)
59
+ # page.versions.after(version)
60
+ #
61
+ # Access the previous/next versions from the versioned model itself
62
+ #
63
+ # version = page.versions.latest
64
+ # version.previous # go back one version
65
+ # version.next # go forward one version
66
+ #
67
+ # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68
+ module Versioned
69
+ CALLBACKS = [:set_new_version, :save_version, :save_version?]
70
+ def self.included(base) # :nodoc:
71
+ base.extend ClassMethods
72
+ end
73
+
74
+ module ClassMethods
75
+ # == Configuration options
76
+ #
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)
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)
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.
83
+ # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
84
+ # * <tt>versions_name</tt> - name to be used to access the versions.. DH
85
+ # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
86
+ # For finer control, pass either a Proc or modify Model#version_condition_met?
87
+ #
88
+ # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
89
+ #
90
+ # or...
91
+ #
92
+ # class Auction
93
+ # def version_condition_met? # totally bypasses the <tt>:if</tt> option
94
+ # !expired?
95
+ # end
96
+ # end
97
+ #
98
+ # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
99
+ # either a symbol or array of symbols.
100
+ #
101
+ # * <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
102
+ # to create an anonymous mixin:
103
+ #
104
+ # class Auction
105
+ # acts_as_versioned do
106
+ # def started?
107
+ # !started_at.nil?
108
+ # end
109
+ # end
110
+ # end
111
+ #
112
+ # or...
113
+ #
114
+ # module AuctionExtension
115
+ # def started?
116
+ # !started_at.nil?
117
+ # end
118
+ # end
119
+ # class Auction
120
+ # acts_as_versioned :extend => AuctionExtension
121
+ # end
122
+ #
123
+ # Example code:
124
+ #
125
+ # @auction = Auction.find(1)
126
+ # @auction.started?
127
+ # @auction.versions.first.started?
128
+ #
129
+ # == Database Schema
130
+ #
131
+ # The model that you're versioning needs to have a 'version' attribute. The model is versioned
132
+ # into a table called #{model}_versions where the model name is singlular. The _versions table should
133
+ # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
134
+ #
135
+ # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
136
+ # then that field is reflected in the versioned model as 'versioned_type' by default.
137
+ #
138
+ # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
139
+ # method, perfect for a migration. It will also create the version column if the main model does not already have it.
140
+ #
141
+ # class AddVersions < ActiveRecord::Migration
142
+ # def self.up
143
+ # # create_versioned_table takes the same options hash
144
+ # # that create_table does
145
+ # Post.create_versioned_table
146
+ # end
147
+ #
148
+ # def self.down
149
+ # Post.drop_versioned_table
150
+ # end
151
+ # end
152
+ #
153
+ # == Changing What Fields Are Versioned
154
+ #
155
+ # By default, acts_as_versioned will version all but these fields:
156
+ #
157
+ # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
158
+ #
159
+ # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
160
+ #
161
+ # class Post < ActiveRecord::Base
162
+ # acts_as_versioned
163
+ # self.non_versioned_columns << 'comments_count'
164
+ # end
165
+ #
166
+ def acts_as_versioned(options = {}, &extension)
167
+ # don't allow multiple calls
168
+ return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
169
+
170
+ send :include, ActiveRecord::Acts::Versioned::ActMethods
171
+
172
+ cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
173
+ :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
174
+ :version_association_options, :version_if_changed, :versions_name
175
+
176
+ self.versioned_class_name = options[:class_name] || "Version"
177
+ self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
178
+ self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
179
+ self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
180
+ self.version_column = options[:version_column] || 'version'
181
+ self.version_sequence_name = options[:sequence_name]
182
+ self.max_version_limit = options[:limit].to_i
183
+ self.version_condition = options[:if] || true
184
+ self.versions_name = options[:versions_name] || :versions
185
+ self.non_versioned_columns = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] + options[:non_versioned_columns].to_a.map(&:to_s)
186
+ self.version_association_options = {
187
+ :class_name => "#{self.to_s}::#{versioned_class_name}",
188
+ :foreign_key => versioned_foreign_key,
189
+ :dependent => :delete_all
190
+ }.merge(options[:association_options] || {})
191
+
192
+ if block_given?
193
+ extension_module_name = "#{versioned_class_name}Extension"
194
+ silence_warnings do
195
+ self.const_set(extension_module_name, Module.new(&extension))
196
+ end
197
+
198
+ options[:extend] = self.const_get(extension_module_name)
199
+ end
200
+
201
+ class_eval <<-CLASS_METHODS
202
+ has_many :#{versions_name.to_s}, version_association_options do
203
+ # finds earliest version of this record
204
+ def earliest
205
+ @earliest ||= find(:first, :order => '#{version_column}')
206
+ end
207
+
208
+ # find latest version of this record
209
+ def latest
210
+ @latest ||= find(:first, :order => '#{version_column} desc')
211
+ end
212
+ end
213
+ before_save :set_new_version
214
+ after_save :save_version
215
+ after_save :clear_old_versions
216
+
217
+ unless options[:if_changed].nil?
218
+ self.track_altered_attributes = true
219
+ options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
220
+ self.version_if_changed = options[:if_changed].map(&:to_s)
221
+ end
222
+
223
+ include options[:extend] if options[:extend].is_a?(Module)
224
+ CLASS_METHODS
225
+
226
+ # create the dynamic versioned model
227
+ const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
228
+ def self.reloadable? ; false ; end
229
+ # find first version before the given version
230
+ def self.before(version)
231
+ find :first, :order => 'version desc',
232
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
233
+ end
234
+
235
+ # find first version after the given version.
236
+ def self.after(version)
237
+ find :first, :order => 'version',
238
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
239
+ end
240
+
241
+ def previous
242
+ self.class.before(self)
243
+ end
244
+
245
+ def next
246
+ self.class.after(self)
247
+ end
248
+
249
+ def versions_count
250
+ page.version
251
+ end
252
+ end
253
+
254
+ versioned_class.cattr_accessor :original_class
255
+ versioned_class.original_class = self
256
+ versioned_class.set_table_name versioned_table_name
257
+ versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
258
+ :class_name => "::#{self.to_s}",
259
+ :foreign_key => versioned_foreign_key
260
+ versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
261
+ versioned_class.set_sequence_name version_sequence_name if version_sequence_name
262
+ end
263
+ end
264
+
265
+ module ActMethods
266
+ def self.included(base) # :nodoc:
267
+ base.extend ClassMethods
268
+ end
269
+
270
+ # Saves a version of the model in the versioned table. This is called in the after_save callback by default
271
+ def save_version
272
+ if @saving_version
273
+ @saving_version = nil
274
+ rev = self.class.versioned_class.new
275
+ clone_versioned_model(self, rev)
276
+ rev.send("#{self.class.version_column}=", send(self.class.version_column))
277
+ rev.send("#{self.class.versioned_foreign_key}=", id)
278
+ rev.save
279
+ end
280
+ end
281
+
282
+ # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
283
+ # Override this method to set your own criteria for clearing old versions.
284
+ def clear_old_versions
285
+ return if self.class.max_version_limit == 0
286
+ excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
287
+ if excess_baggage > 0
288
+ self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
289
+ end
290
+ end
291
+
292
+ # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
293
+ def revert_to(version)
294
+ if version.is_a?(self.class.versioned_class)
295
+ return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
296
+ else
297
+ return false unless version = self.send(self.versions_name).send("find_by_#{self.class.version_column}", version)
298
+ end
299
+ self.clone_versioned_model(version, self)
300
+ send("#{self.class.version_column}=", version.send(self.class.version_column))
301
+ true
302
+ end
303
+
304
+ # Reverts a model to a given version and saves the model.
305
+ # Takes either a version number or an instance of the versioned model
306
+ def revert_to!(version)
307
+ revert_to(version) ? save_without_revision : false
308
+ end
309
+
310
+ # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
311
+ def save_without_revision
312
+ save_without_revision!
313
+ true
314
+ rescue
315
+ false
316
+ end
317
+
318
+ def save_without_revision!
319
+ without_locking do
320
+ without_revision do
321
+ save!
322
+ end
323
+ end
324
+ end
325
+
326
+ def altered?
327
+ track_altered_attributes ? (version_if_changed - changed).length < version_if_changed.length : changed?
328
+ end
329
+
330
+ # Clones a model. Used when saving a new version or reverting a model's version.
331
+ def clone_versioned_model(orig_model, new_model)
332
+ self.class.versioned_columns.each do |col|
333
+ new_model.send("#{col.name}=", orig_model.send(col.name)) if orig_model.has_attribute?(col.name)
334
+ end
335
+
336
+ if orig_model.is_a?(self.class.versioned_class)
337
+ new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
338
+ elsif new_model.is_a?(self.class.versioned_class)
339
+ new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
340
+ end
341
+ end
342
+
343
+ # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
344
+ def save_version?
345
+ version_condition_met? && altered?
346
+ end
347
+
348
+ # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
349
+ # custom version condition checking.
350
+ def version_condition_met?
351
+ case
352
+ when version_condition.is_a?(Symbol)
353
+ send(version_condition)
354
+ when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
355
+ version_condition.call(self)
356
+ else
357
+ version_condition
358
+ end
359
+ end
360
+
361
+ # Executes the block with the versioning callbacks disabled.
362
+ #
363
+ # @foo.without_revision do
364
+ # @foo.save
365
+ # end
366
+ #
367
+ def without_revision(&block)
368
+ self.class.without_revision(&block)
369
+ end
370
+
371
+ # Turns off optimistic locking for the duration of the block
372
+ #
373
+ # @foo.without_locking do
374
+ # @foo.save
375
+ # end
376
+ #
377
+ def without_locking(&block)
378
+ self.class.without_locking(&block)
379
+ end
380
+
381
+ def empty_callback() end #:nodoc:
382
+
383
+ protected
384
+ # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
385
+ def set_new_version
386
+ @saving_version = new_record? || save_version?
387
+ self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
388
+ end
389
+
390
+ # Gets the next available version for the current record, or 1 for a new record
391
+ def next_version
392
+ (new_record? ? 0 : self.send(self.versions_name).calculate(:max, version_column).to_i) + 1
393
+ end
394
+
395
+ module ClassMethods
396
+ # Returns an array of columns that are versioned. See non_versioned_columns
397
+ def versioned_columns
398
+ @versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
399
+ end
400
+
401
+ # Returns an instance of the dynamic versioned model
402
+ def versioned_class
403
+ const_get versioned_class_name
404
+ end
405
+
406
+ # Rake migration task to create the versioned table using options passed to acts_as_versioned
407
+ def create_versioned_table(create_table_options = {})
408
+ # create version column in main table if it does not exist
409
+ if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
410
+ self.connection.add_column table_name, version_column, :integer
411
+ self.reset_column_information
412
+ end
413
+
414
+ return if connection.table_exists?(versioned_table_name)
415
+
416
+ self.connection.create_table(versioned_table_name, create_table_options) do |t|
417
+ t.column versioned_foreign_key, :integer
418
+ t.column version_column, :integer
419
+ end
420
+
421
+ self.versioned_columns.each do |col|
422
+ self.connection.add_column versioned_table_name, col.name, col.type,
423
+ :limit => col.limit,
424
+ :default => col.default,
425
+ :scale => col.scale,
426
+ :precision => col.precision
427
+ end
428
+
429
+ if type_col = self.columns_hash[inheritance_column]
430
+ self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
431
+ :limit => type_col.limit,
432
+ :default => type_col.default,
433
+ :scale => type_col.scale,
434
+ :precision => type_col.precision
435
+ end
436
+
437
+ self.connection.add_index versioned_table_name, versioned_foreign_key
438
+ end
439
+
440
+ # Rake migration task to drop the versioned table
441
+ def drop_versioned_table
442
+ self.connection.drop_table versioned_table_name
443
+ end
444
+
445
+ # Executes the block with the versioning callbacks disabled.
446
+ #
447
+ # Foo.without_revision do
448
+ # @foo.save
449
+ # end
450
+ #
451
+ def without_revision(&block)
452
+ class_eval do
453
+ CALLBACKS.each do |attr_name|
454
+ alias_method "orig_#{attr_name}".to_sym, attr_name
455
+ alias_method attr_name, :empty_callback
456
+ end
457
+ end
458
+ block.call
459
+ ensure
460
+ class_eval do
461
+ CALLBACKS.each do |attr_name|
462
+ alias_method attr_name, "orig_#{attr_name}".to_sym
463
+ end
464
+ end
465
+ end
466
+
467
+ # Turns off optimistic locking for the duration of the block
468
+ #
469
+ # Foo.without_locking do
470
+ # @foo.save
471
+ # end
472
+ #
473
+ def without_locking(&block)
474
+ current = ActiveRecord::Base.lock_optimistically
475
+ ActiveRecord::Base.lock_optimistically = false if current
476
+ begin
477
+ block.call
478
+ ensure
479
+ ActiveRecord::Base.lock_optimistically = true if current
480
+ end
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+
488
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned