hist 0.2.0

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.
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