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,10 @@
1
+ require "singleton"
2
+
3
+ module Hist
4
+ class Config
5
+ include Singleton
6
+
7
+ attr_accessor :default_diff_exclude
8
+ end
9
+
10
+ end
@@ -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">&times;</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>