paper_trail 5.2.3 → 11.0.0

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