paper_trail 2.6.4 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
+ language: ruby
1
2
  rvm:
2
- - 1.8.7
3
3
  - 1.9.2
4
+ - 1.8.7
4
5
  - ree
5
-
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2.7.0
2
+
3
+ - [#164](https://github.com/airblade/paper_trail/pull/164) - Allow for custom serializer for storage of object attributes.
4
+ - [#180](https://github.com/airblade/paper_trail/pull/180) - Store serialized representation of serialized attributes
5
+ on the `object_changes` column in the `Version` table.
6
+ - [#183](https://github.com/airblade/paper_trail/pull/183) - Fully qualify the `Version` class to help prevent
7
+ namespace resolution errors within other gems / plugins.
8
+
1
9
  ## 2.6.4
2
10
 
3
11
  - [#181](https://github.com/airblade/paper_trail/issues/181)/[#182](https://github.com/airblade/paper_trail/pull/182) -
data/README.md CHANGED
@@ -572,7 +572,7 @@ You can store arbitrary model-level metadata alongside each version like this:
572
572
  ```ruby
573
573
  class Article < ActiveRecord::Base
574
574
  belongs_to :author
575
- has_paper_trail :meta => { :author_id => Proc.new { |article| article.author_id },
575
+ has_paper_trail :meta => { :author_id => :author_id,
576
576
  :word_count => :count_words,
577
577
  :answer => 42 }
578
578
  def count_words
@@ -741,6 +741,16 @@ Or a block:
741
741
  end
742
742
  ```
743
743
 
744
+ ## Using a custom serializer
745
+
746
+ By default, PaperTrail stores your changes as a YAML dump. You can override this with the serializer config option:
747
+
748
+ ```ruby
749
+ >> PaperTrail.serializer = MyCustomSerializer
750
+ ```
751
+
752
+ The serializer needs to be a class that responds to a `load` and `dump` method.
753
+
744
754
  ## Deleting Old Versions
745
755
 
746
756
  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:
@@ -840,6 +850,16 @@ Many thanks to:
840
850
  * [Ben Woosley](https://github.com/Empact)
841
851
  * [Philip Arndt](https://github.com/parndt)
842
852
  * [Daniel Vydra](https://github.com/dvydra)
853
+ * [Byron Bowerman](https://github.com/BM5k)
854
+ * [Nicolas Buduroi](https://github.com/budu)
855
+ * [Pikender Sharma](https://github.com/pikender)
856
+ * [Paul Brannan](https://github.com/cout)
857
+ * [Ben Morrall](https://github.com/bmorrall)
858
+ * [Yves Senn](https://github.com/senny)
859
+ * [Ben Atkins](https://github.com/fullbridge-batkins)
860
+ * [Yves Senn](https://github.com/senny)
861
+ * [Tyler Rick](https://github.com/TylerRick)
862
+ * [Bradley Priest](https://github.com/bradleypriest)
843
863
 
844
864
 
845
865
  ## Inspirations
data/lib/paper_trail.rb CHANGED
@@ -4,6 +4,7 @@ require 'paper_trail/config'
4
4
  require 'paper_trail/controller'
5
5
  require 'paper_trail/has_paper_trail'
6
6
  require 'paper_trail/version'
7
+ require 'paper_trail/serializers/yaml'
7
8
 
8
9
  # PaperTrail's module methods can be called in both models and controllers.
9
10
  module PaperTrail
@@ -68,6 +69,9 @@ module PaperTrail
68
69
  paper_trail_store[:controller_info] = value
69
70
  end
70
71
 
72
+ def self.serializer
73
+ PaperTrail.config.serializer
74
+ end
71
75
 
72
76
  private
73
77
 
@@ -3,12 +3,12 @@ require 'singleton'
3
3
  module PaperTrail
4
4
  class Config
5
5
  include Singleton
6
- attr_accessor :enabled, :timestamp_field
7
-
6
+ attr_accessor :enabled, :timestamp_field, :serializer
7
+
8
8
  def initialize
9
- # Indicates whether PaperTrail is on or off.
10
- @enabled = true
9
+ @enabled = true # Indicates whether PaperTrail is on or off.
11
10
  @timestamp_field = :created_at
11
+ @serializer = PaperTrail::Serializers::Yaml
12
12
  end
13
13
  end
14
14
  end
@@ -39,7 +39,7 @@ module PaperTrail
39
39
  attr_accessor self.version_association_name
40
40
 
41
41
  class_attribute :version_class_name
42
- self.version_class_name = options[:class_name] || 'Version'
42
+ self.version_class_name = options[:class_name] || '::Version'
43
43
 
44
44
  class_attribute :paper_trail_options
45
45
  self.paper_trail_options = options.dup
@@ -80,6 +80,44 @@ module PaperTrail
80
80
  def paper_trail_on
81
81
  self.paper_trail_enabled_for_model = true
82
82
  end
83
+
84
+ # Used for Version#object attribute
85
+ def serialize_attributes(attributes)
86
+ serialized_attributes.each do |key, coder|
87
+ if attributes.key?(key)
88
+ attributes[key] = coder.dump(attributes[key])
89
+ end
90
+ end
91
+ end
92
+
93
+ def unserialize_attributes(attributes)
94
+ serialized_attributes.each do |key, coder|
95
+ if attributes.key?(key)
96
+ attributes[key] = coder.load(attributes[key])
97
+ end
98
+ end
99
+ end
100
+
101
+ # Used for Version#object_changes attribute
102
+ def serialize_attribute_changes(changes)
103
+ serialized_attributes.each do |key, coder|
104
+ if changes.key?(key)
105
+ old_value, new_value = changes[key]
106
+ changes[key] = [coder.dump(old_value),
107
+ coder.dump(new_value)]
108
+ end
109
+ end
110
+ end
111
+
112
+ def unserialize_attribute_changes(changes)
113
+ serialized_attributes.each do |key, coder|
114
+ if changes.key?(key)
115
+ old_value, new_value = changes[key]
116
+ changes[key] = [coder.load(old_value),
117
+ coder.load(new_value)]
118
+ end
119
+ end
120
+ end
83
121
  end
84
122
 
85
123
  # Wrap the following methods in a module so we can include them only in the
@@ -151,8 +189,7 @@ module PaperTrail
151
189
  }
152
190
 
153
191
  if changed_notably? and version_class.column_names.include?('object_changes')
154
- # The double negative (reject, !include?) preserves the hash structure of self.changes.
155
- data[:object_changes] = self.changes.reject { |k, _| !notably_changed.include?(k) }.to_yaml
192
+ data[:object_changes] = changes_for_paper_trail.to_yaml
156
193
  end
157
194
 
158
195
  send(self.class.versions_association_name).create merge_metadata(data)
@@ -167,15 +204,20 @@ module PaperTrail
167
204
  :whodunnit => PaperTrail.whodunnit
168
205
  }
169
206
  if version_class.column_names.include? 'object_changes'
170
- # The double negative (reject, !include?) preserves the hash structure of self.changes.
171
- data[:object_changes] = self.changes.reject do |key, value|
172
- !notably_changed.include?(key)
173
- end.to_yaml
207
+ data[:object_changes] = PaperTrail.serializer.dump(changes_for_paper_trail)
174
208
  end
175
209
  send(self.class.versions_association_name).build merge_metadata(data)
176
210
  end
177
211
  end
178
212
 
213
+ def changes_for_paper_trail
214
+ self.changes.keep_if do |key, value|
215
+ notably_changed.include?(key)
216
+ end.tap do |changes|
217
+ self.class.serialize_attribute_changes(changes) # Use serialized value for attributes when necessary
218
+ end
219
+ end
220
+
179
221
  def record_destroy
180
222
  if switched_on? and not new_record?
181
223
  version_class.create merge_metadata(:item_id => self.id,
@@ -221,7 +263,10 @@ module PaperTrail
221
263
  end
222
264
 
223
265
  def object_to_string(object)
224
- object.attributes.except(*self.class.paper_trail_options[:skip]).to_yaml
266
+ _attrs = object.attributes.except(*self.class.paper_trail_options[:skip]).tap do |attributes|
267
+ self.class.serialize_attributes attributes
268
+ end
269
+ PaperTrail.serializer.dump(_attrs)
225
270
  end
226
271
 
227
272
  def changed_notably?
@@ -0,0 +1,15 @@
1
+ require 'yaml'
2
+
3
+ module PaperTrail
4
+ module Serializers
5
+ class Yaml
6
+ def self.load(string)
7
+ YAML.load string
8
+ end
9
+
10
+ def self.dump(hash)
11
+ YAML.dump hash
12
+ end
13
+ end
14
+ end
15
+ end
@@ -55,7 +55,7 @@ class Version < ActiveRecord::Base
55
55
  options.reverse_merge! :has_one => false
56
56
 
57
57
  unless object.nil?
58
- attrs = YAML::load object
58
+ attrs = PaperTrail.serializer.load object
59
59
 
60
60
  # Normally a polymorphic belongs_to relationship allows us
61
61
  # to get the object we belong to by calling, in this case,
@@ -79,6 +79,7 @@ class Version < ActiveRecord::Base
79
79
  model = klass.new
80
80
  end
81
81
 
82
+ model.class.unserialize_attributes attrs
82
83
  attrs.each do |k, v|
83
84
  if model.respond_to?("#{k}=")
84
85
  model.send :write_attribute, k.to_sym, v
@@ -103,7 +104,9 @@ class Version < ActiveRecord::Base
103
104
  def changeset
104
105
  if self.class.column_names.include? 'object_changes'
105
106
  if changes = object_changes
106
- HashWithIndifferentAccess[YAML::load(changes)]
107
+ HashWithIndifferentAccess[PaperTrail.serializer.load(changes)].tap do |changes|
108
+ item_type.constantize.unserialize_attribute_changes(changes)
109
+ end
107
110
  else
108
111
  {}
109
112
  end
@@ -1,3 +1,3 @@
1
1
  module PaperTrail
2
- VERSION = '2.6.4'
2
+ VERSION = '2.7.0'
3
3
  end
@@ -2,4 +2,27 @@ class Person < ActiveRecord::Base
2
2
  has_many :authorships, :dependent => :destroy
3
3
  has_many :books, :through => :authorships
4
4
  has_paper_trail
5
+
6
+ # Convert strings to TimeZone objects when assigned
7
+ def time_zone=(value)
8
+ if value.is_a? ActiveSupport::TimeZone
9
+ super
10
+ else
11
+ zone = ::Time.find_zone(value) # nil if can't find time zone
12
+ super zone
13
+ end
14
+ end
15
+
16
+ # Store TimeZone objects as strings when serialized to database
17
+ class TimeZoneSerializer
18
+ def dump(zone)
19
+ zone.try(:name)
20
+ end
21
+
22
+ def load(value)
23
+ ::Time.find_zone!(value) rescue nil
24
+ end
25
+ end
26
+
27
+ serialize :time_zone, TimeZoneSerializer.new
5
28
  end
@@ -80,6 +80,7 @@ class SetUpTestTables < ActiveRecord::Migration
80
80
 
81
81
  create_table :people, :force => true do |t|
82
82
  t.string :name
83
+ t.string :time_zone
83
84
  end
84
85
 
85
86
  create_table :songs, :force => true do |t|
@@ -49,11 +49,11 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
49
49
  end
50
50
 
51
51
  should 'have removed the skipped attributes when saving the previous version' do
52
- assert_equal nil, YAML::load(@old_article.object)['file_upload']
52
+ assert_equal nil, PaperTrail.serializer.load(@old_article.object)['file_upload']
53
53
  end
54
54
 
55
55
  should 'have kept the non-skipped attributes in the previous version' do
56
- assert_equal 'Some text here.', YAML::load(@old_article.object)['content']
56
+ assert_equal 'Some text here.', PaperTrail.serializer.load(@old_article.object)['content']
57
57
  end
58
58
  end
59
59
  end
@@ -201,7 +201,7 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
201
201
  end
202
202
 
203
203
  should 'have stored changes' do
204
- assert_equal ({'name' => ['Henry', 'Harry']}), YAML::load(@widget.versions.last.object_changes)
204
+ assert_equal ({'name' => ['Henry', 'Harry']}), PaperTrail.serializer.load(@widget.versions.last.object_changes)
205
205
  assert_equal ({'name' => ['Henry', 'Harry']}), @widget.versions.last.changeset
206
206
  end
207
207
 
@@ -916,6 +916,90 @@ class HasPaperTrailModelTest < ActiveSupport::TestCase
916
916
  end
917
917
  end
918
918
 
919
+ context 'When an attribute has a custom serializer' do
920
+ setup { @person = Person.new(:time_zone => "Samoa") }
921
+
922
+ should "be an instance of ActiveSupport::TimeZone" do
923
+ assert_equal ActiveSupport::TimeZone, @person.time_zone.class
924
+ end
925
+
926
+ context 'when the model is saved' do
927
+ setup do
928
+ @changes_before_save = @person.changes.dup
929
+ @person.save!
930
+ end
931
+
932
+ # Test for serialization:
933
+ should 'version.object_changes should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
934
+ assert @person.versions.last.object_changes.length < 105, "object_changes length was #{@person.versions.last.object_changes.length}"
935
+ end
936
+ # It should store the serialized value.
937
+ should 'version.object_changes attribute should have stored the value returned by the attribute serializer' do
938
+ as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object_changes)]
939
+ assert_equal [nil, 'Samoa'], as_stored_in_version[:time_zone]
940
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].serialized_value, as_stored_in_version[:time_zone].last
941
+ end
942
+
943
+ # Tests for unserialization:
944
+ should 'version.changeset should convert the attribute value back to its original, unserialized value' do
945
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].unserialized_value, @person.versions.last.changeset[:time_zone].last
946
+ end
947
+ should "record.changes (before save) returns the original, unserialized values" do
948
+ assert_equal [NilClass, ActiveSupport::TimeZone], @changes_before_save[:time_zone].map(&:class)
949
+ end
950
+ should 'version.changeset should be the same as record.changes was before the save' do
951
+ assert_equal @changes_before_save, @person.versions.last.changeset.delete_if { |key, val| key.to_sym == :id }
952
+ assert_equal [NilClass, ActiveSupport::TimeZone], @person.versions.last.changeset[:time_zone].map(&:class)
953
+ end
954
+
955
+ context 'when that attribute is updated' do
956
+ setup do
957
+ @attribute_value_before_change = @person.instance_variable_get(:@attributes)['time_zone']
958
+ @person.assign_attributes({ :time_zone => 'Pacific Time (US & Canada)' })
959
+ @changes_before_save = @person.changes.dup
960
+ @person.save!
961
+ end
962
+
963
+ # Tests for serialization:
964
+ # Before the serialized attributes fix, the object/object_changes value that was stored was ridiculously long (58723).
965
+ should 'version.object should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
966
+ assert @person.versions.last.object. length < 105, "object length was #{@person.versions.last.object .length}"
967
+ end
968
+ should 'version.object_changes should not have stored the default, ridiculously long (to_yaml) serialization of the TimeZone object' do
969
+ assert @person.versions.last.object_changes.length < 105, "object_changes length was #{@person.versions.last.object_changes.length}"
970
+ end
971
+ # But now it stores the short, serialized value.
972
+ should 'version.object attribute should have stored the value returned by the attribute serializer' do
973
+ as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object)]
974
+ assert_equal 'Samoa', as_stored_in_version[:time_zone]
975
+ assert_equal @attribute_value_before_change.serialized_value, as_stored_in_version[:time_zone]
976
+ end
977
+ should 'version.object_changes attribute should have stored the value returned by the attribute serializer' do
978
+ as_stored_in_version = HashWithIndifferentAccess[YAML::load(@person.versions.last.object_changes)]
979
+ assert_equal ['Samoa', 'Pacific Time (US & Canada)'], as_stored_in_version[:time_zone]
980
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].serialized_value, as_stored_in_version[:time_zone].last
981
+ end
982
+
983
+ # Tests for unserialization:
984
+ should 'version.reify should convert the attribute value back to its original, unserialized value' do
985
+ assert_equal @attribute_value_before_change.unserialized_value, @person.versions.last.reify.time_zone
986
+ end
987
+ should 'version.changeset should convert the attribute value back to its original, unserialized value' do
988
+ assert_equal @person.instance_variable_get(:@attributes)['time_zone'].unserialized_value, @person.versions.last.changeset[:time_zone].last
989
+ end
990
+ should "record.changes (before save) returns the original, unserialized values" do
991
+ assert_equal [ActiveSupport::TimeZone, ActiveSupport::TimeZone], @changes_before_save[:time_zone].map(&:class)
992
+ end
993
+ should 'version.changeset should be the same as record.changes was before the save' do
994
+ assert_equal @changes_before_save, @person.versions.last.changeset
995
+ assert_equal [ActiveSupport::TimeZone, ActiveSupport::TimeZone], @person.versions.last.changeset[:time_zone].map(&:class)
996
+ end
997
+
998
+ end
999
+ end
1000
+ end
1001
+
1002
+
919
1003
  context 'A new model instance which uses a custom Version class' do
920
1004
  setup { @post = Post.new }
921
1005
 
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class CustomSerializer
4
+ require 'json'
5
+ def self.dump(object_hash)
6
+ JSON.dump object_hash
7
+ end
8
+
9
+ def self.load(string)
10
+ JSON.parse string
11
+ end
12
+ end
13
+
14
+ class SerializerTest < ActiveSupport::TestCase
15
+
16
+ context 'YAML Serializer' do
17
+ setup do
18
+ Fluxor.instance_eval <<-END
19
+ has_paper_trail
20
+ END
21
+
22
+ @fluxor = Fluxor.create :name => 'Some text.'
23
+ @fluxor.update_attributes :name => 'Some more text.'
24
+ end
25
+
26
+ should 'work with the default yaml serializer' do
27
+ # Normal behaviour
28
+ assert_equal 2, @fluxor.versions.length
29
+ assert_nil @fluxor.versions[0].reify
30
+ assert_equal 'Some text.', @fluxor.versions[1].reify.name
31
+
32
+
33
+ # Check values are stored as YAML.
34
+ hash = {"widget_id" => nil,"name" =>"Some text.","id" =>1}
35
+ assert_equal YAML.dump(hash), @fluxor.versions[1].object
36
+ assert_equal hash, YAML.load(@fluxor.versions[1].object)
37
+
38
+ end
39
+ end
40
+
41
+ context 'Custom Serializer' do
42
+ setup do
43
+ PaperTrail.config.serializer = CustomSerializer
44
+
45
+ Fluxor.instance_eval <<-END
46
+ has_paper_trail
47
+ END
48
+
49
+ @fluxor = Fluxor.create :name => 'Some text.'
50
+ @fluxor.update_attributes :name => 'Some more text.'
51
+ end
52
+
53
+ teardown do
54
+ PaperTrail.config.serializer = PaperTrail::Serializers::Yaml
55
+ end
56
+
57
+ should 'work with custom serializer' do
58
+ # Normal behaviour
59
+ assert_equal 2, @fluxor.versions.length
60
+ assert_nil @fluxor.versions[0].reify
61
+ assert_equal 'Some text.', @fluxor.versions[1].reify.name
62
+
63
+ # Check values are stored as JSON.
64
+ hash = {"widget_id" => nil,"name" =>"Some text.","id" =>1}
65
+ assert_equal JSON.dump(hash), @fluxor.versions[1].object
66
+ assert_equal hash, JSON.parse(@fluxor.versions[1].object)
67
+
68
+ end
69
+ end
70
+
71
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_trail
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.4
4
+ version: 2.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-16 00:00:00.000000000 Z
12
+ date: 2012-12-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -128,6 +128,7 @@ files:
128
128
  - lib/paper_trail/config.rb
129
129
  - lib/paper_trail/controller.rb
130
130
  - lib/paper_trail/has_paper_trail.rb
131
+ - lib/paper_trail/serializers/yaml.rb
131
132
  - lib/paper_trail/version.rb
132
133
  - lib/paper_trail/version_number.rb
133
134
  - paper_trail.gemspec
@@ -192,6 +193,7 @@ files:
192
193
  - test/test_helper.rb
193
194
  - test/unit/inheritance_column_test.rb
194
195
  - test/unit/model_test.rb
196
+ - test/unit/serializer_test.rb
195
197
  - test/unit/timestamp_test.rb
196
198
  - test/unit/version_test.rb
197
199
  homepage: http://github.com/airblade/paper_trail
@@ -280,5 +282,6 @@ test_files:
280
282
  - test/test_helper.rb
281
283
  - test/unit/inheritance_column_test.rb
282
284
  - test/unit/model_test.rb
285
+ - test/unit/serializer_test.rb
283
286
  - test/unit/timestamp_test.rb
284
287
  - test/unit/version_test.rb