Class: ProductLine
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 =
%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'.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 =
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
Models::Auditable::ALWAYS_IGNORED
Models::Embeddable::MAX_CONTENT_LENGTH
Constants included
from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#do_not_compact_translation_container
#skip_ltree_rebuild
#creator, #updater
#parent
#tag_records, #taggings
#content_embeddings
#children
Has and belongs to many
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
#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
#compact_translation_container, skip_compaction_for
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
#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!
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
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
ransackable_associations, ransortable_attributes, #to_relation
config
#after_commit
#publish_event
Instance Attribute Details
#name_en ⇒ Object
Validations:
175
|
# File 'app/models/product_line.rb', line 175
validates :name_en, presence: true
|
#slug ⇒ Object
176
|
# File 'app/models/product_line.rb', line 176
validates :slug, presence: true
|
Class Method Details
.available_to_public ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are available to public. Active Record Scope
208
|
# File 'app/models/product_line.rb', line 208
scope :available_to_public, -> { where(available_to_public: true) }
|
.by_name_with_descendants ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by name with descendants. Active Record Scope
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_ltree ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by slug ltree. Active Record Scope
225
|
# File 'app/models/product_line.rb', line 225
scope :by_slug_ltree, ->(ltree_path) { where(slug_ltree: ltree_path) }
|
.by_slug_ltree_ancestors ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by slug ltree ancestors. Active Record Scope
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_descendants ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by slug ltree descendants. Active Record Scope
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_ancestors ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by url with ancestors. Active Record Scope
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_descendants ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by url with descendants. Active Record Scope
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_notice ⇒ Object
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
d.gsub!(/[\n\r\t]/, ' ')
d.squeeze!(' ')
d.squish!
d.truncate(char_limit, separator: /[\s|]/, omission: '') if char_limit
end
|
.for_index ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are for index. Active Record Scope
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_portal ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are for sales portal. Active Record Scope
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_portal ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are for support portal. Active Record Scope
233
|
# File 'app/models/product_line.rb', line 233
scope :for_support_portal, -> { where(show_in_support_portal: true) }
|
.heating_system_types ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are heating system types. Active Record Scope
242
|
# File 'app/models/product_line.rb', line 242
scope :heating_system_types, -> { where(is_heating_system_type: true) }
|
.main_product_lines ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are main product lines. Active Record Scope
243
|
# File 'app/models/product_line.rb', line 243
scope :main_product_lines, -> { where(is_main_product_line: true) }
|
.not_self_or_ancestor ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are not self or ancestor. Active Record Scope
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_models ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are past models. Active Record Scope
249
|
# File 'app/models/product_line.rb', line 249
scope :past_models, -> { with_any_tags('past-model') }
|
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
|
.prioritized ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are prioritized. Active Record Scope
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
|
.reviewable ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are reviewable. Active Record Scope
235
236
237
238
239
240
241
|
# File 'app/models/product_line.rb', line 235
scope :reviewable, -> {
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]
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_contains ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are slug ltree contains. Active Record Scope
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_select ⇒ Object
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_sorted ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are support portal sorted. Active Record Scope
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_select ⇒ Object
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
147
148
149
|
# File 'app/models/product_line.rb', line 147
def self.syncs_items_on_ltree_change?
true
end
|
.text_search_with_highlights ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are text search with highlights. Active Record Scope
255
|
# File 'app/models/product_line.rb', line 255
scope :text_search_with_highlights, ->(search_text) { text_search(search_text).with_pg_search_highlight }
|
.unrestricted ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are unrestricted. Active Record Scope
247
|
# File 'app/models/product_line.rb', line 247
scope :unrestricted, -> { where(restricted_for_sales: false) }
|
.with_ancestor_id ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are with ancestor id. Active Record Scope
.with_features ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are with features. Active Record Scope
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_items ⇒ Object
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_lines ⇒ Object
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_items ⇒ Object
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
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
|
#articles ⇒ ActiveRecord::Relation<Article>
197
|
# File 'app/models/product_line.rb', line 197
has_and_belongs_to_many :articles
|
#available_content_locales ⇒ Object
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_path ⇒ Object
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
|
#catalogs ⇒ ActiveRecord::Relation<Catalog>
195
|
# File 'app/models/product_line.rb', line 195
has_many :catalogs, through: :primary_items
|
#complimentary_product_lines ⇒ ActiveRecord::Relation<ProductLine>
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_select ⇒ Object
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_render ⇒ Object
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_name ⇒ Object
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
|
#description ⇒ Object
805
806
807
|
# File 'app/models/product_line.rb', line 805
def description
html_to_text(description_html)
end
|
#digital_asset_product_lines ⇒ ActiveRecord::Relation<DigitalAssetProductLine>
192
|
# File 'app/models/product_line.rb', line 192
has_many :digital_asset_product_lines, inverse_of: :product_line
|
#digital_assets ⇒ ActiveRecord::Relation<DigitalAsset>
193
|
# File 'app/models/product_line.rb', line 193
has_many :digital_assets, through: :digital_asset_product_lines
|
#display_name ⇒ Object
416
417
418
|
# File 'app/models/product_line.rb', line 416
def display_name
public_name.presence || name.presence
end
|
#easy_mat? ⇒ Boolean
710
711
712
|
# File 'app/models/product_line.rb', line 710
def easy_mat?
path_includes?('easy_mat')
end
|
780
781
782
783
784
785
|
# File 'app/models/product_line.rb', line 780
def
public_name.presence ||
short_description.presence ||
lineage_expanded&.gsub(' > ', ' - ') ||
name
end
|
#effective_public_name ⇒ Object
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_video ⇒ Object
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_uid ⇒ Object
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_keywords ⇒ Object
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_title ⇒ Object
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_description ⇒ Object
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_priority ⇒ Object
379
380
381
|
# File 'app/models/product_line.rb', line 379
def effective_support_category_priority
support_category_priority || 100
end
|
#effective_support_category_title ⇒ Object
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_title ⇒ Object
770
771
772
|
# File 'app/models/product_line.rb', line 770
def effective_support_title
support_title.presence || display_name
end
|
#effective_tag_line ⇒ Object
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
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
#environ_flex_roll? ⇒ Boolean
#facets ⇒ ActiveRecord::Relation<Facet>
185
|
# File 'app/models/product_line.rb', line 185
has_and_belongs_to_many :facets
|
#features ⇒ Object
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 fa = ancestors.find(&:has_features?)
fa&.features || []
end
end
|
#flex_roll? ⇒ Boolean
Heating system type predicates using ltree paths
706
707
708
|
# File 'app/models/product_line.rb', line 706
def flex_roll?
path_includes?('flex_roll')
end
|
#full_complimentary_product_line_ids ⇒ Object
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
rt_pl_ids = []
ancestors.each do |pl|
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_type ⇒ Object
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_public ⇒ Object
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_sales ⇒ Object
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_support ⇒ Object
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_reviewable ⇒ Object
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)
return @get_first_show_in_sales_portal_ancestor if instance_variable_defined?(:@get_first_show_in_sales_portal_ancestor) && !exclude_main_line && !exclude_self
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
@get_first_show_in_sales_portal_ancestor = result if !exclude_main_line && !exclude_self && result
result
end
|
#get_first_show_in_sales_portal_ancestor_id ⇒ Object
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_path ⇒ Object
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_ltree ⇒ Object
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_ancestor ⇒ Object
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_path ⇒ Object
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_ltree ⇒ Object
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_line ⇒ Object
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
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_options ⇒ ActiveRecord::Relation<HeatingElementProductLineOption>
191
|
# File 'app/models/product_line.rb', line 191
has_many :heating_element_product_line_options, inverse_of: :product_line
|
#heating_system_type_name ⇒ Object
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_configurations ⇒ ActiveRecord::Relation<RoomConfiguration>
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
766
767
768
|
# File 'app/models/product_line.rb', line 766
def is_cable_system?
path_includes?('cable')
end
|
#is_cork? ⇒ Boolean
Predicate methods using ltree paths for hierarchy checks
#is_led_mirror? ⇒ 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
#is_reviewable? ⇒ Boolean
509
510
511
512
513
514
515
516
|
# File 'app/models/product_line.rb', line 509
def is_reviewable?
return true if reviewable?
self.class.where(reviewable: true)
.where(ProductLine[:ltree_path_ids].ltree_ancestor(ltree_path_ids))
.exists?
end
|
#is_thermalsheet? ⇒ Boolean
#is_underlayment? ⇒ Boolean
#item_product_lines ⇒ ActiveRecord::Relation<ItemProductLine>
182
|
# File 'app/models/product_line.rb', line 182
has_many :item_product_lines, inverse_of: :product_line, dependent: :destroy
|
#items ⇒ ActiveRecord::Relation<Item>
183
|
# File 'app/models/product_line.rb', line 183
has_many :items, through: :item_product_lines
|
#main_line_slug_ltree ⇒ Object
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 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_select ⇒ Object
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
403
404
405
|
# File 'app/models/product_line.rb', line 403
def past_model?
tags.include?('past-model')
end
|
#primary_image ⇒ Image
180
|
# File 'app/models/product_line.rb', line 180
belongs_to :primary_image, class_name: 'Image', optional: true
|
#primary_image_inherited ⇒ Object
518
519
520
|
# File 'app/models/product_line.rb', line 518
def primary_image_inherited
primary_image || parent&.primary_image_inherited
end
|
#primary_items ⇒ ActiveRecord::Relation<Item>
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_categories ⇒ ActiveRecord::Relation<ProductCategory>
202
|
# File 'app/models/product_line.rb', line 202
has_and_belongs_to_many :product_categories
|
#product_specifications ⇒ ActiveRecord::Relation<ProductSpecification>
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_translations ⇒ Object
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_cache ⇒ Object
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_cache ⇒ Object
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_video ⇒ Video
205
|
# File 'app/models/product_line.rb', line 205
belongs_to :quote_builder_video, class_name: 'Video', optional: true
|
#root_system_product_line ⇒ Object
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_id ⇒ Object
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_ltree ⇒ Object
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_ids ⇒ Object
#self_and_inherited_specifications ⇒ Object
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
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
|
#showcases ⇒ ActiveRecord::Relation<Showcase>
184
|
# File 'app/models/product_line.rb', line 184
has_many :showcases
|
#site_maps ⇒ ActiveRecord::Relation<SiteMap>
194
|
# File 'app/models/product_line.rb', line 194
has_many :site_maps, as: :resource, dependent: :destroy
|
#slug_not_reserved_filter_slug ⇒ Object
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_source ⇒ Object
-- FriendlyId ----------------------------------------------------------
893
894
895
|
# File 'app/models/product_line.rb', line 893
def slug_source
name_en
end
|
#snow_melt? ⇒ 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
#snow_melt_mat? ⇒ Boolean
#snow_melt_or_slab_heat? ⇒ 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
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_cable? ⇒ Boolean
#tempzone_easy_mat? ⇒ Boolean
#tempzone_flex_roll? ⇒ Boolean
#tempzone_thin_cable? ⇒ Boolean
#to_s ⇒ Object
484
485
486
|
# File 'app/models/product_line.rb', line 484
def to_s
lineage_expanded || generate_full_name
end
|
#url_paths ⇒ Object
319
320
321
|
# File 'app/models/product_line.rb', line 319
def url_paths
slug_ltree.to_s.split('.')
end
|
#vignette_room ⇒ RoomConfiguration
204
|
# File 'app/models/product_line.rb', line 204
belongs_to :vignette_room, class_name: 'RoomConfiguration', optional: true
|
#warm_cache ⇒ Object
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
|