rich-acts_as_revisable 0.6.0 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,113 +1,120 @@
1
1
  module FatJam
2
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.
3
16
  module Revisable
4
- def self.included(base)
17
+ def self.included(base) #:nodoc:
5
18
  base.send(:extend, ClassMethods)
6
-
7
- base.class_inheritable_hash :aa_revisable_current_revisions
8
- base.aa_revisable_current_revisions = {}
9
-
19
+
10
20
  class << base
11
21
  alias_method_chain :find, :revisable
12
22
  alias_method_chain :with_scope, :revisable
23
+ attr_accessor :revisable_revision_class, :revisable_columns
13
24
  end
14
-
25
+
26
+ base.class_inheritable_hash :revisable_shared_objects
27
+ base.revisable_shared_objects = {}
28
+
15
29
  base.instance_eval do
30
+ attr_accessor :revisable_new_params, :revisable_revision
31
+
16
32
  define_callbacks :before_revise, :after_revise, :before_revert, :after_revert, :before_changeset, :after_changeset, :after_branch_created
17
-
33
+
18
34
  alias_method_chain :save, :revisable
19
35
  alias_method_chain :save!, :revisable
20
-
21
- acts_as_scoped_model :find => {:conditions => {:revisable_is_current => true}}
22
-
23
- has_many :revisions, :class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "revisable_number DESC", :dependent => :destroy
24
- has_many revision_class_name.pluralize.downcase.to_sym, :class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "revisable_number DESC", :dependent => :destroy
25
36
 
26
37
  before_create :before_revisable_create
27
38
  before_update :before_revisable_update
28
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
29
47
  end
30
48
  end
31
-
32
- def before_revisable_create
33
- self[:revisable_is_current] = true
34
- end
35
-
36
- def should_revise?
37
- return true if @aa_revisable_force_revision == true
38
- return false if @aa_revisable_no_revision == true
39
- return false unless self.changed?
40
- !(self.changed.map(&:downcase) & self.class.revisable_columns).blank?
41
- end
42
-
43
- def before_revisable_update
44
- return unless should_revise?
45
- return false unless run_callbacks(:before_revise) { |r, o| r == false}
46
-
47
- @revisable_revision = self.to_revision
48
- end
49
-
50
- def after_revisable_update
51
- if @revisable_revision
52
- @revisable_revision.save
53
- @aa_revisable_was_revised = true
54
- revisions.reload
55
- run_callbacks(:after_revise)
56
- end
57
- end
58
-
59
- def to_revision
60
- rev = self.class.revision_class.new(@aa_revisable_new_params)
61
49
 
62
- rev.revisable_original_id = self.id
63
-
64
- self.class.column_names.each do |col|
65
- next unless self.class.revisable_should_clone_column? col
66
- val = self.send("#{col}_changed?") ? self.send("#{col}_was") : self.send(col)
67
- rev.send("#{col}=", val)
68
- end
69
-
70
- @aa_revisable_new_params = nil
71
-
72
- rev
73
- end
74
-
75
- def save_with_revisable!(*args)
76
- @aa_revisable_new_params ||= args.extract_options!
77
- @aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
78
- save_without_revisable!(*args)
79
- end
80
-
81
- def save_with_revisable(*args)
82
- @aa_revisable_new_params ||= args.extract_options!
83
- @aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
84
- save_without_revisable(*args)
85
- end
86
-
87
- def find_revision(number)
88
- revisions.find_by_revisable_number(number)
89
- end
90
-
91
- def revert_to!(*args)
92
- unless run_callbacks(:before_revert) { |r, o| r == false}
93
- raise ActiveRecord::RecordNotSaved
94
- end
95
-
96
- options = args.extract_options!
97
-
98
- rev = case args.first
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
99
75
  when self.class.revision_class
100
- args.first
76
+ by
101
77
  when :first
102
78
  revisions.last
103
79
  when :previous
104
80
  revisions.first
105
- when Fixnum
106
- revisions.find_by_revisable_number(args.first)
107
81
  when Time
108
- revisions.find(:first, :conditions => ["? >= ? and ? <= ?", :revisable_revised_at, args.first, :revisable_current_at, args.first])
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)
109
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!
110
114
 
115
+ rev = find_revision(what)
116
+ self.reverting_to, self.reverting_from = rev, self
117
+
111
118
  unless rev.run_callbacks(:before_restore) { |r, o| r == false}
112
119
  raise ActiveRecord::RecordNotSaved
113
120
  end
@@ -117,49 +124,112 @@ module FatJam
117
124
  self[col] = rev[col]
118
125
  end
119
126
 
120
- @aa_revisable_no_revision = true if options.delete :without_revision
121
- @aa_revisable_new_params = options
122
-
123
- returning(@aa_revisable_no_revision ? save! : revise!) do
124
- rev.run_callbacks(:after_restore)
125
- run_callbacks(:after_revert)
126
- end
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!
127
137
  end
128
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)
129
156
  def revert_to_without_revision!(*args)
130
157
  options = args.extract_options!
131
158
  options.update({:without_revision => true})
132
159
  revert_to!(*(args << options))
133
160
  end
134
-
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.
135
200
  def revise!
136
201
  return if in_revision?
137
202
 
138
203
  begin
139
- @aa_revisable_force_revision = true
204
+ force_revision!
140
205
  in_revision!
141
206
  save!
142
207
  ensure
143
208
  in_revision!(false)
144
- @aa_revisable_force_revision = false
209
+ force_revision!(false)
145
210
  end
146
211
  end
147
-
148
- def revised?
149
- @aa_revisable_was_revised || false
150
- end
151
-
152
- def in_revision?
153
- key = self.read_attribute(self.class.primary_key)
154
- aa_revisable_current_revisions[key] || false
155
- end
156
-
157
- def in_revision!(val=true)
158
- key = self.read_attribute(self.class.primary_key)
159
- aa_revisable_current_revisions[key] = val
160
- aa_revisable_current_revisions.delete(key) unless val
161
- end
162
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.
163
233
  def changeset(&block)
164
234
  return unless block_given?
165
235
 
@@ -170,24 +240,169 @@ module FatJam
170
240
  end
171
241
 
172
242
  begin
243
+ force_revision!
173
244
  in_revision!
174
245
 
175
246
  returning(yield(self)) do
176
247
  run_callbacks(:after_changeset)
177
248
  end
178
249
  ensure
179
- in_revision!(false)
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 no_revision? # check and see if no_revision! was called in a callback
319
+ self.revisable_revision = nil
320
+ return true
321
+ elsif self.revisable_revision
322
+ self.revisable_revision.save
323
+ revisions.reload
324
+ run_callbacks(:after_revise)
180
325
  end
326
+ force_revision!(false)
327
+ true
181
328
  end
182
329
 
183
- def revision_number
184
- revisions.first.revisable_number
185
- rescue NoMethodError
186
- 0
330
+ # Returns true if the _record_ (not just this instance
331
+ # of the record) is currently being revised.
332
+ def in_revision?
333
+ get_revisable_state(:revision)
334
+ end
335
+
336
+ # Manages the internal state of a +Revisable+ controlling
337
+ # whether or not a record is being revised. This works across
338
+ # instances and is keyed on primary_key.
339
+ def in_revision!(val=true) #:nodoc:
340
+ set_revisable_state(:revision, val)
341
+ end
342
+
343
+ # This returns a new +Revision+ instance with all the appropriate
344
+ # values initialized.
345
+ def to_revision #:nodoc:
346
+ rev = self.class.revision_class.new(self.revisable_new_params)
347
+
348
+ rev.revisable_original_id = self.id
349
+
350
+ new_revision_number = revisions.maximum(:revisable_number) + 1 rescue self.revisable_number
351
+ rev.revisable_number = new_revision_number
352
+ self.revisable_number = new_revision_number + 1
353
+
354
+ self.class.column_names.each do |col|
355
+ next unless self.class.revisable_should_clone_column? col
356
+ val = self.send("#{col}_changed?") ? self.send("#{col}_was") : self.send(col)
357
+ rev.send("#{col}=", val)
358
+ end
359
+
360
+ self.revisable_new_params = nil
361
+
362
+ rev
187
363
  end
188
364
 
189
- module ClassMethods
190
- def with_scope_with_revisable(*args, &block)
365
+ # This returns
366
+ def current_revision
367
+ self
368
+ end
369
+
370
+ def for_revision
371
+ key = self.read_attribute(self.class.primary_key)
372
+ self.class.revisable_shared_objects[key] ||= {}
373
+ end
374
+
375
+ def reverting_to
376
+ for_revision[:reverting_to]
377
+ end
378
+
379
+ def reverting_to=(val)
380
+ for_revision[:reverting_to] = val
381
+ end
382
+
383
+ def reverting_from
384
+ for_revision[:reverting_from]
385
+ end
386
+
387
+ def reverting_from=(val)
388
+ for_revision[:reverting_from] = val
389
+ end
390
+
391
+ def clear_revisable_shared_objects!
392
+ key = self.read_attribute(self.class.primary_key)
393
+ self.class.revisable_shared_objects.delete(key)
394
+ end
395
+
396
+ module ClassMethods
397
+ # acts_as_revisable's override for with_scope that allows for
398
+ # including revisions in the scope.
399
+ #
400
+ # ==== Example
401
+ #
402
+ # with_scope(:with_revisions => true) do
403
+ # ...
404
+ # end
405
+ def with_scope_with_revisable(*args, &block) #:nodoc:
191
406
  options = (args.grep(Hash).first || {})[:find]
192
407
 
193
408
  if options && options.delete(:with_revisions)
@@ -198,8 +413,14 @@ module FatJam
198
413
  with_scope_without_revisable(*args, &block)
199
414
  end
200
415
  end
201
-
202
- def find_with_revisable(*args)
416
+
417
+ # acts_as_revisable's override for find that allows for
418
+ # including revisions in the find.
419
+ #
420
+ # ==== Example
421
+ #
422
+ # find(:all, :with_revisions => true)
423
+ def find_with_revisable(*args) #:nodoc:
203
424
  options = args.grep(Hash).first
204
425
 
205
426
  if options && options.delete(:with_revisions)
@@ -210,36 +431,50 @@ module FatJam
210
431
  find_without_revisable(*args)
211
432
  end
212
433
  end
213
-
434
+
435
+ # Equivalent to:
436
+ # find(..., :with_revisions => true)
214
437
  def find_with_revisions(*args)
215
438
  args << {} if args.grep(Hash).blank?
216
439
  args.grep(Hash).first.update({:with_revisions => true})
217
440
  find_with_revisable(*args)
218
441
  end
219
442
 
220
- def revision_class_name
443
+ # Returns the +revision_class_name+ as configured in
444
+ # +acts_as_revisable+.
445
+ def revision_class_name #:nodoc:
221
446
  self.revisable_options.revision_class_name || "#{self.class_name}Revision"
222
447
  end
223
448
 
224
- def revision_class
225
- @aa_revision_class ||= revision_class_name.constantize
449
+ # Returns the actual +Revision+ class based on the
450
+ # #revision_class_name.
451
+ def revision_class #:nodoc:
452
+ self.revisable_revision_class ||= revision_class_name.constantize
226
453
  end
227
454
 
228
- def revisable_class
455
+ # Returns the revisable_class which in this case is simply +self+.
456
+ def revisable_class #:nodoc:
229
457
  self
230
458
  end
231
459
 
232
- def revisable_columns
233
- return @aa_revisable_columns unless @aa_revisable_columns.blank?
234
- return @aa_revisable_columns ||= [] if self.revisable_options.except == :all
235
- return @aa_revisable_columns ||= [self.revisable_options.only].flatten.map(&:to_s).map(&:downcase) unless self.revisable_options.only.blank?
460
+ # Returns the name of the association acts_as_revisable
461
+ # creates.
462
+ def revisions_association_name #:nodoc:
463
+ revision_class_name.pluralize.downcase
464
+ end
465
+
466
+ # Returns an Array of the columns that are watched for changes.
467
+ def revisable_watch_columns #:nodoc:
468
+ return self.revisable_columns unless self.revisable_columns.blank?
469
+ return self.revisable_columns ||= [] if self.revisable_options.except == :all
470
+ return self.revisable_columns ||= [self.revisable_options.only].flatten.map(&:to_s).map(&:downcase) unless self.revisable_options.only.blank?
236
471
 
237
472
  except = [self.revisable_options.except].flatten || []
238
473
  except += REVISABLE_SYSTEM_COLUMNS
239
474
  except += REVISABLE_UNREVISABLE_COLUMNS
240
475
  except.uniq!
241
476
 
242
- @aa_revisable_columns ||= (column_names - except.map(&:to_s)).flatten.map(&:downcase)
477
+ self.revisable_columns ||= (column_names - except.map(&:to_s)).flatten.map(&:downcase)
243
478
  end
244
479
  end
245
480
  end