hist 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|