paper_trail-association_tracking 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/LICENSE +20 -0
  4. data/README.md +252 -0
  5. data/Rakefile +61 -0
  6. data/lib/generators/paper_trail_association_tracking/install_generator.rb +64 -0
  7. data/lib/generators/paper_trail_association_tracking/templates/add_transaction_id_column_to_versions.rb.erb +13 -0
  8. data/lib/generators/paper_trail_association_tracking/templates/create_version_associations.rb.erb +22 -0
  9. data/lib/paper_trail-association_tracking.rb +68 -0
  10. data/lib/paper_trail_association_tracking/config.rb +26 -0
  11. data/lib/paper_trail_association_tracking/frameworks/active_record.rb +5 -0
  12. data/lib/paper_trail_association_tracking/frameworks/active_record/models/paper_trail/version_association.rb +13 -0
  13. data/lib/paper_trail_association_tracking/frameworks/rails.rb +3 -0
  14. data/lib/paper_trail_association_tracking/frameworks/rails/engine.rb +10 -0
  15. data/lib/paper_trail_association_tracking/frameworks/rspec.rb +20 -0
  16. data/lib/paper_trail_association_tracking/model_config.rb +76 -0
  17. data/lib/paper_trail_association_tracking/paper_trail.rb +38 -0
  18. data/lib/paper_trail_association_tracking/record_trail.rb +200 -0
  19. data/lib/paper_trail_association_tracking/reifier.rb +125 -0
  20. data/lib/paper_trail_association_tracking/reifiers/belongs_to.rb +50 -0
  21. data/lib/paper_trail_association_tracking/reifiers/has_and_belongs_to_many.rb +52 -0
  22. data/lib/paper_trail_association_tracking/reifiers/has_many.rb +112 -0
  23. data/lib/paper_trail_association_tracking/reifiers/has_many_through.rb +92 -0
  24. data/lib/paper_trail_association_tracking/reifiers/has_one.rb +135 -0
  25. data/lib/paper_trail_association_tracking/request.rb +32 -0
  26. data/lib/paper_trail_association_tracking/version.rb +5 -0
  27. data/lib/paper_trail_association_tracking/version_association_concern.rb +13 -0
  28. data/lib/paper_trail_association_tracking/version_concern.rb +37 -0
  29. metadata +260 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21bc61f6e8c1ce4cc16c937554bf13facad85e45cbc7323155f877841202ddc6
4
+ data.tar.gz: 24958dbdcf40ff4351b2aeec0f2b1303459d65fd92fb058fd3061f6fc7f88faa
5
+ SHA512:
6
+ metadata.gz: a03f3497443252136bf6a43389e6629cc693086cbcb0da9172896f83e1e52c747eb9c4461d27f8ff56090ec6a36b3927f7c98537db1d9b71f593cf9503540a5d
7
+ data.tar.gz: 5c10679e72f9b90ba90c92d5af0fc05a6272c3f37a651a1f6471f5adeddb120335ac2aa02eac62c457197a916005144a8788d375ed79da96635471cdb09fb214
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ This project follows [semver 2.0.0](http://semver.org/spec/v2.0.0.html) and the
4
+ recommendations of [keepachangelog.com](http://keepachangelog.com/).
5
+
6
+ ## 0.9.0
7
+
8
+ - [PaperTrail #1070](https://github.com/paper-trail-gem/paper_trail/issues/1070) - Extracted from paper_trail gem in v9.2 was released.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2018 Weston Ganger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # PaperTrail-AssociationTracking
2
+
3
+ [![Build Status][1]][2]
4
+
5
+ Plugin for the [PaperTrail](https://github.com/paper-trail-gem/paper_trail.git) gem to track and reify associations.
6
+
7
+ **PR's will happily be accepted**
8
+
9
+ This gem was extracted from PaperTrail in v9.2 to simplify things in PaperTrail and association tracking seperately.
10
+ At this time, `paper_trail` has a runtime dependency on this gem and will keep running the existing tests related
11
+ to association tracking. This arrangement will be maintained for a few years, if practical.
12
+
13
+ A little history lesson, discussed as early as 2009, and first implemented in late 2014, association
14
+ tracking was part of PT core until 2018 as an experimental feature and was use at your own risk. This gem now
15
+ maintains a list of known issues and we hope the community can help remove some of them via PR's.
16
+
17
+ ## TODO
18
+
19
+ - Continue removing most-non association specs
20
+ - Add consolidated list of paper trail plugins to paper_trail core readme
21
+
22
+ ## Table of Contents
23
+
24
+ <!-- toc -->
25
+
26
+ - [Install](#install)
27
+ - [Associations](#associations)
28
+ - [Known Issues](#known-issues)
29
+ - [Contributing](#contributing)
30
+ - [Credits](#credits)
31
+
32
+ <!-- tocstop -->
33
+
34
+ # Install
35
+
36
+ ```ruby
37
+ # Gemfile
38
+
39
+ gem 'paper_trail' # Requires v9.2+
40
+ gem 'paper_trail-association_tracking'
41
+ ```
42
+
43
+ # Association Tracking
44
+
45
+ This plugin currently can restore three types of associations: Has-One, Has-Many, and
46
+ Has-Many-Through. In order to do this, you will need to do two things:
47
+
48
+ 1. Create a `version_associations` table
49
+ 2. Set `PaperTrail.config.track_associations = true` (e.g. in an initializer)
50
+
51
+ Both will be done for you automatically if you install PaperTrail with the
52
+ `--with_associations` option
53
+ (e.g. `rails generate paper_trail:install --with-associations`)
54
+
55
+ If you want to add this functionality after the initial installation, you will
56
+ need to create the `version_associations` table manually, and you will need to
57
+ ensure that `PaperTrail.config.track_associations = true` is set.
58
+
59
+ PaperTrail will store in the `version_associations` table additional information
60
+ to correlate versions of the association and versions of the model when the
61
+ associated record is changed. When reifying the model, PaperTrail can use this
62
+ table, together with the `transaction_id` to find the correct version of the
63
+ association and reify it. The `transaction_id` is a unique id for version records
64
+ created in the same transaction. It is used to associate the version of the model
65
+ and the version of the association that are created in the same transaction.
66
+
67
+ To restore Has-One associations as they were at the time, pass option `has_one:
68
+ true` to `reify`. To restore Has-Many and Has-Many-Through associations, use
69
+ option `has_many: true`. To restore Belongs-To association, use
70
+ option `belongs_to: true`. For example:
71
+
72
+ ```ruby
73
+ class Location < ActiveRecord::Base
74
+ belongs_to :treasure
75
+ has_paper_trail
76
+ end
77
+
78
+ class Treasure < ActiveRecord::Base
79
+ has_one :location
80
+ has_paper_trail
81
+ end
82
+
83
+ treasure.amount # 100
84
+ treasure.location.latitude # 12.345
85
+
86
+ treasure.update_attributes amount: 153
87
+ treasure.location.update_attributes latitude: 54.321
88
+
89
+ t = treasure.versions.last.reify(has_one: true)
90
+ t.amount # 100
91
+ t.location.latitude # 12.345
92
+ ```
93
+
94
+ If the parent and child are updated in one go, PaperTrail-AssociationTracking can use the
95
+ aforementioned `transaction_id` to reify the models as they were before the
96
+ transaction (instead of before the update to the model).
97
+
98
+ ```ruby
99
+ treasure.amount # 100
100
+ treasure.location.latitude # 12.345
101
+
102
+ Treasure.transaction do
103
+ treasure.location.update_attributes latitude: 54.321
104
+ treasure.update_attributes amount: 153
105
+ end
106
+
107
+ t = treasure.versions.last.reify(has_one: true)
108
+ t.amount # 100
109
+ t.location.latitude # 12.345, instead of 54.321
110
+ ```
111
+
112
+ By default, PaperTrail-AssociationTracking excludes an associated record from the reified parent
113
+ model if the associated record exists in the live model but did not exist as at
114
+ the time the version was created. This is usually what you want if you just want
115
+ to look at the reified version. But if you want to persist it, it would be
116
+ better to pass in option `mark_for_destruction: true` so that the associated
117
+ record is included and marked for destruction. Note that `mark_for_destruction`
118
+ only has [an effect on associations marked with `autosave: true`](http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html#method-i-mark_for_destruction).
119
+
120
+ ```ruby
121
+ class Widget < ActiveRecord::Base
122
+ has_paper_trail
123
+ has_one :wotsit, autosave: true
124
+ end
125
+
126
+ class Wotsit < ActiveRecord::Base
127
+ has_paper_trail
128
+ belongs_to :widget
129
+ end
130
+
131
+ widget = Widget.create(name: 'widget_0')
132
+ widget.update_attributes(name: 'widget_1')
133
+ widget.create_wotsit(name: 'wotsit')
134
+
135
+ widget_0 = widget.versions.last.reify(has_one: true)
136
+ widget_0.wotsit # nil
137
+
138
+ widget_0 = widget.versions.last.reify(has_one: true, mark_for_destruction: true)
139
+ widget_0.wotsit.marked_for_destruction? # true
140
+ widget_0.save!
141
+ widget.reload.wotsit # nil
142
+ ```
143
+
144
+ # Known Issues
145
+
146
+ Associations have the following known issues, in order of descending importance. Use in Production at your own risk.
147
+
148
+ **PR's for these issues will happily be accepted**
149
+
150
+ If you notice anything here that should be updated/removed/edited feel free to create an issue.
151
+
152
+ 1. PaperTrail-AssociationTracking only reifies the first level of associations.
153
+ 1. Sometimes the has_one association will find more than one possible candidate and will raise a `PaperTrailAssociationTracking::Reifiers::HasOne::FoundMoreThanOne` error. For example, see `spec/models/person_spec.rb`
154
+ - If you are not using STI, you may want to just assume the first result (of multiple) is the correct one and continue. PaperTrail <= v8 did this without error or warning. To do so add the following line to your initializer: `PaperTrail.config.association_reify_error_behaviour = :warn`. Valid options are: `[:error, :warn, :ignore]`
155
+ - When using STI, even if you enable `:warn` you will likely still end up recieving an `ActiveRecord::AssociationTypeMismatch` error.
156
+ 1. Not compatible with [transactional tests](https://github.com/rails/rails/blob/591a0bb87fff7583e01156696fbbf929d48d3e54/activerecord/lib/active_record/fixtures.rb#L142), aka. transactional fixtures. - [PT Issue #542](https://github.com/airblade/paper_trail/issues/542)
157
+ 1. Requires database timestamp columns with fractional second precision.
158
+ - Sqlite and postgres timestamps have fractional second precision by default.
159
+ [MySQL timestamps do not](https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html). Furthermore, MySQL 5.5 and earlier do not
160
+ support fractional second precision at all.
161
+ - Also, support for fractional seconds in MySQL was not added to
162
+ rails until ActiveRecord 4.2 (https://github.com/rails/rails/pull/14359).
163
+ 1. PaperTrail-AssociationTracking can't restore an association properly if the association record
164
+ can be updated to replace its parent model (by replacing the foreign key)
165
+ 1. Currently PaperTrail-AssociationTracking only supports a single `version_associations` table.
166
+ Therefore, you can only use a single table to store the versions for
167
+ all related models. Sorry for those who use multiple version tables.
168
+ 1. PaperTrail-AssociationTracking relies on the callbacks on the association model (and the :through
169
+ association model for Has-Many-Through associations) to record the versions
170
+ and the relationship between the versions. If the association is changed
171
+ without invoking the callbacks, Reification won't work. Below are some
172
+ examples:
173
+
174
+ Given these models:
175
+
176
+ ```ruby
177
+ class Book < ActiveRecord::Base
178
+ has_many :authorships, dependent: :destroy
179
+ has_many :authors, through: :authorships, source: :person
180
+ has_paper_trail
181
+ end
182
+
183
+ class Authorship < ActiveRecord::Base
184
+ belongs_to :book
185
+ belongs_to :person
186
+ has_paper_trail # NOTE
187
+ end
188
+
189
+ class Person < ActiveRecord::Base
190
+ has_many :authorships, dependent: :destroy
191
+ has_many :books, through: :authorships
192
+ has_paper_trail
193
+ end
194
+ ```
195
+
196
+ Then each of the following will store authorship versions:
197
+
198
+ ```ruby
199
+ @book.authors << @dostoyevsky
200
+ @book.authors.create name: 'Tolstoy'
201
+ @book.authorships.last.destroy
202
+ @book.authorships.clear
203
+ @book.author_ids = [@solzhenistyn.id, @dostoyevsky.id]
204
+ ```
205
+
206
+ But none of these will:
207
+
208
+ ```ruby
209
+ @book.authors.delete @tolstoy
210
+ @book.author_ids = []
211
+ @book.authors = []
212
+ ```
213
+
214
+ Having said that, you can apparently get all these working (I haven't tested it
215
+ myself) with this patch:
216
+
217
+ ```ruby
218
+ # config/initializers/active_record_patch.rb
219
+
220
+ class HasManyThroughAssociationPatch
221
+ def delete_records(records, method)
222
+ method ||= :destroy
223
+ super
224
+ end
225
+ end
226
+
227
+ ActiveRecord::Associations::HasManyThroughAssociation.prepend(HasManyThroughAssociationPatch)
228
+ ```
229
+
230
+ See [PT Issue #113](https://github.com/paper-trail-gem/paper_trail/issues/113) for a discussion about this.
231
+
232
+
233
+ ### Regarding ActiveRecord Single Table Inheritance (STI)
234
+
235
+ At this time during `reify` any STI `has_one` associations will raise a `PaperTrailAssociationTracking::Reifiers::HasOne::FoundMoreThanOne` error. See [PT Issue #594](https://github.com/airblade/paper_trail/issues/594)
236
+
237
+ Something to note though, is while the PaperTrail gem supports [Single Table Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance), I do NOT recommend STI ever. Your better off rolling your own solution rather than using STI.
238
+
239
+ # Contributing
240
+
241
+ See the paper_trail [contribution guidelines](https://github.com/paper-trail-gem/paper_trail/blob/master/.github/CONTRIBUTING.md)
242
+
243
+ # Credits
244
+
245
+ Plugin authored by [Weston Ganger](https://github.com/westonganger) & Jared Beck
246
+
247
+ Maintained by [Weston Ganger](https://github.com/westonganger) & [Jared Beck](https://github.com/jaredbeck)
248
+
249
+ Associations code originally contributed by Ben Atkins, Jared Beck, Andy Stewart & more
250
+
251
+ [1]: https://api.travis-ci.org/westonganger/paper_trail-association_tracking.svg?branch=master
252
+ [2]: https://travis-ci.org/westonganger/paper_trail-association_tracking
data/Rakefile ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ desc "Set a relevant database.yml for testing"
7
+ task :prepare do
8
+ ENV["DB"] ||= "sqlite"
9
+ FileUtils.cp(
10
+ "spec/dummy_app/config/database.#{ENV['DB']}.yml",
11
+ "spec/dummy_app/config/database.yml"
12
+ )
13
+ end
14
+
15
+ require "rake/testtask"
16
+ desc "Run tests on PaperTrail with Test::Unit."
17
+ Rake::TestTask.new(:test) do |t|
18
+ t.libs << "lib"
19
+ t.libs << "test"
20
+ t.pattern = "test/**/*_test.rb"
21
+ t.verbose = false
22
+
23
+ # Enabling ruby interpreter warnings (-w) is, sadly, impractical. There are
24
+ # too many noisy warnings that we have no control over, e.g. caused by libs we
25
+ # depend on.
26
+ t.warning = false
27
+ end
28
+
29
+ require "rspec/core/rake_task"
30
+ desc "Run tests on PaperTrail with RSpec"
31
+ task(:spec).clear
32
+ RSpec::Core::RakeTask.new(:spec) do |t|
33
+ t.verbose = false # hide list of specs bit.ly/1nVq3Jn
34
+ end
35
+
36
+ task :autocorrect do
37
+ rules = [
38
+ 'FrozenStringLiteralComment',
39
+ 'Layout/EmptyLineAfterMagicComment',
40
+ 'Layout/EmptyLinesAroundBlockBody',
41
+ 'Layout/EmptyLinesAroundClassBody',
42
+ 'Layout/EmptyLinesAroundMethodBody',
43
+ 'Layout/EmptyLinesAroundModuleBody',
44
+ 'Layout/TrailingWhitespace',
45
+ 'Style/EmptyMethod',
46
+ 'Style/TrailingCommaInArguments',
47
+ ]
48
+
49
+ rules.each do |rule|
50
+ `bundle exec rubocop --auto-correct --only #{rule}`
51
+ end
52
+
53
+ Rake::Task['rubocop'].invoke
54
+ end
55
+
56
+ require "rubocop/rake_task"
57
+ RuboCop::RakeTask.new
58
+
59
+ ### TODO: Allow rubocop to fail, but still continue
60
+ desc "Default: run all available test suites"
61
+ task default: %i[prepare test spec]
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module PaperTrailAssociationTracking
7
+ # Installs PaperTrail in a rails app.
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ include ::Rails::Generators::Migration
10
+
11
+ # Class names of MySQL adapters.
12
+ # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
13
+ # - `Mysql2Adapter` - Used by `mysql2` gem.
14
+ MYSQL_ADAPTERS = [
15
+ "ActiveRecord::ConnectionAdapters::MysqlAdapter",
16
+ "ActiveRecord::ConnectionAdapters::Mysql2Adapter"
17
+ ].freeze
18
+
19
+ source_root File.expand_path("../templates", __FILE__)
20
+
21
+ desc "Generates (but does not run) a migration to add a versions table."
22
+
23
+ def create_migrations
24
+ add_paper_trail_migration("create_version_associations")
25
+ add_paper_trail_migration("add_transaction_id_column_to_versions")
26
+ end
27
+
28
+ def create_initializer
29
+ create_file(
30
+ "config/initializers/paper_trail.rb",
31
+ "PaperTrail.config.track_associations = #{!!options.with_associations?}\n",
32
+ "PaperTrail.config.association_reify_error_behaviour = :error"
33
+ )
34
+ end
35
+
36
+ def self.next_migration_number(dirname)
37
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
38
+ end
39
+
40
+ protected
41
+
42
+ def add_paper_trail_migration(template)
43
+ migration_dir = File.expand_path("db/migrate")
44
+ if self.class.migration_exists?(migration_dir, template)
45
+ ::Kernel.warn "Migration already exists: #{template}"
46
+ else
47
+ migration_template(
48
+ "#{template}.rb.erb",
49
+ "db/migrate/#{template}.rb",
50
+ migration_version: migration_version
51
+ )
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def migration_version
58
+ major = ActiveRecord::VERSION::MAJOR
59
+ if major >= 5
60
+ "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,13 @@
1
+ # This migration and CreateVersionAssociations provide the necessary
2
+ # schema for tracking associations.
3
+ class AddTransactionIdColumnToVersions < ActiveRecord::Migration<%= migration_version %>
4
+ def self.up
5
+ add_column :versions, :transaction_id, :integer
6
+ add_index :versions, [:transaction_id]
7
+ end
8
+
9
+ def self.down
10
+ remove_index :versions, [:transaction_id]
11
+ remove_column :versions, :transaction_id
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # This migration and AddTransactionIdColumnToVersions provide the necessary
2
+ # schema for tracking associations.
3
+ class CreateVersionAssociations < ActiveRecord::Migration<%= migration_version %>
4
+ def self.up
5
+ create_table :version_associations do |t|
6
+ t.integer :version_id
7
+ t.string :foreign_key_name, null: false
8
+ t.integer :foreign_key_id
9
+ end
10
+ add_index :version_associations, [:version_id]
11
+ add_index :version_associations,
12
+ %i(foreign_key_name foreign_key_id),
13
+ name: "index_version_associations_on_foreign_key"
14
+ end
15
+
16
+ def self.down
17
+ remove_index :version_associations, [:version_id]
18
+ remove_index :version_associations,
19
+ name: "index_version_associations_on_foreign_key"
20
+ drop_table :version_associations
21
+ end
22
+ end