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
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
|