Class: SearchesController

Inherits:
CrmController show all
Defined in:
app/controllers/searches_controller.rb

Overview

Controller: searches.

Constant Summary

Constants included from Controllers::ReferenceFindable

Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Instance Method Summary collapse

Methods inherited from CrmController

#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #registration_lead_type, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

#advanced_search_panelObject

Lazy-loaded body for the navbar's Advanced Search offcanvas. The navbar
renders an empty <turbo-frame id="advancedSearchOffcanvasFrame"> shell;
crm-navbar Stimulus sets the frame's src to this action on every
show.bs.offcanvas, so the per-type recent-search and template queries
only run when the user actually opens the panel.

expires_in 15.seconds, public: false lets the user's browser cache
the response for rapid re-opens (matches the lazy navbar dropdown
bodies) while keeping responses out of any shared cache. Per-user
data, so private is mandatory.



42
43
44
45
# File 'app/controllers/searches_controller.rb', line 42

def advanced_search_panel
  expires_in 15.seconds, public: false
  render partial: 'advanced_search_offcanvas_body', layout: false
end

#createvoid

This method returns an undefined value.

Creates a search from submitted params, seeded like new.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/controllers/searches_controller.rb', line 107

def create
  template_key = params[:template_key] || :default
  p = search_class.try(:global_favorites).try(:dig, template_key) || {}
  p = p.slice(:query_params, :sort_columns)
  p = p.deep_merge(params[:search].to_h)
  @search = search_class.new(p)

  @search.employee_id = employee.id
  authorize! :create, @search
  if @search.save
    if @search.pinned
      @search.perform(1, nil, nil, true)
      redir_url = get_first_search_result_link
      if redir_url
        flash[:info] = 'Search results were automatically pinned'
        redirect_to redir_url
      else
        redirect_to search_path(@search)
      end
    elsif params[:edit].present?
      redirect_to edit_search_path(@search)
    else
      redirect_to search_path(@search)
    end
  else
    render :new, status: :unprocessable_content
  end
end

#create_onboarding_packetvoid

This method returns an undefined value.

Builds a zipped onboarding-packet CSV of the search's results and offers
it as a download.



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'app/controllers/searches_controller.rb', line 379

def create_onboarding_packet
  require 'zip'
  @search = Search.find(params[:id])
  zip_name = "onboarding_#{@search.id}.zip"
  directory_path = Rails.root.join(Rails.application.config.x.temp_storage_path.to_s, 'users', current_user ? current_user.id.to_s : '')
  FileUtils.mkdir_p(directory_path)
  zip_path = Rails.root.join(directory_path, zip_name)

  Zip::File.open(zip_path, create: true) do |zipfile|
    file_name = "onboarding_#{@search.id}#{"_#{zipfile.size}" if zipfile.size.positive?}.csv"
    file_path = Rails.root.join(directory_path, file_name)
    CSV.open(file_path, 'w', force_quotes: true, encoding: 'WINDOWS-1252') do |csv|
      csv << ['item_name', 'item_sku', 'product_category', 'brand_name', 'collection', 'UPC', 'product_weight',
              'product_height', 'product_width', 'product_depth', 'shipping_weight', 'shipping_height', 'shipping_width',
              'shipping_depth', 'shipping_type', 'product_description', 'bullet_point_1', 'bullet_point_2', 'bullet_point_3', 'bullet_point_4',
              'bullet_point_5', 'country_of_origin', 'Length', 'Amps', 'Weight', 'Width', 'Voltage', 'Heating Wire', 'Floor Types',
              'Approvals', 'Watts', 'BTU per Hour', 'Heated Area']

      @search.search_results.each do |result|
        row = []
        catalog_item = result.resource
        item = catalog_item.item
        row.push(item.name, item.sku, item.brand_id.equal?(1) ? 'WarmlyYours USA' : 'WarmlyYours CA', 'collection',
                 item.upc, item.base_weight, 'product_height', item.width, 'product_depth', item.shipping_weight, item.shipping_height,
                 item.shipping_width, 'shipping_depth', 'shipping_type',
                 item.detailed_description.presence || catalog_item.primary_product_line.description,
                 'bullet_point_1', 'bullet_point_2', 'bullet_point_3', 'bullet_point_4', 'bullet_point_5', item.coo)
        item.product_specifications.each do |spec|
          row << spec.get_specification_data(catalog_item).formatted
        end
        csv << row
      end
    end
    zipfile.add(file_name, file_path)
  end

  session[:download_path] = "/tempfile/#{zip_name}"
  flash[:info] = "File created <br><a href='#{session[:download_path]}' class='btn btn-outline-primary'>Download</a>"
  redirect_back_or_to(root_path)
end

#destroyvoid

This method returns an undefined value.

Deletes the current user's search.



442
443
444
445
446
# File 'app/controllers/searches_controller.rb', line 442

def destroy
  @search = @context_user.searches.find(params[:id])
  flash[:info] = "Search id #{@search.id} deleted." if @search.destroy
  redirect_to_return_path_or_default employee_searches_path(@context_user, type: @search.class.name)
end

#editvoid

This method returns an undefined value.

Loads a search for editing.



100
101
102
# File 'app/controllers/searches_controller.rb', line 100

def edit
  @search = Search.find(params[:id])
end

Finds a link (CRM route or polymorphic path) for the search's first result.

Returns:

  • (String, nil)

    the link, or nil when there are no results or none resolves



250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/controllers/searches_controller.rb', line 250

def get_first_search_result_link
  return unless (first_resource = @search.search_results.first&.resource)

  begin
    first_resource.crm_link
  rescue StandardError
    nil
  end || begin
    polymorphic_path(first_resource)
  rescue StandardError
    nil
  end
end

#indexvoid

This method returns an undefined value.

Lists the employee's saved searches for the given search type, or routes
to search_types when no type is given.



12
13
14
15
16
17
18
19
# File 'app/controllers/searches_controller.rb', line 12

def index
  if params[:type].present?
    search_class # sets the search class
    @searches = employee.searches.where(type: params[:type]).order(created_at: :desc)
  else
    redirect_to(search_types_searches_path)
  end
end

#mass_actionvoid

This method returns an undefined value.

Runs the requested mass action against the search's selected results.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'app/controllers/searches_controller.rb', line 335

def mass_action
  expires_now
  @search = Search.find(params[:id])
  @mass_action_type = params[:mass_action_type]
  if @search.search_results.empty?
    redirect_to search_path(@search), flash: { error: 'You must select some records before performing a mass action.' }
  elsif @mass_action_type.blank? || !@search.respond_to?(@mass_action_type)
    redirect_to search_path(@search), flash: { error: 'You must select a valid mass action to perform.' }
  else
    # Try to find if a partial is present to render a pre-execution form to capture params or an action to perform directly
    @mass_action = OpenStruct.new(params[:mass_action].to_h)
    view_root_path = Rails.root.join('app/views/searches')
    # Is there a search model specific view for this mass action?
    if File.exist?(view_root_path.join(@search.class.name.underscore, "_#{@mass_action_type}.html.erb"))
      @mass_action_partial_path = "/searches/#{@search.class.name.underscore}/#{@mass_action_type}"
    # Is there a generalized search view?
    elsif File.exist?(view_root_path.join("_#{@mass_action_type}.html.erb"))
      @mass_action_partial_path = "/searches/#{@mass_action_type}"
    # execute mass action directly
    else
      @search.send(@mass_action_type.to_sym, self)
    end
  end
end

#newvoid

This method returns an undefined value.

Builds a new, unsaved search, seeded from the given template's global favorites.



84
85
86
87
88
89
90
91
92
93
94
95
# File 'app/controllers/searches_controller.rb', line 84

def new
  template_key = params[:template_key] || :default
  p = search_class.try(:global_favorites).try(:dig, template_key) || {}
  p = p.slice(:query_params, :sort_columns)
  p = p.deep_merge(params[:search] || {})
  p[:set_limit] ||= 50
  p[:employee] ||= employee
  p[:query_params] = params[:query_params]&.to_h.presence || p[:query_params] || {}
  @search = search_class.new(p)
  @search.set_defaults
  authorize! :create, @search
end

#onboarding_packetvoid

This method returns an undefined value.

Renders the onboarding-packet export screen for the search's results.



363
364
365
366
367
368
369
370
371
372
373
# File 'app/controllers/searches_controller.rb', line 363

def onboarding_packet
  @search = Search.find(params[:id])
  # @search.search_results.each do |result|
  #   catalog_item = result.resource
  #   item = catalog_item.item
  #
  # end
  respond_to do |format|
    format.html
  end
end

#perform_mass_actionvoid

This method returns an undefined value.

Validates and runs the mass action submitted from the mass_action confirmation.



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'app/controllers/searches_controller.rb', line 423

def perform_mass_action
  mass_action_params = params[:mass_action].to_h.deep_squish.with_indifferent_access
  @search      = Search.find(params[:id])
  @mass_action = params[:mass_action_type]

  unless @mass_action.present? && @search.respond_to?(@mass_action)
    flash[:error] = "Mass action #{params[:mass_action]} not recognized"
    redirect_to search_path(@search) and return
  end

  # Every mass action method now owns its own background execution by enqueuing
  # a worker and returning { job_id: }. The controller always calls the method
  # directly and routes based on the response type.
  run_mass_action_synchronously(mass_action_params)
end

#pinvoid

This method returns an undefined value.

Marks the search pinned, if it has selected results.



277
278
279
280
281
282
283
284
285
# File 'app/controllers/searches_controller.rb', line 277

def pin
  @search = Search.find(params[:id])
  if @search.search_results.present?
    mark_pinned_and_redirect
  else
    flash[:error] = 'Your search has no results that can be pinned.  Select records before attempting pinning.'
    redirect_to search_path(@search)
  end
end

#pin_allvoid

This method returns an undefined value.

Records every result and marks the search pinned.



267
268
269
270
271
272
# File 'app/controllers/searches_controller.rb', line 267

def pin_all
  @search = Search.find(params[:id])
  @search.record_list('*')
  @search.update_attribute!(:pinned, true)
  mark_pinned_and_redirect
end

#pinned_resultsvoid

This method returns an undefined value.

Returns the given employee's pinned search results.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'app/controllers/searches_controller.rb', line 290

def pinned_results
  results = {}
  @search = Search.where(employee_id: params[:employee_id]).where(pinned: true).first
  @context_type = params[:context_type]
  @context_id = params[:context_id]
  @current_path = params[:current_path]
  if @search
    @pagy, @results = pagy(@search.get_pinned_results, limit: 20)
    results[:status] = :ok
    results[:results] = @results
  else
    results[:status] = :not_found
  end

  respond_to do |format|
    format.turbo_stream
    format.json { render json: results }
    format.html { render layout: false }
  end
end

#recordvoid

This method returns an undefined value.

Records/unrecords the given result ids on the search's mass-action selection.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/controllers/searches_controller.rb', line 160

def record
  @search = Search.find(params[:id])
  check_list_ids = params[:check_list_ids]
  uncheck_list_ids = params[:uncheck_list_ids]

  @search.record_list(check_list_ids)
  @search.unrecord_list(uncheck_list_ids)

  respond_to do |format|
    # Primary client path: render the mass-action button (counter +
    # dropdown items) server-side. The server knows the post-update
    # selected count and total_count, so it renders the right state
    # without any client-side sync logic.
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace("mass-action-button-#{@search.id}", partial: 'searches/search_actions')
    end
    # Kept for any non-browser caller.
    format.json { render json: { status: :ok, selected: @search.search_results.count, checked: check_list_ids, unchecked: uncheck_list_ids } }
  end
end

#refresh_pinnedObject

Prunes the user's pinned set against current data: keeps only the records
that still match the saved criteria (e.g. drops an opportunity that was
closed and no longer satisfies an "open" filter). Never auto-adds new
rows — pinning is a curated subset and we respect that. Mutating action —
POST only, and restricted to the current user's own pinned search.



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/controllers/searches_controller.rb', line 316

def refresh_pinned
  head :forbidden and return unless current_user.id.to_s == params[:employee_id].to_s

  @search = Search.where(employee_id: current_user.id, pinned: true).first
  @current_path = params[:current_path]

  if @search
    @search.refresh_pinned_results
    @pagy, @results = pagy(@search.get_pinned_results, limit: 20)
  end

  respond_to do |format|
    format.turbo_stream { render :pinned_results }
  end
end

#resetvoid

This method returns an undefined value.

Clears the search's saved query params.



139
140
141
142
143
144
# File 'app/controllers/searches_controller.rb', line 139

def reset
  @search = Search.find(params[:id])
  @search.query_params = nil
  @search.save
  redirect_to search_path(@search)
end

#search_and_pinObject

Executes a search and pin in one shot



234
235
236
237
238
239
240
241
242
243
244
245
# File 'app/controllers/searches_controller.rb', line 234

def search_and_pin
  @search = params[:id].present? ? Search.find(params[:id]) : search_class.new(params[:search])
  @search.employee_id = employee.id
  @search.pinned = true
  if @search.save
    @search.perform(1, nil, nil, true) # True stores the pinned search
    @results = @search.pinned_query
  end
  respond_to do |format|
    format.turbo_stream
  end
end

#search_and_showvoid

This method returns an undefined value.

Builds, saves, and runs a search in one step, rendering results inline
(used by embedded/turbo-frame search widgets).



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/controllers/searches_controller.rb', line 197

def search_and_show
  @search = search_class.new(set_limit: params[:set_limit] || 50, employee: @context_user, query_params: params[:query_params].to_h.presence || {},
                             selected_columns: params[:selected_columns], sort_columns: params[:sort_columns])
  @links = params[:links] || {}
  @target_id = params[:target_id] || 'search_and_show'
  action_view = params[:search_mode] == 'full' ? 'show' : 'search_and_show'
  unless @search.save
    # Search criteria failed validation: surface the error rather than
    # returning an empty 200 (which Turbo silently discards).
    flash.now[:error] = "Search could not be saved: #{@search.errors_to_s}"
    respond_to do |format|
      format.html { render plain: flash.now[:error], status: :unprocessable_content }
      format.turbo_stream { render partial: 'shared/flash', status: :unprocessable_content }
    end
    return
  end

  # Perform paginated query; Search will expose Pagy via pagy_from_search
  page = (params[:page] || 1).to_i
  # Pagy uses params[:items] for per-page count, fall back to saved set_limit
  per_page = params[:items].presence || @search.set_limit
  @results = @search.perform(page, params[:sort], params[:direction], false, per_page, true, false)
  # Create Pagy object with request context for URL generation
  if @search.pagy_from_search
    @pagy = pagy(:offset, @results,
                 count: @search.pagy_from_search.count,
                 page: @search.pagy_from_search.page,
                 limit: @search.pagy_from_search.limit)[0]
  end

  respond_to do |format|
    format.html { render action: action_view, layout: should_render_layout? }
    format.turbo_stream
  end
end

#search_typesvoid

This method returns an undefined value.

Lists the available search types.



24
25
26
27
28
29
30
# File 'app/controllers/searches_controller.rb', line 24

def search_types
  @search_types = Search.options
  # `should_render_layout?` keeps the response usable both as a real page
  # (e.g. when SearchesController#index redirects here with no `:type`)
  # and as a turbo-frame fragment.
  render layout: should_render_layout?
end

#showvoid

This method returns an undefined value.

Runs the search and displays its paginated results.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'app/controllers/searches_controller.rb', line 50

def show
  @search = Search.find(params[:id])
  authorize! :read, @search

  begin
    # Ask the search to return the current page; it will set result_set_size and pagy_from_search
    page = (params[:page] || 1).to_i
    # Pagy uses params[:items] for per-page count, fall back to saved set_limit
    per_page = params[:items].presence || @search.set_limit
    @results = @search.perform(page, params[:sort], params[:direction], false, per_page, true, false)
    # Create Pagy object with request context for URL generation
    if @search.pagy_from_search
      @pagy = pagy(:offset, @results,
                   count: @search.pagy_from_search.count,
                   page: @search.pagy_from_search.page,
                   limit: @search.pagy_from_search.limit)[0]
    end
  rescue StandardError => e
    Rails.logger.error "[SearchesController#show] perform failed for Search##{@search.id} (#{@search.type}): #{e.class}#{e.message}"
    Appsignal.report_error(e) do |transaction|
      transaction.set_tags(search_id: @search.id.to_s, search_type: @search.type)
    end
    @pagy = nil
    @results = []
  end

  respond_to do |format|
    format.html
  end
end

#unpinvoid

This method returns an undefined value.

Unpins (destroys) a pinned search result.



149
150
151
152
153
154
155
# File 'app/controllers/searches_controller.rb', line 149

def unpin
  @search_result_id = params[:search_result_id]
  @search_results = SearchResult.destroy_by(id: @search_result_id)
  respond_to do |format|
    format.turbo_stream
  end
end

#updatevoid

This method returns an undefined value.

Updates a search from submitted params.



184
185
186
187
188
189
190
191
# File 'app/controllers/searches_controller.rb', line 184

def update
  @search = Search.find(params[:id])
  if @search.update(params[:search])
    redirect_to search_path(@search)
  else
    render :edit, status: :unprocessable_content
  end
end