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
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "draft_approve"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'draft_approve/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'draft_approve'
|
8
|
+
spec.version = DraftApprove::VERSION
|
9
|
+
spec.authors = ['Andrew Sibley']
|
10
|
+
spec.email = ['andrew.s@38degrees.org.uk']
|
11
|
+
spec.license = 'MIT'
|
12
|
+
spec.homepage = 'https://github.com/38dgs/draft_approve'
|
13
|
+
spec.summary = %q{Save drafts of ActiveRecord models & approve them to apply the changes.}
|
14
|
+
spec.description = %q{
|
15
|
+
All draft data is saved in a separate table, so no need to worry about
|
16
|
+
existing code / SQL accidentally finding non-approved data. Supports draft
|
17
|
+
changes to existing objects, and creating new objects as drafts. Supports
|
18
|
+
'draft transactions' which may update / create many objects, and must be
|
19
|
+
approved / rejected in their entirety.
|
20
|
+
}
|
21
|
+
|
22
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
23
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
24
|
+
# if spec.respond_to?(:metadata)
|
25
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
26
|
+
#
|
27
|
+
# spec.metadata['homepage_uri'] = spec.homepage
|
28
|
+
# spec.metadata['source_code_uri'] = 'https://github.com/38dgs/draft_approve'
|
29
|
+
# spec.metadata['changelog_uri'] = 'https://github.com/38dgs/draft_approve/CHANGELOG.md'
|
30
|
+
# else
|
31
|
+
# raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
|
32
|
+
# end
|
33
|
+
|
34
|
+
# Specify which files should be added to the gem when it is released.
|
35
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
36
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
37
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
38
|
+
end
|
39
|
+
spec.bindir = "exe"
|
40
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
41
|
+
spec.require_paths = ["lib"]
|
42
|
+
|
43
|
+
spec.add_dependency "activerecord", "~> 5.2"
|
44
|
+
|
45
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
46
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
47
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
48
|
+
spec.add_development_dependency "database_cleaner", "~> 1.7"
|
49
|
+
spec.add_development_dependency "sqlite3", "~> 1.3"
|
50
|
+
spec.add_development_dependency "pg", ">= 0.18", "< 2.0"
|
51
|
+
spec.add_development_dependency "factory_bot", "~> 4.11"
|
52
|
+
spec.add_development_dependency "codecov", "~> 0.1"
|
53
|
+
spec.add_development_dependency "appraisal", "~> 2.2"
|
54
|
+
spec.add_development_dependency "pry", "~> 0.12"
|
55
|
+
spec.add_development_dependency "yard", "~> 0.9.18"
|
56
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module DraftApprove
|
2
|
+
|
3
|
+
# Mixin wrapper for +Draft+ and +acts_as_draftable+ objects, such that both
|
4
|
+
# have a consistent API to get current and new values within the context of
|
5
|
+
# a specific +DraftTransaction+.
|
6
|
+
#
|
7
|
+
# References to other objects returned by methods from this class are also
|
8
|
+
# wrapped in a +DraftChangesProxy+, meaning it is relatively easy to chain
|
9
|
+
# and navigate complex association trees within the context of a
|
10
|
+
# +DraftTransaction+.
|
11
|
+
#
|
12
|
+
# This can be useful, for example, to display all changes that will occur
|
13
|
+
# on an object, including changes to all it's associated 'child' objects.
|
14
|
+
#
|
15
|
+
# It is often most convenient to use the
|
16
|
+
# +DraftTransaction#draft_proxy_for+ method to construct a
|
17
|
+
# +DraftApproveProxy+ instance. This will ensure the correct implementation
|
18
|
+
# of +DraftApproveProxy+ is used.
|
19
|
+
#
|
20
|
+
# Classes which include this module must implement the instance methods
|
21
|
+
# +new_value+, +association_changed?+, +associations_added+,
|
22
|
+
# +associations_updated+, +associations_removed+.
|
23
|
+
#
|
24
|
+
# @see DraftTransaction#draft_proxy_for
|
25
|
+
module DraftChangesProxy
|
26
|
+
attr_reader :draft, :draftable, :draftable_class, :draft_transaction
|
27
|
+
|
28
|
+
# Creates a new DraftChangesProxy
|
29
|
+
#
|
30
|
+
# @param object [Object] the +Draft+ object, or the instance of an
|
31
|
+
# +acts_as_draftable+ class, which is being proxied to get changes
|
32
|
+
# @param transaction [DraftTransaction] the +DraftTransaction+ within
|
33
|
+
# which to look for changes. If +object+ is a +Draft+, this parameter
|
34
|
+
# is optional and if not provided will use the +DraftTransaction+
|
35
|
+
# associated with the given +Draft+. If +object+ is not a +Draft+,
|
36
|
+
# this parameter is required.
|
37
|
+
def initialize(object, transaction = nil)
|
38
|
+
if object.blank?
|
39
|
+
raise(ArgumentError, "object is required")
|
40
|
+
end
|
41
|
+
|
42
|
+
if object.new_record?
|
43
|
+
raise(ArgumentError, "object #{object} must already be persisted")
|
44
|
+
end
|
45
|
+
|
46
|
+
if object.is_a? Draft
|
47
|
+
if transaction.present? && object.draft_transaction != transaction
|
48
|
+
raise(ArgumentError, "draft_transaction for #{object} is inconsistent with given draft_transaction #{transaction}")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Construct DraftableProxy from a draft
|
52
|
+
# Note that @draftable may be nil (if this is a CREATE draft)
|
53
|
+
@draft = object
|
54
|
+
@draftable = (object.draftable.present? && object.draftable.persisted?) ? object.draftable : nil
|
55
|
+
@draftable_class = Object.const_get(object.draftable_type)
|
56
|
+
@draft_transaction = object.draft_transaction
|
57
|
+
else
|
58
|
+
if transaction.blank?
|
59
|
+
raise(ArgumentError, "draft_transaction is required when object is a draftable")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Construct DraftableProxy from a draftable
|
63
|
+
# Note that @draft may be nil (if the draftable has no changes within the scope of this transaction)
|
64
|
+
@draft = transaction.drafts.find_by(draftable: object)
|
65
|
+
@draftable = object
|
66
|
+
@draftable_class = object.class
|
67
|
+
@draft_transaction = transaction
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Boolean] +true+ if this +Draft+ is to create a new record,
|
72
|
+
# +false+ otherwise
|
73
|
+
def create?
|
74
|
+
@draft.present? && @draft.create?
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Boolean] +true+ if this +Draft+ is to delete an existing
|
78
|
+
# record, +false+ otherwise
|
79
|
+
def delete?
|
80
|
+
@draft.present? && @draft.delete?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Whether or not the proxied +Draft+ or draftable object has any
|
84
|
+
# changes.
|
85
|
+
#
|
86
|
+
# Note, this method only considers changes to attributes and changes
|
87
|
+
# to any +belongs_to+ references. Any added / changed / deleted
|
88
|
+
# +has_many+ or +has_one+ associations are not considered.
|
89
|
+
#
|
90
|
+
# @return [Boolean] whether or not the proxied object has changes
|
91
|
+
def changed?
|
92
|
+
if @draft.blank?
|
93
|
+
false # No draft for this object, so nothing changed
|
94
|
+
else
|
95
|
+
@draft.draft_changes.present?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# List of attributes on the proxied +Draft+ or draftable object which
|
100
|
+
# have changes.
|
101
|
+
#
|
102
|
+
# Note, this method only considers changes to attributes and changes
|
103
|
+
# to any +belongs_to+ references. Any added / changed / deleted
|
104
|
+
# +has_many+ or +has_one+ associations are not considered.
|
105
|
+
#
|
106
|
+
# @return [Array<String>] array of the attributes which have changed on
|
107
|
+
# the proxied object
|
108
|
+
def changed
|
109
|
+
if @draft.blank?
|
110
|
+
[] # No draft for this object, so no attributes have changed
|
111
|
+
else
|
112
|
+
@draft.draft_changes.keys
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Hash of changes on the proxied +Draft+ or draftable object which
|
117
|
+
# have changes.
|
118
|
+
#
|
119
|
+
# Note, this method only considers changes to attributes and changes
|
120
|
+
# to any +belongs_to+ references. Any added / changed / deleted
|
121
|
+
# +has_many+ or +has_one+ associations are not considered.
|
122
|
+
#
|
123
|
+
# @return [Hash<String, Array>] hash of the changes on the proxied
|
124
|
+
# object, eg. <tt>{ "name" => ["old_name", "new_name"] }</tt>
|
125
|
+
def changes
|
126
|
+
@changes_memo ||= begin # Memoize result
|
127
|
+
if @draft.blank?
|
128
|
+
{} # No draft for this object, so no attributes have changed
|
129
|
+
else
|
130
|
+
@draft.draft_changes.each_with_object({}) do |(k,v), new_hash|
|
131
|
+
new_hash[k] = [current_value(k), new_value(k)]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# The currently persisted value for the given attribute on the proxied
|
138
|
+
# +Draft+ or draftable object.
|
139
|
+
#
|
140
|
+
# @param attribute_name [String]
|
141
|
+
#
|
142
|
+
# @return [Object, nil] the old value of the given attribute, or +nil+
|
143
|
+
# if there was no previous value
|
144
|
+
def current_value(attribute_name)
|
145
|
+
# Create hash with default block for auto-memoization
|
146
|
+
@current_values_memo ||= Hash.new do |hash, attribute|
|
147
|
+
hash[attribute] = begin
|
148
|
+
if @draftable.present?
|
149
|
+
# Current value is what's on the draftable object
|
150
|
+
draft_proxy_for(@draftable.public_send(attribute))
|
151
|
+
else
|
152
|
+
# No draftable exists, so this must be a CREATE draft, meaning
|
153
|
+
# there's no 'old' value...
|
154
|
+
association = @draftable_class.reflect_on_association(attribute)
|
155
|
+
if (association.blank? || association.belongs_to? || association.has_one?)
|
156
|
+
nil # Not an association, or links to a single object
|
157
|
+
else
|
158
|
+
[] # Is a has_many association
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Get memoized value, or calculate and store it
|
165
|
+
@current_values_memo[attribute_name.to_s]
|
166
|
+
end
|
167
|
+
|
168
|
+
# The new, drafted value for the given attribute on the proxied +Draft+
|
169
|
+
# or draftable object. If no changes have been drafted for the given
|
170
|
+
# attribute, then returns the currently persisted value for the
|
171
|
+
# attribute.
|
172
|
+
#
|
173
|
+
# @param attribute_name [String]
|
174
|
+
#
|
175
|
+
# @return [Object, nil] the new value of the given attribute, or the
|
176
|
+
# currently persisted value if there are no draft changes for the
|
177
|
+
# attribute
|
178
|
+
def new_value(attribute_name)
|
179
|
+
raise "#new_value has not been implemented in #{self.class.name}"
|
180
|
+
end
|
181
|
+
|
182
|
+
# Whether any changes will occur to the given association of the proxied
|
183
|
+
# +Draft+ or draftable object.
|
184
|
+
#
|
185
|
+
# @param association_name [String]
|
186
|
+
#
|
187
|
+
# @return [Boolean] +true+ if any objects will be added to this
|
188
|
+
# association, removed from this association, or existing associations
|
189
|
+
# changed in any way. +false+ otherwise.
|
190
|
+
def association_changed?(association_name)
|
191
|
+
raise "#association_changed? has not been implemented in #{self.class.name}"
|
192
|
+
end
|
193
|
+
|
194
|
+
# All associated objects which will be added to the given association of
|
195
|
+
# the proxied +Draft+ or draftable object.
|
196
|
+
#
|
197
|
+
# @param association_name [String]
|
198
|
+
#
|
199
|
+
# @return [Array<DraftChangesProxy>] DraftChangesProxy objects for each
|
200
|
+
# object which will be added to the given association
|
201
|
+
def associations_added(association_name)
|
202
|
+
raise "#associations_added has not been implemented in #{self.class.name}"
|
203
|
+
end
|
204
|
+
|
205
|
+
# All associated objects which have been updated, but remain
|
206
|
+
# the proxied +Draft+ or draftable object.
|
207
|
+
#
|
208
|
+
# @param association_name [String]
|
209
|
+
#
|
210
|
+
# @return [Array<DraftChangesProxy>] DraftChangesProxy objects for each
|
211
|
+
# object which will be added to the given association
|
212
|
+
def associations_updated(association_name)
|
213
|
+
raise "#associations_updated has not been implemented in #{self.class.name}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# All associated objects which will be removed from the given
|
217
|
+
# association of the proxied +Draft+ or draftable object.
|
218
|
+
#
|
219
|
+
# @param association_name [String]
|
220
|
+
#
|
221
|
+
# @return [Array<DraftChangesProxy>] DraftChangesProxy objects for each
|
222
|
+
# object which will be removed from the given association
|
223
|
+
def associations_removed(association_name)
|
224
|
+
raise "#associations_removed has not been implemented in #{self.class.name}"
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns a string representing the current value of the proxied object.
|
228
|
+
#
|
229
|
+
# @return [String] the +to_s+ of the current value of the proxied object
|
230
|
+
# (ie. the value before any changes would take effect). If there is no
|
231
|
+
# current value (ie. this is a proxy for a new draft) then simply
|
232
|
+
# returns "New <classname>".
|
233
|
+
def current_to_s
|
234
|
+
if @draftable.present?
|
235
|
+
return "#{@draftable.to_s} <#{@draftable_class} ##{@draftable.id}>"
|
236
|
+
else
|
237
|
+
# No current draftable
|
238
|
+
return "New #{@draftable_class}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'draft_approve/draftable/class_methods'
|
2
|
+
require 'draft_approve/draftable/instance_methods'
|
3
|
+
require 'draft_approve/models/draft'
|
4
|
+
|
5
|
+
module DraftApprove
|
6
|
+
module Draftable
|
7
|
+
|
8
|
+
# Methods automatically added to +ActiveRecord::Base+ when including the
|
9
|
+
# DraftApprove gem
|
10
|
+
module BaseClassMethods
|
11
|
+
|
12
|
+
# Allows the object to be used as a draftable, adding the
|
13
|
+
# +DraftApprove::Draftable+ instance and class methods to the object.
|
14
|
+
#
|
15
|
+
# @param options [Hash] optional configuration, currently unused
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# class Person < ActiveRecord::Base
|
19
|
+
# acts_as_draftable
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @see DraftApprove::Draftable::InstanceMethods
|
23
|
+
# @see DraftApprove::Draftable::ClassMethods
|
24
|
+
def acts_as_draftable(options={})
|
25
|
+
include DraftApprove::Draftable::InstanceMethods
|
26
|
+
extend DraftApprove::Draftable::ClassMethods
|
27
|
+
|
28
|
+
has_many :drafts, as: :draftable
|
29
|
+
has_one :draft_pending_approval, -> { pending_approval }, class_name: "Draft", as: :draftable, inverse_of: :draftable
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'draft_approve/transaction'
|
2
|
+
|
3
|
+
module DraftApprove
|
4
|
+
module Draftable
|
5
|
+
|
6
|
+
# Class methods automatically added to an ActiveRecord model when
|
7
|
+
# +acts_as_draftable+ is called
|
8
|
+
module ClassMethods
|
9
|
+
##### Basic DraftApprove class methods #####
|
10
|
+
|
11
|
+
# Starts a new +DraftTransaction+ to group together a number of draft
|
12
|
+
# changes that must be approved and applied together.
|
13
|
+
#
|
14
|
+
# @yield the block which creates a group of draft changes that must be
|
15
|
+
# approved and applied together
|
16
|
+
# @param created_by [String] the person or process responsible for
|
17
|
+
# creating the draft changes in this transaction
|
18
|
+
# @param extra_data [Hash] any extra metadata to be associated with
|
19
|
+
# these draft changes
|
20
|
+
#
|
21
|
+
# @return [DraftTransaction, nil] the +DraftTransaction+ which was
|
22
|
+
# created, or +nil+ if no draft changes were saved within the given
|
23
|
+
# block (ie. if approving the +DraftTransaction+ would be a
|
24
|
+
# 'no-operation')
|
25
|
+
def draft_transaction(created_by: nil, extra_data: nil)
|
26
|
+
DraftApprove::Transaction.in_new_draft_transaction(created_by: created_by, extra_data: extra_data) do
|
27
|
+
yield
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
##### Additional convenience DraftApprove class methods #####
|
32
|
+
|
33
|
+
# Creates a new object with the given attributes, and saves the new object
|
34
|
+
# as a draft.
|
35
|
+
#
|
36
|
+
# @param attributes [Hash] a hash of attribute names to attribute values,
|
37
|
+
# like the hash expected by the ActiveRecord +create+ / +create!+
|
38
|
+
# methods
|
39
|
+
#
|
40
|
+
# @return [Draft] the resulting +Draft+ record (*not* the created
|
41
|
+
# draftable object)
|
42
|
+
def draft_create!(attributes)
|
43
|
+
self.new(attributes).draft_save!
|
44
|
+
end
|
45
|
+
|
46
|
+
# Finds an object with the given attributes. If none found, creates a new
|
47
|
+
# object with the given attributes, executes the given block, and saves
|
48
|
+
# the new object as a draft.
|
49
|
+
#
|
50
|
+
# @param attributes [Hash] a hash of attribute names to attribute values,
|
51
|
+
# like the hash expected by the ActiveRecord +find_or_create_by+ method
|
52
|
+
# @yield [instance] a block which sets additional attributes on the newly
|
53
|
+
# created object instance if no existing instance is found
|
54
|
+
#
|
55
|
+
# @return [Object] the draftable object which was found or created
|
56
|
+
# (*not* the +Draft+ object which may have been saved)
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# # Find a person by their name. If no person found, create a person
|
60
|
+
# # with that name, and also set their birth date.
|
61
|
+
# Person.find_or_create_draft_by!(name: 'My Name') do |p|
|
62
|
+
# p.birth_date = '1980-01-01'
|
63
|
+
# end
|
64
|
+
def find_or_create_draft_by!(attributes)
|
65
|
+
instance = self.find_by(attributes)
|
66
|
+
|
67
|
+
if instance.blank?
|
68
|
+
instance = self.new(attributes)
|
69
|
+
|
70
|
+
# Only execute the block if this is a new record
|
71
|
+
yield(instance) if block_given?
|
72
|
+
end
|
73
|
+
|
74
|
+
instance.draft_save!
|
75
|
+
return instance
|
76
|
+
end
|
77
|
+
|
78
|
+
# Finds an object with the given attributes and draft update it with the
|
79
|
+
# given block, or draft create a new object.
|
80
|
+
#
|
81
|
+
# If an object is found matching the given attributes, the given block
|
82
|
+
# is applied to this object and the updates are saved as a draft.
|
83
|
+
#
|
84
|
+
# If no object is found matching the given attributes, a new object is
|
85
|
+
# initialised with the given attributes, and the given block is applied to
|
86
|
+
# this new object before it is saved as a draft.
|
87
|
+
#
|
88
|
+
# @param attributes [Hash] a hash of attribute names to attribute values,
|
89
|
+
# like the hash expected by the ActiveRecord +find_or_create_by+ method
|
90
|
+
# @yield [instance] a block which makes changes to the object instance
|
91
|
+
# which was found or created using the given attributes hash
|
92
|
+
#
|
93
|
+
# @return [Object] the draftable object which was found and updated, or
|
94
|
+
# the draftable object which was created (*not* the +Draft+ object
|
95
|
+
# which may have been saved)
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# # Find a person by their name, and draft update their birth date,
|
99
|
+
# # OR draft create a person with the given name and birth date.
|
100
|
+
# Person.find_and_draft_update_or_create_draft_by!(name: 'My Name') do |p|
|
101
|
+
# p.birth_date = '1980-01-01'
|
102
|
+
# end
|
103
|
+
def find_and_draft_update_or_create_draft_by!(attributes)
|
104
|
+
instance = self.find_by(attributes)
|
105
|
+
|
106
|
+
if instance.blank?
|
107
|
+
instance = self.new(attributes)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Whether or not this is a new record, execute the block to update
|
111
|
+
# additional, non-find_by attributes
|
112
|
+
yield(instance) if block_given?
|
113
|
+
|
114
|
+
instance.draft_save!
|
115
|
+
return instance
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|