Class: Hist::ApplicationRecord

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Discard::Model
Defined in:
app/models/hist/application_record.rb

Direct Known Subclasses

Pending, Version

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.decode(obj:, associations: nil) ⇒ Object



81
82
83
84
85
86
87
88
89
# File 'app/models/hist/application_record.rb', line 81

def self.decode(obj:, associations: nil)
  if obj.class == Hash
    decoded = ActiveSupport::JSON.decode(obj: obj, associations: associations)
  else
    decoded = ActiveSupport::JSON.decode(encode(obj: obj, associations: associations))
  end

  decoded
end

.encode(obj:, associations: nil) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'app/models/hist/application_record.rb', line 35

def self.encode(obj:, associations: nil)
  if associations.nil?
    associations = Hist.model(obj:obj).constantize.hist_config.associations(obj: obj).map(&:name)
  else
    associations.each do |assoc|
      unless Hist.model(obj:obj).constantize.hist_config.valid_association(klass: obj.class, assoc: assoc)
        associations.delete(assoc)
      end
    end
  end


  if associations.nil?
    if obj.class.attribute_names.include?("type")
      encoded = ActiveSupport::JSON.encode obj, methods: :type
    else
      encoded = ActiveSupport::JSON.encode obj
    end
  else
    # Include type in the associations to support STI
    fixed_associations = []

    associations.each do |assoc|
      h = {}
      h[assoc] = {}
      # FIXME: This only works if the type file isn't custom.
      unless obj.send(assoc).nil?
        if obj.send(assoc).respond_to?("klass") && obj.send(assoc).klass.attribute_names.include?("type")
          h[assoc] = {methods: :type}
        elsif obj.send(assoc).class.respond_to?("attribute_names") && obj.send(assoc).class.attribute_names.include?("type")
          h[assoc] = {methods: :type}
        end
      end

      fixed_associations << h
    end
    if obj.class.attribute_names.include?("type")
      encoded = ActiveSupport::JSON.encode obj, include: fixed_associations, methods: :type
    else
      encoded = ActiveSupport::JSON.encode obj, include: fixed_associations
    end
  end

  encoded
end

.fix_save_associations(obj:) ⇒ Object

Many-To-Many ID changes. This causes a problem with historic many-to-many saving.



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'app/models/hist/application_record.rb', line 337

def self.fix_save_associations(obj:)

  associations = Hist.model(obj: obj).constantize.hist_config.associations(klass: obj.class.base_class, exclude_through: true).map(&:name)
  association_details = Hist.model(obj: obj).constantize.hist_config.all_associations(klass: obj.class.base_class, exclude_through: true)

  current_obj = obj.class.find(obj.id)

  # For has_many
  associations.each do |k,v|
    detail = association_details.select { |a| a.name == k}[0]
    existing = current_obj.send(k)
    version_set = obj.send(k)

    unless detail.class == ActiveRecord::Reflection::BelongsToReflection || detail.class == ActiveRecord::Reflection::HasOneReflection
      existing.each do |ex|
        unless version_set.pluck(:id).include? ex.id
          current_obj.send(k).delete(ex)
        end
      end

      if Hist.model(obj: obj).constantize.hist_config.update_associations_on_save(klass: obj.class, assoc: k)
        version_set.each do |ex|
          ex.save!

          unless existing.pluck(:id).include? ex.id
            current_obj.send(k) << ex
          end
        end
      else
        version_set.each do |ex|
          unless existing.pluck(:id).include? ex.id
            ex_obj = ex.class.find(ex.id)
            if ex_obj.present?
              current_obj.send(k) << ex_obj
            else
              ex.save!
              current_obj.send(k) << ex
            end

          end
        end
      end

    end
  end

  current_obj.reload
  current_obj

end

.get(obj:, user: nil, extra: nil, only: 'kept') ⇒ Object



29
30
31
32
33
# File 'app/models/hist/application_record.rb', line 29

def self.get(obj:, user: nil, extra: nil, only: 'kept')
  hash_versions = self.raw_get(obj: obj, user: user, extra: extra, only: only)
  versions = hash_versions.map {|v| v.reify }
  versions
end

.include_keys(h: {}, vals: [], associations: []) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'app/models/hist/application_record.rb', line 179

def self.include_keys(h: {}, vals: [], associations: [])
  return_h = {}
  h.each_key do |k|
    if vals.include?(k.to_s)
      return_h[k] = h[k]
    elsif associations.include? k.to_s
      return_h[k] = []
      h[k].each_with_index do |_, idx|
        return_h[k][idx] = {}
        h[k][idx].each_key do |k2|
          if vals.include? k2.to_s
            return_h[k][idx][k2] = h[k][idx][k2]
          end
        end
      end
    end
  end
  return_h
end

.only_hash_diffs(h1: {}, h2: {}) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/models/hist/application_record.rb', line 165

def self.only_hash_diffs(h1: {}, h2: {})
  return_h1 = {}
  return_h2 = {}

  h1.each_key do |k|
    if h1[k] != h2[k]
      return_h1[k] = h1[k]
      return_h2[k] = h2[k]
    end
  end

  {h1: return_h1, h2: return_h2}
end

.put(obj:, user: nil, extra: nil, exclude: []) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'app/models/hist/application_record.rb', line 91

def self.put(obj:, user: nil, extra: nil, exclude: [])
  encoded = encode(obj: obj)

  # Remove excluded fields... might be a better way to do this.
  decoded = ActiveSupport::JSON.decode encoded
  exclude.each do |attr|
    decoded.delete(attr)
  end

  encoded = ActiveSupport::JSON.encode decoded

  # Check to see if the last version is already saved... don't duplicate
  # Potential flaw with version caching to watch out for
  if obj.raw_versions.present?
    return obj if encoded == obj.raw_versions.first.data
  end

  if user.nil?
    if extra.nil?
      return self.create(model: Hist.model(obj: obj), obj_id: obj.id, data: encoded)
    else
      return self.create(model: Hist.model(obj: obj), obj_id: obj.id, extra: extra.to_s, data: encoded)
    end

  else
    if extra.nil?
      # .to_s to support either user object or username
      return self.create(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s, data: encoded)
    else
      # .to_s to support either user object or username
      return self.create(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s, extra: extra.to_s, data: encoded)
    end

  end
end

.raw_get(obj:, user: nil, extra: nil, only: 'kept') ⇒ Object

This could be done better…



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'app/models/hist/application_record.rb', line 8

def self.raw_get(obj:, user: nil, extra: nil, only: 'kept')
  if user.nil?
    if extra.nil?
      versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id).send(only).reverse
    else
      versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id, extra: extra).send(only).reverse
    end

  else
    if extra.nil?
      # .to_s to support either user object or username
      versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s).send(only).reverse
    else
      # .to_s to support either user object or username
      versions = self.where(model: Hist.model(obj: obj), obj_id: obj.id, whodunnit: user.to_s, extra: extra).send(only).reverse
    end
  end

  versions
end

.remove_key(h: {}, val: '') ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'app/models/hist/application_record.rb', line 199

def self.remove_key(h: {}, val: '')
  #h = passed_h.clone
  h.except! val
  h.each_key do |k|
    if h[k].class == Array
      h[k].each_with_index { |_, idx|
        if h[k][idx].class == Hash
          h[k][idx].except! val
        end
      }
    elsif h[k].class == Hash
      h[k].except! val
    end
  end
end

.to_json(obj:, exclude: [], include: [], associations: nil) ⇒ Object

Need to add exclude



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/models/hist/application_record.rb', line 128

def self.to_json(obj:, exclude: [], include: [], associations: nil)
  if associations.nil?
    associations = Hist.model(obj:obj).constantize.hist_config.associations(obj: obj, exclude_through: true).map(&:name)
  else
    associations.each do |assoc|
      unless Hist.model(obj:obj).constantize.hist_config.valid_association(klass: obj.class.base_class, assoc: assoc)
        associations.delete(assoc)
      end
    end
  end
  assoc_to_s = associations.map { |val| val.to_s }

  obj_hash = decode(obj: obj, associations: associations)

  if exclude.present?
    exclude.each do |e|
      obj_hash = remove_key(h: obj_hash, val: e.to_s)
    end
  end

  if include.present?
    include.map! { |val| val.to_s }
    obj_hash = include_keys(h: obj_hash, vals: include, associations: assoc_to_s)
  end

  # Only include associations we have configured
  #Hist.model(obj:obj).constantize.hist_config.all_associations(klass: obj.class).each do |assoc|
    #obj_hash.delete(assoc.to_s) unless associations.include?(assoc) || associations.include?(assoc.to_s)
  #end

  obj_hash
end

.to_yaml(obj:, exclude: [], include: [], associations: nil) ⇒ Object



161
162
163
# File 'app/models/hist/application_record.rb', line 161

def self.to_yaml(obj:, exclude: [], include: [], associations: nil)
  YAML.dump(self.to_json(obj: obj, exclude: exclude, include: include, associations: associations))
end

Instance Method Details

#reifyObject



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'app/models/hist/application_record.rb', line 215

def reify
  #associations = self.model.constantize.reflect_on_all_associations(:has_many).map(&:name)

  decoded = ActiveSupport::JSON.decode self.data
  decoded.stringify_keys!

  # Potential issue when changing STI class when removing associations... how to get all_associations for all STI?
  if decoded["type"].present?
    associations = self.model.constantize.hist_config.associations(klass: decoded["type"].constantize).map(&:name)
    all_associations = self.model.constantize.hist_config.all_associations(klass: decoded["type"].constantize).map(&:name)
  else
    associations = self.model.constantize.hist_config.associations(klass: self.model.constantize).map(&:name)
    all_associations = self.model.constantize.hist_config.all_associations(klass: self.model.constantize).map(&:name)
  end

  associations_to_process = {}

  # Can't instantiate with the association params... need to process those once the object is up
  all_associations.each do |assoc|
    if decoded.has_key? assoc.to_s
      if associations.include? assoc
        associations_to_process.merge!(decoded.slice(assoc.to_s))
      end

      decoded.delete(assoc.to_s)
    end
  end

  #obj = self.model.constantize.new(decoded)
  if decoded["id"].present? && self.model.constantize.exists?(id: decoded["id"])
    obj = self.model.constantize.find(decoded["id"])
    # 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
    # So must verify each key and drop it otherwise... in the future, update the version to drop that key possibly?
    decoded.each do |k, _|
      setter = :"#{k}="
      unless obj.respond_to?(setter)
        decoded.delete(k)
      end
    end

    obj.assign_attributes(decoded)
  else
    obj = self.model.constantize.new(decoded)
  end

  associations_to_process.each do |k,v|
    assoc_collection = []
    # Has Many
    if v.class == Array
      v.each do |d|
        if d["id"].present? && obj.class.reflect_on_association(k).class_name.constantize.exists?(id: d["id"])
          a = obj.class.reflect_on_association(k).class_name.constantize.find(d["id"])
          # 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
          # So must verify each key and drop it otherwise... in the future, update the version to drop that key possibly?
          d.each do |k2, _|
            setter = :"#{k2}="
            unless a.respond_to?(setter)
              d.delete(k2)
            end
          end

          a.assign_attributes(d)
          assoc_collection << a
        else
          assoc_collection << obj.class.reflect_on_association(k).class_name.constantize.new(d)
        end
      end
      obj.send(k).proxy_association.target = assoc_collection
      # Belongs To
    else
      if v["id"].present? && obj.class.reflect_on_association(k).class_name.constantize.exists?(id: v["id"])
        a = obj.class.reflect_on_association(k).class_name.constantize.find(v["id"])
        # 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
        # So must verify each key and drop it otherwise... in the future, update the version to drop that key possibly?
        v.each do |k2, _|
          setter = :"#{k2}="
          unless a.respond_to?(setter)
            v.delete(k2)
          end
        end

        a.assign_attributes(v)
        assoc_collection = a
      else
        assoc_collection = obj.class.reflect_on_association(k).class_name.constantize.new(v)
      end

      without_persisting(assoc_collection) do
        obj.send("#{k}=".to_sym, assoc_collection)
      end

    end

  end

  if self.class == Hist::Version
    obj.ver_id = self.id
  elsif self.class == Hist::Pending
    obj.pending_id = self.id
  end

  obj.hist_whodunnit = self.whodunnit
  obj.hist_extra = self.extra
  obj.hist_created_at = self.created_at

  obj
end

#without_persisting(record) ⇒ Object

From: github.com/westonganger/paper_trail-association_tracking/blob/5ed8cfbfa48cc773cc8a694dabec5a962d9c6cfe/lib/paper_trail_association_tracking/reifiers/has_one.rb Temporarily suppress #save so we can reassociate with the reified master of a has_one relationship. Since ActiveRecord 5 the related object is saved when it is assigned to the association. ActiveRecord 5 also happens to be the first version that provides #suppress.



328
329
330
331
332
333
334
# File 'app/models/hist/application_record.rb', line 328

def without_persisting(record)
  if record.class.respond_to? :suppress
    record.class.suppress { yield }
  else
    yield
  end
end