paper_trail 4.2.0 → 7.1.3

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