draftsman 0.5.1 → 0.6.0

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