Class: Search
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Search
- Defined in:
- app/models/search.rb
Overview
Single-table-inheritance base class for every saved-query type in
the CRM (e.g. CustomerSearch, OrderSearch, etc.). Holds the user-
editable bundle of query params, selected columns, sort, pagination,
and pinning, and exposes a Ransack-backed query result via
QueryTemplate.
Subclasses configure themselves with default_selected_columns,
default_sort_columns, and composite_columns so each searchable
entity stays in sync with its underlying ActiveRecord model.
Direct Known Subclasses
ActivitySearch, BudgetSearch, CertificationSearch, ContactSearch, CouponSearch, CreditApplicationSearch, CreditMemoSearch, CustomerSearch, DeliverySearch, EmployeeReviewSearch, ExchangeRateSearch, InvoiceSearch, ItAssetSearch, ItemLedgerEntrySearch, ItemSearch, LedgerEntrySearch, LocatorRecordSearch, OpportunitySearch, OrderSearch, OutgoingPaymentSearch, ProductCatalogSearch, PurchaseOrderSearch, QuoteSearch, ReceiptSearch, RmaSearch, SalesCommissionSearch, ServiceJobSearch, SupplierItemSearch, SupportCaseSearch, VoucherSearch
Defined Under Namespace
Classes: ActivityPresenter, ActivityTextPresenter, BudgetPresenter, CertificationPresenter, ContactPresenter, ContactTextPresenter, CouponPresenter, CouponTextPresenter, CreditApplicationPresenter, CreditApplicationTextPresenter, CreditMemoPresenter, CustomerPresenter, CustomerTextPresenter, DeliveryPresenter, DeliveryTextPresenter, EmployeeReviewPresenter, EmployeeReviewTextPresenter, ExchangeRatePresenter, InvoicePresenter, InvoiceSummaryPresenter, InvoiceTextPresenter, ItAssetPresenter, ItemLedgerEntryPresenter, ItemLedgerEntryTextPresenter, ItemPresenter, ItemTextPresenter, LedgerEntryPresenter, LedgerEntryTextPresenter, LocatorRecordPresenter, LocatorRecordTextPresenter, OpportunityPresenter, OpportunitySummaryPresenter, OpportunityTextPresenter, OrderPresenter, OrderTextPresenter, OutgoingPaymentPresenter, PresenterFactory, PresetJobPresenter, PresetJobTextPresenter, ProductCatalogPresenter, ProductCatalogTextPresenter, PurchaseOrderPresenter, PurchaseOrderTextPresenter, QuotePresenter, QuoteTextPresenter, ReceiptPresenter, RmaPresenter, RmaTextPresenter, SalesCommissionPresenter, ServiceJobPresenter, SupplierItemPresenter, SupplierItemTextPresenter, SupportCasePresenter, SupportCaseTextPresenter, VoucherPresenter
Constant Summary collapse
- DISTANCE_SEARCH_KEYS =
Keys consumed by apply_distance_search, apply_customer_cross_reference, and geocoding
in the base class — excluded from the Ransack strict probe across all subclasses. %w[target_coordinates within_miles within_miles_of cross_reference_customer_search_id].freeze
Constants included from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Class Attribute Summary collapse
-
.composite_columns ⇒ Object
readonly
Returns the value of attribute composite_columns.
-
.default_selected_columns ⇒ Object
readonly
Returns the value of attribute default_selected_columns.
-
.default_sort_columns ⇒ Object
readonly
Returns the value of attribute default_sort_columns.
-
.global_favorites ⇒ Object
readonly
Returns the value of attribute global_favorites.
Instance Attribute Summary collapse
-
#composite_columns ⇒ Object
readonly
Returns the value of attribute composite_columns.
-
#pagy_count ⇒ Object
readonly
Expose raw pagination metadata for the controller to build a proper Pagy object with request context.
-
#pagy_limit ⇒ Object
readonly
Expose raw pagination metadata for the controller to build a proper Pagy object with request context.
-
#pagy_page ⇒ Object
readonly
Expose raw pagination metadata for the controller to build a proper Pagy object with request context.
-
#sql_select_columns ⇒ Object
readonly
Returns the value of attribute sql_select_columns.
Belongs to collapse
Has many collapse
Delegated Instance Attributes collapse
-
#view_resource_klass ⇒ Object
Alias for Class#view_resource_klass.
Class Method Summary collapse
- .allowed_role_ids ⇒ Object
-
.available_output_columns ⇒ Object
Return a list of available output columns combining composite columns and actual view defined columns.
-
.base_search_class_name ⇒ Object
Returns a base model view of what we're trying to earch on.
-
.composite_column(*options) ⇒ Object
Sets 'virtual' attribute or composite column which will be rendered in the presenter but require specific columns present in the select, define these at top of the search class such as customer: { select: [:customer_full_name, :customer_id]} in the example above we can specify customer as an output column which will translate in customer_id and customer_full_name being added to the select for the query.
- .custom_criteria_keys ⇒ Object
-
.database_columns ⇒ Object
Returns an array of database level columns names as symbols, these are defined by the view resource class and how its sql view is defined.
-
.default_columns(*columns) ⇒ Object
Sets the default columns to be used when creating this search use default_columns :id, :name, ...
-
.default_sort(*columns) ⇒ Object
Sets the default sort to be used when creating this search use default_columns :id, :name, ...
-
.favorites ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are favorites.
- .friendly_column_name(column_name) ⇒ Object
- .global_favorite(*options) ⇒ Object
- .has_role_for_search?(role_ids) ⇒ Boolean
- .instantiate_query_template(template_key = nil, extra_options = nil) ⇒ Object
- .main_resource_class ⇒ Object
- .main_resource_table ⇒ Object
- .mass_actions_for_select ⇒ Object
-
.maximum_unpersisted_queries ⇒ Object
Actions that must run synchronously in the controller because they render.
- .options(_visible_only = true) ⇒ Object
- .options_classes ⇒ Object
- .query_favorites_templates ⇒ Object
-
.recent_searches_for_employee_id ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are recent searches for employee id.
- .search_name ⇒ Object
- .search_type ⇒ Object
- .search_type_humanized ⇒ Object
-
.select_sort_columns ⇒ Object
Returns available columns for sorting.
- .unpersisted_queries_quote_reached?(search_type, employee_id) ⇒ Boolean
- .view_resource_class ⇒ Object
- .view_resource_klass ⇒ Object
- .view_resource_table ⇒ Object
- .visible? ⇒ Boolean
-
.with_search_results ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are with search results.
Instance Method Summary collapse
-
#all_composite_columns ⇒ Object
Returns a combination of instance level and class level composite columns instance levels are defined in the query build mechanism and unit to this search instance.
-
#append_custom_column(*options) ⇒ Object
Sometimes the criteria will append a custom field to the query that is not part of the initial view, we need to make this field part of the output and also the custom select pass options to this function as you would composite_column, e.g :distance, select: '(select max(x) as distance)'.
- #append_ouput_column(column_name) ⇒ Object
- #append_to_sql_select_columns(*args) ⇒ Object protected
-
#applicable_sort_terms ⇒ Object
Filters down the sort columns available and cross reference them with the columns that are in the query output.
-
#apply_array_match(results, param, column = nil, nil_value = nil, any_value = nil) ⇒ Object
protected
param: an array of values to test against nil_value: of the array of values, which one represent a database null, e.g.
- #apply_criteria ⇒ Object
-
#apply_customer_cross_reference(results, join_key = nil) ⇒ Object
Apply customer cross reference.
- #apply_distance_search(results, options = {}) ⇒ Object
- #apply_select(results) ⇒ Object
-
#apply_sort(results) ⇒ Object
Apply sort columns to a result set, and transforms composite column to their consituents.
-
#available_events ⇒ Object
Assuming our resources implement state machines, retrieves a list of available events that can be triggered.
-
#available_output_columns ⇒ Object
Instance level available output column check, this takes into consideration Dynamic attributes added as the select query is built.
- #cleanup_search_results ⇒ Object protected
-
#composite_column(*options) ⇒ Object
Similar to class version but affects the instance variable list of composite columns.
- #discard_excess ⇒ Object protected
- #effective_name ⇒ Object
-
#effective_short_name ⇒ Object
def create_custom_column(results, column_name, type) pgc = ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(column_name, nil, type) results.columns << pgc unless results.columns.include?(pgc) results.column_names << column_name unless results.column_names.include?(column_name) end.
-
#enqueue_navbar_pinned_refresh ⇒ Object
Schedules a single coalesced Turbo Stream replace of this employee's pinned-items navbar badge.
- #fast_count ⇒ Object
- #get_pinned_results ⇒ Object
-
#get_sort_term(column_name, sort_direction = 'ASC') ⇒ Object
Generate the SQL sort term for a given column, it checks if the column name is a composite column with a custom select, in which case replace the sort term.
- #human_query_params ⇒ Object
- #instantiate_resource_updater(params = {}) ⇒ Object protected
-
#limit_options ⇒ Object
if you want to override the available result set size you can do so in your specialized class.
-
#mass_export(params = {}, cur_user = nil, path = nil) ⇒ Object
Mass Export.
-
#normalize_for_mass_update(_resource, _params) ⇒ Object
Per-record hook called by SearchResourceUpdateWorker before saving each resource.
-
#output_columns_for_select ⇒ Object
Renders a select list of possible output columns including all composite columns, note that we only include class level, permanent composite columns and not instance level which can disapear.
-
#pagy_from_search ⇒ Object
Backward-compatible reader: returns a simple struct with count/page/limit so existing controller code (pagy(:offset, results, count:, page:, limit:)) keeps working unchanged.
- #perform(page = 1, sort = nil, direction = nil, record_results = false, per_page = nil, autosave = true, disable_pagination = false) ⇒ Object
- #perform_selected(selected_columns: nil) ⇒ Object
-
#pinned_query ⇒ Object
Retrieves the pinned search results, in most case this is suffucient but for speed purpose you might want to override and specialize e.g.
-
#prepend_custom_column(*options) ⇒ Object
Same as append_custom_column except it adds it to the front of the column list.
- #prepend_output_column(column_name) ⇒ Object
-
#ransack_probe_params ⇒ Object
Returns the query_params hash with non-Ransack keys stripped for the strict probe.
- #record_list(resource_ids) ⇒ Object
-
#refresh_pinned_results ⇒ Object
Re-runs the saved search criteria as a prune over the user's curated pin selection: keeps the intersection of (currently-pinned IDs) and (records that still match the criteria), drops the rest.
- #remove_other_pins ⇒ Object protected
- #search_name ⇒ Object
-
#select_sort_columns ⇒ Object
Instance level list of possible sort columns.
- #select_statement ⇒ Object protected
- #set_defaults ⇒ Object
-
#set_sort_term(set_sort_column, set_sort_direction = nil) ⇒ Object
Given a single sort column applies it and overrides any previous search ordering.
-
#sort_column1 ⇒ Object
BEGIN INSTANCE VARIABLES #.
- #sort_column1=(val) ⇒ Object
- #sort_column2 ⇒ Object
- #sort_column2=(val) ⇒ Object
- #sort_column3 ⇒ Object
- #sort_column3=(val) ⇒ Object
-
#sort_columns_for_select ⇒ Object
Retrieves a list of sort columns suitable for a select.
-
#sort_columns_to_hash ⇒ Object
Takes all the sort columns specified for a search and returns them as a hash with a symbolized key being the column name and the value being the sort direction e.g.
- #target_geocoding ⇒ Object protected
- #unrecord_list(resource_ids) ⇒ Object
-
#validated_selected_columns ⇒ Object
Presentation layer on top of store array attribute selected columns, makes sure to symbolize keys, prevents duplicate, cleans up blanks.
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Class Attribute Details
.composite_columns ⇒ Object (readonly)
Returns the value of attribute composite_columns.
42 43 44 |
# File 'app/models/search.rb', line 42 def composite_columns @composite_columns end |
.default_selected_columns ⇒ Object (readonly)
Returns the value of attribute default_selected_columns.
42 43 44 |
# File 'app/models/search.rb', line 42 def default_selected_columns @default_selected_columns end |
.default_sort_columns ⇒ Object (readonly)
Returns the value of attribute default_sort_columns.
42 43 44 |
# File 'app/models/search.rb', line 42 def default_sort_columns @default_sort_columns end |
.global_favorites ⇒ Object (readonly)
Returns the value of attribute global_favorites.
42 43 44 |
# File 'app/models/search.rb', line 42 def global_favorites @global_favorites end |
Instance Attribute Details
#composite_columns ⇒ Object (readonly)
Returns the value of attribute composite_columns.
49 50 51 |
# File 'app/models/search.rb', line 49 def composite_columns @composite_columns end |
#pagy_count ⇒ Object (readonly)
Expose raw pagination metadata for the controller to build a proper Pagy object with request context
51 52 53 |
# File 'app/models/search.rb', line 51 def pagy_count @pagy_count end |
#pagy_limit ⇒ Object (readonly)
Expose raw pagination metadata for the controller to build a proper Pagy object with request context
51 52 53 |
# File 'app/models/search.rb', line 51 def pagy_limit @pagy_limit end |
#pagy_page ⇒ Object (readonly)
Expose raw pagination metadata for the controller to build a proper Pagy object with request context
51 52 53 |
# File 'app/models/search.rb', line 51 def pagy_page @pagy_page end |
#sql_select_columns ⇒ Object (readonly)
Returns the value of attribute sql_select_columns.
49 50 51 |
# File 'app/models/search.rb', line 49 def sql_select_columns @sql_select_columns end |
Class Method Details
.allowed_role_ids ⇒ Object
86 87 88 |
# File 'app/models/search.rb', line 86 def self.allowed_role_ids nil end |
.available_output_columns ⇒ Object
Return a list of available output columns combining composite columns
and actual view defined columns. This is a class level method
that does not take into consideration dynamically added fields
which only exist in the context of a query instance
211 212 213 |
# File 'app/models/search.rb', line 211 def self.available_output_columns (composite_columns || {}).keys | database_columns end |
.base_search_class_name ⇒ Object
Returns a base model view of what we're trying to earch on. e.g Order for OrderSearch
222 223 224 |
# File 'app/models/search.rb', line 222 def self.base_search_class_name name.scan(/(\w+)Search/).join end |
.composite_column(*options) ⇒ Object
Sets 'virtual' attribute or composite column which will be
rendered in the presenter but require specific columns present
in the select, define these at top of the search class such as
customer: { select: [:customer_full_name, :customer_id]}
in the example above we can specify customer as an output column
which will translate in customer_id and customer_full_name being added
to the select for the query
202 203 204 205 |
# File 'app/models/search.rb', line 202 def self.composite_column(*) @composite_columns ||= {} @composite_columns[.shift] = ..dup end |
.custom_criteria_keys ⇒ Object
458 459 460 |
# File 'app/models/search.rb', line 458 def self.custom_criteria_keys [] end |
.database_columns ⇒ Object
Returns an array of database level columns names as symbols, these are defined by the view resource class
and how its sql view is defined
217 218 219 |
# File 'app/models/search.rb', line 217 def self.database_columns view_resource_klass.column_names.map(&:to_sym) end |
.default_columns(*columns) ⇒ Object
Sets the default columns to be used when creating this search
use default_columns :id, :name, ... etc at head of search class
191 192 193 |
# File 'app/models/search.rb', line 191 def self.default_columns(*columns) @default_selected_columns = columns end |
.default_sort(*columns) ⇒ Object
Sets the default sort to be used when creating this search
use default_columns :id, :name, ... etc at head of search class
185 186 187 |
# File 'app/models/search.rb', line 185 def self.default_sort(*columns) @default_sort_columns = columns end |
.favorites ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are favorites. Active Record Scope
81 |
# File 'app/models/search.rb', line 81 scope :favorites, -> { where(persist: true) } |
.friendly_column_name(column_name) ⇒ Object
174 175 176 |
# File 'app/models/search.rb', line 174 def self.friendly_column_name(column_name) view_resource_klass.human_attribute_name(column_name) end |
.global_favorite(*options) ⇒ Object
178 179 180 181 |
# File 'app/models/search.rb', line 178 def self.global_favorite(*) @global_favorites ||= {} @global_favorites[.shift] = ..dup end |
.has_role_for_search?(role_ids) ⇒ Boolean
90 91 92 93 94 |
# File 'app/models/search.rb', line 90 def self.has_role_for_search?(role_ids) return true if allowed_role_ids.nil? role_ids.intersect?(allowed_role_ids) end |
.instantiate_query_template(template_key = nil, extra_options = nil) ⇒ Object
133 134 135 136 137 |
# File 'app/models/search.rb', line 133 def self.instantiate_query_template(template_key = nil, = nil) params = global_favorites.try(:[], template_key) || {} params = params.deep_merge() if QueryTemplate.new(self, params) end |
.main_resource_class ⇒ Object
149 150 151 |
# File 'app/models/search.rb', line 149 def self.main_resource_class name.match(/(.+)Search/)[1] end |
.main_resource_table ⇒ Object
153 154 155 |
# File 'app/models/search.rb', line 153 def self.main_resource_table main_resource_class.tableize end |
.mass_actions_for_select ⇒ Object
119 120 121 122 |
# File 'app/models/search.rb', line 119 def self.mass_actions_for_select opts = instance_methods + superclass.instance_methods opts.grep(/^mass_/).sort.map { |m| [m.to_s.humanize, m] }.uniq end |
.maximum_unpersisted_queries ⇒ Object
Actions that must run synchronously in the controller because they render
125 126 127 |
# File 'app/models/search.rb', line 125 def self.maximum_unpersisted_queries 5 end |
.options(_visible_only = true) ⇒ Object
96 97 98 |
# File 'app/models/search.rb', line 96 def self.(_visible_only = true) .map { |search_klass| [search_klass.search_name, search_klass.name] } end |
.options_classes ⇒ Object
100 101 102 103 104 105 |
# File 'app/models/search.rb', line 100 def self. list = %i[activity_search contact_search customer_search delivery_search employee_review_search sales_commission_search invoice_search it_asset_search item_search product_catalog_search item_ledger_entry_search ledger_entry_search supplier_item_search locator_record_search opportunity_search order_search purchase_order_search credit_memo_search quote_search rma_search room_configuration_search support_case_search credit_application_search coupon_search voucher_search outgoing_payment_search receipt_search certification_search] list.sort.map { |scn| scn.to_s.classify.constantize } end |
.query_favorites_templates ⇒ Object
139 140 141 142 143 |
# File 'app/models/search.rb', line 139 def self.query_favorites_templates (global_favorites || {}).map do |_k, | QueryTemplate.new(self, ) end end |
.recent_searches_for_employee_id ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are recent searches for employee id. Active Record Scope
83 |
# File 'app/models/search.rb', line 83 scope :recent_searches_for_employee_id, ->(employee_id) { where(employee_id: employee_id).order(created_at: :desc) } |
.search_name ⇒ Object
115 116 117 |
# File 'app/models/search.rb', line 115 def self.search_name name.titleize end |
.search_type ⇒ Object
107 108 109 |
# File 'app/models/search.rb', line 107 def self.search_type name.tableize.singularize end |
.search_type_humanized ⇒ Object
111 112 113 |
# File 'app/models/search.rb', line 111 def self.search_type_humanized search_type.humanize.titleize end |
.select_sort_columns ⇒ Object
Returns available columns for sorting
170 171 172 |
# File 'app/models/search.rb', line 170 def self.select_sort_columns view_resource_klass.select_sort_columns end |
.unpersisted_queries_quote_reached?(search_type, employee_id) ⇒ Boolean
129 130 131 |
# File 'app/models/search.rb', line 129 def self.unpersisted_queries_quote_reached?(search_type, employee_id) Search.where(employee_id: employee_id).where(type: search_type).where('persist IS NULL or NOT persist').count >= maximum_unpersisted_queries end |
.view_resource_class ⇒ Object
157 158 159 |
# File 'app/models/search.rb', line 157 def self.view_resource_class "View#{name.match(/(.+)Search/)[1]}" end |
.view_resource_klass ⇒ Object
161 162 163 |
# File 'app/models/search.rb', line 161 def self.view_resource_klass view_resource_class.constantize end |
.view_resource_table ⇒ Object
165 166 167 |
# File 'app/models/search.rb', line 165 def self.view_resource_table view_resource_class.tableize end |
.visible? ⇒ Boolean
145 146 147 |
# File 'app/models/search.rb', line 145 def self.visible? true end |
.with_search_results ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are with search results. Active Record Scope
82 |
# File 'app/models/search.rb', line 82 scope :with_search_results, -> { where('exists(select 1 from search_results sr where sr.search_id = searches.id)') } |
Instance Method Details
#all_composite_columns ⇒ Object
Returns a combination of instance level and class level composite columns
instance levels are defined in the query build mechanism and unit to this
search instance. class level are defined in the search class header via composite_column:
284 285 286 |
# File 'app/models/search.rb', line 284 def all_composite_columns (self.class.composite_columns || {}).merge(composite_columns || {}) end |
#append_custom_column(*options) ⇒ Object
Sometimes the criteria will append a custom field to the query that is
not part of the initial view, we need to make this field part of the output
and also the custom select
pass options to this function as you would composite_column, e.g
:distance, select: '(select max(x) as distance)'
317 318 319 320 321 |
# File 'app/models/search.rb', line 317 def append_custom_column(*) column_name = .first.to_s composite_column(*) append_ouput_column(column_name) end |
#append_ouput_column(column_name) ⇒ Object
330 331 332 333 |
# File 'app/models/search.rb', line 330 def append_ouput_column(column_name) selected_columns.delete(column_name) selected_columns << column_name end |
#append_to_sql_select_columns(*args) ⇒ Object (protected)
888 889 890 |
# File 'app/models/search.rb', line 888 def append_to_sql_select_columns(*args) @sql_select_columns += args end |
#applicable_sort_terms ⇒ Object
Filters down the sort columns available and cross reference them with the
columns that are in the query output.
604 605 606 607 608 609 610 611 612 613 614 615 616 |
# File 'app/models/search.rb', line 604 def applicable_sort_terms sorts = [] sort_columns.select do |sort_column_combined| sort_column_fq, sort_order = sort_column_combined.split # Sort columns can be fully qualified (table_name.sort_column) here we # detect this and assign accordingly sort_column, = sort_column_fq.split('.').reverse # available_output_columns never contains the fully qualified version # So we check against the sort_column name sorts << [sort_column_fq, sort_order] if available_output_columns.include? sort_column.to_sym end sorts end |
#apply_array_match(results, param, column = nil, nil_value = nil, any_value = nil) ⇒ Object (protected)
param: an array of values to test against
nil_value: of the array of values, which one represent a database null, e.g. "Open" might translate to records having result null
any_value: of the array of values, which means 'any value', note that this trumps nil_value
865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 |
# File 'app/models/search.rb', line 865 def apply_array_match(results, param, column = nil, nil_value = nil, any_value = nil) if query_params[param].present? column ||= param val_ids = query_params[param].dup.compact.uniq results.unscoped # Start with a blank # Any value just checks for non nil end of story if any_value && val_ids.include?(any_value) results = results.where.not(column => nil) else # nil_value might be a string expressing null so we substitute, e.g. "not blank" val_ids << nil if nil_value && val_ids.delete(nil_value) # Block given we might add more values to check on val_ids = yield(val_ids) if block_given? results = results.where(column => val_ids) if val_ids.present? end end results end |
#apply_criteria ⇒ Object
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 |
# File 'app/models/search.rb', line 431 def apply_criteria criteria_klass = self.class.view_resource_klass # Strict probe: build the search object (no SQL) with unknown conditions # disallowed. If any stored param key is stale/renamed, Ransack raises # InvalidSearchError. We report it to AppSignal and continue — the real # query below uses the default lenient mode so the user is never disrupted. # Custom keys handled by apply_custom_criteria and apply_distance_search are # excluded from the probe to avoid false positives. begin probe_params = ransack_probe_params criteria_klass.ransack(probe_params, ignore_unknown_conditions: false) rescue Ransack::InvalidSearchError => e Appsignal.report_error(e) do |transaction| transaction.( search_type: self.class.name, search_id: id.to_s, query_params: query_params.to_json ) end end results = criteria_klass.ransack(query_params).result results = apply_custom_criteria(results) if respond_to?(:apply_custom_criteria) results end |
#apply_customer_cross_reference(results, join_key = nil) ⇒ Object
Apply customer cross reference
496 497 498 499 500 501 502 503 504 505 |
# File 'app/models/search.rb', line 496 def apply_customer_cross_reference(results, join_key = nil) join_key ||= 'customer_id' fq_join_key = "#{self.class.view_resource_table}.#{join_key}" if query_params[:cross_reference_customer_search_id].present? results = results.where( "EXISTS(select 1 from search_results src WHERE src.resource_type = 'Customer' and src.resource_id = #{fq_join_key} and src.search_id IN (?))", [query_params[:cross_reference_customer_search_id]].flatten.join(',') ) end results end |
#apply_distance_search(results, options = {}) ⇒ Object
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 |
# File 'app/models/search.rb', line 507 def apply_distance_search(results, = {}) custom_table_join = [:custom_table_join] custom_table_alias = [:custom_table_alias] target_coordinates = query_params[:target_coordinates] within_miles = query_params[:within_miles].present? ? query_params[:within_miles].to_i : 25 # custom_table_join ||= "INNER JOIN addresses on addresses.id = address_id" # custom_table_alias ||= "addresses" lat_column = [:lat_column] || 'lat' lng_column = [:lng_column] || 'lng' lat_column = "#{custom_table_alias}.#{lat_column}" if custom_table_alias lng_column = "#{custom_table_alias}.#{lng_column}" if custom_table_alias lat, lng = target_coordinates if target_coordinates.present? && lat && lng origin_lat = Math.deg2rad(lat) origin_lng = Math.deg2rad(lng) distance_formula = <<-EOS (ACOS(COS(#{origin_lat})*COS(#{origin_lng})*COS(RADIANS(#{lat_column}))*COS(RADIANS(#{lng_column}))+ COS(#{origin_lat})*SIN(#{origin_lng})*COS(RADIANS(#{lat_column}))*SIN(RADIANS(#{lng_column}))+ SIN(#{origin_lat})*SIN(RADIANS(#{lat_column})))*3963) EOS results = results.joins(custom_table_join) prepend_custom_column(:distance, raw: "#{distance_formula} as distance", data_type: :float) results = results.where("(#{distance_formula}) < ?", within_miles) set_sort_term(:distance) if sort_columns.blank? else # reset because it won't be there sort_columns.delete('distance') end results end |
#apply_select(results) ⇒ Object
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 |
# File 'app/models/search.rb', line 468 def apply_select(results) # Builds an array of sql select statements based on an array of column name, translating any # composite key to their required actual column sql_select_columns = [] sql_select_columns << if new_record? 'false as selected_attr' else "(EXISTS(select id from search_results sr where sr.resource_id = #{self.class.view_resource_table}.id and sr.search_id = #{id})) as selected_attr" end columns = [:id] validated_selected_columns.each do |column| if (composite_attrs = all_composite_columns[column.to_sym]) sql_select_columns |= [composite_attrs[:raw]].flatten.compact columns |= [composite_attrs[:select]].flatten.compact # create_custom_column(results, column.to_s, composite_attrs[:data_type]) elsif self.class.view_resource_klass.column_names.include?(column.to_s) columns |= [column] else # column does not exist, remove selected_columns.delete(column.to_s) end end sql_select_columns += columns.compact.uniq.map { |column| "#{self.class.view_resource_table}.#{column}" } results.select(sql_select_columns.join(',')) end |
#apply_sort(results) ⇒ Object
Apply sort columns to a result set, and transforms composite column to their
consituents
620 621 622 623 624 625 626 627 628 629 630 631 632 |
# File 'app/models/search.rb', line 620 def apply_sort(results) return results if sort_columns.blank? sorts = applicable_sort_terms if sorts.present? sort_sql = Arel.sql(sorts.map { |(column_name, sort_direction)| get_sort_term(column_name, sort_direction) }.join(',')) results = results.reorder(sort_sql) else # Auto fix bad sort columns update_column(:sort_columns, []) end results end |
#available_events ⇒ Object
Assuming our resources implement state machines, retrieves a list of available events that can be triggered
829 830 831 832 833 834 835 |
# File 'app/models/search.rb', line 829 def available_events events = [] search_results.includes(:resource).filter_map(&:resource).uniq.each do |resource| events += resource.state_events.select { |evt| resource.send(:"can_#{evt}?") } end events.compact.uniq.sort.map { |evt| [evt.to_s.titleize, evt] } end |
#available_output_columns ⇒ Object
Instance level available output column check, this takes into consideration
Dynamic attributes added as the select query is built
268 269 270 |
# File 'app/models/search.rb', line 268 def available_output_columns (composite_columns || {}).keys | self.class.available_output_columns end |
#cleanup_search_results ⇒ Object (protected)
896 897 898 |
# File 'app/models/search.rb', line 896 def cleanup_search_results search_results.delete_all if saved_change_to_query_params? end |
#composite_column(*options) ⇒ Object
Similar to class version but affects the instance variable list of composite
columns. When the query is built both instance and class level composite
columns are combined. THis allows for instance dynamic attributes to be added
such as a distance column when proximity critiera are passed in
276 277 278 279 |
# File 'app/models/search.rb', line 276 def composite_column(*) @composite_columns ||= {} @composite_columns[.shift] = ..dup end |
#discard_excess ⇒ Object (protected)
906 907 908 909 |
# File 'app/models/search.rb', line 906 def discard_excess excess_searches = Search.where(employee_id: employee_id).where(type: self.class.name).where('persist IS NULL or NOT persist').order(id: :desc).offset(self.class.maximum_unpersisted_queries) excess_searches.destroy_all end |
#effective_name ⇒ Object
350 351 352 |
# File 'app/models/search.rb', line 350 def effective_name name.presence || human_query_params.presence || 'Unfiltered Search' end |
#effective_short_name ⇒ Object
def create_custom_column(results, column_name, type)
pgc = ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(column_name, nil, type)
results.columns << pgc unless results.columns.include?(pgc)
results.column_names << column_name unless results.column_names.include?(column_name)
end
346 347 348 |
# File 'app/models/search.rb', line 346 def effective_short_name name || "#{self.class.name.humanize.titleize} #{created_at.to_fs(:crm_default)}" end |
#employee ⇒ Employee
63 |
# File 'app/models/search.rb', line 63 belongs_to :employee, inverse_of: :searches, optional: true |
#enqueue_navbar_pinned_refresh ⇒ Object
Schedules a single coalesced Turbo Stream replace of this employee's
pinned-items navbar badge. Safe to call from any code path that may have
changed the pinned set — bulk SQL helpers, AR callbacks, controllers.
710 711 712 713 714 |
# File 'app/models/search.rb', line 710 def return if employee_id.blank? CrmNavbarRefreshWorker.schedule(user_id: employee_id, badge: :pinned) end |
#fast_count ⇒ Object
375 376 377 378 379 |
# File 'app/models/search.rb', line 375 def fast_count results = apply_criteria results.select(selected_columns.first) results.count end |
#get_pinned_results ⇒ Object
365 366 367 |
# File 'app/models/search.rb', line 365 def get_pinned_results pinned_query.includes(:resource).order(:position) # ensures we only show resource that still exists end |
#get_sort_term(column_name, sort_direction = 'ASC') ⇒ Object
Generate the SQL sort term for a given column,
it checks if the column name is a composite column with a custom select, in which
case replace the sort term
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 |
# File 'app/models/search.rb', line 544 def get_sort_term(column_name, sort_direction = 'ASC') sort_terms = [] if (composite_attrs = all_composite_columns[column_name.to_sym]) if composite_attrs[:order] sort_terms << composite_attrs[:order] elsif composite_attrs[:select] sort_terms = composite_attrs[:select] else sort_terms << column_name end else sort_terms << if self.class.view_resource_table == column_name.split('.')[0] column_name.to_s else "#{self.class.view_resource_table}.#{column_name}" end end sort_terms.map { |term| "#{term} #{sort_direction}" } end |
#human_query_params ⇒ Object
354 355 356 357 358 |
# File 'app/models/search.rb', line 354 def human_query_params query_params.filter_map { |k, v| v.present? ? "#{k.humanize}: #{v}" : nil }.join(' and ') rescue StandardError nil end |
#instantiate_resource_updater(params = {}) ⇒ Object (protected)
884 885 886 |
# File 'app/models/search.rb', line 884 def instantiate_resource_updater(params = {}) ResourceUpdater.new(self, params) end |
#limit_options ⇒ Object
if you want to override the available result set size you can do so in your specialized class.
361 362 363 |
# File 'app/models/search.rb', line 361 def [10, 25, 50, 100, 200, 500, 1000, 5000] end |
#mass_export(params = {}, cur_user = nil, path = nil) ⇒ Object
Mass Export
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 |
# File 'app/models/search.rb', line 721 def mass_export(params = {}, cur_user = nil, path = nil) params ||= {} file_format = params[:file_format].presence || 'CSV' file_extension = file_format.downcase file_name = "export_#{id}.#{file_extension}" directory_path = path || Rails.root.join(Rails.application.config.x.temp_storage_path.to_s, 'users', cur_user ? cur_user.id.to_s : '') FileUtils.mkdir_p(directory_path) file_path = Rails.root.join(directory_path, file_name) # Prepare export data results = perform(nil, nil, nil, false, 100_000, false) .where("exists(select id from search_results sr where sr.search_id = ? and sr.resource_id = #{self.class.view_resource_table}.id)", id) search_composite_map = Search.find(id).type.constantize.composite_columns.to_h validated_composite_columns = validated_selected_columns.map do |column_name| [column_name, search_composite_map[column_name]&.dig(:select, 0) || column_name] end include_headers = params[:include_headers].to_b headers = validated_selected_columns.map { |column_name| self.class.friendly_column_name(column_name) } if file_format == 'XLSX' export_xlsx(file_path, results, validated_composite_columns, headers, include_headers, params) else export_csv(file_path, results, validated_composite_columns, headers, include_headers, params) end { status: :ok, result_file: file_name, result_file_path: file_path } end |
#normalize_for_mass_update(_resource, _params) ⇒ Object
Per-record hook called by SearchResourceUpdateWorker before saving each resource.
Override in subclasses to apply custom normalization or guard logic.
Return false to skip the save and count the record as an error.
Must be public — the worker invokes it from another class (not a valid protected call).
841 842 843 |
# File 'app/models/search.rb', line 841 def normalize_for_mass_update(_resource, _params) true end |
#output_columns_for_select ⇒ Object
Renders a select list of possible output columns including all
composite columns, note that we only include class level, permanent composite
columns and not instance level which can disapear
291 292 293 294 295 |
# File 'app/models/search.rb', line 291 def output_columns_for_select # Start with the ones already selected columns = validated_selected_columns | self.class.available_output_columns.sort_by { |cn| self.class.friendly_column_name(cn) } columns.map { |cn| [self.class.friendly_column_name(cn), cn] } end |
#pagy_from_search ⇒ Object
Backward-compatible reader: returns a simple struct with count/page/limit so existing
controller code (pagy(:offset, results, count:, page:, limit:)) keeps working unchanged.
55 56 57 58 59 |
# File 'app/models/search.rb', line 55 def pagy_from_search return nil unless @pagy_count Data.define(:count, :page, :limit).new(count: @pagy_count, page: @pagy_page, limit: @pagy_limit) end |
#perform(page = 1, sort = nil, direction = nil, record_results = false, per_page = nil, autosave = true, disable_pagination = false) ⇒ Object
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 419 420 421 422 423 424 |
# File 'app/models/search.rb', line 381 def perform(page = 1, sort = nil, direction = nil, record_results = false, per_page = nil, autosave = true, disable_pagination = false) results = apply_criteria # Count BEFORE applying complex SELECT to avoid SQL syntax errors total_count = results.count if !disable_pagination && results.respond_to?(:limit) results = apply_select(results) set_sort_term(sort, direction) results = apply_sort(results) if !disable_pagination && results.respond_to?(:limit) @pagy_page = (page || 1).to_i @pagy_limit = (per_page || set_limit).to_i @pagy_count = total_count results = results.limit(@pagy_limit).offset((@pagy_page - 1) * @pagy_limit) self.result_set_size = @pagy_count else @pagy_count = @pagy_page = @pagy_limit = nil self.result_set_size = results.size end if autosave save if record_results search_results.delete_all results.find_each do |r| search_results.create(resource_type: (begin r.class.main_resource_class rescue StandardError nil end) || r.class.to_s, resource_id: r.id) end end results.to_a.compact.each do |r| r.class.send(:attr_accessor, :search_selected) r.search_selected = if r.respond_to? :selected_attr r.selected_attr else search_results.exists?(resource_type: r.class.name, resource_id: r.id) end end end results end |
#perform_selected(selected_columns: nil) ⇒ Object
426 427 428 429 |
# File 'app/models/search.rb', line 426 def perform_selected(selected_columns: nil) self.selected_columns = selected_columns if selected_columns.present? perform(nil, nil, nil, false, nil, false, true).where("exists(select id from search_results sr where sr.search_id = ? and sr.resource_id = #{self.class.view_resource_table}.id)", id) end |
#pinned_query ⇒ Object
Retrieves the pinned search results, in most case this is suffucient but for speed purpose you might want to override and specialize
e.g. if you pin presenter displays information in associated model you might want to include the associated table
371 372 373 |
# File 'app/models/search.rb', line 371 def pinned_query search_results end |
#prepend_custom_column(*options) ⇒ Object
Same as append_custom_column except it adds it to the front of the column list.
324 325 326 327 328 |
# File 'app/models/search.rb', line 324 def prepend_custom_column(*) column_name = .first.to_s composite_column(*) prepend_output_column(column_name) end |
#prepend_output_column(column_name) ⇒ Object
335 336 337 338 |
# File 'app/models/search.rb', line 335 def prepend_output_column(column_name) selected_columns.delete(column_name) # delete it if present already selected_columns.insert(0, column_name) end |
#ransack_probe_params ⇒ Object
Returns the query_params hash with non-Ransack keys stripped for the strict probe.
Subclasses can override to remove additional dynamic keys (e.g. spec_field_* in ItemSearch).
464 465 466 |
# File 'app/models/search.rb', line 464 def ransack_probe_params query_params.except(*DISTANCE_SEARCH_KEYS, *self.class.custom_criteria_keys) end |
#record_list(resource_ids) ⇒ Object
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 |
# File 'app/models/search.rb', line 634 def record_list(resource_ids) if resource_ids == '*' results = apply_criteria results = results.except(:select).select "'#{self.class.main_resource_class}', #{self.class.view_resource_table}.id, now(), now(), #{id}" # By far the fastest method is straight sql first to wipe all existing SearchResult.delete_by(search_id: id) # Bulk magic sql = %{ INSERT INTO search_results ( resource_type, resource_id, created_at, updated_at, search_id ) #{results.to_sql} ON CONFLICT DO NOTHING } ActiveRecord::Base.lease_connection.execute(sql) else return if resource_ids.blank? search_results = resource_ids.map do |raw_resource_id| resource_type, resource_id = raw_resource_id.split('|') { resource_type: resource_type, resource_id: resource_id, search_id: id } end SearchResult.insert_all(search_results) if search_results.any? end if pinned true end |
#refresh_pinned_results ⇒ Object
Re-runs the saved search criteria as a prune over the user's curated
pin selection: keeps the intersection of (currently-pinned IDs) and
(records that still match the criteria), drops the rest. Never adds new
rows — a user who pinned 5 specific records gets at most 5 back, with
stale ones (e.g. an opportunity that was closed) removed.
Returns the number of stale rows removed.
693 694 695 696 697 698 699 700 701 702 703 704 705 |
# File 'app/models/search.rb', line 693 def refresh_pinned_results main_type = self.class.main_resource_class pinned_ids = search_results.where(resource_type: main_type).pluck(:resource_id) return 0 if pinned_ids.empty? matching_ids = apply_criteria.except(:select).where(id: pinned_ids).ids stale_ids = pinned_ids - matching_ids return 0 if stale_ids.empty? search_results.delete_by(resource_type: main_type, resource_id: stale_ids) if pinned stale_ids.size end |
#remove_other_pins ⇒ Object (protected)
900 901 902 903 904 |
# File 'app/models/search.rb', line 900 def remove_other_pins return unless employee && pinned_changed? && pinned employee.searches.where(pinned: true).update_all(pinned: false) end |
#search_name ⇒ Object
716 717 718 |
# File 'app/models/search.rb', line 716 def search_name name || self.class.search_name end |
#search_results ⇒ ActiveRecord::Relation<SearchResult>
64 |
# File 'app/models/search.rb', line 64 has_many :search_results, inverse_of: :search, dependent: :delete_all |
#select_sort_columns ⇒ Object
Instance level list of possible sort columns
298 299 300 301 302 |
# File 'app/models/search.rb', line 298 def select_sort_columns # Current sort columns current_sort_columns = sort_columns.map { |sc| sc.split&.first&.to_sym } available_output_columns | current_sort_columns end |
#select_statement ⇒ Object (protected)
892 893 894 |
# File 'app/models/search.rb', line 892 def select_statement @sql_select_columns.join(',') end |
#set_defaults ⇒ Object
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 |
# File 'app/models/search.rb', line 811 def set_defaults self.set_limit = 100 if set_limit.blank? self.persist = false if persist.blank? self.name = nil if name.blank? self.selected_columns = (self.class.default_selected_columns.presence || available_output_columns) if selected_columns.nil? || selected_columns.blank? # Apply default sort columns if sort_columns.blank? if self.class.default_sort_columns.present? self.sort_columns = (self.class.default_sort_columns & available_output_columns) elsif available_output_columns.include?(:created_at) # self.sort_columns = ["#{self.class.view_resource_table}.created_at desc"] self.sort_columns = ['created_at DESC'] end end self.sort_columns ||= [] end |
#set_sort_term(set_sort_column, set_sort_direction = nil) ⇒ Object
Given a single sort column applies it and overrides any previous search ordering
565 566 567 568 569 570 571 572 573 574 575 |
# File 'app/models/search.rb', line 565 def set_sort_term(set_sort_column, set_sort_direction = nil) return if set_sort_column.blank? set_sort_direction ||= 'ASC' self.sort_columns = if available_output_columns.include? set_sort_column.to_sym ["#{set_sort_column.downcase} #{set_sort_direction.downcase}"] else [] end end |
#sort_column1 ⇒ Object
BEGIN INSTANCE VARIABLES #
230 231 232 |
# File 'app/models/search.rb', line 230 def sort_column1 sort_columns.try(:[], 0) end |
#sort_column1=(val) ⇒ Object
234 235 236 237 238 239 240 |
# File 'app/models/search.rb', line 234 def sort_column1=(val) if val.present? self.sort_columns = sort_columns.insert(0, val).compact.uniq else sort_columns.delete_at(0) end end |
#sort_column2 ⇒ Object
242 243 244 |
# File 'app/models/search.rb', line 242 def sort_column2 sort_columns.try(:[], 1) end |
#sort_column2=(val) ⇒ Object
246 247 248 249 250 251 252 |
# File 'app/models/search.rb', line 246 def sort_column2=(val) if val.present? self.sort_columns = sort_columns.insert(1, val).compact.uniq else sort_columns.delete_at(1) end end |
#sort_column3 ⇒ Object
254 255 256 |
# File 'app/models/search.rb', line 254 def sort_column3 sort_columns.try(:[], 2) end |
#sort_column3=(val) ⇒ Object
258 259 260 261 262 263 264 |
# File 'app/models/search.rb', line 258 def sort_column3=(val) if val.present? self.sort_columns = sort_columns.insert(2, val).compact.uniq else sort_columns.delete_at(2) end end |
#sort_columns_for_select ⇒ Object
Retrieves a list of sort columns suitable for a select
591 592 593 594 595 596 597 598 599 600 |
# File 'app/models/search.rb', line 591 def sort_columns_for_select selection_list = [] select_sort_columns.each do |sc| selection_list << ["#{sc.to_s.humanize} ascending", "#{sc} ASC"] selection_list << ["#{sc.to_s.humanize} descending", "#{sc} DESC"] end # Add composite keys selection_list end |
#sort_columns_to_hash ⇒ Object
Takes all the sort columns specified for a search and returns them as a
hash with a symbolized key being the column name and the value being the sort
direction
e.g. { :primary_sales_rep_name => :DESC, :amount => :ASC }
581 582 583 584 585 586 587 588 |
# File 'app/models/search.rb', line 581 def sort_columns_to_hash return {} if sort_columns.blank? sort_columns.each_with_object({}) do |sort_combined, hsh| sort_column, sort_direction = sort_combined.split hsh[sort_column.to_sym] = (sort_direction || :ASC).to_sym end end |
#target_geocoding ⇒ Object (protected)
847 848 849 850 851 852 853 854 855 856 857 858 859 |
# File 'app/models/search.rb', line 847 def target_geocoding return unless query_params if query_params[:within_miles_of].present? if query_params[:target_coordinates].blank? || (query_params_was[:within_miles_of] != query_params[:within_miles_of]) logger.info "Attempt geocoding #{query_params[:within_miles_of]}" query_params[:target_coordinates] = Geocoder.coordinates(query_params[:within_miles_of]) errors.add(:query_params, "error, cannot geocode #{query_params[:within_miles_of]}") unless query_params[:target_coordinates] end else query_params.delete(:target_coordinates) end end |
#unrecord_list(resource_ids) ⇒ Object
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 |
# File 'app/models/search.rb', line 665 def unrecord_list(resource_ids) return if resource_ids.blank? if resource_ids == '*' SearchResult.delete_by(search_id: id) else SearchResult.transaction do # One transaction wrap makes this faster hsh_delete = {} resource_ids.each do |raw_resource_id| resource_type, resource_id = raw_resource_id.split('|') hsh_delete[resource_type] ||= [] hsh_delete[resource_type] << resource_id end hsh_delete.each do |(resource_type, resource_ids)| SearchResult.delete_by(search_id: id, resource_type: resource_type, resource_id: resource_ids) end end end if pinned end |
#validated_selected_columns ⇒ Object
Presentation layer on top of store array attribute selected columns,
makes sure to symbolize keys, prevents duplicate, cleans up blanks.
308 309 310 |
# File 'app/models/search.rb', line 308 def validated_selected_columns (selected_columns || []).map { |c| c.presence&.to_sym }.uniq.compact end |
#view_resource_klass ⇒ Object
Alias for Class#view_resource_klass
304 |
# File 'app/models/search.rb', line 304 delegate :view_resource_klass, to: :class |