draft_approve 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/.yardopts +6 -0
- data/Appraisals +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +91 -0
- data/LICENSE.md +21 -0
- data/README.md +329 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/draft_approve.gemspec +56 -0
- data/lib/draft_approve.rb +5 -0
- data/lib/draft_approve/draft_changes_proxy.rb +242 -0
- data/lib/draft_approve/draftable/base_class_methods.rb +33 -0
- data/lib/draft_approve/draftable/class_methods.rb +119 -0
- data/lib/draft_approve/draftable/instance_methods.rb +80 -0
- data/lib/draft_approve/errors.rb +19 -0
- data/lib/draft_approve/models/draft.rb +75 -0
- data/lib/draft_approve/models/draft_transaction.rb +109 -0
- data/lib/draft_approve/persistor.rb +167 -0
- data/lib/draft_approve/serialization/json.rb +16 -0
- data/lib/draft_approve/serialization/json/constants.rb +21 -0
- data/lib/draft_approve/serialization/json/draft_changes_proxy.rb +317 -0
- data/lib/draft_approve/serialization/json/serializer.rb +181 -0
- data/lib/draft_approve/transaction.rb +125 -0
- data/lib/draft_approve/version.rb +3 -0
- data/lib/generators/draft_approve/migration/migration_generator.rb +41 -0
- data/lib/generators/draft_approve/migration/templates/create_draft_approve_tables.rb +25 -0
- metadata +253 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'draft_approve/serialization/json/draft_changes_proxy'
|
2
|
+
require 'draft_approve/serialization/json/serializer'
|
3
|
+
|
4
|
+
module DraftApprove
|
5
|
+
module Serialization
|
6
|
+
module Json
|
7
|
+
def self.get_serializer
|
8
|
+
DraftApprove::Serialization::Json::Serializer
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.get_draft_changes_proxy
|
12
|
+
DraftApprove::Serialization::Json::DraftChangesProxy
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module DraftApprove
|
2
|
+
module Serialization
|
3
|
+
module Json
|
4
|
+
# Constants defining the keys used to point to associations when
|
5
|
+
# serializing draft changes.
|
6
|
+
#
|
7
|
+
# These work in a similar manner to how ActiveRecord polymorphic
|
8
|
+
# associations work, defining the type / class of the associated object,
|
9
|
+
# and it's id.
|
10
|
+
#
|
11
|
+
# IMPORTANT NOTE: These constants are written to the database, so cannot
|
12
|
+
# be updated without requiring a migration of existing draft data. Such a
|
13
|
+
# migration may be very slow, since these constants are embedded in the
|
14
|
+
# JSON generated by this serializer!
|
15
|
+
module Constants
|
16
|
+
TYPE = 'type'.freeze
|
17
|
+
ID = 'id'.freeze
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
require 'draft_approve/draft_changes_proxy'
|
2
|
+
require 'draft_approve/serialization/json/constants'
|
3
|
+
|
4
|
+
module DraftApprove
|
5
|
+
module Serialization
|
6
|
+
module Json
|
7
|
+
|
8
|
+
# Json implementation of +DraftApproveProxy+. Clients should not need to
|
9
|
+
# worry about the specific implementation details of this class, and
|
10
|
+
# should refer to the +DraftApprove::DraftChangesProxy+ module details of
|
11
|
+
# the public API.
|
12
|
+
#
|
13
|
+
# It is often most convenient to use the
|
14
|
+
# +DraftTransaction#draft_proxy_for+ method to construct a
|
15
|
+
# +DraftApproveProxy+ instance. This will ensure the correct
|
16
|
+
# implementation of +DraftApproveProxy+ is used.
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
#
|
20
|
+
# @see DraftApprove::DraftChangesProxy
|
21
|
+
# @see DraftTransaction#draft_proxy_for
|
22
|
+
class DraftChangesProxy
|
23
|
+
include DraftApprove::DraftChangesProxy
|
24
|
+
include Comparable
|
25
|
+
|
26
|
+
# The new, drafted value for the given attribute on the proxied +Draft+
|
27
|
+
# or draftable object. If no changes have been drafted for the given
|
28
|
+
# attribute, then returns the currently persisted value for the
|
29
|
+
# attribute.
|
30
|
+
#
|
31
|
+
# @param attribute_name [String]
|
32
|
+
#
|
33
|
+
# @return [Object, nil] the new value of the given attribute, or the
|
34
|
+
# currently persisted value if there are no draft changes for the
|
35
|
+
# attribute
|
36
|
+
#
|
37
|
+
# @see DraftApprove::DraftChangesProxy#new_value
|
38
|
+
def new_value(attribute_name)
|
39
|
+
# Create hash with default block for auto-memoization
|
40
|
+
@new_values_memo ||= Hash.new do |hash, attribute|
|
41
|
+
hash[attribute] = begin
|
42
|
+
association = @draftable_class.reflect_on_association(attribute)
|
43
|
+
if association.blank?
|
44
|
+
new_value_simple_attribute(attribute)
|
45
|
+
elsif association.belongs_to?
|
46
|
+
new_value_belongs_to_assocation(attribute)
|
47
|
+
else
|
48
|
+
new_value_non_belongs_to_assocation(attribute)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get memoized value, or calculate and store it
|
54
|
+
@new_values_memo[attribute_name.to_s]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Whether any changes will occur to the given association of the proxied
|
58
|
+
# +Draft+ or draftable object.
|
59
|
+
#
|
60
|
+
# @param association_name [String]
|
61
|
+
#
|
62
|
+
# @return [Boolean] +true+ if any objects will be added to this
|
63
|
+
# association, removed from this association, or existing associations
|
64
|
+
# changed in any way. +false+ otherwise.
|
65
|
+
#
|
66
|
+
# @see DraftApprove::DraftChangesProxy#association_changed?
|
67
|
+
def association_changed?(association_name)
|
68
|
+
# Create hash with default block for auto-memoization
|
69
|
+
@association_changed_memo ||= Hash.new do |hash, association_name|
|
70
|
+
hash[association_name] = begin
|
71
|
+
(
|
72
|
+
associations_added(association_name).present? ||
|
73
|
+
associations_updated(association_name).present? ||
|
74
|
+
associations_removed(association_name).present?
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@association_changed_memo[association_name.to_s]
|
80
|
+
end
|
81
|
+
|
82
|
+
# All associated objects which will be added to the given association of
|
83
|
+
# the proxied +Draft+ or draftable object.
|
84
|
+
#
|
85
|
+
# @param association_name [String]
|
86
|
+
#
|
87
|
+
# @return [Array<DraftChangesProxy>] DraftChangesProxy objects for each
|
88
|
+
# object which will be added to the given association
|
89
|
+
#
|
90
|
+
# @see DraftApprove::DraftChangesProxy#associations_added
|
91
|
+
def associations_added(association_name)
|
92
|
+
@associations_added_memo ||= Hash.new do |hash, association_name|
|
93
|
+
hash[association_name] = association_values(association_name, :created)
|
94
|
+
end
|
95
|
+
|
96
|
+
@associations_added_memo[association_name.to_s]
|
97
|
+
end
|
98
|
+
|
99
|
+
# All associated objects which have been updated, but remain
|
100
|
+
# the proxied +Draft+ or draftable object.
|
101
|
+
#
|
102
|
+
# @param association_name [String]
|
103
|
+
#
|
104
|
+
# @return [Array<DraftChangesProxy>] DraftChangesProxy objects for each
|
105
|
+
# object which will be added to the given association
|
106
|
+
#
|
107
|
+
# @see DraftApprove::DraftChangesProxy#associations_updated
|
108
|
+
def associations_updated(association_name)
|
109
|
+
@associations_updated_memo ||= Hash.new do |hash, association_name|
|
110
|
+
hash[association_name] = association_values(association_name, :updated)
|
111
|
+
end
|
112
|
+
|
113
|
+
@associations_updated_memo[association_name.to_s]
|
114
|
+
end
|
115
|
+
|
116
|
+
# All associated objects which will be removed from the given
|
117
|
+
# association of the proxied +Draft+ or draftable object.
|
118
|
+
#
|
119
|
+
# @param association_name [String]
|
120
|
+
#
|
121
|
+
# @return [Array<DraftChangesProxy>] DraftChangesProxy objects for each
|
122
|
+
# object which will be removed from the given association
|
123
|
+
#
|
124
|
+
# @see DraftApprove::DraftChangesProxy#associations_removed
|
125
|
+
def associations_removed(association_name)
|
126
|
+
@associations_removed_memo ||= Hash.new do |hash, association_name|
|
127
|
+
hash[association_name] = association_values(association_name, :deleted)
|
128
|
+
end
|
129
|
+
|
130
|
+
@associations_removed_memo[association_name.to_s]
|
131
|
+
end
|
132
|
+
|
133
|
+
# Override comparable for +DraftChangesProxy+ objects. This is so
|
134
|
+
# operators such as <tt>+</tt> and <tt>-</tt> work accurately when an
|
135
|
+
# array of +DraftChangesProxy+ objects are being returned. It also makes
|
136
|
+
# testing easier.
|
137
|
+
#
|
138
|
+
# @return [Integer] 0 if the given object is a +DraftChangesProxy+
|
139
|
+
# which refers to the same +Draft+ (if any), the same draftable
|
140
|
+
# (if any), the same draftable class, and the same +DraftTransaction+.
|
141
|
+
# Non-zero otherwise.
|
142
|
+
def <=>(other)
|
143
|
+
return -1 unless other.is_a?(self.class)
|
144
|
+
|
145
|
+
[:draft, :draftable, :draftable_class, :draft_transaction].each do |method|
|
146
|
+
comp = self.public_send(method) <=> other.public_send(method)
|
147
|
+
return -1 if comp.nil?
|
148
|
+
return comp unless comp.zero?
|
149
|
+
end
|
150
|
+
|
151
|
+
# Checked all attributes, and all are equal
|
152
|
+
return 0
|
153
|
+
end
|
154
|
+
|
155
|
+
alias :eql? :==
|
156
|
+
|
157
|
+
# Override hash for +DraftChangesProxy+ objects. This is so operators
|
158
|
+
# such as + and - work accurately when an array of +DraftChangesProxy+
|
159
|
+
# objects are being returned. It also makes testing easier.
|
160
|
+
#
|
161
|
+
# @return [Integer] a hash of all the +DraftChangeProxy+s attributes
|
162
|
+
def hash
|
163
|
+
[@draft, @draftable, @draftable_class, @draft_transaction].hash
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
# Helper to get the new value of a simple attribute as a result of this
|
169
|
+
# draft transaction
|
170
|
+
def new_value_simple_attribute(attribute_name)
|
171
|
+
attribute_name = attribute_name.to_s
|
172
|
+
|
173
|
+
# This attribute is a simple value (not an association)
|
174
|
+
if @draft.blank? || !@draft.draft_changes.has_key?(attribute_name)
|
175
|
+
# Either no draft, or no changes for this attribute
|
176
|
+
return draft_proxy_for(@draftable.public_send(attribute_name))
|
177
|
+
else
|
178
|
+
# Draft changes have been made on this attribute...
|
179
|
+
new_value = @draft.draft_changes[attribute_name][1]
|
180
|
+
return draft_proxy_for(new_value)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Helper to get the new value of a belongs_to association as a result
|
185
|
+
# of this draft transaction
|
186
|
+
def new_value_belongs_to_assocation(attribute_name)
|
187
|
+
attribute_name = attribute_name.to_s
|
188
|
+
|
189
|
+
# This attribute is an association where the 'belongs_to' is on this
|
190
|
+
# class...
|
191
|
+
if @draft.blank? || !@draft.draft_changes.has_key?(attribute_name)
|
192
|
+
# Either no draft, or no changes for this attribute
|
193
|
+
return draft_proxy_for(@draftable.public_send(attribute_name))
|
194
|
+
else
|
195
|
+
# Draft changes have been made on this attribute...
|
196
|
+
new_value = @draft.draft_changes[attribute_name][1]
|
197
|
+
|
198
|
+
if new_value.blank?
|
199
|
+
return nil # The association link has been removed on the draft
|
200
|
+
else
|
201
|
+
new_value_class = Object.const_get(new_value[Constants::TYPE])
|
202
|
+
new_value_object = new_value_class.find(new_value[Constants::ID])
|
203
|
+
return draft_proxy_for(new_value_object)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Helper to get the new value of has_one / has_many associations (ie.
|
209
|
+
# get all the objects the association would return after this draft
|
210
|
+
# dransaction has been applied)
|
211
|
+
def new_value_non_belongs_to_assocation(association_name)
|
212
|
+
association_name = association_name.to_s
|
213
|
+
|
214
|
+
associated_instances = []
|
215
|
+
|
216
|
+
# Starting point is all objects already associated
|
217
|
+
associated_instances += current_value(association_name)
|
218
|
+
|
219
|
+
# Add any new objects which will be created by this transaction and
|
220
|
+
# refer to this object
|
221
|
+
associated_instances += associations_added(association_name)
|
222
|
+
|
223
|
+
# Finally remove any associations which will be deleted or not
|
224
|
+
# refer to this object anymore as a reuslt of this transaction
|
225
|
+
associated_instances -= associations_removed(association_name)
|
226
|
+
|
227
|
+
return associated_instances
|
228
|
+
end
|
229
|
+
|
230
|
+
# Helper to get the associations which have been created or deleted as
|
231
|
+
# a result of this draft transaction
|
232
|
+
def association_values(association_name, mode)
|
233
|
+
association_name = association_name.to_s
|
234
|
+
|
235
|
+
association = @draftable_class.reflect_on_association(association_name)
|
236
|
+
if association.blank? || association.belongs_to?
|
237
|
+
raise(ArgumentError, "#{association_name} must be a has_many or has_one association")
|
238
|
+
end
|
239
|
+
|
240
|
+
associated_class_name = association.class_name
|
241
|
+
associated_attribute_name = association.inverse_of.name
|
242
|
+
|
243
|
+
# If we are proxying a concrete object, all associations will point
|
244
|
+
# directly at it, otherwise we are proxying a CREATE draft and
|
245
|
+
# associations will point at the draft
|
246
|
+
required_object = (@draftable.present? ? @draftable : @draft)
|
247
|
+
|
248
|
+
case mode
|
249
|
+
when :created
|
250
|
+
# Looking for newly created associations, so we want to find
|
251
|
+
# objects where the new value (index=1) of the associated
|
252
|
+
# attribute points at this object.
|
253
|
+
# eg. if looking for new memberships for Person 1, we want to find
|
254
|
+
# Membership objects where the json changes look like this:
|
255
|
+
# { "person" => [nil, { "TYPE" => "Person", "ID" => 1 }] }
|
256
|
+
json_query_str_type = "{#{associated_attribute_name},1,#{Constants::TYPE}}"
|
257
|
+
json_query_str_id = "{#{associated_attribute_name},1,#{Constants::ID}}"
|
258
|
+
|
259
|
+
created_associations = @draft_transaction.drafts.where(
|
260
|
+
draftable_type: associated_class_name
|
261
|
+
).where(
|
262
|
+
<<~SQL
|
263
|
+
draft_changes #>> '#{json_query_str_type}' = '#{required_object.class.name}'
|
264
|
+
AND
|
265
|
+
draft_changes #>> '#{json_query_str_id}' = '#{required_object.id}'
|
266
|
+
SQL
|
267
|
+
)
|
268
|
+
|
269
|
+
return draft_proxy_for(created_associations)
|
270
|
+
when :updated
|
271
|
+
# Looking for associations which have draft updates but which still
|
272
|
+
# point at this object (if it didn't previously point at this object
|
273
|
+
# the change is a new association, so covered by the association
|
274
|
+
# created case - if it no longer points at this object the
|
275
|
+
# association has been broken, so covered by the association deleted
|
276
|
+
# case)
|
277
|
+
required_proxy = draft_proxy_for(required_object)
|
278
|
+
|
279
|
+
updated_associations = current_value(association_name).select do |proxy|
|
280
|
+
proxy.changed? &&
|
281
|
+
proxy.new_value(associated_attribute_name) == required_proxy
|
282
|
+
end
|
283
|
+
|
284
|
+
return draft_proxy_for(updated_associations)
|
285
|
+
when :deleted
|
286
|
+
# Looking for current associations which have either been drafted
|
287
|
+
# for complete deletion, or which have had their reference changed
|
288
|
+
# to no longer point at this object
|
289
|
+
required_proxy = draft_proxy_for(required_object)
|
290
|
+
|
291
|
+
deleted_associations = current_value(association_name).select do |proxy|
|
292
|
+
proxy.delete? ||
|
293
|
+
proxy.new_value(associated_attribute_name) != required_proxy
|
294
|
+
end
|
295
|
+
|
296
|
+
return draft_proxy_for(deleted_associations)
|
297
|
+
else
|
298
|
+
raise(ArgumentError, "Unrecognised mode #{mode}")
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# Helper to get a draft proxy for any object before returning it
|
303
|
+
def draft_proxy_for(object)
|
304
|
+
if object.respond_to?(:map)
|
305
|
+
# Object is a collection (likely an ActiveRecord collection), recursively call draft_proxy_for
|
306
|
+
return object.map { |o| draft_proxy_for(o) }
|
307
|
+
elsif (object.is_a? Draft) || (object.respond_to?(:draftable?) && object.draftable?)
|
308
|
+
# Object is a draft or a draftable, wrap it in a DraftChangesProxy
|
309
|
+
return self.class.new(object, @draft_transaction)
|
310
|
+
else
|
311
|
+
return object
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'draft_approve/serialization/json/constants'
|
2
|
+
|
3
|
+
module DraftApprove
|
4
|
+
module Serialization
|
5
|
+
module Json
|
6
|
+
|
7
|
+
# Logic for serializing changes to ActiveRecord models into JSON format,
|
8
|
+
# and deserializing the changes on a +Draft+ object into the new values
|
9
|
+
# for an ActiveRecord model.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class Serializer
|
13
|
+
|
14
|
+
# Serialize changes on an ActiveRecord model into a JSON representation
|
15
|
+
# of the changes.
|
16
|
+
#
|
17
|
+
# @param model [Object] the +acts_as_draftable+ ActiveRecord model whose
|
18
|
+
# changes will be serialized to JSON
|
19
|
+
#
|
20
|
+
# @return [Hash] a hash representation of the changes to the given
|
21
|
+
# model, which can be automatically converted to JSON if persisted to
|
22
|
+
# a JSON formatted database column
|
23
|
+
def self.changes_for_model(model)
|
24
|
+
JsonSerializer.new(model).changes_for_model
|
25
|
+
end
|
26
|
+
|
27
|
+
# Deserialize changes from a +Draft+ object into the new values for the
|
28
|
+
# +acts_as_draftable+ ActiveRecord model the draft relates to.
|
29
|
+
#
|
30
|
+
# @param draft [Draft] the +Draft+ whose changes will be deserialized
|
31
|
+
#
|
32
|
+
# @return [Hash] a hash representation of the new values for the object
|
33
|
+
# the given draft relates to.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# draft = Person.new(name: 'Joe Bloggs').save_draft!
|
37
|
+
# Json::Serializer.new_values_for_draft(draft)
|
38
|
+
# #=> { "name" => [nil, "Joe Bloggs"] }
|
39
|
+
def self.new_values_for_draft(draft)
|
40
|
+
JsonDeserializer.new(draft).new_values_for_draft
|
41
|
+
end
|
42
|
+
|
43
|
+
# Private Inner Class to contain the JSON Serialization logic
|
44
|
+
class JsonSerializer
|
45
|
+
def initialize(model)
|
46
|
+
@model = model
|
47
|
+
end
|
48
|
+
|
49
|
+
def changes_for_model
|
50
|
+
changes = {}
|
51
|
+
@model.class.reflect_on_all_associations(:belongs_to).each do |belongs_to_assoc|
|
52
|
+
changes.merge!(association_change(belongs_to_assoc))
|
53
|
+
end
|
54
|
+
return changes.merge!(non_association_changes)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def association_change(association)
|
60
|
+
old_value = association_old_value(association)
|
61
|
+
new_value = association_new_value(association)
|
62
|
+
|
63
|
+
if old_value == new_value
|
64
|
+
return {}
|
65
|
+
else
|
66
|
+
return { association.name.to_s => [old_value, new_value] }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def non_association_changes
|
71
|
+
association_attribute_names = @model.class.reflect_on_all_associations(:belongs_to).map do |ref|
|
72
|
+
[ref.foreign_type, ref.foreign_key, ref.association_foreign_key]
|
73
|
+
end.flatten.uniq.compact
|
74
|
+
|
75
|
+
non_association_attribute_names = @model.attribute_names - association_attribute_names
|
76
|
+
|
77
|
+
return non_association_attribute_names.each_with_object({}) do |attribute_name, result_hash|
|
78
|
+
if @model.public_send("#{attribute_name}_changed?")
|
79
|
+
result_hash[attribute_name] = @model.public_send("#{attribute_name}_change")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# The old value of an association must be nil or point to a persisted
|
85
|
+
# non-draft object.
|
86
|
+
def association_old_value(association)
|
87
|
+
if association.polymorphic?
|
88
|
+
old_type = @model.public_send("#{association.foreign_type}_was")
|
89
|
+
old_id = @model.public_send("#{association.foreign_key}_was")
|
90
|
+
else
|
91
|
+
old_type = association.class_name
|
92
|
+
old_id = @model.public_send("#{association.foreign_key}_was")
|
93
|
+
end
|
94
|
+
|
95
|
+
return nil if old_id.blank? || old_type.blank?
|
96
|
+
return { Constants::TYPE => old_type, Constants::ID => old_id }
|
97
|
+
end
|
98
|
+
|
99
|
+
# The new value of an association may be nil, or point to a persisted
|
100
|
+
# model, or point to a non-persisted model with a persisted draft.
|
101
|
+
#
|
102
|
+
# Note that if the associated object is not persisted, and has no
|
103
|
+
# persisted draft, then this is an error scenario.
|
104
|
+
def association_new_value(association)
|
105
|
+
associated_obj = @model.public_send(association.name)
|
106
|
+
|
107
|
+
if associated_obj.blank?
|
108
|
+
return nil
|
109
|
+
elsif associated_obj.persisted?
|
110
|
+
if association.polymorphic?
|
111
|
+
return {
|
112
|
+
Constants::TYPE => @model.public_send(association.foreign_type),
|
113
|
+
Constants::ID => @model.public_send(association.foreign_key)
|
114
|
+
}
|
115
|
+
else
|
116
|
+
return {
|
117
|
+
Constants::TYPE => association.class_name,
|
118
|
+
Constants::ID => @model.public_send(association.foreign_key)
|
119
|
+
}
|
120
|
+
end
|
121
|
+
else # associated_obj not persisted - so we need a persisted draft
|
122
|
+
draft = associated_obj.draft_pending_approval
|
123
|
+
|
124
|
+
if draft.blank? || draft.new_record?
|
125
|
+
raise(DraftApprove::Errors::AssociationUnsavedError, "#{association.name} points to an unsaved object")
|
126
|
+
end
|
127
|
+
|
128
|
+
return { Constants::TYPE => draft.class.name, Constants::ID => draft.id }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Private Inner Class to contain the JSON Serialization logic
|
134
|
+
class JsonDeserializer
|
135
|
+
def initialize(draft)
|
136
|
+
@draft = draft
|
137
|
+
end
|
138
|
+
|
139
|
+
def new_values_for_draft
|
140
|
+
draftable_class = Object.const_get(@draft.draftable_type)
|
141
|
+
association_attribute_names = draftable_class.reflect_on_all_associations(:belongs_to).map(&:name).map(&:to_s)
|
142
|
+
|
143
|
+
return @draft.draft_changes.each_with_object({}) do |(attribute_name, change), result_hash|
|
144
|
+
new_value = change[1]
|
145
|
+
if association_attribute_names.include?(attribute_name)
|
146
|
+
result_hash[attribute_name] = associated_model_for_new_value(new_value)
|
147
|
+
else
|
148
|
+
result_hash[attribute_name] = new_value
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def associated_model_for_new_value(new_value)
|
156
|
+
return nil if new_value.nil?
|
157
|
+
|
158
|
+
associated_model_type = new_value[Constants::TYPE]
|
159
|
+
associated_model_id = new_value[Constants::ID]
|
160
|
+
|
161
|
+
associated_class = Object.const_get(associated_model_type)
|
162
|
+
|
163
|
+
if associated_class.ancestors.include? Draft
|
164
|
+
# The associated class is a draft (or subclass).
|
165
|
+
# It must be in the same draft transaction as the draft we're getting values for.
|
166
|
+
associated_draft = @draft.draft_transaction.drafts.find(associated_model_id)
|
167
|
+
|
168
|
+
raise(DraftApprove::Errors::PriorDraftNotAppliedError) if associated_draft.draftable.nil?
|
169
|
+
|
170
|
+
return associated_draft.draftable
|
171
|
+
else
|
172
|
+
return associated_class.find(associated_model_id)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
private_constant :JsonSerializer, :JsonDeserializer
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|