paper_trail 3.0.2 → 3.0.5
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 +23 -0
- data/README.md +14 -9
- data/gemfiles/3.0.gemfile +1 -1
- data/lib/paper_trail.rb +10 -16
- data/lib/paper_trail/frameworks/active_record.rb +5 -0
- data/lib/paper_trail/{version.rb → frameworks/active_record/models/paper_trail/version.rb} +1 -1
- data/lib/paper_trail/frameworks/rails.rb +2 -70
- data/lib/paper_trail/frameworks/rails/controller.rb +78 -0
- data/lib/paper_trail/frameworks/rails/engine.rb +7 -0
- data/lib/paper_trail/has_paper_trail.rb +10 -5
- data/lib/paper_trail/serializers/json.rb +19 -0
- data/lib/paper_trail/serializers/yaml.rb +6 -0
- data/lib/paper_trail/version_concern.rb +34 -15
- data/lib/paper_trail/version_number.rb +16 -1
- data/paper_trail.gemspec +1 -1
- data/spec/models/post_with_status_spec.rb +17 -0
- data/spec/models/version_spec.rb +44 -0
- data/spec/models/widget_spec.rb +61 -4
- data/spec/modules/version_number_spec.rb +44 -0
- data/spec/spec_helper.rb +1 -1
- data/test/dummy/app/models/post_with_status.rb +8 -0
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +5 -1
- data/test/unit/model_test.rb +0 -1
- data/test/unit/serializers/json_test.rb +35 -0
- data/test/unit/serializers/yaml_test.rb +8 -0
- data/test/unit/version_test.rb +2 -2
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ee3ec4b4dd8dfad13cbe5e9b46ccfa30f3318a3
|
4
|
+
data.tar.gz: 20559b46236a0318c4b7e2eb940bfbd79600cfad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cf548b9641d5994bf67b3e886d4ed60fed0d6d16e9134e6374ca8ecaaa7ff3662fd4d97684e2bd1b59cffa710f1e19a58c0d4679433483dcd471fdc4ef201ad
|
7
|
+
data.tar.gz: 3ae713d8799f7ce5b95b5787134eeaf3aada506b1c71a89d3e25e3703c1fbc5a809f183e0a62c521c65f473312261e5049843d01a54166243ddac7a994120332
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
## 3.0.5
|
2
|
+
|
3
|
+
- [#401](https://github.com/airblade/paper_trail/issues/401) / [#406](https://github.com/airblade/paper_trail/issues/406)
|
4
|
+
`PaperTrail::Version` class is not loaded via a `Rails::Engine`, even when the gem is used with in Rails. This feature has
|
5
|
+
will be re-introduced in version `3.1.0`.
|
6
|
+
|
7
|
+
## 3.0.3
|
8
|
+
*This version was yanked from RubyGems and has been replaced by version `3.0.5`, which is identical but does not eager load
|
9
|
+
in the `PaperTrail::Version` class through a `Rails::Engine` when the gem is used on Rails since it was causing issues for some users.*
|
10
|
+
|
11
|
+
- [#386](https://github.com/airblade/paper_trail/issues/386) - Fix eager loading of `versions` association with custom class name
|
12
|
+
in `ActiveRecord` 4.1.
|
13
|
+
- [#384](https://github.com/airblade/paper_trail/issues/384) - Fix `VersionConcern#originator` instance method.
|
14
|
+
- [#383](https://github.com/airblade/paper_trail/pull/383) - Make gem compatible with `ActiveRecord::Enum` (available in `ActiveRecord` 4.1+).
|
15
|
+
- [#380](https://github.com/airblade/paper_trail/pull/380) / [#377](https://github.com/airblade/paper_trail/issues/377) -
|
16
|
+
Add `VersionConcern#where_object` instance method; acts as a helper for querying against the `object` column in versions table.
|
17
|
+
- [#373](https://github.com/airblade/paper_trail/pull/373) - Fix default sort order for the `versions` association in `ActiveRecord` 4.1.
|
18
|
+
- [#372](https://github.com/airblade/paper_trail/pull/372) - Use [Arel](https://github.com/rails/arel) for SQL construction.
|
19
|
+
- [#365](https://github.com/airblade/paper_trail/issues/365) - `VersionConcern#version_at` should return `nil` when receiving a timestamp
|
20
|
+
that occured after the object was destroyed.
|
21
|
+
- Expand `PaperTrail::VERSION` into a module, mimicking the form used by Rails to give it some additional modularity & versatility.
|
22
|
+
- Fixed `VersionConcern#index` instance method so that it conforms to using the primary key for ordering when possible.
|
23
|
+
|
1
24
|
## 3.0.2
|
2
25
|
|
3
26
|
- [#357](https://github.com/airblade/paper_trail/issues/357) - If a `Version` instance is reified and then persisted at that state,
|
data/README.md
CHANGED
@@ -40,9 +40,9 @@ The Rails 2.3 code is on the [`rails2`](https://github.com/airblade/paper_trail/
|
|
40
40
|
|
41
41
|
### Rails 3 & 4
|
42
42
|
|
43
|
-
1. Add
|
43
|
+
1. Add PaperTrail to your `Gemfile`.
|
44
44
|
|
45
|
-
`gem 'paper_trail', '~> 3.0.
|
45
|
+
`gem 'paper_trail', '~> 3.0.5'`
|
46
46
|
|
47
47
|
2. Generate a migration which will add a `versions` table to your database.
|
48
48
|
|
@@ -56,15 +56,15 @@ The Rails 2.3 code is on the [`rails2`](https://github.com/airblade/paper_trail/
|
|
56
56
|
|
57
57
|
### Sinatra
|
58
58
|
|
59
|
-
In order to configure
|
60
|
-
your `Sinatra` app must be using `ActiveRecord` 3 or
|
59
|
+
In order to configure PaperTrail for usage with [Sinatra](http://www.sinatrarb.com),
|
60
|
+
your `Sinatra` app must be using `ActiveRecord` 3 or 4. It is also recommended to use the
|
61
61
|
[Sinatra ActiveRecord Extension](https://github.com/janko-m/sinatra-activerecord) or something similar for managing
|
62
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
|
63
|
+
`Sinatra ActiveRecord Extension`, steps for setting up your app with PaperTrail will look something like this:
|
64
64
|
|
65
|
-
1. Add
|
65
|
+
1. Add PaperTrail to your `Gemfile`.
|
66
66
|
|
67
|
-
`gem 'paper_trail', '~> 3.0.
|
67
|
+
`gem 'paper_trail', '~> 3.0.5'`
|
68
68
|
|
69
69
|
2. Generate a migration to add a `versions` table to your database.
|
70
70
|
|
@@ -813,11 +813,14 @@ If you disable PaperTrail in your test environment but want to enable it for spe
|
|
813
813
|
# in test/test_helper.rb
|
814
814
|
def with_versioning
|
815
815
|
was_enabled = PaperTrail.enabled?
|
816
|
+
was_enabled_for_controller = PaperTrail.enabled_for_controller?
|
816
817
|
PaperTrail.enabled = true
|
818
|
+
PaperTrail.enabled_for_controller = true
|
817
819
|
begin
|
818
820
|
yield
|
819
821
|
ensure
|
820
822
|
PaperTrail.enabled = was_enabled
|
823
|
+
PaperTrail.enabled_for_controller = was_enabled_for_controller
|
821
824
|
end
|
822
825
|
end
|
823
826
|
```
|
@@ -1004,9 +1007,9 @@ Spork.prefork do
|
|
1004
1007
|
end
|
1005
1008
|
```
|
1006
1009
|
|
1007
|
-
### Zeus
|
1010
|
+
### Zeus or Spring
|
1008
1011
|
|
1009
|
-
If you wish to use the `RSpec` or `Cucumber`
|
1012
|
+
If you wish to use the `RSpec` or `Cucumber` helpers with [Zeus](https://github.com/burke/zeus) or [Spring](https://github.com/rails/spring), you will need to
|
1010
1013
|
manually require the helper(s) in your test helper, like so:
|
1011
1014
|
|
1012
1015
|
```ruby
|
@@ -1032,6 +1035,7 @@ export DB=sqlite # this is default
|
|
1032
1035
|
|
1033
1036
|
## Articles
|
1034
1037
|
|
1038
|
+
* [Versioning with PaperTrail](http://www.sitepoint.com/versioning-papertrail), [Ilya Bodrov](http://www.sitepoint.com/author/ibodrov), 10th April 2014
|
1035
1039
|
* [Using PaperTrail to track stack traces](http://rubyrailsexpert.com/?p=36), T James Corcoran's blog, 1st October 2013.
|
1036
1040
|
* [RailsCast #255 - Undo with PaperTrail](http://railscasts.com/episodes/255-undo-with-paper-trail), 28th February 2011.
|
1037
1041
|
* [Keep a Paper Trail with PaperTrail](http://www.linux-mag.com/id/7528), Linux Magazine, 16th September 2009.
|
@@ -1046,6 +1050,7 @@ Please use GitHub's [issue tracker](http://github.com/airblade/paper_trail/issue
|
|
1046
1050
|
|
1047
1051
|
Many thanks to:
|
1048
1052
|
|
1053
|
+
* [Dmitry Polushkin](https://github.com/dmitry)
|
1049
1054
|
* [Russell Osborne](https://github.com/rposborne)
|
1050
1055
|
* [Zachery Hostens](http://github.com/zacheryph)
|
1051
1056
|
* [Jeremy Weiskotten](http://github.com/jeremyw)
|
data/gemfiles/3.0.gemfile
CHANGED
data/lib/paper_trail.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'paper_trail
|
4
|
-
|
1
|
+
# Require core library
|
2
|
+
Dir[File.join(File.dirname(__FILE__), 'paper_trail', '*.rb')].each do |file|
|
3
|
+
require File.join('paper_trail', File.basename(file, '.rb'))
|
4
|
+
end
|
5
5
|
|
6
6
|
# Require serializers
|
7
|
-
Dir[File.join(File.dirname(__FILE__), 'paper_trail', 'serializers', '*.rb')].each
|
7
|
+
Dir[File.join(File.dirname(__FILE__), 'paper_trail', 'serializers', '*.rb')].each do |file|
|
8
|
+
require File.join('paper_trail', 'serializers', File.basename(file, '.rb'))
|
9
|
+
end
|
8
10
|
|
9
11
|
module PaperTrail
|
10
12
|
extend PaperTrail::Cleaner
|
@@ -118,21 +120,13 @@ unless PaperTrail.active_record_protected_attributes?
|
|
118
120
|
rescue LoadError; end # will rescue if `ProtectedAttributes` gem is not available
|
119
121
|
end
|
120
122
|
|
121
|
-
require 'paper_trail/version'
|
122
|
-
|
123
123
|
ActiveSupport.on_load(:active_record) do
|
124
124
|
include PaperTrail::Model
|
125
125
|
end
|
126
126
|
|
127
127
|
# Require frameworks
|
128
|
+
require 'paper_trail/frameworks/active_record'
|
128
129
|
require 'paper_trail/frameworks/sinatra'
|
129
|
-
require 'paper_trail/frameworks/
|
130
|
+
require 'paper_trail/frameworks/rails' if defined? Rails
|
131
|
+
require 'paper_trail/frameworks/rspec' if defined? RSpec::Core
|
130
132
|
require 'paper_trail/frameworks/cucumber' if defined? World
|
131
|
-
|
132
|
-
if defined?(ActionController)
|
133
|
-
require 'paper_trail/frameworks/rails'
|
134
|
-
|
135
|
-
ActiveSupport.on_load(:action_controller) do
|
136
|
-
include PaperTrail::Rails::Controller
|
137
|
-
end
|
138
|
-
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
# This file only needs to be loaded if the gem is being used outside of Rails, since otherwise
|
2
|
+
# the model(s) will get loaded in via the `Rails::Engine`
|
3
|
+
Dir[File.join(File.dirname(__FILE__), 'active_record', 'models', 'paper_trail', '*.rb')].each do |file|
|
4
|
+
require "paper_trail/frameworks/active_record/models/paper_trail/#{File.basename(file, '.rb')}"
|
5
|
+
end
|
@@ -1,74 +1,6 @@
|
|
1
|
+
require 'paper_trail/frameworks/rails/controller'
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
module Rails
|
3
|
-
module Controller
|
4
|
-
|
5
|
-
def self.included(base)
|
6
|
-
base.before_filter :set_paper_trail_enabled_for_controller
|
7
|
-
base.before_filter :set_paper_trail_whodunnit, :set_paper_trail_controller_info
|
8
|
-
end
|
9
|
-
|
10
|
-
protected
|
11
|
-
|
12
|
-
# Returns the user who is responsible for any changes that occur.
|
13
|
-
# By default this calls `current_user` and returns the result.
|
14
|
-
#
|
15
|
-
# Override this method in your controller to call a different
|
16
|
-
# method, e.g. `current_person`, or anything you like.
|
17
|
-
def user_for_paper_trail
|
18
|
-
return unless defined?(current_user)
|
19
|
-
ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
|
20
|
-
rescue NoMethodError
|
21
|
-
current_user
|
22
|
-
end
|
23
|
-
|
24
|
-
# Returns any information about the controller or request that you
|
25
|
-
# want PaperTrail to store alongside any changes that occur. By
|
26
|
-
# default this returns an empty hash.
|
27
|
-
#
|
28
|
-
# Override this method in your controller to return a hash of any
|
29
|
-
# information you need. The hash's keys must correspond to columns
|
30
|
-
# in your `versions` table, so don't forget to add any new columns
|
31
|
-
# you need.
|
32
|
-
#
|
33
|
-
# For example:
|
34
|
-
#
|
35
|
-
# {:ip => request.remote_ip, :user_agent => request.user_agent}
|
36
|
-
#
|
37
|
-
# The columns `ip` and `user_agent` must exist in your `versions` # table.
|
38
|
-
#
|
39
|
-
# Use the `:meta` option to `PaperTrail::Model::ClassMethods.has_paper_trail`
|
40
|
-
# to store any extra model-level data you need.
|
41
|
-
def info_for_paper_trail
|
42
|
-
{}
|
43
|
-
end
|
44
|
-
|
45
|
-
# Returns `true` (default) or `false` depending on whether PaperTrail should
|
46
|
-
# be active for the current request.
|
47
|
-
#
|
48
|
-
# Override this method in your controller to specify when PaperTrail should
|
49
|
-
# be off.
|
50
|
-
def paper_trail_enabled_for_controller
|
51
|
-
::PaperTrail.enabled?
|
52
|
-
end
|
53
|
-
|
54
|
-
private
|
55
|
-
|
56
|
-
# Tells PaperTrail whether versions should be saved in the current request.
|
57
|
-
def set_paper_trail_enabled_for_controller
|
58
|
-
::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
|
59
|
-
end
|
60
|
-
|
61
|
-
# Tells PaperTrail who is responsible for any changes that occur.
|
62
|
-
def set_paper_trail_whodunnit
|
63
|
-
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
64
|
-
end
|
65
|
-
|
66
|
-
# Tells PaperTrail any information from the controller you want
|
67
|
-
# to store alongside any changes that occur.
|
68
|
-
def set_paper_trail_controller_info
|
69
|
-
::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
70
|
-
end
|
71
|
-
|
72
|
-
end
|
73
5
|
end
|
74
6
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module PaperTrail
|
2
|
+
module Rails
|
3
|
+
module Controller
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.before_filter :set_paper_trail_enabled_for_controller
|
7
|
+
base.before_filter :set_paper_trail_whodunnit, :set_paper_trail_controller_info
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Returns the user who is responsible for any changes that occur.
|
13
|
+
# By default this calls `current_user` and returns the result.
|
14
|
+
#
|
15
|
+
# Override this method in your controller to call a different
|
16
|
+
# method, e.g. `current_person`, or anything you like.
|
17
|
+
def user_for_paper_trail
|
18
|
+
return unless defined?(current_user)
|
19
|
+
ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
|
20
|
+
rescue NoMethodError
|
21
|
+
current_user
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns any information about the controller or request that you
|
25
|
+
# want PaperTrail to store alongside any changes that occur. By
|
26
|
+
# default this returns an empty hash.
|
27
|
+
#
|
28
|
+
# Override this method in your controller to return a hash of any
|
29
|
+
# information you need. The hash's keys must correspond to columns
|
30
|
+
# in your `versions` table, so don't forget to add any new columns
|
31
|
+
# you need.
|
32
|
+
#
|
33
|
+
# For example:
|
34
|
+
#
|
35
|
+
# {:ip => request.remote_ip, :user_agent => request.user_agent}
|
36
|
+
#
|
37
|
+
# The columns `ip` and `user_agent` must exist in your `versions` # table.
|
38
|
+
#
|
39
|
+
# Use the `:meta` option to `PaperTrail::Model::ClassMethods.has_paper_trail`
|
40
|
+
# to store any extra model-level data you need.
|
41
|
+
def info_for_paper_trail
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns `true` (default) or `false` depending on whether PaperTrail should
|
46
|
+
# be active for the current request.
|
47
|
+
#
|
48
|
+
# Override this method in your controller to specify when PaperTrail should
|
49
|
+
# be off.
|
50
|
+
def paper_trail_enabled_for_controller
|
51
|
+
::PaperTrail.enabled?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Tells PaperTrail whether versions should be saved in the current request.
|
57
|
+
def set_paper_trail_enabled_for_controller
|
58
|
+
::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
|
59
|
+
end
|
60
|
+
|
61
|
+
# Tells PaperTrail who is responsible for any changes that occur.
|
62
|
+
def set_paper_trail_whodunnit
|
63
|
+
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Tells PaperTrail any information from the controller you want
|
67
|
+
# to store alongside any changes that occur.
|
68
|
+
def set_paper_trail_controller_info
|
69
|
+
::PaperTrail.controller_info = info_for_paper_trail if ::PaperTrail.enabled_for_controller?
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if defined?(::ActionController)
|
76
|
+
::ActiveSupport.on_load(:action_controller) { include PaperTrail::Rails::Controller }
|
77
|
+
end
|
78
|
+
end
|
@@ -61,7 +61,7 @@ module PaperTrail
|
|
61
61
|
|
62
62
|
if ::ActiveRecord::VERSION::MAJOR >= 4 # `has_many` syntax for specifying order uses a lambda in Rails 4
|
63
63
|
has_many self.versions_association_name,
|
64
|
-
lambda {
|
64
|
+
lambda { order(model.timestamp_sort_order) },
|
65
65
|
:class_name => self.version_class_name, :as => :item
|
66
66
|
else
|
67
67
|
has_many self.versions_association_name,
|
@@ -174,7 +174,7 @@ module PaperTrail
|
|
174
174
|
|
175
175
|
# Returns who put the object into its current state.
|
176
176
|
def originator
|
177
|
-
|
177
|
+
(source_version || send(self.class.versions_association_name).last).try(:whodunnit)
|
178
178
|
end
|
179
179
|
|
180
180
|
# Returns the object (not a Version) as it was at the given timestamp.
|
@@ -182,7 +182,8 @@ module PaperTrail
|
|
182
182
|
# Because a version stores how its object looked *before* the change,
|
183
183
|
# we need to look for the first version created *after* the timestamp.
|
184
184
|
v = send(self.class.versions_association_name).subsequent(timestamp, true).first
|
185
|
-
|
185
|
+
return v.reify(reify_options) if v
|
186
|
+
self unless self.destroyed?
|
186
187
|
end
|
187
188
|
|
188
189
|
# Returns the objects (not Versions) as they were between the given times.
|
@@ -290,7 +291,7 @@ module PaperTrail
|
|
290
291
|
end.tap { |changes| self.class.serialize_attribute_changes(changes) }
|
291
292
|
end
|
292
293
|
|
293
|
-
# Invoked via
|
294
|
+
# Invoked via`after_update` callback for when a previous version is reified and then saved
|
294
295
|
def clear_version_instance!
|
295
296
|
send("#{self.class.version_association_name}=", nil)
|
296
297
|
end
|
@@ -342,9 +343,13 @@ module PaperTrail
|
|
342
343
|
all_timestamp_attributes.each do |column|
|
343
344
|
previous[column] = send(column) if self.class.column_names.include?(column.to_s) and not send(column).nil?
|
344
345
|
end
|
346
|
+
enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {}
|
345
347
|
previous.tap do |prev|
|
346
348
|
prev.id = id # `dup` clears the `id` so we add that back
|
347
|
-
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each
|
349
|
+
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
|
350
|
+
before = enums[attr][before] if enums[attr]
|
351
|
+
prev[attr] = before
|
352
|
+
end
|
348
353
|
end
|
349
354
|
end
|
350
355
|
|
@@ -12,6 +12,25 @@ module PaperTrail
|
|
12
12
|
def dump(object)
|
13
13
|
ActiveSupport::JSON.encode object
|
14
14
|
end
|
15
|
+
|
16
|
+
# Returns a SQL condition to be used to match the given field and value in
|
17
|
+
# the serialized object.
|
18
|
+
def where_object_condition(arel_field, field, value)
|
19
|
+
# Convert to JSON to handle strings and nulls correctly.
|
20
|
+
json_value = value.to_json
|
21
|
+
|
22
|
+
# If the value is a number, we need to ensure that we find the next
|
23
|
+
# character too, which is either `,` or `}`, to ensure that searching
|
24
|
+
# for the value 12 doesn't yield false positives when the value is
|
25
|
+
# 123.
|
26
|
+
if value.is_a? Numeric
|
27
|
+
arel_field.matches("%\"#{field}\":#{json_value},%").
|
28
|
+
or(
|
29
|
+
arel_field.matches("%\"#{field}\":#{json_value}}%"))
|
30
|
+
else
|
31
|
+
arel_field.matches("%\"#{field}\":#{json_value}%")
|
32
|
+
end
|
33
|
+
end
|
15
34
|
end
|
16
35
|
end
|
17
36
|
end
|
@@ -12,6 +12,12 @@ module PaperTrail
|
|
12
12
|
def dump(object)
|
13
13
|
::YAML.dump object
|
14
14
|
end
|
15
|
+
|
16
|
+
# Returns a SQL condition to be used to match the given field and value in
|
17
|
+
# the serialized object.
|
18
|
+
def where_object_condition(arel_field, field, value)
|
19
|
+
arel_field.matches("%\n#{field}: #{value}\n%")
|
20
|
+
end
|
15
21
|
end
|
16
22
|
end
|
17
23
|
end
|
@@ -37,35 +37,50 @@ module PaperTrail
|
|
37
37
|
# `timestamp_arg` receives `true`
|
38
38
|
def subsequent(obj, timestamp_arg = false)
|
39
39
|
if timestamp_arg != true && self.primary_key_is_int?
|
40
|
-
return where(
|
40
|
+
return where(arel_table[primary_key].gt(obj.id)).order(arel_table[primary_key].asc)
|
41
41
|
end
|
42
42
|
|
43
43
|
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
44
|
-
where(
|
44
|
+
where(arel_table[PaperTrail.timestamp_field].gt(obj)).order(self.timestamp_sort_order)
|
45
45
|
end
|
46
46
|
|
47
47
|
def preceding(obj, timestamp_arg = false)
|
48
48
|
if timestamp_arg != true && self.primary_key_is_int?
|
49
|
-
return where(
|
49
|
+
return where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
|
50
50
|
end
|
51
51
|
|
52
52
|
obj = obj.send(PaperTrail.timestamp_field) if obj.is_a?(self)
|
53
|
-
where(
|
53
|
+
where(arel_table[PaperTrail.timestamp_field].lt(obj)).order(self.timestamp_sort_order('desc'))
|
54
54
|
end
|
55
55
|
|
56
56
|
|
57
57
|
def between(start_time, end_time)
|
58
|
-
where(
|
59
|
-
start_time
|
58
|
+
where(
|
59
|
+
arel_table[PaperTrail.timestamp_field].gt(start_time).
|
60
|
+
and(arel_table[PaperTrail.timestamp_field].lt(end_time))
|
61
|
+
).order(self.timestamp_sort_order)
|
60
62
|
end
|
61
63
|
|
62
64
|
# defaults to using the primary key as the secondary sort order if possible
|
63
|
-
def timestamp_sort_order(
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
def timestamp_sort_order(direction = 'asc')
|
66
|
+
[arel_table[PaperTrail.timestamp_field].send(direction.downcase)].tap do |array|
|
67
|
+
array << arel_table[primary_key].send(direction.downcase) if self.primary_key_is_int?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Performs an attribute search on the serialized object by invoking the
|
72
|
+
# identically-named method in the serializer being used.
|
73
|
+
def where_object(args = {})
|
74
|
+
raise ArgumentError, 'expected to receive a Hash' unless args.is_a?(Hash)
|
75
|
+
arel_field = arel_table[:object]
|
76
|
+
|
77
|
+
where_conditions = args.map do |field, value|
|
78
|
+
PaperTrail.serializer.where_object_condition(arel_field, field, value)
|
79
|
+
end.reduce do |condition1, condition2|
|
80
|
+
condition1.and(condition2)
|
68
81
|
end
|
82
|
+
|
83
|
+
where(where_conditions)
|
69
84
|
end
|
70
85
|
|
71
86
|
def primary_key_is_int?
|
@@ -189,10 +204,14 @@ module PaperTrail
|
|
189
204
|
end
|
190
205
|
|
191
206
|
def index
|
192
|
-
|
193
|
-
@index ||=
|
194
|
-
|
195
|
-
|
207
|
+
table = self.class.arel_table unless @index
|
208
|
+
@index ||=
|
209
|
+
if self.class.primary_key_is_int?
|
210
|
+
sibling_versions.select(table[self.class.primary_key]).order(table[self.class.primary_key].asc).index(self)
|
211
|
+
else
|
212
|
+
sibling_versions.select([table[PaperTrail.timestamp_field], table[self.class.primary_key]]).
|
213
|
+
order(self.class.timestamp_sort_order).index(self)
|
214
|
+
end
|
196
215
|
end
|
197
216
|
|
198
217
|
private
|
@@ -1,3 +1,18 @@
|
|
1
1
|
module PaperTrail
|
2
|
-
VERSION
|
2
|
+
module VERSION
|
3
|
+
MAJOR = 3
|
4
|
+
MINOR = 0
|
5
|
+
TINY = 5
|
6
|
+
PRE = nil
|
7
|
+
|
8
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
9
|
+
|
10
|
+
def self.to_s
|
11
|
+
STRING
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.version
|
16
|
+
VERSION::STRING
|
17
|
+
end
|
3
18
|
end
|
data/paper_trail.gemspec
CHANGED
@@ -3,7 +3,7 @@ require 'paper_trail/version_number'
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = 'paper_trail'
|
6
|
-
s.version = PaperTrail
|
6
|
+
s.version = PaperTrail.version
|
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
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This model is in the test suite soley for the purpose of testing ActiveRecord::Enum,
|
4
|
+
# which is available in ActiveRecord4+ only
|
5
|
+
describe PostWithStatus do
|
6
|
+
if defined?(ActiveRecord::Enum)
|
7
|
+
with_versioning do
|
8
|
+
let(:post) { PostWithStatus.create!(:status => 'draft') }
|
9
|
+
|
10
|
+
it "should stash the enum value properly in versions" do
|
11
|
+
post.published!
|
12
|
+
post.archived!
|
13
|
+
post.previous_version.published?.should == true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/models/version_spec.rb
CHANGED
@@ -40,5 +40,49 @@ describe PaperTrail::Version do
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
|
+
|
44
|
+
describe "Class" do
|
45
|
+
describe :where_object do
|
46
|
+
it { PaperTrail::Version.should respond_to(:where_object) }
|
47
|
+
|
48
|
+
context "invalid arguments" do
|
49
|
+
it "should raise an error" do
|
50
|
+
expect { PaperTrail::Version.where_object(:foo) }.to raise_error(ArgumentError)
|
51
|
+
expect { PaperTrail::Version.where_object([]) }.to raise_error(ArgumentError)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "valid arguments", :versioning => true do
|
56
|
+
let(:widget) { Widget.new }
|
57
|
+
let(:name) { Faker::Name.first_name }
|
58
|
+
let(:int) { rand(10) + 1 }
|
59
|
+
|
60
|
+
before do
|
61
|
+
widget.update_attributes!(:name => name, :an_integer => int)
|
62
|
+
widget.update_attributes!(:name => 'foobar', :an_integer => 100)
|
63
|
+
widget.update_attributes!(:name => Faker::Name.last_name, :an_integer => 15)
|
64
|
+
end
|
65
|
+
|
66
|
+
context "`serializer == YAML`" do
|
67
|
+
specify { PaperTrail.serializer == PaperTrail::Serializers::YAML }
|
68
|
+
|
69
|
+
it "should be able to locate versions according to their `object` contents" do
|
70
|
+
PaperTrail::Version.where_object(:name => name).should == [widget.versions[1]]
|
71
|
+
PaperTrail::Version.where_object(:an_integer => 100).should == [widget.versions[2]]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "`serializer == JSON`" do
|
76
|
+
before { PaperTrail.serializer = PaperTrail::Serializers::JSON }
|
77
|
+
specify { PaperTrail.serializer == PaperTrail::Serializers::JSON }
|
78
|
+
|
79
|
+
it "should be able to locate versions according to their `object` contents" do
|
80
|
+
PaperTrail::Version.where_object(:name => name).should == [widget.versions[1]]
|
81
|
+
PaperTrail::Version.where_object(:an_integer => 100).should == [widget.versions[2]]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
43
87
|
end
|
44
88
|
end
|
data/spec/models/widget_spec.rb
CHANGED
@@ -61,8 +61,65 @@ describe Widget do
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
+
describe "Association", :versioning => true do
|
65
|
+
describe "sort order" do
|
66
|
+
it "should sort by the timestamp order from the `VersionConcern`" do
|
67
|
+
widget.versions.to_sql.should ==
|
68
|
+
widget.versions.reorder(PaperTrail::Version.timestamp_sort_order).to_sql
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
64
73
|
describe "Methods" do
|
65
74
|
describe "Instance", :versioning => true do
|
75
|
+
describe :originator do
|
76
|
+
it { should respond_to(:originator) }
|
77
|
+
|
78
|
+
describe "return value" do
|
79
|
+
let(:orig_name) { Faker::Name.name }
|
80
|
+
let(:new_name) { Faker::Name.name }
|
81
|
+
before { PaperTrail.whodunnit = orig_name }
|
82
|
+
|
83
|
+
context "accessed from live model instance" do
|
84
|
+
specify { widget.should be_live }
|
85
|
+
|
86
|
+
it "should return the originator for the model at a given state" do
|
87
|
+
widget.originator.should == orig_name
|
88
|
+
widget.whodunnit(new_name) { |w| w.update_attributes(:name => 'Elizabeth') }
|
89
|
+
widget.originator.should == new_name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "accessed from a reified model instance" do
|
94
|
+
before do
|
95
|
+
widget.update_attributes(:name => 'Andy')
|
96
|
+
PaperTrail.whodunnit = new_name
|
97
|
+
widget.update_attributes(:name => 'Elizabeth')
|
98
|
+
end
|
99
|
+
let(:reified_widget) { widget.versions[1].reify }
|
100
|
+
|
101
|
+
it "should return the appropriate originator" do
|
102
|
+
reified_widget.originator.should == orig_name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe :version_at do
|
109
|
+
it { should respond_to(:version_at) }
|
110
|
+
|
111
|
+
context "Timestamp argument is AFTER object has been destroyed" do
|
112
|
+
before do
|
113
|
+
widget.update_attribute(:name, 'foobar')
|
114
|
+
widget.destroy
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should return `nil`" do
|
118
|
+
widget.version_at(Time.now).should be_nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
66
123
|
describe :whodunnit do
|
67
124
|
it { should respond_to(:whodunnit) }
|
68
125
|
|
@@ -127,9 +184,9 @@ describe Widget do
|
|
127
184
|
it { should respond_to(:paper_trail_off!) }
|
128
185
|
|
129
186
|
it 'should set the `paper_trail_enabled_for_model?` to `false`' do
|
130
|
-
subject.paper_trail_enabled_for_model?.should
|
187
|
+
subject.paper_trail_enabled_for_model?.should == true
|
131
188
|
subject.paper_trail_off!
|
132
|
-
subject.paper_trail_enabled_for_model?.should
|
189
|
+
subject.paper_trail_enabled_for_model?.should == false
|
133
190
|
end
|
134
191
|
end
|
135
192
|
|
@@ -154,9 +211,9 @@ describe Widget do
|
|
154
211
|
it { should respond_to(:paper_trail_on!) }
|
155
212
|
|
156
213
|
it 'should set the `paper_trail_enabled_for_model?` to `true`' do
|
157
|
-
subject.paper_trail_enabled_for_model?.should
|
214
|
+
subject.paper_trail_enabled_for_model?.should == false
|
158
215
|
subject.paper_trail_on!
|
159
|
-
subject.paper_trail_enabled_for_model?.should
|
216
|
+
subject.paper_trail_enabled_for_model?.should == true
|
160
217
|
end
|
161
218
|
end
|
162
219
|
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'PaperTrail::VERSION' do
|
4
|
+
|
5
|
+
describe "Constants" do
|
6
|
+
subject { PaperTrail::VERSION }
|
7
|
+
|
8
|
+
describe :MAJOR do
|
9
|
+
it { should be_const_defined(:MAJOR) }
|
10
|
+
it { subject::MAJOR.should be_a(Integer) }
|
11
|
+
end
|
12
|
+
describe :MINOR do
|
13
|
+
it { should be_const_defined(:MINOR) }
|
14
|
+
it { subject::MINOR.should be_a(Integer) }
|
15
|
+
end
|
16
|
+
describe :TINY do
|
17
|
+
it { should be_const_defined(:TINY) }
|
18
|
+
it { subject::TINY.should be_a(Integer) }
|
19
|
+
end
|
20
|
+
describe :PRE do
|
21
|
+
it { should be_const_defined(:PRE) }
|
22
|
+
if PaperTrail::VERSION::PRE
|
23
|
+
it { subject::PRE.should be_instance_of(String) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
describe :STRING do
|
27
|
+
it { should be_const_defined(:STRING) }
|
28
|
+
it { subject::STRING.should be_instance_of(String) }
|
29
|
+
|
30
|
+
it "should join the numbers into a period separated string" do
|
31
|
+
subject::STRING.should ==
|
32
|
+
[subject::MAJOR, subject::MINOR, subject::TINY, subject::PRE].compact.join('.')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
describe PaperTrail do
|
40
|
+
describe :version do
|
41
|
+
it { should respond_to(:version) }
|
42
|
+
its(:version) { should == PaperTrail::VERSION::STRING }
|
43
|
+
end
|
44
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -92,6 +92,10 @@ class SetUpTestTables < ActiveRecord::Migration
|
|
92
92
|
t.string :content
|
93
93
|
end
|
94
94
|
|
95
|
+
create_table :post_with_statuses, :force => true do |t|
|
96
|
+
t.integer :status
|
97
|
+
end
|
98
|
+
|
95
99
|
create_table :animals, :force => true do |t|
|
96
100
|
t.string :name
|
97
101
|
t.string :species # single table inheritance column
|
@@ -100,7 +104,7 @@ class SetUpTestTables < ActiveRecord::Migration
|
|
100
104
|
create_table :documents, :force => true do |t|
|
101
105
|
t.string :name
|
102
106
|
end
|
103
|
-
|
107
|
+
|
104
108
|
create_table :legacy_widgets, :force => true do |t|
|
105
109
|
t.string :name
|
106
110
|
t.integer :version
|
data/test/unit/model_test.rb
CHANGED
@@ -37,4 +37,39 @@ class JSONTest < ActiveSupport::TestCase
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
context '`where_object` class method' do
|
41
|
+
context "when value is a string" do
|
42
|
+
should 'construct correct WHERE query' do
|
43
|
+
matches = PaperTrail::Serializers::JSON.where_object_condition(
|
44
|
+
PaperTrail::Version.arel_table[:object], :arg1, "Val 1")
|
45
|
+
|
46
|
+
assert matches.instance_of?(Arel::Nodes::Matches)
|
47
|
+
assert_equal matches.right, "%\"arg1\":\"Val 1\"%"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when value is `null`" do
|
52
|
+
should 'construct correct WHERE query' do
|
53
|
+
matches = PaperTrail::Serializers::JSON.where_object_condition(
|
54
|
+
PaperTrail::Version.arel_table[:object], :arg1, nil)
|
55
|
+
|
56
|
+
assert matches.instance_of?(Arel::Nodes::Matches)
|
57
|
+
assert_equal matches.right, "%\"arg1\":null%"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when value is a number" do
|
62
|
+
should 'construct correct WHERE query' do
|
63
|
+
grouping = PaperTrail::Serializers::JSON.where_object_condition(
|
64
|
+
PaperTrail::Version.arel_table[:object], :arg1, -3.5)
|
65
|
+
|
66
|
+
assert grouping.instance_of?(Arel::Nodes::Grouping)
|
67
|
+
matches = grouping.select { |v| v.instance_of?(Arel::Nodes::Matches) }
|
68
|
+
# Numeric arguments need to ensure that they match for only the number, not the beginning
|
69
|
+
# of a #, so it uses an Grouping matcher (See notes on `PaperTrail::Serializers::JSON`)
|
70
|
+
assert_equal matches.first.right, "%\"arg1\":-3.5,%"
|
71
|
+
assert_equal matches.last.right, "%\"arg1\":-3.5}%"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
40
75
|
end
|
@@ -37,4 +37,12 @@ class YamlTest < ActiveSupport::TestCase
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
context '`where_object` class method' do
|
41
|
+
should 'construct correct WHERE query' do
|
42
|
+
matches = PaperTrail::Serializers::YAML.where_object_condition(
|
43
|
+
PaperTrail::Version.arel_table[:object], :arg1, "Val 1")
|
44
|
+
assert matches.instance_of?(Arel::Nodes::Matches)
|
45
|
+
assert_equal matches.right, "%\narg1: Val 1\n%"
|
46
|
+
end
|
47
|
+
end
|
40
48
|
end
|
data/test/unit/version_test.rb
CHANGED
@@ -68,7 +68,7 @@ class PaperTrail::VersionTest < ActiveSupport::TestCase
|
|
68
68
|
should "return all versions that were created before the Timestamp" do
|
69
69
|
value = PaperTrail::Version.subsequent(1.hour.ago, true)
|
70
70
|
assert_equal value, @animal.versions.to_a
|
71
|
-
assert_not_nil value.to_sql.match(/ORDER BY
|
71
|
+
assert_not_nil value.to_sql.match(/ORDER BY #{PaperTrail::Version.arel_table[:created_at].asc.to_sql}/)
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
@@ -87,7 +87,7 @@ class PaperTrail::VersionTest < ActiveSupport::TestCase
|
|
87
87
|
should "return all versions that were created before the Timestamp" do
|
88
88
|
value = PaperTrail::Version.preceding(5.seconds.from_now, true)
|
89
89
|
assert_equal value, @animal.versions.reverse
|
90
|
-
assert_not_nil value.to_sql.match(/ORDER BY
|
90
|
+
assert_not_nil value.to_sql.match(/ORDER BY #{PaperTrail::Version.arel_table[:created_at].desc.to_sql}/)
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
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.
|
4
|
+
version: 3.0.5
|
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: 2014-
|
12
|
+
date: 2014-08-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -247,23 +247,28 @@ files:
|
|
247
247
|
- lib/paper_trail.rb
|
248
248
|
- lib/paper_trail/cleaner.rb
|
249
249
|
- lib/paper_trail/config.rb
|
250
|
+
- lib/paper_trail/frameworks/active_record.rb
|
251
|
+
- lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb
|
250
252
|
- lib/paper_trail/frameworks/cucumber.rb
|
251
253
|
- lib/paper_trail/frameworks/rails.rb
|
254
|
+
- lib/paper_trail/frameworks/rails/controller.rb
|
255
|
+
- lib/paper_trail/frameworks/rails/engine.rb
|
252
256
|
- lib/paper_trail/frameworks/rspec.rb
|
253
257
|
- lib/paper_trail/frameworks/rspec/helpers.rb
|
254
258
|
- lib/paper_trail/frameworks/sinatra.rb
|
255
259
|
- lib/paper_trail/has_paper_trail.rb
|
256
260
|
- lib/paper_trail/serializers/json.rb
|
257
261
|
- lib/paper_trail/serializers/yaml.rb
|
258
|
-
- lib/paper_trail/version.rb
|
259
262
|
- lib/paper_trail/version_concern.rb
|
260
263
|
- lib/paper_trail/version_number.rb
|
261
264
|
- paper_trail.gemspec
|
262
265
|
- spec/generators/install_generator_spec.rb
|
263
266
|
- spec/models/joined_version_spec.rb
|
267
|
+
- spec/models/post_with_status_spec.rb
|
264
268
|
- spec/models/version_spec.rb
|
265
269
|
- spec/models/widget_spec.rb
|
266
270
|
- spec/modules/version_concern_spec.rb
|
271
|
+
- spec/modules/version_number_spec.rb
|
267
272
|
- spec/paper_trail_spec.rb
|
268
273
|
- spec/requests/articles_spec.rb
|
269
274
|
- spec/spec_helper.rb
|
@@ -288,6 +293,7 @@ files:
|
|
288
293
|
- test/dummy/app/models/legacy_widget.rb
|
289
294
|
- test/dummy/app/models/person.rb
|
290
295
|
- test/dummy/app/models/post.rb
|
296
|
+
- test/dummy/app/models/post_with_status.rb
|
291
297
|
- test/dummy/app/models/protected_widget.rb
|
292
298
|
- test/dummy/app/models/song.rb
|
293
299
|
- test/dummy/app/models/translation.rb
|
@@ -366,16 +372,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
366
372
|
version: 1.3.6
|
367
373
|
requirements: []
|
368
374
|
rubyforge_project:
|
369
|
-
rubygems_version: 2.2.
|
375
|
+
rubygems_version: 2.2.2
|
370
376
|
signing_key:
|
371
377
|
specification_version: 4
|
372
378
|
summary: Track changes to your models' data. Good for auditing or versioning.
|
373
379
|
test_files:
|
374
380
|
- spec/generators/install_generator_spec.rb
|
375
381
|
- spec/models/joined_version_spec.rb
|
382
|
+
- spec/models/post_with_status_spec.rb
|
376
383
|
- spec/models/version_spec.rb
|
377
384
|
- spec/models/widget_spec.rb
|
378
385
|
- spec/modules/version_concern_spec.rb
|
386
|
+
- spec/modules/version_number_spec.rb
|
379
387
|
- spec/paper_trail_spec.rb
|
380
388
|
- spec/requests/articles_spec.rb
|
381
389
|
- spec/spec_helper.rb
|
@@ -400,6 +408,7 @@ test_files:
|
|
400
408
|
- test/dummy/app/models/legacy_widget.rb
|
401
409
|
- test/dummy/app/models/person.rb
|
402
410
|
- test/dummy/app/models/post.rb
|
411
|
+
- test/dummy/app/models/post_with_status.rb
|
403
412
|
- test/dummy/app/models/protected_widget.rb
|
404
413
|
- test/dummy/app/models/song.rb
|
405
414
|
- test/dummy/app/models/translation.rb
|