rich-acts_as_revisable 0.6.0 → 0.9.8

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.
@@ -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