active_propagation 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +83 -0
- data/README.md +28 -0
- data/Rakefile +2 -0
- data/active_propagation.gemspec +31 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/active_propagation.rb +106 -0
- data/lib/active_propagation/class_extensions.rb +7 -0
- data/lib/active_propagation/instance_extensions.rb +21 -0
- data/lib/active_propagation/propagater_helper.rb +5 -0
- data/lib/active_propagation/version.rb +3 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3e00938b35501723e36afe6ec04011464588d594
|
4
|
+
data.tar.gz: 5c348effb95ccccceabf508bc49e979c88d9d526
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c4136786fc4c3360ab773609ea828aaa59efe957d683e6faa6d660e53cb4c2e1e5b1fcd1ea069b76a00854290fe1a8ea095611d80b3e5f770c1e17c59cc8c135
|
7
|
+
data.tar.gz: bee8f450fc60056c75bad9d0d7e0a0ddb0305f43689ab0a6d8f137f89730388b3014d5d0b41a8a8a78a0cc0420c8d52953af330f0378a786dc5ef4ba4f0821be
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.2
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
active_propagation (0.1.0)
|
5
|
+
activerecord (~> 4.0)
|
6
|
+
activesupport (~> 4.0)
|
7
|
+
sidekiq
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.2.1)
|
13
|
+
activesupport (= 4.2.1)
|
14
|
+
builder (~> 3.1)
|
15
|
+
activerecord (4.2.1)
|
16
|
+
activemodel (= 4.2.1)
|
17
|
+
activesupport (= 4.2.1)
|
18
|
+
arel (~> 6.0)
|
19
|
+
activesupport (4.2.1)
|
20
|
+
i18n (~> 0.7)
|
21
|
+
json (~> 1.7, >= 1.7.7)
|
22
|
+
minitest (~> 5.1)
|
23
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
24
|
+
tzinfo (~> 1.1)
|
25
|
+
arel (6.0.0)
|
26
|
+
builder (3.2.2)
|
27
|
+
celluloid (0.16.0)
|
28
|
+
timers (~> 4.0.0)
|
29
|
+
coderay (1.1.0)
|
30
|
+
connection_pool (2.2.0)
|
31
|
+
database_cleaner (1.4.1)
|
32
|
+
diff-lcs (1.2.5)
|
33
|
+
hitimes (1.2.2)
|
34
|
+
i18n (0.7.0)
|
35
|
+
json (1.8.3)
|
36
|
+
method_source (0.8.2)
|
37
|
+
minitest (5.7.0)
|
38
|
+
pry (0.10.1)
|
39
|
+
coderay (~> 1.1.0)
|
40
|
+
method_source (~> 0.8.1)
|
41
|
+
slop (~> 3.4)
|
42
|
+
rake (10.4.2)
|
43
|
+
redis (3.2.1)
|
44
|
+
redis-namespace (1.5.2)
|
45
|
+
redis (~> 3.0, >= 3.0.4)
|
46
|
+
rspec (3.2.0)
|
47
|
+
rspec-core (~> 3.2.0)
|
48
|
+
rspec-expectations (~> 3.2.0)
|
49
|
+
rspec-mocks (~> 3.2.0)
|
50
|
+
rspec-core (3.2.3)
|
51
|
+
rspec-support (~> 3.2.0)
|
52
|
+
rspec-expectations (3.2.1)
|
53
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
54
|
+
rspec-support (~> 3.2.0)
|
55
|
+
rspec-mocks (3.2.1)
|
56
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
57
|
+
rspec-support (~> 3.2.0)
|
58
|
+
rspec-support (3.2.2)
|
59
|
+
sidekiq (3.3.4)
|
60
|
+
celluloid (>= 0.16.0)
|
61
|
+
connection_pool (>= 2.1.1)
|
62
|
+
json
|
63
|
+
redis (>= 3.0.6)
|
64
|
+
redis-namespace (>= 1.3.1)
|
65
|
+
slop (3.6.0)
|
66
|
+
sqlite3 (1.3.10)
|
67
|
+
thread_safe (0.3.5)
|
68
|
+
timers (4.0.1)
|
69
|
+
hitimes
|
70
|
+
tzinfo (1.2.2)
|
71
|
+
thread_safe (~> 0.1)
|
72
|
+
|
73
|
+
PLATFORMS
|
74
|
+
ruby
|
75
|
+
|
76
|
+
DEPENDENCIES
|
77
|
+
active_propagation!
|
78
|
+
bundler (~> 1.8)
|
79
|
+
database_cleaner
|
80
|
+
pry
|
81
|
+
rake (~> 10.0)
|
82
|
+
rspec
|
83
|
+
sqlite3
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ActivePropagation
|
2
|
+
|
3
|
+
Provides an ActiveRecord extension for propagating changes amongst models.
|
4
|
+
|
5
|
+
Propagations are ran as callbacks and can be propagated synchronously in background jobs, or synchronously within the callback lifecycle. Furthermore, the individual updates on each associated record can be ran in jobs of their own.
|
6
|
+
|
7
|
+
# Usage Example
|
8
|
+
|
9
|
+
Say you have a posts model with an optional `post_id` such that a Post can have many posts and also belong to a post:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class Post < ActiveRecord::Base
|
13
|
+
belongs_to :post
|
14
|
+
has_many :sub_posts
|
15
|
+
|
16
|
+
propagates_changes_to :sub_posts, only: [:title], on: [:update], async: true
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
This will register an `after_commit` callback that will fire a Sidekiq worker to set the titles of all of the sub_posts to be the same as their parent post.
|
21
|
+
|
22
|
+
# Why?
|
23
|
+
|
24
|
+
Many Rails codebases rely heavily on callbacks to keep the application's state consistent. While changes can be propagated performantly and easily using `update_all` or `destroy_all`, if there are callbacks that need to be ran then this isn't possible. active_propagation calls `update` or `destroy` on each instantiated activerecord object so all callbacks are ran. By having an asynch option, even if you expect lots of dependent records and long running callbacks, each update and destroy, as well as their callbacks, are parallelized.
|
25
|
+
|
26
|
+
Caveats:
|
27
|
+
- Currently the only supported job system is Sidekiq. The plan is to make this configurable for multiple backends(probably using ActiveJob).
|
28
|
+
- The callbacks are executed `after_commit`. This is to make sure it plays nicely with Sidekiq.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_propagation/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_propagation"
|
8
|
+
spec.version = ActivePropagation::VERSION
|
9
|
+
spec.authors = ["Yoshiki Schmitz"]
|
10
|
+
spec.email = ["yoshiki@masteryconnect.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{propagates changes across models}
|
13
|
+
spec.description = %q{ActivePropagation provides classes and ActiveRecord extensions for propagating changes amongst models, optionally using Sidekiq to parallelize the process.}
|
14
|
+
spec.homepage = "https://bitbucket.org/doweber/active_propagation/overview"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "sqlite3"
|
25
|
+
spec.add_development_dependency "database_cleaner"
|
26
|
+
spec.add_development_dependency "pry"
|
27
|
+
|
28
|
+
spec.add_dependency "activerecord", "~> 4.0"
|
29
|
+
spec.add_dependency "activesupport", "~> 4.0"
|
30
|
+
spec.add_dependency "sidekiq"
|
31
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "active_propagation"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'sidekiq'
|
3
|
+
require "active_propagation/version"
|
4
|
+
require "active_propagation/class_extensions"
|
5
|
+
require "active_propagation/instance_extensions"
|
6
|
+
require "active_propagation/propagater_helper"
|
7
|
+
|
8
|
+
module ActivePropagation
|
9
|
+
class Propagater
|
10
|
+
def initialize(klass, association, id)
|
11
|
+
@klass, @association, @id = klass, association, id
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
assocs.each do |a|
|
16
|
+
yield a
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def assocs
|
21
|
+
assoc_klass.where(foreign_key => id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def foreign_key
|
25
|
+
klass.reflections[association.to_s].foreign_key
|
26
|
+
end
|
27
|
+
|
28
|
+
def assoc_klass
|
29
|
+
klass.reflections[association.to_s].class_name.constantize
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :klass, :association, :id
|
35
|
+
end
|
36
|
+
|
37
|
+
class AbstractPropagaterWorker
|
38
|
+
include PropagaterHelper
|
39
|
+
include Sidekiq::Worker
|
40
|
+
|
41
|
+
def self.run(klass_str, model_id, assoc, only)
|
42
|
+
self.perform_async(klass_str, model_id, assoc, only)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class AsyncLoopDeletor
|
47
|
+
include PropagaterHelper
|
48
|
+
include Sidekiq::Worker
|
49
|
+
def perform(klass_str, assoc_id)
|
50
|
+
klass = klass_str.constantize
|
51
|
+
klass.find(assoc_id).destroy
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class AsyncDeletor < AbstractPropagaterWorker
|
56
|
+
def perform(klass_str, model_id, assoc, only)
|
57
|
+
klass = klass_str.constantize
|
58
|
+
Propagater.new(klass, assoc, model_id).run do |a|
|
59
|
+
AsyncLoopDeletor.perform_async(klass.to_s, a.id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class AsyncLoopUpdater
|
65
|
+
include Sidekiq::Worker
|
66
|
+
include PropagaterHelper
|
67
|
+
def perform(klass_str, model_id, assoc_id, only)
|
68
|
+
klass = klass_str.constantize
|
69
|
+
model = klass.find(model_id)
|
70
|
+
klass.find(assoc_id).update(propagated_attributes(model, only))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class AsyncUpdater < AbstractPropagaterWorker
|
75
|
+
def perform(klass_str, model_id, assoc, only)
|
76
|
+
klass = klass_str.constantize
|
77
|
+
Propagater.new(klass, assoc, model_id).run do |a|
|
78
|
+
AsyncLoopUpdater.perform_async(klass.to_s, model_id, a.id, only)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Updater
|
84
|
+
extend PropagaterHelper
|
85
|
+
def self.run(klass_str, model_id, assoc, only)
|
86
|
+
klass = klass_str.constantize
|
87
|
+
model = klass.find(model_id)
|
88
|
+
Propagater.new(klass, assoc, model_id).run do |a|
|
89
|
+
a.update(propagated_attributes(model, only))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Deletor
|
95
|
+
extend PropagaterHelper
|
96
|
+
def self.run(klass_str, model_id, assoc, only)
|
97
|
+
klass = klass_str.constantize
|
98
|
+
Propagater.new(klass, assoc, model_id).run do |a|
|
99
|
+
a.destroy
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
ActiveRecord::Base.send(:extend, ActivePropagation::ClassExtensions)
|
106
|
+
ActiveRecord::Base.send(:include, ActivePropagation::InstanceExtensions)
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module ActivePropagation::ClassExtensions
|
2
|
+
def propagates_changes_to(association, only: [], async: false, nested_async: false, on: [:update, :destroy])
|
3
|
+
self.class_variable_set(:@@propagations, self.class_variable_get(:@@propagations) || {})
|
4
|
+
after_commit(:_run_active_propagation, on: on) unless _commit_callbacks.map(&:filter).include?(:_run_active_propagation)
|
5
|
+
self.class_variable_get(:@@propagations)[association] = {only: only, async: async, nested_async: nested_async}
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActivePropagation::InstanceExtensions
|
2
|
+
def _run_active_propagation
|
3
|
+
klass = self.class
|
4
|
+
klass.class_variable_get(:@@propagations).each do |association, config|
|
5
|
+
args = [self.class.to_s, self.id, association.to_s, config[:only]]
|
6
|
+
if self.destroyed?
|
7
|
+
if config[:async]
|
8
|
+
ActivePropagation::AsyncDeletor.run(*args)
|
9
|
+
else
|
10
|
+
ActivePropagation::Deletor.run(*args)
|
11
|
+
end
|
12
|
+
else
|
13
|
+
if config[:async]
|
14
|
+
ActivePropagation::AsyncUpdater.run(*args)
|
15
|
+
else
|
16
|
+
ActivePropagation::Updater.run(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_propagation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yoshiki Schmitz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activerecord
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: activesupport
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sidekiq
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: ActivePropagation provides classes and ActiveRecord extensions for propagating
|
140
|
+
changes amongst models, optionally using Sidekiq to parallelize the process.
|
141
|
+
email:
|
142
|
+
- yoshiki@masteryconnect.com
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- ".ruby-version"
|
150
|
+
- ".travis.yml"
|
151
|
+
- Gemfile
|
152
|
+
- Gemfile.lock
|
153
|
+
- README.md
|
154
|
+
- Rakefile
|
155
|
+
- active_propagation.gemspec
|
156
|
+
- bin/console
|
157
|
+
- bin/setup
|
158
|
+
- lib/active_propagation.rb
|
159
|
+
- lib/active_propagation/class_extensions.rb
|
160
|
+
- lib/active_propagation/instance_extensions.rb
|
161
|
+
- lib/active_propagation/propagater_helper.rb
|
162
|
+
- lib/active_propagation/version.rb
|
163
|
+
homepage: https://bitbucket.org/doweber/active_propagation/overview
|
164
|
+
licenses: []
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.2.2
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: propagates changes across models
|
186
|
+
test_files: []
|
187
|
+
has_rdoc:
|