paper_trail 8.1.2 → 9.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/paper_trail/install_generator.rb +2 -0
- data/lib/paper_trail.rb +130 -78
- data/lib/paper_trail/attribute_serializers/attribute_serializer_factory.rb +3 -1
- data/lib/paper_trail/attribute_serializers/cast_attribute_serializer.rb +2 -0
- data/lib/paper_trail/attribute_serializers/object_attribute.rb +2 -0
- data/lib/paper_trail/attribute_serializers/object_changes_attribute.rb +2 -0
- data/lib/paper_trail/cleaner.rb +2 -0
- data/lib/paper_trail/config.rb +33 -8
- data/lib/paper_trail/frameworks/active_record.rb +2 -0
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version.rb +2 -0
- data/lib/paper_trail/frameworks/active_record/models/paper_trail/version_association.rb +2 -0
- data/lib/paper_trail/frameworks/cucumber.rb +5 -3
- data/lib/paper_trail/frameworks/rails.rb +2 -0
- data/lib/paper_trail/frameworks/rails/controller.rb +28 -16
- data/lib/paper_trail/frameworks/rails/engine.rb +2 -0
- data/lib/paper_trail/frameworks/rspec.rb +5 -3
- data/lib/paper_trail/frameworks/rspec/helpers.rb +2 -0
- data/lib/paper_trail/has_paper_trail.rb +2 -1
- data/lib/paper_trail/model_config.rb +76 -14
- data/lib/paper_trail/queries/versions/where_object.rb +2 -0
- data/lib/paper_trail/queries/versions/where_object_changes.rb +3 -1
- data/lib/paper_trail/record_history.rb +2 -0
- data/lib/paper_trail/record_trail.rb +188 -48
- data/lib/paper_trail/reifier.rb +4 -2
- data/lib/paper_trail/reifiers/belongs_to.rb +2 -0
- data/lib/paper_trail/reifiers/has_and_belongs_to_many.rb +2 -0
- data/lib/paper_trail/reifiers/has_many.rb +2 -0
- data/lib/paper_trail/reifiers/has_many_through.rb +2 -0
- data/lib/paper_trail/reifiers/has_one.rb +52 -4
- data/lib/paper_trail/request.rb +183 -0
- data/lib/paper_trail/serializers/json.rb +2 -2
- data/lib/paper_trail/serializers/yaml.rb +10 -14
- data/lib/paper_trail/type_serializers/postgres_array_serializer.rb +2 -0
- data/lib/paper_trail/version_association_concern.rb +1 -1
- data/lib/paper_trail/version_concern.rb +2 -6
- data/lib/paper_trail/version_number.rb +5 -3
- metadata +8 -21
data/lib/paper_trail/reifier.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "paper_trail/attribute_serializers/object_attribute"
|
2
4
|
require "paper_trail/reifiers/belongs_to"
|
3
5
|
require "paper_trail/reifiers/has_and_belongs_to_many"
|
@@ -54,7 +56,7 @@ module PaperTrail
|
|
54
56
|
# @api private
|
55
57
|
def each_enabled_association(associations)
|
56
58
|
associations.each do |assoc|
|
57
|
-
next unless assoc.klass
|
59
|
+
next unless ::PaperTrail.request.enabled_for_model?(assoc.klass)
|
58
60
|
yield assoc
|
59
61
|
end
|
60
62
|
end
|
@@ -192,7 +194,7 @@ module PaperTrail
|
|
192
194
|
# @api private
|
193
195
|
def reify_habtm_associations(transaction_id, model, options = {})
|
194
196
|
model.class.reflect_on_all_associations(:has_and_belongs_to_many).each do |assoc|
|
195
|
-
pt_enabled = assoc.klass
|
197
|
+
pt_enabled = ::PaperTrail.request.enabled_for_model?(assoc.klass)
|
196
198
|
next unless model.class.paper_trail_save_join_tables.include?(assoc.name) || pt_enabled
|
197
199
|
Reifiers::HasAndBelongsToMany.reify(pt_enabled, assoc, model, options, transaction_id)
|
198
200
|
end
|
@@ -1,12 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
module Reifiers
|
3
5
|
# Reify a single `has_one` association of `model`.
|
4
6
|
# @api private
|
5
7
|
module HasOne
|
8
|
+
# A more helpful error message, instead of the AssociationTypeMismatch
|
9
|
+
# you would get if, eg. we were to try to assign a Bicycle to the :car
|
10
|
+
# association (before, if there were multiple records we would just take
|
11
|
+
# the first and hope for the best).
|
12
|
+
# @api private
|
13
|
+
class FoundMoreThanOne < RuntimeError
|
14
|
+
MESSAGE_FMT = <<~STR
|
15
|
+
Unable to reify has_one association. Expected to find one %s,
|
16
|
+
but found %d.
|
17
|
+
|
18
|
+
This is a known issue, and a good example of why association tracking
|
19
|
+
is an experimental feature that should not be used in production.
|
20
|
+
|
21
|
+
That said, this is a rare error. In spec/models/person_spec.rb we
|
22
|
+
reproduce it by having two STI models with the same foreign_key (Car
|
23
|
+
and Bicycle are both Vehicles and the FK for both is owner_id)
|
24
|
+
|
25
|
+
If you'd like to help fix this error, please read
|
26
|
+
https://github.com/airblade/paper_trail/issues/594
|
27
|
+
and see spec/models/person_spec.rb
|
28
|
+
STR
|
29
|
+
|
30
|
+
def initialize(base_class_name, num_records_found)
|
31
|
+
@base_class_name = base_class_name.to_s
|
32
|
+
@num_records_found = num_records_found.to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
def message
|
36
|
+
format(MESSAGE_FMT, @base_class_name, @num_records_found)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
6
40
|
class << self
|
7
41
|
# @api private
|
8
42
|
def reify(assoc, model, options, transaction_id)
|
9
|
-
version =
|
43
|
+
version = load_version(assoc, model, transaction_id, options[:version_at])
|
10
44
|
return unless version
|
11
45
|
if version.event == "create"
|
12
46
|
create_event(assoc, model, options)
|
@@ -31,15 +65,29 @@ module PaperTrail
|
|
31
65
|
# Given a has-one association `assoc` on `model`, return the version
|
32
66
|
# record from the point in time identified by `transaction_id` or `version_at`.
|
33
67
|
# @api private
|
34
|
-
def
|
68
|
+
def load_version(assoc, model, transaction_id, version_at)
|
69
|
+
base_class_name = assoc.klass.base_class.name
|
70
|
+
versions = load_versions(assoc, model, transaction_id, version_at, base_class_name)
|
71
|
+
case versions.length
|
72
|
+
when 0
|
73
|
+
nil
|
74
|
+
when 1
|
75
|
+
versions.first
|
76
|
+
else
|
77
|
+
raise FoundMoreThanOne.new(base_class_name, versions.length)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
def load_versions(assoc, model, transaction_id, version_at, base_class_name)
|
35
83
|
version_table_name = model.class.paper_trail.version_class.table_name
|
36
84
|
model.class.paper_trail.version_class.joins(:version_associations).
|
37
85
|
where("version_associations.foreign_key_name = ?", assoc.foreign_key).
|
38
86
|
where("version_associations.foreign_key_id = ?", model.id).
|
39
|
-
where("#{version_table_name}.item_type = ?",
|
87
|
+
where("#{version_table_name}.item_type = ?", base_class_name).
|
40
88
|
where("created_at >= ? OR transaction_id = ?", version_at, transaction_id).
|
41
89
|
order("#{version_table_name}.id ASC").
|
42
|
-
|
90
|
+
load
|
43
91
|
end
|
44
92
|
|
45
93
|
# @api private
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "request_store"
|
4
|
+
|
5
|
+
module PaperTrail
|
6
|
+
# Manages variables that affect the current HTTP request, such as `whodunnit`.
|
7
|
+
#
|
8
|
+
# Please do not use `PaperTrail::Request` directly, use `PaperTrail.request`.
|
9
|
+
# Currently, `Request` is a `Module`, but in the future it is quite possible
|
10
|
+
# we may make it a `Class`. If we make such a choice, we will not provide any
|
11
|
+
# warning and will not treat it as a breaking change. You've been warned :)
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
module Request
|
15
|
+
class InvalidOption < RuntimeError
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# @api private
|
20
|
+
def clear_transaction_id
|
21
|
+
self.transaction_id = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Sets any data from the controller that you want PaperTrail to store.
|
25
|
+
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
|
26
|
+
#
|
27
|
+
# PaperTrail.request.controller_info = { ip: request_user_ip }
|
28
|
+
# PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def controller_info=(value)
|
32
|
+
store[:controller_info] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the data from the controller that you want PaperTrail to store.
|
36
|
+
# See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
|
37
|
+
#
|
38
|
+
# PaperTrail.request.controller_info = { ip: request_user_ip }
|
39
|
+
# PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def controller_info
|
43
|
+
store[:controller_info]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Switches PaperTrail off for the given model.
|
47
|
+
# @api public
|
48
|
+
def disable_model(model_class)
|
49
|
+
enabled_for_model(model_class, false)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Switches PaperTrail on for the given model.
|
53
|
+
# @api public
|
54
|
+
def enable_model(model_class)
|
55
|
+
enabled_for_model(model_class, true)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sets whether PaperTrail is enabled or disabled for the current request.
|
59
|
+
# @api public
|
60
|
+
def enabled=(value)
|
61
|
+
store[:enabled] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
|
65
|
+
# See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
|
66
|
+
# @api public
|
67
|
+
def enabled?
|
68
|
+
!!store[:enabled]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sets whether PaperTrail is enabled or disabled for this model in the
|
72
|
+
# current request.
|
73
|
+
# @api public
|
74
|
+
def enabled_for_model(model, value)
|
75
|
+
store[:"enabled_for_#{model}"] = value
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns `true` if PaperTrail is enabled for this model in the current
|
79
|
+
# request, `false` otherwise.
|
80
|
+
# @api public
|
81
|
+
def enabled_for_model?(model)
|
82
|
+
model.include?(::PaperTrail::Model::InstanceMethods) &&
|
83
|
+
!!store.fetch(:"enabled_for_#{model}", true)
|
84
|
+
end
|
85
|
+
|
86
|
+
# @api private
|
87
|
+
def merge(options)
|
88
|
+
options.to_h.each do |k, v|
|
89
|
+
store[k] = v
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @api private
|
94
|
+
def set(options)
|
95
|
+
store.clear
|
96
|
+
merge(options)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a deep copy of the internal hash from our RequestStore. Keys are
|
100
|
+
# all symbols. Values are mostly primitives, but whodunnit can be a Proc.
|
101
|
+
# We cannot use Marshal.dump here because it doesn't support Proc. It is
|
102
|
+
# unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
|
103
|
+
# @api private
|
104
|
+
def to_h
|
105
|
+
store.deep_dup
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
def transaction_id
|
110
|
+
store[:transaction_id]
|
111
|
+
end
|
112
|
+
|
113
|
+
# @api private
|
114
|
+
def transaction_id=(id)
|
115
|
+
store[:transaction_id] = id
|
116
|
+
end
|
117
|
+
|
118
|
+
# Temporarily set `options` and execute a block.
|
119
|
+
# @api private
|
120
|
+
def with(options)
|
121
|
+
return unless block_given?
|
122
|
+
validate_public_options(options)
|
123
|
+
before = to_h
|
124
|
+
merge(options)
|
125
|
+
yield
|
126
|
+
ensure
|
127
|
+
set(before)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Sets who is responsible for any changes that occur during request. You
|
131
|
+
# would normally use this in a migration or on the console, when working
|
132
|
+
# with models directly.
|
133
|
+
#
|
134
|
+
# `value` is usually a string, the name of a person, but you can set
|
135
|
+
# anything that responds to `to_s`. You can also set a Proc, which will
|
136
|
+
# not be evaluated until `whodunnit` is called later, usually right before
|
137
|
+
# inserting a `Version` record.
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
def whodunnit=(value)
|
141
|
+
store[:whodunnit] = value
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns who is reponsible for any changes that occur during request.
|
145
|
+
#
|
146
|
+
# @api public
|
147
|
+
def whodunnit
|
148
|
+
who = store[:whodunnit]
|
149
|
+
who.respond_to?(:call) ? who.call : who
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Returns a Hash, initializing with default values if necessary.
|
155
|
+
# @api private
|
156
|
+
def store
|
157
|
+
RequestStore.store[:paper_trail] ||= {
|
158
|
+
enabled: true
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
# Provide a helpful error message if someone has a typo in one of their
|
163
|
+
# option keys. We don't validate option values here. That's traditionally
|
164
|
+
# been handled with casting (`to_s`, `!!`) in the accessor method.
|
165
|
+
# @api private
|
166
|
+
def validate_public_options(options)
|
167
|
+
options.each do |k, _v|
|
168
|
+
case k
|
169
|
+
when :controller_info,
|
170
|
+
/enabled_for_/,
|
171
|
+
:enabled,
|
172
|
+
:whodunnit
|
173
|
+
next
|
174
|
+
when :transaction_id
|
175
|
+
raise InvalidOption, "Cannot set private option: #{k}"
|
176
|
+
else
|
177
|
+
raise InvalidOption, "Invalid option: #{k}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PaperTrail
|
4
4
|
module Serializers
|
@@ -34,7 +34,7 @@ module PaperTrail
|
|
34
34
|
|
35
35
|
def where_object_changes_condition(*)
|
36
36
|
raise <<-STR.squish.freeze
|
37
|
-
where_object_changes no longer supports reading
|
37
|
+
where_object_changes no longer supports reading JSON from a text
|
38
38
|
column. The old implementation was inaccurate, returning more records
|
39
39
|
than you wanted. This feature was deprecated in 7.1.0 and removed in
|
40
40
|
8.0.0. The json and jsonb datatypes are still supported. See the
|
@@ -1,16 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "yaml"
|
2
4
|
|
3
5
|
module PaperTrail
|
4
6
|
module Serializers
|
5
7
|
# The default serializer for, e.g. `versions.object`.
|
6
8
|
module YAML
|
7
|
-
E_WHERE_OBJ_CHANGES = <<-STR.squish.freeze
|
8
|
-
where_object_changes has a known issue. When reading YAML from a text
|
9
|
-
column, it may return more records than expected. Instead of a warning,
|
10
|
-
this method may raise an error in the future. Please join the discussion
|
11
|
-
at https://github.com/airblade/paper_trail/pull/997
|
12
|
-
STR
|
13
|
-
|
14
9
|
extend self # makes all instance methods become module methods as well
|
15
10
|
|
16
11
|
def load(string)
|
@@ -29,13 +24,14 @@ module PaperTrail
|
|
29
24
|
|
30
25
|
# Returns a SQL LIKE condition to be used to match the given field and
|
31
26
|
# value in the serialized `object_changes`.
|
32
|
-
def where_object_changes_condition(
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
27
|
+
def where_object_changes_condition(*)
|
28
|
+
raise <<-STR.squish.freeze
|
29
|
+
where_object_changes no longer supports reading YAML from a text
|
30
|
+
column. The old implementation was inaccurate, returning more records
|
31
|
+
than you wanted. This feature was deprecated in 8.1.0 and removed in
|
32
|
+
9.0.0. The json and jsonb datatypes are still supported. See
|
33
|
+
discussion at https://github.com/airblade/paper_trail/pull/997
|
34
|
+
STR
|
39
35
|
end
|
40
36
|
end
|
41
37
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "paper_trail/attribute_serializers/object_changes_attribute"
|
3
4
|
require "paper_trail/queries/versions/where_object"
|
4
5
|
require "paper_trail/queries/versions/where_object_changes"
|
@@ -230,11 +231,6 @@ module PaperTrail
|
|
230
231
|
@paper_trail_originator ||= previous.try(:whodunnit)
|
231
232
|
end
|
232
233
|
|
233
|
-
def originator
|
234
|
-
::ActiveSupport::Deprecation.warn "Use paper_trail_originator instead of originator."
|
235
|
-
paper_trail_originator
|
236
|
-
end
|
237
|
-
|
238
234
|
# Returns who changed the item from the state it had in this version. This
|
239
235
|
# is an alias for `whodunnit`.
|
240
236
|
def terminator
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PaperTrail
|
2
4
|
# The version number of the paper_trail gem. Not to be confused with
|
3
5
|
# `PaperTrail::Version`. Ruby constants are case-sensitive, apparently,
|
@@ -5,9 +7,9 @@ module PaperTrail
|
|
5
7
|
# because of this confusion, but it's not worth the breaking change.
|
6
8
|
# People are encouraged to use `PaperTrail.gem_version` instead.
|
7
9
|
module VERSION
|
8
|
-
MAJOR =
|
9
|
-
MINOR =
|
10
|
-
TINY =
|
10
|
+
MAJOR = 9
|
11
|
+
MINOR = 0
|
12
|
+
TINY = 0
|
11
13
|
PRE = nil
|
12
14
|
|
13
15
|
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".").freeze
|
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:
|
4
|
+
version: 9.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Stewart
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2018-03-27 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: '4.2'
|
22
22
|
- - "<"
|
23
23
|
- !ruby/object:Gem::Version
|
24
|
-
version: '5.
|
24
|
+
version: '5.3'
|
25
25
|
type: :runtime
|
26
26
|
prerelease: false
|
27
27
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -31,7 +31,7 @@ dependencies:
|
|
31
31
|
version: '4.2'
|
32
32
|
- - "<"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '5.
|
34
|
+
version: '5.3'
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: request_store
|
37
37
|
requirement: !ruby/object:Gem::Requirement
|
@@ -74,20 +74,6 @@ dependencies:
|
|
74
74
|
- - "~>"
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: '9.1'
|
77
|
-
- !ruby/object:Gem::Dependency
|
78
|
-
name: database_cleaner
|
79
|
-
requirement: !ruby/object:Gem::Requirement
|
80
|
-
requirements:
|
81
|
-
- - "~>"
|
82
|
-
- !ruby/object:Gem::Version
|
83
|
-
version: '1.6'
|
84
|
-
type: :development
|
85
|
-
prerelease: false
|
86
|
-
version_requirements: !ruby/object:Gem::Requirement
|
87
|
-
requirements:
|
88
|
-
- - "~>"
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
version: '1.6'
|
91
77
|
- !ruby/object:Gem::Dependency
|
92
78
|
name: ffaker
|
93
79
|
requirement: !ruby/object:Gem::Requirement
|
@@ -252,7 +238,7 @@ description: |
|
|
252
238
|
Track changes to your models, for auditing or versioning. See how a model looked
|
253
239
|
at any stage in its lifecycle, revert it to any version, or restore it after it
|
254
240
|
has been destroyed.
|
255
|
-
email:
|
241
|
+
email: jared@jaredbeck.com
|
256
242
|
executables: []
|
257
243
|
extensions: []
|
258
244
|
extra_rdoc_files: []
|
@@ -292,6 +278,7 @@ files:
|
|
292
278
|
- lib/paper_trail/reifiers/has_many.rb
|
293
279
|
- lib/paper_trail/reifiers/has_many_through.rb
|
294
280
|
- lib/paper_trail/reifiers/has_one.rb
|
281
|
+
- lib/paper_trail/request.rb
|
295
282
|
- lib/paper_trail/serializers/json.rb
|
296
283
|
- lib/paper_trail/serializers/yaml.rb
|
297
284
|
- lib/paper_trail/type_serializers/postgres_array_serializer.rb
|
@@ -310,7 +297,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
310
297
|
requirements:
|
311
298
|
- - ">="
|
312
299
|
- !ruby/object:Gem::Version
|
313
|
-
version: 2.
|
300
|
+
version: 2.3.0
|
314
301
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
315
302
|
requirements:
|
316
303
|
- - ">="
|
@@ -318,7 +305,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
318
305
|
version: 1.3.6
|
319
306
|
requirements: []
|
320
307
|
rubyforge_project:
|
321
|
-
rubygems_version: 2.7.
|
308
|
+
rubygems_version: 2.7.6
|
322
309
|
signing_key:
|
323
310
|
specification_version: 4
|
324
311
|
summary: Track changes to your models.
|