paper_trail 4.0.0.rc1 → 4.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -58
  3. data/README.md +528 -346
  4. data/lib/generators/paper_trail/templates/add_object_changes_to_versions.rb +6 -1
  5. data/lib/generators/paper_trail/templates/create_versions.rb +8 -1
  6. data/lib/paper_trail.rb +2 -2
  7. data/lib/paper_trail/config.rb +15 -4
  8. data/lib/paper_trail/frameworks/active_record.rb +2 -3
  9. data/lib/paper_trail/has_paper_trail.rb +32 -13
  10. data/lib/paper_trail/version_concern.rb +1 -1
  11. data/lib/paper_trail/version_number.rb +1 -1
  12. data/paper_trail.gemspec +1 -1
  13. data/spec/models/gadget_spec.rb +1 -1
  14. data/spec/models/kitchen/banana_spec.rb +14 -0
  15. data/spec/models/not_on_update_spec.rb +19 -0
  16. data/spec/models/skipper_spec.rb +17 -0
  17. data/spec/models/version_spec.rb +5 -8
  18. data/spec/models/widget_spec.rb +5 -8
  19. data/test/dummy/app/models/kitchen/banana.rb +5 -0
  20. data/test/dummy/app/models/not_on_update.rb +4 -0
  21. data/test/dummy/app/models/skipper.rb +6 -0
  22. data/test/dummy/app/versions/kitchen/banana_version.rb +5 -0
  23. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +25 -0
  24. data/test/paper_trail_test.rb +7 -0
  25. metadata +18 -24
  26. data/test/dummy/public/404.html +0 -26
  27. data/test/dummy/public/422.html +0 -26
  28. data/test/dummy/public/500.html +0 -26
  29. data/test/dummy/public/favicon.ico +0 -0
  30. data/test/dummy/public/javascripts/application.js +0 -2
  31. data/test/dummy/public/javascripts/controls.js +0 -965
  32. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  33. data/test/dummy/public/javascripts/effects.js +0 -1123
  34. data/test/dummy/public/javascripts/rails.js +0 -175
  35. data/test/dummy/public/stylesheets/.gitkeep +0 -0
@@ -1,5 +1,10 @@
1
1
  class AddObjectChangesToVersions < ActiveRecord::Migration
2
+
3
+ # The largest text column available in all supported RDBMS.
4
+ # See `create_versions.rb` for details.
5
+ TEXT_BYTES = 1_073_741_823
6
+
2
7
  def change
3
- add_column :versions, :object_changes, :text
8
+ add_column :versions, :object_changes, :text, limit: TEXT_BYTES
4
9
  end
5
10
  end
@@ -1,11 +1,18 @@
1
1
  class CreateVersions < ActiveRecord::Migration
2
+
3
+ # The largest text column available in all supported RDBMS is
4
+ # 1024^3 - 1 bytes, roughly one gibibyte. We specify a size
5
+ # so that MySQL will use `longtext` instead of `text`. Otherwise,
6
+ # when serializing very large objects, `text` might not be big enough.
7
+ TEXT_BYTES = 1_073_741_823
8
+
2
9
  def change
3
10
  create_table :versions do |t|
4
11
  t.string :item_type, :null => false
5
12
  t.integer :item_id, :null => false
6
13
  t.string :event, :null => false
7
14
  t.string :whodunnit
8
- t.text :object
15
+ t.text :object, :limit => TEXT_BYTES
9
16
  t.datetime :created_at
10
17
  end
11
18
  add_index :versions, [:item_type, :item_id]
data/lib/paper_trail.rb CHANGED
@@ -104,7 +104,7 @@ module PaperTrail
104
104
  end
105
105
 
106
106
  def self.transaction?
107
- ActiveRecord::Base.connection.open_transactions > 0
107
+ ::ActiveRecord::Base.connection.open_transactions > 0
108
108
  end
109
109
 
110
110
  def self.transaction_id
@@ -149,7 +149,7 @@ end
149
149
 
150
150
  # Require frameworks
151
151
  require 'paper_trail/frameworks/sinatra'
152
- if defined? Rails
152
+ if defined? ::Rails
153
153
  require 'paper_trail/frameworks/rails'
154
154
  else
155
155
  require 'paper_trail/frameworks/active_record'
@@ -1,14 +1,14 @@
1
1
  require 'singleton'
2
+ require 'paper_trail/serializers/yaml'
2
3
 
3
4
  module PaperTrail
4
5
  class Config
5
6
  include Singleton
6
- attr_accessor :enabled, :timestamp_field, :serializer, :version_limit
7
+ attr_accessor :timestamp_field, :serializer, :version_limit
7
8
  attr_reader :serialized_attributes
8
9
  attr_writer :track_associations
9
10
 
10
11
  def initialize
11
- @enabled = true # Indicates whether PaperTrail is on or off.
12
12
  @timestamp_field = :created_at
13
13
  @serializer = PaperTrail::Serializers::YAML
14
14
 
@@ -22,9 +22,11 @@ module PaperTrail
22
22
 
23
23
  def serialized_attributes=(value)
24
24
  if ::ActiveRecord::VERSION::MAJOR >= 5
25
- warn("DEPRECATED: ActiveRecord 5.0 deprecated `serialized_attributes` " +
25
+ ::ActiveSupport::Deprecation.warn(
26
+ "ActiveRecord 5.0 deprecated `serialized_attributes` " +
26
27
  "without replacement, so this PaperTrail config setting does " +
27
- "nothing with this version, and is always turned off")
28
+ "nothing with this version, and is always turned off"
29
+ )
28
30
  end
29
31
  @serialized_attributes = value
30
32
  end
@@ -33,5 +35,14 @@ module PaperTrail
33
35
  @track_associations ||= PaperTrail::VersionAssociation.table_exists?
34
36
  end
35
37
  alias_method :track_associations?, :track_associations
38
+
39
+ # Indicates whether PaperTrail is on or off.
40
+ def enabled
41
+ PaperTrail.paper_trail_store[:paper_trail_enabled].nil? || PaperTrail.paper_trail_store[:paper_trail_enabled]
42
+ end
43
+
44
+ def enabled= enable
45
+ PaperTrail.paper_trail_store[:paper_trail_enabled] = enable
46
+ end
36
47
  end
37
48
  end
@@ -1,5 +1,4 @@
1
1
  # This file only needs to be loaded if the gem is being used outside of Rails, since otherwise
2
2
  # the model(s) will get loaded in via the `Rails::Engine`
3
- Dir[File.join(File.dirname(__FILE__), 'active_record', 'models', 'paper_trail', '*.rb')].each do |file|
4
- require "paper_trail/frameworks/active_record/models/paper_trail/#{File.basename(file, '.rb')}"
5
- end
3
+ require "paper_trail/frameworks/active_record/models/paper_trail/version_association"
4
+ require "paper_trail/frameworks/active_record/models/paper_trail/version"
@@ -192,7 +192,7 @@ module PaperTrail
192
192
  end
193
193
 
194
194
  def originator
195
- warn "DEPRECATED: use `paper_trail_originator` instead of `originator`. Support for `originator` will be removed in PaperTrail 4.0"
195
+ ::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
196
196
  self.paper_trail_originator
197
197
  end
198
198
 
@@ -266,11 +266,14 @@ module PaperTrail
266
266
  PaperTrail.whodunnit = current_whodunnit
267
267
  end
268
268
 
269
- # Mimicks behavior of `touch` method from `ActiveRecord::Persistence`, but generates a version
269
+ # Mimics the `touch` method from `ActiveRecord::Persistence`, but also
270
+ # creates a version. A version is created regardless of options such as
271
+ # `:on`, `:if`, or `:unless`.
270
272
  #
271
- # TODO: lookinto leveraging the `after_touch` callback from `ActiveRecord` to allow the
272
- # regular `touch` method go generate a version as normal. May make sense to switch the `record_update`
273
- # method to leverage an `after_update` callback anyways (likely for v4.0.0)
273
+ # TODO: look into leveraging the `after_touch` callback from
274
+ # `ActiveRecord` to allow the regular `touch` method go generate a version
275
+ # as normal. May make sense to switch the `record_update` method to
276
+ # leverage an `after_update` callback anyways (likely for v4.0.0)
274
277
  def touch_with_version(name = nil)
275
278
  raise ActiveRecordError, "can not touch on a new record object" unless persisted?
276
279
 
@@ -279,13 +282,20 @@ module PaperTrail
279
282
  current_time = current_time_from_proper_timezone
280
283
 
281
284
  attributes.each { |column| write_attribute(column, current_time) }
282
- # ensure a version is written even if the `:on` collection is empty
283
- record_update(true) if paper_trail_options[:on] == []
285
+
286
+ record_update(true) unless will_record_after_update?
284
287
  save!(:validate => false)
285
288
  end
286
289
 
287
290
  private
288
291
 
292
+ # Returns true if `save` will cause `record_update`
293
+ # to be called via the `after_update` callback.
294
+ def will_record_after_update?
295
+ on = paper_trail_options[:on]
296
+ on.nil? || on.include?(:update)
297
+ end
298
+
289
299
  def source_version
290
300
  send self.class.version_association_name
291
301
  end
@@ -459,18 +469,27 @@ module PaperTrail
459
469
  attrs
460
470
  end
461
471
 
462
- # This method is invoked in order to determine whether it is appropriate to generate a new version instance.
463
- # Because we are now using `after_(create/update/etc)` callbacks, we need to go out of our way to
464
- # ensure that during updates timestamp attributes are not acknowledged as a notable changes
465
- # to raise false positives when attributes are ignored.
472
+ # This method determines whether it is appropriate to generate a new
473
+ # version instance. A timestamp-only update (e.g. only `updated_at`
474
+ # changed) is considered notable unless an ignored attribute was also
475
+ # changed.
466
476
  def changed_notably?
467
- if self.paper_trail_options[:ignore].any? && (changed & self.paper_trail_options[:ignore]).any?
468
- (notably_changed - timestamp_attributes_for_update_in_model.map(&:to_s)).any?
477
+ if ignored_attr_has_changed?
478
+ timestamps = timestamp_attributes_for_update_in_model.map(&:to_s)
479
+ (notably_changed - timestamps).any?
469
480
  else
470
481
  notably_changed.any?
471
482
  end
472
483
  end
473
484
 
485
+ # An attributed is "ignored" if it is listed in the `:ignore` option
486
+ # and/or the `:skip` option. Returns true if an ignored attribute
487
+ # has changed.
488
+ def ignored_attr_has_changed?
489
+ ignored = self.paper_trail_options[:ignore] + self.paper_trail_options[:skip]
490
+ ignored.any? && (changed & ignored).any?
491
+ end
492
+
474
493
  def notably_changed
475
494
  only = self.paper_trail_options[:only].dup
476
495
  # remove Hash arguments and then evaluate whether the attributes (the keys of the hash) should also get pushed into the collection
@@ -257,7 +257,7 @@ module PaperTrail
257
257
  end
258
258
 
259
259
  def originator
260
- warn "DEPRECATED: use `paper_trail_originator` instead of `originator`. Support for `originator` will be removed in PaperTrail 4.0"
260
+ ::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
261
261
  self.paper_trail_originator
262
262
  end
263
263
 
@@ -3,7 +3,7 @@ module PaperTrail
3
3
  MAJOR = 4
4
4
  MINOR = 0
5
5
  TINY = 0
6
- PRE = 'rc1'
6
+ PRE = 'rc2'
7
7
 
8
8
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
9
9
 
data/paper_trail.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
 
22
22
  s.add_dependency 'activerecord', ['>= 3.0', '< 6.0']
23
23
  s.add_dependency 'activesupport', ['>= 3.0', '< 6.0']
24
- s.add_dependency 'request_store', '~> 1.1.0'
24
+ s.add_dependency 'request_store', '~> 1.1'
25
25
 
26
26
  s.add_development_dependency 'rake', '~> 10.1.1'
27
27
  s.add_development_dependency 'shoulda', '~> 3.5'
@@ -45,7 +45,7 @@ describe Gadget, :type => :model do
45
45
  expect(subject.send(:changed_notably?)).to be true
46
46
  end
47
47
 
48
- it "should not acknowledge ignored attrs and timestamps only" do
48
+ it "should not acknowledge ignored attr (brand)" do
49
49
  subject.brand = 'Acme'
50
50
  expect(subject.send(:changed_notably?)).to be false
51
51
  end
@@ -0,0 +1,14 @@
1
+ require 'rails_helper'
2
+
3
+ module Kitchen
4
+ describe Banana, :type => :model do
5
+ it { is_expected.to be_versioned }
6
+
7
+ describe '#versions' do
8
+ it "returns instances of Kitchen::BananaVersion", :versioning => true do
9
+ banana = described_class.create!
10
+ expect(banana.versions.first).to be_a(Kitchen::BananaVersion)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails_helper'
2
+
3
+ describe NotOnUpdate, :type => :model do
4
+ describe "#touch_with_version", :versioning => true do
5
+ let!(:record) { described_class.create! }
6
+
7
+ it "should create a version, regardless" do
8
+ expect { record.touch_with_version }.to change {
9
+ PaperTrail::Version.count
10
+ }.by(+1)
11
+ end
12
+
13
+ it "increments the `:updated_at` timestamp" do
14
+ before = record.updated_at
15
+ record.touch_with_version
16
+ expect(record.updated_at).to be > before
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ require 'rails_helper'
2
+
3
+ describe Skipper, :type => :model do
4
+ describe "#update_attributes!", :versioning => true do
5
+ context "updating a skipped attribute" do
6
+ let(:t1) { Time.zone.local(2015, 7, 15, 20, 34, 0) }
7
+ let(:t2) { Time.zone.local(2015, 7, 15, 20, 34, 30) }
8
+
9
+ it "should not create a version" do
10
+ skipper = Skipper.create!(:another_timestamp => t1)
11
+ expect {
12
+ skipper.update_attributes!(:another_timestamp => t2)
13
+ }.to_not change { skipper.versions.length }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -58,10 +58,10 @@ describe PaperTrail::Version, :type => :model do
58
58
 
59
59
  context "Has previous version", :versioning => true do
60
60
  let(:name) { Faker::Name.name }
61
- let(:widget) { Widget.create!(name: Faker::Name.name) }
61
+ let(:widget) { Widget.create!(:name => Faker::Name.name) }
62
62
  before do
63
63
  widget.versions.first.update_attributes!(:whodunnit => name)
64
- widget.update_attributes!(name: Faker::Name.first_name)
64
+ widget.update_attributes!(:name => Faker::Name.first_name)
65
65
  end
66
66
  subject { widget.versions.last }
67
67
 
@@ -75,19 +75,16 @@ describe PaperTrail::Version, :type => :model do
75
75
 
76
76
  describe "#originator" do
77
77
  it { is_expected.to respond_to(:originator) }
78
- let(:warning_msg) do
79
- "DEPRECATED: use `paper_trail_originator` instead of `originator`." +
80
- " Support for `originator` will be removed in PaperTrail 4.0"
81
- end
82
78
 
83
79
  it 'should set the invoke `paper_trail_originator`' do
84
- is_expected.to receive(:warn)
80
+ allow(ActiveSupport::Deprecation).to receive(:warn)
85
81
  is_expected.to receive(:paper_trail_originator)
86
82
  subject.originator
87
83
  end
88
84
 
89
85
  it 'should display a deprecation warning' do
90
- is_expected.to receive(:warn).with(warning_msg)
86
+ expect(ActiveSupport::Deprecation).to receive(:warn).
87
+ with(/Use paper_trail_originator instead of originator/)
91
88
  subject.originator
92
89
  end
93
90
  end
@@ -177,19 +177,16 @@ describe Widget, :type => :model do
177
177
  subject { widget }
178
178
 
179
179
  it { is_expected.to respond_to(:originator) }
180
- let(:warning_msg) do
181
- "DEPRECATED: use `paper_trail_originator` instead of `originator`." +
182
- " Support for `originator` will be removed in PaperTrail 4.0"
183
- end
184
180
 
185
181
  it 'should set the invoke `paper_trail_originator`' do
186
- is_expected.to receive(:warn)
182
+ allow(::ActiveSupport::Deprecation).to receive(:warn)
187
183
  is_expected.to receive(:paper_trail_originator)
188
184
  subject.originator
189
185
  end
190
186
 
191
187
  it 'should display a deprecation warning' do
192
- is_expected.to receive(:warn).with(warning_msg)
188
+ expect(::ActiveSupport::Deprecation).to receive(:warn).
189
+ with(/Use paper_trail_originator instead of originator/)
193
190
  subject.originator
194
191
  end
195
192
  end
@@ -252,13 +249,13 @@ describe Widget, :type => :model do
252
249
  describe '#touch_with_version' do
253
250
  it { is_expected.to respond_to(:touch_with_version) }
254
251
 
255
- it "should generate a version" do
252
+ it "creates a version" do
256
253
  count = widget.versions.size
257
254
  widget.touch_with_version
258
255
  expect(widget.versions.size).to eq(count + 1)
259
256
  end
260
257
 
261
- it "should increment the `:updated_at` timestamp" do
258
+ it "increments the `:updated_at` timestamp" do
262
259
  time_was = widget.updated_at
263
260
  widget.touch_with_version
264
261
  expect(widget.updated_at).to be > time_was
@@ -0,0 +1,5 @@
1
+ module Kitchen
2
+ class Banana < ActiveRecord::Base
3
+ has_paper_trail :class_name => "Kitchen::BananaVersion"
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ # This model does not record versions when updated.
2
+ class NotOnUpdate < ActiveRecord::Base
3
+ has_paper_trail :on => [:create, :destroy]
4
+ end
@@ -0,0 +1,6 @@
1
+ class Skipper < ActiveRecord::Base
2
+ has_paper_trail(
3
+ :ignore => [:created_at],
4
+ :skip => [:another_timestamp]
5
+ )
6
+ end
@@ -0,0 +1,5 @@
1
+ module Kitchen
2
+ class BananaVersion < PaperTrail::Version
3
+ self.table_name = 'banana_versions'
4
+ end
5
+ end
@@ -1,5 +1,10 @@
1
1
  class SetUpTestTables < ActiveRecord::Migration
2
2
  def self.up
3
+ create_table :skippers, :force => true do |t|
4
+ t.datetime :another_timestamp
5
+ t.timestamps :null => true
6
+ end
7
+
3
8
  create_table :widgets, :force => true do |t|
4
9
  t.string :name
5
10
  t.text :a_text
@@ -73,6 +78,24 @@ class SetUpTestTables < ActiveRecord::Migration
73
78
  add_index :json_versions, [:item_type, :item_id]
74
79
  end
75
80
 
81
+ create_table :not_on_updates, :force => true do |t|
82
+ t.timestamps :null => true
83
+ end
84
+
85
+ create_table :bananas, :force => true do |t|
86
+ t.timestamps :null => true
87
+ end
88
+
89
+ create_table :banana_versions, :force => true do |t|
90
+ t.string :item_type, :null => false
91
+ t.integer :item_id, :null => false
92
+ t.string :event, :null => false
93
+ t.string :whodunnit
94
+ t.text :object
95
+ t.datetime :created_at
96
+ end
97
+ add_index :banana_versions, [:item_type, :item_id]
98
+
76
99
  create_table :wotsits, :force => true do |t|
77
100
  t.integer :widget_id
78
101
  t.string :name
@@ -191,6 +214,8 @@ class SetUpTestTables < ActiveRecord::Migration
191
214
 
192
215
  def self.down
193
216
  drop_table :animals
217
+ drop_table :skippers
218
+ drop_table :not_on_updates
194
219
  drop_table :posts
195
220
  drop_table :songs
196
221
  drop_table :editors
@@ -8,6 +8,13 @@ class PaperTrailTest < ActiveSupport::TestCase
8
8
  test 'Version Number' do
9
9
  assert PaperTrail.const_defined?(:VERSION)
10
10
  end
11
+
12
+ test 'enabled is thread-safe' do
13
+ Thread.new do
14
+ PaperTrail.enabled = false
15
+ end.join
16
+ assert PaperTrail.enabled?
17
+ end
11
18
 
12
19
  test 'create with plain model class' do
13
20
  widget = Widget.create