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,125 @@
|
|
1
|
+
require 'draft_approve/errors'
|
2
|
+
require 'draft_approve/models/draft_transaction'
|
3
|
+
|
4
|
+
module DraftApprove
|
5
|
+
|
6
|
+
# Logic for handling ActiveRecord database transactions, and creating
|
7
|
+
# +DraftTransaction+ records.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class Transaction
|
11
|
+
|
12
|
+
# Start a new database transaction, and create a new DraftTransaction to
|
13
|
+
# wrap the commands in the block.
|
14
|
+
#
|
15
|
+
# @param created_by [String] the user or process which created this
|
16
|
+
# +DraftTransaction+ and the draft changes within it
|
17
|
+
# @param extra_data [Hash] any extra metadata to be stored with this
|
18
|
+
# +DraftTransaction+
|
19
|
+
# @yield invokes the block, during which methods should be called to save
|
20
|
+
# +Draft+ objects
|
21
|
+
#
|
22
|
+
# @return [DraftTransaction, nil] the resulting +DraftTransaction+, or +nil+
|
23
|
+
# if no +Draft+ changes were saved within the given block
|
24
|
+
def self.in_new_draft_transaction(created_by: nil, extra_data: nil)
|
25
|
+
(draft_transaction, yield_return) = in_new_draft_transaction_helper(created_by: created_by, extra_data: extra_data) do
|
26
|
+
yield
|
27
|
+
end
|
28
|
+
|
29
|
+
# in_new_draft_transaction is used in Model.draft_transaction do ... blocks
|
30
|
+
# so we want to return the transaction itself to the caller
|
31
|
+
return draft_transaction
|
32
|
+
end
|
33
|
+
|
34
|
+
# Ensure the block is running in a database transaction and a
|
35
|
+
# +DraftTransaction+ - if there's not one already, create one.
|
36
|
+
#
|
37
|
+
# @param created_by [String] the user or process which created this
|
38
|
+
# +DraftTransaction+ and the draft changes within it. Ignored if a
|
39
|
+
# +DraftTransaction+ already exists within the scope of the current
|
40
|
+
# thread / fiber.
|
41
|
+
# @param extra_data [Hash] any extra metadata to be stored with this
|
42
|
+
# +DraftTransaction+. Ignored if a +DraftTransaction+ already exists
|
43
|
+
# within the scope of the current thread / fiber.
|
44
|
+
# @yield invokes the block, during which methods should be called to save
|
45
|
+
# +Draft+ objects
|
46
|
+
#
|
47
|
+
# @return [Object] the result of yielding to the given block
|
48
|
+
def self.ensure_in_draft_transaction(created_by: nil, extra_data: nil)
|
49
|
+
draft_transaction = current_draft_transaction
|
50
|
+
|
51
|
+
if draft_transaction
|
52
|
+
# There's an existing draft_transaction, just yield to the block
|
53
|
+
yield
|
54
|
+
else
|
55
|
+
# There's no transaction - start one and yield to the block inside the
|
56
|
+
# new transaction
|
57
|
+
(draft_transaction, yield_return) = in_new_draft_transaction_helper(created_by: created_by, extra_data: extra_data) do
|
58
|
+
yield
|
59
|
+
end
|
60
|
+
|
61
|
+
# ensure_in_draft_transaction is used in model.draft_save! method calls
|
62
|
+
# so we want to return the result of the yield (a draft object) to the caller
|
63
|
+
return yield_return
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get the current +DraftTransaction+ for this thread / fiber, or raise an
|
68
|
+
# error if there is no current +DraftTransaction+
|
69
|
+
#
|
70
|
+
# @return [DraftTransaction]
|
71
|
+
def self.current_draft_transaction!
|
72
|
+
raise DraftApprove::Errors::NoDraftTransactionError unless current_draft_transaction.present?
|
73
|
+
|
74
|
+
current_draft_transaction
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Helper to create a new transaction and return both it and the result of
|
80
|
+
# yielding to the given block
|
81
|
+
def self.in_new_draft_transaction_helper(created_by: nil, extra_data: nil)
|
82
|
+
raise DraftApprove::Errors::NestedDraftTransactionError if current_draft_transaction.present?
|
83
|
+
draft_transaction, yield_return = nil
|
84
|
+
|
85
|
+
ActiveRecord::Base.transaction do
|
86
|
+
begin
|
87
|
+
draft_transaction = DraftTransaction.create!(
|
88
|
+
status: DraftTransaction::PENDING_APPROVAL,
|
89
|
+
created_by: created_by,
|
90
|
+
serialization: serialization_module,
|
91
|
+
extra_data: extra_data
|
92
|
+
)
|
93
|
+
self.current_draft_transaction = draft_transaction
|
94
|
+
yield_return = yield
|
95
|
+
|
96
|
+
# If no drafts exist at this point, this is a no-op Draft Transaction,
|
97
|
+
# so no point storing it - destroy it.
|
98
|
+
# NOTE: We don't rollback the transaction here, because non-draft
|
99
|
+
# changes may have occurred inside the yield block!
|
100
|
+
if draft_transaction.drafts.empty?
|
101
|
+
draft_transaction.destroy!
|
102
|
+
draft_transaction = nil
|
103
|
+
end
|
104
|
+
ensure
|
105
|
+
self.current_draft_transaction = nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return draft_transaction, yield_return
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.serialization_module
|
113
|
+
# TODO: Factor this out into a config setting or something...
|
114
|
+
DraftApprove::Serialization::Json
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.current_draft_transaction
|
118
|
+
Thread.current[:draft_approve_transaction]
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.current_draft_transaction=(draft_transaction)
|
122
|
+
Thread.current[:draft_approve_transaction] = draft_transaction
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module DraftApprove
|
5
|
+
# @api private
|
6
|
+
module Generators
|
7
|
+
class MigrationGenerator < Rails::Generators::Base
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
source_root File.expand_path('../templates', __FILE__)
|
10
|
+
|
11
|
+
desc 'Generates the migrations for DraftApprove'
|
12
|
+
|
13
|
+
def create_migration_file
|
14
|
+
migration_template(
|
15
|
+
'create_draft_approve_tables.rb',
|
16
|
+
'db/migrate/create_draft_approve_tables.rb',
|
17
|
+
{ migration_version: migration_version, json_type: json_type }
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.next_migration_number(path)
|
22
|
+
next_migration_number = current_migration_number(path) + 1
|
23
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def migration_version
|
29
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
30
|
+
end
|
31
|
+
|
32
|
+
def json_type
|
33
|
+
if [:postgresql, :postgis].include? ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
34
|
+
'jsonb'
|
35
|
+
else
|
36
|
+
'json'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class CreateDraftApproveTables < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :draft_transactions, comment: 'Table linking multiple drafts to be applied in sequence, within a transaction' do |t|
|
4
|
+
t.string :status, null: false, index: true, comment: 'The status of the drafts within this transaction (pending approval, approved, rejected, errored)'
|
5
|
+
t.string :created_by, null: true, index: true, comment: 'The user or process which created the drafts in this transaction'
|
6
|
+
t.string :reviewed_by, null: true, index: true, comment: 'The user who approved or rejected the drafts in this transaction'
|
7
|
+
t.string :review_reason, null: true, index: false, comment: 'The reason given by the user for approving or rejecting the drafts in this transaction'
|
8
|
+
t.string :error, null: true, index: false, comment: 'If there was an error while approving this transaction, more information on the error that occurred'
|
9
|
+
t.string :serialization, null: false, index: false, comment: 'The serialization module used for all drafts within this transaction'
|
10
|
+
t.<%= json_type %> :extra_data, null: true, index: false, comment: 'Any extra data associated with this draft transaction, eg. users / roles who are authorised to approve the changes'
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :drafts, comment: 'Drafts of changes to be approved' do |t|
|
16
|
+
t.references :draft_transaction, null: false, index: true, foreign_key: true
|
17
|
+
t.references :draftable, null: true, index: true, polymorphic: true
|
18
|
+
t.string :draft_action_type, null: false
|
19
|
+
t.<%= json_type %> :draft_changes, null: false
|
20
|
+
t.<%= json_type %> :draft_options, null: true
|
21
|
+
|
22
|
+
t.timestamps
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: draft_approve
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Sibley
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-02-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.17'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.17'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.7'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.7'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pg
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.18'
|
104
|
+
- - "<"
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '2.0'
|
107
|
+
type: :development
|
108
|
+
prerelease: false
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0.18'
|
114
|
+
- - "<"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: factory_bot
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '4.11'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '4.11'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: codecov
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0.1'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0.1'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: appraisal
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '2.2'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '2.2'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: pry
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0.12'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - "~>"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0.12'
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: yard
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - "~>"
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: 0.9.18
|
180
|
+
type: :development
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - "~>"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: 0.9.18
|
187
|
+
description: "\n All draft data is saved in a separate table, so no need to worry
|
188
|
+
about\n existing code / SQL accidentally finding non-approved data. Supports
|
189
|
+
draft\n changes to existing objects, and creating new objects as drafts. Supports\n
|
190
|
+
\ 'draft transactions' which may update / create many objects, and must be\n approved
|
191
|
+
/ rejected in their entirety.\n "
|
192
|
+
email:
|
193
|
+
- andrew.s@38degrees.org.uk
|
194
|
+
executables: []
|
195
|
+
extensions: []
|
196
|
+
extra_rdoc_files: []
|
197
|
+
files:
|
198
|
+
- ".gitignore"
|
199
|
+
- ".rspec"
|
200
|
+
- ".travis.yml"
|
201
|
+
- ".yardopts"
|
202
|
+
- Appraisals
|
203
|
+
- CODE_OF_CONDUCT.md
|
204
|
+
- Gemfile
|
205
|
+
- Gemfile.lock
|
206
|
+
- LICENSE.md
|
207
|
+
- README.md
|
208
|
+
- Rakefile
|
209
|
+
- bin/console
|
210
|
+
- bin/setup
|
211
|
+
- draft_approve.gemspec
|
212
|
+
- lib/draft_approve.rb
|
213
|
+
- lib/draft_approve/draft_changes_proxy.rb
|
214
|
+
- lib/draft_approve/draftable/base_class_methods.rb
|
215
|
+
- lib/draft_approve/draftable/class_methods.rb
|
216
|
+
- lib/draft_approve/draftable/instance_methods.rb
|
217
|
+
- lib/draft_approve/errors.rb
|
218
|
+
- lib/draft_approve/models/draft.rb
|
219
|
+
- lib/draft_approve/models/draft_transaction.rb
|
220
|
+
- lib/draft_approve/persistor.rb
|
221
|
+
- lib/draft_approve/serialization/json.rb
|
222
|
+
- lib/draft_approve/serialization/json/constants.rb
|
223
|
+
- lib/draft_approve/serialization/json/draft_changes_proxy.rb
|
224
|
+
- lib/draft_approve/serialization/json/serializer.rb
|
225
|
+
- lib/draft_approve/transaction.rb
|
226
|
+
- lib/draft_approve/version.rb
|
227
|
+
- lib/generators/draft_approve/migration/migration_generator.rb
|
228
|
+
- lib/generators/draft_approve/migration/templates/create_draft_approve_tables.rb
|
229
|
+
homepage: https://github.com/38dgs/draft_approve
|
230
|
+
licenses:
|
231
|
+
- MIT
|
232
|
+
metadata: {}
|
233
|
+
post_install_message:
|
234
|
+
rdoc_options: []
|
235
|
+
require_paths:
|
236
|
+
- lib
|
237
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
238
|
+
requirements:
|
239
|
+
- - ">="
|
240
|
+
- !ruby/object:Gem::Version
|
241
|
+
version: '0'
|
242
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
243
|
+
requirements:
|
244
|
+
- - ">="
|
245
|
+
- !ruby/object:Gem::Version
|
246
|
+
version: '0'
|
247
|
+
requirements: []
|
248
|
+
rubyforge_project:
|
249
|
+
rubygems_version: 2.7.8
|
250
|
+
signing_key:
|
251
|
+
specification_version: 4
|
252
|
+
summary: Save drafts of ActiveRecord models & approve them to apply the changes.
|
253
|
+
test_files: []
|