kkorach-acts_as_revisable 0.9.7

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,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