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 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