@@ -113,20 +113,44 @@ def collect_available_attributes(model_filter = 'all')
113113 model_class = model_filter . constantize
114114 attributes = [ ]
115115
116- # Add mobility attributes
116+ # Add mobility attributes from the model itself
117117 if model_class . respond_to? ( :mobility_attributes )
118118 model_class . mobility_attributes . each do |attr |
119119 attributes << { name : attr . to_s , type : 'text' , source : 'mobility' }
120120 end
121121 end
122122
123- # Add translatable attachment attributes
123+ # Add translatable attachment attributes from the model itself
124124 if model_class . respond_to? ( :mobility_translated_attachments )
125125 model_class . mobility_translated_attachments &.keys &.each do |attr |
126126 attributes << { name : attr . to_s , type : 'file' , source : 'attachment' }
127127 end
128128 end
129129
130+ # For STI models, also check descendants for translatable attributes
131+ load_subclasses ( model_class )
132+ if model_class . respond_to? ( :descendants ) && model_class . descendants . any?
133+ model_class . descendants . each do |subclass |
134+ # Add mobility attributes from subclass
135+ if subclass . respond_to? ( :mobility_attributes )
136+ subclass . mobility_attributes . each do |attr |
137+ attributes << { name : attr . to_s , type : 'text' , source : 'mobility' } unless attributes . any? do |a |
138+ a [ :name ] == attr . to_s
139+ end
140+ end
141+ end
142+
143+ # Add translatable attachment attributes from subclass
144+ next unless subclass . respond_to? ( :mobility_translated_attachments )
145+
146+ subclass . mobility_translated_attachments &.keys &.each do |attr |
147+ attributes << { name : attr . to_s , type : 'file' , source : 'attachment' } unless attributes . any? do |a |
148+ a [ :name ] == attr . to_s
149+ end
150+ end
151+ end
152+ end
153+
130154 attributes . sort_by { |attr | attr [ :name ] }
131155 rescue StandardError => e
132156 Rails . logger . error "Error collecting attributes for #{ model_filter } : #{ e . message } "
@@ -626,8 +650,16 @@ def calculate_model_instance_stats
626650 # Get attribute-specific coverage
627651 attribute_coverage = calculate_attribute_coverage_for_model ( model_name , model_class )
628652
629- # Calculate coverage percentage with bounds checking
630- coverage_percentage = if total_instances . positive? && translated_instances <= total_instances
653+ # Calculate overall coverage percentage as average of attribute coverages
654+ # This is more accurate than just counting instances with ANY translation
655+ coverage_percentage = if attribute_coverage &.any?
656+ # Calculate average coverage across all attributes
657+ attribute_percentages = attribute_coverage . values . map do |attr |
658+ attr [ :coverage_percentage ] || 0.0
659+ end
660+ ( attribute_percentages . sum / attribute_percentages . size ) . round ( 1 )
661+ elsif total_instances . positive? && translated_instances <= total_instances
662+ # Fallback to instance-based calculation if no attributes
631663 ( translated_instances . to_f / total_instances * 100 ) . round ( 1 )
632664 elsif translated_instances > total_instances
633665 Rails . logger . warn "Translation coverage anomaly for #{ model_name } : #{ translated_instances } translated > #{ total_instances } total"
@@ -725,33 +757,59 @@ def calculate_attribute_coverage_for_model(model_name, model_class)
725757 model_class . count
726758 end
727759
728- # Get all mobility attributes for this model
760+ # Debug logging for troubleshooting
761+ Rails . logger . debug "Calculating coverage for #{ model_name } : #{ total_instances } total instances"
762+ Rails . logger . debug "Has mobility_attributes? #{ model_class . respond_to? ( :mobility_attributes ) } "
729763 if model_class . respond_to? ( :mobility_attributes )
730- model_class . mobility_attributes . each do | attribute |
731- attribute_name = attribute . to_s
764+ Rails . logger . debug "Mobility attributes: #{ model_class . mobility_attributes . inspect } "
765+ end
732766
733- # Count instances with translations for this specific attribute
734- instances_with_attribute = count_instances_with_attribute_translations ( model_name , attribute_name )
767+ # Collect mobility attributes from the model and its subclasses ( for STI)
768+ all_attributes = Set . new
735769
736- # Calculate coverage with bounds checking
737- coverage_percentage = if total_instances . positive? && instances_with_attribute <= total_instances
738- ( instances_with_attribute . to_f / total_instances * 100 ) . round ( 1 )
739- elsif instances_with_attribute > total_instances
740- Rails . logger . warn "Attribute coverage anomaly for #{ model_name } .#{ attribute_name } : #{ instances_with_attribute } > #{ total_instances } "
741- 100.0
742- else
743- 0.0
744- end
770+ # Load subclasses to ensure they're available in development
771+ load_subclasses ( model_class )
745772
746- coverage [ attribute_name ] = {
747- instances_translated : instances_with_attribute ,
748- total_instances : total_instances ,
749- coverage_percentage : coverage_percentage ,
750- attribute_type : 'mobility'
751- }
773+ # Get attributes from the main model
774+ if model_class . respond_to? ( :mobility_attributes )
775+ model_class . mobility_attributes . each { |attr | all_attributes . add ( attr . to_s ) }
776+ end
777+
778+ # For STI models, also check subclasses for their translatable attributes
779+ if model_class . respond_to? ( :descendants ) && model_class . descendants . any?
780+ model_class . descendants . each do |subclass |
781+ if subclass . respond_to? ( :mobility_attributes )
782+ Rails . logger . debug "STI subclass #{ subclass . name } has attributes: #{ subclass . mobility_attributes . inspect } "
783+ subclass . mobility_attributes . each { |attr | all_attributes . add ( attr . to_s ) }
784+ end
752785 end
753786 end
754787
788+ Rails . logger . debug "All collected attributes for #{ model_name } : #{ all_attributes . to_a . inspect } "
789+
790+ # Calculate coverage for each unique attribute
791+ all_attributes . each do |attribute_name |
792+ # Count instances with translations for this specific attribute
793+ instances_with_attribute = count_instances_with_attribute_translations ( model_name , attribute_name )
794+
795+ # Calculate coverage with bounds checking
796+ coverage_percentage = if total_instances . positive? && instances_with_attribute <= total_instances
797+ ( instances_with_attribute . to_f / total_instances * 100 ) . round ( 1 )
798+ elsif instances_with_attribute > total_instances
799+ Rails . logger . warn "Attribute coverage anomaly for #{ model_name } .#{ attribute_name } : #{ instances_with_attribute } > #{ total_instances } "
800+ 100.0
801+ else
802+ 0.0
803+ end
804+
805+ coverage [ attribute_name ] = {
806+ instances_translated : instances_with_attribute ,
807+ total_instances : total_instances ,
808+ coverage_percentage : coverage_percentage ,
809+ attribute_type : 'mobility'
810+ }
811+ end
812+
755813 # Get translatable attachment attributes
756814 if model_class . respond_to? ( :mobility_translated_attachments )
757815 model_class . mobility_translated_attachments &.keys &.each do |attachment_name |
@@ -1091,5 +1149,49 @@ def truncate_value(value, limit = 100)
10911149 text = value . to_s . strip
10921150 text . length > limit ? "#{ text [ 0 ..limit ] } ..." : text
10931151 end
1152+
1153+ # Load subclasses for STI models to ensure they're available in development environment
1154+ def load_subclasses ( model_class )
1155+ return unless model_class . respond_to? ( :descendants )
1156+
1157+ # In development, Rails lazy-loads classes, so we need to force-load STI subclasses
1158+ if Rails . env . development?
1159+ # Get the base model's directory path
1160+ base_path = Rails . application . root . join ( 'app' , 'models' )
1161+ engine_path = BetterTogether ::Engine . root . join ( 'app' , 'models' )
1162+
1163+ # Convert class name to file path pattern
1164+ model_path = model_class . name . underscore
1165+
1166+ # Look for subclass files in both app and engine models
1167+ [ base_path , engine_path ] . each do |path |
1168+ # Check for files in the same directory as the base model
1169+ model_dir = File . dirname ( model_path )
1170+ pattern = path . join ( "#{ model_dir } /*.rb" )
1171+
1172+ Dir . glob ( pattern ) . each do |file |
1173+ # Extract class name from file path and try to constantize it
1174+ relative_path = Pathname . new ( file ) . relative_path_from ( path ) . to_s
1175+ class_name = relative_path . gsub ( '.rb' , '' ) . camelize
1176+
1177+ begin
1178+ # Only try to load if it's not the same as the base class
1179+ next if class_name == model_class . name
1180+
1181+ loaded_class = class_name . constantize
1182+
1183+ # Check if it's actually a subclass of our model
1184+ if loaded_class . ancestors . include? ( model_class ) && loaded_class != model_class
1185+ Rails . logger . debug "Successfully loaded subclass: #{ class_name } "
1186+ end
1187+ rescue NameError , LoadError => e
1188+ Rails . logger . debug "Could not load potential subclass #{ class_name } : #{ e . message } "
1189+ end
1190+ end
1191+ end
1192+ end
1193+ rescue StandardError => e
1194+ Rails . logger . warn "Error loading subclasses for #{ model_class . name } : #{ e . message } "
1195+ end
10941196 end
10951197end
0 commit comments