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
- EXCLUDE_RELATIONS_FOR_RELATED_DATA =
Relations that never hold user data worth preserving before a destructive operation: PaperTrail history, denormalized caches, and annotation infrastructure that is always cascade-deleted anyway. Anything that can block a
destroy(dependent: :restrict_with_*or a NO ACTION/RESTRICT DB foreign key) must NOT be listed here - the point of these methods is to surface exactly that. Reverse cache-column lookups (e.g. TaxonName#historical_taxon_names) are also deliberately not listed: they have nodependent:and often no DB FK, so a bulk delete leaves stale pointers behind - a caller doing bulk deletes should have to acknowledge that explicitly viaignore:rather than have it hidden here. TODO: DRY with Unify::EXCLUDE_RELATIONS [ :versions, :dwc_occurrence, :pinboard_items, :cached_map_register, :cached_map_items, :cached_maps ].freeze
Class Method Summary collapse
-
.all_submodels(klass) ⇒ Array of Classes
!! See the built in self.descendants for actual inheritance tracking, this is path based.
-
.alternate_value_attributes(object) ⇒ Array
A list symbols that represent populated, non "cached", non "_id", non reserved attributes.
-
.annotatable_attributes(object) ⇒ Array of Symbols
!! Some models have blacklists (e.g. Serial).
-
.attributes(target) ⇒ Array of Symbol
A list attributes except "id", 'md5_", and postfixed "_id", "_at" This is an arbitrary convention, wrap this to further refine.
- .citable_relations(klass, relationship_type = :all) ⇒ Object
-
.community_data_classes ⇒ Array
All superclass models that are community/shared.
-
.community_models ⇒ Array
All superclass data models.
-
.data_models ⇒ Array
All superclass data models.
-
.filter_sti_relations(klass, relation_names) ⇒ Hash
Filter out STI subclass relations that are redundant with their parent class (e.g.,
protonymbelongs_to duplicatestaxon_namebelongs_to). -
.klass_reflections(klass, relationship_type = :all) ⇒ Object
!! Does NOT return associations on subtypes of klass - see sti_reflections for that.
-
.model_from_file_name(file_name) ⇒ Class
e.g.
- .nested_subclasses(parent = self) ⇒ Hash
-
.no_related_data?(object, ignore: []) ⇒ Boolean
True if object has no data in any has_many or has_one associations.
-
.non_project_data_classes ⇒ Array of Classes
Data models that do not have a project_id attribute.
-
.project_data_classes ⇒ Array of Classes
All models with a project_id attribute.
-
.related_data_counts(klass, ids, ignore: []) ⇒ Hash{Symbol => Integer}
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.
- .relation_targets_community?(relation) ⇒ Boolean
-
.relationship_type(relation) ⇒ Object
collection?, has_mone? belongs_to?.
-
.sti_reflections(klass, relationship_type = :all) ⇒ Object
.klass_reflections for an STI base class and all its descendants.
-
.superclass_models ⇒ Array
All models that inherit directly from ApplicationRecord.
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.
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.
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)
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.
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.
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.
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.
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.
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.[: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.[: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.
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
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
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 |
.no_related_data?(object, ignore: []) ⇒ Boolean
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:).
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.(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.[:through].present? next if excluded.include?(relation.name) = object.send(relation.name) has_data = if relation.collection? .any? else .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.
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.
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 |
.related_data_counts(klass, ids, ignore: []) ⇒ Hash{Symbol => Integer}
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.
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.(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.[: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
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.[: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?
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.
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.
43 44 45 |
# File 'lib/application_enumeration.rb', line 43 def self.superclass_models ApplicationRecord.descendants.select{|a| a.superclass == ApplicationRecord } end |