paper_trail 3.0.0.rc2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -1
- data/README.md +18 -17
- data/lib/paper_trail.rb +1 -0
- data/lib/paper_trail/frameworks/cucumber.rb +5 -5
- data/lib/paper_trail/frameworks/rails.rb +2 -8
- data/lib/paper_trail/version.rb +2 -219
- data/lib/paper_trail/version_concern.rb +221 -0
- data/lib/paper_trail/version_number.rb +1 -1
- data/paper_trail.gemspec +3 -1
- data/spec/modules/version_concern_spec.rb +18 -36
- data/spec/requests/articles_spec.rb +6 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/alt_db_init.rb +44 -0
- data/test/functional/thread_safety_test.rb +2 -2
- data/test/paper_trail_test.rb +4 -0
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c90dbeb20af0ebaa74cf679e80b9d78b299af92d
|
4
|
+
data.tar.gz: b2f30b7cc6460365f92e6d5e7c92873624f1187c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cca17faa39ff342de70157ee32913040fe1ad6098d0f0b446ff470a944071d1b50486c04eba658f021f4ef65d75637cdb0df78cb306275011748ec38847ec887
|
7
|
+
data.tar.gz: 94fc8094261b8c401e228142eeba8d51f5aa5c54a67b522f54e83b9f42562d80feeb981e86b5c73b45094d045d92b3e7f0285571b0e6dd1c78e2d60e07586225
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
## 3.0.0
|
1
|
+
## 3.0.0
|
2
2
|
|
3
|
+
- [#305](https://github.com/airblade/paper_trail/pull/305) - `PaperTrail::VERSION` should be loaded at runtime.
|
3
4
|
- [#295](https://github.com/airblade/paper_trail/issues/295) - Explicitly specify table name for version class when
|
4
5
|
querying attributes. Prevents `AmbiguousColumn` errors on certain `JOIN` statements.
|
5
6
|
- [#289](https://github.com/airblade/paper_trail/pull/289) - Use `ActiveSupport::Concern` for implementation of base functionality on
|
@@ -29,6 +30,7 @@
|
|
29
30
|
- [#119](https://github.com/airblade/paper_trail/issues/119) - Support for [Sinatra](http://www.sinatrarb.com/); decoupled gem from `Rails`.
|
30
31
|
- Renamed the default serializers from `PaperTrail::Serializers::Yaml` and `PaperTrail::Serializers::Json` to the capitalized forms,
|
31
32
|
`PaperTrail::Serializers::YAML` and `PaperTrail::Serializers::JSON`.
|
33
|
+
- Removed deprecated `set_whodunnit` method from Rails Controller scope.
|
32
34
|
|
33
35
|
## 2.7.2
|
34
36
|
|
data/README.md
CHANGED
@@ -32,9 +32,8 @@ There's an excellent [RailsCast on implementing Undo with Paper Trail](http://ra
|
|
32
32
|
|
33
33
|
Works with ActiveRecord 4 and ActiveRecord 3. Note: this code is on the `master` branch and tagged `v3.x`.
|
34
34
|
|
35
|
-
**You are reading the docs for the `master` branch. The latest release of PaperTrail is 2.7.2, the docs for which can be viewed on the [`2.7-stable`](https://github.com/airblade/paper_trail/tree/2.7-stable branch).**
|
36
|
-
|
37
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
|
+
|
38
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.
|
39
38
|
|
40
39
|
## Installation
|
@@ -43,7 +42,7 @@ The Rails 2.3 code is on the [`rails2`](https://github.com/airblade/paper_trail/
|
|
43
42
|
|
44
43
|
1. Add `PaperTrail` to your `Gemfile`.
|
45
44
|
|
46
|
-
`gem 'paper_trail', '
|
45
|
+
`gem 'paper_trail', '~> 3.0.0'`
|
47
46
|
|
48
47
|
2. Generate a migration which will add a `versions` table to your database.
|
49
48
|
|
@@ -57,14 +56,15 @@ The Rails 2.3 code is on the [`rails2`](https://github.com/airblade/paper_trail/
|
|
57
56
|
|
58
57
|
### Sinatra
|
59
58
|
|
60
|
-
In order to configure `PaperTrail` for usage with [
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
In order to configure `PaperTrail` for usage with [Sinatra](http://www.sinatrarb.com),
|
60
|
+
your `Sinatra` app must be using `ActiveRecord` 3 or `ActiveRecord` 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:
|
64
64
|
|
65
65
|
1. Add `PaperTrail` to your `Gemfile`.
|
66
66
|
|
67
|
-
`gem 'paper_trail', '
|
67
|
+
`gem 'paper_trail', '~> 3.0.0'`
|
68
68
|
|
69
69
|
2. Generate a migration to add a `versions` table to your database.
|
70
70
|
|
@@ -84,7 +84,7 @@ PaperTrail provides a helper extension that acts similar to the controller mixin
|
|
84
84
|
|
85
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`)
|
86
86
|
|
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:
|
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:
|
88
88
|
|
89
89
|
```ruby
|
90
90
|
# bleh_app.rb
|
@@ -447,7 +447,7 @@ You can find out whether a model instance is the current, live one -- or whether
|
|
447
447
|
|
448
448
|
## Finding Out Who Was Responsible For A Change
|
449
449
|
|
450
|
-
If your `ApplicationController` has a `current_user` method, PaperTrail will store the value it returns in the
|
450
|
+
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:
|
451
451
|
|
452
452
|
```ruby
|
453
453
|
>> last_change = widget.versions.last
|
@@ -464,7 +464,7 @@ class ApplicationController
|
|
464
464
|
end
|
465
465
|
```
|
466
466
|
|
467
|
-
In a
|
467
|
+
In a console session you can manually set who is responsible like this:
|
468
468
|
|
469
469
|
```ruby
|
470
470
|
>> PaperTrail.whodunnit = 'Andy Stewart'
|
@@ -484,9 +484,9 @@ class PaperTrail::Version < ActiveRecord::Base
|
|
484
484
|
end
|
485
485
|
```
|
486
486
|
|
487
|
-
|
487
|
+
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`.
|
488
488
|
|
489
|
-
To find out who made a
|
489
|
+
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.
|
490
490
|
|
491
491
|
```ruby
|
492
492
|
>> widget = Widget.find 153 # assume widget has 0 versions
|
@@ -533,7 +533,7 @@ end
|
|
533
533
|
|
534
534
|
Alternatively you could store certain metadata for one type of version, and other metadata for other versions.
|
535
535
|
|
536
|
-
If you only use custom version classes and don't use PaperTrail's built-in one, on Rails 3.2 you must:
|
536
|
+
If you only use custom version classes and don't use PaperTrail's built-in one, on Rails `>= 3.2` you must:
|
537
537
|
|
538
538
|
- either declare PaperTrail's version class abstract like this (in `config/initializers/paper_trail_patch.rb`):
|
539
539
|
|
@@ -543,7 +543,7 @@ PaperTrail::Version.module_eval do
|
|
543
543
|
end
|
544
544
|
```
|
545
545
|
|
546
|
-
- or
|
546
|
+
- or create a `versions` table in the database so Rails can instantiate the `PaperTrail::Version` superclass.
|
547
547
|
|
548
548
|
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:
|
549
549
|
|
@@ -668,6 +668,7 @@ See [issue 113](https://github.com/airblade/paper_trail/issues/113) for a discus
|
|
668
668
|
|
669
669
|
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.
|
670
670
|
|
671
|
+
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.
|
671
672
|
|
672
673
|
## Storing metadata
|
673
674
|
|
@@ -706,7 +707,7 @@ end
|
|
706
707
|
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:
|
707
708
|
|
708
709
|
```ruby
|
709
|
-
PaperTrail::Version.
|
710
|
+
PaperTrail::Version.where(:author_id => author_id)
|
710
711
|
```
|
711
712
|
|
712
713
|
Note you can pass a symbol as a value in the `meta` hash to signal a method to call.
|
@@ -771,7 +772,7 @@ On a global level you can turn PaperTrail off like this:
|
|
771
772
|
>> PaperTrail.enabled = false
|
772
773
|
```
|
773
774
|
|
774
|
-
For example, you might want to disable PaperTrail in your Rails application's test environment to speed up your tests. This will do it:
|
775
|
+
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)):
|
775
776
|
|
776
777
|
```ruby
|
777
778
|
# in config/environments/test.rb
|
data/lib/paper_trail.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'paper_trail/config'
|
2
2
|
require 'paper_trail/has_paper_trail'
|
3
3
|
require 'paper_trail/cleaner'
|
4
|
+
require 'paper_trail/version_number'
|
4
5
|
|
5
6
|
# Require serializers
|
6
7
|
Dir[File.join(File.dirname(__FILE__), 'paper_trail', 'serializers', '*.rb')].each { |file| require file }
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# before hook for Cucumber
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
Before do
|
3
|
+
PaperTrail.enabled = false
|
4
|
+
PaperTrail.enabled_for_controller = true
|
5
|
+
PaperTrail.whodunnit = nil
|
6
|
+
PaperTrail.controller_info = {} if defined? Rails
|
7
7
|
end
|
8
8
|
|
9
9
|
module PaperTrail
|
@@ -59,19 +59,13 @@ module PaperTrail
|
|
59
59
|
|
60
60
|
# Tells PaperTrail who is responsible for any changes that occur.
|
61
61
|
def set_paper_trail_whodunnit
|
62
|
-
::PaperTrail.whodunnit = user_for_paper_trail if
|
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
|
62
|
+
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
69
63
|
end
|
70
64
|
|
71
65
|
# Tells PaperTrail any information from the controller you want
|
72
66
|
# to store alongside any changes that occur.
|
73
67
|
def set_paper_trail_controller_info
|
74
|
-
::PaperTrail.controller_info = info_for_paper_trail if
|
68
|
+
::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
75
69
|
end
|
76
70
|
|
77
71
|
end
|
data/lib/paper_trail/version.rb
CHANGED
@@ -1,224 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require File.expand_path('../version_concern', __FILE__)
|
2
2
|
|
3
3
|
module PaperTrail
|
4
|
-
module VersionConcern
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
included do
|
7
|
-
belongs_to :item, :polymorphic => true
|
8
|
-
validates_presence_of :event
|
9
|
-
attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes if PaperTrail.active_record_protected_attributes?
|
10
|
-
|
11
|
-
after_create :enforce_version_limit!
|
12
|
-
end
|
13
|
-
|
14
|
-
module ClassMethods
|
15
|
-
def with_item_keys(item_type, item_id)
|
16
|
-
where :item_type => item_type, :item_id => item_id
|
17
|
-
end
|
18
|
-
|
19
|
-
def creates
|
20
|
-
where :event => 'create'
|
21
|
-
end
|
22
|
-
|
23
|
-
def updates
|
24
|
-
where :event => 'update'
|
25
|
-
end
|
26
|
-
|
27
|
-
def destroys
|
28
|
-
where :event => 'destroy'
|
29
|
-
end
|
30
|
-
|
31
|
-
def not_creates
|
32
|
-
where 'event <> ?', 'create'
|
33
|
-
end
|
34
|
-
|
35
|
-
# These methods accept a timestamp or a version and returns other versions that come before or after
|
36
|
-
def subsequent(obj)
|
37
|
-
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
38
|
-
where("#{table_name}.#{PaperTrail.timestamp_field} > ?", obj).
|
39
|
-
order("#{table_name}.#{PaperTrail.timestamp_field} ASC")
|
40
|
-
end
|
41
|
-
|
42
|
-
def preceding(obj)
|
43
|
-
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
44
|
-
where("#{table_name}.#{PaperTrail.timestamp_field} < ?", obj).
|
45
|
-
order("#{table_name}.#{PaperTrail.timestamp_field} DESC")
|
46
|
-
end
|
47
|
-
|
48
|
-
def between(start_time, end_time)
|
49
|
-
where("#{table_name}.#{PaperTrail.timestamp_field} > ? AND #{table_name}.#{PaperTrail.timestamp_field} < ?",
|
50
|
-
start_time, end_time).order("#{table_name}.#{PaperTrail.timestamp_field} ASC")
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns whether the `object` column is using the `json` type supported by PostgreSQL
|
54
|
-
def object_col_is_json?
|
55
|
-
@object_col_is_json ||= columns_hash['object'].type == :json
|
56
|
-
end
|
57
|
-
|
58
|
-
# Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL
|
59
|
-
def object_changes_col_is_json?
|
60
|
-
@object_changes_col_is_json ||= columns_hash['object_changes'].type == :json
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Restore the item from this version.
|
65
|
-
#
|
66
|
-
# This will automatically restore all :has_one associations as they were "at the time",
|
67
|
-
# if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
|
68
|
-
# to work so you can either change the lookback period (from the default 3 seconds) or
|
69
|
-
# opt out.
|
70
|
-
#
|
71
|
-
# Options:
|
72
|
-
# :has_one set to `false` to opt out of has_one reification.
|
73
|
-
# set to a float to change the lookback time (check whether your db supports
|
74
|
-
# sub-second datetimes if you want them).
|
75
|
-
def reify(options = {})
|
76
|
-
return nil if object.nil?
|
77
|
-
|
78
|
-
without_identity_map do
|
79
|
-
options[:has_one] = 3 if options[:has_one] == true
|
80
|
-
options.reverse_merge! :has_one => false
|
81
|
-
|
82
|
-
attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
|
83
|
-
|
84
|
-
# Normally a polymorphic belongs_to relationship allows us
|
85
|
-
# to get the object we belong to by calling, in this case,
|
86
|
-
# `item`. However this returns nil if `item` has been
|
87
|
-
# destroyed, and we need to be able to retrieve destroyed
|
88
|
-
# objects.
|
89
|
-
#
|
90
|
-
# In this situation we constantize the `item_type` to get hold of
|
91
|
-
# the class...except when the stored object's attributes
|
92
|
-
# include a `type` key. If this is the case, the object
|
93
|
-
# we belong to is using single table inheritance and the
|
94
|
-
# `item_type` will be the base class, not the actual subclass.
|
95
|
-
# If `type` is present but empty, the class is the base class.
|
96
|
-
|
97
|
-
if item
|
98
|
-
model = item
|
99
|
-
# Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
|
100
|
-
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
|
101
|
-
else
|
102
|
-
inheritance_column_name = item_type.constantize.inheritance_column
|
103
|
-
class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
|
104
|
-
klass = class_name.constantize
|
105
|
-
model = klass.new
|
106
|
-
end
|
107
|
-
|
108
|
-
model.class.unserialize_attributes_for_paper_trail attrs
|
109
|
-
|
110
|
-
# Set all the attributes in this version on the model
|
111
|
-
attrs.each do |k, v|
|
112
|
-
if model.respond_to?("#{k}=")
|
113
|
-
model[k.to_sym] = v
|
114
|
-
else
|
115
|
-
logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
model.send "#{model.class.version_association_name}=", self
|
120
|
-
|
121
|
-
unless options[:has_one] == false
|
122
|
-
reify_has_ones model, options[:has_one]
|
123
|
-
end
|
124
|
-
|
125
|
-
model
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Returns what changed in this version of the item. Cf. `ActiveModel::Dirty#changes`.
|
130
|
-
# Returns `nil` if your `versions` table does not have an `object_changes` text column.
|
131
|
-
def changeset
|
132
|
-
return nil unless self.class.column_names.include? 'object_changes'
|
133
|
-
|
134
|
-
_changes = self.class.object_changes_col_is_json? ? object_changes : PaperTrail.serializer.load(object_changes)
|
135
|
-
@changeset ||= HashWithIndifferentAccess.new(_changes).tap do |changes|
|
136
|
-
item_type.constantize.unserialize_attribute_changes(changes)
|
137
|
-
end
|
138
|
-
rescue
|
139
|
-
{}
|
140
|
-
end
|
141
|
-
|
142
|
-
# Returns who put the item into the state stored in this version.
|
143
|
-
def originator
|
144
|
-
@originator ||= previous.whodunnit rescue nil
|
145
|
-
end
|
146
|
-
|
147
|
-
# Returns who changed the item from the state it had in this version.
|
148
|
-
# This is an alias for `whodunnit`.
|
149
|
-
def terminator
|
150
|
-
@terminator ||= whodunnit
|
151
|
-
end
|
152
|
-
alias_method :version_author, :terminator
|
153
|
-
|
154
|
-
def sibling_versions(reload = false)
|
155
|
-
@sibling_versions = nil if reload == true
|
156
|
-
@sibling_versions ||= self.class.with_item_keys(item_type, item_id)
|
157
|
-
end
|
158
|
-
|
159
|
-
def next
|
160
|
-
@next ||= sibling_versions.subsequent(self).first
|
161
|
-
end
|
162
|
-
|
163
|
-
def previous
|
164
|
-
@previous ||= sibling_versions.preceding(self).first
|
165
|
-
end
|
166
|
-
|
167
|
-
def index
|
168
|
-
table_name = self.class.table_name
|
169
|
-
@index ||= sibling_versions.
|
170
|
-
select(["#{table_name}.#{PaperTrail.timestamp_field}", "#{table_name}.#{self.class.primary_key}"]).
|
171
|
-
order("#{table_name}.#{PaperTrail.timestamp_field} ASC").index(self)
|
172
|
-
end
|
173
|
-
|
174
|
-
private
|
175
|
-
|
176
|
-
# In Rails 3.1+, calling reify on a previous version confuses the
|
177
|
-
# IdentityMap, if enabled. This prevents insertion into the map.
|
178
|
-
def without_identity_map(&block)
|
179
|
-
if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
|
180
|
-
::ActiveRecord::IdentityMap.without(&block)
|
181
|
-
else
|
182
|
-
block.call
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Restore the `model`'s has_one associations as they were when this version was
|
187
|
-
# superseded by the next (because that's what the user was looking at when they
|
188
|
-
# made the change).
|
189
|
-
#
|
190
|
-
# The `lookback` sets how many seconds before the model's change we go.
|
191
|
-
def reify_has_ones(model, lookback)
|
192
|
-
model.class.reflect_on_all_associations(:has_one).each do |assoc|
|
193
|
-
child = model.send assoc.name
|
194
|
-
if child.respond_to? :version_at
|
195
|
-
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
|
196
|
-
# Ideally we want the version of the child as it was just before the parent was updated...
|
197
|
-
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
|
198
|
-
# updated on the same form), it's impossible to tell when the overall update started;
|
199
|
-
# and therefore impossible to know when "just before" was.
|
200
|
-
if (child_as_it_was = child.version_at(send(PaperTrail.timestamp_field) - lookback.seconds))
|
201
|
-
child_as_it_was.attributes.each do |k,v|
|
202
|
-
model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
|
203
|
-
end
|
204
|
-
else
|
205
|
-
model.send "#{assoc.name}=", nil
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# checks to see if a value has been set for the `version_limit` config option, and if so enforces it
|
212
|
-
def enforce_version_limit!
|
213
|
-
return unless PaperTrail.config.version_limit.is_a? Numeric
|
214
|
-
previous_versions = sibling_versions.not_creates
|
215
|
-
return unless previous_versions.size > PaperTrail.config.version_limit
|
216
|
-
excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
|
217
|
-
excess_previous_versions.map(&:destroy)
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
4
|
class Version < ::ActiveRecord::Base
|
222
|
-
include VersionConcern
|
5
|
+
include PaperTrail::VersionConcern
|
223
6
|
end
|
224
7
|
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module PaperTrail
|
4
|
+
module VersionConcern
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
belongs_to :item, :polymorphic => true
|
9
|
+
validates_presence_of :event
|
10
|
+
attr_accessible :item_type, :item_id, :event, :whodunnit, :object, :object_changes if PaperTrail.active_record_protected_attributes?
|
11
|
+
|
12
|
+
after_create :enforce_version_limit!
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def with_item_keys(item_type, item_id)
|
17
|
+
where :item_type => item_type, :item_id => item_id
|
18
|
+
end
|
19
|
+
|
20
|
+
def creates
|
21
|
+
where :event => 'create'
|
22
|
+
end
|
23
|
+
|
24
|
+
def updates
|
25
|
+
where :event => 'update'
|
26
|
+
end
|
27
|
+
|
28
|
+
def destroys
|
29
|
+
where :event => 'destroy'
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_creates
|
33
|
+
where 'event <> ?', 'create'
|
34
|
+
end
|
35
|
+
|
36
|
+
# These methods accept a timestamp or a version and returns other versions that come before or after
|
37
|
+
def subsequent(obj)
|
38
|
+
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
39
|
+
where("#{table_name}.#{PaperTrail.timestamp_field} > ?", obj).
|
40
|
+
order("#{table_name}.#{PaperTrail.timestamp_field} ASC")
|
41
|
+
end
|
42
|
+
|
43
|
+
def preceding(obj)
|
44
|
+
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
45
|
+
where("#{table_name}.#{PaperTrail.timestamp_field} < ?", obj).
|
46
|
+
order("#{table_name}.#{PaperTrail.timestamp_field} DESC")
|
47
|
+
end
|
48
|
+
|
49
|
+
def between(start_time, end_time)
|
50
|
+
where("#{table_name}.#{PaperTrail.timestamp_field} > ? AND #{table_name}.#{PaperTrail.timestamp_field} < ?",
|
51
|
+
start_time, end_time).order("#{table_name}.#{PaperTrail.timestamp_field} ASC")
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns whether the `object` column is using the `json` type supported by PostgreSQL
|
55
|
+
def object_col_is_json?
|
56
|
+
@object_col_is_json ||= columns_hash['object'].type == :json
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL
|
60
|
+
def object_changes_col_is_json?
|
61
|
+
@object_changes_col_is_json ||= columns_hash['object_changes'].type == :json
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Restore the item from this version.
|
66
|
+
#
|
67
|
+
# This will automatically restore all :has_one associations as they were "at the time",
|
68
|
+
# if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
|
69
|
+
# to work so you can either change the lookback period (from the default 3 seconds) or
|
70
|
+
# opt out.
|
71
|
+
#
|
72
|
+
# Options:
|
73
|
+
# :has_one set to `false` to opt out of has_one reification.
|
74
|
+
# set to a float to change the lookback time (check whether your db supports
|
75
|
+
# sub-second datetimes if you want them).
|
76
|
+
def reify(options = {})
|
77
|
+
return nil if object.nil?
|
78
|
+
|
79
|
+
without_identity_map do
|
80
|
+
options[:has_one] = 3 if options[:has_one] == true
|
81
|
+
options.reverse_merge! :has_one => false
|
82
|
+
|
83
|
+
attrs = self.class.object_col_is_json? ? object : PaperTrail.serializer.load(object)
|
84
|
+
|
85
|
+
# Normally a polymorphic belongs_to relationship allows us
|
86
|
+
# to get the object we belong to by calling, in this case,
|
87
|
+
# `item`. However this returns nil if `item` has been
|
88
|
+
# destroyed, and we need to be able to retrieve destroyed
|
89
|
+
# objects.
|
90
|
+
#
|
91
|
+
# In this situation we constantize the `item_type` to get hold of
|
92
|
+
# the class...except when the stored object's attributes
|
93
|
+
# include a `type` key. If this is the case, the object
|
94
|
+
# we belong to is using single table inheritance and the
|
95
|
+
# `item_type` will be the base class, not the actual subclass.
|
96
|
+
# If `type` is present but empty, the class is the base class.
|
97
|
+
|
98
|
+
if item
|
99
|
+
model = item
|
100
|
+
# Look for attributes that exist in the model and not in this version. These attributes should be set to nil.
|
101
|
+
(model.attribute_names - attrs.keys).each { |k| attrs[k] = nil }
|
102
|
+
else
|
103
|
+
inheritance_column_name = item_type.constantize.inheritance_column
|
104
|
+
class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
|
105
|
+
klass = class_name.constantize
|
106
|
+
model = klass.new
|
107
|
+
end
|
108
|
+
|
109
|
+
model.class.unserialize_attributes_for_paper_trail attrs
|
110
|
+
|
111
|
+
# Set all the attributes in this version on the model
|
112
|
+
attrs.each do |k, v|
|
113
|
+
if model.respond_to?("#{k}=")
|
114
|
+
model[k.to_sym] = v
|
115
|
+
else
|
116
|
+
logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
model.send "#{model.class.version_association_name}=", self
|
121
|
+
|
122
|
+
unless options[:has_one] == false
|
123
|
+
reify_has_ones model, options[:has_one]
|
124
|
+
end
|
125
|
+
|
126
|
+
model
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns what changed in this version of the item. Cf. `ActiveModel::Dirty#changes`.
|
131
|
+
# Returns `nil` if your `versions` table does not have an `object_changes` text column.
|
132
|
+
def changeset
|
133
|
+
return nil unless self.class.column_names.include? 'object_changes'
|
134
|
+
|
135
|
+
_changes = self.class.object_changes_col_is_json? ? object_changes : PaperTrail.serializer.load(object_changes)
|
136
|
+
@changeset ||= HashWithIndifferentAccess.new(_changes).tap do |changes|
|
137
|
+
item_type.constantize.unserialize_attribute_changes(changes)
|
138
|
+
end
|
139
|
+
rescue
|
140
|
+
{}
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns who put the item into the state stored in this version.
|
144
|
+
def originator
|
145
|
+
@originator ||= previous.whodunnit rescue nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns who changed the item from the state it had in this version.
|
149
|
+
# This is an alias for `whodunnit`.
|
150
|
+
def terminator
|
151
|
+
@terminator ||= whodunnit
|
152
|
+
end
|
153
|
+
alias_method :version_author, :terminator
|
154
|
+
|
155
|
+
def sibling_versions(reload = false)
|
156
|
+
@sibling_versions = nil if reload == true
|
157
|
+
@sibling_versions ||= self.class.with_item_keys(item_type, item_id)
|
158
|
+
end
|
159
|
+
|
160
|
+
def next
|
161
|
+
@next ||= sibling_versions.subsequent(self).first
|
162
|
+
end
|
163
|
+
|
164
|
+
def previous
|
165
|
+
@previous ||= sibling_versions.preceding(self).first
|
166
|
+
end
|
167
|
+
|
168
|
+
def index
|
169
|
+
table_name = self.class.table_name
|
170
|
+
@index ||= sibling_versions.
|
171
|
+
select(["#{table_name}.#{PaperTrail.timestamp_field}", "#{table_name}.#{self.class.primary_key}"]).
|
172
|
+
order("#{table_name}.#{PaperTrail.timestamp_field} ASC").index(self)
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
# In Rails 3.1+, calling reify on a previous version confuses the
|
178
|
+
# IdentityMap, if enabled. This prevents insertion into the map.
|
179
|
+
def without_identity_map(&block)
|
180
|
+
if defined?(::ActiveRecord::IdentityMap) && ::ActiveRecord::IdentityMap.respond_to?(:without)
|
181
|
+
::ActiveRecord::IdentityMap.without(&block)
|
182
|
+
else
|
183
|
+
block.call
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Restore the `model`'s has_one associations as they were when this version was
|
188
|
+
# superseded by the next (because that's what the user was looking at when they
|
189
|
+
# made the change).
|
190
|
+
#
|
191
|
+
# The `lookback` sets how many seconds before the model's change we go.
|
192
|
+
def reify_has_ones(model, lookback)
|
193
|
+
model.class.reflect_on_all_associations(:has_one).each do |assoc|
|
194
|
+
child = model.send assoc.name
|
195
|
+
if child.respond_to? :version_at
|
196
|
+
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
|
197
|
+
# Ideally we want the version of the child as it was just before the parent was updated...
|
198
|
+
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
|
199
|
+
# updated on the same form), it's impossible to tell when the overall update started;
|
200
|
+
# and therefore impossible to know when "just before" was.
|
201
|
+
if (child_as_it_was = child.version_at(send(PaperTrail.timestamp_field) - lookback.seconds))
|
202
|
+
child_as_it_was.attributes.each do |k,v|
|
203
|
+
model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
|
204
|
+
end
|
205
|
+
else
|
206
|
+
model.send "#{assoc.name}=", nil
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# checks to see if a value has been set for the `version_limit` config option, and if so enforces it
|
213
|
+
def enforce_version_limit!
|
214
|
+
return unless PaperTrail.config.version_limit.is_a? Numeric
|
215
|
+
previous_versions = sibling_versions.not_creates
|
216
|
+
return unless previous_versions.size > PaperTrail.config.version_limit
|
217
|
+
excess_previous_versions = previous_versions - previous_versions.last(PaperTrail.config.version_limit)
|
218
|
+
excess_previous_versions.map(&:destroy)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
data/paper_trail.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.platform = Gem::Platform::RUBY
|
8
8
|
s.summary = "Track changes to your models' data. Good for auditing or versioning."
|
9
9
|
s.description = s.summary
|
10
|
-
s.homepage = '
|
10
|
+
s.homepage = 'https://github.com/airblade/paper_trail'
|
11
11
|
s.authors = ['Andy Stewart', 'Ben Atkins']
|
12
12
|
s.email = 'batkinz@gmail.com'
|
13
13
|
s.license = 'MIT'
|
@@ -17,6 +17,8 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ['lib']
|
19
19
|
|
20
|
+
s.required_rubygems_version = '>= 1.3.6'
|
21
|
+
|
20
22
|
s.add_dependency 'activerecord', ['>= 3.0', '< 5.0']
|
21
23
|
s.add_dependency 'activesupport', ['>= 3.0', '< 5.0']
|
22
24
|
|
@@ -2,49 +2,31 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe PaperTrail::VersionConcern do
|
4
4
|
|
5
|
-
before(:all)
|
6
|
-
module Foo
|
7
|
-
class Base < ActiveRecord::Base
|
8
|
-
self.abstract_class = true
|
9
|
-
end
|
10
|
-
|
11
|
-
class Document < Base
|
12
|
-
has_paper_trail :class_name => 'Foo::Version'
|
13
|
-
end
|
14
|
-
|
15
|
-
class Version < Base
|
16
|
-
include PaperTrail::VersionConcern
|
17
|
-
end
|
18
|
-
end
|
19
|
-
Foo::Base.establish_connection(:adapter => 'sqlite3', :database => File.expand_path('../../../test/dummy/db/test-foo.sqlite3', __FILE__))
|
20
|
-
|
21
|
-
module Bar
|
22
|
-
class Base < ActiveRecord::Base
|
23
|
-
self.abstract_class = true
|
24
|
-
end
|
25
|
-
|
26
|
-
class Document < Base
|
27
|
-
has_paper_trail :class_name => 'Bar::Version'
|
28
|
-
end
|
29
|
-
|
30
|
-
class Version < Base
|
31
|
-
include PaperTrail::VersionConcern
|
32
|
-
end
|
33
|
-
end
|
34
|
-
Bar::Base.establish_connection(:adapter => 'sqlite3', :database => File.expand_path('../../../test/dummy/db/test-bar.sqlite3', __FILE__))
|
35
|
-
end
|
5
|
+
before(:all) { require 'support/alt_db_init' }
|
36
6
|
|
37
7
|
it 'allows included class to have different connections' do
|
38
|
-
Foo::Version.connection.should_not
|
8
|
+
Foo::Version.connection.should_not == Bar::Version.connection
|
39
9
|
end
|
40
10
|
|
41
11
|
it 'allows custom version class to share connection with superclass' do
|
42
|
-
Foo::Version.connection.should
|
43
|
-
Bar::Version.connection.should
|
12
|
+
Foo::Version.connection.should == Foo::Document.connection
|
13
|
+
Bar::Version.connection.should == Bar::Document.connection
|
44
14
|
end
|
45
15
|
|
46
16
|
it 'can be used with class_name option' do
|
47
|
-
Foo::Document.version_class_name.should
|
48
|
-
Bar::Document.version_class_name.should
|
17
|
+
Foo::Document.version_class_name.should == 'Foo::Version'
|
18
|
+
Bar::Document.version_class_name.should == 'Bar::Version'
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'persistence', :versioning => true do
|
22
|
+
before do
|
23
|
+
@foo_doc = Foo::Document.create!(:name => 'foobar')
|
24
|
+
@bar_doc = Bar::Document.create!(:name => 'raboof')
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should store versions in the correct corresponding db location' do
|
28
|
+
@foo_doc.versions.first.should be_instance_of(Foo::Version)
|
29
|
+
@bar_doc.versions.first.should be_instance_of(Bar::Version)
|
30
|
+
end
|
49
31
|
end
|
50
32
|
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
RSpec.configure do |c|
|
2
|
+
c.order = 'defined'
|
3
|
+
end
|
4
|
+
|
1
5
|
require 'spec_helper'
|
2
6
|
|
3
7
|
describe "Articles" do
|
@@ -7,7 +11,9 @@ describe "Articles" do
|
|
7
11
|
specify { PaperTrail.enabled?.should be_false }
|
8
12
|
|
9
13
|
it "should not create a version" do
|
14
|
+
PaperTrail.enabled_for_controller?.should be_true
|
10
15
|
expect { post articles_path(valid_params) }.to_not change(PaperTrail::Version, :count)
|
16
|
+
PaperTrail.enabled_for_controller?.should be_false
|
11
17
|
end
|
12
18
|
|
13
19
|
it "should not leak the state of the `PaperTrail.enabled_for_controlller?` into the next test" do
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
# This file copies the test database into locations for the `Foo` and `Bar` namespace,
|
2
|
+
# then defines those namespaces, then establishes the sqlite3 connection for the namespaces
|
3
|
+
# to simulate an application with multiple database connections.
|
4
|
+
|
5
|
+
db_directory = "#{Rails.root}/db"
|
6
|
+
# setup alternate databases
|
7
|
+
if RUBY_VERSION.to_f >= 1.9
|
8
|
+
FileUtils.cp "#{db_directory}/test.sqlite3", "#{db_directory}/test-foo.sqlite3"
|
9
|
+
FileUtils.cp "#{db_directory}/test.sqlite3", "#{db_directory}/test-bar.sqlite3"
|
10
|
+
else
|
11
|
+
require 'ftools'
|
12
|
+
File.cp "#{db_directory}/test.sqlite3", "#{db_directory}/test-foo.sqlite3"
|
13
|
+
File.cp "#{db_directory}/test.sqlite3", "#{db_directory}/test-bar.sqlite3"
|
14
|
+
end
|
15
|
+
|
16
|
+
module Foo
|
17
|
+
class Base < ActiveRecord::Base
|
18
|
+
self.abstract_class = true
|
19
|
+
end
|
20
|
+
|
21
|
+
class Version < Base
|
22
|
+
include PaperTrail::VersionConcern
|
23
|
+
end
|
24
|
+
|
25
|
+
class Document < Base
|
26
|
+
has_paper_trail :class_name => 'Foo::Version'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
Foo::Base.establish_connection(:adapter => 'sqlite3', :database => "#{db_directory}/test-foo.sqlite3")
|
30
|
+
|
31
|
+
module Bar
|
32
|
+
class Base < ActiveRecord::Base
|
33
|
+
self.abstract_class = true
|
34
|
+
end
|
35
|
+
|
36
|
+
class Version < Base
|
37
|
+
include PaperTrail::VersionConcern
|
38
|
+
end
|
39
|
+
|
40
|
+
class Document < Base
|
41
|
+
has_paper_trail :class_name => 'Bar::Version'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
Bar::Base.establish_connection(:adapter => 'sqlite3', :database => "#{db_directory}/test-bar.sqlite3")
|
@@ -6,7 +6,7 @@ class ThreadSafetyTest < ActionController::TestCase
|
|
6
6
|
|
7
7
|
slow_thread = Thread.new do
|
8
8
|
controller = TestController.new
|
9
|
-
controller.send :
|
9
|
+
controller.send :set_paper_trail_whodunnit
|
10
10
|
begin
|
11
11
|
sleep 0.001
|
12
12
|
end while blocked
|
@@ -15,7 +15,7 @@ class ThreadSafetyTest < ActionController::TestCase
|
|
15
15
|
|
16
16
|
fast_thread = Thread.new do
|
17
17
|
controller = TestController.new
|
18
|
-
controller.send :
|
18
|
+
controller.send :set_paper_trail_whodunnit
|
19
19
|
who = PaperTrail.whodunnit
|
20
20
|
blocked = false
|
21
21
|
who
|
data/test/paper_trail_test.rb
CHANGED
@@ -5,6 +5,10 @@ class PaperTrailTest < ActiveSupport::TestCase
|
|
5
5
|
assert_kind_of Module, PaperTrail::Version
|
6
6
|
end
|
7
7
|
|
8
|
+
test 'Version Number' do
|
9
|
+
assert PaperTrail.const_defined?(:VERSION)
|
10
|
+
end
|
11
|
+
|
8
12
|
test 'create with plain model class' do
|
9
13
|
widget = Widget.create
|
10
14
|
assert_equal 1, widget.versions.length
|
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: 3.0.0
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Stewart
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-11
|
12
|
+
date: 2013-12-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -200,6 +200,7 @@ files:
|
|
200
200
|
- lib/paper_trail/serializers/json.rb
|
201
201
|
- lib/paper_trail/serializers/yaml.rb
|
202
202
|
- lib/paper_trail/version.rb
|
203
|
+
- lib/paper_trail/version_concern.rb
|
203
204
|
- lib/paper_trail/version_number.rb
|
204
205
|
- paper_trail.gemspec
|
205
206
|
- spec/models/joined_version_spec.rb
|
@@ -209,6 +210,7 @@ files:
|
|
209
210
|
- spec/paper_trail_spec.rb
|
210
211
|
- spec/requests/articles_spec.rb
|
211
212
|
- spec/spec_helper.rb
|
213
|
+
- spec/support/alt_db_init.rb
|
212
214
|
- test/custom_json_serializer.rb
|
213
215
|
- test/dummy/Rakefile
|
214
216
|
- test/dummy/app/controllers/application_controller.rb
|
@@ -285,7 +287,7 @@ files:
|
|
285
287
|
- test/unit/serializers/yaml_test.rb
|
286
288
|
- test/unit/timestamp_test.rb
|
287
289
|
- test/unit/version_test.rb
|
288
|
-
homepage:
|
290
|
+
homepage: https://github.com/airblade/paper_trail
|
289
291
|
licenses:
|
290
292
|
- MIT
|
291
293
|
metadata: {}
|
@@ -300,12 +302,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
300
302
|
version: '0'
|
301
303
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
302
304
|
requirements:
|
303
|
-
- - '
|
305
|
+
- - '>='
|
304
306
|
- !ruby/object:Gem::Version
|
305
|
-
version: 1.3.
|
307
|
+
version: 1.3.6
|
306
308
|
requirements: []
|
307
309
|
rubyforge_project:
|
308
|
-
rubygems_version: 2.1.
|
310
|
+
rubygems_version: 2.1.11
|
309
311
|
signing_key:
|
310
312
|
specification_version: 4
|
311
313
|
summary: Track changes to your models' data. Good for auditing or versioning.
|
@@ -317,6 +319,7 @@ test_files:
|
|
317
319
|
- spec/paper_trail_spec.rb
|
318
320
|
- spec/requests/articles_spec.rb
|
319
321
|
- spec/spec_helper.rb
|
322
|
+
- spec/support/alt_db_init.rb
|
320
323
|
- test/custom_json_serializer.rb
|
321
324
|
- test/dummy/Rakefile
|
322
325
|
- test/dummy/app/controllers/application_controller.rb
|