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/model.rb
CHANGED
@@ -8,40 +8,50 @@ module Draftsman
|
|
8
8
|
end
|
9
9
|
|
10
10
|
module ClassMethods
|
11
|
-
# Declare this in your model to enable the Draftsman API for it. A draft
|
12
|
-
# association (if one exists).
|
11
|
+
# Declare this in your model to enable the Draftsman API for it. A draft
|
12
|
+
# of the model is available in the `draft` association (if one exists).
|
13
13
|
#
|
14
14
|
# Options:
|
15
15
|
#
|
16
16
|
# :class_name
|
17
|
-
# The name of a custom `Draft` class. This class should inherit from
|
18
|
-
#
|
17
|
+
# The name of a custom `Draft` class. This class should inherit from
|
18
|
+
# `Draftsman::Draft`. A global default can be set for this using
|
19
|
+
# `Draftsman.draft_class_name=` if the default of `Draftsman::Draft` needs
|
20
|
+
# to be overridden.
|
19
21
|
#
|
20
22
|
# :ignore
|
21
|
-
# An array of attributes for which an update to a `Draft` will not be
|
23
|
+
# An array of attributes for which an update to a `Draft` will not be
|
24
|
+
# stored if they are the only ones changed.
|
22
25
|
#
|
23
26
|
# :only
|
24
|
-
# Inverse of `ignore` - a new `Draft` will be created only for these
|
25
|
-
#
|
27
|
+
# Inverse of `ignore` - a new `Draft` will be created only for these
|
28
|
+
# attributes if supplied. It's recommended that you only specify optional
|
29
|
+
# attributes for this (that can be empty).
|
26
30
|
#
|
27
31
|
# :skip
|
28
|
-
# Fields to ignore completely. As with `ignore`, updates to these fields
|
29
|
-
#
|
30
|
-
#
|
32
|
+
# Fields to ignore completely. As with `ignore`, updates to these fields
|
33
|
+
# will not create a new `Draft`. In addition, these fields will not be
|
34
|
+
# included in the serialized versions of the object whenever a new `Draft`
|
35
|
+
# is created.
|
31
36
|
#
|
32
37
|
# :meta
|
33
|
-
# A hash of extra data to store.
|
34
|
-
# `procs` (which are called with
|
35
|
-
# `
|
38
|
+
# A hash of extra data to store. You must add a column to the `drafts`
|
39
|
+
# table for each key. Values are objects or `procs` (which are called with
|
40
|
+
# `self`, i.e. the model with the `has_drafts`). See
|
41
|
+
# `Draftsman::Controller.info_for_draftsman` for an example of how to
|
42
|
+
# store data from the controller.
|
36
43
|
#
|
37
44
|
# :draft
|
38
|
-
# The name to use for the `draft` association shortcut method. Default is
|
45
|
+
# The name to use for the `draft` association shortcut method. Default is
|
46
|
+
# `:draft`.
|
39
47
|
#
|
40
48
|
# :published_at
|
41
|
-
# The name to use for the method which returns the published timestamp.
|
49
|
+
# The name to use for the method which returns the published timestamp.
|
50
|
+
# Default is `published_at`.
|
42
51
|
#
|
43
52
|
# :trashed_at
|
44
|
-
# The name to use for the method which returns the soft delete timestamp.
|
53
|
+
# The name to use for the method which returns the soft delete timestamp.
|
54
|
+
# Default is `trashed_at`.
|
45
55
|
def has_drafts(options = {})
|
46
56
|
# Lazily include the instance methods so we don't clutter up
|
47
57
|
# any more ActiveRecord models than we need to.
|
@@ -50,7 +60,9 @@ module Draftsman
|
|
50
60
|
|
51
61
|
# Define before/around/after callbacks on each drafted model
|
52
62
|
send :extend, ActiveModel::Callbacks
|
53
|
-
|
63
|
+
# TODO: Remove `draft_creation`, `draft_update`, and `draft_destroy` in
|
64
|
+
# v1.0.
|
65
|
+
define_model_callbacks :save_draft, :draft_creation, :draft_update, :draft_destruction, :draft_destroy
|
54
66
|
|
55
67
|
class_attribute :draftsman_options
|
56
68
|
self.draftsman_options = options.dup
|
@@ -78,43 +90,28 @@ module Draftsman
|
|
78
90
|
self.trashed_at_attribute_name = options[:trashed_at] || :trashed_at
|
79
91
|
|
80
92
|
# `belongs_to :draft` association
|
81
|
-
belongs_to
|
93
|
+
belongs_to(self.draft_association_name, class_name: self.draft_class_name, dependent: :destroy)
|
82
94
|
|
83
95
|
# Scopes
|
84
|
-
scope :drafted, (
|
96
|
+
scope :drafted, -> (referenced_table_name = nil) {
|
85
97
|
referenced_table_name = referenced_table_name.present? ? referenced_table_name : table_name
|
98
|
+
where.not(referenced_table_name => { "#{self.draft_association_name}_id" => nil })
|
99
|
+
}
|
86
100
|
|
87
|
-
|
88
|
-
where.not(referenced_table_name => { "#{self.draft_association_name}_id" => nil })
|
89
|
-
else
|
90
|
-
where("#{referenced_table_name}.#{self.draft_association_name}_id IS NOT NULL")
|
91
|
-
end
|
92
|
-
end)
|
93
|
-
|
94
|
-
scope :published, (lambda do |referenced_table_name = nil|
|
101
|
+
scope :published, -> (referenced_table_name = nil) {
|
95
102
|
referenced_table_name = referenced_table_name.present? ? referenced_table_name : table_name
|
103
|
+
where.not(referenced_table_name => { self.published_at_attribute_name => nil })
|
104
|
+
}
|
96
105
|
|
97
|
-
|
98
|
-
where.not(referenced_table_name => { self.published_at_attribute_name => nil })
|
99
|
-
else
|
100
|
-
where("#{self.published_at_attribute_name} IS NOT NULL")
|
101
|
-
end
|
102
|
-
end)
|
103
|
-
|
104
|
-
scope :trashed, (lambda do |referenced_table_name = nil|
|
106
|
+
scope :trashed, -> (referenced_table_name = nil) {
|
105
107
|
referenced_table_name = referenced_table_name.present? ? referenced_table_name : table_name
|
108
|
+
where.not(referenced_table_name => { self.trashed_at_attribute_name => nil })
|
109
|
+
}
|
106
110
|
|
107
|
-
|
108
|
-
where.not(referenced_table_name => { self.trashed_at_attribute_name => nil })
|
109
|
-
else
|
110
|
-
where("#{self.trashed_at_attribute_name} IS NOT NULL")
|
111
|
-
end
|
112
|
-
end)
|
113
|
-
|
114
|
-
scope :live, (lambda do |referenced_table_name = nil|
|
111
|
+
scope :live, -> (referenced_table_name = nil) {
|
115
112
|
referenced_table_name = referenced_table_name.present? ? referenced_table_name : table_name
|
116
113
|
where(referenced_table_name => { self.trashed_at_attribute_name => nil })
|
117
|
-
|
114
|
+
}
|
118
115
|
end
|
119
116
|
|
120
117
|
# Returns draft class.
|
@@ -131,11 +128,6 @@ module Draftsman
|
|
131
128
|
def trashable?
|
132
129
|
draftable? && method_defined?(self.trashed_at_attribute_name)
|
133
130
|
end
|
134
|
-
|
135
|
-
# Returns whether or not the included ActiveRecord can do `where.not(...)` style queries.
|
136
|
-
def where_not?
|
137
|
-
ActiveRecord::VERSION::STRING.to_f >= 4.0
|
138
|
-
end
|
139
131
|
end
|
140
132
|
|
141
133
|
module InstanceMethods
|
@@ -144,39 +136,15 @@ module Draftsman
|
|
144
136
|
send(self.class.draft_association_name).present?
|
145
137
|
end
|
146
138
|
|
147
|
-
#
|
148
|
-
# the objects passed validation and the save was successful.
|
139
|
+
# DEPRECATED: Use `#draft_save` instead.
|
149
140
|
def draft_creation
|
150
|
-
|
151
|
-
|
152
|
-
# We want to save the draft after create
|
153
|
-
return false unless self.save
|
154
|
-
|
155
|
-
data = {
|
156
|
-
:item => self,
|
157
|
-
:event => 'create',
|
158
|
-
:whodunnit => Draftsman.whodunnit,
|
159
|
-
:object => object_attrs_for_draft_record
|
160
|
-
}
|
161
|
-
data[:object_changes] = changes_for_draftsman(previous_changes: true) if track_object_changes_for_draft?
|
162
|
-
data = merge_metadata_for_draft(data)
|
163
|
-
|
164
|
-
send "build_#{self.class.draft_association_name}", data
|
165
|
-
|
166
|
-
if send(self.class.draft_association_name).save
|
167
|
-
write_attribute "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
|
168
|
-
self.update_column "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
|
169
|
-
else
|
170
|
-
raise ActiveRecord::Rollback and return false
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
return true
|
141
|
+
ActiveSupport::Deprecation.warn('`#draft_creation` is deprecated and will be removed from Draftsman 1.0. Use `#save_draft` instead.')
|
142
|
+
_draft_creation
|
175
143
|
end
|
176
144
|
|
177
|
-
# DEPRECATED: Use
|
145
|
+
# DEPRECATED: Use `#draft_destruction` instead.
|
178
146
|
def draft_destroy
|
179
|
-
ActiveSupport::Deprecation.warn('
|
147
|
+
ActiveSupport::Deprecation.warn('`#draft_destroy` is deprecated and will be removed from Draftsman 1.0. Use `draft_destruction` instead.')
|
180
148
|
|
181
149
|
run_callbacks :draft_destroy do
|
182
150
|
_draft_destruction
|
@@ -190,94 +158,25 @@ module Draftsman
|
|
190
158
|
end
|
191
159
|
end
|
192
160
|
|
193
|
-
#
|
194
|
-
# state, the draft is destroyed. Returns `true` or `false` depending on if the object passed validation and the save
|
195
|
-
# was successful.
|
161
|
+
# DEPRECATED: Use `#draft_save` instead.
|
196
162
|
def draft_update
|
197
|
-
|
198
|
-
|
199
|
-
save_only_columns_for_draft
|
200
|
-
|
201
|
-
# We want to save the draft before update
|
202
|
-
return false unless self.valid?
|
203
|
-
|
204
|
-
# If updating a creation draft, also update this item
|
205
|
-
if self.draft? && send(self.class.draft_association_name).create?
|
206
|
-
data = {
|
207
|
-
:item => self,
|
208
|
-
:whodunnit => Draftsman.whodunnit,
|
209
|
-
:object => object_attrs_for_draft_record
|
210
|
-
}
|
211
|
-
|
212
|
-
if track_object_changes_for_draft?
|
213
|
-
data[:object_changes] = changes_for_draftsman(changed_from: self.send(self.class.draft_association_name).changeset)
|
214
|
-
end
|
215
|
-
data = merge_metadata_for_draft(data)
|
216
|
-
send(self.class.draft_association_name).update_attributes data
|
217
|
-
self.save
|
218
|
-
# Destroy the draft if this record has changed back to the original record
|
219
|
-
elsif changed_to_original_for_draft?
|
220
|
-
nilified_draft = send(self.class.draft_association_name)
|
221
|
-
send "#{self.class.draft_association_name}_id=", nil
|
222
|
-
self.save
|
223
|
-
nilified_draft.destroy
|
224
|
-
# Save a draft if record is changed notably
|
225
|
-
elsif changed_notably_for_draft?
|
226
|
-
data = {
|
227
|
-
:item => self,
|
228
|
-
:whodunnit => Draftsman.whodunnit,
|
229
|
-
:object => object_attrs_for_draft_record
|
230
|
-
}
|
231
|
-
data = merge_metadata_for_draft(data)
|
232
|
-
|
233
|
-
# If there's already a draft, update it.
|
234
|
-
if send(self.class.draft_association_name).present?
|
235
|
-
data[:object_changes] = changes_for_draftsman if track_object_changes_for_draft?
|
236
|
-
send(self.class.draft_association_name).update_attributes data
|
237
|
-
update_skipped_attributes
|
238
|
-
# If there's not draft, create an update draft.
|
239
|
-
else
|
240
|
-
data[:event] = 'update'
|
241
|
-
data[:object_changes] = changes_for_draftsman if track_object_changes_for_draft?
|
242
|
-
send "build_#{self.class.draft_association_name}", data
|
243
|
-
|
244
|
-
if send(self.class.draft_association_name).save
|
245
|
-
update_column "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
|
246
|
-
update_skipped_attributes
|
247
|
-
else
|
248
|
-
raise ActiveRecord::Rollback and return false
|
249
|
-
end
|
250
|
-
end
|
251
|
-
# If record is a draft and not changed notably, then update the draft.
|
252
|
-
elsif self.draft?
|
253
|
-
data = {
|
254
|
-
:item => self,
|
255
|
-
:whodunnit => Draftsman.whodunnit,
|
256
|
-
:object => object_attrs_for_draft_record
|
257
|
-
}
|
258
|
-
data[:object_changes] = changes_for_draftsman(changed_from: @object.draft.changeset) if track_object_changes_for_draft?
|
259
|
-
data = merge_metadata_for_draft(data)
|
260
|
-
send(self.class.draft_association_name).update_attributes data
|
261
|
-
update_skipped_attributes
|
262
|
-
# Otherwise, just save the record
|
263
|
-
else
|
264
|
-
self.save
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
rescue Exception => e
|
269
|
-
false
|
163
|
+
ActiveSupport::Deprecation.warn('`#draft_update` is deprecated and will be removed from Draftsman 1.0. Use `#save_draft` instead.')
|
164
|
+
_draft_update
|
270
165
|
end
|
271
166
|
|
272
167
|
# Returns serialized object representing this drafted item.
|
273
168
|
def object_attrs_for_draft_record(object = nil)
|
274
169
|
object ||= self
|
275
170
|
|
276
|
-
|
171
|
+
attrs = object.attributes.except(*self.class.draftsman_options[:skip]).tap do |attributes|
|
277
172
|
self.class.serialize_attributes_for_draftsman attributes
|
278
173
|
end
|
279
174
|
|
280
|
-
self.class.draft_class.object_col_is_json?
|
175
|
+
if self.class.draft_class.object_col_is_json?
|
176
|
+
attrs
|
177
|
+
else
|
178
|
+
Draftsman.serializer.dump(attrs)
|
179
|
+
end
|
281
180
|
end
|
282
181
|
|
283
182
|
# Returns whether or not this item has been published at any point in its lifecycle.
|
@@ -285,6 +184,31 @@ module Draftsman
|
|
285
184
|
self.published_at.present?
|
286
185
|
end
|
287
186
|
|
187
|
+
# Creates or updates draft depending on state of this item and if it has
|
188
|
+
# any drafts.
|
189
|
+
#
|
190
|
+
# - If a completely new record, persists this item to the database and
|
191
|
+
# records a `create` draft.
|
192
|
+
# - If an existing record with an existing `create` draft, updates the
|
193
|
+
# record and the existing `create` draft.
|
194
|
+
# - If an existing record with no existing draft, records changes in an
|
195
|
+
# `update` draft.
|
196
|
+
# - If an existing record with an existing draft (`create` or `update`),
|
197
|
+
# updated back to its original undrafted state, removes associated
|
198
|
+
# `draft record`.
|
199
|
+
#
|
200
|
+
# Returns `true` or `false` depending on if the object passed validation
|
201
|
+
# and the save was successful.
|
202
|
+
def save_draft
|
203
|
+
run_callbacks :save_draft do
|
204
|
+
if self.new_record?
|
205
|
+
_draft_creation
|
206
|
+
else
|
207
|
+
_draft_update
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
288
212
|
# Returns whether or not this item has been trashed
|
289
213
|
def trashed?
|
290
214
|
send(self.class.trashed_at_attribute_name).present?
|
@@ -292,38 +216,73 @@ module Draftsman
|
|
292
216
|
|
293
217
|
private
|
294
218
|
|
219
|
+
# Creates object and records a draft for the object's creation. Returns
|
220
|
+
# `true` or `false` depending on whether or not the objects passed
|
221
|
+
# validation and the save was successful.
|
222
|
+
def _draft_creation
|
223
|
+
transaction do
|
224
|
+
# TODO: Remove callback wrapper in v1.0.
|
225
|
+
run_callbacks :draft_creation do
|
226
|
+
# We want to save the draft after create
|
227
|
+
return false unless self.save
|
228
|
+
|
229
|
+
# Build data to store in draft record.
|
230
|
+
data = {
|
231
|
+
item: self,
|
232
|
+
event: :create,
|
233
|
+
}
|
234
|
+
data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes?
|
235
|
+
data[Draftsman.whodunnit_field] = Draftsman.whodunnit
|
236
|
+
data[:object_changes] = serialized_draft_changeset(changes_for_draftsman(:create)) if track_object_changes_for_draft?
|
237
|
+
data = merge_metadata_for_draft(data)
|
238
|
+
send("build_#{self.class.draft_association_name}", data)
|
239
|
+
|
240
|
+
if send(self.class.draft_association_name).save
|
241
|
+
fk = "#{self.class.draft_association_name}_id"
|
242
|
+
id = send(self.class.draft_association_name).id
|
243
|
+
self.update_column(fk, id)
|
244
|
+
else
|
245
|
+
raise ActiveRecord::Rollback and return false
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
return true
|
251
|
+
end
|
252
|
+
|
295
253
|
# This is only abstracted away at this moment because of the
|
296
254
|
# `draft_destroy` deprecation. Move all of this logic back into
|
297
255
|
# `draft_destruction` after `draft_destroy is removed.`
|
298
256
|
def _draft_destruction
|
299
257
|
transaction do
|
300
258
|
data = {
|
301
|
-
:
|
302
|
-
:
|
303
|
-
:whodunnit => Draftsman.whodunnit,
|
304
|
-
:object => object_attrs_for_draft_record
|
259
|
+
item: self,
|
260
|
+
event: :destroy
|
305
261
|
}
|
262
|
+
data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes?
|
263
|
+
data[Draftsman.whodunnit_field] = Draftsman.whodunnit
|
306
264
|
|
307
265
|
# Stash previous draft in case it needs to be reverted later
|
308
266
|
if self.draft?
|
309
267
|
attrs = send(self.class.draft_association_name).attributes
|
310
268
|
|
311
|
-
data[:previous_draft] =
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
269
|
+
data[:previous_draft] =
|
270
|
+
if self.class.draft_class.previous_draft_col_is_json?
|
271
|
+
attrs
|
272
|
+
else
|
273
|
+
Draftsman.serializer.dump(attrs)
|
274
|
+
end
|
316
275
|
end
|
317
276
|
|
318
277
|
data = merge_metadata_for_draft(data)
|
319
278
|
|
320
279
|
if send(self.class.draft_association_name).present?
|
321
|
-
send(self.class.draft_association_name).
|
280
|
+
send(self.class.draft_association_name).update!(data)
|
322
281
|
else
|
323
282
|
send("build_#{self.class.draft_association_name}", data)
|
324
283
|
send(self.class.draft_association_name).save!
|
325
|
-
send
|
326
|
-
self.update_column
|
284
|
+
send("#{self.class.draft_association_name}_id=", send(self.class.draft_association_name).id)
|
285
|
+
self.update_column("#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id)
|
327
286
|
end
|
328
287
|
|
329
288
|
trash!
|
@@ -332,62 +291,126 @@ module Draftsman
|
|
332
291
|
dependent_associations = self.class.reflect_on_all_associations(:has_one) + self.class.reflect_on_all_associations(:has_many)
|
333
292
|
|
334
293
|
dependent_associations.each do |association|
|
335
|
-
|
336
294
|
if association.klass.draftable? && association.options.has_key?(:dependent) && association.options[:dependent] == :destroy
|
337
295
|
dependents = self.send(association.name)
|
338
296
|
dependents = [dependents] if (dependents && association.macro == :has_one)
|
339
297
|
|
340
|
-
dependents
|
341
|
-
|
342
|
-
|
298
|
+
if dependents
|
299
|
+
dependents.each do |dependent|
|
300
|
+
dependent.draft_destruction unless dependent.draft? && dependent.send(dependent.class.draft_association_name).destroy?
|
301
|
+
end
|
302
|
+
end
|
343
303
|
end
|
344
304
|
end
|
345
305
|
end
|
346
306
|
end
|
347
307
|
|
348
|
-
#
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
# Returns whether or not this instance has changes that should trigger a new draft.
|
360
|
-
def changed_notably_for_draft?
|
361
|
-
notably_changed_attributes_for_draft.any?
|
362
|
-
end
|
363
|
-
|
364
|
-
# Returns whether or not the updates change this draft back to the original state
|
365
|
-
def changed_to_original_for_draft?
|
366
|
-
send(self.draft_association_name).present? && send(self.class.draft_association_name).update? && !changed_notably_for_draft?
|
367
|
-
end
|
368
|
-
|
369
|
-
# Returns array of attributes that have changed for the object.
|
370
|
-
def changes_for_draftsman(options = {})
|
371
|
-
options[:changed_from] ||= {}
|
372
|
-
options[:previous_changes] ||= false
|
308
|
+
# Updates object and records a draft for an `update` event. If the draft
|
309
|
+
# is being updated to the object's original state, the draft is destroyed.
|
310
|
+
# Returns `true` or `false` depending on if the object passed validation
|
311
|
+
# and the save was successful.
|
312
|
+
def _draft_update
|
313
|
+
# TODO: Remove callback wrapper in v1.0.
|
314
|
+
transaction do
|
315
|
+
run_callbacks :draft_update do
|
316
|
+
# Run validations.
|
317
|
+
return false unless self.valid?
|
373
318
|
|
374
|
-
|
319
|
+
# If updating a create draft, also update this item.
|
320
|
+
if self.draft? && send(self.class.draft_association_name).create?
|
321
|
+
the_changes = changes_for_draftsman(:create)
|
322
|
+
data = { item: self }
|
323
|
+
data[Draftsman.whodunnit_field] = Draftsman.whodunnit
|
324
|
+
data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes?
|
325
|
+
data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft?
|
375
326
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
327
|
+
data = merge_metadata_for_draft(data)
|
328
|
+
send(self.class.draft_association_name).update(data)
|
329
|
+
self.save
|
330
|
+
else
|
331
|
+
the_changes = changes_for_draftsman(:update)
|
332
|
+
save_only_columns_for_draft if Draftsman.stash_drafted_changes?
|
333
|
+
|
334
|
+
# Destroy the draft if this record has changed back to the original
|
335
|
+
# record.
|
336
|
+
if self.draft? && the_changes.empty?
|
337
|
+
nilified_draft = send(self.class.draft_association_name)
|
338
|
+
send("#{self.class.draft_association_name}_id=", nil)
|
339
|
+
self.save
|
340
|
+
nilified_draft.destroy
|
341
|
+
# Save an update draft if record is changed notably.
|
342
|
+
elsif !the_changes.empty?
|
343
|
+
data = { item: self, event: :update }
|
344
|
+
data[Draftsman.whodunnit_field] = Draftsman.whodunnit
|
345
|
+
data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes?
|
346
|
+
data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft?
|
347
|
+
data = merge_metadata_for_draft(data)
|
348
|
+
|
349
|
+
# If there's already a draft, update it.
|
350
|
+
if self.draft?
|
351
|
+
send(self.class.draft_association_name).update(data)
|
352
|
+
|
353
|
+
if Draftsman.stash_drafted_changes?
|
354
|
+
update_skipped_attributes
|
355
|
+
else
|
356
|
+
self.save
|
357
|
+
end
|
358
|
+
# If there's not an existing draft, create an update draft.
|
359
|
+
else
|
360
|
+
send("build_#{self.class.draft_association_name}", data)
|
361
|
+
|
362
|
+
if send(self.class.draft_association_name).save
|
363
|
+
update_column("#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id)
|
364
|
+
|
365
|
+
if Draftsman.stash_drafted_changes?
|
366
|
+
update_skipped_attributes
|
367
|
+
else
|
368
|
+
self.save
|
369
|
+
end
|
370
|
+
else
|
371
|
+
raise ActiveRecord::Rollback and return false
|
372
|
+
end
|
373
|
+
end
|
374
|
+
# Otherwise, just save the record.
|
375
|
+
else
|
376
|
+
self.save
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
380
|
end
|
381
|
+
rescue Exception => e
|
382
|
+
false
|
383
|
+
end
|
381
384
|
|
382
|
-
|
383
|
-
|
385
|
+
# Returns hash of attributes that have changed for the object, similar to
|
386
|
+
# how ActiveRecord's `changes` works.
|
387
|
+
def changes_for_draftsman(event)
|
388
|
+
the_changes = {}
|
389
|
+
ignore = self.class.draftsman_options[:ignore]
|
390
|
+
skip = self.class.draftsman_options[:skip]
|
391
|
+
only = self.class.draftsman_options[:only]
|
392
|
+
draftable_attrs = self.attributes.keys - ignore - skip
|
393
|
+
draftable_attrs = draftable_attrs & only if only.present?
|
394
|
+
|
395
|
+
# If there's already an update draft, get its changes and reconcile them
|
396
|
+
# manually.
|
397
|
+
if event == :update
|
398
|
+
# Collect all attributes' previous and new values.
|
399
|
+
draftable_attrs.each do |attr|
|
400
|
+
if self.draft? && self.draft.changeset.key?(attr)
|
401
|
+
the_changes[attr] = [self.draft.changeset[attr].first, send(attr)]
|
402
|
+
else
|
403
|
+
the_changes[attr] = [self.send("#{attr}_was"), send(attr)]
|
404
|
+
end
|
405
|
+
end
|
406
|
+
# If there is no draft or it's for a create, then all draftable
|
407
|
+
# attributes are the changes.
|
408
|
+
else
|
409
|
+
draftable_attrs.each { |attr| the_changes[attr] = [nil, send(attr)] }
|
384
410
|
end
|
385
411
|
|
386
|
-
#
|
387
|
-
|
388
|
-
my_changes = options[:changed_from].merge new_changes
|
389
|
-
|
390
|
-
self.class.draft_class.object_changes_col_is_json? ? my_changes : Draftsman.serializer.dump(my_changes)
|
412
|
+
# Purge attributes that haven't changed.
|
413
|
+
the_changes.delete_if { |key, value| value.first == value.last }
|
391
414
|
end
|
392
415
|
|
393
416
|
# Merges model-level metadata from `meta` and `controller_info` into draft object.
|
@@ -413,52 +436,47 @@ module Draftsman
|
|
413
436
|
data.merge(Draftsman.controller_info || {})
|
414
437
|
end
|
415
438
|
|
416
|
-
# Returns array of attributes that were changed to trigger a draft.
|
417
|
-
def notably_changed_attributes_for_draft(options = {})
|
418
|
-
options[:previous_changes] ||= false
|
419
|
-
|
420
|
-
only = self.class.draftsman_options[:only]
|
421
|
-
only.empty? ? changed_and_not_ignored_for_draft(previous_changes: options[:previous_changes]) : (changed_and_not_ignored_for_draft(previous_changes: options[:previous_changes]) & only)
|
422
|
-
end
|
423
|
-
|
424
439
|
# Save columns outside of the `only` option directly to master table
|
425
440
|
def save_only_columns_for_draft
|
426
441
|
if self.class.draftsman_options[:only].any?
|
427
442
|
only_changes = {}
|
428
|
-
only_changed_attributes = self.
|
443
|
+
only_changed_attributes = self.attributes.keys - self.class.draftsman_options[:only]
|
429
444
|
|
430
|
-
only_changed_attributes.each do |
|
431
|
-
only_changes[
|
445
|
+
only_changed_attributes.each do |key|
|
446
|
+
only_changes[key] = send(key)
|
432
447
|
end
|
433
448
|
|
434
|
-
self.update_columns
|
449
|
+
self.update_columns(only_changes) if only_changes.any?
|
435
450
|
end
|
436
451
|
end
|
437
452
|
|
453
|
+
# Returns changeset data in format appropriate for `object_changes`
|
454
|
+
# column.
|
455
|
+
def serialized_draft_changeset(my_changes)
|
456
|
+
self.class.draft_class.object_changes_col_is_json? ? my_changes : Draftsman.serializer.dump(my_changes)
|
457
|
+
end
|
458
|
+
|
438
459
|
# Returns whether or not the draft class includes an `object_changes` attribute.
|
439
460
|
def track_object_changes_for_draft?
|
440
|
-
self.class.draft_class.column_names.include?
|
461
|
+
self.class.draft_class.column_names.include?('object_changes')
|
441
462
|
end
|
442
463
|
|
443
464
|
# Sets `trashed_at` attribute to now and saves to the database immediately.
|
444
465
|
def trash!
|
445
|
-
|
446
|
-
self.update_column self.class.trashed_at_attribute_name, send(self.class.trashed_at_attribute_name)
|
466
|
+
self.update_column(self.class.trashed_at_attribute_name, Time.now)
|
447
467
|
end
|
448
468
|
|
449
469
|
# Updates skipped attributes' values on this model.
|
450
470
|
def update_skipped_attributes
|
451
|
-
if
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
true
|
461
|
-
end
|
471
|
+
# Skip over this if nothing's being skipped.
|
472
|
+
return true unless draftsman_options[:skip].present?
|
473
|
+
|
474
|
+
keys = self.attributes.keys.select { |key| draftsman_options[:skip].include?(key) }
|
475
|
+
attrs = {}
|
476
|
+
keys.each { |key| attrs[key] = self.send(key) }
|
477
|
+
|
478
|
+
self.reload
|
479
|
+
self.update(attrs)
|
462
480
|
end
|
463
481
|
end
|
464
482
|
end
|
data/lib/draftsman/version.rb
CHANGED