Class: ProductSpecification

Inherits:
ApplicationRecord show all
Includes:
ActionView::Helpers::TagHelper, ActionView::Helpers::UrlHelper, Models::Auditable, Models::Translatable, PgSearch::Model
Defined in:
app/models/product_specification.rb

Overview

== Schema Information

Table name: product_specifications
Database name: primary

id :integer not null, primary key
description :text
do_not_merge :boolean default(FALSE), not null
formatter :string
grouping :string
has_liquid_blurb :boolean default(FALSE), not null
method :string(255) not null
name :string(255) not null
position :integer not null
print_on_item_label :boolean default(FALSE), not null
propagation :integer default("unrestricted")
sku_regexp :string
template :boolean default(FALSE)
text_blurb :text
token :string(255)
tokenizer_character :string(1)
translations :jsonb
units :string(255)
visibility :integer not null
created_at :datetime not null
updated_at :datetime not null
creator_id :integer
image_id :integer
product_category_id :integer
product_line_id :integer
template_product_specification_id :integer
updater_id :integer

Indexes

idx_name_template (name,template)
idx_pc_id_product_line_id (product_category_id,product_line_id)
idx_propagation_token_template (propagation,token,template)
index_product_specifications_grouping (grouping)
index_product_specifications_on_image_id (image_id)
index_product_specifications_on_position (position)
index_product_specifications_on_product_line_id (product_line_id)
index_product_specifications_on_token (token)
product_specifications_template_product_specification_id_idx (template_product_specification_id)

Foreign Keys

fk_rails_... (image_id => digital_assets.id)
fk_rails_... (product_category_id => product_categories.id)
fk_rails_... (product_line_id => product_lines.id)
fk_rails_... (template_product_specification_id => product_specifications.id) ON DELETE => nullify

Constant Summary collapse

TRANSLATION_NAMESPACE =
'ProductSpecification'
SAFE_METHODS =
%w[
  text
  base_weight
  total_shipping_weight
  btu_per_hour
  calculate_amps
  calculate_kilowatts
  calculate_number_of_fixing_strips_at_3_in
  calculate_number_of_fixing_strips_at_4_in
  calculate_number_of_fixing_strips_at_5_in
  calculate_ohms
  calculate_ohms_per_ft
  calculate_geometric_sqft
  calculate_sqft
  calculate_watts
  calculate_watts_per_sqft
  calculate_heated_area_sqft_for_panels
  calculate_heated_area_sqm_for_panels
  calculate_thermal_power_per_hour
  calculate_coverage_for_membrane
  country_of_origin_name
  coverage_at_3_5_in
  coverage_at_3_75_in
  coverage_at_3_in
  coverage_at_4_in
  coverage_at_4_5_in
  coverage_at_5_in
  harmonization_code
  ohms
  package_contents_friendly
  package_content_html
  sensor
  sku
  upc
  watts_per_linear_feet
  watts_per_sqft_at_3_in_spacing
  watts_per_sqft_at_3_5_in_spacing
  watts_per_sqft_at_3_75_in_spacing
  watts_per_sqft_at_4_in_spacing
  watts_per_sqft_at_4_5_in_spacing
  watts_per_sqft_at_5_in_spacing
  width_by_length_text
  width_by_length_text_2
  calculate_size_2d
  calculate_size_2d_in
  calculate_size_2d_ft
  calculate_size_2d_metric_mm
  calculate_size_2d_metric_cm
  calculate_size_3d
  calculate_size_3d_in
  calculate_size_3d_ft
  calculate_size_3d_metric_mm
  calculate_size_3d_metric_cm
].freeze
NUMERIC_SPECS =
%w[
  maximum_current maximum_current_per_relay num_poles
  length width cold_lead_length sqft coverage watts ohms voltage amps
].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Attributes included from Models::Translatable

#do_not_compact_translation_container

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Translatable

#compact_translation_container, skip_compaction_for

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#groupingObject (readonly)



140
# File 'app/models/product_specification.rb', line 140

validates :method, :name, :grouping, presence: true

#methodObject (readonly)



140
# File 'app/models/product_specification.rb', line 140

validates :method, :name, :grouping, presence: true

#nameObject (readonly)



140
# File 'app/models/product_specification.rb', line 140

validates :method, :name, :grouping, presence: true

#should_auto_translateObject

Returns the value of attribute should_auto_translate.



123
124
125
# File 'app/models/product_specification.rb', line 123

def should_auto_translate
  @should_auto_translate
end

#skip_spec_refreshObject

Returns the value of attribute skip_spec_refresh.



123
124
125
# File 'app/models/product_specification.rb', line 123

def skip_spec_refresh
  @skip_spec_refresh
end

#text_blurbObject (readonly)



149
# File 'app/models/product_specification.rb', line 149

validates :text_blurb, numericality: { if: :spec_requires_numeric? }

#text_blurb_lookupObject

Returns the value of attribute text_blurb_lookup.



123
124
125
# File 'app/models/product_specification.rb', line 123

def text_blurb_lookup
  @text_blurb_lookup
end

#unitsObject (readonly)



142
# File 'app/models/product_specification.rb', line 142

validates :units, ruby_unit: true

Class Method Details

.by_product_category_idActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are by product category id. Active Record Scope

Returns:

See Also:



187
188
189
190
191
192
193
194
195
# File 'app/models/product_specification.rb', line 187

scope :by_product_category_id, ->(*pc_id) {
  ids = [pc_id].flatten.compact
  next none if ids.empty?

  paths = ProductCategory.where(id: ids).pluck(:ltree_path_ids).compact
  next none if paths.empty?

  joins(:product_category).where(ProductCategory[:ltree_path_ids].ltree_descendant(paths))
}

.by_product_category_id_ancestryActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are by product category id ancestry. Active Record Scope

Returns:

See Also:



196
197
198
199
# File 'app/models/product_specification.rb', line 196

scope :by_product_category_id_ancestry, ->(*pc_id) {
  ids = [pc_id].flatten.compact
  ids.empty? ? none : where(product_category_id: ProductCategory.self_and_ancestors_ids(ids))
}

.by_product_line_idActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are by product line id. Active Record Scope

Returns:

See Also:



174
175
176
177
178
179
180
181
182
# File 'app/models/product_specification.rb', line 174

scope :by_product_line_id, ->(*pl_id) {
  ids = [pl_id].flatten.compact
  next none if ids.empty?

  paths = ProductLine.where(id: ids).pluck(:ltree_path_ids).compact
  next none if paths.empty?

  joins(:product_line).where(ProductLine[:ltree_path_ids].ltree_descendant(paths))
}

.by_product_line_id_ancestryActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are by product line id ancestry. Active Record Scope

Returns:

See Also:



183
184
185
186
# File 'app/models/product_specification.rb', line 183

scope :by_product_line_id_ancestry, ->(*pl_id) {
  ids = [pl_id].flatten.compact
  ids.empty? ? none : where(product_line_id: ProductLine.self_and_ancestors_ids(ids))
}

.compact_positionsObject



377
378
379
380
381
# File 'app/models/product_specification.rb', line 377

def self.compact_positions
  readable_order.each_with_index do |pf, i|
    pf.update_column(:position, i)
  end
end

.convert_unit_for_locale(ruby_unit, target_locale) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'app/models/product_specification.rb', line 217

def self.convert_unit_for_locale(ruby_unit, target_locale)
  # If our target locale is the US (except for myanmar and libya or some odd country) we are always going to use SI (Système International) aka Metric
  # The only question becomes which unit is best suited
  lang_code, country_code = target_locale.to_s.split('-')

  # This assumes a static conversion from non-SI to SI
  conversion_map = {
    'yd' => 'm',
    'ft' => 'mm', # Later we check if we should do meters instead based on scale
    'in' => 'mm',
    'lbs' => 'kg',
    'oz' => 'g',
    'degF' => 'degC',
    'gal' => 'l',
    'quart' => 'l',
    'pint' => 'l',
    'sqft' => 'm2',
    'sqin' => 'm2'
  }
  # This only supports going one way, from imperial to SI
  target_unit = conversion_map[ruby_unit.units]

  return ruby_unit if target_unit.nil? ||
                      country_code == 'US' ||
                      (country_code.nil? && lang_code == 'en') # No need for conversion

  # Now we're going to do some smart metric conversion
  # If our target unit is > 1000 mm, we will convert to meter
  if target_unit == 'mm' && ruby_unit.convert_to(target_unit).scalar.to_i >= 1000
    target_unit = 'm' # Jump straight to meters
  end
  if target_unit == 'g' && ruby_unit.convert_to(target_unit).scalar.to_i >= 1000
    target_unit = 'kg' # Go to kgs when large numbers
  end
  target_unit = 'ml' if target_unit == 'l' && ruby_unit.convert_to(target_unit).scalar.to_i < 100
  ruby_unit.convert_to(target_unit)
end

.empty_non_template_specsActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are empty non template specs. Active Record Scope

Returns:

See Also:



173
# File 'app/models/product_specification.rb', line 173

scope :empty_non_template_specs, -> { where(text_blurb: nil).where(template: false) }

.express(raw_value, native_unit: nil, formatter: nil, locale: nil) ⇒ Object

Formats a value using a formatter and unit



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'app/models/product_specification.rb', line 494

def self.express(raw_value, native_unit: nil, formatter: nil, locale: nil)
  return raw_value unless native_unit

  # Special cases
  if native_unit == 'y' # Years
    s = "#{raw_value.to_i} "
    s << (raw_value.to_i > 1 ? 'years' : 'year')
    return s
  end

  native_value = "#{raw_value} #{native_unit}".squish
  native_value_pretty = "#{raw_value}#{UnitHelper.unit_symbol(native_unit)}".squish

  # There's little to gain from running the number through the ruby unit
  # parser just to spit it out again, since ruby unit is a US centric parsing
  # you would need to understand the format of the raw_value and convert it first
  return native_value_pretty if formatter.blank? || (locale.present? && locale.to_s.first(2) != 'en')

  native_value_unitized = begin
    # Ruby unit is a very US centric parsing fyi
    RubyUnits::Unit.new(native_value)
  rescue StandardError
    nil
  end
  return native_value unless native_value_unitized

  case formatter
  when 'FeetAndInches'
    inches = native_value_unitized.convert_to('in').scalar
    r = UnitConversions.inches_to_feetinches(inches, separator: ' ')
  when 'PoundsAndOunces'
    s = []
    ounces = native_value_unitized.convert_to('oz').scalar
    pounds = (ounces / 16).truncate
    ounces = (ounces % 16)
    if pounds.positive?
      pounds = ActionController::Base.helpers.number_with_precision pounds, precision: 0, strip_insignificant_zeros: true
      s << "#{pounds} lbs"
    end
    if ounces.positive?
      ounces = ActionController::Base.helpers.number_with_precision ounces, precision: 0, strip_insignificant_zeros: true
      s << "#{ounces} oz"
    end
    r = s.join(', ')
  when 'ClosestRational'
    fs = []
    fs << UnitConversions.closest_rational(raw_value.to_f).to_s
    fs << UnitHelper.unit_symbol(native_unit) if native_unit
    r = fs.compact.join
  else
    native_value_unitized.to_s
    # render with pretty symbol
    v = ActionController::Base.helpers.number_with_precision native_value_unitized.scalar.to_f, precision: 4, strip_insignificant_zeros: true
    r = "#{v}#{UnitHelper.unit_symbol(native_value_unitized.units)}".squish
  end
  r
end

.formatters_for_selectObject



489
490
491
# File 'app/models/product_specification.rb', line 489

def self.formatters_for_select
  %w[FeetAndInches PoundsAndOunces ClosestRational]
end

.generate_name_from_token_and_grouping(token, grouping = nil) ⇒ Object



745
746
747
748
749
750
751
752
753
# File 'app/models/product_specification.rb', line 745

def self.generate_name_from_token_and_grouping(token, grouping = nil)
  token = token.to_s
  if grouping&.start_with?('X-')
    # Namespace, e.g. if X-Amazon then amazon
    namespace = grouping.split('-').last.parameterize.underscore
    token = token.gsub("#{namespace}_", '')
  end
  token.humanize.capitalize
end

.generate_token_from_name_and_grouping(name, grouping = nil) ⇒ Object



734
735
736
737
738
739
740
741
742
743
# File 'app/models/product_specification.rb', line 734

def self.generate_token_from_name_and_grouping(name, grouping = nil)
  tokenized_name = name.parameterize.underscore
  if grouping&.start_with?('X-')

    # Namespace, e.g. if X-Amazon then amazon
    namespace = grouping.split('-').last.parameterize.underscore
    tokenized_name = "#{namespace}_#{tokenized_name}"
  end
  tokenized_name
end

.grouping_select_options(locale = nil) ⇒ Object



422
423
424
425
426
427
# File 'app/models/product_specification.rb', line 422

def self.grouping_select_options(locale = nil)
  locales = [locale, LocaleUtility.root_content_locale(locale)].uniq.compact.presence || Mobility.available_locales
  locales.flat_map do |locale|
    Mobility.with_locale(locale) { i18n.where.not(grouping: [nil, '']).distinct.pluck(:grouping) }
  end.compact.uniq.sort
end

.import_translation_keysObject



383
384
385
386
387
# File 'app/models/product_specification.rb', line 383

def self.import_translation_keys
  ProductSpecification.where.not(translations: {}).where(units: nil).find_each do |ps|
    ps.import_translation_keys
  end
end

.locales_for_language(lang_code) ⇒ Object



209
210
211
# File 'app/models/product_specification.rb', line 209

def self.locales_for_language(lang_code)
  translated_locales.select { |locale| locale.to_s.first(2) == lang_code.to_s }
end

.method_select_optionsObject



329
330
331
# File 'app/models/product_specification.rb', line 329

def self.method_select_options
  where.not(method: [nil, '']).order(:method).distinct.pluck(:method)
end

.methods_for_selectObject



447
448
449
# File 'app/models/product_specification.rb', line 447

def self.methods_for_select
  SAFE_METHODS
end

.name_select_options(grouping: nil) ⇒ Object



408
409
410
411
412
# File 'app/models/product_specification.rb', line 408

def self.name_select_options(grouping: nil)
  search_scope = ProductSpecification.all.order(:name)
  search_scope = search_scope.where(grouping:) if grouping
  [['All', nil]] + search_scope.distinct.pluck(:name).map { |name| [name, name] }
end

.next_positionObject

TODO, we have acts as list, don't need this!



373
374
375
# File 'app/models/product_specification.rb', line 373

def self.next_position
  (maximum(:position) || 0) + 1
end

.options_scope(options = {}, _store_id = nil) ⇒ Object



356
357
358
359
360
361
362
# File 'app/models/product_specification.rb', line 356

def self.options_scope(options = {}, _store_id = nil)
  Rails.logger.debug("ProductLatchable.options_scope", class_name: self.class.to_s)
  res = all
  crits = (options || {}).reject { |_k, v| v.blank? or v == 'NULL' }
  res = res.where(crits) if crits.present?
  res
end

.positionedActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are positioned. Active Record Scope

Returns:

See Also:



200
# File 'app/models/product_specification.rb', line 200

scope :positioned, -> { order(:position) }

.product_category_select_optionsObject



368
369
370
# File 'app/models/product_specification.rb', line 368

def self.product_category_select_options
  [['All', nil]] + ProductCategory.joins(to_s.tableize.to_sym).order(:priority).pluck(:lineage_expanded, :id).uniq
end

.product_line_select_optionsObject



364
365
366
# File 'app/models/product_specification.rb', line 364

def self.product_line_select_options
  [['All', nil]] + ProductLine.joins(to_s.tableize.to_sym).order(:priority).pluck(:lineage_expanded, :id).uniq
end

.propagations_for_selectObject



321
322
323
# File 'app/models/product_specification.rb', line 321

def self.propagations_for_select
  ProductSpecification.propagations.keys.map { |p| [p.titleize, p] }
end

.readable_orderActiveRecord::Relation<ProductSpecification>

A relation of ProductSpecifications that are readable order. Active Record Scope

Returns:

See Also:



167
168
169
170
171
172
# File 'app/models/product_specification.rb', line 167

scope :readable_order, -> {
  left_outer_joins(:product_line)
    .left_outer_joins(:product_category)
    .order(:position, ProductLine[:priority], ProductCategory[:priority], :name, :method)
    .readonly(false)
}

.token_select_optionsObject



325
326
327
# File 'app/models/product_specification.rb', line 325

def self.token_select_options
  where.not(token: [nil, '']).order(:token).distinct.pluck(:token)
end

.translated_lang_codesObject



213
214
215
# File 'app/models/product_specification.rb', line 213

def self.translated_lang_codes
  translated_locales.map { |l| l.to_s.first(2) }.uniq.sort.map(&:to_sym) - [:en]
end

.translated_localesObject



204
205
206
207
# File 'app/models/product_specification.rb', line 204

def self.translated_locales
  # [] + Mobility.available_locales - [I18n.default_locale]
  [] + EDITABLE_TRANSLATIONS.map(&:to_sym) - [I18n.default_locale]
end

.units_select_options(locale = Mobility.locale) ⇒ Object



397
398
399
400
401
# File 'app/models/product_specification.rb', line 397

def self.units_select_options(locale = Mobility.locale)
  I18n.with_locale(locale) do
    UnitHelper.all_units
  end
end

.visibilities_for_selectObject



315
316
317
318
319
# File 'app/models/product_specification.rb', line 315

def self.visibilities_for_select
  [['Internal, for heatwave use only', 'internal'],
   ['Hidden, exposed to API and Web but recommended not displayed', 'hidden'],
   ['Open, exposed to API and Web and display recommended', 'open_visibility']]
end

Instance Method Details

#affected_itemsObject



614
615
616
617
618
619
620
621
622
623
624
# File 'app/models/product_specification.rb', line 614

def affected_items
  if items.present?
    items
  else
    item_query = Item.active
    item_query = item_query.by_product_category_id(product_category_id) if product_category_id.present?
    item_query = item_query.by_product_line_id(product_line_id) if product_line_id.present?
    item_query = item_query.where('items.sku ~ ?', sku_regexp) if sku_regexp.present?
    item_query
  end
end

#affected_items_idsObject



644
645
646
# File 'app/models/product_specification.rb', line 644

def affected_items_ids
  affected_items.pluck(:id)
end

#async_refresh_itemsObject



652
653
654
655
656
# File 'app/models/product_specification.rb', line 652

def async_refresh_items
  return if skip_spec_refresh

  ProductSpecsWorker.perform_async(id)
end

#auto_translate(reset_previous_values: false, locales: nil, attributes: nil) ⇒ Object

We're going to auto translate this spec for each locale, we translate
name, grouping, unit, and text content
TranslationKey is a proxy



258
259
260
261
262
263
264
265
266
267
268
269
270
271
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
305
306
307
308
309
310
311
312
313
# File 'app/models/product_specification.rb', line 258

def auto_translate(reset_previous_values: false, locales: nil, attributes: nil)
  result = {}
  if text_blurb_en.blank?
    logger.warn "Auto translate called on Product Specification #{id} with a blank text_blurb_en, skipping"
    return result
  end

  locales = if locales.nil?
              self.class.translated_locales
            else # make sure they're supported languages
              locales.map(&:to_sym) & self.class.translated_locales
            end

  # Unit based specs will not be localized via text translations but via rubyunit and numeric localization
  ruby_unit = to_ruby_unit
  locales.each do |target_locale|
    translated_text = nil

    if ruby_unit
      converted_unit = self.class.convert_unit_for_locale(ruby_unit, target_locale)
      translated_units = converted_unit.units
      # Only if there was a text blurb, then do we need to store the translated text, otherwise it is just a unit conversion
      translated_text = (I18n.with_locale(target_locale) { ActionController::Base.helpers.number_with_precision(converted_unit.scalar.to_f, strip_insignificant_zeros: true, precision: 2) } if text_blurb.present?)
    else
      # We only need to translate non-numeric values, so that's fun how would you detect that?
      # I will try anything with more than 2 letters contiguous
      translated_text = TranslationKey.translate(text_blurb_en, target_locale, namespace: TRANSLATION_NAMESPACE, resource: self, resource_attribute: 'text_blurb') if text_blurb_en&.match?(/[a-zA-Z]{3}/)
      translated_text ||= text_blurb_en
      translated_units = nil
    end

    # If reset is desired, we have to purge all locale first including those country localized ones
    if reset_previous_values
      target_locales_for_purge = Mobility.available_locales.select { |l| l.to_s.starts_with?(target_locale.to_s) }
      target_locales_for_purge.each do |target_locale_for_purge|
        Mobility.with_locale(target_locale_for_purge) do
          self.name = nil
          self.grouping = nil
          self.units = nil
          self.text_blurb = nil
        end
      end
    end

    # Time to store it with our mobility friend
    Mobility.with_locale(target_locale) do
      result[target_locale] = {
        units: self.units ||= translated_units,
        text_blurb: self.text_blurb ||= translated_text
      }
    end
  end
  @should_auto_translate = false
  @skip_spec_refresh = true
  save && result
end

#cached_liquid_template(content) ⇒ Object

Returns the parsed Liquid template for the given content string, or nil if it contains no
Liquid variables. Results are memoized by content so multi-locale renders share a single parse
for identical content, and the ~99.5% of non-liquid specs skip parsing entirely after the
first call.



634
635
636
637
638
639
640
641
642
# File 'app/models/product_specification.rb', line 634

def cached_liquid_template(content)
  return nil if content.blank?

  @cached_liquid_templates ||= {}
  return @cached_liquid_templates[content] if @cached_liquid_templates.key?(content)

  lt = Liquid::ParseEnvironment.parse(content.to_s)
  @cached_liquid_templates[content] = lt.root.nodelist.any? { |node| node.is_a?(Liquid::Variable) } ? lt : nil
end

#dynamic_method?Boolean

Returns:

  • (Boolean)


443
444
445
# File 'app/models/product_specification.rb', line 443

def dynamic_method?
  !text_method?
end

#get_dynamic_spec_data(target, locale) ⇒ Object



552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'app/models/product_specification.rb', line 552

def get_dynamic_spec_data(target, locale)
  # First try a method in the non default locale
  target = target.call if target.respond_to?(:call)
  if locale != I18n.default_locale
    method_localized = :"#{method}_#{LocaleUtility.parameterized_locale(locale)}"
    spec_data = target.try(method_localized)
  end
  if spec_data.blank?
    Mobility.with_locale(I18n.default_locale) do
      # Fallback to the native method, note we have to use a default locale
      # wrapper here so that calculations retrieving other specs also pull the correct one
      spec_data = target.try(method.to_sym)
    end
  end
  spec_data
end

#get_specification_data(target, options = {}) ⇒ Object

Target could be: item, catalog_item, product_line



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
# File 'app/models/product_specification.rb', line 570

def get_specification_data(target, options = {})
  locale = (options[:locale] || Mobility.locale).to_sym
  spec_data = nil
  if dynamic_method?
    target_chain = case target.class.name
                   when 'Item'
                     target.get_self_and_kit_items(spec_only: true).to_a
                   when 'CatalogItem'
                     target.item.get_self_and_kit_items(spec_only: true).to_a
                   else
                     raise StandardError.new("#{target} is not a valid Item or CatalogItem")
                   end
    target_chain << target.primary_product_line if target.primary_product_line_id.present?
    target_chain << product_line if product_line_id && product_line_id != target.primary_product_line_id
    target_chain.each do |t|
      spec_data = get_dynamic_spec_data(t, locale)
      break if spec_data
    end
  end
  spec_data ||= Mobility.with_locale(locale) { text_blurb.presence }
  spec_data ||= Mobility.with_locale(I18n.default_locale) { text_blurb.presence }
  # If our spec data has liquid tag, we will try to render them based on the items in the target chain
  if spec_data.present? && target.respond_to?(:token_specs_values_for_liquid) && (lt = cached_liquid_template(spec_data))
    spec_data = lt.render(target.token_specs_values_for_liquid(include_legacy: false))
  end

  spec_data = TypeCoercer.coerce(spec_data)

  result = OpenStruct.new(raw: spec_data, formatted: spec_data)
  return result if options[:skip_units] || units.blank? || spec_data.blank?

  # If a tokenizer is present, the spec data is first split according to it
  values = if tokenizer_character.present?
             spec_data.to_s.split(tokenizer_character)
           else
             [spec_data]
           end
  # Each value is passed through the formatter, native units is what v is expressed in, formatter is the
  # Formatter class to filter through, e.g 26" passed through the FormattedFt filter is 2' 4"
  localized_units = Mobility.with_locale(locale) { units } || units
  result.formatted = values.map { |v| self.class.express(v, native_unit: localized_units, formatter:, locale:) }.join(', ')
  result
end

#grouping_select_options(locale = nil) ⇒ Object



414
415
416
417
418
419
420
# File 'app/models/product_specification.rb', line 414

def grouping_select_options(locale = nil)
  r = self.class.grouping_select_options(locale)
  Mobility.with_locale(locale) do
    r << grouping if grouping.present?
  end
  r.compact.uniq.sort
end

#imageImage

Returns:

See Also:



135
# File 'app/models/product_specification.rb', line 135

belongs_to :image, optional: true

#import_translation_keys(force: false) ⇒ Object

This method will import existing translations or link to existing ones



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'app/models/product_specification.rb', line 334

def import_translation_keys(force: false)
  tkrs = []

  translations.each do |(locale_string, existing_translations)|
    existing_translations.each do |attr, translation|
      puts "--- ps: #{id} -- #{attr} -- #{translation}"
      # The presence of resources implies it was imported
      next if translation_key_resources.joins(translation_key: :translations).where(resource_attribute: attr).joins(translation_key: :translations).where(TranslationText[:locale].eq(locale_string)).exists?
      next unless (text = TranslationKey.newline_normalize(send(:"#{attr}_en"))).present? # e.g text_blurb, units
      next unless force || text&.match?(/[a-zA-Z]{3}/) # only 3 letters+

      # Does this translation key exists?
      tk = TranslationKey.where(key: text, namespace: TRANSLATION_NAMESPACE).first_or_create!
      # Do we need to import this translation
      tk.translations.where(locale: locale_string.first(2)).first_or_create!(text: translation)
      # And link our new resource
      tkrs << tk.translation_key_resources.where(resource: self, resource_attribute: attr).first_or_create!
    end
  end
  tkrs
end

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



138
# File 'app/models/product_specification.rb', line 138

has_and_belongs_to_many :items, inverse_of: :direct_product_specifications, autosave: false

#name_select_options(filter_grouping: nil) ⇒ Object



403
404
405
406
# File 'app/models/product_specification.rb', line 403

def name_select_options(filter_grouping: nil)
  filter_grouping = grouping if grouping&.start_with?('X-')
  self.class.name_select_options(grouping: filter_grouping)
end

#product_categoryProductCategory



133
# File 'app/models/product_specification.rb', line 133

belongs_to :product_category, optional: true

#product_lineProductLine



132
# File 'app/models/product_specification.rb', line 132

belongs_to :product_line, optional: true

#refresh_itemsObject



658
659
660
661
662
663
# File 'app/models/product_specification.rb', line 658

def refresh_items
  return if skip_spec_refresh

  affected_items_ids.each { |iid| ItemAttributeWorker.perform_async(iid) }
  affected_items_ids
end

#render_priorityObject

spec value that are static will be first returned, this is so they can be computed and established
first in case they are used by future dynamic calculations. e.g ohms is static and should
be rendered before the calculate_watts method is called which uses it.



454
455
456
# File 'app/models/product_specification.rb', line 454

def render_priority
  text_method? ? 0 : 1
end

#rendering_priorityObject



433
434
435
436
437
438
439
440
441
# File 'app/models/product_specification.rb', line 433

def rendering_priority
  if text_method? && !text_blurb_is_liquid?
    0
  elsif dynamic_method?
    1
  else
    2
  end
end

#set_has_liquid_blurbObject



669
670
671
672
673
674
# File 'app/models/product_specification.rb', line 669

def set_has_liquid_blurb
  blurb_content = [text_blurb_en.presence, *translations.values.filter_map { |v| v['text_blurb'].presence }].compact
  self.has_liquid_blurb = blurb_content.any? do |content|
    (lt = Liquid::ParseEnvironment.parse(content)) && lt.root.nodelist.any? { |node| node.is_a?(Liquid::Variable) }
  end
end

#summaryObject



648
649
650
# File 'app/models/product_specification.rb', line 648

def summary
  "#{product_line.slug_ltree} | #{product_category.url} | #{name} | #{method}"
end

#template_product_specificationProductSpecification



134
# File 'app/models/product_specification.rb', line 134

belongs_to :template_product_specification, class_name: 'ProductSpecification', optional: true

#text_blurb_is_liquid?Boolean

Returns:

  • (Boolean)


626
627
628
# File 'app/models/product_specification.rb', line 626

def text_blurb_is_liquid?
  has_liquid_blurb?
end

#text_method?Boolean

Returns:

  • (Boolean)


429
430
431
# File 'app/models/product_specification.rb', line 429

def text_method?
  method == 'text'
end

#text_output(locale = Mobility.locale) ⇒ Object



466
467
468
469
470
# File 'app/models/product_specification.rb', line 466

def text_output(locale = Mobility.locale)
  Mobility.with_locale(locale) do
    "#{text_blurb}#{unit_symbol}".squish.presence
  end
end

#to_ruby_unit(locale = :en) ⇒ Object



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'app/models/product_specification.rb', line 472

def to_ruby_unit(locale = :en)
  native_value_unitized = nil

  Mobility.with_locale(locale) do
    return unless units.present?

    native_value = "#{text_blurb} #{units}".squish

    native_value_unitized = begin
      RubyUnits::Unit.new(native_value)
    rescue StandardError
      nil
    end
  end
  native_value_unitized
end

#to_sObject



458
459
460
# File 'app/models/product_specification.rb', line 458

def to_s
  "#{name} (#{id})"
end

#translation_key_resourcesActiveRecord::Relation<TranslationKeyResource>

Returns:

See Also:



136
# File 'app/models/product_specification.rb', line 136

has_many :translation_key_resources, as: :resource, dependent: :destroy

#translation_keysActiveRecord::Relation<TranslationKey>

Returns:

See Also:



137
# File 'app/models/product_specification.rb', line 137

has_many :translation_keys, through: :translation_key_resources

#trim_empty_translationsObject



665
666
667
# File 'app/models/product_specification.rb', line 665

def trim_empty_translations
  translations.delete_if { |_, value| !value.key?('text_blurb') || value['text_blurb'].blank? }
end

#unit_symbolObject



462
463
464
# File 'app/models/product_specification.rb', line 462

def unit_symbol
  UnitHelper.unit_symbol(units)
end

#units_select_options(locale = Mobility.locale) ⇒ Object



389
390
391
392
393
394
395
# File 'app/models/product_specification.rb', line 389

def units_select_options(locale = Mobility.locale)
  I18n.with_locale(locale) do
    select_units = self.class.units_select_options(locale)
    select_units << [units, units] unless units.blank? || select_units.any? { |v| v.last == units }
    select_units.compact.uniq.sort
  end
end