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.
- checksums.yaml +4 -4
- data/.travis.yml +12 -0
- data/CHANGELOG.md +34 -0
- data/README.md +244 -181
- data/lib/draftsman/config.rb +4 -1
- data/lib/draftsman/draft.rb +117 -80
- data/lib/draftsman/frameworks/rspec.rb +1 -1
- data/lib/draftsman/model.rb +253 -235
- data/lib/draftsman/version.rb +1 -1
- data/lib/draftsman.rb +26 -5
- data/lib/generators/draftsman/templates/config/initializers/draftsman.rb +16 -7
- data/spec/draftsman_spec.rb +7 -9
- data/spec/dummy/app/controllers/application_controller.rb +2 -2
- data/spec/dummy/app/models/overridden_draft.rb +7 -0
- data/spec/dummy/app/models/talkative.rb +21 -43
- data/spec/dummy/db/schema.rb +1 -0
- data/spec/models/child_spec.rb +17 -17
- data/spec/models/draft_spec.rb +893 -305
- data/spec/models/enumable_spec.rb +3 -2
- data/spec/models/overridden_draft_spec.rb +41 -0
- data/spec/models/parent_spec.rb +10 -10
- data/spec/models/skipper_spec.rb +221 -219
- data/spec/models/talkative_spec.rb +107 -108
- data/spec/models/trashable_spec.rb +6 -10
- data/spec/models/vanilla_spec.rb +570 -229
- data/spec/models/whitelister_spec.rb +489 -348
- metadata +7 -10
- data/spec/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -95
- data/spec/dummy/db/migrate/20150404203627_add_talkatives_table_to_tests.rb +0 -18
- data/spec/dummy/db/migrate/20150408234937_add_only_children.rb +0 -16
- data/spec/dummy/db/migrate/20160328184419_create_enumables.rb +0 -9
data/lib/draftsman/config.rb
CHANGED
@@ -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.
|
data/lib/draftsman/draft.rb
CHANGED
@@ -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, :
|
3
|
+
belongs_to :item, polymorphic: true
|
9
4
|
|
10
5
|
# Validations
|
11
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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.
|
22
|
-
|
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
|
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
|
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
|
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.
|
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
|
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 =
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
155
|
-
#
|
156
|
-
# - For `
|
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
|
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
|
178
|
+
self.item.send("#{self.item.class.published_at_attribute_name}=", Time.now)
|
170
179
|
|
171
180
|
# Clear out draft
|
172
|
-
self.item.send
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
220
|
-
|
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
|
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
|
-
|
227
|
-
|
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`
|
236
|
-
# drafted for destroy,
|
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
|
266
|
+
case self.event.to_sym
|
267
|
+
when :create
|
241
268
|
self.item.destroy
|
242
269
|
self.destroy
|
243
|
-
when
|
244
|
-
|
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
|
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(:
|
256
|
-
|
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(:
|
259
|
-
|
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 ==
|
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
|
320
|
+
draft.send("#{key}=", value)
|
284
321
|
elsif key.to_sym != :id
|
285
|
-
logger.warn
|
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
|
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
|