paper_trail 3.0.2 → 3.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|