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.
- data/LICENSE +20 -0
- data/README.rdoc +221 -0
- data/Rakefile +44 -0
- data/generators/revisable_migration/revisable_migration_generator.rb +21 -0
- data/generators/revisable_migration/templates/migration.rb +14 -0
- data/lib/acts_as_revisable.rb +10 -0
- data/lib/acts_as_revisable/acts/common.rb +209 -0
- data/lib/acts_as_revisable/acts/deletable.rb +33 -0
- data/lib/acts_as_revisable/acts/revisable.rb +485 -0
- data/lib/acts_as_revisable/acts/revision.rb +148 -0
- data/lib/acts_as_revisable/base.rb +54 -0
- data/lib/acts_as_revisable/gem_spec_options.rb +18 -0
- data/lib/acts_as_revisable/options.rb +22 -0
- data/lib/acts_as_revisable/quoted_columns.rb +31 -0
- data/lib/acts_as_revisable/validations.rb +11 -0
- data/lib/acts_as_revisable/version.rb +11 -0
- data/rails/init.rb +1 -0
- data/spec/associations_spec.rb +22 -0
- data/spec/branch_spec.rb +42 -0
- data/spec/deletable_spec.rb +16 -0
- data/spec/find_spec.rb +34 -0
- data/spec/general_spec.rb +115 -0
- data/spec/options_spec.rb +83 -0
- data/spec/quoted_columns_spec.rb +19 -0
- data/spec/revert_spec.rb +42 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/sti_spec.rb +42 -0
- data/spec/validations_spec.rb +25 -0
- metadata +86 -0
@@ -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
|