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,124 @@
|
|
1
|
+
module Hist
|
2
|
+
class HistConfig
|
3
|
+
|
4
|
+
def initialize(associations:nil, model:, max_versions:, max_pendings:, include: [], exclude: [], auto_version:)
|
5
|
+
@associations = associations
|
6
|
+
@model = model
|
7
|
+
@max_versions = max_versions
|
8
|
+
@max_pendings = max_pendings
|
9
|
+
@include = include
|
10
|
+
@exclude = exclude
|
11
|
+
@auto_version = auto_version
|
12
|
+
end
|
13
|
+
|
14
|
+
def auto_version
|
15
|
+
@auto_version
|
16
|
+
end
|
17
|
+
|
18
|
+
def max_versions
|
19
|
+
@max_versions
|
20
|
+
end
|
21
|
+
|
22
|
+
def max_pendings
|
23
|
+
@max_pendings
|
24
|
+
end
|
25
|
+
|
26
|
+
def include
|
27
|
+
@include
|
28
|
+
end
|
29
|
+
|
30
|
+
def exclude
|
31
|
+
@exclude
|
32
|
+
end
|
33
|
+
|
34
|
+
def model
|
35
|
+
@model
|
36
|
+
end
|
37
|
+
|
38
|
+
# Support STI
|
39
|
+
def associations(obj: nil, klass: nil, exclude_through:false)
|
40
|
+
return [] if @associations.nil?
|
41
|
+
|
42
|
+
klass = obj.class if obj.present?
|
43
|
+
klass = self.model.constantize if klass.nil?
|
44
|
+
|
45
|
+
if @associations.present?
|
46
|
+
if @associations.has_key? :all
|
47
|
+
return all_associations(klass: klass, exclude_through: exclude_through)
|
48
|
+
elsif @associations.has_key? :has_many
|
49
|
+
return all_associations(klass: klass, type: :has_many, exclude_through: exclude_through)
|
50
|
+
elsif @associations.has_key? :belongs_to
|
51
|
+
return all_associations(klass: klass,type: :belongs_to, exclude_through: exclude_through)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
return_assocs = []
|
56
|
+
@associations.each do |k, _|
|
57
|
+
return_assocs << (klass.reflect_on_all_associations.select { |a| a.name == k})[0] if valid_association(klass: klass, assoc: k, exclude_through: exclude_through)
|
58
|
+
end
|
59
|
+
|
60
|
+
return_assocs
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_association(klass:, assoc:, exclude_through:false)
|
64
|
+
all_assocs = klass.reflect_on_all_associations
|
65
|
+
association_details = all_assocs.select { |a| a.name == assoc }[0]
|
66
|
+
|
67
|
+
if association_details.present?
|
68
|
+
if exclude_through and association_details.class == ActiveRecord::Reflection::HasManyReflection
|
69
|
+
through_associations = all_assocs.select { |assoc| assoc.class == ActiveRecord::Reflection::ThroughReflection}
|
70
|
+
if through_associations.present?
|
71
|
+
assoc_check = through_associations.select { |assoc| assoc.options[:through] == assoc }
|
72
|
+
return false if assoc_check.present?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
return true
|
76
|
+
end
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
|
80
|
+
def all_associations(klass:, type: nil, exclude_through:false)
|
81
|
+
if type.nil?
|
82
|
+
associations = klass.reflect_on_all_associations
|
83
|
+
else
|
84
|
+
associations = klass.reflect_on_all_associations(type)
|
85
|
+
end
|
86
|
+
|
87
|
+
if exclude_through
|
88
|
+
through_associations = associations.select { |assoc| assoc.class == ActiveRecord::Reflection::ThroughReflection}
|
89
|
+
|
90
|
+
through_associations.each do |t_assoc|
|
91
|
+
assoc_to_delete = associations.select { |assoc| assoc.name == t_assoc.options[:through]}
|
92
|
+
associations.delete(assoc_to_delete[0]) if assoc_to_delete.present?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
associations
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_associations_on_save(klass:, assoc:)
|
100
|
+
if @associations.blank?
|
101
|
+
return false
|
102
|
+
end
|
103
|
+
|
104
|
+
if @associations.has_key? :all
|
105
|
+
return true if @associations[:all][:update_associations_on_save].nil? || @associations[:all][:update_associations_on_save]
|
106
|
+
elsif @associations.has_key? :has_many
|
107
|
+
return true if @associations[:has_many][:update_associations_on_save].nil? || @associations[:has_many][:update_associations_on_save]
|
108
|
+
elsif @associations.has_key? :belongs_to
|
109
|
+
return true if @associations[:belongs_to][:update_associations_on_save].nil? || @associations[:belongs_to][:update_associations_on_save]
|
110
|
+
end
|
111
|
+
|
112
|
+
item = @associations[assoc]
|
113
|
+
return true if !item.nil? and (item[:update_associations_on_save].nil? || item[:update_associations_on_save])
|
114
|
+
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
|
118
|
+
def options
|
119
|
+
@options
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
module Hist
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
#attribute :ver_id, :integer
|
7
|
+
#attribute :pending_id, :integer
|
8
|
+
#attribute :hist_whodunnit, :string
|
9
|
+
#attribute :hist_extra, :string
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def has_hist(associations: nil, max_versions:-1, max_pendings: -1, include: [], exclude: [], auto_version: true)
|
14
|
+
@hist_config = ::Hist::HistConfig.new(associations: associations, model: Hist.model(klass: self), max_versions: max_versions, max_pendings: max_pendings, include: include, exclude: exclude, auto_version: auto_version)
|
15
|
+
end
|
16
|
+
|
17
|
+
def hist_config
|
18
|
+
@hist_config
|
19
|
+
end
|
20
|
+
|
21
|
+
def hist_new_pendings(user: nil, extra: nil, only: 'kept')
|
22
|
+
::Hist::Pending.get(obj: nil, user: user, extra: extra, only: only)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def hist_save_actions
|
27
|
+
Hist::Pending.find(self.pending_id).discard unless self.pending_id.nil?
|
28
|
+
|
29
|
+
# Need to fix associations... won't update properly on parent save cause rails is baka.
|
30
|
+
if self.pending_id.present? || self.ver_id.present?
|
31
|
+
current = ApplicationRecord.fix_save_associations(obj: self)
|
32
|
+
else
|
33
|
+
current = self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Does this happen after reload?
|
37
|
+
u = self.hist_whodunnit if self.record_hist_whodunnit?
|
38
|
+
e = self.hist_extra if self.record_hist_extra?
|
39
|
+
current.record_version(user: u, extra: e) if self.class.base_class.hist_config.auto_version
|
40
|
+
|
41
|
+
if self.pending_id.present? || self.ver_id.present?
|
42
|
+
self.reload
|
43
|
+
else
|
44
|
+
self.reload_hist
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def hist_around_save
|
49
|
+
self.class.transaction do
|
50
|
+
yield
|
51
|
+
self.hist_save_actions
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def hist_after_save
|
56
|
+
self.class.transaction do
|
57
|
+
hist_save_actions
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api public
|
62
|
+
def record_version(user: nil, extra: nil)
|
63
|
+
::Hist::Version.put(obj: self, user: user, extra: extra)
|
64
|
+
end
|
65
|
+
|
66
|
+
def record_pending(user: nil, extra: nil)
|
67
|
+
::Hist::Pending.put(obj: self, user: user, extra: extra)
|
68
|
+
end
|
69
|
+
|
70
|
+
def version_at_temp(date)
|
71
|
+
@versions ||= ::Hist::Version.raw_get(obj: self)
|
72
|
+
@versions.each do |ver|
|
73
|
+
#raise "Date.parse: " + Date.parse(ver.created_at.to_s).to_s + " and date: " + date.to_s + " And equals: " + (Date.parse(ver.created_at.to_s) <= date).to_s + " and ver_id: " + ver.id.to_s
|
74
|
+
if Date.parse(ver.created_at.to_s) <= date
|
75
|
+
#raise ver.reify.ver_id.to_s
|
76
|
+
return ver.reify
|
77
|
+
end
|
78
|
+
end
|
79
|
+
return @versions.last.reify if @version.present?
|
80
|
+
return self
|
81
|
+
end
|
82
|
+
|
83
|
+
def version_at(date)
|
84
|
+
@versions ||= ::Hist::Version.get(obj: self)
|
85
|
+
@versions.each do |ver|
|
86
|
+
#raise "Date.parse: " + Date.parse(ver.created_at.to_s).to_s + " and date: " + date.to_s + " And equals: " + (Date.parse(ver.created_at.to_s) <= date).to_s + " and ver_id: " + ver.id.to_s
|
87
|
+
if Date.parse(ver.hist_created_at.to_s) <= date
|
88
|
+
#raise ver.reify.ver_id.to_s
|
89
|
+
return ver
|
90
|
+
end
|
91
|
+
end
|
92
|
+
return @versions.last if @version.present?
|
93
|
+
return self
|
94
|
+
end
|
95
|
+
|
96
|
+
def raw_versions(user: nil, extra: nil, only: 'kept')
|
97
|
+
@raw_versions ||= ::Hist::Version.raw_get(obj: self, user: nil, extra: nil, only: only)
|
98
|
+
@raw_versions
|
99
|
+
end
|
100
|
+
|
101
|
+
def versions(user: nil, extra: nil, only: 'kept')
|
102
|
+
@versions ||= ::Hist::Version.get(obj: self, user: nil, extra: nil, only: only)
|
103
|
+
@versions
|
104
|
+
end
|
105
|
+
|
106
|
+
def raw_pendings(user: nil, extra: nil, only: 'kept')
|
107
|
+
@raw_pendings ||= ::Hist::Pending.raw_get(obj: self, user: nil, extra: nil, only: only)
|
108
|
+
@raw_pendings
|
109
|
+
end
|
110
|
+
|
111
|
+
def pendings(user: nil, extra: nil, only: 'kept')
|
112
|
+
@pendings ||= ::Hist::Pending.get(obj: self, user: nil, extra: nil, only: only)
|
113
|
+
@pendings
|
114
|
+
end
|
115
|
+
|
116
|
+
def diff_hist(ver:nil, pending: nil, type: :json, exclude: [], include: [], associations: nil, only_diffs: false)
|
117
|
+
if ver.present?
|
118
|
+
return Hist::ApplicationRecord.diff(obj: self, ver: ver, type: type, exclude: exclude, include: include, associations: associations, only_diffs: only_diffs)
|
119
|
+
elsif pending.present?
|
120
|
+
return Hist::ApplicationRecord.diff(obj: self, pending: pending, type: type, exclude: exclude, include: include, associations: associations, only_diffs: only_diffs)
|
121
|
+
else
|
122
|
+
raise 'Error: either ver or pending parameter is required for diff_history'
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
def reload_hist
|
128
|
+
@versions = nil
|
129
|
+
@raw_versions = nil
|
130
|
+
@pendings = nil
|
131
|
+
@raw_pendings = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def reload
|
135
|
+
reload_hist
|
136
|
+
super
|
137
|
+
end
|
138
|
+
|
139
|
+
def hist_json(exclude: [], include: [], associations: nil)
|
140
|
+
Hist::ApplicationRecord.to_json(obj: self, exclude: exclude, include: include, associations: associations)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Attributes essentially... defined as methods as just don't want to save this data as part of the JSON hash
|
144
|
+
def ver_id
|
145
|
+
@ver_id
|
146
|
+
end
|
147
|
+
|
148
|
+
def ver_id=(value)
|
149
|
+
@ver_id = value
|
150
|
+
end
|
151
|
+
|
152
|
+
def pending_id
|
153
|
+
@pending_id
|
154
|
+
end
|
155
|
+
|
156
|
+
def pending_id=(value)
|
157
|
+
@pending_id = value
|
158
|
+
end
|
159
|
+
|
160
|
+
def hist_created_at=(value)
|
161
|
+
@hist_created_at = value
|
162
|
+
end
|
163
|
+
|
164
|
+
def hist_created_at
|
165
|
+
@hist_created_at
|
166
|
+
end
|
167
|
+
|
168
|
+
def hist_whodunnit
|
169
|
+
@hist_whodunnit
|
170
|
+
end
|
171
|
+
|
172
|
+
def hist_whodunnit=(value)
|
173
|
+
@hist_whodunnit_user_set = true
|
174
|
+
@hist_whodunnit = value
|
175
|
+
end
|
176
|
+
|
177
|
+
def system_hist_whodunnit=(value)
|
178
|
+
@hist_whodunnit_user_set = false
|
179
|
+
@hist_whodunnit = value
|
180
|
+
end
|
181
|
+
|
182
|
+
def hist_extra
|
183
|
+
@hist_extra
|
184
|
+
end
|
185
|
+
|
186
|
+
def hist_extra=(value)
|
187
|
+
@hist_whodunnit_extra_set = true
|
188
|
+
@hist_extra = value
|
189
|
+
end
|
190
|
+
|
191
|
+
def system_hist_extra=(value)
|
192
|
+
@hist_whodunnit_extra_set = false
|
193
|
+
@hist_extra = value
|
194
|
+
end
|
195
|
+
|
196
|
+
def record_hist_whodunnit?
|
197
|
+
@hist_whodunnit_user_set
|
198
|
+
end
|
199
|
+
|
200
|
+
def record_hist_extra?
|
201
|
+
@hist_whodunnit_extra_set
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
# Type normally isn't part of the JSON output... try to fix that here...
|
206
|
+
#def as_json(options={})
|
207
|
+
#if self.type.present?
|
208
|
+
#super(options.merge({:methods => :type}))
|
209
|
+
#else
|
210
|
+
#super
|
211
|
+
#end
|
212
|
+
#end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Hist
|
2
|
+
class Pending < Hist::ApplicationRecord
|
3
|
+
|
4
|
+
self.table_name = "hist_pendings"
|
5
|
+
|
6
|
+
def self.start_pending
|
7
|
+
ActiveRecord::Base.transaction do
|
8
|
+
yield
|
9
|
+
raise ActiveRecord::Rollback, "Don't save pending object changes"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.get_new_raw(klass:, user: nil, extra: nil, only: 'kept')
|
14
|
+
if user.nil?
|
15
|
+
if extra.nil?
|
16
|
+
versions = self.where(model: Hist.model(klass: klass), obj_id: nil).send(only).reverse
|
17
|
+
else
|
18
|
+
versions = self.where(model: Hist.model(klass: klass), obj_id: nil, extra: extra).send(only).reverse
|
19
|
+
end
|
20
|
+
|
21
|
+
else
|
22
|
+
if extra.nil?
|
23
|
+
# .to_s to support either user object or username
|
24
|
+
versions = self.where(model: Hist.model(klass: klass), obj_id: nil, user: user.to_s).send(only).reverse
|
25
|
+
else
|
26
|
+
# .to_s to support either user object or username
|
27
|
+
versions = self.where(model: Hist.model(klass: klass), obj_id: nil, user: user.to_s, extra: extra).send(only).reverse
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
versions
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.get_new(klass:, user: nil, extra: nil, only: 'kept')
|
35
|
+
hash_versions = self.get_new_raw(klass: klass, user: user, extra: extra, only: only)
|
36
|
+
versions = hash_versions.map {|v| v.reify }
|
37
|
+
versions
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.put(obj:, user: nil, extra: nil)
|
41
|
+
# Trim old pendings
|
42
|
+
# TODO: make this more efficient
|
43
|
+
if obj.class.base_class.hist_config.max_pendings >= 0
|
44
|
+
versions = self.class.raw_get(obj: obj, only: 'discarded')
|
45
|
+
if versions.size >= obj.class.base_class.hist_config.max_pendings
|
46
|
+
versions.last.destroy!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Hist
|
2
|
+
class Version < Hist::ApplicationRecord
|
3
|
+
|
4
|
+
self.table_name = "hist_versions"
|
5
|
+
|
6
|
+
def self.put(obj:, user: nil, extra: nil)
|
7
|
+
# Trim old versions
|
8
|
+
# TODO: make this more efficient
|
9
|
+
if obj.class.base_class.hist_config.max_versions >= 0
|
10
|
+
versions = self.raw_get(obj: obj)
|
11
|
+
if versions.size >= obj.class.base_class.hist_config.max_versions
|
12
|
+
versions.last.destroy!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<%= stylesheet_link_tag "hist/application", media: "all" %>
|
2
|
+
<%= javascript_include_tag "hist/application" %>
|
3
|
+
|
4
|
+
<div class="modal-dialog" id="hist_modal_container" tabindex="-1" aria-labelledby="modalLabel-hist_modal_container" aria-hidden="true">
|
5
|
+
<div class="modal-content">
|
6
|
+
<div class="modal-header">
|
7
|
+
<button type="button" class="close" data-dismiss="modal">
|
8
|
+
<span aria-hidden="true">×</span>
|
9
|
+
<span class="sr-only">Close</span>
|
10
|
+
</button>
|
11
|
+
<div class="hist-title-left">
|
12
|
+
<h4 class="modal-title"><%= @left_title %></h4>
|
13
|
+
</div>
|
14
|
+
<div class="hist-title-right">
|
15
|
+
<h4 class="modal-title"><%= @right_title %></h4>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
</div>
|
19
|
+
<div class="modal-body">
|
20
|
+
<div id="histAceDiff">
|
21
|
+
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
<div style="clear:both;"></div>
|
25
|
+
<div class="modal-footer">
|
26
|
+
<div class="pull-left"><button type="button" class="btn btn-link" data-dismiss="modal">Close</button></div>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
</div>
|