Class: Showcase
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Showcase
- Extended by:
- FriendlyId
- Defined in:
- app/models/showcase.rb
Overview
== Schema Information
Table name: showcases
Database name: primary
id :bigint not null, primary key
amps :integer
breadcrumbs :string default([]), not null, is an Array
breaker_size :string
city :string
connection_type :string
custom_slug :string
description :text
enabled_buttons :jsonb
floor_plan_display_ids :integer default([]), not null, is an Array
flooring_surface_types :string default([]), not null, is an Array
item_ids :integer default([]), not null, is an Array
name :string
operating_cost :decimal(10, 2)
operating_cost_explanation :text
post_ids :integer default([]), not null, is an Array
product_line_ids :integer default([]), not null, is an Array
project_type :string
quote_ids :integer default([]), not null, is an Array
room_size :string
room_types :string default([]), not null, is an Array
seo_description :text
seo_title :string
short_description :text
state :string
state_code :string
surface :string
volts :integer
wattage :integer
created_at :datetime not null
updated_at :datetime not null
creator_id :integer
customer_id :integer
floor_type_id :integer
legacy_showcase_id :integer
room_configuration_id :integer
updater_id :integer
Indexes
index_showcases_on_creator_id (creator_id)
index_showcases_on_custom_slug (custom_slug) UNIQUE
index_showcases_on_customer_id (customer_id)
index_showcases_on_enabled_buttons (enabled_buttons) USING gin
index_showcases_on_floor_plan_display_ids (floor_plan_display_ids) USING gin
index_showcases_on_floor_type_id (floor_type_id)
index_showcases_on_item_ids (item_ids) USING gin
index_showcases_on_post_ids (post_ids) USING gin
index_showcases_on_product_line_ids (product_line_ids) USING gin
index_showcases_on_quote_ids (quote_ids) USING gin
index_showcases_on_room_configuration_id (room_configuration_id)
index_showcases_on_state_code (state_code)
index_showcases_on_updater_id (updater_id)
Foreign Keys
fk_rails_... (customer_id => parties.id)
fk_rails_... (floor_type_id => floor_types.id)
fk_rails_... (room_configuration_id => room_configurations.id)
Constant Summary collapse
- PROJECT_TYPES =
Project Types
%w[HO PRO].freeze
- AVAILABLE_BUTTONS =
Available buttons.
%w[ button_design_room button_floor_heating_quote button_customize_floor_plan button_snow_melting_quote button_floor_heating button_snow_melting button_roof_gutter_deicing button_pipe_freeze_protection ].freeze
- V2_MIGRATION_CUTOFF =
Date when showcases_v2 was renamed back to showcases (migration 20251013120000).
Versions for the current showcase ID created before this date may belong to a
different (legacy) showcase that happened to reuse the same primary key. Time.utc(2025, 10, 13).freeze
Constants included from Models::Embeddable
Models::Embeddable::MAX_CONTENT_LENGTH
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #custom_slug ⇒ Object readonly
- #enabled_buttons ⇒ Object readonly
-
#name ⇒ Object
readonly
Validations.
- #state ⇒ Object readonly
Belongs to collapse
-
#customer ⇒ Customer
Associations.
- #region_state ⇒ State
Methods included from Models::Auditable
Has many collapse
- #digital_assets ⇒ ActiveRecord::Relation<DigitalAsset>
- #faqs ⇒ ActiveRecord::Relation<ShowcaseFaq>
- #showcase_digital_assets ⇒ ActiveRecord::Relation<ShowcaseDigitalAsset>
- #site_maps ⇒ ActiveRecord::Relation<SiteMap>
Methods included from Models::CrossLinkable
#inbound_content_links, #outbound_content_links
Methods included from Models::Taggable
Methods included from Models::Embeddable
Class Method Summary collapse
-
.archived ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are archived.
-
.by_product_line_id ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are by product line id.
-
.by_quote_id ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are by quote id.
-
.by_sku ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are by sku.
-
.draft ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are draft.
-
.embeddable_content_types ⇒ Object
Embeddable configuration.
- .filter_value_options ⇒ Object
-
.flooring_surface_types_include ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are flooring surface types include.
-
.flush_edge_cache ⇒ Object
quick_links_hash logic is defined in ShowcasePresenter to allow using view helpers.
-
.for_country ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are for country.
-
.for_country_iso3 ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are for country iso3.
-
.for_item ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are for item.
-
.homeowner ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are homeowner.
-
.order_by_images ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are order by images.
-
.professional ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are professional.
- .project_type_options ⇒ Object
-
.published ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are published.
- .ransackable_scopes(_auth_object = nil) ⇒ Object
-
.room_types_include ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are room types include.
-
.with_main_image ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are with main image.
Instance Method Summary collapse
- #breadcrumbs_hash ⇒ Object
- #button_display_name(button_name) ⇒ Object
- #button_enabled?(button_name) ⇒ Boolean
- #content_for_embedding(content_type = :primary) ⇒ Object
- #disable_button(button_name) ⇒ Object
-
#enable_button(button_name) ⇒ Object
NOTE: all_tags method provided by Models::Taggable concern.
-
#homeowner? ⇒ Boolean
Project type helpers.
-
#main_image ⇒ Object
Methods.
- #professional? ⇒ Boolean
- #project_type_label ⇒ Object
- #purge_edge_cache ⇒ Object
- #to_param ⇒ Object
-
#versions_for_audit_trail(_params = {}) ⇒ Object
Combine audit trail versions from both the current showcase ID and, when present, the legacy showcase ID that was used before the v2 migration.
Methods included from Models::CrossLinkable
#content_links_count, #linked_content, #linked_posts, #linked_publications, #linked_showcases, #linked_videos
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::Embeddable
#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::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation
Methods included from Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#custom_slug ⇒ Object (readonly)
226 |
# File 'app/models/showcase.rb', line 226 validates :custom_slug, presence: true, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/, message: 'can only contain lowercase letters, numbers, and hyphens' } |
#enabled_buttons ⇒ Object (readonly)
227 |
# File 'app/models/showcase.rb', line 227 validates :enabled_buttons, presence: true |
#name ⇒ Object (readonly)
Validations
Validations:
224 |
# File 'app/models/showcase.rb', line 224 validates :name, presence: true |
#state ⇒ Object (readonly)
225 |
# File 'app/models/showcase.rb', line 225 validates :state, presence: true |
Class Method Details
.archived ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are archived. Active Record Scope
258 |
# File 'app/models/showcase.rb', line 258 scope :archived, -> { where(state: 'archived') } |
.by_product_line_id ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are by product line id. Active Record Scope
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'app/models/showcase.rb', line 268 scope :by_product_line_id, ->(*product_line_ids) { ids = product_line_ids.flatten.filter_map(&:presence).map(&:to_i).uniq next all if ids.empty? = ids.flat_map do |pid| pl = ProductLine.find_by(id: pid) next [] unless pl [pl.id, pl.ancestors.ids, pl.descendants.ids] end.flatten.compact.uniq next all if .empty? where('array_remove(product_line_ids, NULL) && ARRAY[?]::integer[]', ) } |
.by_quote_id ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are by quote id. Active Record Scope
283 284 285 286 287 288 |
# File 'app/models/showcase.rb', line 283 scope :by_quote_id, ->(*quote_ids) { ids = quote_ids.flatten.filter_map(&:presence).map(&:to_i).uniq next all if ids.empty? where('array_remove(quote_ids, NULL) && ARRAY[?]::integer[]', ids) } |
.by_sku ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are by sku. Active Record Scope
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'app/models/showcase.rb', line 289 scope :by_sku, ->(*skus) { sku_list = skus .flatten .compact .map { |s| s.to_s.strip } .flat_map { |s| s.split(/[\s,]+/) } .map(&:strip) .compact_blank .uniq next all if sku_list.empty? item_ids = Item.where(sku: sku_list).ids next none if item_ids.empty? where('array_remove(item_ids, NULL) && ARRAY[?]::integer[]', item_ids) } |
.draft ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are draft. Active Record Scope
257 |
# File 'app/models/showcase.rb', line 257 scope :draft, -> { where(state: 'draft') } |
.embeddable_content_types ⇒ Object
Embeddable configuration
495 496 497 |
# File 'app/models/showcase.rb', line 495 def self. %i[primary visual] end |
.filter_value_options ⇒ Object
484 485 486 487 488 |
# File 'app/models/showcase.rb', line 484 def self. ['G Shaped - Peninsula', 'L Shaped', 'U Shaped', 'Single Wall or Straight Kitchen', 'Corridor or Gallery Kitchen', 'Small', 'Medium', 'Large', '1-49 sq.ft.', '50-99 sq.ft.', '100-149 sq.ft.', '150-199 sq.ft.', '200+ sq.ft.', 'Shower Only', 'Shower + Bench'] end |
.flooring_surface_types_include ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are flooring surface types include. Active Record Scope
327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'app/models/showcase.rb', line 327 scope :flooring_surface_types_include, ->(*names) { values = names .flatten .filter_map { |n| n.to_s.strip.presence } .uniq next all if values.empty? where( 'ARRAY(SELECT x FROM unnest(array_remove(flooring_surface_types, NULL)) AS x) && ARRAY[?]::varchar[]', values ) } |
.flush_edge_cache ⇒ Object
quick_links_hash logic is defined in ShowcasePresenter to allow using view helpers
470 471 472 473 474 475 476 |
# File 'app/models/showcase.rb', line 470 def self.flush_edge_cache return :disabled unless Cache::EdgeCacheUtility.edge_cache_enabled? urls = SiteMap.where(resource_type: 'Showcase', resource_id: published.select(:id)).map(&:url) EdgeCacheWorker.perform_async('urls' => urls) if urls.present? urls end |
.for_country ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are for country. Active Record Scope
341 342 343 344 345 346 |
# File 'app/models/showcase.rb', line 341 scope :for_country, ->(country_iso) { iso = country_iso.to_s.upcase next all if iso.blank? joins(:region_state).merge(State.by_country_iso(iso)) } |
.for_country_iso3 ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are for country iso3. Active Record Scope
349 350 351 352 353 354 |
# File 'app/models/showcase.rb', line 349 scope :for_country_iso3, ->(country_iso3) { iso3 = country_iso3.to_s.upcase next all if iso3.blank? joins(:region_state).where(states: { country_iso3: iso3 }) } |
.for_item ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are for item. Active Record Scope
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'app/models/showcase.rb', line 309 scope :for_item, ->(item_or_id) { item_id = item_or_id.is_a?(Item) ? item_or_id.id : item_or_id.to_i next none if item_id.zero? # Build the quote-ids subquery as raw SQL so ARRAY() evaluates it once, # not per-row like a correlated EXISTS would. quote_ids_sql = LineItem .where(resource_type: 'Quote', item_id: item_id) .select(:resource_id).distinct.to_sql where( "array_remove(item_ids, NULL) && ARRAY[?]::integer[] " \ "OR array_remove(quote_ids, NULL) && ARRAY(#{quote_ids_sql})::integer[]", [item_id] ) } |
.homeowner ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are homeowner. Active Record Scope
259 |
# File 'app/models/showcase.rb', line 259 scope :homeowner, -> { where(project_type: 'HO') } |
.order_by_images ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are order by images. Active Record Scope
359 360 361 |
# File 'app/models/showcase.rb', line 359 scope :order_by_images, -> { order(Arel.sql('(SELECT COUNT(*) FROM showcase_digital_assets WHERE showcase_digital_assets.showcase_id = showcases.id) DESC')) } |
.professional ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are professional. Active Record Scope
260 |
# File 'app/models/showcase.rb', line 260 scope :professional, -> { where(project_type: 'PRO') } |
.project_type_options ⇒ Object
561 562 563 |
# File 'app/models/showcase.rb', line 561 def self. [%w[Homeowner HO], %w[Professional PRO]] end |
.published ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are published. Active Record Scope
256 |
# File 'app/models/showcase.rb', line 256 scope :published, -> { where(state: 'published') } |
.ransackable_scopes(_auth_object = nil) ⇒ Object
490 491 492 |
# File 'app/models/showcase.rb', line 490 def self.ransackable_scopes(_auth_object = nil) %i[tags_include room_types_include by_product_line_id by_quote_id by_sku for_item for_country for_country_iso3] end |
.room_types_include ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are room types include. Active Record Scope
262 263 264 265 266 267 |
# File 'app/models/showcase.rb', line 262 scope :room_types_include, ->(*room_type_ids) { ids = room_type_ids.flatten.filter_map(&:presence).uniq.map(&:to_s) next all if ids.empty? where('array_remove(room_types, NULL) && ARRAY[?]::varchar[]', ids) } |
.with_main_image ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are with main image. Active Record Scope
357 |
# File 'app/models/showcase.rb', line 357 scope :with_main_image, -> { includes(showcase_digital_assets: :digital_asset) } |
Instance Method Details
#breadcrumbs_hash ⇒ Object
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 |
# File 'app/models/showcase.rb', line 441 def bc = [] # Only use the first breadcrumb if multiple exist = ( || []).first return bc if .blank? begin parts = .split('@') path_url = nil path_name = nil if parts.size == 2 path_name, path_url = parts else path_url = path_name = .split('/').last.to_s.scan(/\w+/).join(' ').humanize end bc << { url: "/#{I18n.locale}/#{path_url}".squeeze('/'), name: path_name } rescue StandardError Rails.logger.error 'Error in Showcase#breadcrumbs_hash path formatting' end if Rails.application.routes.url_helpers.respond_to?(:floor_plans_path) floor_plans_url = Rails.application.routes.url_helpers.floor_plans_path(locale: I18n.locale) bc << { name: 'Floor Plans', url: floor_plans_url } end bc end |
#button_display_name(button_name) ⇒ Object
427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'app/models/showcase.rb', line 427 def () case when 'button_design_room' then 'Design Room' when 'button_floor_heating_quote' then 'Floor Heating Quote' when 'button_customize_floor_plan' then 'Customize Floor Plan' when 'button_snow_melting_quote' then 'Snow Melting Quote' when 'button_floor_heating' then 'Floor Heating' when 'button_snow_melting' then 'Snow Melting' when 'button_roof_gutter_deicing' then 'Roof Gutter Deicing' when 'button_pipe_freeze_protection' then 'Pipe Freeze Protection' else .humanize end end |
#button_enabled?(button_name) ⇒ Boolean
411 412 413 |
# File 'app/models/showcase.rb', line 411 def () .include?() end |
#content_for_embedding(content_type = :primary) ⇒ Object
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 |
# File 'app/models/showcase.rb', line 499 def (content_type = :primary) case content_type.to_sym when :primary parts = [] # Core content parts << "Project: #{name}" if name.present? parts << "Location: #{city}, #{state}" if city.present? || state.present? parts << description if description.present? parts << seo_description if seo_description.present? parts << short_description if short_description.present? # Product context - crucial for finding showcases by product type pl_names = parts << "Product Lines: #{pl_names}" if pl_names.present? # Room and surface context parts << "Room Types: #{room_types.join(', ')}" if room_types.present? parts << "Flooring Types: #{flooring_surface_types.join(', ')}" if flooring_surface_types.present? # FAQs provide additional context if faqs.any? faq_text = faqs.map { |f| "Q: #{f.question}\nA: #{f.answer}" }.join("\n\n") parts << "FAQs:\n#{faq_text}" end parts << "Tags: #{.join(', ')}" if .present? parts.compact.join("\n\n") when :visual # For visual search - describe what's shown in the images parts = [] parts << "Project: #{name}" if name.present? parts << "Location: #{city}, #{state}" if city.present? || state.present? pl_names = parts << "Product Lines: #{pl_names}" if pl_names.present? parts << "Image: #{main_image.title}" if main_image&.title.present? parts << main_image. if main_image&..present? parts << "Image Tags: #{main_image..join(', ')}" if main_image&..present? parts.compact.join("\n\n") end end |
#customer ⇒ Customer
Associations
215 |
# File 'app/models/showcase.rb', line 215 belongs_to :customer, optional: true |
#digital_assets ⇒ ActiveRecord::Relation<DigitalAsset>
218 |
# File 'app/models/showcase.rb', line 218 has_many :digital_assets, through: :showcase_digital_assets |
#disable_button(button_name) ⇒ Object
423 424 425 |
# File 'app/models/showcase.rb', line 423 def () self. = - [] end |
#enable_button(button_name) ⇒ Object
NOTE: all_tags method provided by Models::Taggable concern
417 418 419 420 421 |
# File 'app/models/showcase.rb', line 417 def () return false unless AVAILABLE_BUTTONS.include?() self. = ( + []).uniq end |
#faqs ⇒ ActiveRecord::Relation<ShowcaseFaq>
219 |
# File 'app/models/showcase.rb', line 219 has_many :faqs, class_name: 'ShowcaseFaq', dependent: :destroy |
#homeowner? ⇒ Boolean
Project type helpers
546 547 548 |
# File 'app/models/showcase.rb', line 546 def homeowner? project_type == 'HO' end |
#main_image ⇒ Object
Methods
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'app/models/showcase.rb', line 364 def main_image # Use already-loaded association if available (avoids N+1 when using .with_main_image scope) loaded_assets = if showcase_digital_assets.loaded? showcase_digital_assets.to_a else showcase_digital_assets.includes(:digital_asset).to_a end # Prefer explicitly marked main image marked_main = loaded_assets.find { |sda| sda.is_main && sda.digital_asset&.type == 'Image' } return marked_main.digital_asset if marked_main # Fallback to first image by position loaded_assets.sort_by { |sda| sda.position || Float::INFINITY } .map(&:digital_asset) .find { |da| da&.type == 'Image' } end |
#professional? ⇒ Boolean
550 551 552 |
# File 'app/models/showcase.rb', line 550 def professional? project_type == 'PRO' end |
#project_type_label ⇒ Object
554 555 556 557 558 559 |
# File 'app/models/showcase.rb', line 554 def project_type_label case project_type when 'HO' then 'Homeowner' when 'PRO' then 'Professional' end end |
#purge_edge_cache ⇒ Object
478 479 480 481 482 |
# File 'app/models/showcase.rb', line 478 def purge_edge_cache urls = site_maps.map(&:url) EdgeCacheWorker.perform_async('urls' => urls) if urls.present? urls end |
#region_state ⇒ State
216 |
# File 'app/models/showcase.rb', line 216 belongs_to :region_state, class_name: 'State', foreign_key: 'state_code', primary_key: 'code', optional: true |
#showcase_digital_assets ⇒ ActiveRecord::Relation<ShowcaseDigitalAsset>
217 |
# File 'app/models/showcase.rb', line 217 has_many :showcase_digital_assets, dependent: :destroy |
#site_maps ⇒ ActiveRecord::Relation<SiteMap>
220 |
# File 'app/models/showcase.rb', line 220 has_many :site_maps, as: :resource, dependent: :nullify |
#to_param ⇒ Object
382 383 384 |
# File 'app/models/showcase.rb', line 382 def to_param custom_slug end |
#versions_for_audit_trail(_params = {}) ⇒ Object
Combine audit trail versions from both the current showcase ID and, when present,
the legacy showcase ID that was used before the v2 migration. Excludes:
- versions for the current ID recorded before the v2 migration (wrong showcase)
- versions containing old singular schema fields (room_type_id, product_line_id)
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 |
# File 'app/models/showcase.rb', line 574 def versions_for_audit_trail(_params = {}) base_scope = if legacy_showcase_id.present? && legacy_showcase_id != id # This showcase was migrated from showcase_v2 and received a new ID. # The real historical versions are stored under the legacy ID. RecordVersion.where(item_type: 'Showcase').where( '(item_id = :legacy_id) OR (item_id = :current_id AND created_at >= :cutoff)', legacy_id: legacy_showcase_id, current_id: id, cutoff: V2_MIGRATION_CUTOFF ) else # No legacy mapping (or same ID) — simple filter by created_at scope = versions.where(item_type: 'Showcase', item_id: id) scope = scope.where(created_at: created_at..) if created_at.present? scope end # Exclude versions with old singular schema fields that no longer exist on the model base_scope.where( "(object_changes IS NULL OR (NOT (object_changes ? 'room_type_id') AND NOT (object_changes ? 'product_line_id')))" ) end |