activehistory 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +90 -0
- data/LICENSE +21 -0
- data/README.md +1 -0
- data/Rakefile +12 -0
- data/activehistory.gemspec +40 -0
- data/lib/activehistory.rb +39 -0
- data/lib/activehistory/action.rb +20 -0
- data/lib/activehistory/adapters/active_record.rb +202 -0
- data/lib/activehistory/connection.rb +121 -0
- data/lib/activehistory/event.rb +50 -0
- data/lib/activehistory/exceptions.rb +32 -0
- data/lib/activehistory/regard.rb +11 -0
- data/lib/activehistory/version.rb +3 -0
- data/test/active_record_adapter/association_test/belongs_to_association_test.rb +82 -0
- data/test/active_record_adapter/association_test/has_and_belongs_to_many_test.rb +224 -0
- data/test/active_record_adapter/association_test/has_many_association_test.rb +81 -0
- data/test/active_record_adapter/association_test/has_one_association_test.rb +141 -0
- data/test/active_record_adapter/create_test.rb +58 -0
- data/test/active_record_adapter/destroy_test.rb +38 -0
- data/test/active_record_adapter/event_test.rb +59 -0
- data/test/active_record_adapter/save_test.rb +67 -0
- data/test/factories.rb +36 -0
- data/test/models.rb +60 -0
- data/test/schema.rb +59 -0
- data/test/test_helper.rb +55 -0
- metadata +282 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 643c618d011f3af9b72ba7a7fadf3a713aeb0cf4
|
4
|
+
data.tar.gz: bc2d64e5b59d78deb335733d46eef644defc2656
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 53870be2424fa9b20e5bea593fb7691f9cd9e92214d558039ec25122f20ef5eb399cdd406c655e71bc978a5fc62289302d8b8be402f3767c210af78245d332bd
|
7
|
+
data.tar.gz: 4d8b2f3dea6b2aac7922bc8d28868a4b7b6d4f52917d920559f3041ab10fee6e7a5c6fe1aaa661d34b215f5c7f7d8ae6d6690dba93b1bc92ea6b50f0338fed33
|
data/.gitignore
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
*.gem
|
2
|
+
*.DS_Store
|
3
|
+
/coverage/
|
4
|
+
/tmp/
|
5
|
+
|
6
|
+
# Used by dotenv library to load environment variables.
|
7
|
+
# .env
|
8
|
+
|
9
|
+
## Documentation cache and generated files:
|
10
|
+
/.yardoc/
|
11
|
+
/_yardoc/
|
12
|
+
/doc/
|
13
|
+
/rdoc/
|
14
|
+
|
15
|
+
## Environment normalization:
|
16
|
+
/.bundle/
|
17
|
+
/vendor/bundle
|
18
|
+
/lib/bundler/man/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
activehistory (0.1)
|
5
|
+
activerecord (= 5.0.0.1)
|
6
|
+
arel (~> 7.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (5.0.0.1)
|
12
|
+
activesupport (= 5.0.0.1)
|
13
|
+
activerecord (5.0.0.1)
|
14
|
+
activemodel (= 5.0.0.1)
|
15
|
+
activesupport (= 5.0.0.1)
|
16
|
+
arel (~> 7.0)
|
17
|
+
activesupport (5.0.0.1)
|
18
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
19
|
+
i18n (~> 0.7)
|
20
|
+
minitest (~> 5.1)
|
21
|
+
tzinfo (~> 1.1)
|
22
|
+
addressable (2.4.0)
|
23
|
+
ansi (1.5.0)
|
24
|
+
arel (7.1.2)
|
25
|
+
builder (3.2.2)
|
26
|
+
concurrent-ruby (1.0.2)
|
27
|
+
crack (0.4.3)
|
28
|
+
safe_yaml (~> 1.0.0)
|
29
|
+
docile (1.1.5)
|
30
|
+
factory_girl (4.7.0)
|
31
|
+
activesupport (>= 3.0.0)
|
32
|
+
faker (1.6.6)
|
33
|
+
i18n (~> 0.5)
|
34
|
+
hashdiff (0.3.0)
|
35
|
+
i18n (0.7.0)
|
36
|
+
json (1.8.3)
|
37
|
+
metaclass (0.0.4)
|
38
|
+
minitest (5.9.1)
|
39
|
+
minitest-reporters (1.1.11)
|
40
|
+
ansi
|
41
|
+
builder
|
42
|
+
minitest (>= 5.0)
|
43
|
+
ruby-progressbar
|
44
|
+
mocha (1.1.0)
|
45
|
+
metaclass (~> 0.0.1)
|
46
|
+
pg (0.18.4)
|
47
|
+
rake (11.3.0)
|
48
|
+
rdoc (4.2.2)
|
49
|
+
json (~> 1.4)
|
50
|
+
ruby-progressbar (1.8.1)
|
51
|
+
safe_yaml (1.0.4)
|
52
|
+
sdoc (0.4.1)
|
53
|
+
json (~> 1.7, >= 1.7.7)
|
54
|
+
rdoc (~> 4.0)
|
55
|
+
sdoc-templates-42floors (0.3)
|
56
|
+
sdoc
|
57
|
+
simplecov (0.12.0)
|
58
|
+
docile (~> 1.1.0)
|
59
|
+
json (>= 1.8, < 3)
|
60
|
+
simplecov-html (~> 0.10.0)
|
61
|
+
simplecov-html (0.10.0)
|
62
|
+
thread_safe (0.3.5)
|
63
|
+
tzinfo (1.2.2)
|
64
|
+
thread_safe (~> 0.1)
|
65
|
+
webmock (2.1.0)
|
66
|
+
addressable (>= 2.3.6)
|
67
|
+
crack (>= 0.3.2)
|
68
|
+
hashdiff
|
69
|
+
|
70
|
+
PLATFORMS
|
71
|
+
ruby
|
72
|
+
|
73
|
+
DEPENDENCIES
|
74
|
+
activehistory!
|
75
|
+
bundler
|
76
|
+
factory_girl
|
77
|
+
faker
|
78
|
+
minitest
|
79
|
+
minitest-reporters
|
80
|
+
mocha
|
81
|
+
pg
|
82
|
+
rake
|
83
|
+
rdoc
|
84
|
+
sdoc
|
85
|
+
sdoc-templates-42floors
|
86
|
+
simplecov
|
87
|
+
webmock
|
88
|
+
|
89
|
+
BUNDLED WITH
|
90
|
+
1.12.5
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 42Floors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# activehistory-client
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path("../lib/activehistory/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "activehistory"
|
5
|
+
s.version = ActiveHistory::VERSION
|
6
|
+
s.authors = ["Jon Bracy"]
|
7
|
+
s.email = ["jonbracy@gmail.com"]
|
8
|
+
s.homepage = "https://activehistory.com"
|
9
|
+
s.summary = %q{Track changes to ActiveRecord models}
|
10
|
+
s.description = <<~DESC
|
11
|
+
ActiveHistory tracks and logs changes to your ActiveRecord models and
|
12
|
+
relationships for auditing in the future.
|
13
|
+
DESC
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# Developoment
|
21
|
+
s.add_development_dependency 'rake'
|
22
|
+
s.add_development_dependency 'rdoc'
|
23
|
+
s.add_development_dependency 'sdoc'
|
24
|
+
s.add_development_dependency 'bundler'
|
25
|
+
s.add_development_dependency 'minitest'
|
26
|
+
s.add_development_dependency 'minitest-reporters'
|
27
|
+
s.add_development_dependency 'mocha'
|
28
|
+
s.add_development_dependency 'faker'
|
29
|
+
s.add_development_dependency 'factory_girl'
|
30
|
+
s.add_development_dependency 'webmock'
|
31
|
+
s.add_development_dependency 'sdoc-templates-42floors'
|
32
|
+
s.add_development_dependency 'simplecov'
|
33
|
+
s.add_development_dependency 'pg'
|
34
|
+
|
35
|
+
# Runtime
|
36
|
+
# s.add_runtime_dependency 'msgpack'
|
37
|
+
# s.add_runtime_dependency 'cookie_store'
|
38
|
+
s.add_runtime_dependency 'arel', '~> 7.0'
|
39
|
+
s.add_runtime_dependency 'activerecord', '5.0.0.1'
|
40
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveHistory
|
2
|
+
|
3
|
+
mattr_accessor :connection
|
4
|
+
|
5
|
+
def self.configure(settings)
|
6
|
+
@@connection = ActiveHistory::Connection.new(settings)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.configured?
|
10
|
+
class_variable_defined?(:@@connection)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.url
|
14
|
+
@@connection.url
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.encapsulate(id_or_options=nil)
|
18
|
+
Thread.current[:activehistory_event] = id_or_options
|
19
|
+
yield
|
20
|
+
ensure
|
21
|
+
if Thread.current[:activehistory_event].is_a?(ActiveHistory::Event)
|
22
|
+
Thread.current[:activehistory_event].save!
|
23
|
+
end
|
24
|
+
Thread.current[:activehistory_event] = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'activehistory/connection'
|
30
|
+
require 'activehistory/event'
|
31
|
+
require 'activehistory/action'
|
32
|
+
require 'activehistory/regard'
|
33
|
+
require 'activehistory/version'
|
34
|
+
require 'activehistory/exceptions'
|
35
|
+
|
36
|
+
if defined?(ActiveRecord::VERSION)
|
37
|
+
require 'activehistory/adapters/active_record'
|
38
|
+
ActiveRecord::Base.include(ActiveHistory::Adapter::ActiveRecord)
|
39
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class ActiveHistory::Action
|
2
|
+
|
3
|
+
attr_accessor :type, :timestamp, :subject, :diff
|
4
|
+
|
5
|
+
def initialize(attrs)
|
6
|
+
attrs.each do |k,v|
|
7
|
+
self.send("#{k}=", v)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
{
|
13
|
+
diff: diff.as_json,
|
14
|
+
subject: @subject,
|
15
|
+
timestamp: @timestamp.iso8601(3),
|
16
|
+
type: @type
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
module ActiveHistory::Adapter
|
2
|
+
module ActiveRecord
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
|
7
|
+
def self.extended(other)
|
8
|
+
other.before_save :activehistory_start
|
9
|
+
other.before_destroy :activehistory_start
|
10
|
+
|
11
|
+
other.after_create { activehistory_track(:create) }
|
12
|
+
other.after_update { activehistory_track(:update) }
|
13
|
+
other.before_destroy { activehistory_track(:destroy) }
|
14
|
+
|
15
|
+
other.after_commit { activehistory_complete }
|
16
|
+
end
|
17
|
+
|
18
|
+
def inherited(subclass)
|
19
|
+
super
|
20
|
+
|
21
|
+
subclass.instance_variable_set('@activehistory', @activehistory.clone) if defined?(@activehistory)
|
22
|
+
end
|
23
|
+
|
24
|
+
def track(exclude: [], habtm_model: nil)
|
25
|
+
@activehistory = {exclude: Array(exclude), habtm_model: habtm_model}
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
|
29
|
+
super
|
30
|
+
habtm_model = self.const_get("HABTM_#{name.to_s.camelize}")
|
31
|
+
|
32
|
+
habtm_model.track habtm_model: {
|
33
|
+
:left_side => { foreign_key: "#{base_class.name.underscore}_id", inverse_of: name.to_s },
|
34
|
+
name.to_s.singularize.to_sym => {inverse_of: self.name.underscore.pluralize.to_s}
|
35
|
+
}
|
36
|
+
|
37
|
+
callback = ->(method, owner, record) {
|
38
|
+
owner.activehistory_association_udpated(
|
39
|
+
record.class.reflect_on_association(owner.class.reflect_on_association(name.to_s).options[:inverse_of].to_s),
|
40
|
+
owner.id,
|
41
|
+
removed: [record.id],
|
42
|
+
timestamp: owner.activehistory_timestamp
|
43
|
+
)
|
44
|
+
record.activehistory_association_udpated(
|
45
|
+
owner.class.reflect_on_association(name.to_s),
|
46
|
+
record.id,
|
47
|
+
removed: [owner.id],
|
48
|
+
timestamp: owner.activehistory_timestamp
|
49
|
+
)
|
50
|
+
}
|
51
|
+
self.send("after_remove_for_#{name}=", Array(self.send("after_remove_for_#{name}")).compact + [callback])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def activehistory_id
|
56
|
+
"#{self.class.name}/#{id}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def activehistory_timestamp
|
60
|
+
@activehistory_timestamp ||= Time.now.utc
|
61
|
+
end
|
62
|
+
|
63
|
+
def activehistory_start
|
64
|
+
if activehistory_tracking && !instance_variable_defined?(:@activehistory_finish)
|
65
|
+
@activehistory_finish = !Thread.current[:activehistory_event]
|
66
|
+
end
|
67
|
+
@activehistory_timestamp = Time.now.utc
|
68
|
+
end
|
69
|
+
|
70
|
+
def activehistory_complete
|
71
|
+
@activehistory_timestamp = nil
|
72
|
+
if instance_variable_defined?(:@activehistory_finish) && @activehistory_finish && activehistory_tracking
|
73
|
+
activehistory_event.save! if activehistory_event
|
74
|
+
Thread.current[:activehistory_event] = nil
|
75
|
+
@activehistory_timestamp = nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def activehistory_tracking
|
80
|
+
if ActiveHistory.configured? && self.class.instance_variable_defined?(:@activehistory)
|
81
|
+
self.class.instance_variable_get(:@activehistory)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def activehistory_event
|
86
|
+
case Thread.current[:activehistory_event]
|
87
|
+
when ActiveHistory::Event
|
88
|
+
Thread.current[:activehistory_event]
|
89
|
+
when Hash
|
90
|
+
Thread.current[:activehistory_event][:timestamp] ||= @activehistory_timestamp
|
91
|
+
Thread.current[:activehistory_event] = ActiveHistory::Event.new(Thread.current[:activehistory_event])
|
92
|
+
when Fixnum
|
93
|
+
else
|
94
|
+
Thread.current[:activehistory_event] = ActiveHistory::Event.new(timestamp: @activehistory_timestamp)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def activehistory_track(type)
|
99
|
+
return if !activehistory_tracking
|
100
|
+
|
101
|
+
if type == :create || type == :update
|
102
|
+
diff = self.changes.select { |k,v| !activehistory_tracking[:exclude].include?(k.to_sym) }
|
103
|
+
if type == :create
|
104
|
+
self.class.columns.each do |column|
|
105
|
+
if !diff[column.name] && !activehistory_tracking[:exclude].include?(column.name.to_sym) && column.default != self.attributes[column.name]
|
106
|
+
diff[column.name] = [nil, self.attributes[column.name]]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
elsif type == :destroy
|
111
|
+
diff = self.attributes.select { |k| !activehistory_tracking[:exclude].include?(k.to_sym) }.map { |k, i| [k, [i, nil]] }.to_h
|
112
|
+
end
|
113
|
+
|
114
|
+
return if type == :update && diff.size == 0
|
115
|
+
|
116
|
+
if !activehistory_tracking[:habtm_model]
|
117
|
+
activehistory_event.action!({
|
118
|
+
type: type,
|
119
|
+
subject: self.activehistory_id,
|
120
|
+
diff: diff,
|
121
|
+
timestamp: @activehistory_timestamp
|
122
|
+
})
|
123
|
+
end
|
124
|
+
|
125
|
+
self._reflections.each do |key, reflection|
|
126
|
+
foreign_key = activehistory_tracking.dig(:habtm_model, reflection.name, :foreign_key) || reflection.foreign_key
|
127
|
+
|
128
|
+
if areflection = self.class.reflect_on_association(reflection.name)
|
129
|
+
if areflection.macro == :has_and_belongs_to_many && type == :create
|
130
|
+
self.send("#{areflection.name.to_s.singularize}_ids").each do |fid|
|
131
|
+
next unless fid
|
132
|
+
activehistory_association_udpated(areflection, fid, added: [diff['id'][1]], timestamp: activehistory_timestamp)
|
133
|
+
activehistory_association_udpated(areflection.klass.reflect_on_association(areflection.options[:inverse_of]), diff['id'][1], added: [fid], timestamp: activehistory_timestamp, type: :create)
|
134
|
+
end
|
135
|
+
elsif areflection.macro == :has_and_belongs_to_many && type == :destroy
|
136
|
+
self.send("#{areflection.name.to_s.singularize}_ids").each do |fid|
|
137
|
+
activehistory_association_udpated(areflection, fid, removed: [diff['id'][0]], timestamp: activehistory_timestamp, type: :update)
|
138
|
+
activehistory_association_udpated(areflection.klass.reflect_on_association(areflection.options[:inverse_of]), diff['id'][0], removed: [fid], timestamp: activehistory_timestamp, type: :update)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
next unless reflection.macro == :belongs_to && (type == :destroy || diff.has_key?(foreign_key))
|
144
|
+
|
145
|
+
case type
|
146
|
+
when :create
|
147
|
+
old_id = nil
|
148
|
+
new_id = diff[foreign_key][1]
|
149
|
+
when :destroy
|
150
|
+
old_id = diff[foreign_key][0]
|
151
|
+
new_id = nil
|
152
|
+
else
|
153
|
+
old_id = diff[foreign_key][0]
|
154
|
+
new_id = diff[foreign_key][1]
|
155
|
+
end
|
156
|
+
|
157
|
+
relation_id = self.id || diff.find { |k, v| k != foreign_key }[1][1]
|
158
|
+
|
159
|
+
if reflection.polymorphic?
|
160
|
+
else
|
161
|
+
activehistory_association_udpated(reflection, old_id, removed: [relation_id], timestamp: activehistory_timestamp) if old_id
|
162
|
+
activehistory_association_udpated(reflection, new_id, added: [relation_id], timestamp: activehistory_timestamp) if new_id
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
def activehistory_association_udpated(reflection, id, added: [], removed: [], timestamp: nil, type: :update)
|
170
|
+
if inverse_of = activehistory_tracking.dig(:habtm_model, reflection.name, :inverse_of)
|
171
|
+
inverse_of = reflection.klass.reflect_on_association(inverse_of)
|
172
|
+
else
|
173
|
+
inverse_of = reflection.inverse_of
|
174
|
+
end
|
175
|
+
|
176
|
+
if inverse_of.nil?
|
177
|
+
puts "NO INVERSE for #{self.class}.#{reflection.name}!!!"
|
178
|
+
return
|
179
|
+
end
|
180
|
+
|
181
|
+
model_name = reflection.klass.base_class.model_name.name
|
182
|
+
|
183
|
+
action = activehistory_event.action_for("#{model_name}/#{id}") || activehistory_event.action!({
|
184
|
+
type: type,
|
185
|
+
subject: "#{model_name}/#{id}",
|
186
|
+
timestamp: timestamp# || Time.now
|
187
|
+
})
|
188
|
+
|
189
|
+
action.diff ||= {}
|
190
|
+
if inverse_of.collection? || activehistory_tracking[:habtm_model]
|
191
|
+
diff_key = "#{inverse_of.name.to_s.singularize}_ids"
|
192
|
+
action.diff[diff_key] ||= [[], []]
|
193
|
+
action.diff[diff_key][0] |= removed
|
194
|
+
action.diff[diff_key][1] |= added
|
195
|
+
else
|
196
|
+
diff_key = "#{inverse_of.name.to_s.singularize}_id"
|
197
|
+
action.diff[diff_key] ||= [removed.first, added.first]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|