draft_approve 0.1.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 +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
|