Class: DigitalAsset

Inherits:
ApplicationRecord show all
Extended by:
FriendlyId
Includes:
Models::Auditable, Models::ItemScopable, Models::Taggable, PgSearch::Model
Defined in:
app/models/digital_asset.rb

Overview

== Schema Information

Table name: digital_assets
Database name: primary

id :integer not null, primary key
ai_metadata_suggestions :jsonb
ai_visual_description :text
air_date :date
asset :jsonb
attachment_format :string(10)
attachment_height :integer
attachment_mime_type :string
attachment_name :string
attachment_size :integer
attachment_uid :string
attachment_width :integer
background_color :string
category :string(255)
cloudflare_data :jsonb not null
cloudflare_uid :string
duration_in_seconds :integer
expanded_description :text
fingerprint :bigint
fingerprint_legacy :string
image_colorspace :string
image_dpi :integer
inactive :boolean default(FALSE), not null
linked_assets_uids :string default([]), is an Array
locales :string default([]), not null, is an Array
location :string
merged_from_ids :integer default([]), is an Array
meta_description :text
meta_keywords :string
meta_title :string(255)
notes :text
position :integer default(100), not null
poster_format :string
poster_mime_type :string
poster_name :string
poster_offset :integer
poster_uid :string
reference_number :string
series :string
slug :string(140)
source :string
structured_transcript_json :jsonb
sub_header :string(255)
thumbnail_url :string
title :string(255)
transcribed_at :datetime
transcript :text
transcription_state :enum default("pending")
translations :jsonb
type :string
url :string(255)
video_has_no_spoken_words :boolean default(FALSE)
vision_analyzed_at :datetime
vision_model_used :string
youtube_caption_synced_at :datetime
youtube_chapters_draft :jsonb
youtube_chapters_generation_error :text
youtube_chapters_generation_status :string
youtube_description :string
youtube_privacy_status :string
youtube_synced_at :datetime
youtube_title :string
youtube_upload_date :datetime
youtube_upload_status :string
created_at :datetime not null
updated_at :datetime not null
assemblyai_transcript_id :string
asset_file_id :string
cloudinary_asset_id :string
creator_id :integer
legacy_wistia_id :string(255)
poster_image_id :integer
purge_cache_request_id :string
updater_id :integer
youtube_id :string
youtube_thumbnail_image_id :integer

Indexes

by_type_inactive_id (type,inactive,id)
index_digital_assets_on_asset_file_id (asset_file_id) UNIQUE
index_digital_assets_on_cloudflare_uid (cloudflare_uid)
index_digital_assets_on_creator_id (creator_id)
index_digital_assets_on_inactive (inactive)
index_digital_assets_on_merged_from_ids (merged_from_ids) USING gin
index_digital_assets_on_poster_image_id (poster_image_id)
index_digital_assets_on_poster_offset (poster_offset)
index_digital_assets_on_slug (slug)
index_digital_assets_on_source (source)
index_digital_assets_on_transcription_state (transcription_state)
index_digital_assets_on_translations (translations) USING gin
index_digital_assets_on_type_and_slug (type,slug) UNIQUE
index_digital_assets_on_updater_id (updater_id)
index_digital_assets_on_url (url)
index_digital_assets_on_vision_analyzed_at (vision_analyzed_at)
index_digital_assets_on_youtube_thumbnail_image_id (youtube_thumbnail_image_id)
index_images_on_fingerprint (fingerprint) WHERE (((type)::text = 'Image'::text) AND (fingerprint IS NOT NULL))
type_category (type,category)
type_entity_id (type,legacy_wistia_id)
type_title (type,title)

Foreign Keys

fk_rails_... (creator_id => parties.id)
fk_rails_... (poster_image_id => digital_assets.id) ON DELETE => nullify
fk_rails_... (updater_id => parties.id)
fk_rails_... (youtube_thumbnail_image_id => digital_assets.id) ON DELETE => nullify

Direct Known Subclasses

Image, Video

Constant Summary collapse

%w[for-product-page for-support-page no-index].freeze
HIDDEN_TAGS =

Recognised hidden tags.

%w[installation-plan pdf-thumbnail room-configuration video-poster auto-generated review-avatar].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Has many collapse

Methods included from Models::Taggable

#tag_records, #taggings

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Taggable

#add_tag, all_tags, #has_tag?, normalize_tag_names, not_tagged_with, #remove_tag, #tag_list, #tag_list=, #taggable_type_for_tagging, tagged_with, #tags, #tags=, tags_cloud, tags_exclude, tags_include, with_all_tags, with_any_tags, without_all_tags, without_any_tags

Methods included from Models::ItemScopable

by_product_category_id, by_product_category_path, by_product_category_path_exact, by_product_category_url, by_product_category_url_exact, by_product_line_path_full, by_product_line_url, by_product_line_url_full, not_by_product_category_id

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #creator, #should_not_save_version, #stamp_record, #updater

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#force_new_slugObject

Returns the value of attribute force_new_slug.



125
126
127
# File 'app/models/digital_asset.rb', line 125

def force_new_slug
  @force_new_slug
end

#refresh_cacheObject

Returns the value of attribute refresh_cache.



125
126
127
# File 'app/models/digital_asset.rb', line 125

def refresh_cache
  @refresh_cache
end

#titleObject (readonly)



144
# File 'app/models/digital_asset.rb', line 144

validates :title, presence: true

#urlObject (readonly)



145
# File 'app/models/digital_asset.rb', line 145

validates :url, uniqueness: { if: :url }

Class Method Details

.activeActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are active. Active Record Scope

Returns:

See Also:



162
# File 'app/models/digital_asset.rb', line 162

scope :active, -> { where(inactive: false, invalid_at: nil) }

.all_localesObject

NOTE: all_tags method provided by Models::Taggable concern



372
373
374
# File 'app/models/digital_asset.rb', line 372

def self.all_locales
  pluck(Arel.sql('distinct unnest(locales) as locale')).compact.sort
end

.available_banner_tagsObject



569
570
571
572
573
# File 'app/models/digital_asset.rb', line 569

def self.available_banner_tags
  PagesController.page_ids.reject { |page_id| page_id.start_with?('h-') }.map do |page_id|
    banner_tag_for(page_id)
  end.uniq.sort
end

.available_og_image_tagsObject



581
582
583
584
585
# File 'app/models/digital_asset.rb', line 581

def self.available_og_image_tags
  PagesController.page_ids.reject { |page_id| page_id.start_with?('h-') }.map do |page_id|
    og_image_tag_for(page_id)
  end.uniq.sort
end

.available_page_tagsObject

Generate the available 'for-' page tags for all CMS pages.
Uses the same convention as the cohesive migration:
'for-' + page_id.tr('/', '-').parameterize + '-page'



545
546
547
548
549
# File 'app/models/digital_asset.rb', line 545

def self.available_page_tags
  PagesController.page_ids.reject { |page_id| page_id.start_with?('h-') }.map do |page_id|
    "for-#{page_id.tr('/', '-').parameterize}-page"
  end.uniq.sort
end

.available_page_tags_with_pathsObject



551
552
553
554
555
556
# File 'app/models/digital_asset.rb', line 551

def self.available_page_tags_with_paths
  PagesController.page_ids.reject { |page_id| page_id.start_with?('h-') }.each_with_object({}) do |page_id, hash|
    tag = "for-#{page_id.tr('/', '-').parameterize}-page"
    hash[tag] = "/#{page_id}"
  end
end

Build the banner image tag for a given CMS page id.
e.g. banner_tag_for("floor-heating/bathroom") => "banner-for-floor-heating-bathroom-page"



565
566
567
# File 'app/models/digital_asset.rb', line 565

def self.banner_tag_for(page_id)
  "banner-for-#{page_id.to_s.tr('/', '-').parameterize}-page"
end

.by_categoryActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by category. Active Record Scope

Returns:

See Also:



224
# File 'app/models/digital_asset.rb', line 224

scope :by_category, ->(cat) { where(category: cat) }

.by_item_idsActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by item ids. Active Record Scope

Returns:

See Also:



225
226
227
228
229
230
# File 'app/models/digital_asset.rb', line 225

scope :by_item_ids, ->(*item_ids) {
  item_ids = [item_ids].flatten.filter_map(&:presence).uniq
  return none if item_ids.blank?

  where('exists(select 1 from digital_assets_items dai where dai.item_id IN (?) and dai.digital_asset_id = digital_assets.id)', item_ids)
}

.by_item_skusActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by item skus. Active Record Scope

Returns:

See Also:



275
# File 'app/models/digital_asset.rb', line 275

scope :by_item_skus, ->(*item_skus) { joins(:items).where(items: { sku: item_skus }) }

.by_party_idsActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by party ids. Active Record Scope

Returns:

See Also:



294
295
296
297
298
299
# File 'app/models/digital_asset.rb', line 294

scope :by_party_ids, ->(*party_ids) {
  party_ids = [party_ids].flatten.filter_map(&:presence).uniq
  return none if party_ids.blank?

  where('exists(select 1 from digital_assets_parties dap inner join parties p on p.id = dap.party_id where (dap.party_id IN (:party_ids) OR p.customer_id IN (:party_ids)) and dap.digital_asset_id = digital_assets.id)', party_ids: party_ids)
}

.by_product_category_id_directActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by product category id direct. Active Record Scope

Returns:

See Also:



205
206
207
208
209
210
# File 'app/models/digital_asset.rb', line 205

scope :by_product_category_id_direct, ->(*pc_ids) {
  ids = [pc_ids].flatten
  return none if ids.empty?

  where('exists(select 1 from digital_assets_product_categories dapc where dapc.digital_asset_id = digital_assets.id and dapc.product_category_id in (?))', ids)
}

.by_product_category_id_direct_or_optionalActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by product category id direct or optional. Active Record Scope

Returns:

See Also:



211
212
213
214
215
216
217
218
219
220
# File 'app/models/digital_asset.rb', line 211

scope :by_product_category_id_direct_or_optional, ->(*pc_ids) {
  ids = [pc_ids].flatten
  return none if ids.empty?

  sql = <<-EOS
    exists(select 1 from digital_assets_product_categories dapc where dapc.digital_asset_id = digital_assets.id and dapc.product_category_id in (?))
    OR not exists(select 1 from digital_assets_product_categories dapc where dapc.digital_asset_id = digital_assets.id)
  EOS
  where(sql, ids)
}

.by_product_line_idActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by product line id. Active Record Scope

Returns:

See Also:



181
182
183
184
185
186
187
188
189
190
191
192
# File 'app/models/digital_asset.rb', line 181

scope :by_product_line_id, ->(*pl_ids) {
  ids = [pl_ids].flatten.filter_map(&:presence).map(&:to_i).uniq
  return none if ids.empty?

  where(
    'EXISTS(SELECT 1 FROM digital_asset_product_lines dapl ' \
    'INNER JOIN product_lines pl ON pl.id = dapl.product_line_id ' \
    'WHERE dapl.digital_asset_id = digital_assets.id ' \
    'AND pl.ltree_path_ids <@ ANY(SELECT ltree_path_ids FROM product_lines WHERE id = ANY(ARRAY[?]::integer[])))',
    ids
  )
}

.by_product_line_id_directActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by product line id direct. Active Record Scope

Returns:

See Also:



175
176
177
178
179
180
# File 'app/models/digital_asset.rb', line 175

scope :by_product_line_id_direct, ->(*pl_ids) {
  ids = [pl_ids].flatten.filter_map(&:presence).uniq
  return none if ids.empty?

  where('EXISTS(SELECT 1 FROM digital_asset_product_lines dapl WHERE dapl.digital_asset_id = digital_assets.id AND dapl.product_line_id IN (?))', ids)
}

.by_product_line_pathActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are by product line path. Active Record Scope

Returns:

See Also:



324
325
326
327
328
329
330
331
332
333
# File 'app/models/digital_asset.rb', line 324

scope :by_product_line_path, ->(slug_path) {
  return all if slug_path.blank?

  where(
    'EXISTS(SELECT 1 FROM digital_asset_product_lines dapl ' \
    'INNER JOIN product_lines pl ON pl.id = dapl.product_line_id ' \
    'WHERE dapl.digital_asset_id = digital_assets.id AND pl.ltree_path_slugs <@ ?)',
    slug_path
  )
}

.categorizedActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are categorized. Active Record Scope

Returns:

See Also:



308
# File 'app/models/digital_asset.rb', line 308

scope :categorized, -> { where.not(category: nil) }

.exclude_tagsActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are exclude tags. Active Record Scope

Returns:

See Also:



280
281
282
283
284
285
# File 'app/models/digital_asset.rb', line 280

scope :exclude_tags, ->(*tag_names) {
  tag_names = [tag_names].flatten.filter_map(&:presence).map(&:downcase)
  return all if tag_names.empty?

  not_tagged_with(tag_names)
}

.imagesActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are images. Active Record Scope

Returns:

See Also:



309
# File 'app/models/digital_asset.rb', line 309

scope :images, -> { where(type: 'Image') }

.invalidatedActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are invalidated. Active Record Scope

Returns:

See Also:



167
# File 'app/models/digital_asset.rb', line 167

scope :invalidated, -> { where.not(invalid_at: nil) }

.localized_forActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are localized for. Active Record Scope

Returns:

See Also:



292
# File 'app/models/digital_asset.rb', line 292

scope :localized_for, ->(*locales) { where.overlap(locales: LocaleUtility.locales_and_fallbacks(locales)) }

.localized_for_or_notActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are localized for or not. Active Record Scope

Returns:

See Also:



293
# File 'app/models/digital_asset.rb', line 293

scope :localized_for_or_not, ->(*locales) { localized_for(locales).or(where(DigitalAsset[:locales].eq('{}'))) }

.not_by_party_idsActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are not by party ids. Active Record Scope

Returns:

See Also:



302
303
304
305
306
307
# File 'app/models/digital_asset.rb', line 302

scope :not_by_party_ids, ->(*party_ids) {
  party_ids = [party_ids].flatten.filter_map(&:presence).uniq
  return all if party_ids.blank?

  where('NOT exists(select 1 from digital_assets_parties dap inner join parties p on p.id = dap.party_id where (dap.party_id IN (:party_ids) OR p.customer_id IN (:party_ids)) and dap.digital_asset_id = digital_assets.id)', party_ids: party_ids)
}

.not_by_product_line_idActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are not by product line id. Active Record Scope

Returns:

See Also:



193
194
195
196
197
198
199
200
201
202
203
204
# File 'app/models/digital_asset.rb', line 193

scope :not_by_product_line_id, ->(*pl_ids) {
  ids = [pl_ids].flatten.compact.map(&:to_i)
  return all if ids.empty?

  where.not(
    'EXISTS(SELECT 1 FROM digital_asset_product_lines dapl ' \
    'INNER JOIN product_lines pl ON pl.id = dapl.product_line_id ' \
    'WHERE dapl.digital_asset_id = digital_assets.id ' \
    'AND pl.ltree_path_ids <@ ANY(SELECT ltree_path_ids FROM product_lines WHERE id = ANY(ARRAY[?]::integer[])))',
    ids
  )
}

.not_by_product_line_pathActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are not by product line path. Active Record Scope

Returns:

See Also:



337
338
339
340
341
342
343
344
345
346
# File 'app/models/digital_asset.rb', line 337

scope :not_by_product_line_path, ->(slug_path) {
  return all if slug_path.blank?

  where.not(
    'EXISTS(SELECT 1 FROM digital_asset_product_lines dapl ' \
    'INNER JOIN product_lines pl ON pl.id = dapl.product_line_id ' \
    'WHERE dapl.digital_asset_id = digital_assets.id AND pl.ltree_path_slugs <@ ?)',
    slug_path
  )
}

.og_image_tag_for(page_id) ⇒ Object

Build the og:image tag for a given CMS page id.
e.g. og_image_tag_for("floor-heating/bathroom") => "og-image-for-floor-heating-bathroom-page"



577
578
579
# File 'app/models/digital_asset.rb', line 577

def self.og_image_tag_for(page_id)
  "og-image-for-#{page_id.to_s.tr('/', '-').parameterize}-page"
end

.page_tag_for(page_id) ⇒ Object

Build the canonical page tag for a given CMS page id (e.g. "towel-warmer").



559
560
561
# File 'app/models/digital_asset.rb', line 559

def self.page_tag_for(page_id)
  "for-#{page_id.to_s.tr('/', '-').parameterize}-page"
end

.ransackable_scopes(_auth_object = nil) ⇒ Object



364
365
366
367
368
# File 'app/models/digital_asset.rb', line 364

def self.ransackable_scopes(_auth_object = nil)
  %i[keyword_search by_product_line_id by_product_line_path by_product_category_id_direct tag_presence
     by_party_ids by_item_ids related_to_item_id show_hidden_tags exclude_tags tags_include
     not_by_product_line_id not_by_party_ids]
end

A relation of DigitalAssets that are related to item id. Active Record Scope

Returns:

See Also:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'app/models/digital_asset.rb', line 234

scope :related_to_item_id, ->(item_id) {
  return none if item_id.blank?

  item = Item.find_by(id: item_id)
  return none unless item

  # Get product line path for ltree ancestor matching
  pl_path = item.primary_pl_path_slugs
  pc_path = item.pc_path_slugs

  # Build OR conditions for:
  # 1. Directly linked to the item
  # 2. Linked to item's product line or any ancestor (using ltree)
  # 3. Linked to item's product category or any ancestor (using ltree)
  conditions = []
  binds = {}

  # Direct item link
  conditions << 'EXISTS(SELECT 1 FROM digital_assets_items dai WHERE dai.item_id = :item_id AND dai.digital_asset_id = digital_assets.id)'
  binds[:item_id] = item_id

  # Product line link (item's PL path is descendant of or equal to the image's PL path)
  if pl_path.present?
    conditions << "EXISTS(SELECT 1 FROM digital_asset_product_lines dapl
                   INNER JOIN product_lines pl ON pl.id = dapl.product_line_id
                   WHERE dapl.digital_asset_id = digital_assets.id
                   AND :pl_path <@ pl.ltree_path_slugs)"
    binds[:pl_path] = pl_path
  end

  # Product category link (item's PC path is descendant of or equal to the image's PC path)
  if pc_path.present?
    conditions << "EXISTS(SELECT 1 FROM digital_assets_product_categories dapc
                   INNER JOIN product_categories pc ON pc.id = dapc.product_category_id
                   WHERE dapc.digital_asset_id = digital_assets.id
                   AND :pc_path <@ pc.ltree_path_slugs)"
    binds[:pc_path] = pc_path
  end

  where(conditions.join(' OR '), binds)
}

.show_hidden_tagsActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are show hidden tags. Active Record Scope

Returns:

See Also:



278
# File 'app/models/digital_asset.rb', line 278

scope :show_hidden_tags, ->(val) { val.to_b ? all : not_tagged_with(HIDDEN_TAGS) }

.tag_presenceActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are tag presence. Active Record Scope

Returns:

See Also:



287
288
289
290
291
# File 'app/models/digital_asset.rb', line 287

scope :tag_presence, ->(tag_present) {
  return if tag_present.blank?

  tag_present.to_b ? joins(:taggings).distinct : where.missing(:taggings)
}

.tagged_with_allActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are tagged with all. Active Record Scope

Returns:

See Also:



277
# File 'app/models/digital_asset.rb', line 277

scope :tagged_with_all, ->(*tag_names) { with_all_tags(*tag_names) }

.validActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are valid. Active Record Scope

Returns:

See Also:



164
# File 'app/models/digital_asset.rb', line 164

scope :valid, -> { where(invalid_at: nil) }

.videosActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are videos. Active Record Scope

Returns:

See Also:



310
# File 'app/models/digital_asset.rb', line 310

scope :videos, -> { where(type: 'Video') }

.with_product_line_urlsActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are with product line urls. Active Record Scope

Returns:

See Also:



311
312
313
314
315
316
317
318
319
320
321
# File 'app/models/digital_asset.rb', line 311

scope :with_product_line_urls, -> {
  all.select_append('
   ARRAY(
     select pl.slug_ltree::text
     from product_lines pl
     inner join digital_asset_product_lines dapl on dapl.product_line_id = pl.id
     where
      dapl.digital_asset_id = digital_assets.id
    ) as product_line_urls
  ')
}

.without_product_categoriesActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are without product categories. Active Record Scope

Returns:

See Also:



222
# File 'app/models/digital_asset.rb', line 222

scope :without_product_categories, -> { where.missing(:product_categories) }

.without_product_linesActiveRecord::Relation<DigitalAsset>

A relation of DigitalAssets that are without product lines. Active Record Scope

Returns:

See Also:



223
# File 'app/models/digital_asset.rb', line 223

scope :without_product_lines, -> { where.missing(:digital_asset_product_lines) }

Instance Method Details

#alertsObject



376
377
378
379
380
# File 'app/models/digital_asset.rb', line 376

def alerts
  r = []
  r << 'SEO Title is too long, keep it at 65 characters or less' if meta_title&.size&.> 65
  r
end

#all_my_itemsObject



527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'app/models/digital_asset.rb', line 527

def all_my_items
  item_ids = items.active.ids

  plids = product_line_ids
  pcids = product_category_ids
  if plids.present? && pcids.present?
    related_items = Item.active.condition_new
    related_items = related_items.by_product_line_id(plids) if plids.present?
    related_items = related_items.by_product_category_id(pcids) if pcids.present?
    item_ids += related_items.ids
  end
  item_ids.uniq!
  Item.where(id: item_ids).order(:sku)
end

#asset_identifierObject



433
434
435
# File 'app/models/digital_asset.rb', line 433

def asset_identifier
  SecureRandom.base58(6).downcase
end


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

def cross_links_opportunities_to_parties
  self.party_ids = party_ids | opportunities.pluck(:customer_id)
  true
end

#digital_asset_product_linesActiveRecord::Relation<DigitalAssetProductLine>

Returns:

See Also:



132
# File 'app/models/digital_asset.rb', line 132

has_many :digital_asset_product_lines, -> { order(:position) }, inverse_of: :digital_asset, validate: false

#dimensionsObject



453
454
455
456
457
# File 'app/models/digital_asset.rb', line 453

def dimensions
  return unless attachment_width.present? && attachment_height.present?

  "#{attachment_width} x #{attachment_height}"
end

#externally_referenced?Boolean

True when some record we do NOT own points at this asset, so a purge must
retain it (vs. owned dependents like embeddings/profiles, which cascade).
Base assets expose no external references; subclasses (Image) override.

Returns:

  • (Boolean)


404
405
406
# File 'app/models/digital_asset.rb', line 404

def externally_referenced?
  false
end

#file_basenameObject



408
409
410
411
412
# File 'app/models/digital_asset.rb', line 408

def file_basename
  return if attachment_name.blank?

  File.basename(attachment_name, '.*')
end

#generated_imagesActiveRecord::Relation<GeneratedImage>

Returns:

See Also:



135
# File 'app/models/digital_asset.rb', line 135

has_many :generated_images, foreign_key: :source_image_id, dependent: :destroy, inverse_of: :source_image

#invalidate!(reason: nil) ⇒ Boolean

Quarantine this asset: stamp the moment its file was detected permanently
broken so it drops out of active fetches/embedding and starts the 30-day
recovery clock before InvalidDigitalAssetPurgeWorker hard-deletes it.
Idempotent — never moves the clock once set (a re-detection shouldn't extend
the retention window). Returns true if it newly invalidated.

Parameters:

  • reason (String, nil) (defaults to: nil)

    short note for the log (e.g. the error message)

Returns:

  • (Boolean)

    true if it was just invalidated, false if already invalid



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

def invalidate!(reason: nil)
  # Atomic conditional update — only the first writer flips invalid_at, so
  # concurrent callers can't overwrite it and extend the 30-day purge clock.
  # (update_all bypasses the in-memory attribute; reload if you need it.)
  return false if self.class.where(id: id, invalid_at: nil).update_all(invalid_at: Time.current).zero?

  Rails.logger.warn("[DigitalAsset] Invalidated ##{id} (#{self.class.name})#{reason && ": #{reason}"}")
  true
end

#is_image?Boolean

Returns:

  • (Boolean)


478
479
480
481
482
# File 'app/models/digital_asset.rb', line 478

def is_image?
  return true if type == 'Image'

  false
end

#is_video?Boolean

Returns:

  • (Boolean)


472
473
474
475
476
# File 'app/models/digital_asset.rb', line 472

def is_video?
  return true if type == 'Video'

  false
end

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



138
# File 'app/models/digital_asset.rb', line 138

has_and_belongs_to_many :items, validate: false

#opportunitiesActiveRecord::Relation<Opportunity>

Returns:

See Also:



142
# File 'app/models/digital_asset.rb', line 142

has_and_belongs_to_many :opportunities, validate: false, inverse_of: :digital_assets

#partiesActiveRecord::Relation<Party>

Returns:

  • (ActiveRecord::Relation<Party>)

See Also:



139
# File 'app/models/digital_asset.rb', line 139

has_and_belongs_to_many :parties, validate: false

#product_categoriesActiveRecord::Relation<ProductCategory>

Returns:

See Also:



137
# File 'app/models/digital_asset.rb', line 137

has_and_belongs_to_many :product_categories, validate: false

#product_linesActiveRecord::Relation<ProductLine>

Returns:

See Also:



133
# File 'app/models/digital_asset.rb', line 133

has_many :product_lines, through: :digital_asset_product_lines, validate: false

#product_lines_displayObject



449
450
451
# File 'app/models/digital_asset.rb', line 449

def product_lines_display
  product_lines.map(&:name).join(', ')
end

#product_lines_for_sortingObject



441
442
443
# File 'app/models/digital_asset.rb', line 441

def product_lines_for_sorting
  product_lines.map(&:self_and_ancestors).flatten.map(&:slug_ltree).map { |slt| "product-line-#{slt}" }.join(' ')
end

#purge_edge_cacheObject



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

def purge_edge_cache
  urls = site_maps.map(&:url)
  EdgeCacheWorker.perform_async('urls' => urls) if urls.present?
end

#reviewsActiveRecord::Relation<Review>

Legacy associations removed: showcases, showcase_rooms

Returns:

  • (ActiveRecord::Relation<Review>)

See Also:



141
# File 'app/models/digital_asset.rb', line 141

has_and_belongs_to_many :reviews, validate: false

#sanitize_urlsObject



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
# File 'app/models/digital_asset.rb', line 494

def sanitize_urls
  sd = Seo::DeparameterizeLinks.new
  ld = Seo::HtmlLinkSanitizer.new
  imgf = Seo::ImageMissingSizeFiller.new

  if expanded_description.present?
    logger.info "Sanitizing urls in expanded_description for digital asset:#{id}"
    h = expanded_description
    h = ld.process(h).html_out
    h = sd.process(h).html_out
    h = imgf.process(h).html_out
    # Nokogiri URL-encodes spaces in href attributes, corrupting {{ locale }} to {{%20locale%20}}.
    h = h.gsub(/\{\{%20([\w_]+)%20\}\}/) { "{{#{Regexp.last_match(1)}}}" }
    self.expanded_description = h
  end

  return if transcript.blank?

  logger.info "Sanitizing urls in transcript for digital asset:#{id}"
  h = transcript
  h = ld.process(h).html_out
  h = sd.process(h).html_out
  h = imgf.process(h).html_out
  # Nokogiri URL-encodes spaces in href attributes, corrupting {{ locale }} to {{%20locale%20}}.
  h = h.gsub(/\{\{%20([\w_]+)%20\}\}/) { "{{#{Regexp.last_match(1)}}}" }
  self.transcript = h
end

#seo_titleObject



414
415
416
417
# File 'app/models/digital_asset.rb', line 414

def seo_title
  s = meta_title.presence || title.presence || file_basename
  s&.first(133)
end

#should_generate_new_friendly_id?Boolean

Returns:

  • (Boolean)


437
438
439
# File 'app/models/digital_asset.rb', line 437

def should_generate_new_friendly_id?
  meta_title_changed? || title_changed? || attachment_name_changed? || url_changed? || force_new_slug.to_b
end

#should_sanitize_urls?Boolean

Returns:

  • (Boolean)


522
523
524
525
# File 'app/models/digital_asset.rb', line 522

def should_sanitize_urls?
  (expanded_description_changed? && expanded_description.present?) ||
    (transcript_changed? && transcript.present?)
end

#site_mapsActiveRecord::Relation<SiteMap>

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



134
# File 'app/models/digital_asset.rb', line 134

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

#slug_candidatesObject



419
420
421
422
423
424
425
426
427
# File 'app/models/digital_asset.rb', line 419

def slug_candidates
  sc = []
  if url.present?
    sc << [:url]
    sc << %i[url asset_identifier]
  end
  sc << %i[seo_title asset_identifier]
  sc
end

#tags_displayObject



445
446
447
# File 'app/models/digital_asset.rb', line 445

def tags_display
  tags.join(', ')
end

#thumbnail_urlObject



429
430
431
# File 'app/models/digital_asset.rb', line 429

def thumbnail_url
  # Placeholder, implement in specialized class
end


459
460
461
462
463
464
465
466
467
468
469
470
# File 'app/models/digital_asset.rb', line 459

def touch_related
  return unless @refresh_cache.to_b

  if product_line_ids.present?
    paths = ProductLine.where(id: product_line_ids).pluck(:ltree_path_ids).compact
    unless paths.empty?
      ProductLine.where(ProductLine[:ltree_path_ids].ltree_descendant(paths))
                 .find_each(&:touch)
    end
  end
  items.find_each(&:touch)
end