activehistory 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sunstone.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1 @@
1
+ # activehistory-client
@@ -0,0 +1,12 @@
1
+ require 'rake/testtask'
2
+ require 'rdoc/task'
3
+
4
+ task :console do
5
+ exec 'irb -I lib -r activehistory.rb'
6
+ end
7
+ task :c => :console
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << 'test'
11
+ t.test_files = FileList['test/**/*_test.rb']
12
+ end
@@ -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