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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +30 -0
- data/README.md +235 -0
- data/Rakefile +32 -0
- data/app/assets/config/hist_manifest.js +2 -0
- data/app/assets/javascripts/hist/application.js +18 -0
- data/app/assets/javascripts/hist/vendor/ace-diff/ace-diff.min.js +2 -0
- data/app/assets/javascripts/hist/vendor/bootstrap/bootstrap.min.js +7 -0
- data/app/assets/javascripts/hist/vendor/bootstrap/popper.min.js +5 -0
- data/app/assets/javascripts/hist/version_diff.js +64 -0
- data/app/assets/stylesheets/hist/application.css +16 -0
- data/app/assets/stylesheets/hist/default.scss +20 -0
- data/app/assets/stylesheets/hist/vendor/ace-diff/ace-diff.min.css +2 -0
- data/app/controllers/hist/application_controller.rb +71 -0
- data/app/controllers/hist/pendings_controller.rb +21 -0
- data/app/controllers/hist/versions_controller.rb +171 -0
- data/app/helpers/hist/application_helper.rb +4 -0
- data/app/jobs/hist/application_job.rb +4 -0
- data/app/mailers/hist/application_mailer.rb +6 -0
- data/app/models/hist/application_record.rb +389 -0
- data/app/models/hist/config.rb +10 -0
- data/app/models/hist/hist_config.rb +124 -0
- data/app/models/hist/model.rb +214 -0
- data/app/models/hist/pending.rb +53 -0
- data/app/models/hist/version.rb +20 -0
- data/app/views/hist/_modal_popup.html.erb +29 -0
- data/app/views/hist/versions/diff.js.erb +53 -0
- data/app/views/layouts/hist/application.html.erb +16 -0
- data/app/views/partials/hist/_modal.html.erb +1 -0
- data/config/routes.rb +8 -0
- data/lib/generators/hist/db_generator.rb +39 -0
- data/lib/generators/hist/initializer_generator.rb +15 -0
- data/lib/generators/hist/install_generator.rb +29 -0
- data/lib/generators/hist/routes_generator.rb +29 -0
- data/lib/generators/hist/templates/db/create_hist_pendings.rb.erb +40 -0
- data/lib/generators/hist/templates/db/create_hist_versions.rb.erb +40 -0
- data/lib/generators/hist/templates/init/hist.rb +9 -0
- data/lib/hist.rb +19 -0
- data/lib/hist/engine.rb +33 -0
- data/lib/hist/versionnumber.rb +3 -0
- data/lib/tasks/hist_tasks.rake +4 -0
- 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,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
|