Module: ApplicationEnumeration

Defined in:
lib/application_enumeration.rb

Overview

Methods for enumerating models, tables, columns etc.

!! If you think that a method belongs here chances are it already exists in a Rails extension.

Note the use of Module.nesting (http://urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/)

Constant Summary collapse

[
  :versions,
  :dwc_occurrence,
  :pinboard_items,
  :cached_map_register,
  :cached_map_items,
  :cached_maps
].freeze

Class Method Summary collapse

Class Method Details

.all_submodels(klass) ⇒ Array of Classes

!! See the built in self.descendants for actual inheritance tracking, this is path based. Used in Ranks.

Parameters:

  • klass (Object)

Returns:

  • (Array of Classes) —

    all models in the /app/models/#klassklass.name (not necessarily inheriting)



82
83
84
# File 'lib/application_enumeration.rb', line 82

def self.all_submodels(klass)
  Dir.glob(Rails.root + "app/models/#{klass.name.underscore}/**/*.rb").collect{|a| self.model_from_file_name(a) }
end

.alternate_value_attributes(object) ⇒ Array

Returns a list symbols that represent populated, non "cached", non "_id", non reserved attributes.

Returns:

  • (Array) —

    a list symbols that represent populated, non "cached", non "_id", non reserved attributes



25
26
27
28
29
30
31
# File 'lib/application_enumeration.rb', line 25

def self.alternate_value_attributes(object)
  if object.class::ALTERNATE_VALUES_FOR.blank?
    raise("#{object.class} attempted to annotate a class without ALTERNATE_VALUES_FOR -  please inform the programmers")
  else
    object.attributes.select{|k,v| v.present? && object.class::ALTERNATE_VALUES_FOR.include?(k.to_sym)}.keys.map(&:to_sym)
  end
end

.annotatable_attributes(object) ⇒ Array of Symbols

!! Some models have blacklists (e.g. Serial)

Parameters:

  • object (Object)

Returns:

  • (Array of Symbols) —

    a whitelist of the attributes of a given instance that may be annotated



37
38
39
# File 'lib/application_enumeration.rb', line 37

def self.annotatable_attributes(object)
  object.attributes.select{|k,v| v.present? && !(k =~ /.*_id\z|cached_*.*/)}.keys.map(&:to_sym) - ( RESERVED_ATTRIBUTES - [:parent_id])
end

.attributes(target) ⇒ Array of Symbol

Returns a list attributes except "id", 'md5_", and postfixed "_id", "_at" This is an arbitrary convention, wrap this to further refine.

Parameters:

Returns:

  • (Array of Symbol) —

    a list attributes except "id", 'md5_", and postfixed "_id", "_at" This is an arbitrary convention, wrap this to further refine.



19
20
21
# File 'lib/application_enumeration.rb', line 19

def self.attributes(target)
   target.attributes.select{|k,v| !(k =~ /\Amd5_|_id\z|\Aid\z|_at\z/)}.symbolize_keys.keys.sort
end

.citable_relations(klass, relationship_type = :all) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/application_enumeration.rb', line 168

def self.citable_relations(klass, relationship_type = :all)
  types = relationship_type == :all ?
    [:has_many, :has_one, :belongs_to] : [relationship_type]

  h = {}

  types.each do |t|
    h[t] = klass.reflect_on_all_associations(t).filter_map do |r|
      if r.klass.ancestors.include?(Shared::Citations)
        if t == :has_many
          r.plural_name.to_sym
        else
          r.name.to_sym
        end
      else
        nil
      end
    end
  end

  h
end

.community_data_classes ⇒ Array

Returns all superclass models that are community/shared.

Returns:

  • (Array) —

    all superclass models that are community/shared



61
62
63
# File 'lib/application_enumeration.rb', line 61

def self.community_data_classes
  superclass_models.select{|a| a < Shared::SharedAcrossProjects }
end

.community_models ⇒ Array

Returns all superclass data models.

Returns:

  • (Array) —

    all superclass data models



73
74
75
# File 'lib/application_enumeration.rb', line 73

def self.community_models
  superclass_models.select{|a| a < Shared::SharedAcrossProjects}
end

.data_models ⇒ Array

Returns all superclass data models.

Returns:

  • (Array) —

    all superclass data models



67
68
69
# File 'lib/application_enumeration.rb', line 67

def self.data_models
  superclass_models.select{|a| a < Shared::IsData}
end

.filter_sti_relations(klass, relation_names) ⇒ Hash

Filter out STI subclass relations that are redundant with their parent class (e.g., protonym belongs_to duplicates taxon_name belongs_to). A subclass relation is only dropped when the parent association has no scope

  • an unscoped parent always covers all records the subclass relation would return.

!! There's no general way to compare scoped STI relations, so this method does not filter out STI relations when the parent relation is scoped. !! TODO: maybe we return a separate list of those if any occur.

Parameters:

  • klass (Class) —

    an ActiveRecord model class

  • relation_names (Array<Symbol>)

Returns:

  • (Hash) —

    { relation_name => reflection }



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/application_enumeration.rb', line 319

def self.filter_sti_relations(klass, relation_names)
  reflections =
    relation_names.map { |name| [name, klass.reflect_on_association(name)] }.to_h

  filtered_names = relation_names.reject do |relation_name|
    reflection = reflections[relation_name]
    next false if reflection.nil? || reflection.options[:polymorphic]

    # Drop this relation if its class is a subclass of an unscoped parent relation,
    # or if it has the same class but is scoped while the other is not.
    reflections.any? do |other_name, other_reflection|
      next false if relation_name == other_name
      next false if other_reflection.nil? || other_reflection.options[:polymorphic]
      next false unless other_reflection.scope.nil?
      reflection.klass < other_reflection.klass ||
        (reflection.klass == other_reflection.klass && reflection.scope.present?)
    end
  end

  reflections.slice(*filtered_names)
end

.klass_reflections(klass, relationship_type = :all) ⇒ Object

!! Does NOT return associations on subtypes of klass - see sti_reflections for that.

Returns:

  • Array of AR associations



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/application_enumeration.rb', line 105

def self.klass_reflections(klass, relationship_type = :all)
  relationship_types = relationship_type == :all ?
    [:belongs_to, :has_one, :has_many] : [relationship_type]

  a = []
  relationship_types.each do |t|
    a += klass.reflect_on_all_associations(t).sort{ |a, b|
      a.name <=> b.name
    }
  end

  a
end

.model_from_file_name(file_name) ⇒ Class

e.g. given 'app/models/specimen.rb' the Specimen class is returned

Parameters:

  • file_name (String)

Returns:

  • (Class) —

    represented by a path included filename from /app/models.



90
91
92
# File 'lib/application_enumeration.rb', line 90

def self.model_from_file_name(file_name)
  file_name.split(/app\/models\//).last[0..-4].split(/\\/).collect{|b| b.camelize}.join('::').safe_constantize
end

.nested_subclasses(parent = self) ⇒ Hash

Parameters:

  • parent (Object) (defaults to: self)

Returns:

  • (Hash)


96
97
98
99
100
# File 'lib/application_enumeration.rb', line 96

def self.nested_subclasses(parent = self)
  parent.subclasses.inject({}) { | hsh, subclass |
    hsh.merge!(subclass.name => nested_subclasses(subclass))
  }
end

Returns true if object has no data in any has_many or has_one associations. Excludes through associations and the relations named in EXCLUDE_RELATIONS_FOR_RELATED_DATA (+ ignore:).

Parameters:

  • object (ApplicationRecord)
  • ignore (Array<Symbol>) (defaults to: []) —

    additional relation names to ignore

Returns:

  • (Boolean) —

    true if object has no data in any has_many or has_one associations. Excludes through associations and the relations named in EXCLUDE_RELATIONS_FOR_RELATED_DATA (+ ignore:).



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/application_enumeration.rb', line 217

def self.no_related_data?(object, ignore: [])
  excluded = EXCLUDE_RELATIONS_FOR_RELATED_DATA + ignore

  (klass_reflections(object.class, :has_many) + klass_reflections(object.class, :has_one)).each do |relation|
    next if relation.options[:through].present?
    next if excluded.include?(relation.name)

    related = object.send(relation.name)

    has_data = if relation.collection?
                 related.any?
               else
                 related.present?
               end

    return false if has_data
  end

  true
end

.non_project_data_classes ⇒ Array of Classes

Returns data models that do not have a project_id attribute.

Returns:

  • (Array of Classes) —

    data models that do not have a project_id attribute



55
56
57
# File 'lib/application_enumeration.rb', line 55

def self.non_project_data_classes
  data_models - project_data_classes
end

.project_data_classes ⇒ Array of Classes

Returns all models with a project_id attribute.

Returns:

  • (Array of Classes) —

    all models with a project_id attribute



49
50
51
# File 'lib/application_enumeration.rb', line 49

def self.project_data_classes
  superclass_models.select{|a| a.column_names.include?('project_id') }
end

Bulk equivalent of .no_related_data? — checks whether any record in a set (not a single instance) has related data, using one COUNT query per relation rather than one check per record. Intended for auditing before a bulk destructive operation (e.g. delete_all) that will not run Rails callbacks/dependent: options, where a per-record no_related_data? loop would not scale to thousands+ of records.

!! STI caveat: only checks reflections declared on klass itself, the same as .reflect_on_all_associations. A subclass-only association (e.g. Protonym#type_materials, not declared on the base TaxonName class) is invisible if klass is the base class. Callers working with an STI model should determine which concrete type(s) are actually present in ids and call this once per type, merging results — this method does not guess at descendants itself. .sti_reflections lists the subclass-only associations.

Parameters:

  • klass (Class) —

    an ActiveRecord model class (e.g. TaxonName, Otu)

  • ids (ActiveRecord::Relation, Array<Integer>) —

    the id set to check. A Relation is strongly preferred for large sets — it's used as a subquery, so it never embeds a literal id list per relation query — while a plain Array is fine for small or already- materialized sets.

  • ignore (Array<Symbol>) (defaults to: []) —

    additional relation names not to check — e.g. because the caller cleans them up explicitly elsewhere

Returns:

  • (Hash{Symbol => Integer}) —

    relation name => row count, for every non-excluded has_many/has_one relation (excluding through) with at least one row referencing ids. Empty hash means clean.

    !! The counts answer "is there related data, and roughly how much" — they are not exact and must not be summed for a per-record total: the same underlying row is reported under every relation name that points at it (e.g. citations / origin_citation / subsequent_citations on Otu). See the scope note in the body for the one case where a count can also be too high in absolute terms.



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
# File 'lib/application_enumeration.rb', line 272

def self.related_data_counts(klass, ids, ignore: [])
  excluded = EXCLUDE_RELATIONS_FOR_RELATED_DATA + ignore

  counts = {}

  (klass_reflections(klass, :has_many) + klass_reflections(klass, :has_one)).each do |relation|
    next if relation.options[:through].present?
    next if excluded.include?(relation.name)

    child_klass = relation.klass
    foreign_key = relation.foreign_key
    type_column = relation.type # polymorphic type column name, or nil

    scope = type_column ?
      child_klass.where(type_column => klass.base_class.name, foreign_key => ids) :
      child_klass.where(foreign_key => ids)

    # Apply the association's own scope lambda (e.g. `-> { where(position: 1) }`
    # on Otu#current_taxon_determinations) so a fully-scoped relation reports
    # its scoped count rather than every row sharing the foreign key.
    # Owner-dependent scopes (`->(owner) { ... }`, arity > 0) can't be
    # evaluated against a *set* of ids, so they are left unapplied — that only
    # ever inflates a count, it can't hide a non-empty relation.
    if relation.scope && relation.scope.arity.zero?
      scope = scope.instance_exec(&relation.scope)
    end

    count = scope.count
    counts[relation.name] = count if count > 0
  end

  counts
end

.relation_targets_community?(relation) ⇒ Boolean

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/application_enumeration.rb', line 140

def self.relation_targets_community?(relation)
  case relationship_type(relation)
  when :has_many
    relation.class_name.safe_constantize.is_community?
  when :has_one
    raise TaxonWorks::Error, "Has one support not implemented in unify, throw eggs at the devs."
  when :belongs_to
    if k = relation.options[:class_name]
      k.safe_constantize.is_community?
    else
      raise TaxonWorks::Error, "Missing attribute class_name on #{relation.name}."
    end
  end
end

.relationship_type(relation) ⇒ Object

collection?, has_mone? belongs_to?

Raises:

  • (TaxonWorks::Error)


156
157
158
159
160
161
162
163
164
165
166
# File 'lib/application_enumeration.rb', line 156

def self.relationship_type(relation)
  if relation.collection? # class.name.match('HasMany')
    return :has_many
  elsif relation.has_one? # class.name.match('HasOne')
    return :has_one
  elsif relation.belongs_to? # class.name.match('BelongsTo')
    return :belongs_to
  end

  raise TaxonWorks::Error, "Unknown relationship type for #{relation.name}."
end

.sti_reflections(klass, relationship_type = :all) ⇒ Object

.klass_reflections for an STI base class and all its descendants. Reflections only see associations declared on the class itself (or inherited), so a base class misses subclass-only associations, e.g. ObservationMatrixColumnItem misses ObservationMatrixColumnItem::Single::Descriptor#descriptor. Use this when operating on a whole table rather than a single instance.

!! Relies on descendants being loaded, i.e. call Rails.application.eager_load! first in rake tasks.

Returns:

  • Array of AR associations unique by macro, name, foreign key and target class, base class reflections first



132
133
134
135
136
137
138
# File 'lib/application_enumeration.rb', line 132

def self.sti_reflections(klass, relationship_type = :all)
  ([klass] + klass.descendants)
    .flat_map { |k| klass_reflections(k, relationship_type) }
    .uniq { |r|
      [r.macro, r.name, r.foreign_key.to_s, r.polymorphic? ? nil : r.class_name]
    }
end

.superclass_models ⇒ Array

Returns all models that inherit directly from ApplicationRecord.

Returns:

  • (Array) —

    all models that inherit directly from ApplicationRecord



43
44
45
# File 'lib/application_enumeration.rb', line 43

def self.superclass_models
  ApplicationRecord.descendants.select{|a| a.superclass == ApplicationRecord }
end