paper_trail 2.7.2 → 3.0.0.beta1
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +13 -4
- data/CHANGELOG.md +15 -0
- data/README.md +109 -38
- data/Rakefile +9 -3
- data/gemfiles/3.0.gemfile +31 -0
- data/lib/generators/paper_trail/install_generator.rb +7 -4
- data/lib/paper_trail.rb +15 -9
- data/lib/paper_trail/cleaner.rb +34 -0
- data/lib/paper_trail/frameworks/cucumber.rb +31 -0
- data/lib/paper_trail/frameworks/rails.rb +79 -0
- data/lib/paper_trail/frameworks/rspec.rb +24 -0
- data/lib/paper_trail/frameworks/rspec/extensions.rb +20 -0
- data/lib/paper_trail/frameworks/sinatra.rb +31 -0
- data/lib/paper_trail/has_paper_trail.rb +22 -20
- data/lib/paper_trail/version.rb +188 -161
- data/lib/paper_trail/version_number.rb +1 -1
- data/paper_trail.gemspec +10 -6
- data/spec/models/widget_spec.rb +13 -0
- data/spec/paper_trail_spec.rb +47 -0
- data/spec/spec_helper.rb +41 -0
- data/test/dummy/app/controllers/widgets_controller.rb +10 -2
- data/test/dummy/app/models/protected_widget.rb +1 -1
- data/test/dummy/app/models/widget.rb +6 -1
- data/test/dummy/app/versions/post_version.rb +1 -1
- data/test/dummy/config/application.rb +5 -6
- data/test/dummy/config/environments/development.rb +6 -4
- data/test/dummy/config/environments/production.rb +6 -0
- data/test/dummy/config/environments/test.rb +4 -4
- data/test/dummy/config/initializers/paper_trail.rb +4 -2
- data/test/functional/controller_test.rb +2 -2
- data/test/functional/modular_sinatra_test.rb +44 -0
- data/test/functional/sinatra_test.rb +45 -0
- data/test/functional/thread_safety_test.rb +1 -1
- data/test/paper_trail_test.rb +2 -2
- data/test/unit/cleaner_test.rb +143 -0
- data/test/unit/inheritance_column_test.rb +3 -3
- data/test/unit/model_test.rb +74 -55
- data/test/unit/protected_attrs_test.rb +12 -7
- data/test/unit/timestamp_test.rb +2 -2
- data/test/unit/version_test.rb +37 -20
- metadata +86 -26
- data/lib/paper_trail/controller.rb +0 -75
@@ -0,0 +1,34 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module Cleaner
|
3
|
+
# Destroys all but the most recent version(s) for items on a given date (or on all dates). Useful for deleting drafts.
|
4
|
+
#
|
5
|
+
# Options:
|
6
|
+
# :keeping An `integer` indicating the number of versions to be kept for each item per date.
|
7
|
+
# Defaults to `1`.
|
8
|
+
# :date Should either be a `Date` object specifying which date to destroy versions for or `:all`,
|
9
|
+
# which will specify that all dates should be cleaned. Defaults to `:all`.
|
10
|
+
# :item_id The `id` for the item to be cleaned on, or `nil`, which causes all items to be cleaned.
|
11
|
+
# Defaults to `nil`.
|
12
|
+
def clean_versions!(options = {})
|
13
|
+
options = {:keeping => 1, :date => :all}.merge(options)
|
14
|
+
gather_versions(options[:item_id], options[:date]).each do |item_id, versions|
|
15
|
+
versions.group_by { |v| v.created_at.to_date }.each do |date, versions| # now group the versions by date and iterate through those
|
16
|
+
versions.pop(options[:keeping]) # remove the number of versions we wish to keep from the collection of versions prior to destruction
|
17
|
+
versions.map(&:destroy)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Returns a hash of versions grouped by the `item_id` attribute formatted like this: {:item_id => PaperTrail::Version}.
|
25
|
+
# If `item_id` or `date` is set, versions will be narrowed to those pointing at items with those ids that were created on specified date.
|
26
|
+
def gather_versions(item_id = nil, date = :all)
|
27
|
+
raise "`date` argument must receive a Timestamp or `:all`" unless date == :all || date.respond_to?(:to_date)
|
28
|
+
versions = item_id ? PaperTrail::Version.where(:item_id => item_id) : PaperTrail::Version
|
29
|
+
versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all
|
30
|
+
versions = PaperTrail::Version.all if versions == PaperTrail::Version # if versions has not been converted to an ActiveRecord::Relation yet, do so now
|
31
|
+
versions.group_by(&:item_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
if defined? World
|
2
|
+
# before hook for Cucumber
|
3
|
+
before do
|
4
|
+
::PaperTrail.enabled = false
|
5
|
+
::PaperTrail.whodunnit = nil
|
6
|
+
::PaperTrail.controller_info = {} if defined? ::Rails
|
7
|
+
end
|
8
|
+
|
9
|
+
module PaperTrail
|
10
|
+
module Cucumber
|
11
|
+
module Extensions
|
12
|
+
# :call-seq:
|
13
|
+
# with_versioning
|
14
|
+
#
|
15
|
+
# enable versioning for specific blocks
|
16
|
+
|
17
|
+
def with_versioning
|
18
|
+
was_enabled = ::PaperTrail.enabled?
|
19
|
+
::PaperTrail.enabled = true
|
20
|
+
begin
|
21
|
+
yield
|
22
|
+
ensure
|
23
|
+
::PaperTrail.enabled = was_enabled
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
World PaperTrail::Cucumber::Extensions
|
31
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module Rails
|
3
|
+
module Controller
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
if defined?(ActionController) && base == ActionController::Base
|
7
|
+
base.before_filter :set_paper_trail_enabled_for_controller
|
8
|
+
base.before_filter :set_paper_trail_whodunnit, :set_paper_trail_controller_info
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
# Returns the user who is responsible for any changes that occur.
|
15
|
+
# By default this calls `current_user` and returns the result.
|
16
|
+
#
|
17
|
+
# Override this method in your controller to call a different
|
18
|
+
# method, e.g. `current_person`, or anything you like.
|
19
|
+
def user_for_paper_trail
|
20
|
+
current_user if defined?(current_user)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns any information about the controller or request that you
|
24
|
+
# want PaperTrail to store alongside any changes that occur. By
|
25
|
+
# default this returns an empty hash.
|
26
|
+
#
|
27
|
+
# Override this method in your controller to return a hash of any
|
28
|
+
# information you need. The hash's keys must correspond to columns
|
29
|
+
# in your `versions` table, so don't forget to add any new columns
|
30
|
+
# you need.
|
31
|
+
#
|
32
|
+
# For example:
|
33
|
+
#
|
34
|
+
# {:ip => request.remote_ip, :user_agent => request.user_agent}
|
35
|
+
#
|
36
|
+
# The columns `ip` and `user_agent` must exist in your `versions` # table.
|
37
|
+
#
|
38
|
+
# Use the `:meta` option to `PaperTrail::Model::ClassMethods.has_paper_trail`
|
39
|
+
# to store any extra model-level data you need.
|
40
|
+
def info_for_paper_trail
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns `true` (default) or `false` depending on whether PaperTrail should
|
45
|
+
# be active for the current request.
|
46
|
+
#
|
47
|
+
# Override this method in your controller to specify when PaperTrail should
|
48
|
+
# be off.
|
49
|
+
def paper_trail_enabled_for_controller
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Tells PaperTrail whether versions should be saved in the current request.
|
56
|
+
def set_paper_trail_enabled_for_controller
|
57
|
+
::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
|
58
|
+
end
|
59
|
+
|
60
|
+
# Tells PaperTrail who is responsible for any changes that occur.
|
61
|
+
def set_paper_trail_whodunnit
|
62
|
+
::PaperTrail.whodunnit = user_for_paper_trail if paper_trail_enabled_for_controller
|
63
|
+
end
|
64
|
+
|
65
|
+
# DEPRECATED: please use `set_paper_trail_whodunnit` instead.
|
66
|
+
def set_whodunnit
|
67
|
+
logger.warn '[PaperTrail]: the `set_whodunnit` controller method has been deprecated. Please rename to `set_paper_trail_whodunnit`.'
|
68
|
+
set_paper_trail_whodunnit
|
69
|
+
end
|
70
|
+
|
71
|
+
# Tells PaperTrail any information from the controller you want
|
72
|
+
# to store alongside any changes that occur.
|
73
|
+
def set_paper_trail_controller_info
|
74
|
+
::PaperTrail.controller_info = info_for_paper_trail if paper_trail_enabled_for_controller
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
if defined? RSpec
|
2
|
+
require 'rspec/core'
|
3
|
+
require 'rspec/matchers'
|
4
|
+
require File.expand_path('../rspec/extensions', __FILE__)
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.include ::PaperTrail::RSpec::Extensions
|
8
|
+
|
9
|
+
config.before(:each) do
|
10
|
+
::PaperTrail.enabled = false
|
11
|
+
::PaperTrail.whodunnit = nil
|
12
|
+
::PaperTrail.controller_info = {} if defined?(::Rails) && defined?(::RSpec::Rails)
|
13
|
+
end
|
14
|
+
|
15
|
+
config.before(:each, :versioning => true) do
|
16
|
+
::PaperTrail.enabled = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec::Matchers.define :be_versioned do
|
21
|
+
# check to see if the model has `has_paper_trail` declared on it
|
22
|
+
match { |actual| actual.kind_of?(::PaperTrail::Model::InstanceMethods) }
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module RSpec
|
3
|
+
module Extensions
|
4
|
+
# :call-seq:
|
5
|
+
# with_versioning
|
6
|
+
#
|
7
|
+
# enable versioning for specific blocks
|
8
|
+
|
9
|
+
def with_versioning
|
10
|
+
was_enabled = ::PaperTrail.enabled?
|
11
|
+
::PaperTrail.enabled = true
|
12
|
+
begin
|
13
|
+
yield
|
14
|
+
ensure
|
15
|
+
::PaperTrail.enabled = was_enabled
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module PaperTrail
|
3
|
+
|
4
|
+
# Register this module inside your Sinatra application to gain access to controller-level methods used by PaperTrail
|
5
|
+
def self.registered(app)
|
6
|
+
app.helpers Sinatra::PaperTrail
|
7
|
+
app.before { set_paper_trail_whodunnit }
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Returns the user who is responsible for any changes that occur.
|
13
|
+
# By default this calls `current_user` and returns the result.
|
14
|
+
#
|
15
|
+
# Override this method in your controller to call a different
|
16
|
+
# method, e.g. `current_person`, or anything you like.
|
17
|
+
def user_for_paper_trail
|
18
|
+
current_user if defined?(current_user)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Tells PaperTrail who is responsible for any changes that occur.
|
24
|
+
def set_paper_trail_whodunnit
|
25
|
+
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled?
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
register Sinatra::PaperTrail if defined?(register)
|
31
|
+
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] || 'PaperTrail::Version'
|
43
43
|
|
44
44
|
class_attribute :paper_trail_options
|
45
45
|
self.paper_trail_options = options.dup
|
@@ -59,18 +59,21 @@ module PaperTrail
|
|
59
59
|
|
60
60
|
attr_accessor :paper_trail_event
|
61
61
|
|
62
|
-
has_many
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
62
|
+
if ActiveRecord::VERSION::STRING.to_f >= 4.0 # `has_many` syntax for specifying order uses a lambda in Rails 4
|
63
|
+
has_many self.versions_association_name,
|
64
|
+
lambda { order("#{PaperTrail.timestamp_field} ASC") },
|
65
|
+
:class_name => self.version_class_name, :as => :item
|
66
|
+
else
|
67
|
+
has_many self.versions_association_name,
|
68
|
+
:class_name => version_class_name,
|
69
|
+
:as => :item,
|
70
|
+
:order => "#{PaperTrail.timestamp_field} ASC"
|
71
|
+
end
|
71
72
|
|
72
|
-
|
73
|
-
|
73
|
+
options_on = Array(options[:on]) # so that a single symbol can be passed in without wrapping it in an `Array`
|
74
|
+
after_create :record_create, :if => :save_version? if options_on.empty? || options_on.include?(:create)
|
75
|
+
before_update :record_update, :if => :save_version? if options_on.empty? || options_on.include?(:update)
|
76
|
+
after_destroy :record_destroy, :if => :save_version? if options_on.empty? || options_on.include?(:destroy)
|
74
77
|
end
|
75
78
|
|
76
79
|
# Switches PaperTrail off for this class.
|
@@ -190,7 +193,7 @@ module PaperTrail
|
|
190
193
|
end
|
191
194
|
|
192
195
|
def record_create
|
193
|
-
if
|
196
|
+
if paper_trail_switched_on?
|
194
197
|
data = {
|
195
198
|
:event => paper_trail_event || 'create',
|
196
199
|
:whodunnit => PaperTrail.whodunnit
|
@@ -199,13 +202,12 @@ module PaperTrail
|
|
199
202
|
if changed_notably? and version_class.column_names.include?('object_changes')
|
200
203
|
data[:object_changes] = PaperTrail.serializer.dump(changes_for_paper_trail)
|
201
204
|
end
|
202
|
-
|
203
|
-
send(self.class.versions_association_name).create merge_metadata(data)
|
205
|
+
send(self.class.versions_association_name).create! merge_metadata(data)
|
204
206
|
end
|
205
207
|
end
|
206
208
|
|
207
209
|
def record_update
|
208
|
-
if
|
210
|
+
if paper_trail_switched_on? && changed_notably?
|
209
211
|
data = {
|
210
212
|
:event => paper_trail_event || 'update',
|
211
213
|
:object => object_to_string(item_before_change),
|
@@ -227,14 +229,14 @@ module PaperTrail
|
|
227
229
|
end
|
228
230
|
|
229
231
|
def record_destroy
|
230
|
-
if
|
232
|
+
if paper_trail_switched_on? and not new_record?
|
231
233
|
version_class.create merge_metadata(:item_id => self.id,
|
232
234
|
:item_type => self.class.base_class.name,
|
233
235
|
:event => paper_trail_event || 'destroy',
|
234
236
|
:object => object_to_string(item_before_change),
|
235
237
|
:whodunnit => PaperTrail.whodunnit)
|
238
|
+
send(self.class.versions_association_name).send :load_target
|
236
239
|
end
|
237
|
-
send(self.class.versions_association_name).send :load_target
|
238
240
|
end
|
239
241
|
|
240
242
|
def merge_metadata(data)
|
@@ -266,7 +268,7 @@ module PaperTrail
|
|
266
268
|
end
|
267
269
|
previous.tap do |prev|
|
268
270
|
prev.id = id
|
269
|
-
changed_attributes.each { |attr, before| prev[attr] = before }
|
271
|
+
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each { |attr, before| prev[attr] = before }
|
270
272
|
end
|
271
273
|
end
|
272
274
|
|
@@ -292,7 +294,7 @@ module PaperTrail
|
|
292
294
|
changed - ignore - skip
|
293
295
|
end
|
294
296
|
|
295
|
-
def
|
297
|
+
def paper_trail_switched_on?
|
296
298
|
PaperTrail.enabled? && PaperTrail.enabled_for_controller? && self.class.paper_trail_enabled_for_model
|
297
299
|
end
|
298
300
|
|
data/lib/paper_trail/version.rb
CHANGED
@@ -1,198 +1,225 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module PaperTrail
|
2
|
+
class Version < ActiveRecord::Base
|
3
|
+
belongs_to :item, :polymorphic => true
|
4
|
+
validates_presence_of :event
|
5
|
+
attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes if PaperTrail.active_record_protected_attributes?
|
5
6
|
|
6
|
-
|
7
|
+
after_create :enforce_version_limit!
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def self.with_item_keys(item_type, item_id)
|
10
|
+
where :item_type => item_type, :item_id => item_id
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def self.creates
|
14
|
+
where :event => 'create'
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def self.updates
|
18
|
+
where :event => 'update'
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
def self.destroys
|
22
|
+
where :event => 'destroy'
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
def self.not_creates
|
26
|
+
where 'event <> ?', 'create'
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
29
|
+
scope :subsequent, lambda { |version|
|
30
|
+
where("#{self.primary_key} > ?", version).order("#{self.primary_key} ASC")
|
31
|
+
}
|
32
|
+
|
33
|
+
scope :preceding, lambda { |version|
|
34
|
+
where("#{self.primary_key} < ?", version).order("#{self.primary_key} DESC")
|
35
|
+
}
|
36
|
+
|
37
|
+
scope :following, lambda { |timestamp|
|
38
|
+
# TODO: is this :order necessary, considering its presence on the has_many :versions association?
|
39
|
+
where("#{PaperTrail.timestamp_field} > ?", timestamp).
|
40
|
+
order("#{PaperTrail.timestamp_field} ASC, #{self.primary_key} ASC")
|
41
|
+
}
|
42
|
+
|
43
|
+
scope :between, lambda { |start_time, end_time|
|
44
|
+
where("#{PaperTrail.timestamp_field} > ? AND #{PaperTrail.timestamp_field} < ?", start_time, end_time).
|
45
|
+
order("#{PaperTrail.timestamp_field} ASC, #{self.primary_key} ASC")
|
46
|
+
}
|
47
|
+
|
48
|
+
# Restore the item from this version.
|
49
|
+
#
|
50
|
+
# This will automatically restore all :has_one associations as they were "at the time",
|
51
|
+
# if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
|
52
|
+
# to work so you can either change the lookback period (from the default 3 seconds) or
|
53
|
+
# opt out.
|
54
|
+
#
|
55
|
+
# Options:
|
56
|
+
# +:has_one+ set to `false` to opt out of has_one reification.
|
57
|
+
# set to a float to change the lookback time (check whether your db supports
|
58
|
+
# sub-second datetimes if you want them).
|
59
|
+
def reify(options = {})
|
60
|
+
without_identity_map do
|
61
|
+
options[:has_one] = 3 if options[:has_one] == true
|
62
|
+
options.reverse_merge! :has_one => false
|
63
|
+
|
64
|
+
unless object.nil?
|
65
|
+
attrs = PaperTrail.serializer.load object
|
66
|
+
|
67
|
+
# Normally a polymorphic belongs_to relationship allows us
|
68
|
+
# to get the object we belong to by calling, in this case,
|
69
|
+
# +item+. However this returns nil if +item+ has been
|
70
|
+
# destroyed, and we need to be able to retrieve destroyed
|
71
|
+
# objects.
|
72
|
+
#
|
73
|
+
# In this situation we constantize the +item_type+ to get hold of
|
74
|
+
# the class...except when the stored object's attributes
|
75
|
+
# include a +type+ key. If this is the case, the object
|
76
|
+
# we belong to is using single table inheritance and the
|
77
|
+
# +item_type+ will be the base class, not the actual subclass.
|
78
|
+
# If +type+ is present but empty, the class is the base class.
|
79
|
+
|
80
|
+
if item
|
81
|
+
model = item
|
82
|
+
# Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
|
83
|
+
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
|
84
|
+
else
|
85
|
+
inheritance_column_name = item_type.constantize.inheritance_column
|
86
|
+
class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
|
87
|
+
klass = class_name.constantize
|
88
|
+
model = klass.new
|
89
|
+
end
|
89
90
|
|
90
|
-
|
91
|
+
model.class.unserialize_attributes_for_paper_trail attrs
|
91
92
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
93
|
+
# Set all the attributes in this version on the model
|
94
|
+
attrs.each do |k, v|
|
95
|
+
if model.respond_to?("#{k}=")
|
96
|
+
model[k.to_sym] = v
|
97
|
+
else
|
98
|
+
logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
|
99
|
+
end
|
98
100
|
end
|
99
|
-
end
|
100
101
|
|
101
|
-
|
102
|
+
model.send "#{model.class.version_association_name}=", self
|
102
103
|
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
unless options[:has_one] == false
|
105
|
+
reify_has_ones model, options[:has_one]
|
106
|
+
end
|
106
107
|
|
107
|
-
|
108
|
+
model
|
109
|
+
end
|
108
110
|
end
|
109
111
|
end
|
110
|
-
end
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
# Returns what changed in this version of the item. Cf. `ActiveModel::Dirty#changes`.
|
114
|
+
# Returns nil if your `versions` table does not have an `object_changes` text column.
|
115
|
+
def changeset
|
116
|
+
return nil unless self.class.column_names.include? 'object_changes'
|
116
117
|
|
117
|
-
|
118
|
-
|
118
|
+
HashWithIndifferentAccess.new(PaperTrail.serializer.load(object_changes)).tap do |changes|
|
119
|
+
item_type.constantize.unserialize_attribute_changes(changes)
|
120
|
+
end
|
121
|
+
rescue
|
122
|
+
{}
|
119
123
|
end
|
120
|
-
rescue
|
121
|
-
{}
|
122
|
-
end
|
123
124
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
125
|
+
# Returns who put the item into the state stored in this version.
|
126
|
+
def originator
|
127
|
+
previous.try :whodunnit
|
128
|
+
end
|
128
129
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
130
|
+
# Returns who changed the item from the state it had in this version.
|
131
|
+
# This is an alias for `whodunnit`.
|
132
|
+
def terminator
|
133
|
+
whodunnit
|
134
|
+
end
|
134
135
|
|
135
|
-
|
136
|
-
|
137
|
-
|
136
|
+
def sibling_versions
|
137
|
+
self.class.with_item_keys(item_type, item_id)
|
138
|
+
end
|
138
139
|
|
139
|
-
|
140
|
-
|
141
|
-
|
140
|
+
def next
|
141
|
+
sibling_versions.subsequent(self).first
|
142
|
+
end
|
142
143
|
|
143
|
-
|
144
|
-
|
145
|
-
|
144
|
+
def previous
|
145
|
+
sibling_versions.preceding(self).first
|
146
|
+
end
|
146
147
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
148
|
+
def index
|
149
|
+
id_column = self.class.primary_key.to_sym
|
150
|
+
sibling_versions.select(id_column).order("#{id_column} ASC").map(&id_column).index(self.send(id_column))
|
151
|
+
end
|
151
152
|
|
152
|
-
|
153
|
+
private
|
153
154
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
155
|
+
# In Rails 3.1+, calling reify on a previous version confuses the
|
156
|
+
# IdentityMap, if enabled. This prevents insertion into the map.
|
157
|
+
def without_identity_map(&block)
|
158
|
+
if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
|
159
|
+
ActiveRecord::IdentityMap.without(&block)
|
160
|
+
else
|
161
|
+
block.call
|
162
|
+
end
|
161
163
|
end
|
162
|
-
end
|
163
164
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
165
|
+
# Restore the `model`'s has_one associations as they were when this version was
|
166
|
+
# superseded by the next (because that's what the user was looking at when they
|
167
|
+
# made the change).
|
168
|
+
#
|
169
|
+
# The `lookback` sets how many seconds before the model's change we go.
|
170
|
+
def reify_has_ones(model, lookback)
|
171
|
+
model.class.reflect_on_all_associations(:has_one).each do |assoc|
|
172
|
+
child = model.send assoc.name
|
173
|
+
if child.respond_to? :version_at
|
174
|
+
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
|
175
|
+
# Ideally we want the version of the child as it was just before the parent was updated...
|
176
|
+
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
|
177
|
+
# updated on the same form), it's impossible to tell when the overall update started;
|
178
|
+
# and therefore impossible to know when "just before" was.
|
179
|
+
if (child_as_it_was = child.version_at(send(PaperTrail.timestamp_field) - lookback.seconds))
|
180
|
+
child_as_it_was.attributes.each do |k,v|
|
181
|
+
model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
|
182
|
+
end
|
183
|
+
else
|
184
|
+
model.send "#{assoc.name}=", nil
|
181
185
|
end
|
182
|
-
else
|
183
|
-
model.send "#{assoc.name}=", nil
|
184
186
|
end
|
185
187
|
end
|
186
188
|
end
|
189
|
+
|
190
|
+
# checks to see if a value has been set for the `version_limit` config option, and if so enforces it
|
191
|
+
def enforce_version_limit!
|
192
|
+
return unless PaperTrail.config.version_limit.is_a? Numeric
|
193
|
+
previous_versions = sibling_versions.not_creates
|
194
|
+
return unless previous_versions.size > PaperTrail.config.version_limit
|
195
|
+
excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
|
196
|
+
excess_previous_versions.map(&:destroy)
|
197
|
+
end
|
198
|
+
|
187
199
|
end
|
200
|
+
end
|
188
201
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
|
195
|
-
excess_previous_versions.map(&:destroy)
|
202
|
+
# Legacy support for old applications using the original non-namespaced `Version` class
|
203
|
+
class Version < PaperTrail::Version
|
204
|
+
def initialize(*args)
|
205
|
+
warn "DEPRECATED: Please use the namespaced `PaperTrail::Version` class instead. Support for the non-namespaced `Version` class will be removed in PaperTrail 3.1."
|
206
|
+
super
|
196
207
|
end
|
197
208
|
|
209
|
+
class << self
|
210
|
+
def find(*args)
|
211
|
+
warn "DEPRECATED: Please use the namespaced `PaperTrail::Version` class instead. Support for the non-namespaced `Version` class will be removed in PaperTrail 3.1."
|
212
|
+
super
|
213
|
+
end
|
214
|
+
|
215
|
+
def first(*args)
|
216
|
+
warn "DEPRECATED: Please use the namespaced `PaperTrail::Version` class instead. Support for the non-namespaced `Version` class will be removed in PaperTrail 3.1."
|
217
|
+
super
|
218
|
+
end
|
219
|
+
|
220
|
+
def last(*args)
|
221
|
+
warn "DEPRECATED: Please use the namespaced `PaperTrail::Version` class instead. Support for the non-namespaced `Version` class will be removed in PaperTrail 3.1."
|
222
|
+
super
|
223
|
+
end
|
224
|
+
end
|
198
225
|
end
|