paper_trail 3.0.0.rc2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|