activehistory 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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