paper_trail 3.0.6 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -2
  4. data/.travis.yml +14 -5
  5. data/CHANGELOG.md +215 -8
  6. data/CONTRIBUTING.md +84 -0
  7. data/README.md +922 -502
  8. data/Rakefile +2 -2
  9. data/doc/bug_report_template.rb +65 -0
  10. data/gemfiles/ar3.gemfile +61 -0
  11. data/lib/generators/paper_trail/install_generator.rb +22 -3
  12. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +6 -1
  13. data/lib/generators/paper_trail/templates/add_transaction_id_column_to_versions.rb +11 -0
  14. data/lib/generators/paper_trail/templates/create_version_associations.rb +17 -0
  15. data/lib/generators/paper_trail/templates/create_versions.rb +22 -1
  16. data/lib/paper_trail.rb +52 -22
  17. data/lib/paper_trail/attributes_serialization.rb +89 -0
  18. data/lib/paper_trail/cleaner.rb +32 -15
  19. data/lib/paper_trail/config.rb +35 -2
  20. data/lib/paper_trail/frameworks/active_record.rb +4 -5
  21. data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +7 -0
  22. data/lib/paper_trail/frameworks/rails.rb +1 -0
  23. data/lib/paper_trail/frameworks/rails/controller.rb +27 -11
  24. data/lib/paper_trail/frameworks/rspec.rb +5 -0
  25. data/lib/paper_trail/frameworks/sinatra.rb +3 -1
  26. data/lib/paper_trail/has_paper_trail.rb +304 -148
  27. data/lib/paper_trail/record_history.rb +59 -0
  28. data/lib/paper_trail/reifier.rb +270 -0
  29. data/lib/paper_trail/serializers/json.rb +13 -2
  30. data/lib/paper_trail/serializers/yaml.rb +16 -2
  31. data/lib/paper_trail/version_association_concern.rb +15 -0
  32. data/lib/paper_trail/version_concern.rb +160 -122
  33. data/lib/paper_trail/version_number.rb +3 -3
  34. data/paper_trail.gemspec +22 -9
  35. data/spec/generators/install_generator_spec.rb +4 -4
  36. data/spec/models/animal_spec.rb +36 -0
  37. data/spec/models/boolit_spec.rb +48 -0
  38. data/spec/models/callback_modifier_spec.rb +96 -0
  39. data/spec/models/fluxor_spec.rb +19 -0
  40. data/spec/models/gadget_spec.rb +14 -12
  41. data/spec/models/joined_version_spec.rb +9 -9
  42. data/spec/models/json_version_spec.rb +103 -0
  43. data/spec/models/kitchen/banana_spec.rb +14 -0
  44. data/spec/models/not_on_update_spec.rb +19 -0
  45. data/spec/models/post_with_status_spec.rb +3 -3
  46. data/spec/models/skipper_spec.rb +46 -0
  47. data/spec/models/thing_spec.rb +11 -0
  48. data/spec/models/version_spec.rb +195 -44
  49. data/spec/models/widget_spec.rb +136 -76
  50. data/spec/modules/paper_trail_spec.rb +27 -0
  51. data/spec/modules/version_concern_spec.rb +8 -8
  52. data/spec/modules/version_number_spec.rb +16 -16
  53. data/spec/paper_trail/config_spec.rb +52 -0
  54. data/spec/paper_trail_spec.rb +17 -17
  55. data/spec/rails_helper.rb +34 -0
  56. data/spec/requests/articles_spec.rb +10 -14
  57. data/spec/spec_helper.rb +81 -34
  58. data/spec/support/alt_db_init.rb +1 -1
  59. data/test/dummy/app/controllers/application_controller.rb +1 -1
  60. data/test/dummy/app/controllers/articles_controller.rb +4 -1
  61. data/test/dummy/app/models/animal.rb +2 -0
  62. data/test/dummy/app/models/book.rb +4 -0
  63. data/test/dummy/app/models/boolit.rb +4 -0
  64. data/test/dummy/app/models/callback_modifier.rb +45 -0
  65. data/test/dummy/app/models/chapter.rb +9 -0
  66. data/test/dummy/app/models/citation.rb +5 -0
  67. data/test/dummy/app/models/customer.rb +4 -0
  68. data/test/dummy/app/models/editor.rb +4 -0
  69. data/test/dummy/app/models/editorship.rb +5 -0
  70. data/test/dummy/app/models/fruit.rb +5 -0
  71. data/test/dummy/app/models/kitchen/banana.rb +5 -0
  72. data/test/dummy/app/models/line_item.rb +4 -0
  73. data/test/dummy/app/models/not_on_update.rb +4 -0
  74. data/test/dummy/app/models/order.rb +5 -0
  75. data/test/dummy/app/models/paragraph.rb +5 -0
  76. data/test/dummy/app/models/person.rb +13 -3
  77. data/test/dummy/app/models/post.rb +0 -1
  78. data/test/dummy/app/models/quotation.rb +5 -0
  79. data/test/dummy/app/models/section.rb +6 -0
  80. data/test/dummy/app/models/skipper.rb +6 -0
  81. data/test/dummy/app/models/song.rb +20 -0
  82. data/test/dummy/app/models/thing.rb +3 -0
  83. data/test/dummy/app/models/whatchamajigger.rb +4 -0
  84. data/test/dummy/app/models/widget.rb +5 -0
  85. data/test/dummy/app/versions/json_version.rb +3 -0
  86. data/test/dummy/app/versions/kitchen/banana_version.rb +5 -0
  87. data/test/dummy/config/application.rb +6 -0
  88. data/test/dummy/config/database.postgres.yml +1 -1
  89. data/test/dummy/config/environments/test.rb +5 -1
  90. data/test/dummy/config/initializers/paper_trail.rb +6 -1
  91. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +143 -3
  92. data/test/dummy/db/schema.rb +169 -25
  93. data/test/functional/controller_test.rb +4 -2
  94. data/test/functional/modular_sinatra_test.rb +1 -1
  95. data/test/functional/sinatra_test.rb +1 -1
  96. data/test/paper_trail_test.rb +7 -0
  97. data/test/test_helper.rb +38 -2
  98. data/test/time_travel_helper.rb +15 -0
  99. data/test/unit/associations_test.rb +726 -0
  100. data/test/unit/inheritance_column_test.rb +6 -6
  101. data/test/unit/model_test.rb +109 -125
  102. data/test/unit/protected_attrs_test.rb +4 -3
  103. data/test/unit/serializer_test.rb +6 -6
  104. data/test/unit/serializers/json_test.rb +17 -4
  105. data/test/unit/serializers/yaml_test.rb +5 -1
  106. data/test/unit/version_test.rb +87 -69
  107. metadata +172 -75
  108. data/gemfiles/3.0.gemfile +0 -42
  109. data/test/dummy/public/404.html +0 -26
  110. data/test/dummy/public/422.html +0 -26
  111. data/test/dummy/public/500.html +0 -26
  112. data/test/dummy/public/favicon.ico +0 -0
  113. data/test/dummy/public/javascripts/application.js +0 -2
  114. data/test/dummy/public/javascripts/controls.js +0 -965
  115. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  116. data/test/dummy/public/javascripts/effects.js +0 -1123
  117. data/test/dummy/public/javascripts/prototype.js +0 -6001
  118. data/test/dummy/public/javascripts/rails.js +0 -175
  119. data/test/dummy/public/stylesheets/.gitkeep +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 342a6b8d2c4c6685107d14ae36f6711d7fe09d78
4
- data.tar.gz: f7d83394a6bb503a898509b26e9adf0f712c1dfb
3
+ metadata.gz: e7937d44654d4f77422ffe61d4bc2d81b7ed4026
4
+ data.tar.gz: 9ec1f8bc013795818775e58e1cd9c33e5dd36499
5
5
  SHA512:
6
- metadata.gz: d240d05e510adf0a4e178a27cda8e490b99750384a15af935daa6019f02f8ce944c7894fdea76fb970a07dde7bdbbf62de8c314a24f5684f961d8da949fe3b64
7
- data.tar.gz: 078716e13039aebf41ea3c10e78180e78d1879ab30095a39298678bb88e6e5659a760f67449cbffee22492419943988df53eac04417fd8da8259804a08fe4732
6
+ metadata.gz: e546ad5e910cc2a08aeaaf0dca6754e05f3a89fbfb022b49a078025e09a623e359a552c2e52f3126c84662fad2cd804c34db709689211852ff43e32ff40db3a1
7
+ data.tar.gz: 0ef05118041333567e076bc8407330917cc36ec5d23aec22fd6dced9367b10a466c4284eccddbb3f4d0675c4154165f24097eafe4acf0135e133c426a2b71d4b
data/.gitignore CHANGED
@@ -12,6 +12,11 @@ pkg/*
12
12
  .bundle
13
13
  .rbenv-version
14
14
  Gemfile.lock
15
+ gemfiles/*.lock
15
16
  vendor/*
16
17
  .idea
17
18
  .rvmrc
19
+ .tags
20
+ .tags_sorted_by_file
21
+ .ruby-version
22
+ .ruby-gemset
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format progress
2
1
  --color
3
-
2
+ --require spec_helper
data/.travis.yml CHANGED
@@ -6,21 +6,28 @@ rvm:
6
6
  - jruby-19mode
7
7
  - jruby-18mode
8
8
  env:
9
- - DB=mysql
10
- - DB=postgres
11
- - DB=sqlite
9
+ global:
10
+ - TRAVIS=true
11
+ matrix:
12
+ - DB=mysql
13
+ - DB=postgres
14
+ - DB=sqlite
15
+
16
+ sudo: false
12
17
 
13
18
  before_script:
19
+ - mysql --version
14
20
  - sh -c "if [ \"$DB\" = 'mysql' ]; then mysql -e 'create database paper_trail_test;'; fi"
15
21
  - sh -c "if [ \"$DB\" = 'mysql' ]; then mysql -e 'create database paper_trail_bar; '; fi"
16
22
  - sh -c "if [ \"$DB\" = 'mysql' ]; then mysql -e 'create database paper_trail_foo; '; fi"
23
+ - psql --version
17
24
  - sh -c "if [ \"$DB\" = 'postgres' ]; then psql -c 'create database paper_trail_test;' -U postgres; fi"
18
25
  - sh -c "if [ \"$DB\" = 'postgres' ]; then psql -c 'create database paper_trail_bar;' -U postgres; fi"
19
26
  - sh -c "if [ \"$DB\" = 'postgres' ]; then psql -c 'create database paper_trail_foo;' -U postgres; fi"
20
27
 
21
28
  gemfile:
22
29
  - Gemfile
23
- - gemfiles/3.0.gemfile
30
+ - gemfiles/ar3.gemfile
24
31
 
25
32
  matrix:
26
33
  fast_finish: true
@@ -29,4 +36,6 @@ matrix:
29
36
  gemfile: Gemfile
30
37
  - rvm: 1.8.7
31
38
  gemfile: Gemfile
32
-
39
+
40
+ addons:
41
+ postgresql: "9.4"
data/CHANGELOG.md CHANGED
@@ -1,13 +1,220 @@
1
+ ## 4.2.0 (2016-05-31)
2
+
3
+ ### Breaking Changes
4
+
5
+ - None
6
+
7
+ ### Added
8
+
9
+ - [#808](https://github.com/airblade/paper_trail/pull/808) -
10
+ Warn when destroy callback is set to :after with ActiveRecord 5
11
+ option `belongs_to_required_by_default` set to `true`.
12
+
13
+ ### Fixed
14
+
15
+ - None
16
+
17
+ ## 4.1.0 (2016-01-30)
18
+
19
+ ### Breaking Changes
20
+
21
+ - None
22
+
23
+ ### Added
24
+
25
+ - A way to control the order of AR callbacks.
26
+ [#614](https://github.com/airblade/paper_trail/pull/614)
27
+ - Added `unversioned_attributes` option to `reify`.
28
+ [#579](https://github.com/airblade/paper_trail/pull/579)
29
+
30
+ ### Fixed
31
+
32
+ - None
33
+
34
+ ## 4.0.2
35
+
36
+ ### Breaking Changes
37
+
38
+ - None
39
+
40
+ ### Added
41
+
42
+ - None
43
+
44
+ ### Fixed
45
+
46
+ - [#696](https://github.com/airblade/paper_trail/issues/696) /
47
+ [#697](https://github.com/airblade/paper_trail/pull/697)
48
+ Bind JSON query parameters in `where_object` and `where_object_changes`.
49
+
50
+ ## 4.0.1
51
+
52
+ ### Breaking Changes
53
+
54
+ - None
55
+
56
+ ### Added
57
+
58
+ - None
59
+
60
+ ### Fixed
61
+
62
+ - [#636](https://github.com/airblade/paper_trail/issues/636) -
63
+ Should compile assets without a db connection
64
+ - [#589](https://github.com/airblade/paper_trail/pull/589) /
65
+ [#588](https://github.com/airblade/paper_trail/issues/588) -
66
+ Fixes timestamp for "create" versions
67
+
68
+ ## 4.0.0
69
+
70
+ This major release adds JSON column support in PostgreSQL, limited support for
71
+ versioning associations, various new configuration options, and a year's worth
72
+ of bug fixes. Thanks to everyone who helped test the two betas and two release
73
+ candidates.
74
+
75
+ ### Breaking Changes
76
+
77
+ - Using a Rails initializer to reopen PaperTrail::Version or otherwise extend
78
+ PaperTrail is no longer recommended. An alternative is described in the
79
+ readme. See https://github.com/airblade/paper_trail/pull/557 and
80
+ https://github.com/airblade/paper_trail/pull/492.
81
+ - If you depend on the `RSpec` or `Cucumber` helpers, you must
82
+ [require them in your test helper](https://github.com/airblade/paper_trail#testing).
83
+ - [#566](https://github.com/airblade/paper_trail/pull/566) - Removed deprecated
84
+ methods `paper_trail_on` and `paper_trail_off`. Use `paper_trail_on!` and
85
+ `paper_trail_off!` instead.
86
+ - [#458](https://github.com/airblade/paper_trail/pull/458) - Version metadata
87
+ (the `:meta` option) from AR attributes for `create` events will now save the
88
+ current value instead of `nil`.
89
+ - [#391](https://github.com/airblade/paper_trail/issues/391) - `object_changes`
90
+ value should dump to `YAML` as a normal `Hash` instead of an
91
+ `ActiveSupport::HashWithIndifferentAccess`.
92
+ - [#375](https://github.com/airblade/paper_trail/pull/375) /
93
+ [#374](https://github.com/airblade/paper_trail/issues/374) /
94
+ [#354](https://github.com/airblade/paper_trail/issues/354) /
95
+ [#131](https://github.com/airblade/paper_trail/issues/131) -
96
+ Versions are now saved with an `after_` callback, instead of a `before_`
97
+ callback. This ensures that the timestamp field for a version matches the
98
+ corresponding timestamp in the model.
99
+ - `3da1f104` - `PaperTrail.config` and `PaperTrail.configure` are now
100
+ identical: both return the `PaperTrail::Config` instance and also
101
+ yield it if a block is provided.
102
+
103
+ ### Added
104
+
105
+ - [#525](https://github.com/airblade/paper_trail/issues/525) /
106
+ [#512](https://github.com/airblade/paper_trail/pull/512) -
107
+ Support for virtual accessors and redefined setter and getter methods.
108
+ - [#518](https://github.com/airblade/paper_trail/pull/518) - Support for
109
+ querying against PostgreSQL's
110
+ [`JSON` and `JSONB` column types](http://www.postgresql.org/docs/9.4/static/datatype-json.html)
111
+ via `PaperTrail::VersionConcern#where_object` and
112
+ `PaperTrail::VersionConcern#where_object_changes`
113
+ - [#507](https://github.com/airblade/paper_trail/pull/507) -
114
+ New option: `:save_changes` controls whether or not to save changes to the
115
+ `object_changes` column (if it exists).
116
+ - [#500](https://github.com/airblade/paper_trail/pull/500) - Support for
117
+ passing an empty array to the `on` option (`on: []`) to disable all
118
+ automatic versioning.
119
+ - [#494](https://github.com/airblade/paper_trail/issues/494) - The install
120
+ generator will warn the user if the migration they are attempting to
121
+ generate already exists.
122
+ - [#484](https://github.com/airblade/paper_trail/pull/484) - Support for
123
+ [PostgreSQL's `JSONB` Type](http://www.postgresql.org/docs/9.4/static/datatype-json.html)
124
+ for storing `object` and `object_changes`.
125
+ - [#439](https://github.com/airblade/paper_trail/pull/439) /
126
+ [#12](https://github.com/airblade/paper_trail/issues/12) -
127
+ Support for versioning associations (has many, has one, etc.) one level deep.
128
+ - [#420](https://github.com/airblade/paper_trail/issues/420) - Add
129
+ `VersionConcern#where_object_changes` instance method; acts as a helper for
130
+ querying against the `object_changes` column in versions table.
131
+ - [#416](https://github.com/airblade/paper_trail/issues/416) - Added a
132
+ `config` option for enabling/disabling utilization of
133
+ `serialized_attributes` for `ActiveRecord`, necessary because
134
+ `serialized_attributes` has been deprecated in `ActiveRecord` version `4.2`
135
+ and will be removed in version `5.0`
136
+ - [#399](https://github.com/airblade/paper_trail/pull/399) - Add `:dup`
137
+ argument for options hash to `reify` which forces a new model instance.
138
+ - [#394](https://github.com/airblade/paper_trail/pull/394) - Add RSpec matcher
139
+ `have_a_version_with` for easier testing.
140
+ - [#347](https://github.com/airblade/paper_trail/pull/347) - Autoload
141
+ `ActiveRecord` models in via a `Rails::Engine` when the gem is used with
142
+ `Rails`.
143
+
144
+ ### Fixed
145
+
146
+ - [#563](https://github.com/airblade/paper_trail/pull/563) - Fixed a bug in
147
+ `touch_with_version` so that it will still create a version even when the
148
+ `on` option is, e.g. `[:create]`.
149
+ - [#541](https://github.com/airblade/paper_trail/pull/541) -
150
+ `PaperTrail.config.enabled` should be Thread Safe
151
+ - [#451](https://github.com/airblade/paper_trail/issues/451) - Fix `reify`
152
+ method in context of model where the base class has a default scope, and the
153
+ live instance is not scoped within that default scope.
154
+ - [#440](https://github.com/airblade/paper_trail/pull/440) - `versions`
155
+ association should clear/reload after a transaction rollback.
156
+ - [#438](https://github.com/airblade/paper_trail/issues/438) -
157
+ `ModelKlass.paper_trail_enabled_for_model?` should return `false` if
158
+ `has_paper_trail` has not been declared on the class.
159
+ - [#404](https://github.com/airblade/paper_trail/issues/404) /
160
+ [#428](https://github.com/airblade/paper_trail/issues/428) -
161
+ `model_instance.dup` does not need to be invoked when examining what the
162
+ instance looked like before changes were persisted, which avoids issues if a
163
+ 3rd party has overriden the `dup` behavior. Also fixes errors occuring when
164
+ a user attempts to update the inheritance column on an STI model instance in
165
+ `ActiveRecord` 4.1.x
166
+ - [#427](https://github.com/airblade/paper_trail/pull/427) - Fix `reify`
167
+ method in context of model where a column has been removed.
168
+ - [#414](https://github.com/airblade/paper_trail/issues/414) - Fix
169
+ functionality `ignore` argument to `has_paper_trail` in `ActiveRecord` 4.
170
+ - [#413](https://github.com/airblade/paper_trail/issues/413) - Utilize
171
+ [RequestStore](https://github.com/steveklabnik/request_store) to ensure that
172
+ the `PaperTrail.whodunnit` is set in a thread safe manner within Rails and
173
+ Sinatra.
174
+ - [#381](https://github.com/airblade/paper_trail/issues/381) - Fix `irb`
175
+ warning: `can't alias context from irb_context`. `Rspec` and `Cucumber`
176
+ helpers should not be loaded by default, regardless of whether those
177
+ libraries are loaded.
178
+ - [#248](https://github.com/airblade/paper_trail/issues/248) - In MySQL, to
179
+ prevent truncation, generated migrations now use `longtext` instead of `text`.
180
+ - Methods handling serialized attributes should fallback to the currently set
181
+ Serializer instead of always falling back to `PaperTrail::Serializers::YAML`.
182
+
183
+ ### Deprecated
184
+
185
+ - [#479](https://github.com/airblade/paper_trail/issues/479) - Deprecated
186
+ `originator` method, use `paper_trail_originator`.
187
+
188
+ ## 3.0.9
189
+
190
+ - [#479](https://github.com/airblade/paper_trail/issues/479) - Deprecated
191
+ `originator` method in favor of `paper_trail_originator` Deprecation warning
192
+ informs users that the `originator` of the methods will be removed in
193
+ version `4.0`. (Backported from v4)
194
+ - Updated deprecation warnings for `Model.paper_trail_on` and
195
+ `Model.paper_trail_off` to have display correct version number the methods
196
+ will be removed (`4.0`)
197
+
198
+ ## 3.0.8
199
+
200
+ - [#525](https://github.com/airblade/paper_trail/issues/525) / [#512](https://github.com/airblade/paper_trail/pull/512) -
201
+ Support for virtual accessors and redefined setter and getter methods.
202
+
203
+ ## 3.0.7
204
+
205
+ - [#404](https://github.com/airblade/paper_trail/issues/404) / [#428](https://github.com/airblade/paper_trail/issues/428) -
206
+ Fix errors occuring when a user attempts to update the inheritance column on an STI model instance in `ActiveRecord` 4.1.x
207
+
1
208
  ## 3.0.6
2
209
 
3
- - [#414](https://github.com/airblade/paper_trail/issues/414) - Fix functionality `ignore` argument to `has_paper_trail`
4
- in `ActiveRecord` 4.
210
+ - [#414](https://github.com/airblade/paper_trail/issues/414) - Backport fix for `ignore` argument to `has_paper_trail` in
211
+ `ActiveRecord` 4.
5
212
 
6
213
  ## 3.0.5
7
214
 
8
- - [#401](https://github.com/airblade/paper_trail/issues/401) / [#406](https://github.com/airblade/paper_trail/issues/406)
215
+ - [#401](https://github.com/airblade/paper_trail/issues/401) / [#406](https://github.com/airblade/paper_trail/issues/406) -
9
216
  `PaperTrail::Version` class is not loaded via a `Rails::Engine`, even when the gem is used with in Rails. This feature has
10
- will be re-introduced in version `3.1.0`.
217
+ will be re-introduced in version `4.0`.
11
218
  - [#398](https://github.com/airblade/paper_trail/pull/398) - Only require the `RSpec` helper if `RSpec::Core` is required.
12
219
 
13
220
  ## 3.0.3
@@ -52,7 +259,7 @@ in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is use
52
259
  with Rails `4.1.0.rc1`.
53
260
  - [#334](https://github.com/airblade/paper_trail/pull/334) - Add small-scope `whodunnit` method to `PaperTrail::Model::InstanceMethods`.
54
261
  - [#329](https://github.com/airblade/paper_trail/issues/329) - Add `touch_with_version` method to `PaperTrail::Model::InstanceMethods`,
55
- to allow for generating a version `touch`ing a model.
262
+ to allow for generating a version while `touch`ing a model.
56
263
  - [#328](https://github.com/airblade/paper_trail/pull/328) / [#326](https://github.com/airblade/paper_trail/issues/326) /
57
264
  [#307](https://github.com/airblade/paper_trail/issues/307) - `Model.paper_trail_enabled_for_model?` and
58
265
  `model_instance.without_versioning` is now thread-safe.
@@ -63,8 +270,8 @@ in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is use
63
270
  - [#312](https://github.com/airblade/paper_trail/issues/312) - Fix RSpec `with_versioning` class level helper method.
64
271
  - `model_instance.without_versioning` now yields the `model_instance`, enabling syntax like this:
65
272
  `model_instance.without_versioning { |obj| obj.update_attributes(:name => 'value') }`.
66
- - Deprecated `Model.paper_trail_on` and `Model.paper_trail_off` in favor of bang versions of the methods. Deprecation warning
67
- informs users that the non-bang versions of the methods will be removed in version `3.1.0`.
273
+ - Deprecated `Model.paper_trail_on` and `Model.paper_trail_off` in favor of bang versions of the methods.
274
+ Deprecation warning informs users that the non-bang versions of the methods will be removed in version `4.0`
68
275
 
69
276
  ## 3.0.0
70
277
 
@@ -91,7 +298,7 @@ in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is use
91
298
  - [#216](https://github.com/airblade/paper_trail/pull/216) - Added helper & extension for [RSpec](https://github.com/rspec/rspec),
92
299
  and helper for [Cucumber](http://cukes.info).
93
300
  - [#212](https://github.com/airblade/paper_trail/pull/212) - Added `PaperTrail::Cleaner` module, useful for discarding draft versions.
94
- - [#207](https://github.com/airblade/paper_trail/issues/207) - Versions for `'create'` events are now created with `create!` instead of
301
+ - [#207](https://github.com/airblade/paper_trail/issues/207) - Versions for `'create'` events are now created with `create!` instead of
95
302
  `create` so that an exception gets raised if it is appropriate to do so.
96
303
  - [#199](https://github.com/airblade/paper_trail/pull/199) - Rails 4 compatibility.
97
304
  - [#165](https://github.com/airblade/paper_trail/pull/165) - Namespaced the `Version` class under the `PaperTrail` module.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,84 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in PaperTrail!
4
+
5
+ Ask usage questions on Stack Overflow:
6
+ http://stackoverflow.com/tags/papertrail
7
+
8
+ **Please do not use github issues to ask usage questions.**
9
+
10
+ On github, we appreciate bug reports, feature
11
+ suggestions, and especially pull requests.
12
+
13
+ Thanks, and happy (paper) trails :)
14
+
15
+ ## Reporting Bugs
16
+
17
+ Please use our [bug report template][1].
18
+
19
+ ## Development
20
+
21
+ Testing is a little awkward because the test suite:
22
+
23
+ 1. contains a rails app with three databases (test, foo, and bar)
24
+ 1. supports three different RDBMS': sqlite, mysql, and postgres
25
+
26
+ Run tests with sqlite:
27
+
28
+ ```
29
+ # Create the appropriate database config. file
30
+ rm test/dummy/config/database.yml
31
+ DB=sqlite bundle exec rake prepare
32
+
33
+ # If this is the first test run ever, create databases
34
+ cd test/dummy
35
+ RAILS_ENV=test bundle exec rake db:setup
36
+ RAILS_ENV=foo bundle exec rake db:setup
37
+ RAILS_ENV=bar bundle exec rake db:setup
38
+ cd ../..
39
+
40
+ # Run tests
41
+ DB=sqlite bundle exec rake
42
+ ```
43
+
44
+ Run tests with mysql:
45
+
46
+ ```
47
+ # Create the appropriate database config. file
48
+ rm test/dummy/config/database.yml
49
+ DB=mysql bundle exec rake prepare
50
+
51
+ # If this is the first test run ever, create databases
52
+ cd test/dummy
53
+ RAILS_ENV=test bundle exec rake db:setup
54
+ RAILS_ENV=foo bundle exec rake db:setup
55
+ RAILS_ENV=bar bundle exec rake db:setup
56
+ cd ../..
57
+
58
+ # Run tests
59
+ DB=mysql bundle exec rake
60
+ ```
61
+
62
+ Run tests with postgres:
63
+
64
+ ```
65
+ # Create the appropriate database config. file
66
+ rm test/dummy/config/database.yml
67
+ DB=postgres bundle exec rake prepare
68
+
69
+ # If this is the first test run ever, create databases.
70
+ # Unlike mysql, use create/migrate instead of setup.
71
+ cd test/dummy
72
+ RAILS_ENV=test bundle exec rake db:create
73
+ RAILS_ENV=test bundle exec rake db:migrate
74
+ RAILS_ENV=foo bundle exec rake db:create
75
+ RAILS_ENV=foo bundle exec rake db:migrate
76
+ RAILS_ENV=bar bundle exec rake db:create
77
+ RAILS_ENV=bar bundle exec rake db:migrate
78
+ cd ../..
79
+
80
+ # Run tests
81
+ DB=postgres bundle exec rake
82
+ ```
83
+
84
+ [1]: https://github.com/airblade/paper_trail/blob/master/doc/bug_report_template.rb
data/README.md CHANGED
@@ -1,99 +1,136 @@
1
- # PaperTrail [![Build Status](https://travis-ci.org/airblade/paper_trail.svg?branch=3.0-stable)](https://travis-ci.org/airblade/paper_trail) [![Dependency Status](https://img.shields.io/gemnasium/airblade/paper_trail.svg)](https://gemnasium.com/airblade/paper_trail)
2
-
3
- PaperTrail lets you track changes to your models' data. It's good for auditing or versioning. You can see how a model looked at any stage in its lifecycle, revert it to any version, and even undelete it after it's been destroyed.
4
-
5
- There's an excellent [RailsCast on implementing Undo with Paper Trail](http://railscasts.com/episodes/255-undo-with-paper-trail).
6
-
7
- ## Features
8
-
9
- * Stores every create, update and destroy (or only the lifecycle events you specify).
10
- * Does not store updates which don't change anything.
11
- * Allows you to specify attributes (by inclusion or exclusion) which must change for a Version to be stored.
12
- * Allows you to get at every version, including the original, even once destroyed.
13
- * Allows you to get at every version even if the schema has since changed.
14
- * Allows you to get at the version as of a particular time.
15
- * Option to automatically restore `has_one` associations as they were at the time.
16
- * Automatically records who was responsible via your controller. PaperTrail calls `current_user` by default, if it exists, but you can have it call any method you like.
17
- * Allows you to set who is responsible at model-level (useful for migrations).
18
- * Allows you to store arbitrary model-level metadata with each version (useful for filtering versions).
19
- * Allows you to store arbitrary controller-level information with each version, e.g. remote IP.
20
- * Can be turned off/on per class (useful for migrations).
21
- * Can be turned off/on per request (useful for testing with an external service).
22
- * Can be turned off/on globally (useful for testing).
23
- * No configuration necessary.
24
- * Stores everything in a single database table by default (generates migration for you), or can use separate tables for separate models.
25
- * Supports custom version classes so different models' versions can have different behaviour.
26
- * Supports custom name for versions association.
27
- * Thoroughly tested.
28
- * Threadsafe.
29
-
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)
30
45
 
31
46
  ## Compatibility
32
47
 
33
- Works with ActiveRecord 4 and ActiveRecord 3. Note: this code is on the `master` branch and tagged `v3.x`.
34
-
35
- Version 2 is on the branch named [`2.7-stable`](https://github.com/airblade/paper_trail/tree/2.7-stable) and is tagged `v2.x`, and works with Rails 3.
36
-
37
- The Rails 2.3 code is on the [`rails2`](https://github.com/airblade/paper_trail/tree/rails2) branch and tagged `v1.x`. These branches are both stable with their respective versions of Rails but will not have new features added/backported to them.
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 |
38
54
 
39
55
  ## Installation
40
56
 
41
- ### Rails 3 & 4
42
-
43
57
  1. Add PaperTrail to your `Gemfile`.
44
58
 
45
- `gem 'paper_trail', '~> 3.0.6'`
59
+ `gem 'paper_trail', '~> 4.2.0'`
46
60
 
47
- 2. Generate a migration which will add a `versions` table to your database.
61
+ 1. Add a `versions` table to your database.
48
62
 
49
- `bundle exec rails generate paper_trail:install`
50
-
51
- 3. Run the migration.
52
-
53
- `bundle exec rake db:migrate`
63
+ ```
64
+ bundle exec rails generate paper_trail:install
65
+ bundle exec rake db:migrate
66
+ ```
54
67
 
55
- 4. Add `has_paper_trail` to the models you want to track.
68
+ 1. Add `has_paper_trail` to the models you want to track.
56
69
 
57
- ### Sinatra
58
-
59
- In order to configure PaperTrail for usage with [Sinatra](http://www.sinatrarb.com),
60
- your `Sinatra` app must be using `ActiveRecord` 3 or 4. It is also recommended to use the
61
- [Sinatra ActiveRecord Extension](https://github.com/janko-m/sinatra-activerecord) or something similar for managing
62
- your applications `ActiveRecord` connection in a manner similar to the way `Rails` does. If using the aforementioned
63
- `Sinatra ActiveRecord Extension`, steps for setting up your app with PaperTrail will look something like this:
70
+ ## Basic Usage
64
71
 
65
- 1. Add PaperTrail to your `Gemfile`.
72
+ Add `has_paper_trail` to your model to record every `create`, `update`,
73
+ and `destroy`.
66
74
 
67
- `gem 'paper_trail', '~> 3.0.6'`
75
+ ```ruby
76
+ class Widget < ActiveRecord::Base
77
+ has_paper_trail
78
+ end
79
+ ```
68
80
 
69
- 2. Generate a migration to add a `versions` table to your database.
81
+ This gives you a `versions` method which returns the "paper trail" of changes to
82
+ your model.
70
83
 
71
- `bundle exec rake db:create_migration NAME=create_versions`
84
+ ```ruby
85
+ widget = Widget.find 42
86
+ widget.versions
87
+ # [<PaperTrail::Version>, <PaperTrail::Version>, ...]
88
+ ```
72
89
 
73
- 3. Copy contents of [create_versions.rb](https://raw.github.com/airblade/paper_trail/master/lib/generators/paper_trail/templates/create_versions.rb)
74
- into the `create_versions` migration that was generated into your `db/migrate` directory.
90
+ Once you have a version, you can find out what happened:
75
91
 
76
- 4. Run the migration.
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
+ ```
77
102
 
78
- `bundle exec rake db:migrate`
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.
79
107
 
80
- 5. Add `has_paper_trail` to the models you want to track.
108
+ ```ruby
109
+ widget = Widget.find 153
110
+ widget.name # 'Doobly'
81
111
 
112
+ # Add has_paper_trail to Widget model.
82
113
 
83
- PaperTrail provides a helper extension that acts similar to the controller mixin it provides for `Rails` applications.
114
+ widget.versions # []
115
+ widget.update_attributes :name => 'Wotsit'
116
+ widget.versions.last.reify.name # 'Doobly'
117
+ widget.versions.last.event # 'update'
118
+ ```
84
119
 
85
- It will set `PaperTrail.whodunnit` to whatever is returned by a method named `user_for_paper_trail` which you can define inside your Sinatra Application. (by default it attempts to invoke a method named `current_user`)
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.
86
124
 
87
- If you're using the modular [`Sinatra::Base`](http://www.sinatrarb.com/intro.html#Modular%20vs.%20Classic%20Style) style of application, you will need to register the extension:
125
+ Here's a helpful table showing what PaperTrail stores:
88
126
 
89
- ```ruby
90
- # bleh_app.rb
91
- require 'sinatra/base'
127
+ | *Event* | *create* | *update* | *destroy* |
128
+ | -------------- | -------- | -------- | --------- |
129
+ | *Model Before* | nil | widget | widget |
130
+ | *Model After* | widget | widget | nil |
92
131
 
93
- class BlehApp < Sinatra::Base
94
- register PaperTrail::Sinatra
95
- end
96
- ```
132
+ PaperTrail stores the values in the Model Before column. Most other
133
+ auditing/versioning plugins store the After column.
97
134
 
98
135
  ## API Summary
99
136
 
@@ -101,21 +138,23 @@ When you declare `has_paper_trail` in your model, you get these methods:
101
138
 
102
139
  ```ruby
103
140
  class Widget < ActiveRecord::Base
104
- has_paper_trail # you can pass various options here
141
+ has_paper_trail
105
142
  end
106
143
 
107
- # Returns this widget's versions. You can customise the name of the association.
144
+ # Returns this widget's versions. You can customise the name of the
145
+ # association.
108
146
  widget.versions
109
147
 
110
148
  # Return the version this widget was reified from, or nil if it is live.
111
149
  # You can customise the name of the method.
112
150
  widget.version
113
151
 
114
- # Returns true if this widget is the current, live one; or false if it is from a previous version.
152
+ # Returns true if this widget is the current, live one; or false if it is from
153
+ # a previous version.
115
154
  widget.live?
116
155
 
117
156
  # Returns who put the widget into its current state.
118
- widget.originator
157
+ widget.paper_trail_originator
119
158
 
120
159
  # Returns the widget (not a version) as it looked at the given timestamp.
121
160
  widget.version_at(timestamp)
@@ -126,7 +165,8 @@ widget.previous_version
126
165
  # Returns the widget (not a version) as it became next.
127
166
  widget.next_version
128
167
 
129
- # Generates a version for a `touch` event (`widget.touch` does NOT generate a version)
168
+ # Generates a version for a `touch` event (`widget.touch` does NOT generate a
169
+ # version)
130
170
  widget.touch_with_version
131
171
 
132
172
  # Turn PaperTrail off for all widgets.
@@ -135,8 +175,10 @@ Widget.paper_trail_off!
135
175
  # Turn PaperTrail on for all widgets.
136
176
  Widget.paper_trail_on!
137
177
 
138
- # Check wheter PaperTrail is enabled for all widgets
178
+ # Is PaperTrail enabled for Widget, the class?
139
179
  Widget.paper_trail_enabled_for_model?
180
+
181
+ # Is PaperTrail enabled for widget, the instance?
140
182
  widget.paper_trail_enabled_for_model?
141
183
  ```
142
184
 
@@ -146,8 +188,11 @@ And a `PaperTrail::Version` instance has these methods:
146
188
  # Returns the item restored from this version.
147
189
  version.reify(options = {})
148
190
 
191
+ # Return a new item from this version
192
+ version.reify(dup: true)
193
+
149
194
  # Returns who put the item into the state stored in this version.
150
- version.originator
195
+ version.paper_trail_originator
151
196
 
152
197
  # Returns who changed the item from the state it had in this version.
153
198
  version.terminator
@@ -165,6 +210,15 @@ version.index
165
210
 
166
211
  # Returns the event that caused this version (create|update|destroy).
167
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)
168
222
  ```
169
223
 
170
224
  In your controllers you can override these methods:
@@ -179,110 +233,70 @@ user_for_paper_trail
179
233
  info_for_paper_trail
180
234
  ```
181
235
 
182
- ## Basic Usage
236
+ ## Choosing Lifecycle Events To Monitor
183
237
 
184
- PaperTrail is simple to use. Just add 15 characters to a model to get a paper trail of every `create`, `update`, and `destroy`.
238
+ You can choose which events to track with the `on` option. For example, to
239
+ ignore `create` events:
185
240
 
186
241
  ```ruby
187
- class Widget < ActiveRecord::Base
188
- has_paper_trail
242
+ class Article < ActiveRecord::Base
243
+ has_paper_trail :on => [:update, :destroy]
189
244
  end
190
245
  ```
191
246
 
192
- This gives you a `versions` method which returns the paper trail of changes to your model.
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.
193
250
 
194
- ```ruby
195
- >> widget = Widget.find 42
196
- >> widget.versions # [<PaperTrail::Version>, <PaperTrail::Version>, ...]
197
- ```
198
-
199
- Once you have a version, you can find out what happened:
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.
200
256
 
201
257
  ```ruby
202
- >> v = widget.versions.last
203
- >> v.event # 'update' (or 'create' or 'destroy')
204
- >> v.whodunnit # '153' (if the update was via a controller and
205
- # the controller has a current_user method,
206
- # here returning the id of the current user)
207
- >> v.created_at # when the update occurred
208
- >> widget = v.reify # the widget as it was before the update;
209
- # would be nil for a create event
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'
210
269
  ```
211
270
 
212
- PaperTrail stores the pre-change version of the model, unlike some other auditing/versioning plugins, so you can retrieve the original version. This is useful when you start keeping a paper trail for models that already have records in the database.
213
-
214
- ```ruby
215
- >> widget = Widget.find 153
216
- >> widget.name # 'Doobly'
217
-
218
- # Add has_paper_trail to Widget model.
219
-
220
- >> widget.versions # []
221
- >> widget.update_attributes :name => 'Wotsit'
222
- >> widget.versions.last.reify.name # 'Doobly'
223
- >> widget.versions.last.event # 'update'
224
- ```
225
-
226
- This also means that PaperTrail does not waste space storing a version of the object as it currently stands. The `versions` method gives you previous versions; to get the current one just call a finder on your `Widget` model as usual.
227
-
228
- Here's a helpful table showing what PaperTrail stores:
229
-
230
- <table>
231
- <tr>
232
- <th>Event</th>
233
- <th>Model Before</th>
234
- <th>Model After</th>
235
- </tr>
236
- <tr>
237
- <td>create</td>
238
- <td>nil</td>
239
- <td>widget</td>
240
- </tr>
241
- <tr>
242
- <td>update</td>
243
- <td>widget</td>
244
- <td>widget'</td>
245
- <tr>
246
- <td>destroy</td>
247
- <td>widget</td>
248
- <td>nil</td>
249
- </tr>
250
- </table>
251
-
252
- PaperTrail stores the values in the Model Before column. Most other auditing/versioning plugins store the After column.
271
+ ### Controlling the Order of AR Callbacks
253
272
 
254
-
255
- ## Choosing Lifecycle Events To Monitor
256
-
257
- You can choose which events to track with the `on` option. For example, to ignore `create` events:
273
+ The `has_paper_trail` method installs AR callbacks. If you need to control
274
+ their order, use the `paper_trail_on_*` methods.
258
275
 
259
276
  ```ruby
260
277
  class Article < ActiveRecord::Base
261
- has_paper_trail :on => [:update, :destroy]
262
- end
263
- ```
264
278
 
265
- You may also have the `PaperTrail::Version` model save a custom string in it's `event` field instead of the typical `create`, `update`, `destroy`.
266
- PaperTrail supplies a custom accessor method called `paper_trail_event`, which it will attempt to use to fill the `event` field before
267
- falling back on one of the default events.
279
+ # Include PaperTrail, but do not add any callbacks yet. Passing the
280
+ # empty array to `:on` omits callbacks.
281
+ has_paper_trail :on => []
268
282
 
269
- ```ruby
270
- >> a = Article.create
271
- >> a.versions.size # 1
272
- >> a.versions.last.event # 'create'
273
- >> a.paper_trail_event = 'update title'
274
- >> a.update_attributes :title => 'My Title'
275
- >> a.versions.size # 2
276
- >> a.versions.last.event # 'update title'
277
- >> a.paper_trail_event = nil
278
- >> a.update_attributes :title => "Alternate"
279
- >> a.versions.size # 3
280
- >> a.versions.last.event # 'update'
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
281
288
  ```
282
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
+
283
295
  ## Choosing When To Save New Versions
284
296
 
285
- You can choose the conditions when to add new versions with the `if` and `unless` options. For example, to save versions only for US non-draft translations:
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:
286
300
 
287
301
  ```ruby
288
302
  class Translation < ActiveRecord::Base
@@ -302,16 +316,19 @@ class Article < ActiveRecord::Base
302
316
  end
303
317
  ```
304
318
 
305
- This means that changes to just the `title` or `rating` will not store another version of the article. It does not mean that the `title` and `rating` attributes will be ignored if some other change causes a new `PaperTrail::Version` to be created. For example:
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:
306
323
 
307
324
  ```ruby
308
- >> a = Article.create
309
- >> a.versions.length # 1
310
- >> a.update_attributes :title => 'My Title', :rating => 3
311
- >> a.versions.length # 1
312
- >> a.update_attributes :title => 'Greeting', :content => 'Hello'
313
- >> a.versions.length # 2
314
- >> a.previous_version.title # 'My Title'
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'
315
332
  ```
316
333
 
317
334
  Or, you can specify a list of all attributes you care about:
@@ -325,13 +342,13 @@ end
325
342
  This means that only changes to the `title` will save a version of the article:
326
343
 
327
344
  ```ruby
328
- >> a = Article.create
329
- >> a.versions.length # 1
330
- >> a.update_attributes :title => 'My Title'
331
- >> a.versions.length # 2
332
- >> a.update_attributes :content => 'Hello'
333
- >> a.versions.length # 2
334
- >> a.previous_version.content # nil
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
335
352
  ```
336
353
 
337
354
  The `:ignore` and `:only` options can also accept `Hash` arguments, where the :
@@ -342,26 +359,31 @@ class Article < ActiveRecord::Base
342
359
  end
343
360
  ```
344
361
 
345
- This means that if the `title` is not blank, then only changes to the `title` will save a version of the article:
362
+ This means that if the `title` is not blank, then only changes to the `title`
363
+ will save a version of the article:
346
364
 
347
365
  ```ruby
348
- >> a = Article.create
349
- >> a.versions.length # 1
350
- >> a.update_attributes :content => 'Hello'
351
- >> a.versions.length # 2
352
- >> a.update_attributes :title => 'My Title'
353
- >> a.versions.length # 3
354
- >> a.update_attributes :content => 'Hai'
355
- >> a.versions.length # 3
356
- >> a.previous_version.content # "Hello"
357
- >> a.update_attributes :title => 'Dif Title'
358
- >> a.versions.length # 4
359
- >> a.previous_version.content # "Hai"
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"
360
378
  ```
361
379
 
362
- Passing both `:ignore` and `:only` options will result in the article being saved if a changed attribute is included in `:only` but not in `:ignore`.
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`.
363
382
 
364
- You can skip fields altogether with the `:skip` option. As with `:ignore`, updates to these fields will not create a new `PaperTrail::Version`. In addition, these fields will not be included in the serialized version of the object whenever a new `PaperTrail::Version` is created.
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.
365
387
 
366
388
  For example:
367
389
 
@@ -371,226 +393,384 @@ class Article < ActiveRecord::Base
371
393
  end
372
394
  ```
373
395
 
374
- ## Reverting And Undeleting A Model
396
+ ## Turning PaperTrail Off/On
375
397
 
376
- PaperTrail makes reverting to a previous version easy:
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:
377
408
 
378
409
  ```ruby
379
- >> widget = Widget.find 42
380
- >> widget.update_attributes :name => 'Blah blah'
381
- # Time passes....
382
- >> widget = widget.previous_version # the widget as it was before the update
383
- >> widget.save # reverted
410
+ PaperTrail.enabled = false
384
411
  ```
385
412
 
386
- Alternatively you can find the version at a given time:
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)):
387
417
 
388
418
  ```ruby
389
- >> widget = widget.version_at(1.day.ago) # the widget as it was one day ago
390
- >> widget.save # reverted
419
+ # in config/environments/test.rb
420
+ config.after_initialize do
421
+ PaperTrail.enabled = false
422
+ end
391
423
  ```
392
424
 
393
- Note `version_at` gives you the object, not a version, so you don't need to call `reify`.
394
-
395
- Undeleting is just as simple:
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:
396
427
 
397
428
  ```ruby
398
- >> widget = Widget.find 42
399
- >> widget.destroy
400
- # Time passes....
401
- >> widget = PaperTrail::Version.find(153).reify # the widget as it was before destruction
402
- >> widget.save # the widget lives!
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
403
442
  ```
404
443
 
405
- In fact you could use PaperTrail to implement an undo system, though I haven't had the opportunity yet to do it myself. However [Ryan Bates has](http://railscasts.com/episodes/255-undo-with-paper-trail)!
444
+ And then use it in your tests like this:
406
445
 
446
+ ```ruby
447
+ test "something that needs versioning" do
448
+ with_versioning do
449
+ # your test
450
+ end
451
+ end
452
+ ```
407
453
 
408
- ## Navigating Versions
454
+ ### Per request
409
455
 
410
- You can call `previous_version` and `next_version` on an item to get it as it was/became. Note that these methods reify the item for you.
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`:
411
459
 
412
460
  ```ruby
413
- >> live_widget = Widget.find 42
414
- >> live_widget.versions.length # 4 for example
415
- >> widget = live_widget.previous_version # => widget == live_widget.versions.last.reify
416
- >> widget = widget.previous_version # => widget == live_widget.versions[-2].reify
417
- >> widget = widget.next_version # => widget == live_widget.versions.last.reify
418
- >> widget.next_version # live_widget
461
+ class ApplicationController < ActionController::Base
462
+ def paper_trail_enabled_for_controller
463
+ request.user_agent != 'Disable User-Agent'
464
+ end
465
+ end
419
466
  ```
420
467
 
421
- If instead you have a particular `version` of an item you can navigate to the previous and next versions.
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:
422
472
 
423
473
  ```ruby
424
- >> widget = Widget.find 42
425
- >> version = widget.versions[-2] # assuming widget has several versions
426
- >> previous = version.previous
427
- >> next = version.next
474
+ Widget.paper_trail_off!
428
475
  ```
429
476
 
430
- You can find out which of an item's versions yours is:
477
+ And on again like this:
431
478
 
432
479
  ```ruby
433
- >> current_version_number = version.index # 0-based
480
+ Widget.paper_trail_on!
434
481
  ```
435
482
 
436
- Finally, if you got an item by reifying one of its versions, you can navigate back to the version it came from:
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:
437
487
 
438
488
  ```ruby
439
- >> latest_version = Widget.find(42).versions.last
440
- >> widget = latest_version.reify
441
- >> widget.version == latest_version # true
489
+ @widget.without_versioning :destroy
442
490
  ```
443
491
 
444
- You can find out whether a model instance is the current, live one -- or whether it came instead from a previous version -- with `live?`:
492
+ Or a block:
445
493
 
446
494
  ```ruby
447
- >> widget = Widget.find 42
448
- >> widget.live? # true
449
- >> widget = widget.previous_version
450
- >> widget.live? # false
495
+ @widget.without_versioning do
496
+ @widget.update_attributes :name => 'Ford'
497
+ end
451
498
  ```
452
499
 
453
- ## Finding Out Who Was Responsible For A Change
500
+ ## Limiting the Number of Versions Created
454
501
 
455
- If your `ApplicationController` has a `current_user` method, PaperTrail will store the value it returns in the version's `whodunnit` column. Note that this column is of type `String`, so you will have to convert it to an integer if it's an id and you want to look up the user later on:
502
+ Configure `version_limit` to cap the number of versions saved per record. This
503
+ does not apply to `create` events.
456
504
 
457
505
  ```ruby
458
- >> last_change = widget.versions.last
459
- >> user_who_made_the_change = User.find last_change.whodunnit.to_i
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
460
510
  ```
461
511
 
462
- You may want PaperTrail to call a different method to find out who is responsible. To do so, override the `user_for_paper_trail` method in your controller like this:
512
+ ## Reverting And Undeleting A Model
513
+
514
+ PaperTrail makes reverting to a previous version easy:
463
515
 
464
516
  ```ruby
465
- class ApplicationController
466
- def user_for_paper_trail
467
- logged_in? ? current_member.id : 'Public user' # or whatever
468
- end
469
- end
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
470
522
  ```
471
523
 
472
- In a console session you can manually set who is responsible like this:
524
+ Alternatively you can find the version at a given time:
473
525
 
474
526
  ```ruby
475
- >> PaperTrail.whodunnit = 'Andy Stewart'
476
- >> widget.update_attributes :name => 'Wibble'
477
- >> widget.versions.last.whodunnit # Andy Stewart
527
+ widget = widget.version_at(1.day.ago) # the widget as it was one day ago
528
+ widget.save # reverted
478
529
  ```
479
530
 
480
- You can avoid having to do this manually by setting your initializer to pick up the username of the current user from the OS, like this:
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:
481
535
 
482
536
  ```ruby
483
- # config/initializers/paper_trail.rb
484
- if defined?(::Rails::Console)
485
- PaperTrail.whodunnit = "#{`whoami`.strip}: console"
486
- elsif File.basename($0) == "rake"
487
- PaperTrail.whodunnit = "#{`whoami`.strip}: rake #{ARGV.join ' '}"
488
- end
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!
489
542
  ```
490
543
 
491
- Sometimes you want to define who is responsible for a change in a small scope without overwriting value of `PaperTrail.whodunnit`. It is possible to define the `whodunnit` value for an operation inside a block like this:
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.
492
553
 
493
554
  ```ruby
494
- >> PaperTrail.whodunnit = 'Andy Stewart'
495
- >> widget.whodunnit('Lucas Souza') do
496
- >> widget.update_attributes :name => 'Wibble'
497
- >> end
498
- >> widget.versions.last.whodunnit # Lucas Souza
499
- >> widget.update_attributes :name => 'Clair'
500
- >> widget.versions.last.whodunnit # Andy Stewart
501
- >> widget.whodunnit('Ben Atkins') { |w| w.update_attributes :name => 'Beth' } # this syntax also works
502
- >> widget.versions.last.whodunnit # Ben Atkins
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
503
561
  ```
504
562
 
505
- A version's `whodunnit` records who changed the object causing the `version` to be stored. Because a version stores the object as it looked before the change (see the table above), `whodunnit` returns who stopped the object looking like this -- not who made it look like this. Hence `whodunnit` is aliased as `terminator`.
563
+ If instead you have a particular `version` of an item you can navigate to the
564
+ previous and next versions.
506
565
 
507
- To find out who made a version's object look that way, use `version.originator`. And to find out who made a "live" object look like it does, use `originator` on the object.
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:
508
574
 
509
575
  ```ruby
510
- >> widget = Widget.find 153 # assume widget has 0 versions
511
- >> PaperTrail.whodunnit = 'Alice'
512
- >> widget.update_attributes :name => 'Yankee'
513
- >> widget.originator # 'Alice'
514
- >> PaperTrail.whodunnit = 'Bob'
515
- >> widget.update_attributes :name => 'Zulu'
516
- >> widget.originator # 'Bob'
517
- >> first_version, last_version = widget.versions.first, widget.versions.last
518
- >> first_version.whodunnit # 'Alice'
519
- >> first_version.originator # nil
520
- >> first_version.terminator # 'Alice'
521
- >> last_version.whodunnit # 'Bob'
522
- >> last_version.originator # 'Alice'
523
- >> last_version.terminator # 'Bob'
576
+ current_version_number = version.index # 0-based
524
577
  ```
525
578
 
526
- ## Custom Version Classes
579
+ If you got an item by reifying one of its versions, you can navigate back to the
580
+ version it came from:
527
581
 
528
- You can specify custom version subclasses with the `:class_name` option:
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?`:
529
590
 
530
591
  ```ruby
531
- class PostVersion < PaperTrail::Version
532
- # custom behaviour, e.g:
533
- self.table_name = :post_versions
534
- end
592
+ widget = Widget.find 42
593
+ widget.live? # true
594
+ widget = widget.previous_version
595
+ widget.live? # false
596
+ ```
535
597
 
536
- class Post < ActiveRecord::Base
537
- has_paper_trail :class_name => 'PostVersion'
538
- end
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")
539
603
  ```
540
604
 
541
- This allows you to store each model's versions in a separate table, which is useful if you have a lot of versions being created.
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:
542
648
 
543
- If you are using Postgres, you should also define the sequence that your custom version class will use:
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
+ ```
544
679
 
545
680
  ```ruby
546
- class PostVersion < PaperTrail::Version
547
- self.table_name = :post_versions
548
- self.sequence_name = :post_version_id_seq
549
- end
681
+ PaperTrail::Version.delete_all ["created_at < ?", 1.week.ago]
550
682
  ```
551
683
 
552
- Alternatively you could store certain metadata for one type of version, and other metadata for other versions.
684
+ ## Finding Out Who Was Responsible For A Change
553
685
 
554
- If you only use custom version classes and don't use PaperTrail's built-in one, on Rails `>= 3.2` you must:
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.
555
689
 
556
- - either declare the `PaperTrail::Version` class to be abstract like this (in an initializer):
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:
557
693
 
558
694
  ```ruby
559
- PaperTrail::Version.module_eval do
560
- self.abstract_class = true
695
+ class ApplicationController
696
+ def user_for_paper_trail
697
+ logged_in? ? current_member.id : 'Public user' # or whatever
698
+ end
561
699
  end
562
700
  ```
563
701
 
564
- - or create a `versions` table in the database so Rails can instantiate the `PaperTrail::Version` superclass.
565
-
566
- You can also specify custom names for the versions and version associations. This is useful if you already have `versions` or/and `version` methods on your model. For example:
702
+ In a console session you can manually set who is responsible like this:
567
703
 
568
704
  ```ruby
569
- class Post < ActiveRecord::Base
570
- has_paper_trail :versions => :paper_trail_versions,
571
- :version => :paper_trail_version
705
+ PaperTrail.whodunnit = 'Andy Stewart'
706
+ widget.update_attributes :name => 'Wibble'
707
+ widget.versions.last.whodunnit # Andy Stewart
708
+ ```
572
709
 
573
- # Existing versions method. We don't want to clash.
574
- def versions
575
- ...
576
- end
577
- # Existing version method. We don't want to clash.
578
- def version
579
- ...
580
- end
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'
581
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'
582
753
  ```
583
754
 
584
755
  ## Associations
585
756
 
586
- I haven't yet found a good way to get PaperTrail to automatically restore associations when you reify a model. See [here for a little more info](http://airbladesoftware.com/notes/undo-and-redo-with-papertrail).
587
-
588
- If you can think of a good way to achieve this, please let me know.
589
-
757
+ **Experimental feature**, see caveats below.
590
758
 
591
- ## Has-One Associations
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.
592
770
 
593
- PaperTrail can restore `:has_one` associations as they were at (actually, 3 seconds before) the time.
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:
594
774
 
595
775
  ```ruby
596
776
  class Location < ActiveRecord::Base
@@ -603,32 +783,90 @@ class Treasure < ActiveRecord::Base
603
783
  has_paper_trail
604
784
  end
605
785
 
606
- >> treasure.amount # 100
607
- >> treasure.location.latitude # 12.345
786
+ treasure.amount # 100
787
+ treasure.location.latitude # 12.345
608
788
 
609
- >> treasure.update_attributes :amount => 153
610
- >> treasure.location.update_attributes :latitude => 54.321
789
+ treasure.update_attributes :amount => 153
790
+ treasure.location.update_attributes :latitude => 54.321
611
791
 
612
- >> t = treasure.versions.last.reify(:has_one => true)
613
- >> t.amount # 100
614
- >> t.location.latitude # 12.345
792
+ t = treasure.versions.last.reify(:has_one => true)
793
+ t.amount # 100
794
+ t.location.latitude # 12.345
615
795
  ```
616
796
 
617
- The implementation is complicated by the edge case where the parent and child are updated in one go, e.g. in one web request or database transaction. PaperTrail doesn't know about different models being updated "together", so you can't ask it definitively to get the child as it was before the joint parent-and-child update.
618
-
619
- The correct solution is to make PaperTrail aware of requests or transactions (c.f. [Efficiency's transaction ID middleware](http://github.com/efficiency20/ops_middleware/blob/master/lib/e20/ops/middleware/transaction_id_middleware.rb)). In the meantime we work around the problem by finding the child as it was a few seconds before the parent was updated. By default we go 3 seconds before but you can change this by passing the desired number of seconds to the `:has_one` option:
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).
620
800
 
621
801
  ```ruby
622
- >> t = treasure.versions.last.reify(:has_one => 1) # look back 1 second instead of 3
623
- ```
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
624
809
 
625
- If you are shuddering, take solace from knowing PaperTrail opts out of these shenanigans by default. This means your `:has_one` associated objects will be the live ones, not the ones the user saw at the time. Since PaperTrail doesn't auto-restore `:has_many` associations (I can't get it to work) or `:belongs_to` (I ran out of time looking at `:has_many`), this at least makes your associations wrong consistently ;)
810
+ t = treasure.versions.last.reify(:has_one => true)
811
+ t.amount # 100
812
+ t.location.latitude # 12.345, instead of 54.321
813
+ ```
626
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].
627
822
 
823
+ ```ruby
824
+ class Widget < ActiveRecord::Base
825
+ has_paper_trail
826
+ has_one :wotsit, autosave: true
827
+ end
628
828
 
629
- ## Has-Many-Through Associations
829
+ class Wotsit < ActiveRecord::Base
830
+ has_paper_trail
831
+ belongs_to :widget
832
+ end
630
833
 
631
- PaperTrail can track most changes to the join table. Specifically it can track all additions but it can only track removals which fire the `after_destroy` callback on the join table. Here are some examples:
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:
632
870
 
633
871
  Given these models:
634
872
 
@@ -655,21 +893,23 @@ end
655
893
  Then each of the following will store authorship versions:
656
894
 
657
895
  ```ruby
658
- >> @book.authors << @dostoyevsky
659
- >> @book.authors.create :name => 'Tolstoy'
660
- >> @book.authorships.last.destroy
661
- >> @book.authorships.clear
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]
662
901
  ```
663
902
 
664
903
  But none of these will:
665
904
 
666
905
  ```ruby
667
- >> @book.authors.delete @tolstoy
668
- >> @book.author_ids = [@solzhenistyn.id, @dostoyevsky.id]
669
- >> @book.authors = []
906
+ @book.authors.delete @tolstoy
907
+ @book.author_ids = []
908
+ @book.authors = []
670
909
  ```
671
910
 
672
- Having said that, you can apparently get all these working (I haven't tested it myself) with this patch:
911
+ Having said that, you can apparently get all these working (I haven't tested it
912
+ myself) with this patch:
673
913
 
674
914
  ```ruby
675
915
  # In config/initializers/active_record_patch.rb
@@ -688,13 +928,9 @@ module ActiveRecord
688
928
  end
689
929
  ```
690
930
 
691
- See [issue 113](https://github.com/airblade/paper_trail/issues/113) for a discussion about this.
692
-
693
- There may be a way to store authorship versions, probably using association callbacks, no matter how the collection is manipulated but I haven't found it yet. Let me know if you do.
931
+ See [issue 113][16] for a discussion about this.
694
932
 
695
- There has been some discussion of how to implement PaperTrail to fully track HABTM associations. See [pull 90](https://github.com/airblade/paper_trail/pull/90) for an implementation that has worked for some.
696
-
697
- ## Storing metadata
933
+ ## Storing Metadata
698
934
 
699
935
  You can store arbitrary model-level metadata alongside each version like this:
700
936
 
@@ -710,33 +946,29 @@ class Article < ActiveRecord::Base
710
946
  end
711
947
  ```
712
948
 
713
- PaperTrail will call your proc with the current article and store the result in the `author_id` column of the `versions` table.
714
-
715
- N.B. You must also:
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.
716
952
 
717
- * Add your metadata columns to the `versions` table.
718
- * Declare your metadata columns using `attr_accessible`. (If you are using `ActiveRecord 3`, or `ActiveRecord 4` with the [ProtectedAttributes](https://github.com/rails/protected_attributes) gem)
953
+ ### Advantages of Metadata
719
954
 
720
- For example:
721
-
722
- ```ruby
723
- # config/initializers/paper_trail.rb
724
- module PaperTrail
725
- class Version < ActiveRecord::Base
726
- attr_accessible :author_id, :word_count, :answer
727
- end
728
- end
729
- ```
730
-
731
- Why would you do this? In this example, `author_id` is an attribute of `Article` and PaperTrail will store it anyway in a serialized form in the `object` column of the `version` record. But let's say you wanted to pull out all versions for a particular author; without the metadata you would have to deserialize (reify) each `version` object to see if belonged to the author in question. Clearly this is inefficient. Using the metadata you can find just those versions you want:
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:
732
962
 
733
963
  ```ruby
734
964
  PaperTrail::Version.where(:author_id => author_id)
735
965
  ```
736
966
 
737
- Note you can pass a symbol as a value in the `meta` hash to signal a method to call.
967
+ ### Metadata from Controllers
738
968
 
739
- You can also store any information you like from your controller. Just override the `info_for_paper_trail` method in your controller to return a hash whose keys correspond to columns in your `versions` table. E.g.:
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.
740
972
 
741
973
  ```ruby
742
974
  class ApplicationController
@@ -746,215 +978,271 @@ class ApplicationController
746
978
  end
747
979
  ```
748
980
 
749
- Remember to add those extra columns to your `versions` table and use `attr_accessible` ;)
750
-
751
- **NOTE FOR RAILS 4:** If you're using [Strong Parameters](https://github.com/rails/strong_parameters) in Rails 4 and have *not* included the `protected_attributes` gem, there's no need to declare your metadata columns using `attr_accessible`.
981
+ ### Protected Attributes and Metadata
752
982
 
753
-
754
- ## Diffing Versions
755
-
756
- There are two scenarios: diffing adjacent versions and diffing non-adjacent versions.
757
-
758
- The best way to diff adjacent versions is to get PaperTrail to do it for you. If you add an `object_changes` text column to your `versions` table, either at installation time with the `rails generate paper_trail:install --with-changes` option or manually, PaperTrail will store the `changes` diff (excluding any attributes PaperTrail is ignoring) in each `update` version. You can use the `version.changeset` method to retrieve it. For example:
983
+ If you are using rails 3 or the [protected_attributes][17] gem you must declare
984
+ your metadata columns to be `attr_accessible`.
759
985
 
760
986
  ```ruby
761
- >> widget = Widget.create :name => 'Bob'
762
- >> widget.versions.last.changeset # {'name' => [nil, 'Bob']}
763
- >> widget.update_attributes :name => 'Robert'
764
- >> widget.versions.last.changeset # {'name' => ['Bob', 'Robert']}
765
- >> widget.destroy
766
- >> widget.versions.last.changeset # {}
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
767
994
  ```
768
995
 
769
- Note PaperTrail only stores the changes for creation and updates; it doesn't store anything when an object is destroyed.
996
+ If you're using [strong_parameters][18] instead of [protected_attributes][17]
997
+ then there is no need to use `attr_accessible`.
770
998
 
771
- Please be aware that PaperTrail doesn't use diffs internally. When I designed PaperTrail I wanted simplicity and robustness so I decided to make each version of an object self-contained. A version stores all of its object's data, not a diff from the previous version. This means you can delete any version without affecting any other.
772
-
773
- To diff non-adjacent versions you'll have to write your own code. These libraries may help:
774
-
775
- For diffing two strings:
999
+ ## Custom Version Classes
776
1000
 
777
- * [htmldiff](http://github.com/myobie/htmldiff): expects but doesn't require HTML input and produces HTML output. Works very well but slows down significantly on large (e.g. 5,000 word) inputs.
778
- * [differ](http://github.com/pvande/differ): expects plain text input and produces plain text/coloured/HTML/any output. Can do character-wise, word-wise, line-wise, or arbitrary-boundary-string-wise diffs. Works very well on non-HTML input.
779
- * [diff-lcs](https://github.com/halostatue/diff-lcs): old-school, line-wise diffs.
1001
+ You can specify custom version subclasses with the `:class_name` option:
780
1002
 
781
- For diffing two ActiveRecord objects:
1003
+ ```ruby
1004
+ class PostVersion < PaperTrail::Version
1005
+ # custom behaviour, e.g:
1006
+ self.table_name = :post_versions
1007
+ end
782
1008
 
783
- * [Jeremy Weiskotten's PaperTrail fork](http://github.com/jeremyw/paper_trail/blob/master/lib/paper_trail/has_paper_trail.rb#L151-156): uses ActiveSupport's diff to return an array of hashes of the changes.
784
- * [activerecord-diff](http://github.com/tim/activerecord-diff): rather like ActiveRecord::Dirty but also allows you to specify which columns to compare.
1009
+ class Post < ActiveRecord::Base
1010
+ has_paper_trail :class_name => 'PostVersion'
1011
+ end
1012
+ ```
785
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`).
786
1015
 
787
- ## Turning PaperTrail Off/On
1016
+ ### Advantages
788
1017
 
789
- Sometimes you don't want to store changes. Perhaps you are only interested in changes made by your users and don't need to store changes you make yourself in, say, a migration -- or when testing your application.
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.
790
1021
 
791
- You can turn PaperTrail on or off in three ways: globally, per request, or per class.
1022
+ ### Configuration
792
1023
 
793
- ### Globally
794
-
795
- On a global level you can turn PaperTrail off like this:
1024
+ If you are using Postgres, you should also define the sequence that your custom
1025
+ version class will use:
796
1026
 
797
1027
  ```ruby
798
- >> PaperTrail.enabled = false
1028
+ class PostVersion < PaperTrail::Version
1029
+ self.table_name = :post_versions
1030
+ self.sequence_name = :post_versions_id_seq
1031
+ end
799
1032
  ```
800
1033
 
801
- For example, you might want to disable PaperTrail in your Rails application's test environment to speed up your tests. This will do it (note: this gets done automatically for `RSpec` and `Cucumber`, please see the [Testing section](#testing)):
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`.
802
1037
 
803
1038
  ```ruby
804
- # in config/environments/test.rb
805
- config.after_initialize do
806
- PaperTrail.enabled = false
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
807
1045
  end
808
1046
  ```
809
1047
 
810
- If you disable PaperTrail in your test environment but want to enable it for specific tests, you can add a helper like this to your test helper:
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:
811
1051
 
812
1052
  ```ruby
813
- # in test/test_helper.rb
814
- def with_versioning
815
- was_enabled = PaperTrail.enabled?
816
- was_enabled_for_controller = PaperTrail.enabled_for_controller?
817
- PaperTrail.enabled = true
818
- PaperTrail.enabled_for_controller = true
819
- begin
820
- yield
821
- ensure
822
- PaperTrail.enabled = was_enabled
823
- PaperTrail.enabled_for_controller = was_enabled_for_controller
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
+ ...
824
1064
  end
825
1065
  end
826
1066
  ```
827
1067
 
828
- And then use it in your tests like this:
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:
829
1072
 
830
1073
  ```ruby
831
- test "something that needs versioning" do
832
- with_versioning do
833
- # your test
834
- end
835
- end
1074
+ PaperTrail.serializer = MyCustomSerializer
836
1075
  ```
837
1076
 
838
- ### Per request
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]
839
1082
 
840
- You can turn PaperTrail on or off per request by adding a `paper_trail_enabled_for_controller` method to your controller which returns `true` or `false`:
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:
841
1088
 
842
1089
  ```ruby
843
- class ApplicationController < ActionController::Base
844
- def paper_trail_enabled_for_controller
845
- request.user_agent != 'Disable User-Agent'
846
- end
1090
+ create_table :versions do |t|
1091
+ ...
1092
+ t.json :object # Full object changes
1093
+ t.json :object_changes # Optional column-level changes
1094
+ ...
847
1095
  end
848
1096
  ```
849
1097
 
850
- ### Per class
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
851
1102
 
852
- If you are about change some widgets and you don't want a paper trail of your changes, you can turn PaperTrail off like this:
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:
853
1107
 
854
1108
  ```ruby
855
- >> Widget.paper_trail_off!
856
- ```
1109
+ add_column :versions, :object, :new_object, :jsonb # or :json
857
1110
 
858
- And on again like this:
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
859
1115
 
860
- ```ruby
861
- >> Widget.paper_trail_on!
1116
+ remove_column :versions, :object
1117
+ rename_column :versions, :new_object, :object
862
1118
  ```
863
1119
 
864
- ### Per method call
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.
865
1124
 
866
- You can call a method without creating a new version using `without_versioning`. It takes either a method name as a symbol:
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.
867
1128
 
868
1129
  ```ruby
869
- @widget.without_versioning :destroy
1130
+ rename_column :versions, :object, :old_object
1131
+ add_column :versions, :object, :jsonb # or :json
870
1132
  ```
871
1133
 
872
- Or a block:
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.
873
1137
 
874
1138
  ```ruby
875
- @widget.without_versioning do
876
- @widget.update_attributes :name => 'Ford'
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)
877
1141
  end
878
1142
  ```
879
1143
 
880
- ## Using a custom serializer
881
-
882
- By default, PaperTrail stores your changes as a `YAML` dump. You can override this with the serializer config option:
1144
+ Finally, in another migration, remove the old column.
883
1145
 
884
1146
  ```ruby
885
- >> PaperTrail.serializer = MyCustomSerializer
1147
+ remove_column :versions, :old_object
886
1148
  ```
887
1149
 
888
- A valid serializer is a `module` (or `class`) that defines a `load` and `dump` method. These serializers are included in the gem for your convenience:
889
-
890
- * [YAML](https://github.com/airblade/paper_trail/blob/master/lib/paper_trail/serializers/yaml.rb) - Default
891
- * [JSON](https://github.com/airblade/paper_trail/blob/master/lib/paper_trail/serializers/json.rb)
1150
+ If you use the optional `object_changes` column, don't forget to convert it
1151
+ also, using the same technique.
892
1152
 
893
- ## Limiting the number of versions created per object instance
1153
+ #### Convert a Column from Text to JSON
894
1154
 
895
- If you are wary of your `versions` table growing to an unwieldy size, or just don't care to track more than a certain number of versions per object,
896
- there is a configuration option that can be set to cap the number of versions saved per object. Note that this value must be numeric, and it only applies to
897
- versions other than `create` events (which will always be preserved if they are stored).
898
-
899
- ```ruby
900
- # will make it so that a maximum of 4 versions will be stored for each object
901
- # (the 3 most recent ones plus a `create` event)
902
- >> PaperTrail.config.version_limit = 3
903
- # disables/removes the version limit
904
- >> PaperTrail.config.version_limit = nil
905
- ```
906
-
907
- ## Deleting Old Versions
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.
908
1159
 
909
- Over time your `versions` table will grow to an unwieldy size. Because each version is self-contained (see the Diffing section above for more) you can simply delete any records you don't want any more. For example:
1160
+ Using SQL:
910
1161
 
911
1162
  ```sql
912
- sql> delete from versions where created_at < 2010-06-01;
1163
+ alter table versions
1164
+ alter column object type jsonb
1165
+ using object::jsonb;
913
1166
  ```
914
1167
 
1168
+ Using ActiveRecord:
1169
+
915
1170
  ```ruby
916
- >> PaperTrail::Version.delete_all ["created_at < ?", 1.week.ago]
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
917
1180
  ```
918
1181
 
919
1182
  ## Testing
920
1183
 
921
- You may want to turn PaperTrail off to speed up your tests. See the [Turning PaperTrail Off/On](#turning-papertrail-offon) section above for tips on usage with `Test::Unit`.
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`.
922
1187
 
923
1188
  ### RSpec
924
1189
 
925
- PaperTrail provides a helper that works with [RSpec](https://github.com/rspec/rspec) to make it easier to control when `PaperTrail` is enabled
926
- during testing. By default, PaperTrail will be turned off for all tests.
927
- When you wish to enable PaperTrail for a test you can either wrap the test in a `with_versioning` block, or pass in `:versioning => true` option to a spec block, like so:
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:
928
1211
 
929
1212
  ```ruby
930
1213
  describe "RSpec test group" do
931
1214
  it 'by default, PaperTrail will be turned off' do
932
- PaperTrail.should_not be_enabled
1215
+ expect(PaperTrail).to_not be_enabled
933
1216
  end
934
1217
 
935
1218
  with_versioning do
936
1219
  it 'within a `with_versioning` block it will be turned on' do
937
- PaperTrail.should be_enabled
1220
+ expect(PaperTrail).to be_enabled
938
1221
  end
939
1222
  end
940
1223
 
941
1224
  it 'can be turned on at the `it` or `describe` level like this', :versioning => true do
942
- PaperTrail.should be_enabled
1225
+ expect(PaperTrail).to be_enabled
943
1226
  end
944
1227
  end
945
1228
  ```
946
1229
 
947
- The helper will also reset the `PaperTrail.whodunnit` value to `nil` before each test to help prevent data spillover between tests.
948
- If you are using PaperTrail with Rails, the helper will automatically set the `PaperTrail.controller_info` value to `{}` as well, again, to help prevent data spillover between tests.
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.
949
1234
 
950
- There is also a `be_versioned` matcher provided by PaperTrail's RSpec helper which can be leveraged like so:
1235
+ There is also a `be_versioned` matcher provided by PaperTrail's RSpec helper
1236
+ which can be leveraged like so:
951
1237
 
952
1238
  ```ruby
953
1239
  class Widget < ActiveRecord::Base
954
1240
  end
955
1241
 
956
1242
  describe Widget do
957
- it { should_not be_versioned }
1243
+ it "is not versioned by default" do
1244
+ is_expected.to_not be_versioned
1245
+ end
958
1246
 
959
1247
  describe "add versioning to the `Widget` class" do
960
1248
  before(:all) do
@@ -963,16 +1251,52 @@ describe Widget do
963
1251
  end
964
1252
  end
965
1253
 
966
- it { should be_versioned }
1254
+ it "enables paper trail" do
1255
+ is_expected.to be_versioned
1256
+ end
967
1257
  end
968
1258
  end
969
1259
  ```
970
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
+
971
1281
  ### Cucumber
972
1282
 
973
- PaperTrail provides a helper for [Cucumber](http://cukes.info) that works similar to the RSpec helper.
974
- By default, PaperTrail will be turned off for all scenarios by a `before` hook added by the helper.
975
- When you wish to enable PaperTrail for a scenario, you can wrap code in a `with_versioning` block in a step, like so:
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:
976
1300
 
977
1301
  ```ruby
978
1302
  Given /I want versioning on my model/ do
@@ -982,25 +1306,28 @@ Given /I want versioning on my model/ do
982
1306
  end
983
1307
  ```
984
1308
 
985
- The helper will also reset the `PaperTrail.whodunnit` value to `nil` before each test to help prevent data spillover between tests.
986
- If you are using PaperTrail with Rails, the helper will automatically set the `PaperTrail.controller_info` value to `{}` as well, again, to help prevent data spillover between tests.
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.
987
1313
 
988
1314
  ### Spork
989
1315
 
990
- If you wish to use the `RSpec` or `Cucumber` helpers with [Spork](https://github.com/sporkrb/spork), you will need to
991
- manually require the helper(s) in your `prefork` block on your test helper, like so:
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:
992
1319
 
993
1320
  ```ruby
994
- # spec/spec_helper.rb
1321
+ # spec/rails_helper.rb
995
1322
 
996
1323
  require 'spork'
997
1324
 
998
1325
  Spork.prefork do
999
1326
  # This file is copied to spec/ when you run 'rails generate rspec:install'
1000
1327
  ENV["RAILS_ENV"] ||= 'test'
1328
+ require 'spec_helper'
1001
1329
  require File.expand_path("../../config/environment", __FILE__)
1002
1330
  require 'rspec/rails'
1003
- require 'rspec/autorun'
1004
1331
  require 'paper_trail/frameworks/rspec'
1005
1332
  require 'paper_trail/frameworks/cucumber'
1006
1333
  ...
@@ -1009,13 +1336,15 @@ end
1009
1336
 
1010
1337
  ### Zeus or Spring
1011
1338
 
1012
- If you wish to use the `RSpec` or `Cucumber` helpers with [Zeus](https://github.com/burke/zeus) or [Spring](https://github.com/rails/spring), you will need to
1013
- manually require the helper(s) in your test helper, like so:
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:
1014
1342
 
1015
1343
  ```ruby
1016
- # spec/spec_helper.rb
1344
+ # spec/rails_helper.rb
1017
1345
 
1018
1346
  ENV["RAILS_ENV"] ||= 'test'
1347
+ require 'spec_helper'
1019
1348
  require File.expand_path("../../config/environment", __FILE__)
1020
1349
  require 'rspec/rails'
1021
1350
  require 'paper_trail/frameworks/rspec'
@@ -1023,9 +1352,13 @@ require 'paper_trail/frameworks/rspec'
1023
1352
 
1024
1353
  ## Testing PaperTrail
1025
1354
 
1026
- Paper Trail has facilities to test aganist Postgres, Mysql and SQLite. To switch between DB engines you will need to export the DB Variable for the engine you wish to test aganist.
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.
1027
1358
 
1028
- Though be aware we do not have the abilty to create the db's (except sqlite) for you. You can look at .travis.yml before_script for an example of how to create the db's needed.
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.
1029
1362
 
1030
1363
  ```
1031
1364
  export DB=postgres
@@ -1033,19 +1366,70 @@ export DB=mysql
1033
1366
  export DB=sqlite # this is default
1034
1367
  ```
1035
1368
 
1036
- ## Articles
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'`
1037
1381
 
1038
- * [Versioning with PaperTrail](http://www.sitepoint.com/versioning-papertrail), [Ilya Bodrov](http://www.sitepoint.com/author/ibodrov), 10th April 2014
1039
- * [Using PaperTrail to track stack traces](http://rubyrailsexpert.com/?p=36), T James Corcoran's blog, 1st October 2013.
1040
- * [RailsCast #255 - Undo with PaperTrail](http://railscasts.com/episodes/255-undo-with-paper-trail), 28th February 2011.
1041
- * [Keep a Paper Trail with PaperTrail](http://www.linux-mag.com/id/7528), Linux Magazine, 16th September 2009.
1382
+ 2. Generate a migration to add a `versions` table to your database.
1383
+
1384
+ `bundle exec rake db:create_migration NAME=create_versions`
1042
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.
1043
1428
 
1044
1429
  ## Problems
1045
1430
 
1046
1431
  Please use GitHub's [issue tracker](http://github.com/airblade/paper_trail/issues).
1047
1432
 
1048
-
1049
1433
  ## Contributors
1050
1434
 
1051
1435
  Many thanks to:
@@ -1101,15 +1485,51 @@ Many thanks to:
1101
1485
  * [Chulki Lee](https://github.com/chulkilee)
1102
1486
  * [Lucas Souza](https://github.com/lucasas)
1103
1487
  * [Russell Osborne](https://github.com/rposborne)
1104
-
1488
+ * [Ben Li](https://github.com/bli)
1489
+ * [Felix Liu](https://github.com/lyfeyaj)
1105
1490
 
1106
1491
  ## Inspirations
1107
1492
 
1108
1493
  * [Simply Versioned](http://github.com/github/simply_versioned)
1109
1494
  * [Acts As Audited](http://github.com/collectiveidea/acts_as_audited)
1110
1495
 
1111
-
1112
1496
  ## Intellectual Property
1113
1497
 
1114
1498
  Copyright (c) 2011 Andy Stewart (boss@airbladesoftware.com).
1115
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