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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0c33a2d2b003d082bede1ad845e1a4b158707782
4
- data.tar.gz: 4e47a0a036926d20dca09f9cf22ab7c7c492dadd
3
+ metadata.gz: c90dbeb20af0ebaa74cf679e80b9d78b299af92d
4
+ data.tar.gz: b2f30b7cc6460365f92e6d5e7c92873624f1187c
5
5
  SHA512:
6
- metadata.gz: ae06bb1637cea1664c1f75ede2ac0195cac6f44a490d2988921a71d5461ac8d65cd96171ddbe4ad8d273340c9cba21aed937fdf755bdd56438d96ee49e261deb
7
- data.tar.gz: 63587c8419be2ed421ffee4019564b84f64ed1536f5e323437070aab0144e3216800f32698154f98193d383087a0cc2e7ed04f647ed451aa73da8c2a281fbd79
6
+ metadata.gz: cca17faa39ff342de70157ee32913040fe1ad6098d0f0b446ff470a944071d1b50486c04eba658f021f4ef65d75637cdb0df78cb306275011748ec38847ec887
7
+ data.tar.gz: 94fc8094261b8c401e228142eeba8d51f5aa5c54a67b522f54e83b9f42562d80feeb981e86b5c73b45094d045d92b3e7f0285571b0e6dd1c78e2d60e07586225
data/CHANGELOG.md CHANGED
@@ -1,5 +1,6 @@
1
- ## 3.0.0 (Unreleased)
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', '>= 3.0.0.rc1'`
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 [`Sinatra`](http://www.sinatrarb.com), your Sinatra app must be using `ActiveRecord` 3 or greater.
61
- It is also recommended to use the [`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 `Sinatra ActiveRecord Extension`,
63
- steps for setting up your app with `PaperTrail` will look something like this:
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', '>= 3.0.0.rc1'`
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 `version`'s `whodunnit` column. Note that this column is a 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:
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 migration or in `rails console` you can set who is responsible like this:
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
- N.B. 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`.
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 `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.
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 define a `versions` table in the database so Rails can instantiate the version superclass.
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.all(:conditions => ['author_id = ?', author_id])
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
- before do
3
- ::PaperTrail.enabled = false
4
- ::PaperTrail.enabled_for_controller = true
5
- ::PaperTrail.whodunnit = nil
6
- ::PaperTrail.controller_info = {} if defined? ::Rails
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 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
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 paper_trail_enabled_for_controller
68
+ ::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
75
69
  end
76
70
 
77
71
  end
@@ -1,224 +1,7 @@
1
- require 'active_support/concern'
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
@@ -1,3 +1,3 @@
1
1
  module PaperTrail
2
- VERSION = '3.0.0.rc2'
2
+ VERSION = '3.0.0'
3
3
  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 = 'http://github.com/airblade/paper_trail'
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) do
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 eq Bar::Version.connection
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 eq Foo::Document.connection
43
- Bar::Version.connection.should eq Bar::Document.connection
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 eq 'Foo::Version'
48
- Bar::Document.version_class_name.should eq 'Bar::Version'
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
@@ -39,5 +39,5 @@ RSpec.configure do |config|
39
39
  # order dependency and want to debug it, you can fix the order by providing
40
40
  # the seed, which is printed after each run.
41
41
  # --seed 1234
42
- # config.order = 'random'
42
+ config.order ||= 'random'
43
43
  end
@@ -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 :set_whodunnit
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 :set_whodunnit
18
+ controller.send :set_paper_trail_whodunnit
19
19
  who = PaperTrail.whodunnit
20
20
  blocked = false
21
21
  who
@@ -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.rc2
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-16 00:00:00.000000000 Z
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: http://github.com/airblade/paper_trail
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.1
307
+ version: 1.3.6
306
308
  requirements: []
307
309
  rubyforge_project:
308
- rubygems_version: 2.1.10
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