kkorach-acts_as_revisable 0.9.7

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