Class: ProductSpecification
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 :enum 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 :enum 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
- CALCULATION_DEPENDENCIES =
Maps each dynamic (non-text) calculation method to the spec tokens its
value is DERIVED from, plus a human-readable formula. The rendered value of
a calculated spec is recomputed from these inputs every refresh, so editing
its text_blurb does not change what items display — and quietly setting a
value that contradicts the inputs breaks the relationship (e.g. Ohm's law,
W = V*A). Surfaced before any edit so the editor understands the dependency.
Methods not listed are still dynamic (pulled from item attributes) but carry
no cross-spec dependency worth calling out; the *_at_* families are matched
by pattern in #inferred_calculation_dependency.
{
'calculate_watts' => { derives_from: %w[voltage amps ohms], formula: "Ohm's law / power: W = V*A, or V^2/R, or A^2*R (any two of voltage, amps, ohms)" },
'calculate_amps' => { derives_from: %w[voltage ohms watts], formula: "Ohm's law: A = V/R, or W/V, or sqrt(W/R)" },
'calculate_ohms' => { derives_from: %w[voltage amps watts], formula: "Ohm's law: R = V/A, or V^2/W, or W/A^2" },
'calculate_kilowatts' => { derives_from: %w[watts voltage amps ohms], formula: 'kW = W / 1000 (W falls back to calculate_watts)' },
'btu_per_hour' => { derives_from: %w[watts thermal_output_efficiency], formula: 'BTU/hr = W * (efficiency/100) * BTU_PER_HOUR_PER_WATT' },
'calculate_thermal_power_per_hour' => { derives_from: %w[watts thermal_output_efficiency], formula: 'kW/hr = W * (efficiency/100) / 1000' },
'calculate_ohms_per_ft' => { derives_from: %w[ohms heating_cable_length length], formula: 'ohms/ft = ohms / length_ft' },
'calculate_watts_per_sqft' => { derives_from: %w[watts coverage], formula: 'W/sqft = watts / coverage (each may itself be calculated)' },
'watts_per_linear_feet' => { derives_from: %w[watts length], formula: 'W/linear-ft = watts / (length_in / 12)' },
'calculate_sqft' => { derives_from: %w[width length coverage], formula: 'sqft = (length * width) / 144 (heated-area variant adds padding)' },
'calculate_geometric_sqft' => { derives_from: %w[width length coverage], formula: 'sqft = (length * width) / 144 (no heated-area padding)' },
'calculate_heated_area_sqft_for_panels' => { derives_from: %w[watts], formula: 'sqft = ceil(watts / 7)' },
'calculate_heated_area_sqm_for_panels' => { derives_from: %w[watts], formula: 'sqm = heated_area_sqft * 0.092903' }
}.freeze
Models::Auditable::ALWAYS_IGNORED
Constants included
from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#do_not_compact_translation_container
#creator, #updater
Has and belongs to many
collapse
Delegated Instance Attributes
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
-
#affected_items ⇒ Object
-
#async_refresh_items ⇒ Object
-
#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.
-
#cached_liquid_template(content) ⇒ Object
Returns the parsed Liquid template for the given content string, or nil if it contains no Liquid variables.
-
#calculation_explanation ⇒ Hash{Symbol => Object, nil}
Explains how a dynamic spec's value is derived, for surfacing BEFORE an edit.
-
#dynamic_method? ⇒ Boolean
-
#get_dynamic_spec_data(target, locale) ⇒ Object
-
#get_specification_data(target, options = {}) ⇒ Object
Target could be: item, catalog_item, product_line.
-
#grouping_select_options(locale = nil) ⇒ Object
-
#import_translation_keys(force: false) ⇒ Object
This method will import existing translations or link to existing ones.
-
#name_select_options(filter_grouping: nil) ⇒ Object
-
#refresh_items ⇒ Object
-
#render_priority ⇒ Object
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.
-
#rendering_priority ⇒ Object
-
#set_has_liquid_blurb ⇒ Object
-
#summary ⇒ Object
-
#text_blurb_is_liquid? ⇒ Boolean
-
#text_method? ⇒ Boolean
-
#text_output(locale = Mobility.locale) ⇒ Object
-
#to_ruby_unit(locale = :en) ⇒ Object
-
#to_s ⇒ Object
-
#trim_empty_translations ⇒ Object
-
#unit_symbol ⇒ Object
-
#units_select_options(locale = Mobility.locale) ⇒ Object
#compact_translation_container, skip_compaction_for
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
config
#after_commit
#publish_event
Instance Attribute Details
#grouping ⇒ Object
172
|
# File 'app/models/product_specification.rb', line 172
validates :method, :name, :grouping, presence: true
|
#method ⇒ Object
172
|
# File 'app/models/product_specification.rb', line 172
validates :method, :name, :grouping, presence: true
|
#name ⇒ Object
172
|
# File 'app/models/product_specification.rb', line 172
validates :method, :name, :grouping, presence: true
|
#should_auto_translate ⇒ Object
Returns the value of attribute should_auto_translate.
152
153
154
|
# File 'app/models/product_specification.rb', line 152
def should_auto_translate
@should_auto_translate
end
|
#skip_spec_refresh ⇒ Object
Returns the value of attribute skip_spec_refresh.
152
153
154
|
# File 'app/models/product_specification.rb', line 152
def skip_spec_refresh
@skip_spec_refresh
end
|
#text_blurb ⇒ Object
181
|
# File 'app/models/product_specification.rb', line 181
validates :text_blurb, numericality: { if: :spec_requires_numeric? }
|
#text_blurb_lookup ⇒ Object
Returns the value of attribute text_blurb_lookup.
152
153
154
|
# File 'app/models/product_specification.rb', line 152
def text_blurb_lookup
@text_blurb_lookup
end
|
#units ⇒ Object
174
|
# File 'app/models/product_specification.rb', line 174
validates :units, ruby_unit: true
|
Class Method Details
.by_product_category_id ⇒ ActiveRecord::Relation<ProductSpecification>
A relation of ProductSpecifications that are by product category id. Active Record Scope
217
218
219
220
221
222
223
224
225
|
# File 'app/models/product_specification.rb', line 217
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_ancestry ⇒ ActiveRecord::Relation<ProductSpecification>
A relation of ProductSpecifications that are by product category id ancestry. Active Record Scope
226
227
228
229
|
# File 'app/models/product_specification.rb', line 226
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_id ⇒ ActiveRecord::Relation<ProductSpecification>
A relation of ProductSpecifications that are by product line id. Active Record Scope
204
205
206
207
208
209
210
211
212
|
# File 'app/models/product_specification.rb', line 204
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_ancestry ⇒ ActiveRecord::Relation<ProductSpecification>
A relation of ProductSpecifications that are by product line id ancestry. Active Record Scope
213
214
215
216
|
# File 'app/models/product_specification.rb', line 213
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_positions ⇒ Object
407
408
409
410
411
|
# File 'app/models/product_specification.rb', line 407
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
247
248
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
282
283
|
# File 'app/models/product_specification.rb', line 247
def self.convert_unit_for_locale(ruby_unit, target_locale)
lang_code, country_code = target_locale.to_s.split('-')
conversion_map = {
'yd' => 'm',
'ft' => 'mm', 'in' => 'mm',
'lbs' => 'kg',
'oz' => 'g',
'degF' => 'degC',
'gal' => 'l',
'quart' => 'l',
'pint' => 'l',
'sqft' => 'm2',
'sqin' => 'm2'
}
target_unit = conversion_map[ruby_unit.units]
return ruby_unit if target_unit.nil? ||
country_code == 'US' ||
(country_code.nil? && lang_code == 'en')
if target_unit == 'mm' && ruby_unit.convert_to(target_unit).scalar.to_i >= 1000
target_unit = 'm' end
if target_unit == 'g' && ruby_unit.convert_to(target_unit).scalar.to_i >= 1000
target_unit = 'kg' 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_specs ⇒ ActiveRecord::Relation<ProductSpecification>
A relation of ProductSpecifications that are empty non template specs. Active Record Scope
203
|
# File 'app/models/product_specification.rb', line 203
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
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
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
|
# File 'app/models/product_specification.rb', line 547
def self.express(raw_value, native_unit: nil, formatter: nil, locale: nil)
return raw_value unless native_unit
if native_unit == 'y' 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
return native_value_pretty if formatter.blank? || (locale.present? && locale.to_s.first(2) != 'en')
native_value_unitized = begin
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
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
|
542
543
544
|
# File 'app/models/product_specification.rb', line 542
def self.formatters_for_select
%w[FeetAndInches PoundsAndOunces ClosestRational]
end
|
.generate_name_from_token_and_grouping(token, grouping = nil) ⇒ Object
814
815
816
817
818
819
820
821
822
|
# File 'app/models/product_specification.rb', line 814
def self.generate_name_from_token_and_grouping(token, grouping = nil)
token = token.to_s
if grouping&.start_with?('X-')
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
803
804
805
806
807
808
809
810
811
812
|
# File 'app/models/product_specification.rb', line 803
def self.generate_token_from_name_and_grouping(name, grouping = nil)
tokenized_name = name.parameterize.underscore
if grouping&.start_with?('X-')
namespace = grouping.split('-').last.parameterize.underscore
tokenized_name = "#{namespace}_#{tokenized_name}"
end
tokenized_name
end
|
.grouping_select_options(locale = nil) ⇒ Object
450
451
452
453
454
455
|
# File 'app/models/product_specification.rb', line 450
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_keys ⇒ Object
413
414
415
|
# File 'app/models/product_specification.rb', line 413
def self.import_translation_keys
ProductSpecification.where.not(translations: {}).where(units: nil).find_each(&:import_translation_keys)
end
|
.locales_for_language(lang_code) ⇒ Object
239
240
241
|
# File 'app/models/product_specification.rb', line 239
def self.locales_for_language(lang_code)
translated_locales.select { |locale| locale.to_s.first(2) == lang_code.to_s }
end
|
.method_select_options ⇒ Object
359
360
361
|
# File 'app/models/product_specification.rb', line 359
def self.method_select_options
where.not(method: [nil, '']).order(:method).distinct.pluck(:method)
end
|
.methods_for_select ⇒ Object
500
501
502
|
# File 'app/models/product_specification.rb', line 500
def self.methods_for_select
SAFE_METHODS
end
|
.name_select_options(grouping: nil) ⇒ Object
436
437
438
439
440
|
# File 'app/models/product_specification.rb', line 436
def self.name_select_options(grouping: nil)
search_scope = ProductSpecification.order(:name)
search_scope = search_scope.where(grouping:) if grouping
[['All', nil]] + search_scope.distinct.pluck(:name).map { |name| [name, name] }
end
|
.next_position ⇒ Object
TODO, we have acts as list, don't need this!
403
404
405
|
# File 'app/models/product_specification.rb', line 403
def self.next_position
(maximum(:position) || 0) + 1
end
|
.options_scope(options = {}, _store_id = nil) ⇒ Object
386
387
388
389
390
391
392
|
# File 'app/models/product_specification.rb', line 386
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
|
A relation of ProductSpecifications that are positioned. Active Record Scope
230
|
# File 'app/models/product_specification.rb', line 230
scope :positioned, -> { order(:position) }
|
.product_category_select_options ⇒ Object
398
399
400
|
# File 'app/models/product_specification.rb', line 398
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_options ⇒ Object
394
395
396
|
# File 'app/models/product_specification.rb', line 394
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_select ⇒ Object
351
352
353
|
# File 'app/models/product_specification.rb', line 351
def self.propagations_for_select
ProductSpecification.propagations.keys.map { |p| [p.titleize, p] }
end
|
.readable_order ⇒ ActiveRecord::Relation<ProductSpecification>
A relation of ProductSpecifications that are readable order. Active Record Scope
197
198
199
200
201
202
|
# File 'app/models/product_specification.rb', line 197
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_options ⇒ Object
355
356
357
|
# File 'app/models/product_specification.rb', line 355
def self.token_select_options
where.not(token: [nil, '']).order(:token).distinct.pluck(:token)
end
|
.translated_lang_codes ⇒ Object
243
244
245
|
# File 'app/models/product_specification.rb', line 243
def self.translated_lang_codes
translated_locales.map { |l| l.to_s.first(2) }.uniq.sort.map(&:to_sym) - [:en]
end
|
.translated_locales ⇒ Object
234
235
236
237
|
# File 'app/models/product_specification.rb', line 234
def self.translated_locales
[] + EDITABLE_TRANSLATIONS.map(&:to_sym) - [I18n.default_locale]
end
|
.units_select_options(locale = Mobility.locale) ⇒ Object
425
426
427
428
429
|
# File 'app/models/product_specification.rb', line 425
def self.units_select_options(locale = Mobility.locale)
I18n.with_locale(locale) do
UnitHelper.all_units
end
end
|
.visibilities_for_select ⇒ Object
345
346
347
348
349
|
# File 'app/models/product_specification.rb', line 345
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_items ⇒ Object
667
668
669
670
671
672
673
674
675
676
677
|
# File 'app/models/product_specification.rb', line 667
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_ids ⇒ Object
Alias for Affected_items#ids
697
|
# File 'app/models/product_specification.rb', line 697
delegate :ids, to: :affected_items, prefix: true
|
#async_refresh_items ⇒ Object
703
704
705
706
707
|
# File 'app/models/product_specification.rb', line 703
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
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
|
# File 'app/models/product_specification.rb', line 288
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 locales.map(&:to_sym) & self.class.translated_locales
end
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
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
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_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
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.
687
688
689
690
691
692
693
694
695
|
# File 'app/models/product_specification.rb', line 687
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?(Liquid::Variable) ? lt : nil
end
|
#calculation_explanation ⇒ Hash{Symbol => Object, nil}
Explains how a dynamic spec's value is derived, for surfacing BEFORE an edit.
Returns nil for static (text) specs — those are safe to edit in place.
For calculated specs it names the input tokens and the formula so an editor
(human or agent) knows that editing text_blurb is a no-op for the rendered
value and that a contradictory value would break the relationship.
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
|
# File 'app/models/product_specification.rb', line 482
def calculation_explanation
return if text_method?
dep = CALCULATION_DEPENDENCIES[method] || inferred_calculation_dependency(method)
derives = dep ? dep[:derives_from] : []
note =
if derives.any?
"This spec is COMPUTED (method=#{method}); its rendered value is recalculated from " \
"#{derives.join(', ')}. Editing text_blurb will NOT change what items display and risks " \
"an inconsistency (e.g. breaking Ohm's law, W = V*A). To change a value, correct the input " \
'specs, or create an item-specific text override with clone_spec_to_item.'
else
"This spec is dynamic (method=#{method}); its value is pulled from the item, not text_blurb. " \
'Editing text_blurb only sets a fallback used when the computation returns nil.'
end
{ method: method, derives_from: derives, formula: dep && dep[:formula], note: note }
end
|
#dynamic_method? ⇒ Boolean
471
472
473
|
# File 'app/models/product_specification.rb', line 471
def dynamic_method?
!text_method?
end
|
#get_dynamic_spec_data(target, locale) ⇒ Object
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
|
# File 'app/models/product_specification.rb', line 605
def get_dynamic_spec_data(target, 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
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
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
|
# File 'app/models/product_specification.rb', line 623
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, "#{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 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?
values = if tokenizer_character.present?
spec_data.to_s.split(tokenizer_character)
else
[spec_data]
end
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
442
443
444
445
446
447
448
|
# File 'app/models/product_specification.rb', line 442
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
|
167
|
# File 'app/models/product_specification.rb', line 167
belongs_to :image, optional: true
|
#import_translation_keys(force: false) ⇒ Object
This method will import existing translations or link to existing ones
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
|
# File 'app/models/product_specification.rb', line 364
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}"
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 if (text = TranslationKey.newline_normalize(send(:"#{attr}_en"))).blank? next unless force || text&.match?(/[a-zA-Z]{3}/)
tk = TranslationKey.where(key: text, namespace: TRANSLATION_NAMESPACE).first_or_create!
tk.translations.where(locale: locale_string.first(2)).first_or_create!(text: translation)
tkrs << tk.translation_key_resources.where(resource: self, resource_attribute: attr).first_or_create!
end
end
tkrs
end
|
#items ⇒ ActiveRecord::Relation<Item>
170
|
# File 'app/models/product_specification.rb', line 170
has_and_belongs_to_many :items, inverse_of: :direct_product_specifications, autosave: false
|
#name_select_options(filter_grouping: nil) ⇒ Object
431
432
433
434
|
# File 'app/models/product_specification.rb', line 431
def name_select_options(filter_grouping: nil)
filter_grouping = grouping if grouping&.start_with?('X-')
self.class.name_select_options(grouping: filter_grouping)
end
|
165
|
# File 'app/models/product_specification.rb', line 165
belongs_to :product_category, optional: true
|
164
|
# File 'app/models/product_specification.rb', line 164
belongs_to :product_line, optional: true
|
#refresh_items ⇒ Object
709
710
711
712
713
714
|
# File 'app/models/product_specification.rb', line 709
def refresh_items
return if skip_spec_refresh
affected_items_ids.each { |iid| ItemAttributeWorker.perform_async(iid) }
affected_items_ids
end
|
#render_priority ⇒ Object
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.
507
508
509
|
# File 'app/models/product_specification.rb', line 507
def render_priority
text_method? ? 0 : 1
end
|
#rendering_priority ⇒ Object
461
462
463
464
465
466
467
468
469
|
# File 'app/models/product_specification.rb', line 461
def rendering_priority
if text_method? && !text_blurb_is_liquid?
0
elsif dynamic_method?
1
else
2
end
end
|
#set_has_liquid_blurb ⇒ Object
720
721
722
723
724
725
|
# File 'app/models/product_specification.rb', line 720
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?(Liquid::Variable)
end
end
|
#summary ⇒ Object
699
700
701
|
# File 'app/models/product_specification.rb', line 699
def summary
"#{product_line.slug_ltree} | #{product_category.url} | #{name} | #{method}"
end
|
166
|
# File 'app/models/product_specification.rb', line 166
belongs_to :template_product_specification, class_name: 'ProductSpecification', optional: true
|
#text_blurb_is_liquid? ⇒ Boolean
679
680
681
|
# File 'app/models/product_specification.rb', line 679
def text_blurb_is_liquid?
has_liquid_blurb?
end
|
#text_method? ⇒ Boolean
457
458
459
|
# File 'app/models/product_specification.rb', line 457
def text_method?
method == 'text'
end
|
#text_output(locale = Mobility.locale) ⇒ Object
519
520
521
522
523
|
# File 'app/models/product_specification.rb', line 519
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
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
|
# File 'app/models/product_specification.rb', line 525
def to_ruby_unit(locale = :en)
native_value_unitized = nil
Mobility.with_locale(locale) do
return if units.blank?
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_s ⇒ Object
511
512
513
|
# File 'app/models/product_specification.rb', line 511
def to_s
"#{name} (#{id})"
end
|
#translation_key_resources ⇒ ActiveRecord::Relation<TranslationKeyResource>
168
|
# File 'app/models/product_specification.rb', line 168
has_many :translation_key_resources, as: :resource, dependent: :destroy
|
#translation_keys ⇒ ActiveRecord::Relation<TranslationKey>
169
|
# File 'app/models/product_specification.rb', line 169
has_many :translation_keys, through: :translation_key_resources
|
#trim_empty_translations ⇒ Object
716
717
718
|
# File 'app/models/product_specification.rb', line 716
def trim_empty_translations
translations.delete_if { |_, value| !value.key?('text_blurb') || value['text_blurb'].blank? }
end
|
#unit_symbol ⇒ Object
515
516
517
|
# File 'app/models/product_specification.rb', line 515
def unit_symbol
UnitHelper.unit_symbol(units)
end
|
#units_select_options(locale = Mobility.locale) ⇒ Object
417
418
419
420
421
422
423
|
# File 'app/models/product_specification.rb', line 417
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
|