draftsman 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,13 +3,16 @@ require 'singleton'
3
3
  module Draftsman
4
4
  class Config
5
5
  include Singleton
6
- attr_accessor :serializer, :timestamp_field
6
+ attr_accessor :serializer, :timestamp_field, :whodunnit_field, :stash_drafted_changes
7
+ alias :stash_drafted_changes? :stash_drafted_changes
7
8
 
8
9
  def initialize
9
10
  @timestamp_field = :created_at
10
11
  @mutex = Mutex.new
11
12
  @serializer = Draftsman::Serializers::Yaml
12
13
  @enabled = true
14
+ @whodunnit_field = :whodunnit
15
+ @stash_drafted_changes = true
13
16
  end
14
17
 
15
18
  # Indicates whether Draftsman is on or off. Default: true.
@@ -1,33 +1,35 @@
1
1
  class Draftsman::Draft < ActiveRecord::Base
2
- # Mass assignment (for <= ActiveRecord 3.x)
3
- if Draftsman.active_record_protected_attributes?
4
- attr_accessible :item_type, :item_id, :item, :event, :whodunnit, :object, :object_changes, :previous_draft
5
- end
6
-
7
2
  # Associations
8
- belongs_to :item, :polymorphic => true
3
+ belongs_to :item, polymorphic: true
9
4
 
10
5
  # Validations
11
- validates_presence_of :event
12
-
13
- def self.with_item_keys(item_type, item_id)
14
- scoped :conditions => { :item_type => item_type, :item_id => item_id }
15
- end
6
+ validates :event, presence: true
16
7
 
17
- def self.creates
18
- where :event => 'create'
19
- end
8
+ # Scopes
9
+ # Returns `where` that filters to only `create` drafts.
10
+ scope :creates, -> { where(event: :create) }
11
+ # Returns `where` that filters to only `destroy` drafts.
12
+ scope :destroys, -> { where(event: :destroy) }
13
+ # Returns `where` that filters to only `update` drafts.
14
+ scope :updates, -> { where(event: :update) }
20
15
 
21
- def self.destroys
22
- where :event => 'destroy'
16
+ def self.with_item_keys(item_type, item_id)
17
+ scoped conditions: { item_type: item_type, item_id: item_id }
23
18
  end
24
19
 
25
- # Returns whether the `object` column is using the `json` type supported by PostgreSQL.
20
+ # Returns whether the `object` column is using the `json` type supported by
21
+ # PostgreSQL.
26
22
  def self.object_col_is_json?
27
23
  @object_col_is_json ||= columns_hash['object'].type == :json
28
24
  end
29
25
 
30
- # Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL.
26
+ # Returns whether or not this class has an `object_changes` column.
27
+ def self.object_changes_col_present?
28
+ column_names.include?('object_changes')
29
+ end
30
+
31
+ # Returns whether the `object_changes` column is using the `json` type
32
+ # supported by PostgreSQL.
31
33
  def self.object_changes_col_is_json?
32
34
  @object_changes_col_is_json ||= columns_hash['object_changes'].type == :json
33
35
  end
@@ -37,15 +39,11 @@ class Draftsman::Draft < ActiveRecord::Base
37
39
  @previous_draft_col_is_json ||= columns_hash['previous_draft'].type == :json
38
40
  end
39
41
 
40
- def self.updates
41
- where :event => 'update'
42
- end
43
-
44
42
  # Returns what changed in this draft. Similar to `ActiveModel::Dirty#changes`.
45
- # Returns `nil` if your `drafts` table does not have an `object_changes` text column.
43
+ # Returns `nil` if your `drafts` table does not have an `object_changes` text
44
+ # column.
46
45
  def changeset
47
- return nil unless self.class.column_names.include? 'object_changes'
48
-
46
+ return nil unless self.class.object_changes_col_present?
49
47
  @changeset ||= load_changeset
50
48
  end
51
49
 
@@ -59,14 +57,20 @@ class Draftsman::Draft < ActiveRecord::Base
59
57
  self.event == 'destroy'
60
58
  end
61
59
 
62
- # Returns related draft dependencies that would be along for the ride for a `publish!` action.
60
+ # Returns related draft dependencies that would be along for the ride for a
61
+ # `publish!` action.
63
62
  def draft_publication_dependencies
64
63
  dependencies = []
65
64
 
66
- my_item = self.item.draft? ? self.item.draft.reify : self.item
65
+ my_item =
66
+ if Draftsman.stash_drafted_changes? && self.item.draft?
67
+ self.item.draft.reify
68
+ else
69
+ self.item
70
+ end
67
71
 
68
- case self.event
69
- when 'create', 'update'
72
+ case self.event.to_sym
73
+ when :create, :update
70
74
  associations = my_item.class.reflect_on_all_associations(:belongs_to)
71
75
 
72
76
  associations.each do |association|
@@ -82,7 +86,7 @@ class Draftsman::Draft < ActiveRecord::Base
82
86
  dependencies << dependency.draft if dependency.present? && dependency.draft? && dependency.draft.create?
83
87
  end
84
88
  end
85
- when 'destroy'
89
+ when :destroy
86
90
  associations = my_item.class.reflect_on_all_associations(:has_one) + my_item.class.reflect_on_all_associations(:has_many)
87
91
 
88
92
  associations.each do |association|
@@ -106,17 +110,19 @@ class Draftsman::Draft < ActiveRecord::Base
106
110
  dependencies
107
111
  end
108
112
 
109
- # Returns related draft dependencies that would be along for the ride for a `revert!` action.
113
+ # Returns related draft dependencies that would be along for the ride for a
114
+ # `revert!` action.
110
115
  def draft_reversion_dependencies
111
116
  dependencies = []
112
117
 
113
- case self.event
114
- when 'create'
118
+ case self.event.to_sym
119
+ when :create
115
120
  associations = self.item.class.reflect_on_all_associations(:has_one) + self.item.class.reflect_on_all_associations(:has_many)
116
121
 
117
122
  associations.each do |association|
118
123
  if association.klass.draftable?
119
- # Reconcile different association types into an array, even if `has_one` produces a single-item
124
+ # Reconcile different association types into an array, even if
125
+ # `has_one` produces a single-item
120
126
  associated_dependencies =
121
127
  case association.macro
122
128
  when :has_one
@@ -130,7 +136,7 @@ class Draftsman::Draft < ActiveRecord::Base
130
136
  end
131
137
  end
132
138
  end
133
- when 'destroy'
139
+ when :destroy
134
140
  associations = self.item.class.reflect_on_all_associations(:belongs_to)
135
141
 
136
142
  associations.each do |association|
@@ -151,25 +157,28 @@ class Draftsman::Draft < ActiveRecord::Base
151
157
  dependencies
152
158
  end
153
159
 
154
- # Publishes this draft's associated `item`, publishes its `item`'s dependencies, and destroys itself.
155
- # - For `create` drafts, adds a value for the `published_at` timestamp on the item and destroys the draft.
156
- # - For `update` drafts, applies the drafted changes to the item and destroys the draft.
160
+ # Publishes this draft's associated `item`, publishes its `item`'s
161
+ # dependencies, and destroys itself.
162
+ # - For `create` drafts, adds a value for the `published_at` timestamp on the
163
+ # item and destroys the draft.
164
+ # - For `update` drafts, applies the drafted changes to the item and destroys
165
+ # the draft.
157
166
  # - For `destroy` drafts, destroys the item and the draft.
158
167
  def publish!
159
168
  ActiveRecord::Base.transaction do
160
- case self.event
161
- when 'create', 'update'
169
+ case self.event.to_sym
170
+ when :create, :update
162
171
  # Parents must be published too
163
172
  self.draft_publication_dependencies.each { |dependency| dependency.publish! }
164
173
 
165
174
  # Update drafts need to copy over data to main record
166
- self.item.attributes = self.reify.attributes if self.update?
175
+ self.item.attributes = self.reify.attributes if Draftsman.stash_drafted_changes? && self.update?
167
176
 
168
177
  # Write `published_at` attribute
169
- self.item.send "#{self.item.class.published_at_attribute_name}=", Time.now
178
+ self.item.send("#{self.item.class.published_at_attribute_name}=", Time.now)
170
179
 
171
180
  # Clear out draft
172
- self.item.send "#{self.item.class.draft_association_name}_id=", nil
181
+ self.item.send("#{self.item.class.draft_association_name}_id=", nil)
173
182
 
174
183
  # Determine which columns should be updated
175
184
  only = self.item.class.draftsman_options[:only]
@@ -182,13 +191,13 @@ class Draftsman::Draft < ActiveRecord::Base
182
191
  self.item.attributes.slice(*attributes_to_change).each do |key, value|
183
192
  self.item.send("#{key}=", value)
184
193
  end
185
- self.item.save(:validate => false)
186
-
194
+
195
+ self.item.save(validate: false)
187
196
  self.item.reload
188
197
 
189
198
  # Destroy draft
190
199
  self.destroy
191
- when 'destroy'
200
+ when :destroy
192
201
  self.item.destroy
193
202
  end
194
203
  end
@@ -198,33 +207,50 @@ class Draftsman::Draft < ActiveRecord::Base
198
207
  #
199
208
  # Example usage:
200
209
  #
201
- # `@category = @category.reify if @category.draft?`
210
+ # `@category = @category.draft.reify if @category.draft?`
202
211
  def reify
212
+ # This appears to be necessary if for some reason the draft's model
213
+ # hasn't been loaded (such as when done in the console).
214
+ unless defined? self.item_type
215
+ require self.item_type.underscore
216
+ end
217
+
203
218
  without_identity_map do
204
- if !self.previous_draft.nil?
219
+ # Create draft doesn't require reification.
220
+ if self.create?
221
+ self.item
222
+ # If a previous draft is stashed, restore that.
223
+ elsif self.previous_draft.present?
205
224
  reify_previous_draft.reify
206
- elsif !self.object.nil?
207
- # This appears to be necessary if for some reason the draft's model hasn't been loaded (such as when done in the console).
208
- unless defined? self.item_type
209
- require self.item_type.underscore
225
+ # Prefer changeset for refication if it's present.
226
+ elsif !self.changeset.empty?
227
+ self.changeset.each do |key, value|
228
+ # Skip counter_cache columns
229
+ if self.item.respond_to?("#{key}=") && !key.end_with?('_count')
230
+ self.item.send("#{key}=", value.last)
231
+ elsif !key.end_with?('_count')
232
+ logger.warn("Attribute #{key} does not exist on #{self.item_type} (Draft ID: #{self.id}).")
233
+ end
210
234
  end
211
235
 
212
- model = item.reload
213
-
214
- attrs = self.class.object_col_is_json? ? self.object : Draftsman.serializer.load(object)
215
- model.class.unserialize_attributes_for_draftsman attrs
236
+ self.item.send("#{self.item.class.draft_association_name}=", self)
237
+ self.item
238
+ # Reify based on object if it's all that's available.
239
+ elsif self.object.present?
240
+ attrs = self.class.object_col_is_json? ? self.object : Draftsman.serializer.load(self.object)
241
+ self.item.class.unserialize_attributes_for_draftsman(attrs)
216
242
 
217
243
  attrs.each do |key, value|
218
244
  # Skip counter_cache columns
219
- if model.respond_to?("#{key}=") && !key.end_with?('_count')
220
- model.send "#{key}=", value
245
+ if self.item.respond_to?("#{key}=") && !key.end_with?('_count')
246
+ self.item.send("#{key}=", value)
221
247
  elsif !key.end_with?('_count')
222
- logger.warn "Attribute #{key} does not exist on #{item_type} (Draft ID: #{id})."
248
+ logger.warn("Attribute #{key} does not exist on #{self.item_type} (Draft ID: #{self.id}).")
223
249
  end
224
250
  end
225
251
 
226
- model.send "#{model.class.draft_association_name}=", self
227
- model
252
+ self.item.send("#{self.item.class.draft_association_name}=", self)
253
+ self.item
228
254
  end
229
255
  end
230
256
  end
@@ -232,18 +258,29 @@ class Draftsman::Draft < ActiveRecord::Base
232
258
  # Reverts this draft.
233
259
  # - For create drafts, destroys the draft and the item.
234
260
  # - For update drafts, destroys the draft only.
235
- # - For destroy drafts, destroys the draft and undoes the `trashed_at` timestamp on the item. If a previous draft was
236
- # drafted for destroy, restores the draft.
261
+ # - For destroy drafts, destroys the draft and undoes the `trashed_at`
262
+ # timestamp on the item. If a previous draft was drafted for destroy,
263
+ # restores the draft.
237
264
  def revert!
238
265
  ActiveRecord::Base.transaction do
239
- case self.event
240
- when 'create'
266
+ case self.event.to_sym
267
+ when :create
241
268
  self.item.destroy
242
269
  self.destroy
243
- when 'update'
244
- self.item.class.where(:id => self.item).update_all("#{self.item.class.draft_association_name}_id".to_sym => nil)
270
+ when :update
271
+ # If we're not stashing changes, we need to restore original values from
272
+ # the changeset.
273
+ if self.class.object_changes_col_present? && !Draftsman.stash_drafted_changes?
274
+ self.changeset.each do |attr, values|
275
+ self.item.send("#{attr}=", values.first) if self.item.respond_to?(attr)
276
+ end
277
+ end
278
+ # Then clear out the draft ID.
279
+ self.item.send("#{self.item.class.draft_association_name}_id=", nil)
280
+ self.item.save!(validate: false)
281
+ # Then destroy draft.
245
282
  self.destroy
246
- when 'destroy'
283
+ when :destroy
247
284
  # Parents must be restored too
248
285
  self.draft_reversion_dependencies.each { |dependency| dependency.revert! }
249
286
 
@@ -252,13 +289,13 @@ class Draftsman::Draft < ActiveRecord::Base
252
289
  prev_draft = reify_previous_draft
253
290
  prev_draft.save!
254
291
 
255
- self.item.class.where(:id => self.item).update_all "#{self.item.class.draft_association_name}_id".to_sym => prev_draft.id,
256
- self.item.class.trashed_at_attribute_name => nil
292
+ self.item.class.where(id: self.item).update_all "#{self.item.class.draft_association_name}_id".to_sym => prev_draft.id,
293
+ self.item.class.trashed_at_attribute_name => nil
257
294
  else
258
- self.item.class.where(:id => self.item).update_all "#{self.item.class.draft_association_name}_id".to_sym => nil,
259
- self.item.class.trashed_at_attribute_name => nil
295
+ self.item.class.where(id: self.item).update_all "#{self.item.class.draft_association_name}_id".to_sym => nil,
296
+ self.item.class.trashed_at_attribute_name => nil
260
297
  end
261
-
298
+
262
299
  self.destroy
263
300
  end
264
301
  end
@@ -266,7 +303,7 @@ class Draftsman::Draft < ActiveRecord::Base
266
303
 
267
304
  # Returns whether or not this is an `update` event.
268
305
  def update?
269
- self.event == 'update'
306
+ self.event.to_sym == :update
270
307
  end
271
308
 
272
309
  private
@@ -280,9 +317,9 @@ private
280
317
 
281
318
  attrs.each do |key, value|
282
319
  if key.to_sym != :id && draft.respond_to?("#{key}=")
283
- draft.send "#{key}=", value
320
+ draft.send("#{key}=", value)
284
321
  elsif key.to_sym != :id
285
- logger.warn "Attribute #{key} does not exist on #{item_type} (Draft ID: #{self.id})."
322
+ logger.warn("Attribute #{key} does not exist on #{item_type} (Draft ID: #{self.id}).")
286
323
  end
287
324
  end
288
325
  end
@@ -292,7 +329,7 @@ private
292
329
 
293
330
  def without_identity_map(&block)
294
331
  if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
295
- ActiveRecord::IdentityMap.without &block
332
+ ActiveRecord::IdentityMap.without(&block)
296
333
  else
297
334
  block.call
298
335
  end
@@ -300,7 +337,7 @@ private
300
337
 
301
338
  def load_changeset
302
339
  changes = HashWithIndifferentAccess.new(object_changes_deserialized)
303
- item_type.constantize.unserialize_draft_attribute_changes(changes)
340
+ self.item_type.constantize.unserialize_draft_attribute_changes(changes)
304
341
  changes
305
342
  rescue
306
343
  {}
@@ -308,9 +345,9 @@ private
308
345
 
309
346
  def object_changes_deserialized
310
347
  if self.class.object_changes_col_is_json?
311
- object_changes
348
+ self.object_changes
312
349
  else
313
- Draftsman.serializer.load(object_changes)
350
+ Draftsman.serializer.load(self.object_changes)
314
351
  end
315
352
  end
316
353
  end
@@ -1,4 +1,4 @@
1
- if defined? RSpec
1
+ if defined? RSpec::Core
2
2
  require 'rspec/core'
3
3
  require 'rspec/matchers'
4
4