hist 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +30 -0
  3. data/README.md +235 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/hist_manifest.js +2 -0
  6. data/app/assets/javascripts/hist/application.js +18 -0
  7. data/app/assets/javascripts/hist/vendor/ace-diff/ace-diff.min.js +2 -0
  8. data/app/assets/javascripts/hist/vendor/bootstrap/bootstrap.min.js +7 -0
  9. data/app/assets/javascripts/hist/vendor/bootstrap/popper.min.js +5 -0
  10. data/app/assets/javascripts/hist/version_diff.js +64 -0
  11. data/app/assets/stylesheets/hist/application.css +16 -0
  12. data/app/assets/stylesheets/hist/default.scss +20 -0
  13. data/app/assets/stylesheets/hist/vendor/ace-diff/ace-diff.min.css +2 -0
  14. data/app/controllers/hist/application_controller.rb +71 -0
  15. data/app/controllers/hist/pendings_controller.rb +21 -0
  16. data/app/controllers/hist/versions_controller.rb +171 -0
  17. data/app/helpers/hist/application_helper.rb +4 -0
  18. data/app/jobs/hist/application_job.rb +4 -0
  19. data/app/mailers/hist/application_mailer.rb +6 -0
  20. data/app/models/hist/application_record.rb +389 -0
  21. data/app/models/hist/config.rb +10 -0
  22. data/app/models/hist/hist_config.rb +124 -0
  23. data/app/models/hist/model.rb +214 -0
  24. data/app/models/hist/pending.rb +53 -0
  25. data/app/models/hist/version.rb +20 -0
  26. data/app/views/hist/_modal_popup.html.erb +29 -0
  27. data/app/views/hist/versions/diff.js.erb +53 -0
  28. data/app/views/layouts/hist/application.html.erb +16 -0
  29. data/app/views/partials/hist/_modal.html.erb +1 -0
  30. data/config/routes.rb +8 -0
  31. data/lib/generators/hist/db_generator.rb +39 -0
  32. data/lib/generators/hist/initializer_generator.rb +15 -0
  33. data/lib/generators/hist/install_generator.rb +29 -0
  34. data/lib/generators/hist/routes_generator.rb +29 -0
  35. data/lib/generators/hist/templates/db/create_hist_pendings.rb.erb +40 -0
  36. data/lib/generators/hist/templates/db/create_hist_versions.rb.erb +40 -0
  37. data/lib/generators/hist/templates/init/hist.rb +9 -0
  38. data/lib/hist.rb +19 -0
  39. data/lib/hist/engine.rb +33 -0
  40. data/lib/hist/versionnumber.rb +3 -0
  41. data/lib/tasks/hist_tasks.rake +4 -0
  42. metadata +156 -0
@@ -0,0 +1,2 @@
1
+ /*! Ace-diff | github.com/ace-diff/ace-diff */
2
+ .acediff__wrap{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;position:absolute;bottom:0;top:0;left:0;height:100%;width:100%;overflow:auto}.acediff__gutter{-webkit-box-flex:0;-ms-flex:0 0 60px;flex:0 0 60px;border-left:1px solid #999;border-right:1px solid #999;overflow:hidden}.acediff__gutter,.acediff__gutter svg{background-color:#efefef}.acediff__left,.acediff__right{height:100%;-webkit-box-flex:1;-ms-flex:1;flex:1}.acediff__diffLine{background-color:#d8f2ff;border-top:1px solid #a2d7f2;border-bottom:1px solid #a2d7f2;position:absolute;z-index:4}.acediff__diffLine.targetOnly{height:0!important;border-top:1px solid #a2d7f2;border-bottom:0;position:absolute}.acediff__connector{fill:#d8f2ff;stroke:#a2d7f2}.acediff__copy--left,.acediff__copy--right{position:relative}.acediff__copy--left div,.acediff__copy--right div{color:#000;text-shadow:1px 1px #fff;position:absolute;margin:2px 3px;cursor:pointer}.acediff__copy--right div:hover{color:#004ea0}.acediff__copy--left{float:right}.acediff__copy--left div{right:0}.acediff__copy--left div:hover{color:#c98100}
@@ -0,0 +1,71 @@
1
+ module Hist
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ def diff_base(model)
6
+ @aceMode = params[:mode].to_sym if params.has_key? :mode
7
+ @aceMode ||= :yaml
8
+
9
+ @height = params[:height] if params.has_key? :height
10
+ @height ||= 'screen'
11
+
12
+ field_path = params[:field_path] if params.has_key? :field_path
13
+ field_path ||= ''
14
+
15
+ exclude = params[:exclude] if params.has_key? :exclude
16
+ exclude ||= []
17
+
18
+ include = params[:include] if params.has_key? :include
19
+ include ||= []
20
+
21
+ # Remove some less needed differential fields
22
+ if include.blank? && !Hist.config.default_diff_exclude.nil?
23
+ if Hist.config.default_diff_exclude.class == Array
24
+ exclude += Hist.config.default_diff_exclude
25
+ else
26
+ exclude << Hist.config.default_diff_exclude
27
+ end
28
+
29
+ exclude.uniq!
30
+ end
31
+
32
+ only_diffs = false
33
+ only_diffs = params[:only_diffs] if params.has_key? :only_diffs
34
+
35
+ type = params[:type].to_sym if params.has_key? :type
36
+ type ||= :json
37
+
38
+ if params[:left_id] == 'current'
39
+ obj_right = model.find(params[:right_id]).reify
40
+ obj_left = obj_right.class.find(obj_right.id)
41
+ else
42
+ if params[:right_id] == 'current'
43
+ obj_left = model.find(params[:left_id]).reify
44
+ obj_right = obj_left.class.find(obj_left.id)
45
+ else
46
+ obj_right = model.find(params[:right_id]).reify
47
+ obj_left = model.find(params[:left_id]).reify
48
+ end
49
+ end
50
+
51
+ @diff = {left: obj_left.hist_json(exclude: exclude, include: include), right: obj_right.hist_json(exclude: exclude, include: include)}
52
+
53
+ if only_diffs
54
+ diff_vals = ApplicationRecord.only_hash_diffs(h1: @diff[:left], h2: @diff[:right])
55
+ @diff[:left] = diff_vals[:h1]
56
+ @diff[:right] = diff_vals[:h2]
57
+ end
58
+
59
+ if field_path.present?
60
+ @diff[:left] = eval('@diff[:left]' + field_path)
61
+ @diff[:right] = eval('@diff[:right]' + field_path)
62
+ end
63
+
64
+ @diff_escaped = {}
65
+ @diff_escaped[:left] = ActiveSupport::JSON.encode(@diff[:left])
66
+ @diff_escaped[:right] = ActiveSupport::JSON.encode(@diff[:right])
67
+
68
+ return [obj_left, obj_right]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,21 @@
1
+ module Hist
2
+ class PendingsController < Hist::ApplicationController
3
+ prepend_view_path 'app/views/hist/versions'
4
+
5
+ def diff
6
+ obj_left, obj_right = diff_base(Hist::Pending)
7
+
8
+ if obj_left.ver_id.nil?
9
+ @right_title = "Current Version (#{Time.now.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
10
+ else
11
+ @right_title = "Submitted (Pending): #{obj_left.pending_id} (#{obj_left.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
12
+ end
13
+
14
+ if obj_right.ver_id.nil?
15
+ @left_title = "Current Version (#{Time.now.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
16
+ else
17
+ @left_title = "Submitted (Pending): #{obj_right.pending_id} (#{obj_right.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,171 @@
1
+ module Hist
2
+ class VersionsController < Hist::ApplicationController
3
+ def diff_old
4
+ @aceMode = params[:mode].to_sym if params.has_key? :mode
5
+ @aceMode ||= :yaml
6
+
7
+ @height = params[:height] if params.has_key? :height
8
+ @height ||= 'screen'
9
+
10
+ field_path = params[:field_path] if params.has_key? :field_path
11
+ field_path ||= ''
12
+
13
+ exclude = params[:exclude] if params.has_key? :exclude
14
+ exclude ||= []
15
+
16
+ include = params[:include] if params.has_key? :include
17
+ include ||= []
18
+
19
+ # Remove some less needed differential fields
20
+ if include.blank?
21
+ exclude << 'created_at'
22
+ exclude << 'hist_extra'
23
+ exclude << 'whodunnit'
24
+ exclude << 'pending_id'
25
+ exclude << 'ver_id'
26
+ exclude << 'user_id'
27
+ exclude.uniq!
28
+ end
29
+
30
+ only_diffs = false
31
+ only_diffs = params[:only_diffs] if params.has_key? :only_diffs
32
+
33
+ type = params[:type].to_sym if params.has_key? :type
34
+ type ||= :json
35
+
36
+ if params[:left_id] == 'current'
37
+ obj_right = Hist::Version.find(params[:right_id]).reify
38
+ obj_left = obj_right.class.find(obj_right.id)
39
+ else
40
+ if params[:right_id] == 'current'
41
+ obj_left = Hist::Version.find(params[:left_id]).reify
42
+ obj_right = obj_left.class.find(obj_left.id)
43
+ else
44
+ obj_right = Hist::Version.find(params[:right_id]).reify
45
+ obj_left = Hist::Version.find(params[:left_id]).reify
46
+ end
47
+ end
48
+
49
+ @diff = {left: obj_left.hist_json(exclude: exclude, include: include), right: obj_right.hist_json(exclude: exclude, include: include)}
50
+
51
+ if only_diffs
52
+ diff_vals = ApplicationRecord.only_hash_diffs(h1: @diff[:left], h2: @diff[:right])
53
+ @diff[:left] = diff_vals[:h1]
54
+ @diff[:right] = diff_vals[:h2]
55
+ end
56
+
57
+ if field_path.present?
58
+ @diff[:left] = eval('@diff[:left]' + field_path)
59
+ @diff[:right] = eval('@diff[:right]' + field_path)
60
+ end
61
+
62
+ @diff_escaped = {}
63
+ @diff_escaped[:left] = ActiveSupport::JSON.encode(@diff[:left])
64
+ @diff_escaped[:right] = ActiveSupport::JSON.encode(@diff[:right])
65
+
66
+
67
+ if obj_left.ver_id.nil?
68
+ @left_title = "Current Version (#{Time.now.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
69
+ else
70
+ if obj_left.respond_to?(:hist_created_at)
71
+ @left_title = "Version #{obj_left.ver_id} (#{obj_left.hist_created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
72
+ elsif obj_left.respond_to?(:created_at)
73
+ @left_title = "Version #{obj_left.ver_id} (#{obj_left.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
74
+ elsif obj_left.respond_to?(:updated_at)
75
+ @left_title = "Version #{obj_left.ver_id} (#{obj_left.updated_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
76
+ else
77
+ @left_title = "Version"
78
+ end
79
+ end
80
+
81
+ if obj_right.ver_id.nil?
82
+ @right_title = "Current Version (#{Time.now.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
83
+ else
84
+ if obj_right.respond_to?(:hist_created_at)
85
+ @right_title = "Version #{obj_right.ver_id} (#{obj_right.hist_created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
86
+ elsif obj_right.respond_to?(:created_at)
87
+ @right_title = "Version #{obj_right.ver_id} (#{obj_right.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
88
+ elsif obj_right.respond_to?(:updated_at)
89
+ # FIXME: DO BETTER
90
+ obj_right.versions.each_with_index do |ver, index|
91
+ if ver.ver_id.to_s == obj_right.ver_id
92
+ @left_title = "Version #{obj_right.versions.size - (index)} (#{obj_right.updated_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
93
+ end
94
+ end
95
+ #@right_title = "Version #{obj_right.ver_id} (#{obj_right.updated_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
96
+ else
97
+ @right_title = "Version"
98
+ end
99
+ end
100
+ end
101
+
102
+ def diff
103
+ obj_left, obj_right = diff_base(Hist::Version)
104
+
105
+
106
+ if obj_left.ver_id.nil?
107
+ @left_title = "Current Version (#{Time.now.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
108
+ else
109
+ if obj_left.respond_to?(:hist_created_at)
110
+ #@left_title = "Version #{obj_left.ver_id} (#{obj_left.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
111
+ # FIXME: DO BETTER
112
+ obj_left.versions.each_with_index do |ver, index|
113
+ if ver.ver_id.to_s == obj_left.ver_id.to_s
114
+ @left_title = "Version #{obj_left.versions.size - (index)} (#{obj_left.hist_created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
115
+ end
116
+ end
117
+ elsif obj_left.respond_to?(:created_at)
118
+ #@left_title = "Version #{obj_left.ver_id} (#{obj_left.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
119
+ # FIXME: DO BETTER
120
+ obj_left.versions.each_with_index do |ver, index|
121
+ if ver.ver_id.to_s == obj_left.ver_id.to_s
122
+ @left_title = "Version #{obj_left.versions.size - (index)} (#{obj_left.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
123
+ end
124
+ end
125
+ elsif obj_left.respond_to?(:updated_at)
126
+ # FIXME: DO BETTER
127
+ obj_left.versions.each_with_index do |ver, index|
128
+ if ver.ver_id.to_s == obj_left.ver_id.to_s
129
+ @left_title = "Version #{obj_left.versions.size - (index)} (#{obj_left.updated_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
130
+ end
131
+ end
132
+
133
+ else
134
+ @left_title = "Version"
135
+ end
136
+ end
137
+
138
+ if obj_right.ver_id.nil?
139
+ @right_title = "Current Version (#{Time.now.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
140
+ else
141
+
142
+ if obj_right.respond_to?(:hist_created_at)
143
+ # FIXME: DO BETTER
144
+ obj_right.versions.each_with_index do |ver, index|
145
+ if ver.ver_id.to_s == obj_right.ver_id.to_s
146
+ @right_title = "Version #{obj_right.versions.size - (index)} (#{obj_right.hist_created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
147
+ end
148
+ end
149
+ elsif obj_right.respond_to?(:created_at)
150
+ #@right_title = "Version #{obj_right.ver_id} (#{obj_right.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
151
+ # FIXME: DO BETTER
152
+ obj_right.versions.each_with_index do |ver, index|
153
+ if ver.ver_id.to_s == obj_right.ver_id.to_s
154
+ @right_title = "Version #{obj_right.versions.size - (index)} (#{obj_right.created_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
155
+ end
156
+ end
157
+ elsif obj_right.respond_to?(:updated_at)
158
+ #@right_title = "Version #{obj_right.ver_id} (#{obj_right.updated_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
159
+ # FIXME: DO BETTER
160
+ obj_right.versions.each_with_index do |ver, index|
161
+ if ver.ver_id.to_s == obj_right.ver_id.to_s
162
+ @right_title = "Version #{obj_right.versions.size - (index)} (#{obj_right.updated_at.in_time_zone('Eastern Time (US & Canada)').strftime('%B %e, %Y at %I:%M %p')} EST)"
163
+ end
164
+ end
165
+ else
166
+ @right_title = "Version"
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,4 @@
1
+ module Hist
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Hist
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Hist
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,389 @@
1
+ module Hist
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ include Discard::Model
4
+
5
+ self.abstract_class = true
6
+
7
+ # This could be done better...
8
+ def self.raw_get(obj:, user: nil, extra: nil, only: 'kept')
9
+ if user.nil?
10
+ if extra.nil?
11
+ versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id).send(only).reverse
12
+ else
13
+ versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id, extra: extra).send(only).reverse
14
+ end
15
+
16
+ else
17
+ if extra.nil?
18
+ # .to_s to support either user object or username
19
+ versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s).send(only).reverse
20
+ else
21
+ # .to_s to support either user object or username
22
+ versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s, extra: extra).send(only).reverse
23
+ end
24
+ end
25
+
26
+ versions
27
+ end
28
+
29
+ def self.get(obj:, user: nil, extra: nil, only: 'kept')
30
+ hash_versions = self.raw_get(obj: obj, user: user, extra: extra, only: only)
31
+ versions = hash_versions.map {|v| v.reify }
32
+ versions
33
+ end
34
+
35
+ def self.encode(obj:, associations: nil)
36
+ if associations.nil?
37
+ associations = Hist.model(obj:obj).constantize.hist_config.associations(obj: obj).map(&:name)
38
+ else
39
+ associations.each do |assoc|
40
+ unless Hist.model(obj:obj).constantize.hist_config.valid_association(klass: obj.class, assoc: assoc)
41
+ associations.delete(assoc)
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ if associations.nil?
48
+ if obj.class.attribute_names.include?("type")
49
+ encoded = ActiveSupport::JSON.encode obj, methods: :type
50
+ else
51
+ encoded = ActiveSupport::JSON.encode obj
52
+ end
53
+ else
54
+ # Include type in the associations to support STI
55
+ fixed_associations = []
56
+
57
+ associations.each do |assoc|
58
+ h = {}
59
+ h[assoc] = {}
60
+ # FIXME: This only works if the type file isn't custom.
61
+ unless obj.send(assoc).nil?
62
+ if obj.send(assoc).respond_to?("klass") && obj.send(assoc).klass.attribute_names.include?("type")
63
+ h[assoc] = {methods: :type}
64
+ elsif obj.send(assoc).class.respond_to?("attribute_names") && obj.send(assoc).class.attribute_names.include?("type")
65
+ h[assoc] = {methods: :type}
66
+ end
67
+ end
68
+
69
+ fixed_associations << h
70
+ end
71
+ if obj.class.attribute_names.include?("type")
72
+ encoded = ActiveSupport::JSON.encode obj, include: fixed_associations, methods: :type
73
+ else
74
+ encoded = ActiveSupport::JSON.encode obj, include: fixed_associations
75
+ end
76
+ end
77
+
78
+ encoded
79
+ end
80
+
81
+ def self.decode(obj:, associations: nil)
82
+ if obj.class == Hash
83
+ decoded = ActiveSupport::JSON.decode(obj: obj, associations: associations)
84
+ else
85
+ decoded = ActiveSupport::JSON.decode(encode(obj: obj, associations: associations))
86
+ end
87
+
88
+ decoded
89
+ end
90
+
91
+ def self.put(obj:, user: nil, extra: nil, exclude: [])
92
+ encoded = encode(obj: obj)
93
+
94
+ # Remove excluded fields... might be a better way to do this.
95
+ decoded = ActiveSupport::JSON.decode encoded
96
+ exclude.each do |attr|
97
+ decoded.delete(attr)
98
+ end
99
+
100
+ encoded = ActiveSupport::JSON.encode decoded
101
+
102
+ # Check to see if the last version is already saved... don't duplicate
103
+ # Potential flaw with version caching to watch out for
104
+ if obj.raw_versions.present?
105
+ return obj if encoded == obj.raw_versions.first.data
106
+ end
107
+
108
+ if user.nil?
109
+ if extra.nil?
110
+ return self.create(model: Hist.model(obj: obj), obj_id: obj.id, data: encoded)
111
+ else
112
+ return self.create(model: Hist.model(obj: obj), obj_id: obj.id, extra: extra.to_s, data: encoded)
113
+ end
114
+
115
+ else
116
+ if extra.nil?
117
+ # .to_s to support either user object or username
118
+ return self.create(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s, data: encoded)
119
+ else
120
+ # .to_s to support either user object or username
121
+ return self.create(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s, extra: extra.to_s, data: encoded)
122
+ end
123
+
124
+ end
125
+ end
126
+
127
+ # Need to add exclude[ActiveRecord::Reflection::ThroughReflection]
128
+ def self.to_json(obj:, exclude: [], include: [], associations: nil)
129
+ if associations.nil?
130
+ associations = Hist.model(obj:obj).constantize.hist_config.associations(obj: obj, exclude_through: true).map(&:name)
131
+ else
132
+ associations.each do |assoc|
133
+ unless Hist.model(obj:obj).constantize.hist_config.valid_association(klass: obj.class.base_class, assoc: assoc)
134
+ associations.delete(assoc)
135
+ end
136
+ end
137
+ end
138
+ assoc_to_s = associations.map { |val| val.to_s }
139
+
140
+ obj_hash = decode(obj: obj, associations: associations)
141
+
142
+ if exclude.present?
143
+ exclude.each do |e|
144
+ obj_hash = remove_key(h: obj_hash, val: e.to_s)
145
+ end
146
+ end
147
+
148
+ if include.present?
149
+ include.map! { |val| val.to_s }
150
+ obj_hash = include_keys(h: obj_hash, vals: include, associations: assoc_to_s)
151
+ end
152
+
153
+ # Only include associations we have configured
154
+ #Hist.model(obj:obj).constantize.hist_config.all_associations(klass: obj.class).each do |assoc|
155
+ #obj_hash.delete(assoc.to_s) unless associations.include?(assoc) || associations.include?(assoc.to_s)
156
+ #end
157
+
158
+ obj_hash
159
+ end
160
+
161
+ def self.to_yaml(obj:, exclude: [], include: [], associations: nil)
162
+ YAML.dump(self.to_json(obj: obj, exclude: exclude, include: include, associations: associations))
163
+ end
164
+
165
+ def self.only_hash_diffs(h1: {}, h2: {})
166
+ return_h1 = {}
167
+ return_h2 = {}
168
+
169
+ h1.each_key do |k|
170
+ if h1[k] != h2[k]
171
+ return_h1[k] = h1[k]
172
+ return_h2[k] = h2[k]
173
+ end
174
+ end
175
+
176
+ {h1: return_h1, h2: return_h2}
177
+ end
178
+
179
+ def self.include_keys(h: {}, vals: [], associations: [])
180
+ return_h = {}
181
+ h.each_key do |k|
182
+ if vals.include?(k.to_s)
183
+ return_h[k] = h[k]
184
+ elsif associations.include? k.to_s
185
+ return_h[k] = []
186
+ h[k].each_with_index do |_, idx|
187
+ return_h[k][idx] = {}
188
+ h[k][idx].each_key do |k2|
189
+ if vals.include? k2.to_s
190
+ return_h[k][idx][k2] = h[k][idx][k2]
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ return_h
197
+ end
198
+
199
+ def self.remove_key(h: {}, val: '')
200
+ #h = passed_h.clone
201
+ h.except! val
202
+ h.each_key do |k|
203
+ if h[k].class == Array
204
+ h[k].each_with_index { |_, idx|
205
+ if h[k][idx].class == Hash
206
+ h[k][idx].except! val
207
+ end
208
+ }
209
+ elsif h[k].class == Hash
210
+ h[k].except! val
211
+ end
212
+ end
213
+ end
214
+
215
+ def reify
216
+ #associations = self.model.constantize.reflect_on_all_associations(:has_many).map(&:name)
217
+
218
+ decoded = ActiveSupport::JSON.decode self.data
219
+ decoded.stringify_keys!
220
+
221
+ # Potential issue when changing STI class when removing associations... how to get all_associations for all STI?
222
+ if decoded["type"].present?
223
+ associations = self.model.constantize.hist_config.associations(klass: decoded["type"].constantize).map(&:name)
224
+ all_associations = self.model.constantize.hist_config.all_associations(klass: decoded["type"].constantize).map(&:name)
225
+ else
226
+ associations = self.model.constantize.hist_config.associations(klass: self.model.constantize).map(&:name)
227
+ all_associations = self.model.constantize.hist_config.all_associations(klass: self.model.constantize).map(&:name)
228
+ end
229
+
230
+ associations_to_process = {}
231
+
232
+ # Can't instantiate with the association params... need to process those once the object is up
233
+ all_associations.each do |assoc|
234
+ if decoded.has_key? assoc.to_s
235
+ if associations.include? assoc
236
+ associations_to_process.merge!(decoded.slice(assoc.to_s))
237
+ end
238
+
239
+ decoded.delete(assoc.to_s)
240
+ end
241
+ end
242
+
243
+ #obj = self.model.constantize.new(decoded)
244
+ if decoded["id"].present? && self.model.constantize.exists?(id: decoded["id"])
245
+ obj = self.model.constantize.find(decoded["id"])
246
+ # If a version attribute no longer exists, will error at: https://github.com/rails/rails/blob/v5.2.0/activemodel/lib/active_model/attribute_assignment.rb
247
+ # So must verify each key and drop it otherwise... in the future, update the version to drop that key possibly?
248
+ decoded.each do |k, _|
249
+ setter = :"#{k}="
250
+ unless obj.respond_to?(setter)
251
+ decoded.delete(k)
252
+ end
253
+ end
254
+
255
+ obj.assign_attributes(decoded)
256
+ else
257
+ obj = self.model.constantize.new(decoded)
258
+ end
259
+
260
+ associations_to_process.each do |k,v|
261
+ assoc_collection = []
262
+ # Has Many
263
+ if v.class == Array
264
+ v.each do |d|
265
+ if d["id"].present? && obj.class.reflect_on_association(k).class_name.constantize.exists?(id: d["id"])
266
+ a = obj.class.reflect_on_association(k).class_name.constantize.find(d["id"])
267
+ # If a version attribute no longer exists, will error at: https://github.com/rails/rails/blob/v5.2.0/activemodel/lib/active_model/attribute_assignment.rb
268
+ # So must verify each key and drop it otherwise... in the future, update the version to drop that key possibly?
269
+ d.each do |k2, _|
270
+ setter = :"#{k2}="
271
+ unless a.respond_to?(setter)
272
+ d.delete(k2)
273
+ end
274
+ end
275
+
276
+ a.assign_attributes(d)
277
+ assoc_collection << a
278
+ else
279
+ assoc_collection << obj.class.reflect_on_association(k).class_name.constantize.new(d)
280
+ end
281
+ end
282
+ obj.send(k).proxy_association.target = assoc_collection
283
+ # Belongs To
284
+ else
285
+ if v["id"].present? && obj.class.reflect_on_association(k).class_name.constantize.exists?(id: v["id"])
286
+ a = obj.class.reflect_on_association(k).class_name.constantize.find(v["id"])
287
+ # If a version attribute no longer exists, will error at: https://github.com/rails/rails/blob/v5.2.0/activemodel/lib/active_model/attribute_assignment.rb
288
+ # So must verify each key and drop it otherwise... in the future, update the version to drop that key possibly?
289
+ v.each do |k2, _|
290
+ setter = :"#{k2}="
291
+ unless a.respond_to?(setter)
292
+ v.delete(k2)
293
+ end
294
+ end
295
+
296
+ a.assign_attributes(v)
297
+ assoc_collection = a
298
+ else
299
+ assoc_collection = obj.class.reflect_on_association(k).class_name.constantize.new(v)
300
+ end
301
+
302
+ without_persisting(assoc_collection) do
303
+ obj.send("#{k}=".to_sym, assoc_collection)
304
+ end
305
+
306
+ end
307
+
308
+ end
309
+
310
+ if self.class == Hist::Version
311
+ obj.ver_id = self.id
312
+ elsif self.class == Hist::Pending
313
+ obj.pending_id = self.id
314
+ end
315
+
316
+ obj.hist_whodunnit = self.whodunnit
317
+ obj.hist_extra = self.extra
318
+ obj.hist_created_at = self.created_at
319
+
320
+ obj
321
+ end
322
+
323
+ # From: https://github.com/westonganger/paper_trail-association_tracking/blob/5ed8cfbfa48cc773cc8a694dabec5a962d9c6cfe/lib/paper_trail_association_tracking/reifiers/has_one.rb
324
+ # Temporarily suppress #save so we can reassociate with the reified
325
+ # master of a has_one relationship. Since ActiveRecord 5 the related
326
+ # object is saved when it is assigned to the association. ActiveRecord
327
+ # 5 also happens to be the first version that provides #suppress.
328
+ def without_persisting(record)
329
+ if record.class.respond_to? :suppress
330
+ record.class.suppress { yield }
331
+ else
332
+ yield
333
+ end
334
+ end
335
+
336
+ # Many-To-Many ID changes. This causes a problem with historic many-to-many saving.
337
+ def self.fix_save_associations(obj:)
338
+
339
+ associations = Hist.model(obj: obj).constantize.hist_config.associations(klass: obj.class.base_class, exclude_through: true).map(&:name)
340
+ association_details = Hist.model(obj: obj).constantize.hist_config.all_associations(klass: obj.class.base_class, exclude_through: true)
341
+
342
+ current_obj = obj.class.find(obj.id)
343
+
344
+ # For has_many
345
+ associations.each do |k,v|
346
+ detail = association_details.select { |a| a.name == k}[0]
347
+ existing = current_obj.send(k)
348
+ version_set = obj.send(k)
349
+
350
+ unless detail.class == ActiveRecord::Reflection::BelongsToReflection || detail.class == ActiveRecord::Reflection::HasOneReflection
351
+ existing.each do |ex|
352
+ unless version_set.pluck(:id).include? ex.id
353
+ current_obj.send(k).delete(ex)
354
+ end
355
+ end
356
+
357
+ if Hist.model(obj: obj).constantize.hist_config.update_associations_on_save(klass: obj.class, assoc: k)
358
+ version_set.each do |ex|
359
+ ex.save!
360
+
361
+ unless existing.pluck(:id).include? ex.id
362
+ current_obj.send(k) << ex
363
+ end
364
+ end
365
+ else
366
+ version_set.each do |ex|
367
+ unless existing.pluck(:id).include? ex.id
368
+ ex_obj = ex.class.find(ex.id)
369
+ if ex_obj.present?
370
+ current_obj.send(k) << ex_obj
371
+ else
372
+ ex.save!
373
+ current_obj.send(k) << ex
374
+ end
375
+
376
+ end
377
+ end
378
+ end
379
+
380
+ end
381
+ end
382
+
383
+ current_obj.reload
384
+ current_obj
385
+
386
+ end
387
+
388
+ end
389
+ end