acts_as_revisable 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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