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.
@@ -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: []