draft_approve 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module DraftApprove
2
+ VERSION = "0.1.0"
3
+ 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: []