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 :integer default("popularity"), not null
support_portal_should_use_images :boolean default(FALSE), not null
support_priority :integer default(1000)
support_sub_lines_sort_method :integer default("default"), not null
support_title :string
tag_line :string
translations :jsonb
created_at :datetime
updated_at :datetime
default_product_category_id :integer
item_image_id :integer
lifestyle_image_id :integer
parent_id :integer
primary_image_id :integer
quote_builder_video_id :bigint
vignette_room_id :integer
Indexes
by_ismpl_atp_sisp (is_main_product_line,available_to_public,show_in_sales_portal)
by_ismpl_sisp (is_main_product_line,show_in_support_portal)
idx_id_is_heating_system_type (id,is_heating_system_type)
idx_id_reviewable (id,reviewable)
idx_id_show_in_support_portal (id,show_in_support_portal)
idx_id_sisp_atp (id,show_in_sales_portal,available_to_public)
idx_product_lines_ltree_path_ids (ltree_path_ids) USING gist
idx_product_lines_ltree_path_slugs (ltree_path_slugs) USING gist
idx_product_lines_slug_ltree (slug_ltree) USING gist
index_product_lines_on_app_code (app_code)
index_product_lines_on_cached_ancestor_ids (cached_ancestor_ids) USING gin
index_product_lines_on_legacy_url (legacy_url) UNIQUE
index_product_lines_on_name (name)
index_product_lines_on_parent_id (parent_id)
index_product_lines_on_parent_id_and_slug (parent_id,slug) UNIQUE
index_product_lines_on_quote_builder_video_id (quote_builder_video_id)
index_product_lines_on_reviewable (reviewable)
index_product_lines_on_root_slug (slug) UNIQUE WHERE (parent_id IS NULL)
index_product_lines_on_show_in_support_portal (show_in_support_portal)
index_product_lines_on_vignette_room_id (vignette_room_id)
product_lines_default_product_category_id_idx (default_product_category_id)
Foreign Keys
fk_rails_... (default_product_category_id => product_categories.id)
fk_rails_... (quote_builder_video_id => digital_assets.id)
Defined Under Namespace
Classes: CacheFlusher, ContentOptimizer, ImageRetriever, PublicationRetriever, SpecificationsMasher, VideoRetriever
Constant Summary
collapse
- TRANSLATABLE_ATTRIBUTES =
%i[description_html feature_1 feature_2 feature_3 feature_4 feature_5
name public_name repairability_notice seo_description seo_keywords seo_support_description seo_support_title
seo_title short_description support_category_title support_intro support_title tag_line].freeze
- TRANSLATION_NAMESPACE =
'ItemAttributes'
- IQ_SUPPORTED_HEATING_SYSTEMS =
['TempZone Flex Roll', 'TempZone Easy Mat', 'TempZone Shower Mat', 'TempZone Cable', 'TempZone Thin Cable',
'Environ Easy Mat', 'Slab Heat Cable', 'Slab Heat Mat', 'Snow Melt Cable', 'Snow Melt Mat', 'Environ Flex Roll'].freeze
- CONTROL_PRODUCT_LINE_ID =
31
- RESERVED_SECTION_SLUGS =
Slugs reserved by the catalog URL resolver as section suffixes.
A ProductLine with any of these as its slug would be unreachable.
%w[support reviews faqs].freeze
- RESERVED_FILTER_SLUGS =
All filter slugs that could collide with product line slugs under towel-warmer
(
TowelWarmerFilterSlugs::FINISH_SLUGS.keys +
TowelWarmerFilterSlugs::COMPOSITE_FINISH_SLUGS.keys +
TowelWarmerFilterSlugs::STYLE_SLUGS.keys +
TowelWarmerFilterSlugs::CONNECTION_SLUGS.keys +
TowelWarmerFilterSlugs::MOUNTING_SLUGS.keys +
TowelWarmerFilterSlugs::SIZE_SLUGS
).freeze
- HEATING_SYSTEM_TYPE_NAMES =
{
'countertop_heater' => 'Countertop Heater',
'floor_heating.environ.easy_mat' => 'Environ Easy Mat',
'floor_heating.environ.flex_roll' => 'Environ Flex Roll',
'floor_heating.slab_heat.cable' => 'Slab Heat Cable',
'floor_heating.slab_heat.mat' => 'Slab Heat Mat',
'floor_heating.tempzone.cable' => 'TempZone Cable',
'floor_heating.tempzone.ruler_cable' => 'TempZone Ruler Cable',
'floor_heating.tempzone.custom_mat' => 'TempZone Custom Mat',
'floor_heating.tempzone.easy_mat' => 'TempZone Easy Mat',
'floor_heating.tempzone.flex_roll' => 'TempZone Flex Roll',
'floor_heating.tempzone.shower_mat.bench' => 'Shower Mat Bench',
'floor_heating.tempzone.shower_mat.floor' => 'Shower Mat Floor',
'floor_heating.tempzone.shower_mat' => 'Shower Mat',
'floor_heating.tempzone.thin_cable' => 'TempZone Thin Cable',
'pipe_freeze_protection.constant_wattage_cable' => 'Pipe Freeze Protection Constant Wattage Cable',
'pipe_freeze_protection.self_regulating_cable' => 'Pipe Freeze Protection Self Regulating Cable',
'radiant_panel.ember' => 'Radiant Panel Ember',
'roof_and_gutter_deicing.constant_wattage_pre_assembled_plug_in_kits' => 'Roof & Gutter Deicing Plug-in Kits',
'roof_and_gutter_deicing.self_regulating_cut_to_length_cable' => 'Roof & Gutter Deicing Cable',
'snow_melting.cable' => 'Snow Melt Cable',
'snow_melting.mat.powermat' => 'Snow Melt PowerMat',
'snow_melting.mat.omnimat' => 'Snow Melt OmniMat',
'snow_melting.mat.ecomat' => 'Snow Melt EcoMat',
'snow_melting.mat' => 'Snow Melt Mat'
}.freeze
Models::Auditable::ALWAYS_IGNORED
Models::Embeddable::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH
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
#publish_event
Instance Attribute Details
#name_en ⇒ Object
Validations:
166
|
# File 'app/models/product_line.rb', line 166
validates :name_en, presence: true
|
#slug ⇒ Object
167
|
# File 'app/models/product_line.rb', line 167
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
199
|
# File 'app/models/product_line.rb', line 199
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
202
203
204
205
|
# File 'app/models/product_line.rb', line 202
scope :by_name_with_descendants, ->(pl_n) {
path = where(name: pl_n).pick(:ltree_path_ids)
path ? where(ProductLine[:ltree_path_ids].ltree_descendant(path)) : none
}
|
.by_slug_ltree ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by slug ltree. Active Record Scope
216
|
# File 'app/models/product_line.rb', line 216
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
218
|
# File 'app/models/product_line.rb', line 218
scope :by_slug_ltree_ancestors, ->(ltree_path) { where(arel_table[:slug_ltree].ltree_ancestor(ltree_path)) }
|
.by_slug_ltree_descendants ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by slug ltree descendants. Active Record Scope
217
|
# File 'app/models/product_line.rb', line 217
scope :by_slug_ltree_descendants, ->(ltree_path) { where(arel_table[:slug_ltree].ltree_descendant(ltree_path)) }
|
.by_url_with_ancestors ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by url with ancestors. Active Record Scope
210
211
212
213
|
# File 'app/models/product_line.rb', line 210
scope :by_url_with_ancestors, ->(slug_or_url) {
slug = LtreePaths.slug_ltree_from_legacy_hyphen_url(slug_or_url)
slug.present? ? by_slug_ltree_ancestors(slug) : none
}
|
.by_url_with_descendants ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are by url with descendants. Active Record Scope
206
207
208
209
|
# File 'app/models/product_line.rb', line 206
scope :by_url_with_descendants, ->(slug_or_url) {
slug = LtreePaths.slug_ltree_from_legacy_hyphen_url(slug_or_url)
slug.present? ? by_slug_ltree_descendants(slug) : none
}
|
.canonical_paths_for(product_lines) ⇒ Object
Batch-precompute canonical_path for a collection to avoid N+1.
Returns a Hash of { product_line_id => canonical_path_string }.
Skips non-public intermediary product lines, consistent with #canonical_path.
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
|
# File 'app/models/product_line.rb', line 342
def self.canonical_paths_for(product_lines)
return {} if product_lines.blank?
all_ids = product_lines.flat_map { |pl| Array(pl.cached_ancestor_ids) + [pl.id] }.uniq
rows = ProductLine.where(id: all_ids).pluck(:id, :slug, :level, :available_to_public, :is_main_product_line)
public_ids = Set.new
slugs_map = {}
levels_map = {}
rows.each do |row_id, row_slug, row_level, row_public, row_main|
slugs_map[row_id] = row_slug
levels_map[row_id] = row_level
public_ids << row_id if row_public || row_main
end
product_lines.each_with_object({}) do |pl, result|
ids = (Array(pl.cached_ancestor_ids) + [pl.id]).select { |pid| public_ids.include?(pid) }
path = ids.sort_by { |pid| levels_map[pid] || 0 }.filter_map { |pid| slugs_map[pid] }.join('/')
result[pl.id] = path.presence
end
end
|
.default_repairability_notice ⇒ Object
669
670
671
|
# File 'app/models/product_line.rb', line 669
def self.default_repairability_notice
@default_repairability_notice ||= I18n.t('products.notices.default_quebec_repairability.text', default: nil).presence
end
|
.effective_seo_description(product_line, char_limit: SEO_META_DESCRIPTION_MAX_LENGTH) ⇒ Object
786
787
788
789
790
791
792
793
794
795
796
797
|
# File 'app/models/product_line.rb', line 786
def self.effective_seo_description(product_line, char_limit: SEO_META_DESCRIPTION_MAX_LENGTH)
d = product_line.seo_description.presence
d ||= product_line.short_description.presence
d ||= product_line.description.presence
return unless d
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
235
|
# File 'app/models/product_line.rb', line 235
scope :for_index, -> { available_to_public.where(is_main_product_line: false, show_in_sales_portal: true).order(:priority, :slug_ltree) }
|
.for_sales_portal ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are for sales portal. Active Record Scope
237
|
# File 'app/models/product_line.rb', line 237
scope :for_sales_portal, -> { where(available_to_public: true, show_in_sales_portal: true) }
|
.for_support_portal ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are for support portal. Active Record Scope
224
|
# File 'app/models/product_line.rb', line 224
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
233
|
# File 'app/models/product_line.rb', line 233
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
234
|
# File 'app/models/product_line.rb', line 234
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
220
221
222
223
|
# File 'app/models/product_line.rb', line 220
scope :not_self_or_ancestor, ->(pl) {
plids = [pl.id.to_i] + pl.ancestors.map(&:id)
where(ProductLine[:id].not_in(plids))
}
|
.past_models ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are past models. Active Record Scope
240
|
# File 'app/models/product_line.rb', line 240
scope :past_models, -> { with_any_tags('past-model') }
|
422
423
424
425
426
|
# File 'app/models/product_line.rb', line 422
def self.photo_tags_for_select
ProductLine.where('cardinality(photofeed_tags) > 0 and photofeed_tags IS NOT NULL').pluck(:photofeed_tags).flatten.uniq.select do |e|
e.present?
end.compact.sort_by(&:downcase)
end
|
.prioritized ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are prioritized. Active Record Scope
200
|
# File 'app/models/product_line.rb', line 200
scope :prioritized, -> { order(:priority) }
|
.ransackable_attributes(_auth_object = nil) ⇒ Object
Exclude ltree-typed columns from generic _cont / _eq predicates — they produce
operator does not exist: ltree ~~* unknown when Ransack applies ILIKE (AppSignal #4449).
254
255
256
|
# File 'app/models/product_line.rb', line 254
def self.ransackable_attributes(_auth_object = nil)
super - %w[slug_ltree ltree_path_ids ltree_path_slugs]
end
|
.ransackable_scopes(_auth_object = nil) ⇒ Object
248
249
250
|
# File 'app/models/product_line.rb', line 248
def self.ransackable_scopes(_auth_object = nil)
%i[with_ancestor_id text_search_with_highlights slug_ltree_contains]
end
|
.reviewable ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are reviewable. Active Record Scope
226
227
228
229
230
231
232
|
# File 'app/models/product_line.rb', line 226
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
265
266
267
268
269
270
271
272
273
274
275
|
# File 'app/models/product_line.rb', line 265
def self.select_options(options = {})
res = ProductLine.all
res = res.by_name_with_descendants(options[:name]) if options[:name]
res = res.by_slug_ltree_descendants(options[:slug_ltree]) if options[:slug_ltree]
res = res.reviewable if options[:reviewable]
res = res.heating_system_types if options[:is_heating_system_type]
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
258
259
260
261
262
263
|
# File 'app/models/product_line.rb', line 258
scope :slug_ltree_contains, ->(term) {
sanitized = term.to_s.strip.tr(' ', '_').gsub(/[^a-zA-Z0-9_.]/, '')
return none if sanitized.blank?
where(arel_table[:slug_ltree].ltree_matches("*.#{sanitized}*.*"))
}
|
.support_items_sort_method_for_select ⇒ Object
473
474
475
|
# File 'app/models/product_line.rb', line 473
def self.support_items_sort_method_for_select
support_items_sort_methods.keys.map { |k| [k.to_s.titleize, k] }
end
|
.support_portal_sorted ⇒ ActiveRecord::Relation<ProductLine>
A relation of ProductLines that are support portal sorted. Active Record Scope
225
|
# File 'app/models/product_line.rb', line 225
scope :support_portal_sorted, -> { for_support_portal.reorder(:support_priority, ProductLine[:popularity].desc) }
|
.support_sub_lines_sort_method_for_select ⇒ Object
469
470
471
|
# File 'app/models/product_line.rb', line 469
def self.support_sub_lines_sort_method_for_select
support_sub_lines_sort_methods.keys.map { |k| [k.to_s.titleize, k] }
end
|
.syncs_items_on_ltree_change? ⇒ Boolean
Enable item ltree sync when product line paths change
142
143
144
|
# File 'app/models/product_line.rb', line 142
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
246
|
# File 'app/models/product_line.rb', line 246
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
238
|
# File 'app/models/product_line.rb', line 238
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
239
|
# File 'app/models/product_line.rb', line 239
scope :with_features, -> { where.not(feature_1: nil, feature_2: nil, feature_3: nil, feature_4: nil, feature_5: nil) }
|
Instance Method Details
#all_my_items ⇒ Object
428
429
430
|
# File 'app/models/product_line.rb', line 428
def all_my_items
Item.where(primary_product_line_id: self_and_descendants_ids)
end
|
#all_my_items_all_product_lines ⇒ Object
432
433
434
|
# File 'app/models/product_line.rb', line 432
def all_my_items_all_product_lines
Item.by_product_line_id(id)
end
|
#all_my_support_items ⇒ Object
436
437
438
439
440
441
442
|
# File 'app/models/product_line.rb', line 436
def all_my_support_items
items = all_my_items
items = items.by_product_category_id(product_category_ids) if product_category_ids.present?
items.goods_visible_for_support
.includes(:taggings, primary_product_line: :taggings)
.order(:sku)
end
|
#any_translatable_attribute_changed? ⇒ Boolean
We detect if any of the translatable attributes have changed, this can be used
to trigger auto translate or cleanup functionalities
825
826
827
|
# File 'app/models/product_line.rb', line 825
def any_translatable_attribute_changed?
TRANSLATABLE_ATTRIBUTES.any? { |attr| send(:"#{attr}_changed?") || send(:"saved_change_to_#{attr}?") }
end
|
#articles ⇒ ActiveRecord::Relation<Article>
188
|
# File 'app/models/product_line.rb', line 188
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.
846
847
848
|
# File 'app/models/product_line.rb', line 846
def available_content_locales
Catalog.joins(catalog_items: :item).merge(all_my_items).merge(CatalogItem.not_discontinued).pluck(:locales).flatten.compact.uniq.sort
end
|
#canonical_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.
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
|
# File 'app/models/product_line.rb', line 323
def canonical_path
@canonical_path ||= begin
ancestor_ids = Array(cached_ancestor_ids)
path = if ancestor_ids.present?
rows = ProductLine.where(id: ancestor_ids + [id])
.pluck(:id, :slug, :level, :available_to_public, :is_main_product_line)
rows.select! { |_, _, _, pub, main| pub || main }
rows.sort_by! { |_, _, lvl, _, _| lvl }
rows.map { |_, s, _, _, _| s }.compact.join('/')
else
slug.to_s
end
path.presence
end
end
|
#canonical_url(locale: I18n.locale) ⇒ Object
Canonical URL with locale prefix.
e.g. "/en-US/floor-heating/tempzone/flex-roll"
366
367
368
|
# File 'app/models/product_line.rb', line 366
def canonical_url(locale: I18n.locale)
"/#{locale}/#{canonical_path}"
end
|
#catalogs ⇒ ActiveRecord::Relation<Catalog>
186
|
# File 'app/models/product_line.rb', line 186
has_many :catalogs, through: :primary_items
|
#complimentary_product_lines ⇒ ActiveRecord::Relation<ProductLine>
189
190
|
# File 'app/models/product_line.rb', line 189
has_and_belongs_to_many :complimentary_product_lines, class_name: 'ProductLine', join_table: :complimentary_product_lines,
association_foreign_key: :complimentary_product_line_id
|
#complimentary_product_lines_for_select ⇒ Object
463
464
465
466
467
|
# File 'app/models/product_line.rb', line 463
def complimentary_product_lines_for_select
ProductLine.not_self_or_ancestor(self)
.order(:lineage_expanded)
.pluck(:lineage_expanded, :id)
end
|
#content_for_embedding(_content_type = :primary) ⇒ Object
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
|
# File 'app/models/product_line.rb', line 850
def content_for_embedding(_content_type = :primary)
parts = []
parts << lineage_expanded.presence
parts << "Public name: #{effective_public_name}" if effective_public_name.present? && effective_public_name != name
parts << "Tag line: #{effective_tag_line}" if effective_tag_line.present?
parts << "Summary: #{effective_short_description}" if effective_short_description.present?
desc_text = Nokogiri::HTML(description_html.to_s).text.gsub(/\s+/, ' ').strip
parts << "Description: #{desc_text}" if desc_text.present?
features = [feature_1, feature_2, feature_3, feature_4, feature_5].select(&:present?)
parts << "Features: #{features.join(' | ')}" if features.any?
parts << "SEO: #{seo_description}" if seo_description.present?
tag_names = tags.reject(&:blank?)
parts << "Tags: #{tag_names.join(', ')}" if tag_names.any?
parts.compact.join("\n").presence
end
|
#content_locales_to_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.
840
841
842
|
# File 'app/models/product_line.rb', line 840
def content_locales_to_render
(Mobility.available_locales & ([I18n.default_locale] + available_content_locales)).uniq
end
|
#decorated_product_line_name ⇒ Object
411
412
413
414
415
416
417
418
419
420
|
# File 'app/models/product_line.rb', line 411
def decorated_product_line_name
dn = name_en
dn << ' ♔' if is_main_product_line
dn << ' ☆' if reviewable
dn << ' ♨' if is_heating_system_type
dn << ' ☼' if available_to_public
dn << ' $' if show_in_sales_portal
dn << ' ✆' if show_in_support_portal
dn
end
|
#description ⇒ Object
799
800
801
|
# File 'app/models/product_line.rb', line 799
def description
html_to_text(description_html)
end
|
#digital_asset_product_lines ⇒ ActiveRecord::Relation<DigitalAssetProductLine>
183
|
# File 'app/models/product_line.rb', line 183
has_many :digital_asset_product_lines, inverse_of: :product_line
|
#digital_assets ⇒ ActiveRecord::Relation<DigitalAsset>
184
|
# File 'app/models/product_line.rb', line 184
has_many :digital_assets, through: :digital_asset_product_lines
|
#display_name ⇒ Object
407
408
409
|
# File 'app/models/product_line.rb', line 407
def display_name
public_name.presence || name.presence
end
|
#easy_mat? ⇒ Boolean
704
705
706
|
# File 'app/models/product_line.rb', line 704
def easy_mat?
path_includes?('easy_mat')
end
|
774
775
776
777
778
779
|
# File 'app/models/product_line.rb', line 774
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
518
519
520
|
# File 'app/models/product_line.rb', line 518
def effective_public_name
public_name.presence || self_and_ancestors.where.not(public_name: [nil, '']).order(level: :desc).pick(:public_name) || name
end
|
#effective_quote_builder_video ⇒ Object
530
531
532
|
# File 'app/models/product_line.rb', line 530
def effective_quote_builder_video
quote_builder_video || self_and_ancestors.where.not(quote_builder_video_id: nil).order(level: :desc).first&.quote_builder_video
end
|
#effective_quote_builder_video_uid ⇒ Object
534
535
536
|
# File 'app/models/product_line.rb', line 534
def effective_quote_builder_video_uid
effective_quote_builder_video&.cloudflare_uid
end
|
#effective_seo_description(char_limit: SEO_META_DESCRIPTION_MAX_LENGTH) ⇒ Object
781
782
783
784
|
# File 'app/models/product_line.rb', line 781
def effective_seo_description(char_limit: SEO_META_DESCRIPTION_MAX_LENGTH)
self.class.effective_seo_description(self, char_limit:) ||
self.class.effective_seo_description(get_first_reviewable || root, char_limit:)
end
|
#effective_seo_keywords ⇒ Object
300
301
302
303
304
305
306
307
308
|
# File 'app/models/product_line.rb', line 300
def effective_seo_keywords
return seo_keywords if seo_keywords.present?
plr = get_first_reviewable
return plr.seo_keywords if plr && plr.seo_keywords.present?
keywords = generate_full_name_array + [public_name]
keywords.map(&:presence).compact
end
|
#effective_seo_title ⇒ Object
768
769
770
771
772
|
# File 'app/models/product_line.rb', line 768
def effective_seo_title
pn = seo_title.presence || public_name.presence
pn ||= lineage_expanded.gsub(' > ', ' ')
pn.truncate(SEO_TITLE_MAX_LENGTH, separator: /\s/, omission: '')
end
|
#effective_short_description ⇒ Object
526
527
528
|
# File 'app/models/product_line.rb', line 526
def effective_short_description
short_description.presence || self_and_ancestors.where.not(short_description: [nil, '']).order(level: :desc).pick(:short_description)
end
|
#effective_support_category_priority ⇒ Object
370
371
372
|
# File 'app/models/product_line.rb', line 370
def effective_support_category_priority
support_category_priority || 100
end
|
#effective_support_category_title ⇒ Object
374
375
376
|
# File 'app/models/product_line.rb', line 374
def effective_support_category_title
support_category_title.presence || display_name
end
|
#effective_support_title ⇒ Object
764
765
766
|
# File 'app/models/product_line.rb', line 764
def effective_support_title
support_title.presence || display_name
end
|
#effective_tag_line ⇒ Object
522
523
524
|
# File 'app/models/product_line.rb', line 522
def effective_tag_line
tag_line.presence || self_and_ancestors.where.not(tag_line: [nil, '']).order(level: :desc).pick(:tag_line)
end
|
#embedding_content_changed? ⇒ Boolean
871
872
873
874
875
876
877
878
879
880
881
882
883
|
# File 'app/models/product_line.rb', line 871
def embedding_content_changed?
saved_change_to_name? ||
saved_change_to_public_name? ||
saved_change_to_description_html? ||
saved_change_to_short_description? ||
saved_change_to_tag_line? ||
saved_change_to_feature_1? ||
saved_change_to_feature_2? ||
saved_change_to_feature_3? ||
saved_change_to_feature_4? ||
saved_change_to_feature_5? ||
saved_change_to_seo_description?
end
|
#environ_easy_mat? ⇒ Boolean
#environ_flex_roll? ⇒ Boolean
#facets ⇒ ActiveRecord::Relation<Facet>
176
|
# File 'app/models/product_line.rb', line 176
has_and_belongs_to_many :facets
|
#features ⇒ Object
398
399
400
401
402
403
404
405
|
# File 'app/models/product_line.rb', line 398
def features
if has_features?
[feature_1, feature_2, feature_3, feature_4, feature_5].map(&:presence).compact
else fa = ancestors.detect(&:has_features?)
fa&.features || []
end
end
|
#flex_roll? ⇒ Boolean
Heating system type predicates using ltree paths
700
701
702
|
# File 'app/models/product_line.rb', line 700
def flex_roll?
path_includes?('flex_roll')
end
|
#full_complimentary_product_line_ids ⇒ Object
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
|
# File 'app/models/product_line.rb', line 538
def full_complimentary_product_line_ids
Rails.cache.fetch([cache_key, :complimentary_product_lines], expires_in: 1.month) do
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
629
630
631
632
|
# File 'app/models/product_line.rb', line 629
def get_first_heating_system_type
return @get_first_heating_system_type if instance_variable_defined?(:@get_first_heating_system_type)
@get_first_heating_system_type = self_and_ancestors.where(is_heating_system_type: true).order('level desc, priority asc').first
end
|
#get_first_public ⇒ Object
681
682
683
684
685
|
# File 'app/models/product_line.rb', line 681
def get_first_public
return self if available_to_public?
self_and_ancestors.where(available_to_public: true).order(level: :desc, priority: :asc).first
end
|
#get_first_public_for_sales ⇒ Object
687
688
689
690
691
|
# File 'app/models/product_line.rb', line 687
def get_first_public_for_sales
return self if available_to_public? && !is_main_product_line? && show_in_sales_portal?
self_and_ancestors.where(available_to_public: true, is_main_product_line: false, show_in_sales_portal: true).order(level: :desc, priority: :asc).first
end
|
#get_first_public_for_support ⇒ Object
693
694
695
696
697
|
# File 'app/models/product_line.rb', line 693
def get_first_public_for_support
return self if available_to_public? && !is_main_product_line? && show_in_support_portal?
self_and_ancestors.where(available_to_public: true, is_main_product_line: false, show_in_support_portal: true).order(level: :desc, priority: :asc).first
end
|
#get_first_reviewable ⇒ Object
624
625
626
627
|
# File 'app/models/product_line.rb', line 624
def get_first_reviewable
return @get_first_reviewable if instance_variable_defined?(:@get_first_reviewable)
@get_first_reviewable = self_and_ancestors.where(reviewable: true).order('level desc, priority asc').first
end
|
#get_first_show_in_sales_portal_ancestor(exclude_main_line: false, exclude_self: false) ⇒ Object
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
|
# File 'app/models/product_line.rb', line 573
def get_first_show_in_sales_portal_ancestor(exclude_main_line: false, exclude_self: false)
if instance_variable_defined?(:@get_first_show_in_sales_portal_ancestor) && !exclude_main_line && !exclude_self
return @get_first_show_in_sales_portal_ancestor
end
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
591
592
593
|
# File 'app/models/product_line.rb', line 591
def get_first_show_in_sales_portal_ancestor_id
get_first_show_in_sales_portal_ancestor&.id
end
|
#get_first_show_in_sales_portal_ancestor_path ⇒ Object
Returns ltree path for the first ancestor shown in sales portal
609
610
611
|
# File 'app/models/product_line.rb', line 609
def get_first_show_in_sales_portal_ancestor_path
get_first_show_in_sales_portal_ancestor&.ltree_path_slugs
end
|
#get_first_show_in_sales_portal_ancestor_slug_ltree ⇒ Object
595
596
597
|
# File 'app/models/product_line.rb', line 595
def get_first_show_in_sales_portal_ancestor_slug_ltree
get_first_show_in_sales_portal_ancestor&.slug_ltree
end
|
#get_first_show_in_support_portal_ancestor ⇒ Object
599
600
601
|
# File 'app/models/product_line.rb', line 599
def get_first_show_in_support_portal_ancestor
self_and_ancestors.where(show_in_support_portal: true).order('level desc, priority asc').first
end
|
#get_first_show_in_support_portal_ancestor_path ⇒ Object
Returns ltree path for the first ancestor shown in support portal
614
615
616
|
# File 'app/models/product_line.rb', line 614
def get_first_show_in_support_portal_ancestor_path
get_first_show_in_support_portal_ancestor&.ltree_path_slugs
end
|
#get_first_show_in_support_portal_ancestor_slug_ltree ⇒ Object
604
605
606
|
# File 'app/models/product_line.rb', line 604
def get_first_show_in_support_portal_ancestor_slug_ltree
get_first_show_in_support_portal_ancestor&.slug_ltree
end
|
#get_main_product_line ⇒ Object
Returns the root/main product line (top of hierarchy)
Uses ltree path to find the root ancestor
620
621
622
|
# File 'app/models/product_line.rb', line 620
def get_main_product_line
root
end
|
#has_features? ⇒ Boolean
390
391
392
|
# File 'app/models/product_line.rb', line 390
def has_features?
[feature_1, feature_2, feature_3, feature_4, feature_5].any?(&:present?)
end
|
#heating_element_product_line_options ⇒ ActiveRecord::Relation<HeatingElementProductLineOption>
182
|
# File 'app/models/product_line.rb', line 182
has_many :heating_element_product_line_options, inverse_of: :product_line
|
#heating_system_type_name ⇒ Object
661
662
663
664
665
666
667
|
# File 'app/models/product_line.rb', line 661
def heating_system_type_name
ltree = slug_ltree.to_s
HEATING_SYSTEM_TYPE_NAMES.each do |prefix, name|
return name if ltree.start_with?(prefix)
end
display_name || lineage_expanded&.titleize || slug.to_s.tr('-', ' ').titleize
end
|
#installed_room_configurations ⇒ ActiveRecord::Relation<RoomConfiguration>
191
192
|
# File 'app/models/product_line.rb', line 191
has_and_belongs_to_many :installed_room_configurations, class_name: 'RoomConfiguration', join_table: :installed_product_lines,
association_foreign_key: :room_configuration_id
|
#is_cable_system? ⇒ Boolean
760
761
762
|
# File 'app/models/product_line.rb', line 760
def is_cable_system?
path_includes?('cable')
end
|
#is_cork? ⇒ Boolean
Predicate methods using ltree paths for hierarchy checks
#is_led_mirror? ⇒ Boolean
673
674
675
|
# File 'app/models/product_line.rb', line 673
def is_led_mirror?
descendant_of_path?(LtreePaths::PL_LED_MIRROR)
end
|
#is_made_to_order_led_mirror? ⇒ Boolean
#is_reviewable? ⇒ Boolean
502
503
504
505
506
507
508
509
|
# File 'app/models/product_line.rb', line 502
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>
173
|
# File 'app/models/product_line.rb', line 173
has_many :item_product_lines, inverse_of: :product_line, dependent: :destroy
|
#items ⇒ ActiveRecord::Relation<Item>
174
|
# File 'app/models/product_line.rb', line 174
has_many :items, through: :item_product_lines
|
#main_line_slug_ltree ⇒ Object
452
453
454
|
# File 'app/models/product_line.rb', line 452
def main_line_slug_ltree
root.slug_ltree if root&.is_main_product_line
end
|
#move_to_new_parent(new_product_line_id) ⇒ Object
Convenience method to move a product line to a new parent
while you can just update parent_id too, this will also refresh/touch items and their specs
associated with this product line.
381
382
383
384
385
386
387
388
|
# File 'app/models/product_line.rb', line 381
def move_to_new_parent(new_product_line_id)
item_ids = all_my_items.pluck(:id)
self.parent_id = new_product_line_id
save item_ids.each { |iid| ItemAttributeWorker.perform_async(iid) }
{ new_slug_ltree: slug_ltree, item_queued_for_update: item_ids.size }
end
|
#normalize_friendly_id(value) ⇒ Object
895
896
897
|
# File 'app/models/product_line.rb', line 895
def normalize_friendly_id(value)
value.to_s.parameterize
end
|
#parent_options_for_select ⇒ Object
456
457
458
459
460
461
|
# File 'app/models/product_line.rb', line 456
def parent_options_for_select
pl = ProductLine.all
pl = pl.where(ProductLine[:id].not_eq(id)) unless new_record?
pl = pl.order(:lineage_expanded)
pl.pluck(:lineage_expanded, :id)
end
|
#past_model? ⇒ Boolean
394
395
396
|
# File 'app/models/product_line.rb', line 394
def past_model?
tags.include?('past-model')
end
|
#primary_image ⇒ Image
171
|
# File 'app/models/product_line.rb', line 171
belongs_to :primary_image, class_name: 'Image', optional: true
|
#primary_image_inherited ⇒ Object
511
512
513
|
# File 'app/models/product_line.rb', line 511
def primary_image_inherited
primary_image || (parent ? parent.primary_image_inherited : nil)
end
|
#primary_items ⇒ ActiveRecord::Relation<Item>
177
178
179
180
181
|
# File 'app/models/product_line.rb', line 177
has_many :primary_items,
inverse_of: :primary_product_line,
class_name: 'Item',
foreign_key: :primary_product_line_id,
dependent: :nullify
|
#product_categories ⇒ ActiveRecord::Relation<ProductCategory>
193
|
# File 'app/models/product_line.rb', line 193
has_and_belongs_to_many :product_categories
|
#product_specifications ⇒ ActiveRecord::Relation<ProductSpecification>
172
|
# File 'app/models/product_line.rb', line 172
has_many :product_specifications, inverse_of: :product_line, dependent: :destroy
|
#product_type(separator: nil, limit: nil) ⇒ Object
811
812
813
814
815
|
# File 'app/models/product_line.rb', line 811
def product_type(separator: nil, limit: nil)
Feed::ProductTypeGenerator.process(product_line: self,
separator:,
limit:)
end
|
#prune_unused_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.
831
832
833
834
835
836
|
# File 'app/models/product_line.rb', line 831
def prune_unused_translations
valid_locales = content_locales_to_render
translations.each do |(locale, _translation_attribute)|
translations.delete(locale) unless locale.to_sym.in?(valid_locales)
end
end
|
#purge_cache ⇒ Object
807
808
809
|
# File 'app/models/product_line.rb', line 807
def purge_cache
CacheWorker.perform_async('cache_class' => 'ProductLine::CacheFlusher', 'params' => id)
end
|
#purge_edge_cache ⇒ Object
286
287
288
289
290
291
292
293
294
|
# File 'app/models/product_line.rb', line 286
def purge_edge_cache
res = []
urls = self_and_descendants.flat_map { |pl| pl.site_maps.map(&:url) }
if urls.present?
jid = EdgeCacheWorker.perform_async('urls' => urls)
res += urls.map { |url| { url:, jid: } }
end
res
end
|
#quote_builder_video ⇒ Video
196
|
# File 'app/models/product_line.rb', line 196
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.
557
558
559
560
561
562
563
|
# File 'app/models/product_line.rb', line 557
def root_system_product_line
@root_system_product_line ||= begin
get_first_heating_system_type ||
get_first_show_in_sales_portal_ancestor(exclude_main_line: false) ||
get_first_reviewable
end
end
|
#root_system_product_line_id ⇒ Object
565
566
567
|
# File 'app/models/product_line.rb', line 565
def root_system_product_line_id
root_system_product_line&.id
end
|
#root_system_product_line_slug_ltree ⇒ Object
569
570
571
|
# File 'app/models/product_line.rb', line 569
def root_system_product_line_slug_ltree
root_system_product_line&.slug_ltree
end
|
#self_and_descendants_product_category_ids ⇒ Object
#self_and_inherited_specifications ⇒ Object
448
449
450
|
# File 'app/models/product_line.rb', line 448
def self_and_inherited_specifications
ProductSpecification.where(product_line_id: self_and_ancestors_ids)
end
|
#should_generate_new_friendly_id? ⇒ Boolean
891
892
893
|
# File 'app/models/product_line.rb', line 891
def should_generate_new_friendly_id?
slug.blank? || will_save_change_to_attribute?(:name_en)
end
|
#showcases ⇒ ActiveRecord::Relation<Showcase>
175
|
# File 'app/models/product_line.rb', line 175
has_many :showcases
|
#site_maps ⇒ ActiveRecord::Relation<SiteMap>
185
|
# File 'app/models/product_line.rb', line 185
has_many :site_maps, as: :resource, dependent: :destroy
|
#slug_not_reserved_filter_slug ⇒ Object
899
900
901
902
903
904
905
906
907
|
# File 'app/models/product_line.rb', line 899
def slug_not_reserved_filter_slug
return unless slug.present?
if slug.in?(RESERVED_SECTION_SLUGS)
errors.add(:slug, "'#{slug}' is reserved as a catalog URL section keyword")
elsif root&.slug == 'towel-warmer' && slug.in?(RESERVED_FILTER_SLUGS)
errors.add(:slug, "conflicts with towel warmer filter slug '#{slug}'")
end
end
|
#slug_source ⇒ Object
-- FriendlyId ----------------------------------------------------------
887
888
889
|
# File 'app/models/product_line.rb', line 887
def slug_source
name_en
end
|
#snow_melt? ⇒ Boolean
736
737
738
|
# File 'app/models/product_line.rb', line 736
def snow_melt?
descendant_of_path?(LtreePaths::PL_SNOW_MELTING)
end
|
#snow_melt_cable? ⇒ Boolean
#snow_melt_mat? ⇒ Boolean
#snow_melt_or_slab_heat? ⇒ Boolean
752
753
754
|
# File 'app/models/product_line.rb', line 752
def snow_melt_or_slab_heat?
snow_melt? || slab_heat?
end
|
#supported_in_iq? ⇒ Boolean
803
804
805
|
# File 'app/models/product_line.rb', line 803
def supported_in_iq?
IQ_SUPPORTED_HEATING_SYSTEMS.include?(heating_system_type_name)
end
|
#tempzone_cable? ⇒ Boolean
#tempzone_easy_mat? ⇒ Boolean
#tempzone_flex_roll? ⇒ Boolean
#tempzone_thin_cable? ⇒ Boolean
#to_s ⇒ Object
477
478
479
|
# File 'app/models/product_line.rb', line 477
def to_s
lineage_expanded || generate_full_name
end
|
#url_paths ⇒ Object
310
311
312
|
# File 'app/models/product_line.rb', line 310
def url_paths
slug_ltree.to_s.split('.')
end
|
#vignette_room ⇒ RoomConfiguration
195
|
# File 'app/models/product_line.rb', line 195
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
280
281
282
283
284
|
# File 'app/models/product_line.rb', line 280
def warm_cache
return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled?
site_maps.each(&:warm_cache)
end
|