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 :enum default("popularity"), not null
support_portal_should_use_images :boolean default(FALSE), not null
support_priority :integer default(1000)
support_sub_lines_sort_method :enum 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 =

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 =

Translation namespace.

'ItemAttributes'.freeze
IQ_SUPPORTED_HEATING_SYSTEMS =

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 =

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 =

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',
  'infrared_heating_panels.ember' => 'Infrared Heating 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::MAX_CONTENT_LENGTH

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

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 Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#name_enObject (readonly)

Validations

Validations:



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

validates :name_en, presence: true

#slugObject (readonly)



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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.



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'app/models/product_line.rb', line 351

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



675
676
677
# File 'app/models/product_line.rb', line 675

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



792
793
794
795
796
797
798
799
800
801
802
803
# File 'app/models/product_line.rb', line 792

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.photo_tags_for_selectObject



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

def self.photo_tags_for_select
  ProductLine.where('cardinality(photofeed_tags) > 0 and photofeed_tags IS NOT NULL').pluck(:photofeed_tags).flatten.uniq.compact_blank.compact.sort_by(&:downcase)
end

.prioritizedActiveRecord::Relation<ProductLine>

A relation of ProductLines that are prioritized. Active Record Scope

Returns:

See Also:



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

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).



263
264
265
# File 'app/models/product_line.rb', line 263

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

.ransackable_scopes(_auth_object = nil) ⇒ Object



257
258
259
# File 'app/models/product_line.rb', line 257

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:



235
236
237
238
239
240
241
# File 'app/models/product_line.rb', line 235

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



274
275
276
277
278
279
280
281
282
283
284
# File 'app/models/product_line.rb', line 274

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:



267
268
269
270
271
272
# File 'app/models/product_line.rb', line 267

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



480
481
482
# File 'app/models/product_line.rb', line 480

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:



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

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

.support_sub_lines_sort_method_for_selectObject



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

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)


147
148
149
# File 'app/models/product_line.rb', line 147

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:



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

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:



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

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:



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

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:



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

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

Instance Method Details

#all_my_itemsObject



435
436
437
# File 'app/models/product_line.rb', line 435

def all_my_items
  Item.where(primary_product_line_id: self_and_descendants_ids)
end

#all_my_items_all_product_linesObject



439
440
441
# File 'app/models/product_line.rb', line 439

def all_my_items_all_product_lines
  Item.by_product_line_id(id)
end

#all_my_support_itemsObject



443
444
445
446
447
448
449
# File 'app/models/product_line.rb', line 443

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)


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

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:



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

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.



852
853
854
# File 'app/models/product_line.rb', line 852

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.



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/models/product_line.rb', line 332

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.filter_map { |_, s, _, _, _| s }.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"



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

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

#catalogsActiveRecord::Relation<Catalog>

Returns:

  • (ActiveRecord::Relation<Catalog>)

See Also:



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

has_many :catalogs, through: :primary_items

#complimentary_product_linesActiveRecord::Relation<ProductLine>

Returns:

See Also:



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

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



470
471
472
473
474
# File 'app/models/product_line.rb', line 470

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



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
# File 'app/models/product_line.rb', line 856

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].compact_blank
  parts << "Features: #{features.join(' | ')}" if features.any?

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

  tag_names = tags.compact_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.



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

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

#decorated_product_line_nameObject



420
421
422
423
424
425
426
427
428
429
# File 'app/models/product_line.rb', line 420

def decorated_product_line_name
  dn = name_en.dup
  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



805
806
807
# File 'app/models/product_line.rb', line 805

def description
  html_to_text(description_html)
end

#digital_asset_product_linesActiveRecord::Relation<DigitalAssetProductLine>

Returns:

See Also:



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

has_many :digital_asset_product_lines, inverse_of: :product_line

#digital_assetsActiveRecord::Relation<DigitalAsset>

Returns:

See Also:



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

has_many :digital_assets, through: :digital_asset_product_lines

#display_nameObject



416
417
418
# File 'app/models/product_line.rb', line 416

def display_name
  public_name.presence || name.presence
end

#easy_mat?Boolean

Returns:

  • (Boolean)


710
711
712
# File 'app/models/product_line.rb', line 710

def easy_mat?
  path_includes?('easy_mat')
end

#effective_header_titleObject



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

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



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

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



537
538
539
# File 'app/models/product_line.rb', line 537

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



541
542
543
# File 'app/models/product_line.rb', line 541

def effective_quote_builder_video_uid
  effective_quote_builder_video&.cloudflare_uid
end

#effective_seo_description(char_limit: SEO_META_DESCRIPTION_MAX_LENGTH) ⇒ Object



787
788
789
790
# File 'app/models/product_line.rb', line 787

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



309
310
311
312
313
314
315
316
317
# File 'app/models/product_line.rb', line 309

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.filter_map(&:presence)
end

#effective_seo_titleObject



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

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



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

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



379
380
381
# File 'app/models/product_line.rb', line 379

def effective_support_category_priority
  support_category_priority || 100
end

#effective_support_category_titleObject



383
384
385
# File 'app/models/product_line.rb', line 383

def effective_support_category_title
  support_category_title.presence || display_name
end

#effective_support_titleObject



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

def effective_support_title
  support_title.presence || display_name
end

#effective_tag_lineObject



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

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)


877
878
879
880
881
882
883
884
885
886
887
888
889
# File 'app/models/product_line.rb', line 877

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)


738
739
740
# File 'app/models/product_line.rb', line 738

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

#environ_flex_roll?Boolean

Returns:

  • (Boolean)


734
735
736
# File 'app/models/product_line.rb', line 734

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

#facetsActiveRecord::Relation<Facet>

Returns:

  • (ActiveRecord::Relation<Facet>)

See Also:



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

has_and_belongs_to_many :facets

#featuresObject



407
408
409
410
411
412
413
414
# File 'app/models/product_line.rb', line 407

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

#flex_roll?Boolean

Heating system type predicates using ltree paths

Returns:

  • (Boolean)


706
707
708
# File 'app/models/product_line.rb', line 706

def flex_roll?
  path_includes?('flex_roll')
end

#full_complimentary_product_line_idsObject



545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'app/models/product_line.rb', line 545

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



633
634
635
636
637
# File 'app/models/product_line.rb', line 633

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



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

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



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

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



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

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



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

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



578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'app/models/product_line.rb', line 578

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
  return @get_first_show_in_sales_portal_ancestor if instance_variable_defined?(:@get_first_show_in_sales_portal_ancestor) && !exclude_main_line && !exclude_self

  # 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



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

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



612
613
614
# File 'app/models/product_line.rb', line 612

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



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

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



602
603
604
# File 'app/models/product_line.rb', line 602

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



617
618
619
# File 'app/models/product_line.rb', line 617

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



607
608
609
# File 'app/models/product_line.rb', line 607

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



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

def get_main_product_line
  root
end

#has_features?Boolean

Returns:

  • (Boolean)


399
400
401
# File 'app/models/product_line.rb', line 399

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:



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

has_many :heating_element_product_line_options, inverse_of: :product_line

#heating_system_type_nameObject



667
668
669
670
671
672
673
# File 'app/models/product_line.rb', line 667

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:



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

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)


766
767
768
# File 'app/models/product_line.rb', line 766

def is_cable_system?
  path_includes?('cable')
end

#is_cerazorb?Boolean

Returns:

  • (Boolean)


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

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)


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

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

#is_led_mirror?Boolean

Returns:

  • (Boolean)


679
680
681
# File 'app/models/product_line.rb', line 679

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

#is_made_to_order_led_mirror?Boolean

Returns:

  • (Boolean)


505
506
507
# File 'app/models/product_line.rb', line 505

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

#is_prodeso?Boolean

Returns:

  • (Boolean)


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

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

#is_reviewable?Boolean

Returns:

  • (Boolean)


509
510
511
512
513
514
515
516
# File 'app/models/product_line.rb', line 509

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)


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

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

#is_underlayment?Boolean

Returns:

  • (Boolean)


501
502
503
# File 'app/models/product_line.rb', line 501

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

#item_product_linesActiveRecord::Relation<ItemProductLine>

Returns:

See Also:



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

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

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

has_many :items, through: :item_product_lines

#main_line_slug_ltreeObject



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

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.



390
391
392
393
394
395
396
397
# File 'app/models/product_line.rb', line 390

def move_to_new_parent(new_product_line_id)
  item_ids = all_my_items.ids
  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



901
902
903
# File 'app/models/product_line.rb', line 901

def normalize_friendly_id(value)
  value.to_s.parameterize
end

#parent_options_for_selectObject



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

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)


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

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

#primary_imageImage

Associations

Returns:

See Also:



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

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

#primary_image_inheritedObject



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

def primary_image_inherited
  primary_image || parent&.primary_image_inherited
end

#primary_itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

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:



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

has_and_belongs_to_many :product_categories

#product_specificationsActiveRecord::Relation<ProductSpecification>

Returns:

See Also:



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

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

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



817
818
819
820
821
# File 'app/models/product_line.rb', line 817

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.



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

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



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

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

#purge_edge_cacheObject



295
296
297
298
299
300
301
302
303
# File 'app/models/product_line.rb', line 295

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:



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

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

#roof_deicing?Boolean

Returns:

  • (Boolean)


762
763
764
# File 'app/models/product_line.rb', line 762

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.



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

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

#root_system_product_line_idObject



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

def root_system_product_line_id
  root_system_product_line&.id
end

#root_system_product_line_slug_ltreeObject



574
575
576
# File 'app/models/product_line.rb', line 574

def root_system_product_line_slug_ltree
  root_system_product_line&.slug_ltree
end

#self_and_descendants_product_category_idsObject



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

def self_and_descendants_product_category_ids
  ProductCategory.self_and_descendants_ids(product_category_ids)
end

#self_and_inherited_specificationsObject



455
456
457
# File 'app/models/product_line.rb', line 455

def self_and_inherited_specifications
  ProductSpecification.where(product_line_id: self_and_ancestors_ids)
end

#should_generate_new_friendly_id?Boolean

Returns:

  • (Boolean)


897
898
899
# File 'app/models/product_line.rb', line 897

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

#showcasesActiveRecord::Relation<Showcase>

Returns:

See Also:



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

has_many :showcases

#site_mapsActiveRecord::Relation<SiteMap>

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



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

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

#slab_heat?Boolean

Returns:

  • (Boolean)


754
755
756
# File 'app/models/product_line.rb', line 754

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

#slug_not_reserved_filter_slugObject



905
906
907
908
909
910
911
912
913
# File 'app/models/product_line.rb', line 905

def slug_not_reserved_filter_slug
  return if slug.blank?

  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 ----------------------------------------------------------



893
894
895
# File 'app/models/product_line.rb', line 893

def slug_source
  name_en
end

#snow_melt?Boolean

Returns:

  • (Boolean)


742
743
744
# File 'app/models/product_line.rb', line 742

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

#snow_melt_cable?Boolean

Returns:

  • (Boolean)


750
751
752
# File 'app/models/product_line.rb', line 750

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

#snow_melt_mat?Boolean

Returns:

  • (Boolean)


746
747
748
# File 'app/models/product_line.rb', line 746

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

#snow_melt_or_slab_heat?Boolean

Returns:

  • (Boolean)


758
759
760
# File 'app/models/product_line.rb', line 758

def snow_melt_or_slab_heat?
  snow_melt? || slab_heat?
end

#supported_in_iq?Boolean

Returns:

  • (Boolean)


809
810
811
# File 'app/models/product_line.rb', line 809

def supported_in_iq?
  IQ_SUPPORTED_HEATING_SYSTEMS.include?(heating_system_type_name)
end

#tempzone?Boolean

Returns:

  • (Boolean)


714
715
716
# File 'app/models/product_line.rb', line 714

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

#tempzone_cable?Boolean

Returns:

  • (Boolean)


726
727
728
# File 'app/models/product_line.rb', line 726

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

#tempzone_easy_mat?Boolean

Returns:

  • (Boolean)


722
723
724
# File 'app/models/product_line.rb', line 722

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

#tempzone_flex_roll?Boolean

Returns:

  • (Boolean)


718
719
720
# File 'app/models/product_line.rb', line 718

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

#tempzone_thin_cable?Boolean

Returns:

  • (Boolean)


730
731
732
# File 'app/models/product_line.rb', line 730

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

#to_sObject



484
485
486
# File 'app/models/product_line.rb', line 484

def to_s
  lineage_expanded || generate_full_name
end

#url_pathsObject



319
320
321
# File 'app/models/product_line.rb', line 319

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

#vignette_roomRoomConfiguration

Returns:

  • (RoomConfiguration)

See Also:



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

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



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

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

  site_maps.each(&:warm_cache)
end