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 =
%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::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
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 Models::EventPublishable
Instance Attribute Details
#custom_slug ⇒ Object (readonly)
225 |
# File 'app/models/showcase.rb', line 225 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)
226 |
# File 'app/models/showcase.rb', line 226 validates :enabled_buttons, presence: true |
#name ⇒ Object (readonly)
Validations
Validations:
223 |
# File 'app/models/showcase.rb', line 223 validates :name, presence: true |
#state ⇒ Object (readonly)
224 |
# File 'app/models/showcase.rb', line 224 validates :state, presence: true |
Class Method Details
.archived ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are archived. Active Record Scope
257 |
# File 'app/models/showcase.rb', line 257 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
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'app/models/showcase.rb', line 267 scope :by_product_line_id, ->(*product_line_ids) { ids = product_line_ids.flatten.map(&:presence).compact.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.pluck(:id), pl.descendants.pluck(:id)] 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
282 283 284 285 286 287 |
# File 'app/models/showcase.rb', line 282 scope :by_quote_id, ->(*quote_ids) { ids = quote_ids.flatten.map(&:presence).compact.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
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'app/models/showcase.rb', line 288 scope :by_sku, ->(*skus) { sku_list = skus .flatten .compact .map { |s| s.to_s.strip } .flat_map { |s| s.split(/[\s,]+/) } .map(&:strip) .reject(&:blank?) .uniq next all if sku_list.empty? item_ids = Item.where(sku: sku_list).pluck(:id) 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
256 |
# File 'app/models/showcase.rb', line 256 scope :draft, -> { where(state: 'draft') } |
.embeddable_content_types ⇒ Object
Embeddable configuration
481 482 483 |
# File 'app/models/showcase.rb', line 481 def self. %i[primary visual] end |
.filter_value_options ⇒ Object
470 471 472 473 474 |
# File 'app/models/showcase.rb', line 470 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
326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'app/models/showcase.rb', line 326 scope :flooring_surface_types_include, ->(*names) { values = names .flatten .map { |n| n.to_s.strip.presence } .compact .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
456 457 458 459 460 461 462 |
# File 'app/models/showcase.rb', line 456 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
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'app/models/showcase.rb', line 308 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
258 |
# File 'app/models/showcase.rb', line 258 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
259 |
# File 'app/models/showcase.rb', line 259 scope :professional, -> { where(project_type: 'PRO') } |
.project_type_options ⇒ Object
547 548 549 |
# File 'app/models/showcase.rb', line 547 def self. [%w[Homeowner HO], %w[Professional PRO]] end |
.published ⇒ ActiveRecord::Relation<Showcase>
A relation of Showcases that are published. Active Record Scope
255 |
# File 'app/models/showcase.rb', line 255 scope :published, -> { where(state: 'published') } |
.ransackable_scopes(_auth_object = nil) ⇒ Object
476 477 478 |
# File 'app/models/showcase.rb', line 476 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
261 262 263 264 265 266 |
# File 'app/models/showcase.rb', line 261 scope :room_types_include, ->(*room_type_ids) { ids = room_type_ids.flatten.map(&:presence).compact.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
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'app/models/showcase.rb', line 427 def bc = [] # Only use the first breadcrumb if multiple exist = ( || []).first return bc unless .present? 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
413 414 415 416 417 418 419 420 421 422 423 424 425 |
# File 'app/models/showcase.rb', line 413 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
397 398 399 |
# File 'app/models/showcase.rb', line 397 def () .include?() end |
#content_for_embedding(content_type = :primary) ⇒ Object
485 486 487 488 489 490 491 492 493 494 495 496 497 498 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 |
# File 'app/models/showcase.rb', line 485 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
214 |
# File 'app/models/showcase.rb', line 214 belongs_to :customer, optional: true |
#digital_assets ⇒ ActiveRecord::Relation<DigitalAsset>
217 |
# File 'app/models/showcase.rb', line 217 has_many :digital_assets, through: :showcase_digital_assets |
#disable_button(button_name) ⇒ Object
409 410 411 |
# File 'app/models/showcase.rb', line 409 def () self. = - [] end |
#enable_button(button_name) ⇒ Object
Note: all_tags method provided by Models::Taggable concern
403 404 405 406 407 |
# File 'app/models/showcase.rb', line 403 def () return false unless AVAILABLE_BUTTONS.include?() self. = ( + []).uniq end |
#faqs ⇒ ActiveRecord::Relation<ShowcaseFaq>
218 |
# File 'app/models/showcase.rb', line 218 has_many :faqs, class_name: 'ShowcaseFaq', dependent: :destroy |
#homeowner? ⇒ Boolean
Project type helpers
532 533 534 |
# File 'app/models/showcase.rb', line 532 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
536 537 538 |
# File 'app/models/showcase.rb', line 536 def professional? project_type == 'PRO' end |
#project_type_label ⇒ Object
540 541 542 543 544 545 |
# File 'app/models/showcase.rb', line 540 def project_type_label case project_type when 'HO' then 'Homeowner' when 'PRO' then 'Professional' end end |
#purge_edge_cache ⇒ Object
464 465 466 467 468 |
# File 'app/models/showcase.rb', line 464 def purge_edge_cache urls = site_maps.map(&:url) EdgeCacheWorker.perform_async('urls' => urls) if urls.present? urls end |
#region_state ⇒ State
215 |
# File 'app/models/showcase.rb', line 215 belongs_to :region_state, class_name: 'State', foreign_key: 'state_code', primary_key: 'code', optional: true |
#showcase_digital_assets ⇒ ActiveRecord::Relation<ShowcaseDigitalAsset>
216 |
# File 'app/models/showcase.rb', line 216 has_many :showcase_digital_assets, dependent: :destroy |
#site_maps ⇒ ActiveRecord::Relation<SiteMap>
219 |
# File 'app/models/showcase.rb', line 219 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)
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 |
# File 'app/models/showcase.rb', line 560 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 |