acts_as_revisable 1.1.1

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.
@@ -0,0 +1,33 @@
1
+ module WithoutScope
2
+ module ActsAsRevisable
3
+ module Deletable
4
+ def self.included(base)
5
+ base.instance_eval do
6
+ define_callbacks :before_revise_on_destroy, :after_revise_on_destroy
7
+ end
8
+ end
9
+
10
+ def destroy
11
+ now = Time.current
12
+
13
+ prev = self.revisions.first
14
+ self.revisable_deleted_at = now
15
+ self.revisable_is_current = false
16
+
17
+ self.revisable_current_at = if prev
18
+ prev.update_attribute(:revisable_revised_at, now)
19
+ prev.revisable_revised_at + 1.second
20
+ else
21
+ self.created_at
22
+ end
23
+
24
+ self.revisable_revised_at = self.revisable_deleted_at
25
+
26
+ return false unless run_callbacks(:before_revise_on_destroy) { |r, o| r == false}
27
+ returning(self.save(:without_revision => true)) do
28
+ run_callbacks(:after_revise_on_destroy)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,485 @@
1
+ module WithoutScope
2
+ module ActsAsRevisable
3
+
4
+ # This module is mixed into the revision classes.
5
+ #
6
+ # ==== Callbacks
7
+ #
8
+ # * +before_revise+ is called before the record is revised.
9
+ # * +after_revise+ is called after the record is revised.
10
+ # * +before_revert+ is called before the record is reverted.
11
+ # * +after_revert+ is called after the record is reverted.
12
+ # * +before_changeset+ is called before a changeset block is called.
13
+ # * +after_changeset+ is called after a changeset block is called.
14
+ # * +after_branch_created+ is called on the new revisable instance
15
+ # created by branching after it's been created.
16
+ module Revisable
17
+ def self.included(base) #:nodoc:
18
+ base.send(:extend, ClassMethods)
19
+
20
+ class << base
21
+ attr_accessor :revisable_revision_class, :revisable_columns
22
+ end
23
+
24
+ base.class_inheritable_hash :revisable_shared_objects
25
+ base.revisable_shared_objects = {}
26
+
27
+ base.instance_eval do
28
+ attr_accessor :revisable_new_params, :revisable_revision
29
+
30
+ define_callbacks :before_revise, :after_revise, :before_revert, :after_revert, :before_changeset, :after_changeset, :after_branch_created
31
+
32
+ before_create :before_revisable_create
33
+ before_update :before_revisable_update
34
+ after_update :after_revisable_update
35
+ after_save :clear_revisable_shared_objects!, :unless => :is_reverting?
36
+
37
+ default_scope :conditions => {:revisable_is_current => true}
38
+
39
+ [:revisions, revisions_association_name.to_sym].each do |assoc|
40
+ has_many assoc, (revisable_options.revision_association_options || {}).merge({:class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "#{quoted_table_name}.#{connection.quote_column_name(:revisable_number)} DESC", :dependent => :destroy})
41
+ end
42
+ end
43
+
44
+ if !Object.const_defined?(base.revision_class_name) && base.revisable_options.generate_revision_class?
45
+ Object.const_set(base.revision_class_name, Class.new(ActiveRecord::Base)).instance_eval do
46
+ acts_as_revision
47
+ end
48
+ end
49
+ end
50
+
51
+ # Finds a specific revision of self.
52
+ #
53
+ # The +by+ parameter can be a revision_class instance,
54
+ # the symbols :first, :previous or :last, a Time instance
55
+ # or an Integer.
56
+ #
57
+ # When passed a revision_class instance, this method
58
+ # simply returns it. This is used primarily by revert_to!.
59
+ #
60
+ # When passed :first it returns the first revision created.
61
+ #
62
+ # When passed :previous or :last it returns the last revision
63
+ # created.
64
+ #
65
+ # When passed a Time instance it returns the revision that
66
+ # was the current record at the given time.
67
+ #
68
+ # When passed an Integer it returns the revision with that
69
+ # revision_number.
70
+ def find_revision(by)
71
+ by = Integer(by) if by.is_a?(String) && by.match(/[0-9]+/)
72
+
73
+ case by
74
+ when self.class
75
+ by
76
+ when self.class.revision_class
77
+ by
78
+ when :first
79
+ revisions.last
80
+ when :previous, :last
81
+ revisions.first
82
+ when Time
83
+ revisions.find(:first, :conditions => ["? >= ? and ? <= ?", :revisable_revised_at, by, :revisable_current_at, by])
84
+ when self.revisable_number
85
+ self
86
+ else
87
+ revisions.find_by_revisable_number(by)
88
+ end
89
+ end
90
+
91
+ # Returns a revisable_class instance initialized with the record
92
+ # found using find_revision.
93
+ #
94
+ # The +what+ parameter is simply passed to find_revision and the
95
+ # returned record forms the basis of the reverted record.
96
+ #
97
+ # ==== Callbacks
98
+ #
99
+ # * +before_revert+ is called before the record is reverted.
100
+ # * +after_revert+ is called after the record is reverted.
101
+ #
102
+ # If :without_revision => true has not been passed the
103
+ # following callbacks are also called:
104
+ #
105
+ # * +before_revise+ is called before the record is revised.
106
+ # * +after_revise+ is called after the record is revised.
107
+ def revert_to(what, *args, &block) #:yields:
108
+ is_reverting!
109
+
110
+ unless run_callbacks(:before_revert) { |r, o| r == false}
111
+ raise ActiveRecord::RecordNotSaved
112
+ end
113
+
114
+ options = args.extract_options!
115
+
116
+ rev = find_revision(what)
117
+ self.reverting_to, self.reverting_from = rev, self
118
+
119
+ unless rev.run_callbacks(:before_restore) { |r, o| r == false}
120
+ raise ActiveRecord::RecordNotSaved
121
+ end
122
+
123
+ self.class.column_names.each do |col|
124
+ next unless self.class.revisable_should_clone_column? col
125
+ self[col] = rev[col]
126
+ end
127
+
128
+ self.no_revision! if options.delete :without_revision
129
+ self.revisable_new_params = options
130
+
131
+ yield(self) if block_given?
132
+ rev.run_callbacks(:after_restore)
133
+ run_callbacks(:after_revert)
134
+ self
135
+ ensure
136
+ is_reverting!(false)
137
+ clear_revisable_shared_objects!
138
+ end
139
+
140
+ # Same as revert_to except it also saves the record.
141
+ def revert_to!(what, *args)
142
+ revert_to(what, *args) do
143
+ self.no_revision? ? save! : revise!
144
+ end
145
+ end
146
+
147
+ # Equivalent to:
148
+ # revert_to(:without_revision => true)
149
+ def revert_to_without_revision(*args)
150
+ options = args.extract_options!
151
+ options.update({:without_revision => true})
152
+ revert_to(*(args << options))
153
+ end
154
+
155
+ # Equivalent to:
156
+ # revert_to!(:without_revision => true)
157
+ def revert_to_without_revision!(*args)
158
+ options = args.extract_options!
159
+ options.update({:without_revision => true})
160
+ revert_to!(*(args << options))
161
+ end
162
+
163
+ # Globally sets the reverting state of this record.
164
+ def is_reverting!(val=true) #:nodoc:
165
+ set_revisable_state(:reverting, val)
166
+ end
167
+
168
+ # Returns true if the _record_ (not just this instance
169
+ # of the record) is currently being reverted.
170
+ def is_reverting?
171
+ get_revisable_state(:reverting) || false
172
+ end
173
+
174
+ # Sets whether or not to force a revision.
175
+ def force_revision!(val=true) #:nodoc:
176
+ set_revisable_state(:force_revision, val)
177
+ end
178
+
179
+ # Returns true if a revision should be forced.
180
+ def force_revision? #:nodoc:
181
+ get_revisable_state(:force_revision) || false
182
+ end
183
+
184
+ # Sets whether or not a revision should be created.
185
+ def no_revision!(val=true) #:nodoc:
186
+ set_revisable_state(:no_revision, val)
187
+ end
188
+
189
+ # Returns true if no revision should be created.
190
+ def no_revision? #:nodoc:
191
+ get_revisable_state(:no_revision) || false
192
+ end
193
+
194
+ # Force an immediate revision whether or
195
+ # not any columns have been modified.
196
+ #
197
+ # The +args+ catch-all argument is not used. It's primarily
198
+ # there to allow +revise!+ to be used directly as an association
199
+ # callback since association callbacks are passed an argument.
200
+ #
201
+ # ==== Callbacks
202
+ #
203
+ # * +before_revise+ is called before the record is revised.
204
+ # * +after_revise+ is called after the record is revised.
205
+ def revise!(*args)
206
+ return if in_revision?
207
+
208
+ begin
209
+ force_revision!
210
+ in_revision!
211
+ save!
212
+ ensure
213
+ in_revision!(false)
214
+ force_revision!(false)
215
+ end
216
+ end
217
+
218
+ # Groups statements that could trigger several revisions into
219
+ # a single revision. The revision is created once #save is called.
220
+ #
221
+ # ==== Example
222
+ #
223
+ # @project.revision_number # => 1
224
+ # @project.changeset do |project|
225
+ # # each one of the following statements would
226
+ # # normally trigger a revision
227
+ # project.update_attribute(:name, "new name")
228
+ # project.revise!
229
+ # project.revise!
230
+ # end
231
+ # @project.save
232
+ # @project.revision_number # => 2
233
+ #
234
+ # ==== Callbacks
235
+ #
236
+ # * +before_changeset+ is called before a changeset block is called.
237
+ # * +after_changeset+ is called after a changeset block is called.
238
+ def changeset(&block)
239
+ return unless block_given?
240
+
241
+ return yield(self) if in_revision?
242
+
243
+ unless run_callbacks(:before_changeset) { |r, o| r == false}
244
+ raise ActiveRecord::RecordNotSaved
245
+ end
246
+
247
+ begin
248
+ force_revision!
249
+ in_revision!
250
+
251
+ returning(yield(self)) do
252
+ run_callbacks(:after_changeset)
253
+ end
254
+ ensure
255
+ in_revision!(false)
256
+ end
257
+ end
258
+
259
+ # Same as +changeset+ except it also saves the record.
260
+ def changeset!(&block)
261
+ changeset do
262
+ block.call(self)
263
+ save!
264
+ end
265
+ end
266
+
267
+ def without_revisions!
268
+ return if in_revision? || !block_given?
269
+
270
+ begin
271
+ no_revision!
272
+ in_revision!
273
+ yield
274
+ save!
275
+ ensure
276
+ in_revision!(false)
277
+ no_revision!(false)
278
+ end
279
+ end
280
+
281
+ # acts_as_revisable's override for ActiveRecord::Base's #save!
282
+ def save!(*args) #:nodoc:
283
+ self.revisable_new_params ||= args.extract_options!
284
+ self.no_revision! if self.revisable_new_params.delete :without_revision
285
+ super
286
+ end
287
+
288
+ # acts_as_revisable's override for ActiveRecord::Base's #save
289
+ def save(*args) #:nodoc:
290
+ self.revisable_new_params ||= args.extract_options!
291
+ self.no_revision! if self.revisable_new_params.delete :without_revision
292
+ super(args)
293
+ end
294
+
295
+ # Set some defaults for a newly created +Revisable+ instance.
296
+ def before_revisable_create #:nodoc:
297
+ self[:revisable_is_current] = true
298
+ self.revision_number ||= 0
299
+ end
300
+
301
+ # Checks whether or not a +Revisable+ should be revised.
302
+ def should_revise? #:nodoc:
303
+ return false if new_record?
304
+ return true if force_revision?
305
+ return false if no_revision?
306
+ return false unless self.changed?
307
+ !(self.changed.map(&:downcase) & self.class.revisable_watch_columns).blank?
308
+ end
309
+
310
+ # Checks whether or not a revision should be stored.
311
+ # If it should be, it initialized the revision_class
312
+ # and stores it in an accessor for later saving.
313
+ def before_revisable_update #:nodoc:
314
+ return unless should_revise?
315
+ in_revision!
316
+
317
+ unless run_callbacks(:before_revise) { |r, o| r == false}
318
+ in_revision!(false)
319
+ return false
320
+ end
321
+
322
+ self.revisable_revision = self.to_revision
323
+ end
324
+
325
+ # Checks if an initialized revision_class has been stored
326
+ # in the accessor. If it has been, this instance is saved.
327
+ def after_revisable_update #:nodoc:
328
+ if no_revision? # check and see if no_revision! was called in a callback
329
+ self.revisable_revision = nil
330
+ return true
331
+ elsif self.revisable_revision
332
+ self.revisable_revision.save
333
+ revisions.reload
334
+ run_callbacks(:after_revise)
335
+ end
336
+ in_revision!(false)
337
+ force_revision!(false)
338
+ true
339
+ end
340
+
341
+ # Returns true if the _record_ (not just this instance
342
+ # of the record) is currently being revised.
343
+ def in_revision?
344
+ get_revisable_state(:revision)
345
+ end
346
+
347
+ # Manages the internal state of a +Revisable+ controlling
348
+ # whether or not a record is being revised. This works across
349
+ # instances and is keyed on primary_key.
350
+ def in_revision!(val=true) #:nodoc:
351
+ set_revisable_state(:revision, val)
352
+ end
353
+
354
+ # This returns a new +Revision+ instance with all the appropriate
355
+ # values initialized.
356
+ def to_revision #:nodoc:
357
+ rev = self.class.revision_class.new(self.revisable_new_params)
358
+
359
+ rev.revisable_original_id = self.id
360
+
361
+ new_revision_number = revisions.maximum(:revisable_number) + 1 rescue self.revision_number
362
+ rev.revision_number = new_revision_number
363
+ self.revision_number = new_revision_number + 1
364
+
365
+ self.class.column_names.each do |col|
366
+ next unless self.class.revisable_should_clone_column? col
367
+ val = self.send("#{col}_changed?") ? self.send("#{col}_was") : self.send(col)
368
+ rev.send("#{col}=", val)
369
+ end
370
+
371
+ self.revisable_new_params = nil
372
+
373
+ rev
374
+ end
375
+
376
+ # This returns
377
+ def current_revision
378
+ self
379
+ end
380
+
381
+ def for_revision
382
+ key = self.read_attribute(self.class.primary_key)
383
+ self.class.revisable_shared_objects[key] ||= {}
384
+ end
385
+
386
+ def reverting_to
387
+ for_revision[:reverting_to]
388
+ end
389
+
390
+ def reverting_to=(val)
391
+ for_revision[:reverting_to] = val
392
+ end
393
+
394
+ def reverting_from
395
+ for_revision[:reverting_from]
396
+ end
397
+
398
+ def reverting_from=(val)
399
+ for_revision[:reverting_from] = val
400
+ end
401
+
402
+ def clear_revisable_shared_objects!
403
+ key = self.read_attribute(self.class.primary_key)
404
+ self.class.revisable_shared_objects.delete(key)
405
+ end
406
+
407
+ module ClassMethods
408
+ # acts_as_revisable's override for with_scope that allows for
409
+ # including revisions in the scope.
410
+ #
411
+ # ==== Example
412
+ #
413
+ # with_scope(:with_revisions => true) do
414
+ # ...
415
+ # end
416
+ def with_scope(*args, &block) #:nodoc:
417
+ options = (args.grep(Hash).first || {})[:find]
418
+
419
+ if options && options.delete(:with_revisions)
420
+ with_exclusive_scope do
421
+ super(*args, &block)
422
+ end
423
+ else
424
+ super(*args, &block)
425
+ end
426
+ end
427
+
428
+ # acts_as_revisable's override for find that allows for
429
+ # including revisions in the find.
430
+ #
431
+ # ==== Example
432
+ #
433
+ # find(:all, :with_revisions => true)
434
+ def find(*args) #:nodoc:
435
+ options = args.grep(Hash).first
436
+
437
+ if options && options.delete(:with_revisions)
438
+ with_exclusive_scope do
439
+ super(*args)
440
+ end
441
+ else
442
+ super(*args)
443
+ end
444
+ end
445
+
446
+ # Returns the +revision_class_name+ as configured in
447
+ # +acts_as_revisable+.
448
+ def revision_class_name #:nodoc:
449
+ self.revisable_options.revision_class_name || "#{self.name}Revision"
450
+ end
451
+
452
+ # Returns the actual +Revision+ class based on the
453
+ # #revision_class_name.
454
+ def revision_class #:nodoc:
455
+ self.revisable_revision_class ||= self.revision_class_name.constantize
456
+ end
457
+
458
+ # Returns the revisable_class which in this case is simply +self+.
459
+ def revisable_class #:nodoc:
460
+ self
461
+ end
462
+
463
+ # Returns the name of the association acts_as_revisable
464
+ # creates.
465
+ def revisions_association_name #:nodoc:
466
+ revision_class_name.pluralize.underscore
467
+ end
468
+
469
+ # Returns an Array of the columns that are watched for changes.
470
+ def revisable_watch_columns #:nodoc:
471
+ return self.revisable_columns unless self.revisable_columns.blank?
472
+ return self.revisable_columns ||= [] if self.revisable_options.except == :all
473
+ return self.revisable_columns ||= [self.revisable_options.only].flatten.map(&:to_s).map(&:downcase) unless self.revisable_options.only.blank?
474
+
475
+ except = [self.revisable_options.except].flatten || []
476
+ except += REVISABLE_SYSTEM_COLUMNS
477
+ except += REVISABLE_UNREVISABLE_COLUMNS
478
+ except.uniq!
479
+
480
+ self.revisable_columns ||= (column_names - except.map(&:to_s)).flatten.map(&:downcase)
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end