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
Array of AR associations.
-
.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?.
-
.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
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/application_enumeration.rb', line 145 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.
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/application_enumeration.rb', line 296 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
Returns Array of AR associations.
103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/application_enumeration.rb', line 103 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:).
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/application_enumeration.rb', line 194 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.
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 |
# File 'lib/application_enumeration.rb', line 249 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
117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/application_enumeration.rb', line 117 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?
133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/application_enumeration.rb', line 133 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 |
.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 |