paper_trail 2.6.4 → 2.7.0

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