Class: ProductLine

Inherits:
ApplicationRecord show all
Extended by:
FriendlyId
Includes:
Memery, Models::Auditable, Models::Embeddable, Models::LtreeLineage, Models::LtreePathBuilder, Models::Taggable, Models::Translatable, Models::Utilities::Html, PgSearch::Model
Defined in:
app/models/product_line.rb

Overview

== Schema Information

Table name: product_lines
Database name: primary

id :integer not null, primary key
app_code :string
available_to_public :boolean default(TRUE), not null
blog_tag :string(255)
cached_ancestor_ids :integer default([]), is an Array
children_count :integer
description_html :text
feature_1 :string
feature_2 :string
feature_3 :string
feature_4 :string
feature_5 :string
first_sale_date :date
inactive :boolean default(FALSE), not null
is_heating_system_type :boolean default(FALSE), not null
is_main_product_line :boolean default(FALSE), not null
last_sale_date :date
legacy_url :string(255)
level :integer
lineage_expanded :string(255)
ltree_path_ids :ltree
ltree_path_slugs :ltree
name :string(255)
path :string
photofeed_tags :string(255) default([]), not null, is an Array
popularity :integer default(0)
popularity_offset :integer default(0)
priority :integer default(100)
public_name :string
repairability_notice :text
restricted_for_sales :boolean default(FALSE), not null
reviewable :boolean default(FALSE), not null
seo_description :text
seo_keywords :string
seo_support_description :string(255)
seo_support_title :string(255)
seo_title :string
short_description :string
show_in_sales_portal :boolean default(FALSE), not null
show_in_support_portal :boolean default(TRUE), not null
slug :string
slug_ltree :ltree
support_category_priority :integer default(100)
support_category_title :string
support_intro :text
support_items_sort_method :integer default("popularity"), not null
support_portal_should_use_images :boolean default(FALSE), not null
support_priority :integer default(1000)
support_sub_lines_sort_method :integer default("default"), not null
support_title :string
tag_line :string
translations :jsonb
created_at :datetime
updated_at :datetime
default_product_category_id :integer
item_image_id :integer
lifestyle_image_id :integer
parent_id :integer
primary_image_id :integer
quote_builder_video_id :bigint
vignette_room_id :integer

Indexes

by_ismpl_atp_sisp (is_main_product_line,available_to_public,show_in_sales_portal)
by_ismpl_sisp (is_main_product_line,show_in_support_portal)
idx_id_is_heating_system_type (id,is_heating_system_type)
idx_id_reviewable (id,reviewable)
idx_id_show_in_support_portal (id,show_in_support_portal)
idx_id_sisp_atp (id,show_in_sales_portal,available_to_public)
idx_product_lines_ltree_path_ids (ltree_path_ids) USING gist
idx_product_lines_ltree_path_slugs (ltree_path_slugs) USING gist
idx_product_lines_slug_ltree (slug_ltree) USING gist
index_product_lines_on_app_code (app_code)
index_product_lines_on_cached_ancestor_ids (cached_ancestor_ids) USING gin
index_product_lines_on_legacy_url (legacy_url) UNIQUE
index_product_lines_on_name (name)
index_product_lines_on_parent_id (parent_id)
index_product_lines_on_parent_id_and_slug (parent_id,slug) UNIQUE
index_product_lines_on_quote_builder_video_id (quote_builder_video_id)
index_product_lines_on_reviewable (reviewable)
index_product_lines_on_root_slug (slug) UNIQUE WHERE (parent_id IS NULL)
index_product_lines_on_show_in_support_portal (show_in_support_portal)
index_product_lines_on_vignette_room_id (vignette_room_id)
product_lines_default_product_category_id_idx (default_product_category_id)

Foreign Keys

fk_rails_... (default_product_category_id => product_categories.id)
fk_rails_... (quote_builder_video_id => digital_assets.id)

Defined Under Namespace

Classes: CacheFlusher, ContentOptimizer, ImageRetriever, PublicationRetriever, SpecificationsMasher, VideoRetriever

Constant Summary collapse

TRANSLATABLE_ATTRIBUTES =
%i[description_html feature_1 feature_2 feature_3 feature_4 feature_5
name public_name repairability_notice seo_description seo_keywords seo_support_description seo_support_title
seo_title short_description support_category_title support_intro support_title tag_line].freeze
TRANSLATION_NAMESPACE =
'ItemAttributes'
IQ_SUPPORTED_HEATING_SYSTEMS =
['TempZone Flex Roll', 'TempZone Easy Mat', 'TempZone Shower Mat', 'TempZone Cable', 'TempZone Thin Cable',
'Environ Easy Mat', 'Slab Heat Cable', 'Slab Heat Mat', 'Snow Melt Cable', 'Snow Melt Mat', 'Environ Flex Roll'].freeze
CONTROL_PRODUCT_LINE_ID =
31
RESERVED_SECTION_SLUGS =

Slugs reserved by the catalog URL resolver as section suffixes.
A ProductLine with any of these as its slug would be unreachable.

%w[support reviews faqs].freeze
RESERVED_FILTER_SLUGS =

All filter slugs that could collide with product line slugs under towel-warmer

(
  TowelWarmerFilterSlugs::FINISH_SLUGS.keys +
  TowelWarmerFilterSlugs::COMPOSITE_FINISH_SLUGS.keys +
  TowelWarmerFilterSlugs::STYLE_SLUGS.keys +
  TowelWarmerFilterSlugs::CONNECTION_SLUGS.keys +
  TowelWarmerFilterSlugs::MOUNTING_SLUGS.keys +
  TowelWarmerFilterSlugs::SIZE_SLUGS
).freeze
HEATING_SYSTEM_TYPE_NAMES =
{
  'countertop_heater' => 'Countertop Heater',
  'floor_heating.environ.easy_mat' => 'Environ Easy Mat',
  'floor_heating.environ.flex_roll' => 'Environ Flex Roll',
  'floor_heating.slab_heat.cable' => 'Slab Heat Cable',
  'floor_heating.slab_heat.mat' => 'Slab Heat Mat',
  'floor_heating.tempzone.cable' => 'TempZone Cable',
  'floor_heating.tempzone.ruler_cable' => 'TempZone Ruler Cable',
  'floor_heating.tempzone.custom_mat' => 'TempZone Custom Mat',
  'floor_heating.tempzone.easy_mat' => 'TempZone Easy Mat',
  'floor_heating.tempzone.flex_roll' => 'TempZone Flex Roll',
  'floor_heating.tempzone.shower_mat.bench' => 'Shower Mat Bench',
  'floor_heating.tempzone.shower_mat.floor' => 'Shower Mat Floor',
  'floor_heating.tempzone.shower_mat' => 'Shower Mat',
  'floor_heating.tempzone.thin_cable' => 'TempZone Thin Cable',
  'pipe_freeze_protection.constant_wattage_cable' => 'Pipe Freeze Protection Constant Wattage Cable',
  'pipe_freeze_protection.self_regulating_cable' => 'Pipe Freeze Protection Self Regulating Cable',
  'radiant_panel.ember' => 'Radiant Panel Ember',
  'roof_and_gutter_deicing.constant_wattage_pre_assembled_plug_in_kits' => 'Roof & Gutter Deicing Plug-in Kits',
  'roof_and_gutter_deicing.self_regulating_cut_to_length_cable' => 'Roof & Gutter Deicing Cable',
  'snow_melting.cable' => 'Snow Melt Cable',
  'snow_melting.mat.powermat' => 'Snow Melt PowerMat',
  'snow_melting.mat.omnimat' => 'Snow Melt OmniMat',
  'snow_melting.mat.ecomat' => 'Snow Melt EcoMat',
  'snow_melting.mat' => 'Snow Melt Mat'
}.freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Embeddable

Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH

Instance Attribute Summary collapse

Attributes included from Models::Translatable

#do_not_compact_translation_container

Attributes included from Models::LtreePathBuilder

#skip_ltree_rebuild

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Methods included from Models::LtreeLineage

#parent

Has many collapse

Methods included from Models::Taggable

#tag_records, #taggings

Methods included from Models::Embeddable

#content_embeddings

Methods included from Models::LtreeLineage

#children

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Taggable

#add_tag, all_tags, #has_tag?, normalize_tag_names, not_tagged_with, #remove_tag, #tag_list, #tag_list=, #taggable_type_for_tagging, tagged_with, #tags, #tags=, tags_cloud, tags_exclude, tags_include, with_all_tags, with_any_tags, without_all_tags, without_any_tags

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 included from Models::LtreePathBuilder

#build_ltree_path_ids_value, #build_ltree_path_slugs_value, #build_slug_ltree_value, builds_ltree_path, #compute_ltree_ancestor_ids, #derive_cached_ancestor_ids, #ltree_descendant_ids, rebuild_all_ltree_paths!, rebuild_subtree_ltree_paths!

Methods included from Models::Embeddable

embeddable_content_types, #embeddable_locales, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #locale_for_embedding, #needs_chunking?, regenerate_all_embeddings, semantic_search

Methods included from Models::LtreeLineage

acts_as_ltree_lineage, ancestors_ids, #ancestors_ids, define_ltree_scopes, define_ordered_ltree_methods, #descendant_of_path?, descendants_ids, #descendants_ids, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #ltree_ancestor_of?, #ltree_descendant_of?, #ltree_slug, #ltree_slug_path, #path_includes?, #root, #root?, #root_id, root_ids, self_ancestors_and_descendants_ids, #self_ancestors_and_descendants_ids, self_and_ancestors_ids, #self_and_ancestors_ids, #self_and_children, self_and_descendants_ids, #self_and_descendants_ids, #self_and_siblings, #siblings

Methods inherited from ApplicationRecord

ransackable_associations, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#name_enObject (readonly)

Validations

Validations:



166
# File 'app/models/product_line.rb', line 166

validates :name_en, presence: true

#slugObject (readonly)



167
# File 'app/models/product_line.rb', line 167

validates :slug, presence: true

Class Method Details

.available_to_publicActiveRecord::Relation<ProductLine>

A relation of ProductLines that are available to public. Active Record Scope

Returns:

See Also:



199
# File 'app/models/product_line.rb', line 199

scope :available_to_public,     -> { where(available_to_public: true) }

.by_name_with_descendantsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are by name with descendants. Active Record Scope

Returns:

See Also:



202
203
204
205
# File 'app/models/product_line.rb', line 202

scope :by_name_with_descendants, ->(pl_n) {
  path = where(name: pl_n).pick(:ltree_path_ids)
  path ? where(ProductLine[:ltree_path_ids].ltree_descendant(path)) : none
}

.by_slug_ltreeActiveRecord::Relation<ProductLine>

A relation of ProductLines that are by slug ltree. Active Record Scope

Returns:

See Also:



216
# File 'app/models/product_line.rb', line 216

scope :by_slug_ltree,              ->(ltree_path) { where(slug_ltree: ltree_path) }

.by_slug_ltree_ancestorsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are by slug ltree ancestors. Active Record Scope

Returns:

See Also:



218
# File 'app/models/product_line.rb', line 218

scope :by_slug_ltree_ancestors,    ->(ltree_path) { where(arel_table[:slug_ltree].ltree_ancestor(ltree_path)) }

.by_slug_ltree_descendantsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are by slug ltree descendants. Active Record Scope

Returns:

See Also:



217
# File 'app/models/product_line.rb', line 217

scope :by_slug_ltree_descendants,  ->(ltree_path) { where(arel_table[:slug_ltree].ltree_descendant(ltree_path)) }

.by_url_with_ancestorsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are by url with ancestors. Active Record Scope

Returns:

See Also:



210
211
212
213
# File 'app/models/product_line.rb', line 210

scope :by_url_with_ancestors, ->(slug_or_url) {
  slug = LtreePaths.slug_ltree_from_legacy_hyphen_url(slug_or_url)
  slug.present? ? by_slug_ltree_ancestors(slug) : none
}

.by_url_with_descendantsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are by url with descendants. Active Record Scope

Returns:

See Also:



206
207
208
209
# File 'app/models/product_line.rb', line 206

scope :by_url_with_descendants, ->(slug_or_url) {
  slug = LtreePaths.slug_ltree_from_legacy_hyphen_url(slug_or_url)
  slug.present? ? by_slug_ltree_descendants(slug) : none
}

.canonical_paths_for(product_lines) ⇒ Object

Batch-precompute canonical_path for a collection to avoid N+1.
Returns a Hash of { product_line_id => canonical_path_string }.
Skips non-public intermediary product lines, consistent with #canonical_path.



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'app/models/product_line.rb', line 342

def self.canonical_paths_for(product_lines)
  return {} if product_lines.blank?

  all_ids = product_lines.flat_map { |pl| Array(pl.cached_ancestor_ids) + [pl.id] }.uniq
  rows = ProductLine.where(id: all_ids).pluck(:id, :slug, :level, :available_to_public, :is_main_product_line)

  public_ids = Set.new
  slugs_map = {}
  levels_map = {}
  rows.each do |row_id, row_slug, row_level, row_public, row_main|
    slugs_map[row_id] = row_slug
    levels_map[row_id] = row_level
    public_ids << row_id if row_public || row_main
  end

  product_lines.each_with_object({}) do |pl, result|
    ids = (Array(pl.cached_ancestor_ids) + [pl.id]).select { |pid| public_ids.include?(pid) }
    path = ids.sort_by { |pid| levels_map[pid] || 0 }.filter_map { |pid| slugs_map[pid] }.join('/')
    result[pl.id] = path.presence
  end
end

.default_repairability_noticeObject



669
670
671
# File 'app/models/product_line.rb', line 669

def self.default_repairability_notice
  @default_repairability_notice ||= I18n.t('products.notices.default_quebec_repairability.text', default: nil).presence
end

.effective_seo_description(product_line, char_limit: SEO_META_DESCRIPTION_MAX_LENGTH) ⇒ Object



786
787
788
789
790
791
792
793
794
795
796
797
# File 'app/models/product_line.rb', line 786

def self.effective_seo_description(product_line, char_limit: SEO_META_DESCRIPTION_MAX_LENGTH)
  d = product_line.seo_description.presence
  d ||= product_line.short_description.presence
  d ||= product_line.description.presence
  return unless d

  # We replace by spaces first in case line feed is used as a sentence separator.
  d.gsub!(/[\n\r\t]/, ' ')
  d.squeeze!(' ')
  d.squish!
  d.truncate(char_limit, separator: /[\s|]/, omission: '') if char_limit
end

.for_indexActiveRecord::Relation<ProductLine>

A relation of ProductLines that are for index. Active Record Scope

Returns:

See Also:



235
# File 'app/models/product_line.rb', line 235

scope :for_index,               -> { available_to_public.where(is_main_product_line: false, show_in_sales_portal: true).order(:priority, :slug_ltree) }

.for_sales_portalActiveRecord::Relation<ProductLine>

A relation of ProductLines that are for sales portal. Active Record Scope

Returns:

See Also:



237
# File 'app/models/product_line.rb', line 237

scope :for_sales_portal,         -> { where(available_to_public: true, show_in_sales_portal: true) }

.for_support_portalActiveRecord::Relation<ProductLine>

A relation of ProductLines that are for support portal. Active Record Scope

Returns:

See Also:



224
# File 'app/models/product_line.rb', line 224

scope :for_support_portal,      -> { where(show_in_support_portal: true) }

.heating_system_typesActiveRecord::Relation<ProductLine>

A relation of ProductLines that are heating system types. Active Record Scope

Returns:

See Also:



233
# File 'app/models/product_line.rb', line 233

scope :heating_system_types,    -> { where(is_heating_system_type: true) }

.main_product_linesActiveRecord::Relation<ProductLine>

A relation of ProductLines that are main product lines. Active Record Scope

Returns:

See Also:



234
# File 'app/models/product_line.rb', line 234

scope :main_product_lines,      -> { where(is_main_product_line: true) }

.not_self_or_ancestorActiveRecord::Relation<ProductLine>

A relation of ProductLines that are not self or ancestor. Active Record Scope

Returns:

See Also:



220
221
222
223
# File 'app/models/product_line.rb', line 220

scope :not_self_or_ancestor,    ->(pl) {
  plids = [pl.id.to_i] + pl.ancestors.map(&:id)
  where(ProductLine[:id].not_in(plids))
}

.past_modelsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are past models. Active Record Scope

Returns:

See Also:



240
# File 'app/models/product_line.rb', line 240

scope :past_models,              -> { with_any_tags('past-model') }

.photo_tags_for_selectObject



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

def self.photo_tags_for_select
  ProductLine.where('cardinality(photofeed_tags) > 0 and photofeed_tags IS NOT NULL').pluck(:photofeed_tags).flatten.uniq.select do |e|
    e.present?
  end.compact.sort_by(&:downcase)
end

.prioritizedActiveRecord::Relation<ProductLine>

A relation of ProductLines that are prioritized. Active Record Scope

Returns:

See Also:



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

scope :prioritized,             -> { order(:priority) }

.ransackable_attributes(_auth_object = nil) ⇒ Object

Exclude ltree-typed columns from generic _cont / _eq predicates — they produce
operator does not exist: ltree ~~* unknown when Ransack applies ILIKE (AppSignal #4449).



254
255
256
# File 'app/models/product_line.rb', line 254

def self.ransackable_attributes(_auth_object = nil)
  super - %w[slug_ltree ltree_path_ids ltree_path_slugs]
end

.ransackable_scopes(_auth_object = nil) ⇒ Object



248
249
250
# File 'app/models/product_line.rb', line 248

def self.ransackable_scopes(_auth_object = nil)
  %i[with_ancestor_id text_search_with_highlights slug_ltree_contains]
end

.reviewableActiveRecord::Relation<ProductLine>

A relation of ProductLines that are reviewable. Active Record Scope

Returns:

See Also:



226
227
228
229
230
231
232
# File 'app/models/product_line.rb', line 226

scope :reviewable,              -> {
  # Use ltree to get all descendants of reviewable product lines in one query
  reviewable_paths = where(reviewable: true).pluck(:ltree_path_ids).compact
  return none if reviewable_paths.empty?

  where(ProductLine[:ltree_path_ids].ltree_descendant(reviewable_paths))
}

.select_options(options = {}) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
# File 'app/models/product_line.rb', line 265

def self.select_options(options = {})
  res = ProductLine.all
  res = res.by_name_with_descendants(options[:name]) if options[:name]
  res = res.by_slug_ltree_descendants(options[:slug_ltree]) if options[:slug_ltree]
  res = res.reviewable if options[:reviewable]
  res = res.heating_system_types if options[:is_heating_system_type]
  # NOTE: non_binders option removed - no binder product lines exist
  res = res.for_sales_portal if options[:for_sales_portal]
  res = yield(res) if block_given?
  res.map { |pl| [pl.lineage_expanded.presence || pl.name_en, pl.id] }.sort_by { |pl| pl[0].to_s }
end

.slug_ltree_containsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are slug ltree contains. Active Record Scope

Returns:

See Also:



258
259
260
261
262
263
# File 'app/models/product_line.rb', line 258

scope :slug_ltree_contains, ->(term) {
  sanitized = term.to_s.strip.tr(' ', '_').gsub(/[^a-zA-Z0-9_.]/, '')
  return none if sanitized.blank?

  where(arel_table[:slug_ltree].ltree_matches("*.#{sanitized}*.*"))
}

.support_items_sort_method_for_selectObject



473
474
475
# File 'app/models/product_line.rb', line 473

def self.support_items_sort_method_for_select
  support_items_sort_methods.keys.map { |k| [k.to_s.titleize, k] }
end

.support_portal_sortedActiveRecord::Relation<ProductLine>

A relation of ProductLines that are support portal sorted. Active Record Scope

Returns:

See Also:



225
# File 'app/models/product_line.rb', line 225

scope :support_portal_sorted,   -> { for_support_portal.reorder(:support_priority, ProductLine[:popularity].desc) }

.support_sub_lines_sort_method_for_selectObject



469
470
471
# File 'app/models/product_line.rb', line 469

def self.support_sub_lines_sort_method_for_select
  support_sub_lines_sort_methods.keys.map { |k| [k.to_s.titleize, k] }
end

.syncs_items_on_ltree_change?Boolean

Enable item ltree sync when product line paths change

Returns:

  • (Boolean)


142
143
144
# File 'app/models/product_line.rb', line 142

def self.syncs_items_on_ltree_change?
  true
end

.text_search_with_highlightsActiveRecord::Relation<ProductLine>

A relation of ProductLines that are text search with highlights. Active Record Scope

Returns:

See Also:



246
# File 'app/models/product_line.rb', line 246

scope :text_search_with_highlights, ->(search_text) { text_search(search_text).with_pg_search_highlight }

.unrestrictedActiveRecord::Relation<ProductLine>

A relation of ProductLines that are unrestricted. Active Record Scope

Returns:

See Also:



238
# File 'app/models/product_line.rb', line 238

scope :unrestricted,             -> { where(restricted_for_sales: false) }

.with_ancestor_idActiveRecord::Relation<ProductLine>

A relation of ProductLines that are with ancestor id. Active Record Scope

Returns:

See Also:



236
# File 'app/models/product_line.rb', line 236

scope :with_ancestor_id, ->(pl_ids) { where(id: ProductLine.self_and_descendants_ids([pl_ids].flatten.uniq.compact)) }

.with_featuresActiveRecord::Relation<ProductLine>

A relation of ProductLines that are with features. Active Record Scope

Returns:

See Also:



239
# File 'app/models/product_line.rb', line 239

scope :with_features,            -> { where.not(feature_1: nil, feature_2: nil, feature_3: nil, feature_4: nil, feature_5: nil) }

Instance Method Details

#all_my_itemsObject



428
429
430
# File 'app/models/product_line.rb', line 428

def all_my_items
  Item.where(primary_product_line_id: self_and_descendants_ids)
end

#all_my_items_all_product_linesObject



432
433
434
# File 'app/models/product_line.rb', line 432

def all_my_items_all_product_lines
  Item.by_product_line_id(id)
end

#all_my_support_itemsObject



436
437
438
439
440
441
442
# File 'app/models/product_line.rb', line 436

def all_my_support_items
  items = all_my_items
  items = items.by_product_category_id(product_category_ids) if product_category_ids.present?
  items.goods_visible_for_support
       .includes(:taggings, primary_product_line: :taggings)
       .order(:sku)
end

#any_translatable_attribute_changed?Boolean

We detect if any of the translatable attributes have changed, this can be used
to trigger auto translate or cleanup functionalities

Returns:

  • (Boolean)


825
826
827
# File 'app/models/product_line.rb', line 825

def any_translatable_attribute_changed?
  TRANSLATABLE_ATTRIBUTES.any? { |attr| send(:"#{attr}_changed?") || send(:"saved_change_to_#{attr}?") }
end

#articlesActiveRecord::Relation<Article>

Returns:

  • (ActiveRecord::Relation<Article>)

See Also:



188
# File 'app/models/product_line.rb', line 188

has_and_belongs_to_many :articles

#available_content_localesObject

Find locales used by all the items of this product line, this means this product line and its descendants
This information is stored in the catalogs.



846
847
848
# File 'app/models/product_line.rb', line 846

def available_content_locales
  Catalog.joins(catalog_items: :item).merge(all_my_items).merge(CatalogItem.not_discontinued).pluck(:locales).flatten.compact.uniq.sort
end

#canonical_pathObject

Hierarchical slug path built from FriendlyId slug chain, including only
public-facing product lines (available_to_public or is_main_product_line).
Internal organizational nodes (e.g. "twin-conductor", "120-v") are skipped
to produce shorter, cleaner SEO URLs.

e.g. "floor-heating/tempzone/flex-roll" (skips twin-conductor/120-v)

Uses cached_ancestor_ids (array stored on the record) to batch-load
ancestor data in a single query instead of walking the tree.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'app/models/product_line.rb', line 323

def canonical_path
  @canonical_path ||= begin
    ancestor_ids = Array(cached_ancestor_ids)
    path = if ancestor_ids.present?
             rows = ProductLine.where(id: ancestor_ids + [id])
                      .pluck(:id, :slug, :level, :available_to_public, :is_main_product_line)
             rows.select! { |_, _, _, pub, main| pub || main }
             rows.sort_by! { |_, _, lvl, _, _| lvl }
             rows.map { |_, s, _, _, _| s }.compact.join('/')
           else
             slug.to_s
           end
    path.presence
  end
end

#canonical_url(locale: I18n.locale) ⇒ Object

Canonical URL with locale prefix.
e.g. "/en-US/floor-heating/tempzone/flex-roll"



366
367
368
# File 'app/models/product_line.rb', line 366

def canonical_url(locale: I18n.locale)
  "/#{locale}/#{canonical_path}"
end

#catalogsActiveRecord::Relation<Catalog>

Returns:

  • (ActiveRecord::Relation<Catalog>)

See Also:



186
# File 'app/models/product_line.rb', line 186

has_many :catalogs, through: :primary_items

#complimentary_product_linesActiveRecord::Relation<ProductLine>

Returns:

See Also:



189
190
# File 'app/models/product_line.rb', line 189

has_and_belongs_to_many :complimentary_product_lines, class_name: 'ProductLine', join_table: :complimentary_product_lines,
association_foreign_key: :complimentary_product_line_id

#complimentary_product_lines_for_selectObject



463
464
465
466
467
# File 'app/models/product_line.rb', line 463

def complimentary_product_lines_for_select
  ProductLine.not_self_or_ancestor(self)
             .order(:lineage_expanded)
             .pluck(:lineage_expanded, :id)
end

#content_for_embedding(_content_type = :primary) ⇒ Object



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
# File 'app/models/product_line.rb', line 850

def content_for_embedding(_content_type = :primary)
  parts = []
  parts << lineage_expanded.presence
  parts << "Public name: #{effective_public_name}" if effective_public_name.present? && effective_public_name != name
  parts << "Tag line: #{effective_tag_line}" if effective_tag_line.present?
  parts << "Summary: #{effective_short_description}" if effective_short_description.present?

  desc_text = Nokogiri::HTML(description_html.to_s).text.gsub(/\s+/, ' ').strip
  parts << "Description: #{desc_text}" if desc_text.present?

  features = [feature_1, feature_2, feature_3, feature_4, feature_5].select(&:present?)
  parts << "Features: #{features.join(' | ')}" if features.any?

  parts << "SEO: #{seo_description}" if seo_description.present?

  tag_names = tags.reject(&:blank?)
  parts << "Tags: #{tag_names.join(', ')}" if tag_names.any?

  parts.compact.join("\n").presence
end

#content_locales_to_renderObject

Which locales should we render for this product line? This is the intersection of the
configured locales for mobility and the default locale + the locales used by the catalogs.



840
841
842
# File 'app/models/product_line.rb', line 840

def content_locales_to_render
  (Mobility.available_locales & ([I18n.default_locale] + available_content_locales)).uniq
end

#decorated_product_line_nameObject



411
412
413
414
415
416
417
418
419
420
# File 'app/models/product_line.rb', line 411

def decorated_product_line_name
  dn = name_en
  dn << '' if is_main_product_line
  dn << '' if reviewable
  dn << '' if is_heating_system_type
  dn << '' if available_to_public
  dn << ' $' if show_in_sales_portal
  dn << '' if show_in_support_portal
  dn
end

#descriptionObject



799
800
801
# File 'app/models/product_line.rb', line 799

def description
  html_to_text(description_html)
end

#digital_asset_product_linesActiveRecord::Relation<DigitalAssetProductLine>

Returns:

See Also:



183
# File 'app/models/product_line.rb', line 183

has_many :digital_asset_product_lines, inverse_of: :product_line

#digital_assetsActiveRecord::Relation<DigitalAsset>

Returns:

See Also:



184
# File 'app/models/product_line.rb', line 184

has_many :digital_assets, through: :digital_asset_product_lines

#display_nameObject



407
408
409
# File 'app/models/product_line.rb', line 407

def display_name
  public_name.presence || name.presence
end

#easy_mat?Boolean

Returns:

  • (Boolean)


704
705
706
# File 'app/models/product_line.rb', line 704

def easy_mat?
  path_includes?('easy_mat')
end

#effective_header_titleObject



774
775
776
777
778
779
# File 'app/models/product_line.rb', line 774

def effective_header_title
  public_name.presence ||
    short_description.presence ||
    lineage_expanded&.gsub(' > ', ' - ') ||
    name
end

#effective_public_nameObject

Quote Builder effective methods - climb ltree for inheritance
These use self_and_ancestors (ltree-powered) to find first non-null value



518
519
520
# File 'app/models/product_line.rb', line 518

def effective_public_name
  public_name.presence || self_and_ancestors.where.not(public_name: [nil, '']).order(level: :desc).pick(:public_name) || name
end

#effective_quote_builder_videoObject



530
531
532
# File 'app/models/product_line.rb', line 530

def effective_quote_builder_video
  quote_builder_video || self_and_ancestors.where.not(quote_builder_video_id: nil).order(level: :desc).first&.quote_builder_video
end

#effective_quote_builder_video_uidObject



534
535
536
# File 'app/models/product_line.rb', line 534

def effective_quote_builder_video_uid
  effective_quote_builder_video&.cloudflare_uid
end

#effective_seo_description(char_limit: SEO_META_DESCRIPTION_MAX_LENGTH) ⇒ Object



781
782
783
784
# File 'app/models/product_line.rb', line 781

def effective_seo_description(char_limit: SEO_META_DESCRIPTION_MAX_LENGTH)
  self.class.effective_seo_description(self, char_limit:) ||
    self.class.effective_seo_description(get_first_reviewable || root, char_limit:)
end

#effective_seo_keywordsObject



300
301
302
303
304
305
306
307
308
# File 'app/models/product_line.rb', line 300

def effective_seo_keywords
  return seo_keywords if seo_keywords.present?

  plr = get_first_reviewable
  return plr.seo_keywords if plr && plr.seo_keywords.present?

  keywords = generate_full_name_array + [public_name]
  keywords.map(&:presence).compact
end

#effective_seo_titleObject



768
769
770
771
772
# File 'app/models/product_line.rb', line 768

def effective_seo_title
  pn = seo_title.presence || public_name.presence
  pn ||= lineage_expanded.gsub(' > ', ' ')
  pn.truncate(SEO_TITLE_MAX_LENGTH, separator: /\s/, omission: '')
end

#effective_short_descriptionObject



526
527
528
# File 'app/models/product_line.rb', line 526

def effective_short_description
  short_description.presence || self_and_ancestors.where.not(short_description: [nil, '']).order(level: :desc).pick(:short_description)
end

#effective_support_category_priorityObject



370
371
372
# File 'app/models/product_line.rb', line 370

def effective_support_category_priority
  support_category_priority || 100
end

#effective_support_category_titleObject



374
375
376
# File 'app/models/product_line.rb', line 374

def effective_support_category_title
  support_category_title.presence || display_name
end

#effective_support_titleObject



764
765
766
# File 'app/models/product_line.rb', line 764

def effective_support_title
  support_title.presence || display_name
end

#effective_tag_lineObject



522
523
524
# File 'app/models/product_line.rb', line 522

def effective_tag_line
  tag_line.presence || self_and_ancestors.where.not(tag_line: [nil, '']).order(level: :desc).pick(:tag_line)
end

#embedding_content_changed?Boolean

Returns:

  • (Boolean)


871
872
873
874
875
876
877
878
879
880
881
882
883
# File 'app/models/product_line.rb', line 871

def embedding_content_changed?
  saved_change_to_name? ||
    saved_change_to_public_name? ||
    saved_change_to_description_html? ||
    saved_change_to_short_description? ||
    saved_change_to_tag_line? ||
    saved_change_to_feature_1? ||
    saved_change_to_feature_2? ||
    saved_change_to_feature_3? ||
    saved_change_to_feature_4? ||
    saved_change_to_feature_5? ||
    saved_change_to_seo_description?
end

#environ_easy_mat?Boolean

Returns:

  • (Boolean)


732
733
734
# File 'app/models/product_line.rb', line 732

def environ_easy_mat?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_ENVIRON_EASY_MAT)
end

#environ_flex_roll?Boolean

Returns:

  • (Boolean)


728
729
730
# File 'app/models/product_line.rb', line 728

def environ_flex_roll?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_ENVIRON_FLEX_ROLL)
end

#facetsActiveRecord::Relation<Facet>

Returns:

  • (ActiveRecord::Relation<Facet>)

See Also:



176
# File 'app/models/product_line.rb', line 176

has_and_belongs_to_many :facets

#featuresObject



398
399
400
401
402
403
404
405
# File 'app/models/product_line.rb', line 398

def features
  if has_features?
    [feature_1, feature_2, feature_3, feature_4, feature_5].map(&:presence).compact
  else # Return the first ancestor with features or nothing
    fa = ancestors.detect(&:has_features?)
    fa&.features || []
  end
end

#flex_roll?Boolean

Heating system type predicates using ltree paths

Returns:

  • (Boolean)


700
701
702
# File 'app/models/product_line.rb', line 700

def flex_roll?
  path_includes?('flex_roll')
end

#full_complimentary_product_line_idsObject



538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'app/models/product_line.rb', line 538

def full_complimentary_product_line_ids
  Rails.cache.fetch([cache_key, :complimentary_product_lines], expires_in: 1.month) do
    # Start with the product line selected and all its descendants
    rt_pl_ids = []
    # Now to evaluate the complimentary lines, we climb the tree up and down
    ancestors.each do |pl|
      # Then each complimentary line is added back in + all the children
      rt_pl_ids << pl.id
      pl.complimentary_product_lines.each do |plc|
        rt_pl_ids += plc.self_and_descendants_ids
      end
    end
    rt_pl_ids += self_and_descendants_ids
    rt_pl_ids.uniq
  end
end

#get_first_heating_system_typeObject



629
630
631
632
# File 'app/models/product_line.rb', line 629

def get_first_heating_system_type
  return @get_first_heating_system_type if instance_variable_defined?(:@get_first_heating_system_type)
  @get_first_heating_system_type = self_and_ancestors.where(is_heating_system_type: true).order('level desc, priority asc').first
end

#get_first_publicObject



681
682
683
684
685
# File 'app/models/product_line.rb', line 681

def get_first_public
  return self if available_to_public?

  self_and_ancestors.where(available_to_public: true).order(level: :desc, priority: :asc).first
end

#get_first_public_for_salesObject



687
688
689
690
691
# File 'app/models/product_line.rb', line 687

def get_first_public_for_sales
  return self if available_to_public? && !is_main_product_line? && show_in_sales_portal?

  self_and_ancestors.where(available_to_public: true, is_main_product_line: false, show_in_sales_portal: true).order(level: :desc, priority: :asc).first
end

#get_first_public_for_supportObject



693
694
695
696
697
# File 'app/models/product_line.rb', line 693

def get_first_public_for_support
  return self if available_to_public? && !is_main_product_line? && show_in_support_portal?

  self_and_ancestors.where(available_to_public: true, is_main_product_line: false, show_in_support_portal: true).order(level: :desc, priority: :asc).first
end

#get_first_reviewableObject



624
625
626
627
# File 'app/models/product_line.rb', line 624

def get_first_reviewable
  return @get_first_reviewable if instance_variable_defined?(:@get_first_reviewable)
  @get_first_reviewable = self_and_ancestors.where(reviewable: true).order('level desc, priority asc').first
end

#get_first_show_in_sales_portal_ancestor(exclude_main_line: false, exclude_self: false) ⇒ Object



573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'app/models/product_line.rb', line 573

def get_first_show_in_sales_portal_ancestor(exclude_main_line: false, exclude_self: false)
  # Check for pre-populated value first (set by preload_product_type_roots optimization)
  # Only use pre-populated value if it matches the exclude_main_line criteria
  if instance_variable_defined?(:@get_first_show_in_sales_portal_ancestor) && !exclude_main_line && !exclude_self
    return @get_first_show_in_sales_portal_ancestor
  end
  
  # Exclude the main lines from this check, e.g towel-warmer wouldn't be valid
  scope = (exclude_self ? ancestors : self_and_ancestors).for_sales_portal
  scope = scope.where.not(is_main_product_line: true) if exclude_main_line
  result = scope.order('level desc, priority asc').first
  
  # Cache the result if using default parameters
  @get_first_show_in_sales_portal_ancestor = result if !exclude_main_line && !exclude_self && result
  
  result
end

#get_first_show_in_sales_portal_ancestor_idObject



591
592
593
# File 'app/models/product_line.rb', line 591

def get_first_show_in_sales_portal_ancestor_id
  get_first_show_in_sales_portal_ancestor&.id
end

#get_first_show_in_sales_portal_ancestor_pathObject

Returns ltree path for the first ancestor shown in sales portal



609
610
611
# File 'app/models/product_line.rb', line 609

def get_first_show_in_sales_portal_ancestor_path
  get_first_show_in_sales_portal_ancestor&.ltree_path_slugs
end

#get_first_show_in_sales_portal_ancestor_slug_ltreeObject



595
596
597
# File 'app/models/product_line.rb', line 595

def get_first_show_in_sales_portal_ancestor_slug_ltree
  get_first_show_in_sales_portal_ancestor&.slug_ltree
end

#get_first_show_in_support_portal_ancestorObject



599
600
601
# File 'app/models/product_line.rb', line 599

def get_first_show_in_support_portal_ancestor
  self_and_ancestors.where(show_in_support_portal: true).order('level desc, priority asc').first
end

#get_first_show_in_support_portal_ancestor_pathObject

Returns ltree path for the first ancestor shown in support portal



614
615
616
# File 'app/models/product_line.rb', line 614

def get_first_show_in_support_portal_ancestor_path
  get_first_show_in_support_portal_ancestor&.ltree_path_slugs
end

#get_first_show_in_support_portal_ancestor_slug_ltreeObject



604
605
606
# File 'app/models/product_line.rb', line 604

def get_first_show_in_support_portal_ancestor_slug_ltree
  get_first_show_in_support_portal_ancestor&.slug_ltree
end

#get_main_product_lineObject

Returns the root/main product line (top of hierarchy)
Uses ltree path to find the root ancestor



620
621
622
# File 'app/models/product_line.rb', line 620

def get_main_product_line
  root
end

#has_features?Boolean

Returns:

  • (Boolean)


390
391
392
# File 'app/models/product_line.rb', line 390

def has_features?
  [feature_1, feature_2, feature_3, feature_4, feature_5].any?(&:present?)
end

#heating_element_product_line_optionsActiveRecord::Relation<HeatingElementProductLineOption>

Returns:

See Also:



182
# File 'app/models/product_line.rb', line 182

has_many :heating_element_product_line_options, inverse_of: :product_line

#heating_system_type_nameObject



661
662
663
664
665
666
667
# File 'app/models/product_line.rb', line 661

def heating_system_type_name
  ltree = slug_ltree.to_s
  HEATING_SYSTEM_TYPE_NAMES.each do |prefix, name|
    return name if ltree.start_with?(prefix)
  end
  display_name || lineage_expanded&.titleize || slug.to_s.tr('-', ' ').titleize
end

#installed_room_configurationsActiveRecord::Relation<RoomConfiguration>

Returns:

  • (ActiveRecord::Relation<RoomConfiguration>)

See Also:



191
192
# File 'app/models/product_line.rb', line 191

has_and_belongs_to_many :installed_room_configurations, class_name: 'RoomConfiguration', join_table: :installed_product_lines,
association_foreign_key: :room_configuration_id

#is_cable_system?Boolean

Returns:

  • (Boolean)


760
761
762
# File 'app/models/product_line.rb', line 760

def is_cable_system?
  path_includes?('cable')
end

#is_cerazorb?Boolean

Returns:

  • (Boolean)


486
487
488
# File 'app/models/product_line.rb', line 486

def is_cerazorb?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT_CERAZORB)
end

#is_cork?Boolean

Predicate methods using ltree paths for hierarchy checks

Returns:

  • (Boolean)


482
483
484
# File 'app/models/product_line.rb', line 482

def is_cork?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT_CORK)
end

#is_led_mirror?Boolean

Returns:

  • (Boolean)


673
674
675
# File 'app/models/product_line.rb', line 673

def is_led_mirror?
  descendant_of_path?(LtreePaths::PL_LED_MIRROR)
end

#is_made_to_order_led_mirror?Boolean

Returns:

  • (Boolean)


498
499
500
# File 'app/models/product_line.rb', line 498

def is_made_to_order_led_mirror?
  descendant_of_path?(LtreePaths::PL_LED_MIRROR_MADE_TO_ORDER)
end

#is_prodeso?Boolean

Returns:

  • (Boolean)


677
678
679
# File 'app/models/product_line.rb', line 677

def is_prodeso?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT_PRODESO)
end

#is_reviewable?Boolean

Returns:

  • (Boolean)


502
503
504
505
506
507
508
509
# File 'app/models/product_line.rb', line 502

def is_reviewable?
  return true if reviewable?

  # Check if any ancestor is reviewable using ltree
  self.class.where(reviewable: true)
      .where(ProductLine[:ltree_path_ids].ltree_ancestor(ltree_path_ids))
      .exists?
end

#is_thermalsheet?Boolean

Returns:

  • (Boolean)


490
491
492
# File 'app/models/product_line.rb', line 490

def is_thermalsheet?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT_THERMALSHEET)
end

#is_underlayment?Boolean

Returns:

  • (Boolean)


494
495
496
# File 'app/models/product_line.rb', line 494

def is_underlayment?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT)
end

#item_product_linesActiveRecord::Relation<ItemProductLine>

Returns:

See Also:



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

has_many :item_product_lines, inverse_of: :product_line, dependent: :destroy

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



174
# File 'app/models/product_line.rb', line 174

has_many :items, through: :item_product_lines

#main_line_slug_ltreeObject



452
453
454
# File 'app/models/product_line.rb', line 452

def main_line_slug_ltree
  root.slug_ltree if root&.is_main_product_line
end

#move_to_new_parent(new_product_line_id) ⇒ Object

Convenience method to move a product line to a new parent
while you can just update parent_id too, this will also refresh/touch items and their specs
associated with this product line.



381
382
383
384
385
386
387
388
# File 'app/models/product_line.rb', line 381

def move_to_new_parent(new_product_line_id)
  item_ids = all_my_items.pluck(:id)
  self.parent_id = new_product_line_id
  save # This in turns updates lineage on all children
  # Async update all items
  item_ids.each { |iid| ItemAttributeWorker.perform_async(iid) }
  { new_slug_ltree: slug_ltree, item_queued_for_update: item_ids.size }
end

#normalize_friendly_id(value) ⇒ Object



895
896
897
# File 'app/models/product_line.rb', line 895

def normalize_friendly_id(value)
  value.to_s.parameterize
end

#parent_options_for_selectObject



456
457
458
459
460
461
# File 'app/models/product_line.rb', line 456

def parent_options_for_select
  pl = ProductLine.all
  pl = pl.where(ProductLine[:id].not_eq(id)) unless new_record?
  pl = pl.order(:lineage_expanded)
  pl.pluck(:lineage_expanded, :id)
end

#past_model?Boolean

Returns:

  • (Boolean)


394
395
396
# File 'app/models/product_line.rb', line 394

def past_model?
  tags.include?('past-model')
end

#primary_imageImage

Associations

Returns:

See Also:



171
# File 'app/models/product_line.rb', line 171

belongs_to :primary_image, class_name: 'Image', optional: true

#primary_image_inheritedObject



511
512
513
# File 'app/models/product_line.rb', line 511

def primary_image_inherited
  primary_image || (parent ? parent.primary_image_inherited : nil)
end

#primary_itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



177
178
179
180
181
# File 'app/models/product_line.rb', line 177

has_many :primary_items,
inverse_of: :primary_product_line,
class_name: 'Item',
foreign_key: :primary_product_line_id,
dependent: :nullify

#product_categoriesActiveRecord::Relation<ProductCategory>

Returns:

See Also:



193
# File 'app/models/product_line.rb', line 193

has_and_belongs_to_many :product_categories

#product_specificationsActiveRecord::Relation<ProductSpecification>

Returns:

See Also:



172
# File 'app/models/product_line.rb', line 172

has_many :product_specifications, inverse_of: :product_line, dependent: :destroy

#product_type(separator: nil, limit: nil) ⇒ Object



811
812
813
814
815
# File 'app/models/product_line.rb', line 811

def product_type(separator: nil, limit: nil)
  Feed::ProductTypeGenerator.process(product_line: self,
                                     separator:,
                                     limit:)
end

#prune_unused_translationsObject

We prune unused translations, this is done by removing all the locales that are not used
We know a locale is not used by querying the catalogs that are using it.



831
832
833
834
835
836
# File 'app/models/product_line.rb', line 831

def prune_unused_translations
  valid_locales = content_locales_to_render
  translations.each do |(locale, _translation_attribute)|
    translations.delete(locale) unless locale.to_sym.in?(valid_locales)
  end
end

#purge_cacheObject



807
808
809
# File 'app/models/product_line.rb', line 807

def purge_cache
  CacheWorker.perform_async('cache_class' => 'ProductLine::CacheFlusher', 'params' => id)
end

#purge_edge_cacheObject



286
287
288
289
290
291
292
293
294
# File 'app/models/product_line.rb', line 286

def purge_edge_cache
  res = []
  urls = self_and_descendants.flat_map { |pl| pl.site_maps.map(&:url) }
  if urls.present?
    jid = EdgeCacheWorker.perform_async('urls' => urls)
    res += urls.map { |url| { url:, jid: } }
  end
  res
end

#quote_builder_videoVideo

Returns:

See Also:



196
# File 'app/models/product_line.rb', line 196

belongs_to :quote_builder_video, class_name: 'Video', optional: true

#roof_deicing?Boolean

Returns:

  • (Boolean)


756
757
758
# File 'app/models/product_line.rb', line 756

def roof_deicing?
  descendant_of_path?(LtreePaths::PL_ROOF_GUTTER_DEICING)
end

#root_system_product_lineObject

We often check for the significant umbrella product line from which we will
derive our search for compatible accesories, controls, etc.



557
558
559
560
561
562
563
# File 'app/models/product_line.rb', line 557

def root_system_product_line
  @root_system_product_line ||= begin
    get_first_heating_system_type ||
      get_first_show_in_sales_portal_ancestor(exclude_main_line: false) ||
      get_first_reviewable
  end
end

#root_system_product_line_idObject



565
566
567
# File 'app/models/product_line.rb', line 565

def root_system_product_line_id
  root_system_product_line&.id
end

#root_system_product_line_slug_ltreeObject



569
570
571
# File 'app/models/product_line.rb', line 569

def root_system_product_line_slug_ltree
  root_system_product_line&.slug_ltree
end

#self_and_descendants_product_category_idsObject



444
445
446
# File 'app/models/product_line.rb', line 444

def self_and_descendants_product_category_ids
  ProductCategory.self_and_descendants_ids(product_category_ids)
end

#self_and_inherited_specificationsObject



448
449
450
# File 'app/models/product_line.rb', line 448

def self_and_inherited_specifications
  ProductSpecification.where(product_line_id: self_and_ancestors_ids)
end

#should_generate_new_friendly_id?Boolean

Returns:

  • (Boolean)


891
892
893
# File 'app/models/product_line.rb', line 891

def should_generate_new_friendly_id?
  slug.blank? || will_save_change_to_attribute?(:name_en)
end

#showcasesActiveRecord::Relation<Showcase>

Returns:

See Also:



175
# File 'app/models/product_line.rb', line 175

has_many :showcases

#site_mapsActiveRecord::Relation<SiteMap>

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



185
# File 'app/models/product_line.rb', line 185

has_many :site_maps, as: :resource, dependent: :destroy

#slab_heat?Boolean

Returns:

  • (Boolean)


748
749
750
# File 'app/models/product_line.rb', line 748

def slab_heat?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_SLAB_HEAT)
end

#slug_not_reserved_filter_slugObject



899
900
901
902
903
904
905
906
907
# File 'app/models/product_line.rb', line 899

def slug_not_reserved_filter_slug
  return unless slug.present?

  if slug.in?(RESERVED_SECTION_SLUGS)
    errors.add(:slug, "'#{slug}' is reserved as a catalog URL section keyword")
  elsif root&.slug == 'towel-warmer' && slug.in?(RESERVED_FILTER_SLUGS)
    errors.add(:slug, "conflicts with towel warmer filter slug '#{slug}'")
  end
end

#slug_sourceObject

-- FriendlyId ----------------------------------------------------------



887
888
889
# File 'app/models/product_line.rb', line 887

def slug_source
  name_en
end

#snow_melt?Boolean

Returns:

  • (Boolean)


736
737
738
# File 'app/models/product_line.rb', line 736

def snow_melt?
  descendant_of_path?(LtreePaths::PL_SNOW_MELTING)
end

#snow_melt_cable?Boolean

Returns:

  • (Boolean)


744
745
746
# File 'app/models/product_line.rb', line 744

def snow_melt_cable?
  descendant_of_path?(LtreePaths::PL_SNOW_MELTING_CABLE)
end

#snow_melt_mat?Boolean

Returns:

  • (Boolean)


740
741
742
# File 'app/models/product_line.rb', line 740

def snow_melt_mat?
  descendant_of_path?(LtreePaths::PL_SNOW_MELTING_MAT)
end

#snow_melt_or_slab_heat?Boolean

Returns:

  • (Boolean)


752
753
754
# File 'app/models/product_line.rb', line 752

def snow_melt_or_slab_heat?
  snow_melt? || slab_heat?
end

#supported_in_iq?Boolean

Returns:

  • (Boolean)


803
804
805
# File 'app/models/product_line.rb', line 803

def supported_in_iq?
  IQ_SUPPORTED_HEATING_SYSTEMS.include?(heating_system_type_name)
end

#tempzone?Boolean

Returns:

  • (Boolean)


708
709
710
# File 'app/models/product_line.rb', line 708

def tempzone?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE)
end

#tempzone_cable?Boolean

Returns:

  • (Boolean)


720
721
722
# File 'app/models/product_line.rb', line 720

def tempzone_cable?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CABLE)
end

#tempzone_easy_mat?Boolean

Returns:

  • (Boolean)


716
717
718
# File 'app/models/product_line.rb', line 716

def tempzone_easy_mat?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_EASY_MAT)
end

#tempzone_flex_roll?Boolean

Returns:

  • (Boolean)


712
713
714
# File 'app/models/product_line.rb', line 712

def tempzone_flex_roll?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_FLEX_ROLL)
end

#tempzone_thin_cable?Boolean

Returns:

  • (Boolean)


724
725
726
# File 'app/models/product_line.rb', line 724

def tempzone_thin_cable?
  descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_THIN_CABLE)
end

#to_sObject



477
478
479
# File 'app/models/product_line.rb', line 477

def to_s
  lineage_expanded || generate_full_name
end

#url_pathsObject



310
311
312
# File 'app/models/product_line.rb', line 310

def url_paths
  slug_ltree.to_s.split('.')
end

#vignette_roomRoomConfiguration

Returns:

  • (RoomConfiguration)

See Also:



195
# File 'app/models/product_line.rb', line 195

belongs_to :vignette_room, class_name: 'RoomConfiguration', optional: true

#warm_cacheObject

Legacy lineage cache removed - ltree queries are fast enough
See: doc/tasks/202512032250_LTREE_HIERARCHY_MIGRATION.md



280
281
282
283
284
# File 'app/models/product_line.rb', line 280

def warm_cache
  return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?

  site_maps.each(&:warm_cache)
end