Module: Mock::ImportConcern::ClassMethods

Defined in:
app/models/concerns/mock/import_concern.rb

Overview

———- Class Methods ———-

Instance Method Summary collapse

Instance Method Details

#apply_content_to_model(contents_hash, model, root_model) ⇒ Object

Public: Applies the contents hash provided onto the model object provided. This includes core properties of the model, as well as related objects. For related objects, we effectively call into the import() method on the related class, building out our object graph with recursion.

contents_hash - the hash of attributes for a candidate instance of this model.
model         - the model instance we are setting values into from the contents hash.
root_model    - represents the root model for the model we're currently importing. We need this
                for when we make re-entrant calls into the import() method for related 
                models to our model instance.

Returns nothing.



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
322
323
324
325
326
327
328
329
# File 'app/models/concerns/mock/import_concern.rb', line 284

def apply_content_to_model(contents_hash, model, root_model)
  has_many_relationships_hash = has_many_relations_hash()
  has_one_relationships_hash = has_one_relations_hash()

  # Any contents in hash not in a pair will generate an exception. Defend against such malformed input
  # content with an exception handling block.
  begin
    contents_hash.each_pair do |key, value|
      #log "Processing key from contents hash: '#{key}'"

      next if key == 'id' # Defend against these, which really shouldn't be in the source input files anyways.

      if model.has_attribute? key
        #log "Processing attribute: #{key} | value: #{value}"
        model.send("#{key}=", value) # Superior to instance_variable_set b/c we're dealing with ActiveRecord attributes

      elsif has_one_relationships_hash.keys.include? key
        #log "The key: #{key} is one of our has_one relations"
        klazz = has_one_relationships_hash[key]
        if klazz.respond_to? :import
          #log "#{klazz.to_s} responds to import."
          related_has_one_entity = klazz.import(value, Enums::DuplicateStrategy::ALLOW, root_model)
          #log "++About to add '#{related_has_one_entity.class.name}' as a has one to '#{self.name}' via the key '#{key}'"
          mutator_key = "#{key}=".to_sym
          model.send(mutator_key, related_has_one_entity)
          #log "..Completed adding '#{related_has_one_entity.class.name}' as a has one to '#{self.name}' via the key '#{key}'"
        end

      elsif has_many_relationships_hash.keys.include? key
        #log "The key: #{key} is one of our has_many relations"
        klazz = has_many_relationships_hash[key]
        if klazz.respond_to? :import
          #log "#{klazz.to_s} responds to import."
          value.each do |related_object_data|
            model.send(key) << klazz.import(related_object_data, Enums::DuplicateStrategy::ALLOW, root_model)
          end
        else
          log "#{klazz.to_s} does NOT respond to import."
        end
      end
    end
  rescue Exception => exception
    log_error "ERROR: Ran into an exception importing model #{self.to_s}. #{exception.class.to_s}: #{exception}."
    root_model.import_errors << "#{exception.class.to_s}: #{exception.message}"
  end
end

#apply_duplicate_handling_strategy(contents_hash, duplicate_strategy, root_model) ⇒ Object

Public: Determines whether we should allow, replace or skip duplicates. We’ll first search for a duplicate and if one exists, we’ll then apply the duplicate strategy provided in determining how to deal with the duplicate. As we go, we’ll add entries to the root model’s import_notices array if warranted. For imports that should be skipped, we set the skip_import flag on the root model indicating that it should not be saved.

contents_hash       - the hash of attributes for a candidate instance of this model.
duplicate_strategy  - a constant from the Enums::DuplicateStrategy module. This indicates whether duplicates
                      should be allowed, skipped or replace that which they duplicate. This is only meaningful 
                      when importing a root model.
root_model          - if not specified, we'll eventually set it by walking up the parent resource chain 
                      on the model we do instantiate.

Returns nothing.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'app/models/concerns/mock/import_concern.rb', line 248

def apply_duplicate_handling_strategy(contents_hash, duplicate_strategy, root_model)
  if has_existing_instance?(contents_hash)
    case duplicate_strategy
      when Enums::DuplicateStrategy::ALLOW
        # Do nothing; allow it if there happened to be a duplicate.
        # Log this in import_notices
        root_model.import_notices << "Imported as an allowed duplicate."
      when Enums::DuplicateStrategy::REPLACE
        # Remove existing entries
        num_dupes_removed = remove_duplicates(contents_hash)
        # Log this in import_notices
        root_model.import_notices << "Imported, but had to delete #{num_dupes_removed} duplicate(s) prior."
      when Enums::DuplicateStrategy::SKIP
        # Log this in import_notices
        root_model.import_notices << "Skipped importing this, as it was a duplicate."
        # Break out of this import; nothing more for us to do. We'll set a return flag to indicate this.
        root_model.skip_import = true
      else
        # Do nothing. Assume our caller has already checked for valid duplicate_strategy values.
    end # case
  end
end

#has_existing_instance?(contents_hash) ⇒ Boolean

Public: Takes a Hash of attributes for this model, and does a cross check if there exists an instance of this model already with the same values as those passed in. In doing our check, we <strong>only</strong> consider those attributes present in the array returned from a call to uniqueness_attributes().

contents_hash   - The hash of attributes for a candidate instance of this model.

Returns Boolean whether or not a model instance exists with the provided contents hash.

Returns:

  • (Boolean)


122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'app/models/concerns/mock/import_concern.rb', line 122

def has_existing_instance?(contents_hash)
  conditions_hash = uniqueness_conditions(contents_hash)
  #log "#{self.to_s}: Looking for existing instance using contents_hash: #{contents_hash}"
  #log "#{self.to_s}: Looking for existing instance with attributes: #{conditions_hash}"

  # Do we have an empty set of conditions?
  if conditions_hash.empty?
    # YES: Return a no match result. This shouldn't happen, but if we allow this answer,
    # every record will be matched with the no-constraint conditions, and then possibly removed.
    log_warn "Interchange::ImportConcern has_existing_instance(): Ran into situation where conditions_hash" \
        "computed was empty. This is dangerous; a wildcard match like this could cause us to delete all" \
        "existing records as duplicates. Ensure uniqueness_attributes array on root models are set correctly."
    false
  else
    # NO: We have some conditions that we can filter on, so we'll do a search based on these
    # conditions and let the existence thereof be our verdict.
    self.where(conditions_hash).exists?
  end
end

#has_many_relations_hashObject

Public: Creates a hash that maps relationship name to model class constant, for our has_many relations.

Returns Hash of the mixing in model’s ActiveRecord has_many relations with relation names as keys

and destination classes as values.


102
103
104
105
106
107
108
109
110
111
# File 'app/models/concerns/mock/import_concern.rb', line 102

def has_many_relations_hash
  relations_reflection = self.reflect_on_all_associations(:has_many)
  relations_hash = {}
  relations_reflection.each do |relation|
    relations_hash[relation.name.to_s] = relation.class_name.constantize
  end

  #log "has_many_relations_hash: #{relations_hash}"
  relations_hash
end

#has_one_relations_hashObject

Public: Creates a hash that maps relationship name to model class constant, for our has_one relations.

Returns Hash of the mixing in model’s ActiveRecord has_one relations with relation names as keys

and destination classes as values.


86
87
88
89
90
91
92
93
94
95
# File 'app/models/concerns/mock/import_concern.rb', line 86

def has_one_relations_hash
  relations_reflection = self.reflect_on_all_associations(:has_one)
  relations_hash = {}
  relations_reflection.each do |relation|
    relations_hash[relation.name.to_s] = relation.class_name.constantize
  end

  #log "has_one_relations_hash: #{relations_hash}"
  relations_hash
end

#import(contents, duplicate_strategy, root_model = nil) ⇒ Object

Public: Performs an import operation on the mixing in model, using the model contents provided.

Note: when we ourselves call import() on related models, we effectively ignore the duplicate_strategy, as those related models will by definition, always be non-root models. We only care about duplicates when it comes to models that define top level collections (i.e. root models).

contents            - the contents of a new model to be imported. This may be a proper Ruby Hash.
duplicate_strategy  - a constant from the Enums::DuplicateStrategy module. This indicates whether duplicates
                      should be allowed, skipped or replace that which they duplicate. 
                      This is only meaningful when importing a root model.
root_model          - if not specified, we'll eventually set it by walking up the parent resource chain 
                      on the model we do instantiate (default: parent resource).

Returns ActiveRecord::Base the model instance we just built.



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/models/concerns/mock/import_concern.rb', line 200

def import(contents, duplicate_strategy, root_model=nil)
  #log "#{self.to_s}: Asked to import data."

  # Convent the contents passed in to a Hash if it is currently a String of (presumably) JSON.
  #if contents.is_a? String
  #  log 'We were passed a String of contents'
  #  contents_hash = JSON.parse(contents)
  #else
  #  log 'We were passed a Hash of contents'
  #  contents_hash = contents
  #end

  contents_hash = contents
  model = self.new

  unless root_model
    if model.is_root_resource?
      root_model = model
      root_model.import_errors = [] # Initialize the list of errors
      root_model.import_notices = [] # Initialize the list of notices

      # Determine if we have duplicates, and if so, handle them as directed by duplicate_strategy:
      apply_duplicate_handling_strategy(contents_hash, duplicate_strategy, root_model)
    end
  end

  # Apply values from the contents hash onto the model:
  apply_content_to_model(contents_hash, model, root_model)

  # Return the new model just built; callers are responsible for saving, when appropriate.
  model
end

#remove_duplicates(contents_hash) ⇒ Object

Internal: Finds existing instances of this model that match the uniqueness attributes defined for this model, and destroys them. Presumably, this is to make way for a newly imported instance that would then be a dupe.

contents_hash - the hash of attributes for a candidate instance of this model.

Returns Fixnum the number of instances we found (and presumably destroyed).



175
176
177
178
179
180
181
182
183
# File 'app/models/concerns/mock/import_concern.rb', line 175

def remove_duplicates(contents_hash)
  existing_instances = retrieve_existing_instances(contents_hash)
  existing_instances.each do | existing_instance |
      log "#{self.to_s}: remove_duplicates(): Destroying instance #{existing_instance.id}"
      existing_instance.destroy
  end

  existing_instances.count
end

#retrieve_any_existing_instance(contents_hash) ⇒ Object

Internal: Returns the first matching existing instance of this model found, otherwise nil.

contents_hash - The hash of attributes for a candidate instance of this model.

Returns ActiveRecord::Base an instance of this model that matched a subset of the values in the contents hash



161
162
163
164
165
166
# File 'app/models/concerns/mock/import_concern.rb', line 161

def retrieve_any_existing_instance(contents_hash)
  existing = retrieve_existing_instances(contents_hash)
  unless existing.empty?
    existing.first
  end
end

#retrieve_existing_instances(contents_hash) ⇒ Object

Internal: Retrieves all instances of this model that match a subset of the values in the contents hash; specifically, the uniqueness values.

contents_hash   - The hash of attributes for a candidate instance of this model.

Return Array of all instances of this model that matched a subset of the values in the contents hash, as

defined by the uniqueness_attributes


150
151
152
153
# File 'app/models/concerns/mock/import_concern.rb', line 150

def retrieve_existing_instances(contents_hash)
  conditions_hash = uniqueness_conditions(contents_hash)
  self.where(conditions_hash)
end

#uniqueness_attributesObject

Public: Provides an array of those attributes whose values together, determine non-synthetic uniqueness. We define an empty array here. Classes that use this module mixin should override with there own list of keys as symbols. Never include the :id attribute, because that is synthetic.

Returns Array of symbols representing attributes that together, are a business compound primary key.



51
52
53
# File 'app/models/concerns/mock/import_concern.rb', line 51

def uniqueness_attributes
  []
end

#uniqueness_conditions(contents_hash) ⇒ Object

Public: Takes a hash of attributes and pulls out just those that match our uniqueness attributes. The resultant hash can be used as conditions in a <emphasis>where</emphasis> clause to query with.

contents_hash - The hash of attributes for a candidate instance of this model.

Returns Hash containing properties as key-value pairs, which uniquely identify an instance of the mixed

in model.


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'app/models/concerns/mock/import_concern.rb', line 63

def uniqueness_conditions(contents_hash)
  conditions_hash = {}
  log_info "Mock::ImportConcern: uniqueness_conditions(): uniqueness_attributes to work with is: #{uniqueness_attributes}"
  uniqueness_attributes.each do | unique_attribute_key |
    #log "Mock::ImportConcern: uniqueness_conditions(): Searching for key: #{unique_attribute_key}"
    # The contents hash we'll be looking through is JSON, with string keys instead of symbol keys,
    # and from testing, apparently that matters! Hence, we do a 'to_s' on each key before looking
    # through the contents_hash for it:
    search_value = contents_hash[unique_attribute_key.to_s]
    if search_value
      #log "Mock::ImportConcern: uniqueness_conditions(): Found value #{search_value} for key: #{unique_attribute_key}"
      conditions_hash[unique_attribute_key] = search_value
    end
  end

  conditions_hash
end