paper_trail 7.1.0 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
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