paper_trail-association_tracking 0.0.1

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