Class: Upload

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/upload.rb

Overview

== Schema Information

Table name: uploads
Database name: primary

id :integer not null, primary key
asset :jsonb
attachment_mime_type :string
attachment_name :string
attachment_processing :boolean
attachment_size :integer
attachment_uid :string
category :string(255)
document_date :date
expiration_date :date
note :text
position :integer
resource_type :string(255)
template :string
thumbnail_asset :jsonb
type :string(30)
created_at :datetime
updated_at :datetime
resource_id :integer

Indexes

idx_resource_type_category (resource_type,category) WHERE (resource_id IS NULL)
idx_resource_type_category_resource_id (resource_type,category,resource_id)
index_uploads_on_category (category)
index_uploads_on_resource_type_and_resource_id (resource_type,resource_id)
index_uploads_on_type (type) WHERE (type IS NOT NULL)

Direct Known Subclasses

Literature

Defined Under Namespace

Classes: CurrentPaths

Constant Summary collapse

MODEL_TO_CATEGORIES =

.each_with_object({}) {|cat, hsh| hsh[cat] = cat.to_s.humanize.titleize }.freeze

{
  activity: %i[activity_attachment msrp_quote_pdf_transmission organization_quote_pdf_transmission room_layout],
  call_record: %i[call_record],
  certification: %i[agreement liability_insurance],
  check: %i[check],
  credit_memo: %i[credit_memo_pdf statement_of_account_pdf],
  contact: %i[liability_insurance contract],
  customer: %i[liability_insurance contract],
  delivery: %i[all_intl_forms_pdf all_labels_pdf manually_generated_combined_pdf manual_ship_label pick_slip_pdf serial_numbers_pdf ship_bol_pdf ship_ci_pdf electronic_ship_ci_pdf split_pick_slip_pdf],
  employee: %i[certificate contract resume form],
  item: %i[usmca_certificate other_certificate],
  invoice: %i[invoice_pdf statement_of_account_pdf],
  mail_activity: %i[all_labels_pdf],
  manifest: %i[manifest_pdf],
  speedee_manifest: %i[summary_pdf manifest_csv],
  opportunity: %i[installation_plan_cad installation_plan_image room_layout room_layout_image],
  order: %i[custom_order_agreement custom_packing_slip_pdf early_ship_label installation_plan_pdf invalid_pick_slip_pdf manual_ship_label manual_smart_preset_form master_carton_label other pick_slip_pdf purchase_order ship_bol_pdf],
  purchase_order: %i[purchase_order],
  quote: %i[copy_of_plans_pdf msrp_quote_pdf organization_quote_pdf],
  rma: %i[letter_ship_label_pdf photo rma_return_instructions_pdf],
  rma_item: %i[photo not_returned_photo],
  room_configuration: %i[design_guideline electrical_plan_pdf installation_plan_cad installation_plan_image installation_plan_pdf installation_heated_area other photo room_layout room_layout_image warranty_card warranty_card_pdf
                         heat_loss_customer_form heat_loss_report multizone_plan multizone_plan_pdf],
  service_job: %i[room_layout tstat_job_result],
  shipment: %i[container_label letter_ship_label_pdf manual_ship_label ship_bol_pdf ship_label_image ship_label_pdf],
  statement_of_account: %i[statement_of_account_pdf],
  store: %i[super_pick_slip],
  supplier: %i[price_list contract],
  edi_communication_log: %i[custom_packing_slip_pdf feed_document_result_xml feed_document_result_json feed_document_result_gzip],
  exported_catalog_item_packet: %i[exported_packet],
  catalog_item: %i[amalytix_snapshot],
  video: %i[audio_extraction captions],
  assistant_conversation: %i[assistant_attachment],
  tax_exemption: %i[tax_exemption]
}.freeze
UNCLASSIFIED_CATEGORIES =

Unclassified categories.

%i[accounting_documents all_postage_labels_path_pdf article_pdf fedex_manifest google_local_inventory_feed misc preview_organization_quote_pdf price_list_pdf reviews_io_product_feed super_invoice super_pick_slip super_pick_slip_pdf super_serial_number_label_pdf super_soa
agreement_pdf order_review archive declaration_of_steel_aluminimum_copper].freeze
EXCLUDE_CATEGORIES_FOR_MANUAL_UPLOAD =

we don't want people accidentally manually uploading the wrong category when these are only generated by HW

%i[all_postage_labels_path_pdf article_pdf preview_organization_quote_pdf price_list_pdf super_pick_slip_pdf super_serial_number_label_pdf super_soa agreement_pdf electrical_plan_pdf installation_plan_pdf multizone_plan_pdf].freeze
CATEGORIES =

Categories.

(MODEL_TO_CATEGORIES.values.flatten + UNCLASSIFIED_CATEGORIES).uniq
IMAGE_EXTENSIONS =

Image extensions.

Marcel::EXTENSIONS.select { |_, mime_type| mime_type.start_with?('image/') }.keys.freeze
RESOURCES_WITH_UPLOADS_COUNT =

Counter cache for uploads_count on polymorphic resources
Only increment for resource types that have an uploads_count column

%w[Activity Customer Contact Employee Order Quote Rma Shipment Supplier].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

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, ransackable_scopes, 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

#attachmentObject (readonly)



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

validates :attachment, presence: true

#importObject

Returns the value of attribute import.



128
129
130
# File 'app/models/upload.rb', line 128

def import
  @import
end

#local_file_pathObject

Returns the value of attribute local_file_path.



128
129
130
# File 'app/models/upload.rb', line 128

def local_file_path
  @local_file_path
end

#should_destroyObject

Returns the value of attribute should_destroy.



128
129
130
# File 'app/models/upload.rb', line 128

def should_destroy
  @should_destroy
end

#templateObject (readonly)



143
# File 'app/models/upload.rb', line 143

validates :template, presence: true, if: proc { |u| u.category == 'installation_plan_image' }

Class Method Details

.all_categories_for_selectObject



176
177
178
# File 'app/models/upload.rb', line 176

def self.all_categories_for_select
  CATEGORIES.sort.map { |c| [c.to_s.humanize.titleize, c.to_s] }
end

.custom_ship_labelsActiveRecord::Relation<Upload>

A relation of Uploads that are custom ship labels. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



150
# File 'app/models/upload.rb', line 150

scope :custom_ship_labels, -> { where(category: %w[master_carton_label manual_ship_label]) }

.has_document_date?(resource_type) ⇒ Boolean

Returns:

  • (Boolean)


549
550
551
# File 'app/models/upload.rb', line 549

def self.has_document_date?(resource_type)
  %w[StatementOfAccount CreditMemo Invoice].include?(resource_type)
end

.has_role_restrictions?(resource_type) ⇒ Boolean

Returns:

  • (Boolean)


541
542
543
# File 'app/models/upload.rb', line 541

def self.has_role_restrictions?(resource_type)
  resource_type == 'Employee'
end

.in_categoryActiveRecord::Relation<Upload>

A relation of Uploads that are in category. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



147
# File 'app/models/upload.rb', line 147

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

.invalidActiveRecord::Relation<Upload>

A relation of Uploads that are invalid. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



146
# File 'app/models/upload.rb', line 146

scope :invalid, -> { where("category LIKE 'invalid_%'") }

.literaturesActiveRecord::Relation<Upload>

A relation of Uploads that are literatures. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



151
# File 'app/models/upload.rb', line 151

scope :literatures, -> { where(type: 'Literature').joins(:publication) }

.most_recent_firstActiveRecord::Relation<Upload>

A relation of Uploads that are most recent first. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



148
# File 'app/models/upload.rb', line 148

scope :most_recent_first, -> { reorder('uploads.created_at DESC') }

.options_for_categories_select(class_name, include_other: true, exclude: []) ⇒ Object



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

def self.options_for_categories_select(class_name, include_other: true, exclude: [])
  categories = []
  if class_name.present?
    symbolized_class_name = class_name if class_name.is_a?(Symbol)
    symbolized_class_name ||= class_name.tableize.singularize.to_sym
    new_categories = MODEL_TO_CATEGORIES[symbolized_class_name]
    categories += new_categories if new_categories.present?
  end
  categories += [:other] if include_other
  categories.reject! { |cat| cat.in?(exclude) }
  categories.reject! { |cat| cat.in?(EXCLUDE_CATEGORIES_FOR_MANUAL_UPLOAD) }
  categories.uniq.map { |cat| [cat.to_s.humanize.titleize.gsub('Custom', 'This is public and always enclosed: * Custom'), cat] }.sort_by { |cat_arr| cat_arr.last == :purchase_order ? '0' : cat_arr.first }
end

.sorted_by_positionActiveRecord::Relation<Upload>

A relation of Uploads that are sorted by position. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



149
# File 'app/models/upload.rb', line 149

scope :sorted_by_position, -> { reorder('uploads.position, uploads.created_at DESC') }

.temp_location(filename) ⇒ Object



328
329
330
331
332
# File 'app/models/upload.rb', line 328

def self.temp_location(filename)
  tmp_dir = Rails.application.config.x.temp_storage_path
  FileUtils.mkdir(tmp_dir) unless File.directory?(tmp_dir)
  tmp_dir.join(filename)
end

.uploadify(path, category, resource = nil, filename = nil, _do_not_delete = false, expiration_date = nil) ⇒ Object

Raises:

  • (ArgumentError)


287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/models/upload.rb', line 287

def self.uploadify(path, category, resource = nil, filename = nil, _do_not_delete = false, expiration_date = nil)
  raise ArgumentError, "Upload path is nil (category: #{category}, resource: #{resource&.class&.name} ##{resource&.id})" if path.nil?

  if URI(path.to_s).host.present?
    uploadify_from_url(file_name: filename, url: path, category:, resource:, expiration_date:)
  elsif File.exist?(path)
    upload = new(category:, resource:, expiration_date:)
    upload.attachment_name ||= filename
    upload.attachment = File.new(path)
    upload.save!
    # We store a local path to speed up when the upload is called within the same thread
    upload.set_local_file_path(path)
    upload
  else
    msg = "File is missing at #{path}, category: #{category}, resource: #{resource&.class&.name} #{resource&.id}"
    raise StandardError, msg
  end
end

.uploadify_from_data(file_name:, data:, category:, resource: nil) ⇒ Object



306
307
308
309
310
311
312
313
314
315
# File 'app/models/upload.rb', line 306

def self.uploadify_from_data(file_name:, data:, category:, resource: nil)
  file_name = ActiveStorage::Filename.new(file_name).sanitized
  file_path = temp_location(file_name)
  File.open(file_path, 'wb') do |file|
    file.write data
    file.flush
    file.fsync
  end
  uploadify(file_path, category, resource, file_name)
end

.uploadify_from_url(file_name:, url:, category:, resource: nil, _expiration_date: nil, headers: nil) ⇒ Object



317
318
319
320
321
322
323
324
325
326
# File 'app/models/upload.rb', line 317

def self.uploadify_from_url(file_name:, url:, category:, resource: nil, _expiration_date: nil, headers: nil)
  require 'down'
  file_path = temp_location(file_name)
  headers = headers.stringify_keys if headers.present?
  headers ||= {}
  Retryable.retryable(tries: 3, sleep: ->(n) { 4**n }, on: Retryable::TIMEOUT_CLASSES) do |_attempt_number, _exception|
    Down.download(url, headers:, destination: file_path) { |client| client.timeout(read: 60) }
  end
  uploadify(file_path, category, resource, file_name)
end

.validActiveRecord::Relation<Upload>

A relation of Uploads that are valid. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



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

scope :valid, -> { where("category NOT LIKE 'invalid_%'") }

Instance Method Details

#active_download_token(expires_at = nil) ⇒ Object



184
185
186
187
188
# File 'app/models/upload.rb', line 184

def active_download_token(expires_at = nil)
  dt = download_tokens.where('expires_at IS NULL or expires_at > ?', Time.current).order(Arel.sql('expires_at IS NOT NULL, expires_at DESC')).first
  dt ||= create_new_download_token(expires_at)
  dt.token
end

#articlesActiveRecord::Relation<Article>

Returns:

  • (ActiveRecord::Relation<Article>)

See Also:



106
# File 'app/models/upload.rb', line 106

has_and_belongs_to_many :articles

#attachment_extObject



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

def attachment_ext
  File.extname(attachment_name).downcase.delete_prefix('.') if attachment_name.present?
end

#browser_previewable?Boolean

True for file types modern browsers can render natively (PDFs and images).
Used by the show action to decide whether to serve inline or force download.

Returns:

  • (Boolean)


428
429
430
# File 'app/models/upload.rb', line 428

def browser_previewable?
  is_pdf? || is_image?
end

#cad_category?Boolean

Returns:

  • (Boolean)


537
538
539
# File 'app/models/upload.rb', line 537

def cad_category?
  category.in?(%w[room_layout_cad installation_plan_cad tracer_solution_cad])
end

#clone_with_file(new_resource = nil) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'app/models/upload.rb', line 268

def clone_with_file(new_resource = nil)
  new_upload = nil
  return if attachment.blank?

  begin
    new_upload = Upload.new(category:,
                            resource: new_resource,
                            template:,
                            note:)
    new_upload.attachment_url = presigned_url(expires_in: 10.minutes)
    new_upload.attachment_name = attachment_name
    new_upload.attachment_mime_type = mime_type
  rescue StandardError => e
    Rails.logger.error "File could not be found for #{id}, #{e}"
    new_upload = nil
  end
  new_upload
end

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



105
# File 'app/models/upload.rb', line 105

has_and_belongs_to_many :communications

#create_new_download_token(expires_at = nil) ⇒ Object



180
181
182
# File 'app/models/upload.rb', line 180

def create_new_download_token(expires_at = nil)
  download_tokens.create(expires_at:)
end

#deletable?Boolean

Returns:

  • (Boolean)


371
372
373
374
375
376
377
# File 'app/models/upload.rb', line 371

def deletable?
  if resource_type == 'RoomConfiguration'
    !resource.complete?
  else
    false
  end
end

#determine_orientationObject



214
215
216
217
218
219
220
221
# File 'app/models/upload.rb', line 214

def determine_orientation
  case template
  when /landscape/
    'Landscape'
  else
    'Portrait'
  end
end

#determine_page_sizeObject



199
200
201
202
203
204
205
206
207
208
# File 'app/models/upload.rb', line 199

def determine_page_size
  case template
  when /tabloid/
    'Tabloid'
  when /plotter/
    'Plotter'
  else
    'Letter'
  end
end

#download_tokensActiveRecord::Relation<DownloadToken>

Returns:

See Also:



103
# File 'app/models/upload.rb', line 103

has_many :download_tokens, inverse_of: :upload

#file_exists?Boolean

Returns:

  • (Boolean)


361
362
363
364
365
366
367
368
369
# File 'app/models/upload.rb', line 361

def file_exists?
  return false if attachment.nil?

  signed_url = presigned_url(expires_in: 60.minutes)
  response = HTTP.headers('Range' => 'bytes=0-0').get(signed_url)
  response.status.success?
rescue HTTP::Error, Dragonfly::Job::Fetch::NotFound
  false
end

#file_name_for_downloadObject



210
211
212
# File 'app/models/upload.rb', line 210

def file_name_for_download
  attachment_name # Literature subclass specializes this
end

#get_local_file_pathObject

Returns the local file path but only if the file still exists on disk.
Checks the instance attr first, then falls back to the process-level cache
so that freshly loaded instances of the same record can reuse temp files.



560
561
562
563
564
565
566
567
568
569
570
# File 'app/models/upload.rb', line 560

def get_local_file_path
  path = local_file_path || (id && CurrentPaths[id])
  if path.present? && File.exist?(path.to_s)
    self.local_file_path = path
    path
  else
    self.local_file_path = nil
    CurrentPaths.delete(id) if id
    nil
  end
end

#has_document_date?Boolean

Returns:

  • (Boolean)


553
554
555
# File 'app/models/upload.rb', line 553

def has_document_date?
  self.class.has_document_date?(resource_type)
end

#has_role_restrictions?Boolean

Returns:

  • (Boolean)


545
546
547
# File 'app/models/upload.rb', line 545

def has_role_restrictions?
  self.class.has_role_restrictions?(resource_type)
end

#has_thumbnail?Boolean

Returns:

  • (Boolean)


432
433
434
# File 'app/models/upload.rb', line 432

def has_thumbnail?
  is_image_or_pdf?
end

#image_attachment(dimensions) ⇒ Object



436
437
438
439
440
441
442
# File 'app/models/upload.rb', line 436

def image_attachment(dimensions)
  if is_image?
    attachment.thumb(dimensions)
  elsif is_pdf? # pdf
    pdf_thumbnail_attachment(dimensions)
  end
end

#image_category?Boolean

Returns:

  • (Boolean)


533
534
535
# File 'app/models/upload.rb', line 533

def image_category?
  category.in?(%w[installation_plan_image tracer_solution_image])
end

#image_url(dimensions) ⇒ Object



444
445
446
# File 'app/models/upload.rb', line 444

def image_url(dimensions)
  image_attachment(dimensions)&.url
end

#is_audio?Boolean

Returns:

  • (Boolean)


398
399
400
# File 'app/models/upload.rb', line 398

def is_audio?
  mime_type&.start_with?('audio/')
end

#is_image?Boolean

Returns:

  • (Boolean)


406
407
408
# File 'app/models/upload.rb', line 406

def is_image?
  mime_type&.start_with?('image/') || (undefined_mime_type? && IMAGE_EXTENSIONS.include?(attachment_ext))
end

#is_image_or_pdf?Boolean

Returns:

  • (Boolean)


410
411
412
# File 'app/models/upload.rb', line 410

def is_image_or_pdf?
  is_image? || is_pdf?
end

#is_mp3?Boolean

Returns:

  • (Boolean)


393
394
395
396
# File 'app/models/upload.rb', line 393

def is_mp3?
  (mime_type =~ %r{^audio/(?:mp3|mpeg|mpeg3|mpg|x-mp3|x-mpeg|x-mpeg3|x-mpegaudio|x-mpg)$}).present? ||
    (undefined_mime_type? && attachment_ext == 'mp3')
end

#is_pdf?Boolean

Returns:

  • (Boolean)


422
423
424
# File 'app/models/upload.rb', line 422

def is_pdf?
  mime_type == 'application/pdf' || (undefined_mime_type? && attachment_ext == 'pdf')
end

#is_video?Boolean

Returns:

  • (Boolean)


402
403
404
# File 'app/models/upload.rb', line 402

def is_video?
  mime_type&.start_with?('video/')
end

#is_wav?Boolean

Returns:

  • (Boolean)


388
389
390
391
# File 'app/models/upload.rb', line 388

def is_wav?
  ['audio/wav', 'audio/x-wav'].include?(mime_type) ||
    (undefined_mime_type? && attachment_ext == 'wav')
end

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



108
# File 'app/models/upload.rb', line 108

has_and_belongs_to_many :items

#mime_typeObject



379
380
381
382
383
384
385
386
# File 'app/models/upload.rb', line 379

def mime_type
  return attachment_mime_type if attachment_mime_type.present?

  # Since we disabled automatic MIME type detection in Dragonfly,
  # we should rely on the stored attachment_mime_type field
  # This avoids triggering file downloads from S3
  attachment_mime_type
end

#mime_type_symbolObject



517
518
519
520
521
# File 'app/models/upload.rb', line 517

def mime_type_symbol
  attachment.ext.to_sym
rescue StandardError
  nil
end

#ok_to_delete?Boolean

Returns:

  • (Boolean)


523
524
525
526
527
# File 'app/models/upload.rb', line 523

def ok_to_delete?
  # Check if this upload is used as literature for any publication (Item)
  # Use direct database query to avoid association caching issues
  !Item.exists?(literature_id: id) && resource.nil?
end

#options_for_categories_select(include_other: true, exclude: []) ⇒ Object



354
355
356
357
358
359
# File 'app/models/upload.rb', line 354

def options_for_categories_select(include_other: true, exclude: [])
  class_name_to_use = resource&.class&.name
  # When upload are used with Item, the resource is nil
  class_name_to_use ||= :item if items.present?
  self.class.options_for_categories_select(class_name_to_use, include_other:, exclude:)
end

#pdf_category?Boolean

Returns:

  • (Boolean)


529
530
531
# File 'app/models/upload.rb', line 529

def pdf_category?
  category.in?(%w[custom_packing_slip_pdf purchase_order])
end

#pdf_thumbnail_attachment(dimensions = '600x600>', density: 150) ⇒ Object



448
449
450
451
452
453
454
455
# File 'app/models/upload.rb', line 448

def pdf_thumbnail_attachment(dimensions = '600x600>', density: 150)
  return unless is_pdf?

  # Using libvips options: page (first page), dpi for resolution
  # 150 DPI is sufficient for thumbnails up to ~1200px (8.5" * 150 = 1275px)
  # AVIF output for best compression/quality ratio; Cloudflare handles browser fallback
  attachment.thumb(dimensions, format: :avif, input_options: { page: 0, dpi: density })
end

#pdf_thumbnail_url(dimensions = '600x600>', density: 400) ⇒ Object



457
458
459
460
461
# File 'app/models/upload.rb', line 457

def pdf_thumbnail_url(dimensions = '600x600>', density: 400)
  return unless is_pdf?

  pdf_thumbnail_attachment(dimensions, density:).url
end

#presigned_url(expires_in: 1.day, download: false, file_name: nil) ⇒ Object

Generate a presigned URL for the attachment locally without S3 network calls.
Uses Dragonfly's datastore.url_for which is purely local AWS Sig V4 computation.
Use this for preview/download links where you need a signed URL quickly.



478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'app/models/upload.rb', line 478

def presigned_url(expires_in: 1.day, download: false, file_name: nil)
  return nil unless attachment_uid.present? && attachment.present?

  query_params = {}
  if download || file_name.present?
    safe_filename = (file_name || attachment_name).to_s.parameterize(separator: '_')
    safe_filename = "#{safe_filename}#{File.extname(attachment_name)}" unless safe_filename.include?('.')
    disposition = download ? 'attachment' : 'inline'
    query_params['response-content-disposition'] = "#{disposition}; filename=\"#{safe_filename}\""
  end

  # Get datastore from the attachment's app (not hardcoded)
  attachment.app.datastore.url_for(
    attachment_uid,
    scheme: 'https',
    expires: expires_in.from_now.to_i,
    query: query_params.presence
  )
end

#publicationItem

Returns:

See Also:



90
# File 'app/models/upload.rb', line 90

has_one :publication, class_name: 'Item', inverse_of: :literature, foreign_key: :literature_id

#recipient_party_idObject



498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'app/models/upload.rb', line 498

def recipient_party_id
  p = begin
    resource.customer_id
  rescue StandardError
    nil
  end
  p ||= begin
    resource.party_id
  rescue StandardError
    nil
  end
  p ||= begin
    upload.resource.supplier_id
  rescue StandardError
    nil
  end
  p
end

#resourceResource

Returns:

  • (Resource)

See Also:



89
# File 'app/models/upload.rb', line 89

belongs_to :resource, polymorphic: true, optional: true

#resource_comboObject



256
257
258
# File 'app/models/upload.rb', line 256

def resource_combo
  "#{resource_type}|#{resource_id}" if resource
end

#resource_combo=(combo_val) ⇒ Object



260
261
262
263
264
265
266
# File 'app/models/upload.rb', line 260

def resource_combo=(combo_val)
  return if combo_val.blank?

  values = combo_val.split('|')
  self.resource_type = values[0]
  self.resource_id = values[1]
end

#rolesActiveRecord::Relation<Role>

Returns:

  • (ActiveRecord::Relation<Role>)

See Also:



107
# File 'app/models/upload.rb', line 107

has_and_belongs_to_many :roles

#scope_conditionObject

Uploads should only be sorted based on the presence of resource id of category
otherwise if those are null then the whole upload table gets a shuffle. big
performance hit and deadlocks
:resource_id, :category



159
160
161
162
163
164
165
166
167
168
169
170
# File 'app/models/upload.rb', line 159

def scope_condition
  if resource_type && resource_id
    cnds = {
      resource_type:,
      resource_id:
    }
    cnds[:category] = category if category.present?
    cnds
  else
    { id: -1 }
  end
end

#set_local_file_path(file_path) ⇒ Object

Ensures our local file path is stored in the temp storage directory (for
sendfile header acceleration) and registers it in the request-scoped cache
so other instances of the same record can find it without hitting S3.



575
576
577
578
579
580
581
582
583
584
# File 'app/models/upload.rb', line 575

def set_local_file_path(file_path)
  if File.dirname(file_path.to_s) == Rails.application.config.x.temp_storage_path.to_s
    self.local_file_path = file_path
  else
    self.local_file_path = Rails.application.config.x.temp_storage_path.join(File.basename(file_path))
    FileUtils.cp(file_path, local_file_path.to_s)
  end
  CurrentPaths[id] = local_file_path.to_s if id.present?
  local_file_path
end

#template_options_for_selectObject



348
349
350
351
352
# File 'app/models/upload.rb', line 348

def template_options_for_select
  return [] unless resource.respond_to?(:template_options_for_select)

  resource.template_options_for_select
end

#thumbnail_2x_url(dimensions: '184x122>') ⇒ Object



469
470
471
472
473
# File 'app/models/upload.rb', line 469

def thumbnail_2x_url(dimensions: '184x122>')
  return unless has_thumbnail?

  image_attachment(dimensions)&.url
end

#thumbnail_url(dimensions: '92x61>') ⇒ Object



463
464
465
466
467
# File 'app/models/upload.rb', line 463

def thumbnail_url(dimensions: '92x61>')
  return unless has_thumbnail?

  image_attachment(dimensions)&.url
end

#titleObject



194
195
196
197
# File 'app/models/upload.rb', line 194

def title
  # this is to support serialization of Installation Plan Vignettes
  category.humanize.titleize.to_s
end

#to_file(file_path: nil, file_name: nil) ⇒ Object

Downloads the file using the Down library to a temporary place



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'app/models/upload.rb', line 224

def to_file(file_path: nil, file_name: nil)
  # If we still have a local copy of the file we will return this right away
  if get_local_file_path
    Rails.logger.info "to_file getting upload #{id} from local path: #{get_local_file_path}"
    # If file_path specified, copy the file to that location
    return get_local_file_path unless file_path.present? && file_path.to_s != get_local_file_path

    FileUtils.cp get_local_file_path, file_path
    file_path

  else
    remote_url = presigned_url(expires_in: 15.minutes)
    Rails.logger.info "to_file getting upload #{id} from remote path #{remote_url}"
    # Is there an extension to this upload?
    remote_file_name = attachment_name.presence || URI.parse(url).path
    ext = begin
      File.extname(remote_file_name)
    rescue StandardError
      nil
    end
    # Future you can also look at the response headers using a chunked io download
    file_name ||= "tmp_#{id}_#{Time.current.strftime('%Y-%m-%d-%H%M%S')}#{ext}"
    file_path ||= Rails.application.config.x.temp_storage_path.join(file_name)
    Retryable.retryable(tries: 3, sleep: ->(n) { 4**n }, on: Retryable::TIMEOUT_CLASSES) do |attempt_number, exception|
      ErrorReporting.error(exception, message: "Exception on download attempt #{attempt_number} for #{remote_url}") if exception
      Down::Http.download(remote_url, destination: file_path) { |client| client.timeout(read: 30) }
    end
    set_local_file_path(file_path) # Speed up further retrievals
  end
  file_path.to_s
end

#to_sObject



190
191
192
# File 'app/models/upload.rb', line 190

def to_s
  attachment_name || "Upload id #{id}"
end

#undefined_mime_type?Boolean

Returns:

  • (Boolean)


418
419
420
# File 'app/models/upload.rb', line 418

def undefined_mime_type?
  mime_type.blank? || mime_type == 'application/octet-stream'
end