paper_trail 7.1.0 → 7.1.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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/lib/paper_trail/record_trail.rb +1 -1
  3. data/lib/paper_trail/version_number.rb +1 -1
  4. metadata +3 -274
  5. data/.github/CONTRIBUTING.md +0 -151
  6. data/.github/ISSUE_TEMPLATE.md +0 -13
  7. data/.gitignore +0 -23
  8. data/.rspec +0 -2
  9. data/.rubocop.yml +0 -135
  10. data/.rubocop_todo.yml +0 -41
  11. data/.travis.yml +0 -41
  12. data/Appraisals +0 -26
  13. data/CHANGELOG.md +0 -739
  14. data/Gemfile +0 -2
  15. data/MIT-LICENSE +0 -20
  16. data/README.md +0 -1613
  17. data/Rakefile +0 -38
  18. data/doc/bug_report_template.rb +0 -73
  19. data/doc/triage.md +0 -27
  20. data/doc/warning_about_not_setting_whodunnit.md +0 -33
  21. data/gemfiles/ar_4.0.gemfile +0 -7
  22. data/gemfiles/ar_4.2.gemfile +0 -7
  23. data/gemfiles/ar_5.0.gemfile +0 -8
  24. data/gemfiles/ar_5.1.gemfile +0 -8
  25. data/gemfiles/ar_master.gemfile +0 -9
  26. data/lib/generators/paper_trail/default_initializer.rb +0 -0
  27. data/paper_trail.gemspec +0 -49
  28. data/spec/controllers/articles_controller_spec.rb +0 -28
  29. data/spec/controllers/widgets_controller_spec.rb +0 -85
  30. data/spec/dummy_app/Rakefile +0 -7
  31. data/spec/dummy_app/app/controllers/application_controller.rb +0 -30
  32. data/spec/dummy_app/app/controllers/articles_controller.rb +0 -16
  33. data/spec/dummy_app/app/controllers/test_controller.rb +0 -5
  34. data/spec/dummy_app/app/controllers/widgets_controller.rb +0 -28
  35. data/spec/dummy_app/app/models/animal.rb +0 -4
  36. data/spec/dummy_app/app/models/article.rb +0 -25
  37. data/spec/dummy_app/app/models/authorship.rb +0 -5
  38. data/spec/dummy_app/app/models/bar_habtm.rb +0 -4
  39. data/spec/dummy_app/app/models/book.rb +0 -9
  40. data/spec/dummy_app/app/models/boolit.rb +0 -4
  41. data/spec/dummy_app/app/models/callback_modifier.rb +0 -45
  42. data/spec/dummy_app/app/models/car.rb +0 -3
  43. data/spec/dummy_app/app/models/cat.rb +0 -2
  44. data/spec/dummy_app/app/models/chapter.rb +0 -9
  45. data/spec/dummy_app/app/models/citation.rb +0 -5
  46. data/spec/dummy_app/app/models/custom_primary_key_record.rb +0 -15
  47. data/spec/dummy_app/app/models/customer.rb +0 -4
  48. data/spec/dummy_app/app/models/document.rb +0 -8
  49. data/spec/dummy_app/app/models/dog.rb +0 -2
  50. data/spec/dummy_app/app/models/editor.rb +0 -4
  51. data/spec/dummy_app/app/models/editorship.rb +0 -5
  52. data/spec/dummy_app/app/models/elephant.rb +0 -3
  53. data/spec/dummy_app/app/models/fluxor.rb +0 -3
  54. data/spec/dummy_app/app/models/foo_habtm.rb +0 -5
  55. data/spec/dummy_app/app/models/foo_widget.rb +0 -2
  56. data/spec/dummy_app/app/models/fruit.rb +0 -5
  57. data/spec/dummy_app/app/models/gadget.rb +0 -3
  58. data/spec/dummy_app/app/models/kitchen/banana.rb +0 -5
  59. data/spec/dummy_app/app/models/legacy_widget.rb +0 -6
  60. data/spec/dummy_app/app/models/line_item.rb +0 -4
  61. data/spec/dummy_app/app/models/not_on_update.rb +0 -4
  62. data/spec/dummy_app/app/models/on/create.rb +0 -6
  63. data/spec/dummy_app/app/models/on/destroy.rb +0 -6
  64. data/spec/dummy_app/app/models/on/empty_array.rb +0 -6
  65. data/spec/dummy_app/app/models/on/update.rb +0 -6
  66. data/spec/dummy_app/app/models/order.rb +0 -5
  67. data/spec/dummy_app/app/models/paragraph.rb +0 -5
  68. data/spec/dummy_app/app/models/person.rb +0 -39
  69. data/spec/dummy_app/app/models/post.rb +0 -3
  70. data/spec/dummy_app/app/models/post_with_status.rb +0 -7
  71. data/spec/dummy_app/app/models/quotation.rb +0 -5
  72. data/spec/dummy_app/app/models/section.rb +0 -6
  73. data/spec/dummy_app/app/models/skipper.rb +0 -3
  74. data/spec/dummy_app/app/models/song.rb +0 -37
  75. data/spec/dummy_app/app/models/thing.rb +0 -3
  76. data/spec/dummy_app/app/models/translation.rb +0 -11
  77. data/spec/dummy_app/app/models/truck.rb +0 -4
  78. data/spec/dummy_app/app/models/vehicle.rb +0 -4
  79. data/spec/dummy_app/app/models/whatchamajigger.rb +0 -4
  80. data/spec/dummy_app/app/models/widget.rb +0 -8
  81. data/spec/dummy_app/app/models/wotsit.rb +0 -8
  82. data/spec/dummy_app/app/versions/custom_primary_key_record_version.rb +0 -3
  83. data/spec/dummy_app/app/versions/joined_version.rb +0 -6
  84. data/spec/dummy_app/app/versions/json_version.rb +0 -3
  85. data/spec/dummy_app/app/versions/kitchen/banana_version.rb +0 -5
  86. data/spec/dummy_app/app/versions/post_version.rb +0 -3
  87. data/spec/dummy_app/config.ru +0 -4
  88. data/spec/dummy_app/config/application.rb +0 -39
  89. data/spec/dummy_app/config/boot.rb +0 -24
  90. data/spec/dummy_app/config/database.mysql.yml +0 -19
  91. data/spec/dummy_app/config/database.postgres.yml +0 -15
  92. data/spec/dummy_app/config/database.sqlite.yml +0 -15
  93. data/spec/dummy_app/config/environment.rb +0 -5
  94. data/spec/dummy_app/config/environments/development.rb +0 -36
  95. data/spec/dummy_app/config/environments/production.rb +0 -74
  96. data/spec/dummy_app/config/environments/test.rb +0 -46
  97. data/spec/dummy_app/config/initializers/backtrace_silencers.rb +0 -9
  98. data/spec/dummy_app/config/initializers/inflections.rb +0 -10
  99. data/spec/dummy_app/config/initializers/mime_types.rb +0 -5
  100. data/spec/dummy_app/config/initializers/paper_trail.rb +0 -1
  101. data/spec/dummy_app/config/initializers/secret_token.rb +0 -9
  102. data/spec/dummy_app/config/initializers/session_store.rb +0 -8
  103. data/spec/dummy_app/config/locales/en.yml +0 -5
  104. data/spec/dummy_app/config/routes.rb +0 -4
  105. data/spec/dummy_app/db/migrate/20110208155312_set_up_test_tables.rb +0 -344
  106. data/spec/dummy_app/db/schema.rb +0 -298
  107. data/spec/generators/install_generator_spec.rb +0 -88
  108. data/spec/models/animal_spec.rb +0 -61
  109. data/spec/models/article_spec.rb +0 -186
  110. data/spec/models/boolit_spec.rb +0 -41
  111. data/spec/models/callback_modifier_spec.rb +0 -92
  112. data/spec/models/car_spec.rb +0 -13
  113. data/spec/models/custom_primary_key_record_spec.rb +0 -18
  114. data/spec/models/document_spec.rb +0 -57
  115. data/spec/models/gadget_spec.rb +0 -63
  116. data/spec/models/joined_version_spec.rb +0 -41
  117. data/spec/models/json_version_spec.rb +0 -101
  118. data/spec/models/kitchen/banana_spec.rb +0 -14
  119. data/spec/models/legacy_widget_spec.rb +0 -40
  120. data/spec/models/not_on_update_spec.rb +0 -22
  121. data/spec/models/on/create_spec.rb +0 -27
  122. data/spec/models/on/destroy_spec.rb +0 -27
  123. data/spec/models/on/empty_array_spec.rb +0 -30
  124. data/spec/models/on/update_spec.rb +0 -27
  125. data/spec/models/post_with_status_spec.rb +0 -46
  126. data/spec/models/skipper_spec.rb +0 -42
  127. data/spec/models/thing_spec.rb +0 -11
  128. data/spec/models/translation_spec.rb +0 -70
  129. data/spec/models/vehicle_spec.rb +0 -5
  130. data/spec/models/version_spec.rb +0 -282
  131. data/spec/models/widget_spec.rb +0 -338
  132. data/spec/modules/paper_trail_spec.rb +0 -27
  133. data/spec/modules/version_concern_spec.rb +0 -28
  134. data/spec/modules/version_number_spec.rb +0 -18
  135. data/spec/paper_trail/associations_spec.rb +0 -965
  136. data/spec/paper_trail/cleaner_spec.rb +0 -152
  137. data/spec/paper_trail/config_spec.rb +0 -45
  138. data/spec/paper_trail/model_spec.rb +0 -992
  139. data/spec/paper_trail/serializer_spec.rb +0 -85
  140. data/spec/paper_trail/serializers/custom_json_serializer_spec.rb +0 -18
  141. data/spec/paper_trail/serializers/custom_yaml_serializer_spec.rb +0 -45
  142. data/spec/paper_trail/serializers/json_spec.rb +0 -57
  143. data/spec/paper_trail/serializers/yaml_spec.rb +0 -42
  144. data/spec/paper_trail/thread_safety_spec.rb +0 -44
  145. data/spec/paper_trail/version_limit_spec.rb +0 -55
  146. data/spec/paper_trail/version_spec.rb +0 -96
  147. data/spec/paper_trail_spec.rb +0 -122
  148. data/spec/requests/articles_spec.rb +0 -34
  149. data/spec/spec_helper.rb +0 -78
  150. data/spec/support/alt_db_init.rb +0 -54
  151. data/spec/support/custom_json_serializer.rb +0 -13
data/Gemfile DELETED
@@ -1,2 +0,0 @@
1
- source "https://rubygems.org"
2
- gemspec
data/MIT-LICENSE DELETED
@@ -1,20 +0,0 @@
1
- Copyright (c) 2009 Andy Stewart, AirBlade Software Ltd.
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 DELETED
@@ -1,1613 +0,0 @@
1
- # PaperTrail
2
-
3
- [![Build Status][4]][5] [![Dependency Status][6]][7]
4
-
5
- Track changes to your models, for auditing or versioning. See how a model looked
6
- at any stage in its lifecycle, revert it to any version, or restore it after it
7
- has been destroyed.
8
-
9
- ## Documentation
10
-
11
- | Version | Documentation |
12
- | -------------- | ------------- |
13
- | Unreleased | https://github.com/airblade/paper_trail/blob/master/README.md |
14
- | 7.1.0 | https://github.com/airblade/paper_trail/blob/v7.1.0/README.md |
15
- | 6.0.2 | https://github.com/airblade/paper_trail/blob/v6.0.2/README.md |
16
- | 5.2.3 | https://github.com/airblade/paper_trail/blob/v5.2.3/README.md |
17
- | 4.2.0 | https://github.com/airblade/paper_trail/blob/v4.2.0/README.md |
18
- | 3.0.9 | https://github.com/airblade/paper_trail/blob/v3.0.9/README.md |
19
- | 2.7.2 | https://github.com/airblade/paper_trail/blob/v2.7.2/README.md |
20
- | 1.6.5 | https://github.com/airblade/paper_trail/blob/v1.6.5/README.md |
21
-
22
- ## Table of Contents
23
-
24
- - [1. Introduction](#1-introduction)
25
- - [1.a. Compatibility](#1a-compatibility)
26
- - [1.b. Installation](#1b-installation)
27
- - [1.c. Basic Usage](#1c-basic-usage)
28
- - [1.d. API Summary](#1d-api-summary)
29
- - [1.e. Configuration](#1e-configuration)
30
- - [2. Limiting What is Versioned, and When](#2-limiting-what-is-versioned-and-when)
31
- - [2.a. Choosing Lifecycle Events To Monitor](#2a-choosing-lifecycle-events-to-monitor)
32
- - [2.b. Choosing When To Save New Versions](#2b-choosing-when-to-save-new-versions)
33
- - [2.c. Choosing Attributes To Monitor](#2c-choosing-attributes-to-monitor)
34
- - [2.d. Turning PaperTrail Off](#2d-turning-papertrail-off)
35
- - [2.e. Limiting the Number of Versions Created](#2e-limiting-the-number-of-versions-created)
36
- - [3. Working With Versions](#3-working-with-versions)
37
- - [3.a. Reverting And Undeleting A Model](#3a-reverting-and-undeleting-a-model)
38
- - [3.b. Navigating Versions](#3b-navigating-versions)
39
- - [3.c. Diffing Versions](#3c-diffing-versions)
40
- - [3.d. Deleting Old Versions](#3d-deleting-old-versions)
41
- - [4. Saving More Information About Versions](#4-saving-more-information-about-versions)
42
- - [4.a. Finding Out Who Was Responsible For A Change](#4a-finding-out-who-was-responsible-for-a-change)
43
- - [4.b. Associations](#4b-associations)
44
- - [4.b.1. Known Issues](#4b1-known-issues)
45
- - [4.c. Storing metadata](#4c-storing-metadata)
46
- - [5. ActiveRecord](#5-activerecord)
47
- - [5.a. Single Table Inheritance](#5a-single-table-inheritance-sti)
48
- - [5.b. Configuring the `versions` Association](#5b-configuring-the-versions-association)
49
- - [5.c. Generators](#5c-generators)
50
- - [5.d. Protected Attributes](#5d-protected-attributes)
51
- - [6. Extensibility](#6-extensibility)
52
- - [6.a. Custom Version Classes](#6a-custom-version-classes)
53
- - [6.b. Custom Serializer](#6b-custom-serializer)
54
- - [7. Testing](#7-testing)
55
- - [7.a Minitest](#7a-minitest)
56
- - [7.b RSpec](#7b-rspec)
57
- - [7.c Cucumber](#7c-cucumber)
58
- - [7.d Spork](#7d-spork)
59
- - [7.e Zeus or Spring](#7e-zeus-or-spring)
60
- - [8. Integration with Other Libraries](#8-integration-with-other-libraries)
61
-
62
- ## 1. Introduction
63
-
64
- ### 1.a. Compatibility
65
-
66
- | paper_trail | branch | tags | ruby | activerecord |
67
- | -------------- | ---------- | ------ | -------- | ------------- |
68
- | unreleased | master | | >= 2.1.0 | >= 4.0, < 6 |
69
- | 7 | 7-stable | v7.x | >= 2.1.0 | >= 4.0, < 6 |
70
- | 6 | 6-stable | v6.x | >= 1.9.3 | >= 4.0, < 6 |
71
- | 5 | 5-stable | v5.x | >= 1.9.3 | >= 3.0, < 5.1 |
72
- | 4 | 4-stable | v4.x | >= 1.8.7 | >= 3.0, < 5.1 |
73
- | 3 | 3.0-stable | v3.x | >= 1.8.7 | >= 3.0, < 5 |
74
- | 2 | 2.7-stable | v2.x | >= 1.8.7 | >= 3.0, < 4 |
75
- | 1 | rails2 | v1.x | >= 1.8.7 | >= 2.3, < 3 |
76
-
77
- ### 1.b. Installation
78
-
79
- 1. Add PaperTrail to your `Gemfile`.
80
-
81
- `gem 'paper_trail'`
82
-
83
- 1. Add a `versions` table to your database and an initializer file for configuration:
84
-
85
- ```
86
- bundle exec rails generate paper_trail:install
87
- bundle exec rake db:migrate
88
- ```
89
-
90
- If using [rails_admin][38], you must enable the experimental
91
- [Associations](#4b-associations) feature. For more information on this
92
- generator, see [section 5.c. Generators](#5c-generators).
93
-
94
- 1. Add `has_paper_trail` to the models you want to track.
95
-
96
- ```ruby
97
- class Widget < ActiveRecord::Base
98
- has_paper_trail
99
- end
100
- ```
101
-
102
- 1. If your controllers have a `current_user` method, you can easily [track who
103
- is responsible for changes](#4a-finding-out-who-was-responsible-for-a-change)
104
- by adding a controller callback.
105
-
106
- ```ruby
107
- class ApplicationController
108
- before_action :set_paper_trail_whodunnit
109
- end
110
- ```
111
-
112
- ### 1.c. Basic Usage
113
-
114
- Your models now have a `versions` method which returns the "paper trail" of
115
- changes to your model.
116
-
117
- ```ruby
118
- widget = Widget.find 42
119
- widget.versions
120
- # [<PaperTrail::Version>, <PaperTrail::Version>, ...]
121
- ```
122
-
123
- Once you have a version, you can find out what happened:
124
-
125
- ```ruby
126
- v = widget.versions.last
127
- v.event # 'update', 'create', or 'destroy'
128
- v.created_at # When the `event` occurred
129
- v.whodunnit # If the update was via a controller and the
130
- # controller has a current_user method, returns the
131
- # id of the current user as a string.
132
- widget = v.reify # The widget as it was before the update
133
- # (nil for a create event)
134
- ```
135
-
136
- PaperTrail stores the pre-change version of the model, unlike some other
137
- auditing/versioning plugins, so you can retrieve the original version. This is
138
- useful when you start keeping a paper trail for models that already have records
139
- in the database.
140
-
141
- ```ruby
142
- widget = Widget.find 153
143
- widget.name # 'Doobly'
144
-
145
- # Add has_paper_trail to Widget model.
146
-
147
- widget.versions # []
148
- widget.update_attributes name: 'Wotsit'
149
- widget.versions.last.reify.name # 'Doobly'
150
- widget.versions.last.event # 'update'
151
- ```
152
-
153
- This also means that PaperTrail does not waste space storing a version of the
154
- object as it currently stands. The `versions` method gives you previous
155
- versions; to get the current one just call a finder on your `Widget` model as
156
- usual.
157
-
158
- Here's a helpful table showing what PaperTrail stores:
159
-
160
- | *Event* | *create* | *update* | *destroy* |
161
- | -------------- | -------- | -------- | --------- |
162
- | *Model Before* | nil | widget | widget |
163
- | *Model After* | widget | widget | nil |
164
-
165
- PaperTrail stores the values in the Model Before column. Most other
166
- auditing/versioning plugins store the After column.
167
-
168
- ### 1.d. API Summary
169
-
170
- When you declare `has_paper_trail` in your model, you get these methods:
171
-
172
- ```ruby
173
- class Widget < ActiveRecord::Base
174
- has_paper_trail
175
- end
176
-
177
- # Returns this widget's versions. You can customise the name of the
178
- # association, but overriding this method is not supported.
179
- widget.versions
180
-
181
- # Return the version this widget was reified from, or nil if it is live.
182
- # You can customise the name of the method.
183
- widget.version
184
-
185
- # Returns true if this widget is the current, live one; or false if it is from
186
- # a previous version.
187
- widget.paper_trail.live?
188
-
189
- # Returns who put the widget into its current state.
190
- widget.paper_trail.originator
191
-
192
- # Returns the widget (not a version) as it looked at the given timestamp.
193
- widget.paper_trail.version_at(timestamp)
194
-
195
- # Returns the widget (not a version) as it was most recently.
196
- widget.paper_trail.previous_version
197
-
198
- # Returns the widget (not a version) as it became next.
199
- widget.paper_trail.next_version
200
-
201
- # Generates a version for a `touch` event (`widget.touch` does NOT generate a
202
- # version)
203
- widget.paper_trail.touch_with_version
204
-
205
- # Turn PaperTrail off for all widgets.
206
- Widget.paper_trail.disable
207
-
208
- # Turn PaperTrail on for all widgets.
209
- Widget.paper_trail.enable
210
-
211
- # Is PaperTrail enabled for Widget, the class?
212
- Widget.paper_trail.enabled?
213
-
214
- # Is PaperTrail enabled for widget, the instance?
215
- widget.paper_trail.enabled_for_model?
216
- ```
217
-
218
- And a `PaperTrail::Version` instance (which is just an ordinary ActiveRecord
219
- instance, with all the usual methods) adds these methods:
220
-
221
- ```ruby
222
- # Returns the item restored from this version.
223
- version.reify(options = {})
224
-
225
- # Return a new item from this version
226
- version.reify(dup: true)
227
-
228
- # Returns who put the item into the state stored in this version.
229
- version.paper_trail_originator
230
-
231
- # Returns who changed the item from the state it had in this version.
232
- version.terminator
233
- version.whodunnit
234
- version.version_author
235
-
236
- # Returns the next version.
237
- version.next
238
-
239
- # Returns the previous version.
240
- version.previous
241
-
242
- # Returns the index of this version in all the versions.
243
- version.index
244
-
245
- # Returns the event that caused this version (create|update|destroy).
246
- version.event
247
-
248
- # Query the `versions.object` column (or `object_changes` column), by
249
- # attributes, using the SQL LIKE operator. Known issue: inconsistent results for
250
- # numeric values due to limitations of SQL wildcard matchers against the
251
- # serialized objects.
252
- PaperTrail::Version.where_object(attr1: val1, attr2: val2)
253
- PaperTrail::Version.where_object_changes(attr1: val1)
254
- ```
255
-
256
- In your controllers you can override these methods:
257
-
258
- ```ruby
259
- # Returns the user who is responsible for any changes that occur.
260
- # Defaults to current_user.
261
- user_for_paper_trail
262
-
263
- # Returns any information about the controller or request that you want
264
- # PaperTrail to store alongside any changes that occur.
265
- info_for_paper_trail
266
- ```
267
-
268
- ### 1.e. Configuration
269
-
270
- Many aspects of PaperTrail are configurable for individual models; typically
271
- this is achieved by passing options to the `has_paper_trail` method within
272
- a given model.
273
-
274
- Some aspects of PaperTrail are configured globally for all models. These
275
- settings are assigned directly on the `PaperTrail.config` object.
276
- A common place to put these settings is in a Rails initializer file
277
- such as `config/initializers/paper_trail.rb` or in an environment-specific
278
- configuration file such as `config/environments/test.rb`.
279
-
280
- ## 2. Limiting What is Versioned, and When
281
-
282
- ### 2.a. Choosing Lifecycle Events To Monitor
283
-
284
- You can choose which events to track with the `on` option. For example, to
285
- ignore `create` events:
286
-
287
- ```ruby
288
- class Article < ActiveRecord::Base
289
- has_paper_trail on: [:update, :destroy]
290
- end
291
- ```
292
-
293
- `has_paper_trail` installs callbacks for these lifecycle events. If there are
294
- other callbacks in your model, their order relative to those installed by
295
- PaperTrail may matter, so be aware of any potential interactions.
296
-
297
- You may also have the `PaperTrail::Version` model save a custom string in its
298
- `event` field instead of the typical `create`, `update`, `destroy`. PaperTrail
299
- supplies a custom accessor method called `paper_trail_event`, which it will
300
- attempt to use to fill the `event` field before falling back on one of the
301
- default events.
302
-
303
- ```ruby
304
- a = Article.create
305
- a.versions.size # 1
306
- a.versions.last.event # 'create'
307
- a.paper_trail_event = 'update title'
308
- a.update_attributes title: 'My Title'
309
- a.versions.size # 2
310
- a.versions.last.event # 'update title'
311
- a.paper_trail_event = nil
312
- a.update_attributes title: 'Alternate'
313
- a.versions.size # 3
314
- a.versions.last.event # 'update'
315
- ```
316
-
317
- #### Controlling the Order of AR Callbacks
318
-
319
- The `has_paper_trail` method installs AR callbacks. If you need to control
320
- their order, use the `paper_trail_on_*` methods.
321
-
322
- ```ruby
323
- class Article < ActiveRecord::Base
324
-
325
- # Include PaperTrail, but do not add any callbacks yet. Passing the
326
- # empty array to `:on` omits callbacks.
327
- has_paper_trail on: []
328
-
329
- # Add callbacks in the order you need.
330
- paper_trail.on_destroy # add destroy callback
331
- paper_trail.on_update # etc.
332
- paper_trail.on_create
333
- end
334
- ```
335
-
336
- The `paper_trail.on_destroy` method can be further configured to happen
337
- `:before` or `:after` the destroy event. In PaperTrail 4, the default is
338
- `:after`. In PaperTrail 5, the default will be `:before`, to support
339
- ActiveRecord 5. (see https://github.com/airblade/paper_trail/pull/683)
340
-
341
- ### 2.b. Choosing When To Save New Versions
342
-
343
- You can choose the conditions when to add new versions with the `if` and
344
- `unless` options. For example, to save versions only for US non-draft
345
- translations:
346
-
347
- ```ruby
348
- class Translation < ActiveRecord::Base
349
- has_paper_trail if: Proc.new { |t| t.language_code == 'US' },
350
- unless: Proc.new { |t| t.type == 'DRAFT' }
351
- end
352
- ```
353
-
354
- #### Choosing Based on Changed Attributes
355
-
356
- Starting with PaperTrail 4.0, versions are saved during an after-callback. If
357
- you decide whether to save a new version based on changed attributes, please
358
- use attribute_name_was instead of attribute_name.
359
-
360
- ### 2.c. Choosing Attributes To Monitor
361
-
362
- #### Ignore
363
-
364
- You can `ignore` changes to certain attributes:
365
-
366
- ```ruby
367
- class Article < ActiveRecord::Base
368
- has_paper_trail ignore: [:title, :rating]
369
- end
370
- ```
371
-
372
- Changes to just the `title` or `rating` will not create a version record.
373
- Changes to other attributes will create a version record.
374
-
375
- ```ruby
376
- a = Article.create
377
- a.versions.length # 1
378
- a.update_attributes title: 'My Title', rating: 3
379
- a.versions.length # 1
380
- a.update_attributes title: 'Greeting', content: 'Hello'
381
- a.versions.length # 2
382
- a.paper_trail.previous_version.title # 'My Title'
383
- ```
384
-
385
- #### Only
386
-
387
- Or, you can specify a list of the `only` attributes you care about:
388
-
389
- ```ruby
390
- class Article < ActiveRecord::Base
391
- has_paper_trail only: [:title]
392
- end
393
- ```
394
-
395
- Only changes to the `title` will create a version record.
396
-
397
- ```ruby
398
- a = Article.create
399
- a.versions.length # 1
400
- a.update_attributes title: 'My Title'
401
- a.versions.length # 2
402
- a.update_attributes content: 'Hello'
403
- a.versions.length # 2
404
- a.paper_trail.previous_version.content # nil
405
- ```
406
-
407
- The `:ignore` and `:only` options can also accept `Hash` arguments.
408
-
409
- ```ruby
410
- class Article < ActiveRecord::Base
411
- has_paper_trail only: { title: Proc.new { |obj| !obj.title.blank? } }
412
- end
413
- ```
414
-
415
- If the `title` is not blank, then only changes to the `title`
416
- will create a version record.
417
-
418
- ```ruby
419
- a = Article.create
420
- a.versions.length # 1
421
- a.update_attributes content: 'Hello'
422
- a.versions.length # 2
423
- a.update_attributes title: 'Title One'
424
- a.versions.length # 3
425
- a.update_attributes content: 'Hai'
426
- a.versions.length # 3
427
- a.paper_trail.previous_version.content # "Hello"
428
- a.update_attributes title: 'Title Two'
429
- a.versions.length # 4
430
- a.paper_trail.previous_version.content # "Hai"
431
- ```
432
-
433
- Configuring both `:ignore` and `:only` is not recommended, but it should work as
434
- expected. Passing both `:ignore` and `:only` options will result in the
435
- article being saved if a changed attribute is included in `:only` but not in
436
- `:ignore`.
437
-
438
- #### Skip
439
-
440
- You can skip attributes completely with the `:skip` option. As with `:ignore`,
441
- updates to these attributes will not create a version record. In addition, if a
442
- version record is created for some other reason, these attributes will not be
443
- persisted.
444
-
445
- ```ruby
446
- class Article < ActiveRecord::Base
447
- has_paper_trail skip: [:file_upload]
448
- end
449
- ```
450
-
451
- ### 2.d. Turning PaperTrail Off
452
-
453
- PaperTrail is on by default, but sometimes you don't want to record versions.
454
-
455
- #### Per Process
456
-
457
- Turn PaperTrail off for all threads in a `ruby` process.
458
-
459
- ```ruby
460
- PaperTrail.enabled = false
461
- ```
462
-
463
- This is commonly used to speed up tests. See [Testing](#7-testing) below.
464
-
465
- There is also a rails config option that does the same thing.
466
-
467
- ```ruby
468
- # in config/environments/test.rb
469
- config.paper_trail.enabled = false
470
- ```
471
-
472
- #### Per Request
473
-
474
- Add a `paper_trail_enabled_for_controller` method to your controller.
475
-
476
- ```ruby
477
- class ApplicationController < ActionController::Base
478
- def paper_trail_enabled_for_controller
479
- request.user_agent != 'Disable User-Agent'
480
- end
481
- end
482
- ```
483
-
484
- #### Per Class
485
-
486
- ```ruby
487
- Widget.paper_trail.disable
488
- Widget.paper_trail.enable
489
- ```
490
-
491
- #### Per Method
492
-
493
- You can call a method without creating a new version using `without_versioning`.
494
- It takes either a method name as a symbol:
495
-
496
- ```ruby
497
- @widget.paper_trail.without_versioning :destroy
498
- ```
499
-
500
- Or a block:
501
-
502
- ```ruby
503
- @widget.paper_trail.without_versioning do
504
- @widget.update_attributes name: 'Ford'
505
- end
506
- ```
507
-
508
- PaperTrail is disabled for the whole model
509
- (e.g. `Widget`), not just for the instance (e.g. `@widget`).
510
-
511
- ### 2.e. Limiting the Number of Versions Created
512
-
513
- Configure `version_limit` to cap the number of versions saved per record. This
514
- does not apply to `create` events.
515
-
516
- ```ruby
517
- # Limit: 4 versions per record (3 most recent, plus a `create` event)
518
- PaperTrail.config.version_limit = 3
519
- # Remove the limit
520
- PaperTrail.config.version_limit = nil
521
- ```
522
-
523
- ## 3. Working With Versions
524
-
525
- ### 3.a. Reverting And Undeleting A Model
526
-
527
- PaperTrail makes reverting to a previous version easy:
528
-
529
- ```ruby
530
- widget = Widget.find 42
531
- widget.update_attributes name: 'Blah blah'
532
- # Time passes....
533
- widget = widget.paper_trail.previous_version # the widget as it was before the update
534
- widget.save # reverted
535
- ```
536
-
537
- Alternatively you can find the version at a given time:
538
-
539
- ```ruby
540
- widget = widget.paper_trail.version_at(1.day.ago) # the widget as it was one day ago
541
- widget.save # reverted
542
- ```
543
-
544
- Note `version_at` gives you the object, not a version, so you don't need to call
545
- `reify`.
546
-
547
- Undeleting is just as simple:
548
-
549
- ```ruby
550
- widget = Widget.find(42)
551
- widget.destroy
552
- # Time passes....
553
- versions = widget.versions # versions ordered by versions.created_at, ascending
554
- widget = versions.last.reify # the widget as it was before destruction
555
- widget.save # the widget lives!
556
- ```
557
-
558
- You could even use PaperTrail to implement an undo system; [Ryan Bates has!][3]
559
-
560
- If your model uses [optimistic locking][1] don't forget to [increment your
561
- `lock_version`][2] before saving or you'll get a `StaleObjectError`.
562
-
563
- ### 3.b. Navigating Versions
564
-
565
- You can call `previous_version` and `next_version` on an item to get it as it
566
- was/became. Note that these methods reify the item for you.
567
-
568
- ```ruby
569
- live_widget = Widget.find 42
570
- live_widget.versions.length # 4, for example
571
- widget = live_widget.paper_trail.previous_version # => widget == live_widget.versions.last.reify
572
- widget = widget.paper_trail.previous_version # => widget == live_widget.versions[-2].reify
573
- widget = widget.paper_trail.next_version # => widget == live_widget.versions.last.reify
574
- widget.paper_trail.next_version # live_widget
575
- ```
576
-
577
- If instead you have a particular `version` of an item you can navigate to the
578
- previous and next versions.
579
-
580
- ```ruby
581
- widget = Widget.find 42
582
- version = widget.versions[-2] # assuming widget has several versions
583
- previous = version.previous
584
- next = version.next
585
- ```
586
-
587
- You can find out which of an item's versions yours is:
588
-
589
- ```ruby
590
- current_version_number = version.index # 0-based
591
- ```
592
-
593
- If you got an item by reifying one of its versions, you can navigate back to the
594
- version it came from:
595
-
596
- ```ruby
597
- latest_version = Widget.find(42).versions.last
598
- widget = latest_version.reify
599
- widget.version == latest_version # true
600
- ```
601
-
602
- You can find out whether a model instance is the current, live one -- or whether
603
- it came instead from a previous version -- with `live?`:
604
-
605
- ```ruby
606
- widget = Widget.find 42
607
- widget.live? # true
608
- widget = widget.paper_trail.previous_version
609
- widget.live? # false
610
- ```
611
-
612
- And you can perform `WHERE` queries for object versions based on attributes:
613
-
614
- ```ruby
615
- # All versions that meet these criteria.
616
- PaperTrail::Version.where_object(content: 'Hello', title: 'Article')
617
- ```
618
-
619
- ### 3.c. Diffing Versions
620
-
621
- There are two scenarios: diffing adjacent versions and diffing non-adjacent
622
- versions.
623
-
624
- The best way to diff adjacent versions is to get PaperTrail to do it for you.
625
- If you add an `object_changes` text column to your `versions` table, either at
626
- installation time with the `rails generate paper_trail:install --with-changes`
627
- option or manually, PaperTrail will store the `changes` diff (excluding any
628
- attributes PaperTrail is ignoring) in each `update` version. You can use the
629
- `version.changeset` method to retrieve it. For example:
630
-
631
- ```ruby
632
- widget = Widget.create name: 'Bob'
633
- widget.versions.last.changeset
634
- # {
635
- # "name"=>[nil, "Bob"],
636
- # "created_at"=>[nil, 2015-08-10 04:10:40 UTC],
637
- # "updated_at"=>[nil, 2015-08-10 04:10:40 UTC],
638
- # "id"=>[nil, 1]
639
- # }
640
- widget.update_attributes name: 'Robert'
641
- widget.versions.last.changeset
642
- # {
643
- # "name"=>["Bob", "Robert"],
644
- # "updated_at"=>[2015-08-10 04:13:19 UTC, 2015-08-10 04:13:19 UTC]
645
- # }
646
- widget.destroy
647
- widget.versions.last.changeset
648
- # {}
649
- ```
650
-
651
- The `object_changes` are only stored for creation and updates, not when an
652
- object is destroyed.
653
-
654
- Please be aware that PaperTrail doesn't use diffs internally. When I designed
655
- PaperTrail I wanted simplicity and robustness so I decided to make each version
656
- of an object self-contained. A version stores all of its object's data, not a
657
- diff from the previous version. This means you can delete any version without
658
- affecting any other.
659
-
660
- To diff non-adjacent versions you'll have to write your own code. These
661
- libraries may help:
662
-
663
- For diffing two strings:
664
-
665
- * [htmldiff][19]: expects but doesn't require HTML input and produces HTML
666
- output. Works very well but slows down significantly on large (e.g. 5,000
667
- word) inputs.
668
- * [differ][20]: expects plain text input and produces plain
669
- text/coloured/HTML/any output. Can do character-wise, word-wise, line-wise,
670
- or arbitrary-boundary-string-wise diffs. Works very well on non-HTML input.
671
- * [diff-lcs][21]: old-school, line-wise diffs.
672
-
673
- For diffing two ActiveRecord objects:
674
-
675
- * [Jeremy Weiskotten's PaperTrail fork][22]: uses ActiveSupport's diff to return
676
- an array of hashes of the changes.
677
- * [activerecord-diff][23]: rather like ActiveRecord::Dirty but also allows you
678
- to specify which columns to compare.
679
-
680
- If you want to selectively record changes for some models but not others you
681
- can opt out of recording changes by passing `save_changes: false` to your
682
- `has_paper_trail` method declaration.
683
-
684
- ### 3.d. Deleting Old Versions
685
-
686
- Over time your `versions` table will grow to an unwieldy size. Because each
687
- version is self-contained (see the Diffing section above for more) you can
688
- simply delete any records you don't want any more. For example:
689
-
690
- ```sql
691
- sql> delete from versions where created_at < 2010-06-01;
692
- ```
693
-
694
- ```ruby
695
- PaperTrail::Version.delete_all ['created_at < ?', 1.week.ago]
696
- ```
697
-
698
- ## 4. Saving More Information About Versions
699
-
700
- ### 4.a. Finding Out Who Was Responsible For A Change
701
-
702
- Set `PaperTrail.whodunnit=`, and that value will be stored in the version's
703
- `whodunnit` column.
704
-
705
- ```ruby
706
- PaperTrail.whodunnit = 'Andy Stewart'
707
- widget.update_attributes name: 'Wibble'
708
- widget.versions.last.whodunnit # Andy Stewart
709
- ```
710
-
711
- `whodunnit` also accepts a block, a convenient way to temporarily set the value.
712
-
713
- ```ruby
714
- PaperTrail.whodunnit('Dorian Marié') do
715
- widget.update_attributes name: 'Wibble'
716
- end
717
- ```
718
-
719
- `whodunnit` also accepts a `Proc`.
720
-
721
- ```ruby
722
- PaperTrail.whodunnit = proc do
723
- caller.first{ |c| c.starts_with? Rails.root.to_s }
724
- end
725
- ```
726
-
727
- If your controller has a `current_user` method, PaperTrail provides a
728
- `before_action` that will assign `current_user.id` to `PaperTrail.whodunnit`.
729
- You can add this `before_action` to your `ApplicationController`.
730
-
731
- ```ruby
732
- class ApplicationController
733
- before_action :set_paper_trail_whodunnit
734
- end
735
- ```
736
-
737
- You may want `set_paper_trail_whodunnit` to call a different method to find out
738
- who is responsible. To do so, override the `user_for_paper_trail` method in
739
- your controller like this:
740
-
741
- ```ruby
742
- class ApplicationController
743
- def user_for_paper_trail
744
- logged_in? ? current_member.id : 'Public user' # or whatever
745
- end
746
- end
747
- ```
748
-
749
- See also: [Setting whodunnit in the rails console][33]
750
-
751
- Sometimes you want to define who is responsible for a change in a small scope
752
- without overwriting value of `PaperTrail.whodunnit`. It is possible to define
753
- the `whodunnit` value for an operation inside a block like this:
754
-
755
- ```ruby
756
- PaperTrail.whodunnit = 'Andy Stewart'
757
- widget.paper_trail.whodunnit('Lucas Souza') do
758
- widget.update_attributes name: 'Wibble'
759
- end
760
- widget.versions.last.whodunnit # Lucas Souza
761
- widget.update_attributes name: 'Clair'
762
- widget.versions.last.whodunnit # Andy Stewart
763
- ```
764
-
765
- A version's `whodunnit` records who changed the object causing the `version` to
766
- be stored. Because a version stores the object as it looked before the change
767
- (see the table above), `whodunnit` returns who stopped the object looking like
768
- this -- not who made it look like this. Hence `whodunnit` is aliased as
769
- `terminator`.
770
-
771
- To find out who made a version's object look that way, use
772
- `version.paper_trail_originator`. And to find out who made a "live" object look
773
- like it does, call `paper_trail_originator` on the object.
774
-
775
- ```ruby
776
- widget = Widget.find 153 # assume widget has 0 versions
777
- PaperTrail.whodunnit = 'Alice'
778
- widget.update_attributes name: 'Yankee'
779
- widget.paper_trail.originator # 'Alice'
780
- PaperTrail.whodunnit = 'Bob'
781
- widget.update_attributes name: 'Zulu'
782
- widget.paper_trail.originator # 'Bob'
783
- first_version, last_version = widget.versions.first, widget.versions.last
784
- first_version.whodunnit # 'Alice'
785
- first_version.paper_trail_originator # nil
786
- first_version.terminator # 'Alice'
787
- last_version.whodunnit # 'Bob'
788
- last_version.paper_trail_originator # 'Alice'
789
- last_version.terminator # 'Bob'
790
- ```
791
-
792
- #### Storing an ActiveRecord globalid in whodunnit
793
-
794
- If you would like `whodunnit` to return an `ActiveRecord` object instead of a
795
- string, please try the [paper_trail-globalid][37] gem.
796
-
797
- ### 4.b. Associations
798
-
799
- **Experimental feature**, not recommended for production. See known issues
800
- below.
801
-
802
- PaperTrail can restore three types of associations: Has-One, Has-Many, and
803
- Has-Many-Through. In order to do this, you will need to do two things:
804
-
805
- 1. Create a `version_associations` table
806
- 2. Set `PaperTrail.config.track_associations = true` (e.g. in an initializer)
807
-
808
- Both will be done for you automatically if you install PaperTrail with the
809
- `--with_associations` option
810
- (e.g. `rails generate paper_trail:install --with-associations`)
811
-
812
- If you want to add this functionality after the initial installation, you will
813
- need to create the `version_associations` table manually, and you will need to
814
- ensure that `PaperTrail.config.track_associations = true` is set.
815
-
816
- PaperTrail will store in the `version_associations` table additional information
817
- to correlate versions of the association and versions of the model when the
818
- associated record is changed. When reifying the model, PaperTrail can use this
819
- table, together with the `transaction_id` to find the correct version of the
820
- association and reify it. The `transaction_id` is a unique id for version records
821
- created in the same transaction. It is used to associate the version of the model
822
- and the version of the association that are created in the same transaction.
823
-
824
- To restore Has-One associations as they were at the time, pass option `has_one:
825
- true` to `reify`. To restore Has-Many and Has-Many-Through associations, use
826
- option `has_many: true`. To restore Belongs-To association, use
827
- option `belongs_to: true`. For example:
828
-
829
- ```ruby
830
- class Location < ActiveRecord::Base
831
- belongs_to :treasure
832
- has_paper_trail
833
- end
834
-
835
- class Treasure < ActiveRecord::Base
836
- has_one :location
837
- has_paper_trail
838
- end
839
-
840
- treasure.amount # 100
841
- treasure.location.latitude # 12.345
842
-
843
- treasure.update_attributes amount: 153
844
- treasure.location.update_attributes latitude: 54.321
845
-
846
- t = treasure.versions.last.reify(has_one: true)
847
- t.amount # 100
848
- t.location.latitude # 12.345
849
- ```
850
-
851
- If the parent and child are updated in one go, PaperTrail can use the
852
- aforementioned `transaction_id` to reify the models as they were before the
853
- transaction (instead of before the update to the model).
854
-
855
- ```ruby
856
- treasure.amount # 100
857
- treasure.location.latitude # 12.345
858
-
859
- Treasure.transaction do
860
- treasure.location.update_attributes latitude: 54.321
861
- treasure.update_attributes amount: 153
862
- end
863
-
864
- t = treasure.versions.last.reify(has_one: true)
865
- t.amount # 100
866
- t.location.latitude # 12.345, instead of 54.321
867
- ```
868
-
869
- By default, PaperTrail excludes an associated record from the reified parent
870
- model if the associated record exists in the live model but did not exist as at
871
- the time the version was created. This is usually what you want if you just want
872
- to look at the reified version. But if you want to persist it, it would be
873
- better to pass in option `mark_for_destruction: true` so that the associated
874
- record is included and marked for destruction. Note that `mark_for_destruction`
875
- only has [an effect on associations marked with `autosave: true`][32].
876
-
877
- ```ruby
878
- class Widget < ActiveRecord::Base
879
- has_paper_trail
880
- has_one :wotsit, autosave: true
881
- end
882
-
883
- class Wotsit < ActiveRecord::Base
884
- has_paper_trail
885
- belongs_to :widget
886
- end
887
-
888
- widget = Widget.create(name: 'widget_0')
889
- widget.update_attributes(name: 'widget_1')
890
- widget.create_wotsit(name: 'wotsit')
891
-
892
- widget_0 = widget.versions.last.reify(has_one: true)
893
- widget_0.wotsit # nil
894
-
895
- widget_0 = widget.versions.last.reify(has_one: true, mark_for_destruction: true)
896
- widget_0.wotsit.marked_for_destruction? # true
897
- widget_0.save!
898
- widget.reload.wotsit # nil
899
- ```
900
-
901
- #### 4.b.1. Known Issues
902
-
903
- Associations are an **experimental feature** and have the following known
904
- issues, in order of descending importance.
905
-
906
- 1. PaperTrail only reifies the first level of associations.
907
- 1. [#542](https://github.com/airblade/paper_trail/issues/542) -
908
- Not compatible with [transactional tests][34], aka. transactional fixtures.
909
- 1. Requires database timestamp columns with fractional second precision.
910
- - Sqlite and postgres timestamps have fractional second precision by default.
911
- [MySQL timestamps do not][35]. Furthermore, MySQL 5.5 and earlier do not
912
- support fractional second precision at all.
913
- - Also, support for fractional seconds in MySQL was not added to
914
- rails until ActiveRecord 4.2 (https://github.com/rails/rails/pull/14359).
915
- 1. PaperTrail can't restore an association properly if the association record
916
- can be updated to replace its parent model (by replacing the foreign key)
917
- 1. Currently PaperTrail only supports a single `version_associations` table.
918
- Therefore, you can only use a single table to store the versions for
919
- all related models. Sorry for those who use multiple version tables.
920
- 1. PaperTrail relies on the callbacks on the association model (and the :through
921
- association model for Has-Many-Through associations) to record the versions
922
- and the relationship between the versions. If the association is changed
923
- without invoking the callbacks, Reification won't work. Below are some
924
- examples:
925
-
926
- Given these models:
927
-
928
- ```ruby
929
- class Book < ActiveRecord::Base
930
- has_many :authorships, dependent: :destroy
931
- has_many :authors, through: :authorships, source: :person
932
- has_paper_trail
933
- end
934
-
935
- class Authorship < ActiveRecord::Base
936
- belongs_to :book
937
- belongs_to :person
938
- has_paper_trail # NOTE
939
- end
940
-
941
- class Person < ActiveRecord::Base
942
- has_many :authorships, dependent: :destroy
943
- has_many :books, through: :authorships
944
- has_paper_trail
945
- end
946
- ```
947
-
948
- Then each of the following will store authorship versions:
949
-
950
- ```ruby
951
- @book.authors << @dostoyevsky
952
- @book.authors.create name: 'Tolstoy'
953
- @book.authorships.last.destroy
954
- @book.authorships.clear
955
- @book.author_ids = [@solzhenistyn.id, @dostoyevsky.id]
956
- ```
957
-
958
- But none of these will:
959
-
960
- ```ruby
961
- @book.authors.delete @tolstoy
962
- @book.author_ids = []
963
- @book.authors = []
964
- ```
965
-
966
- Having said that, you can apparently get all these working (I haven't tested it
967
- myself) with this patch:
968
-
969
- ```ruby
970
- # In config/initializers/active_record_patch.rb
971
- module ActiveRecord
972
- # = Active Record Has Many Through Association
973
- module Associations
974
- class HasManyThroughAssociation < HasManyAssociation #:nodoc:
975
- alias_method :original_delete_records, :delete_records
976
-
977
- def delete_records(records, method)
978
- method ||= :destroy
979
- original_delete_records(records, method)
980
- end
981
- end
982
- end
983
- end
984
- ```
985
-
986
- See [issue 113][16] for a discussion about this.
987
-
988
- ### 4.c. Storing Metadata
989
-
990
- You can store arbitrary model-level metadata alongside each version like this:
991
-
992
- ```ruby
993
- class Article < ActiveRecord::Base
994
- belongs_to :author
995
- has_paper_trail meta: { author_id: :author_id,
996
- word_count: :count_words,
997
- answer: 42 }
998
- def count_words
999
- 153
1000
- end
1001
- end
1002
- ```
1003
-
1004
- PaperTrail will call your proc with the current article and store the result in
1005
- the `author_id` column of the `versions` table.
1006
- Don't forget to add any such columns to your `versions` table.
1007
-
1008
- #### Advantages of Metadata
1009
-
1010
- Why would you do this? In this example, `author_id` is an attribute of
1011
- `Article` and PaperTrail will store it anyway in a serialized form in the
1012
- `object` column of the `version` record. But let's say you wanted to pull out
1013
- all versions for a particular author; without the metadata you would have to
1014
- deserialize (reify) each `version` object to see if belonged to the author in
1015
- question. Clearly this is inefficient. Using the metadata you can find just
1016
- those versions you want:
1017
-
1018
- ```ruby
1019
- PaperTrail::Version.where(author_id: author_id)
1020
- ```
1021
-
1022
- #### Metadata from Controllers
1023
-
1024
- You can also store any information you like from your controller. Override
1025
- the `info_for_paper_trail` method in your controller to return a hash whose keys
1026
- correspond to columns in your `versions` table.
1027
-
1028
- ```ruby
1029
- class ApplicationController
1030
- def info_for_paper_trail
1031
- { ip: request.remote_ip, user_agent: request.user_agent }
1032
- end
1033
- end
1034
- ```
1035
-
1036
- ## 5. ActiveRecord
1037
-
1038
- ### 5.a. Single Table Inheritance (STI)
1039
-
1040
- PaperTrail supports [Single Table Inheritance][39], and even supports an
1041
- un-versioned base model, as of `23ffbdc7e1`.
1042
-
1043
- ```ruby
1044
- class Fruit < ActiveRecord::Base
1045
- # un-versioned base model
1046
- end
1047
- class Banana < Fruit
1048
- has_paper_trail
1049
- end
1050
- ```
1051
-
1052
- However, there is a known issue when reifying [associations](#associations),
1053
- see https://github.com/airblade/paper_trail/issues/594
1054
-
1055
- ### 5.b. Configuring the `versions` Association
1056
-
1057
- You may configure the name of the `versions` association by passing
1058
- a different name to `has_paper_trail`.
1059
-
1060
- ```ruby
1061
- class Post < ActiveRecord::Base
1062
- has_paper_trail class_name: 'Version', versions: :drafts
1063
- end
1064
-
1065
- Post.new.versions # => NoMethodError
1066
- ```
1067
-
1068
- Overriding (instead of configuring) the `versions` method is not supported.
1069
- Overriding associations is not recommended in general.
1070
-
1071
- ### 5.c. Generators
1072
-
1073
- PaperTrail has one generator, `paper_trail:install`. It writes, but does not
1074
- run, a migration file. It also creates a PaperTrail configuration initializer.
1075
- The migration adds (at least) the `versions` table. The
1076
- most up-to-date documentation for this generator can be found by running `rails
1077
- generate paper_trail:install --help`, but a copy is included here for
1078
- convenience.
1079
-
1080
- ```
1081
- Usage:
1082
- rails generate paper_trail:install [options]
1083
-
1084
- Options:
1085
- [--with-changes], [--no-with-changes] # Store changeset (diff) with each version
1086
- [--with-associations], [--no-with-associations] # Store transactional IDs to support association restoration
1087
-
1088
- Runtime options:
1089
- -f, [--force] # Overwrite files that already exist
1090
- -p, [--pretend], [--no-pretend] # Run but do not make any changes
1091
- -q, [--quiet], [--no-quiet] # Suppress status output
1092
- -s, [--skip], [--no-skip] # Skip files that already exist
1093
-
1094
- Generates (but does not run) a migration to add a versions table. Also generates an initializer file for configuring PaperTrail
1095
- ```
1096
-
1097
- ### 5.d. Protected Attributes
1098
-
1099
- As of version 6, PT no longer supports rails 3 or the [protected_attributes][17]
1100
- gem. If you are still using them, you may use PT 5 or lower. We recommend
1101
- upgrading to [strong_parameters][18] as soon as possible.
1102
-
1103
- If you must use [protected_attributes][17] for now, and want to use PT > 5, you
1104
- can reopen `PaperTrail::Version` and add the following `attr_accessible` fields:
1105
-
1106
- ```ruby
1107
- # app/models/paper_trail/version.rb
1108
- module PaperTrail
1109
- class Version < ActiveRecord::Base
1110
- include PaperTrail::VersionConcern
1111
- attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes, :created_at
1112
- end
1113
- end
1114
- ```
1115
-
1116
- This unsupported workaround has been tested with protected_attributes 1.0.9 /
1117
- rails 4.2.8 / paper_trail 7.0.3.
1118
-
1119
- ## 6. Extensibility
1120
-
1121
- ### 6.a. Custom Version Classes
1122
-
1123
- You can specify custom version subclasses with the `:class_name` option:
1124
-
1125
- ```ruby
1126
- class PostVersion < PaperTrail::Version
1127
- # custom behaviour, e.g:
1128
- self.table_name = :post_versions
1129
- end
1130
-
1131
- class Post < ActiveRecord::Base
1132
- has_paper_trail class_name: 'PostVersion'
1133
- end
1134
- ```
1135
-
1136
- Unlike ActiveRecord's `class_name`, you'll have to supply the complete module
1137
- path to the class (e.g. `Foo::BarVersion` if your class is inside the module
1138
- `Foo`).
1139
-
1140
- #### Advantages
1141
-
1142
- 1. For models which have a lot of versions, storing each model's versions in a
1143
- separate table can improve the performance of certain database queries.
1144
- 1. Store different version [metadata](#storing-metadata) for different models.
1145
-
1146
- #### Configuration
1147
-
1148
- If you are using Postgres, you should also define the sequence that your custom
1149
- version class will use:
1150
-
1151
- ```ruby
1152
- class PostVersion < PaperTrail::Version
1153
- self.table_name = :post_versions
1154
- self.sequence_name = :post_versions_id_seq
1155
- end
1156
- ```
1157
-
1158
- If you only use custom version classes and don't have a `versions` table, you
1159
- must let ActiveRecord know that the `PaperTrail::Version` class is an
1160
- `abstract_class`.
1161
-
1162
- ```ruby
1163
- # app/models/paper_trail/version.rb
1164
- module PaperTrail
1165
- class Version < ActiveRecord::Base
1166
- include PaperTrail::VersionConcern
1167
- self.abstract_class = true
1168
- end
1169
- end
1170
- ```
1171
-
1172
- You can also specify custom names for the versions and version associations.
1173
- This is useful if you already have `versions` or/and `version` methods on your
1174
- model. For example:
1175
-
1176
- ```ruby
1177
- class Post < ActiveRecord::Base
1178
- has_paper_trail versions: :paper_trail_versions,
1179
- version: :paper_trail_version
1180
-
1181
- # Existing versions method. We don't want to clash.
1182
- def versions
1183
- # ...
1184
- end
1185
-
1186
- # Existing version method. We don't want to clash.
1187
- def version
1188
- # ...
1189
- end
1190
- end
1191
- ```
1192
-
1193
- ### 6.b. Custom Serializer
1194
-
1195
- By default, PaperTrail stores your changes as a `YAML` dump. You can override
1196
- this with the serializer config option:
1197
-
1198
- ```ruby
1199
- PaperTrail.serializer = MyCustomSerializer
1200
- ```
1201
-
1202
- A valid serializer is a `module` (or `class`) that defines a `load` and `dump`
1203
- method. These serializers are included in the gem for your convenience:
1204
-
1205
- * [PaperTrail::Serializers::YAML][24] - Default
1206
- * [PaperTrail::Serializers::JSON][25]
1207
-
1208
- #### PostgreSQL JSON column type support
1209
-
1210
- If you use PostgreSQL, and would like to store your `object` (and/or
1211
- `object_changes`) data in a column of [type `json` or type `jsonb`][26], specify
1212
- `json` instead of `text` for these columns in your migration:
1213
-
1214
- ```ruby
1215
- create_table :versions do |t|
1216
- # ...
1217
- t.json :object # Full object changes
1218
- t.json :object_changes # Optional column-level changes
1219
- # ...
1220
- end
1221
- ```
1222
-
1223
- If you use the PostgreSQL `json` or `jsonb` column type, you do not need
1224
- to specify a `PaperTrail.serializer`.
1225
-
1226
- ##### Convert existing YAML data to JSON
1227
-
1228
- If you've been using PaperTrail for a while with the default YAML serializer
1229
- and you want to switch to JSON or JSONB, you're in a bit of a bind because
1230
- there's no automatic way to migrate your data. The first (slow) option is to
1231
- loop over every record and parse it in Ruby, then write to a temporary column:
1232
-
1233
- ```ruby
1234
- add_column :versions, :new_object, :jsonb # or :json
1235
-
1236
- PaperTrail::Version.reset_column_information
1237
- PaperTrail::Version.find_each do |version|
1238
- version.update_column :new_object, YAML.load(version.object)
1239
- end
1240
-
1241
- remove_column :versions, :object
1242
- rename_column :versions, :new_object, :object
1243
- ```
1244
-
1245
- This technique can be very slow if you have a lot of data. Though slow, it is
1246
- safe in databases where transactions are protected against DDL, such as
1247
- Postgres. In databases without such protection, such as MySQL, a table lock may
1248
- be necessary.
1249
-
1250
- If the above technique is too slow for your needs, and you're okay doing without
1251
- PaperTrail data temporarily, you can create the new column without converting
1252
- the data.
1253
-
1254
- ```ruby
1255
- rename_column :versions, :object, :old_object
1256
- add_column :versions, :object, :jsonb # or :json
1257
- ```
1258
-
1259
- After that migration, your historical data still exists as YAML, and new data
1260
- will be stored as JSON. Next, convert records from YAML to JSON using a
1261
- background script.
1262
-
1263
- ```ruby
1264
- PaperTrail::Version.where.not(old_object: nil).find_each do |version|
1265
- version.update_columns old_object: nil, object: YAML.load(version.old_object)
1266
- end
1267
- ```
1268
-
1269
- Finally, in another migration, remove the old column.
1270
-
1271
- ```ruby
1272
- remove_column :versions, :old_object
1273
- ```
1274
-
1275
- If you use the optional `object_changes` column, don't forget to convert it
1276
- also, using the same technique.
1277
-
1278
- ##### Convert a Column from Text to JSON
1279
-
1280
- If your `object` column already contains JSON data, and you want to change its
1281
- data type to `json` or `jsonb`, you can use the following [DDL][36]. Of course,
1282
- if your `object` column contains YAML, you must first convert the data to JSON
1283
- (see above) before you can change the column type.
1284
-
1285
- Using SQL:
1286
-
1287
- ```sql
1288
- alter table versions
1289
- alter column object type jsonb
1290
- using object::jsonb;
1291
- ```
1292
-
1293
- Using ActiveRecord:
1294
-
1295
- ```ruby
1296
- class ConvertVersionsObjectToJson < ActiveRecord::Migration
1297
- def up
1298
- change_column :versions, :object, 'jsonb USING object::jsonb'
1299
- end
1300
-
1301
- def down
1302
- change_column :versions, :object, 'text USING object::text'
1303
- end
1304
- end
1305
- ```
1306
-
1307
- ## 7. Testing
1308
-
1309
- You may want to turn PaperTrail off to speed up your tests. See [Turning
1310
- PaperTrail Off](#turning-papertrail-off) above.
1311
-
1312
- ### 7.a. Minitest
1313
-
1314
- First, disable PT for the entire `ruby` process.
1315
-
1316
- ```ruby
1317
- # in config/environments/test.rb
1318
- config.after_initialize do
1319
- PaperTrail.enabled = false
1320
- end
1321
- ```
1322
-
1323
- Then, to enable PT for specific tests, you can add a `with_versioning` test
1324
- helper method.
1325
-
1326
- ```ruby
1327
- # in test/test_helper.rb
1328
- def with_versioning
1329
- was_enabled = PaperTrail.enabled?
1330
- was_enabled_for_controller = PaperTrail.enabled_for_controller?
1331
- PaperTrail.enabled = true
1332
- PaperTrail.enabled_for_controller = true
1333
- begin
1334
- yield
1335
- ensure
1336
- PaperTrail.enabled = was_enabled
1337
- PaperTrail.enabled_for_controller = was_enabled_for_controller
1338
- end
1339
- end
1340
- ```
1341
-
1342
- Then, use the helper in your tests.
1343
-
1344
- ```ruby
1345
- test 'something that needs versioning' do
1346
- with_versioning do
1347
- # your test
1348
- end
1349
- end
1350
- ```
1351
-
1352
- ### 7.b. RSpec
1353
-
1354
- PaperTrail provides a helper, `paper_trail/frameworks/rspec.rb`, that works with
1355
- [RSpec][27] to make it easier to control when `PaperTrail` is enabled during
1356
- testing.
1357
-
1358
- ```ruby
1359
- # spec/rails_helper.rb
1360
- ENV["RAILS_ENV"] ||= 'test'
1361
- require 'spec_helper'
1362
- require File.expand_path("../../config/environment", __FILE__)
1363
- require 'rspec/rails'
1364
- # ...
1365
- require 'paper_trail/frameworks/rspec'
1366
- ```
1367
-
1368
- With the helper loaded, PaperTrail will be turned off for all tests by
1369
- default. To enable PaperTrail for a test you can either wrap the
1370
- test in a `with_versioning` block, or pass in `versioning: true` option to a
1371
- spec block.
1372
-
1373
- ```ruby
1374
- describe 'RSpec test group' do
1375
- it 'by default, PaperTrail will be turned off' do
1376
- expect(PaperTrail).to_not be_enabled
1377
- end
1378
-
1379
- with_versioning do
1380
- it 'within a `with_versioning` block it will be turned on' do
1381
- expect(PaperTrail).to be_enabled
1382
- end
1383
- end
1384
-
1385
- it 'can be turned on at the `it` or `describe` level', versioning: true do
1386
- expect(PaperTrail).to be_enabled
1387
- end
1388
- end
1389
- ```
1390
-
1391
- The helper will also reset the `PaperTrail.whodunnit` value to `nil` before each
1392
- test to help prevent data spillover between tests. If you are using PaperTrail
1393
- with Rails, the helper will automatically set the `PaperTrail.controller_info`
1394
- value to `{}` as well, again, to help prevent data spillover between tests.
1395
-
1396
- There is also a `be_versioned` matcher provided by PaperTrail's RSpec helper
1397
- which can be leveraged like so:
1398
-
1399
- ```ruby
1400
- class Widget < ActiveRecord::Base
1401
- end
1402
-
1403
- describe Widget do
1404
- it 'is not versioned by default' do
1405
- is_expected.to_not be_versioned
1406
- end
1407
-
1408
- describe 'add versioning to the `Widget` class' do
1409
- before(:all) do
1410
- class Widget < ActiveRecord::Base
1411
- has_paper_trail
1412
- end
1413
- end
1414
-
1415
- it 'enables paper trail' do
1416
- is_expected.to be_versioned
1417
- end
1418
- end
1419
- end
1420
- ```
1421
-
1422
- #### Matchers
1423
-
1424
- The `have_a_version_with` matcher makes assertions about versions using
1425
- `where_object`, based on the `object` column.
1426
-
1427
- ```ruby
1428
- describe '`have_a_version_with` matcher' do
1429
- it 'is possible to do assertions on version attributes' do
1430
- widget.update_attributes!(name: 'Leonard', an_integer: 1)
1431
- widget.update_attributes!(name: 'Tom')
1432
- widget.update_attributes!(name: 'Bob')
1433
- expect(widget).to have_a_version_with name: 'Leonard', an_integer: 1
1434
- expect(widget).to have_a_version_with an_integer: 1
1435
- expect(widget).to have_a_version_with name: 'Tom'
1436
- end
1437
- end
1438
- ```
1439
-
1440
- The `have_a_version_with_changes` matcher makes assertions about versions using
1441
- `where_object_changes`, based on the optional
1442
- [`object_changes` column](#3c-diffing-versions).
1443
-
1444
- ```ruby
1445
- describe '`have_a_version_with_changes` matcher' do
1446
- it 'is possible to do assertions on version changes' do
1447
- widget.update_attributes!(name: 'Leonard', an_integer: 1)
1448
- widget.update_attributes!(name: 'Tom')
1449
- widget.update_attributes!(name: 'Bob')
1450
- expect(widget).to have_a_version_with_changes name: 'Leonard', an_integer: 2
1451
- expect(widget).to have_a_version_with_changes an_integer: 2
1452
- expect(widget).to have_a_version_with_changes name: 'Bob'
1453
- end
1454
- end
1455
- ```
1456
-
1457
- For more examples of the RSpec matchers, see the
1458
- [Widget spec](https://github.com/airblade/paper_trail/blob/master/spec/models/widget_spec.rb)
1459
-
1460
- ### 7.c. Cucumber
1461
-
1462
- PaperTrail provides a helper for [Cucumber][28] that works similar to the RSpec
1463
- helper. If you want to use the helper, you will need to require in your cucumber
1464
- helper like so:
1465
-
1466
- ```ruby
1467
- # features/support/env.rb
1468
-
1469
- ENV["RAILS_ENV"] ||= 'cucumber'
1470
- require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
1471
- # ...
1472
- require 'paper_trail/frameworks/cucumber'
1473
- ```
1474
-
1475
- When the helper is loaded, PaperTrail will be turned off for all scenarios by a
1476
- `before` hook added by the helper by default. When you want to enable PaperTrail
1477
- for a scenario, you can wrap code in a `with_versioning` block in a step, like
1478
- so:
1479
-
1480
- ```ruby
1481
- Given /I want versioning on my model/ do
1482
- with_versioning do
1483
- # PaperTrail will be turned on for all code inside of this block
1484
- end
1485
- end
1486
- ```
1487
-
1488
- The helper will also reset the `PaperTrail.whodunnit` value to `nil` before each
1489
- test to help prevent data spillover between tests. If you are using PaperTrail
1490
- with Rails, the helper will automatically set the `PaperTrail.controller_info`
1491
- value to `{}` as well, again, to help prevent data spillover between tests.
1492
-
1493
- ### 7.d. Spork
1494
-
1495
- If you want to use the `RSpec` or `Cucumber` helpers with [Spork][29], you will
1496
- need to manually require the helper(s) in your `prefork` block on your test
1497
- helper, like so:
1498
-
1499
- ```ruby
1500
- # spec/rails_helper.rb
1501
-
1502
- require 'spork'
1503
-
1504
- Spork.prefork do
1505
- # This file is copied to spec/ when you run 'rails generate rspec:install'
1506
- ENV["RAILS_ENV"] ||= 'test'
1507
- require 'spec_helper'
1508
- require File.expand_path("../../config/environment", __FILE__)
1509
- require 'rspec/rails'
1510
- require 'paper_trail/frameworks/rspec'
1511
- require 'paper_trail/frameworks/cucumber'
1512
- # ...
1513
- end
1514
- ```
1515
-
1516
- ### 7.e. Zeus or Spring
1517
-
1518
- If you want to use the `RSpec` or `Cucumber` helpers with [Zeus][30] or
1519
- [Spring][31], you will need to manually require the helper(s) in your test
1520
- helper, like so:
1521
-
1522
- ```ruby
1523
- # spec/rails_helper.rb
1524
-
1525
- ENV["RAILS_ENV"] ||= 'test'
1526
- require 'spec_helper'
1527
- require File.expand_path("../../config/environment", __FILE__)
1528
- require 'rspec/rails'
1529
- require 'paper_trail/frameworks/rspec'
1530
- ```
1531
-
1532
- ## 8. Integration with Other Libraries
1533
-
1534
- - [ActiveAdmin][42]
1535
- - Sinatra - [paper_trail-sinatra][41]
1536
-
1537
- ## Articles
1538
-
1539
- * [Jutsu #8 - Version your RoR models with PaperTrail](http://samurails.com/gems/papertrail/),
1540
- [Thibault](http://samurails.com/about-me/), 29th September 2014
1541
- * [Versioning with PaperTrail](http://www.sitepoint.com/versioning-papertrail),
1542
- [Ilya Bodrov](http://www.sitepoint.com/author/ibodrov), 10th April 2014
1543
- * [Using PaperTrail to track stack traces](http://web.archive.org/web/20141120233916/http://rubyrailsexpert.com/?p=36),
1544
- T James Corcoran's blog, 1st October 2013.
1545
- * [RailsCast #255 - Undo with PaperTrail](http://railscasts.com/episodes/255-undo-with-paper-trail),
1546
- 28th February 2011.
1547
- * [Keep a Paper Trail with PaperTrail](http://www.linux-mag.com/id/7528),
1548
- Linux Magazine, 16th September 2009.
1549
-
1550
- ## Problems
1551
-
1552
- Please use GitHub's [issue tracker](http://github.com/airblade/paper_trail/issues).
1553
-
1554
- ## Contributors
1555
-
1556
- Created by Andy Stewart in 2010, maintained since 2012 by Ben Atkins, since 2015
1557
- by Jared Beck, with contributions by over 150 people.
1558
-
1559
- https://github.com/airblade/paper_trail/graphs/contributors
1560
-
1561
- ## Contributing
1562
-
1563
- See our [contribution guidelines][43]
1564
-
1565
- ## Inspirations
1566
-
1567
- * [Simply Versioned](http://github.com/github/simply_versioned)
1568
- * [Acts As Audited](http://github.com/collectiveidea/acts_as_audited)
1569
-
1570
- ## Intellectual Property
1571
-
1572
- Copyright (c) 2011 Andy Stewart (boss@airbladesoftware.com).
1573
- Released under the MIT licence.
1574
-
1575
- [1]: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
1576
- [2]: https://github.com/airblade/paper_trail/issues/163
1577
- [3]: http://railscasts.com/episodes/255-undo-with-paper-trail
1578
- [4]: https://api.travis-ci.org/airblade/paper_trail.svg?branch=master
1579
- [5]: https://travis-ci.org/airblade/paper_trail
1580
- [6]: https://img.shields.io/gemnasium/airblade/paper_trail.svg
1581
- [7]: https://gemnasium.com/airblade/paper_trail
1582
- [9]: https://github.com/airblade/paper_trail/tree/3.0-stable
1583
- [10]: https://github.com/airblade/paper_trail/tree/2.7-stable
1584
- [11]: https://github.com/airblade/paper_trail/tree/rails2
1585
- [14]: https://raw.github.com/airblade/paper_trail/master/lib/generators/paper_trail/templates/create_versions.rb
1586
- [16]: https://github.com/airblade/paper_trail/issues/113
1587
- [17]: https://github.com/rails/protected_attributes
1588
- [18]: https://github.com/rails/strong_parameters
1589
- [19]: http://github.com/myobie/htmldiff
1590
- [20]: http://github.com/pvande/differ
1591
- [21]: https://github.com/halostatue/diff-lcs
1592
- [22]: http://github.com/jeremyw/paper_trail/blob/master/lib/paper_trail/has_paper_trail.rb#L151-156
1593
- [23]: http://github.com/tim/activerecord-diff
1594
- [24]: https://github.com/airblade/paper_trail/blob/master/lib/paper_trail/serializers/yaml.rb
1595
- [25]: https://github.com/airblade/paper_trail/blob/master/lib/paper_trail/serializers/json.rb
1596
- [26]: http://www.postgresql.org/docs/9.4/static/datatype-json.html
1597
- [27]: https://github.com/rspec/rspec
1598
- [28]: http://cukes.info
1599
- [29]: https://github.com/sporkrb/spork
1600
- [30]: https://github.com/burke/zeus
1601
- [31]: https://github.com/rails/spring
1602
- [32]: http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html#method-i-mark_for_destruction
1603
- [33]: https://github.com/airblade/paper_trail/wiki/Setting-whodunnit-in-the-rails-console
1604
- [34]: https://github.com/rails/rails/blob/591a0bb87fff7583e01156696fbbf929d48d3e54/activerecord/lib/active_record/fixtures.rb#L142
1605
- [35]: https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html
1606
- [36]: http://www.postgresql.org/docs/9.4/interactive/ddl.html
1607
- [37]: https://github.com/ankit1910/paper_trail-globalid
1608
- [38]: https://github.com/sferik/rails_admin
1609
- [39]: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance
1610
- [40]: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Polymorphic+Associations
1611
- [41]: https://github.com/jaredbeck/paper_trail-sinatra
1612
- [42]: https://github.com/activeadmin/activeadmin/wiki/Auditing-via-paper_trail-%28change-history%29
1613
- [43]: https://github.com/airblade/paper_trail/blob/master/.github/CONTRIBUTING.md