Class: Search
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Search
- Defined in:
- app/models/search.rb
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
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.
-
#sort_column1 ⇒ Object
BEGIN INSTANCE VARIABLES #.
-
#sort_column2 ⇒ Object
Returns the value of attribute sort_column2.
-
#sort_column3 ⇒ Object
Returns the value of attribute sort_column3.
-
#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_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 Models::EventPublishable
Class Attribute Details
.composite_columns ⇒ Object (readonly)
Returns the value of attribute composite_columns.
32 33 34 |
# File 'app/models/search.rb', line 32 def composite_columns @composite_columns end |
.default_selected_columns ⇒ Object (readonly)
Returns the value of attribute default_selected_columns.
32 33 34 |
# File 'app/models/search.rb', line 32 def default_selected_columns @default_selected_columns end |
.default_sort_columns ⇒ Object (readonly)
Returns the value of attribute default_sort_columns.
32 33 34 |
# File 'app/models/search.rb', line 32 def default_sort_columns @default_sort_columns end |
.global_favorites ⇒ Object (readonly)
Returns the value of attribute global_favorites.
32 33 34 |
# File 'app/models/search.rb', line 32 def global_favorites @global_favorites end |
Instance Attribute Details
#composite_columns ⇒ Object (readonly)
Returns the value of attribute composite_columns.
39 40 41 |
# File 'app/models/search.rb', line 39 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
41 42 43 |
# File 'app/models/search.rb', line 41 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
41 42 43 |
# File 'app/models/search.rb', line 41 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
41 42 43 |
# File 'app/models/search.rb', line 41 def pagy_page @pagy_page end |
#sort_column1 ⇒ Object
BEGIN INSTANCE VARIABLES #
220 221 222 |
# File 'app/models/search.rb', line 220 def sort_column1 @sort_column1 end |
#sort_column2 ⇒ Object
Returns the value of attribute sort_column2.
39 40 41 |
# File 'app/models/search.rb', line 39 def sort_column2 @sort_column2 end |
#sort_column3 ⇒ Object
Returns the value of attribute sort_column3.
39 40 41 |
# File 'app/models/search.rb', line 39 def sort_column3 @sort_column3 end |
#sql_select_columns ⇒ Object (readonly)
Returns the value of attribute sql_select_columns.
39 40 41 |
# File 'app/models/search.rb', line 39 def sql_select_columns @sql_select_columns end |
Class Method Details
.allowed_role_ids ⇒ Object
76 77 78 |
# File 'app/models/search.rb', line 76 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
201 202 203 |
# File 'app/models/search.rb', line 201 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
212 213 214 |
# File 'app/models/search.rb', line 212 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
192 193 194 195 |
# File 'app/models/search.rb', line 192 def self.composite_column(*) @composite_columns ||= {} @composite_columns[.shift] = ..dup end |
.custom_criteria_keys ⇒ Object
448 449 450 |
# File 'app/models/search.rb', line 448 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
207 208 209 |
# File 'app/models/search.rb', line 207 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
181 182 183 |
# File 'app/models/search.rb', line 181 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
175 176 177 |
# File 'app/models/search.rb', line 175 def self.default_sort(*columns) @default_sort_columns = columns end |
.favorites ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are favorites. Active Record Scope
71 |
# File 'app/models/search.rb', line 71 scope :favorites, -> { where(persist: true) } |
.friendly_column_name(column_name) ⇒ Object
164 165 166 |
# File 'app/models/search.rb', line 164 def self.friendly_column_name(column_name) view_resource_klass.human_attribute_name(column_name) end |
.global_favorite(*options) ⇒ Object
168 169 170 171 |
# File 'app/models/search.rb', line 168 def self.global_favorite(*) @global_favorites ||= {} @global_favorites[.shift] = ..dup end |
.has_role_for_search?(role_ids) ⇒ Boolean
80 81 82 83 84 |
# File 'app/models/search.rb', line 80 def self.has_role_for_search?(role_ids) return true if allowed_role_ids.nil? (role_ids & allowed_role_ids).present? end |
.instantiate_query_template(template_key = nil, extra_options = nil) ⇒ Object
123 124 125 126 127 |
# File 'app/models/search.rb', line 123 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
139 140 141 |
# File 'app/models/search.rb', line 139 def self.main_resource_class name.match(/(.+)Search/)[1] end |
.main_resource_table ⇒ Object
143 144 145 |
# File 'app/models/search.rb', line 143 def self.main_resource_table main_resource_class.tableize end |
.mass_actions_for_select ⇒ Object
109 110 111 112 |
# File 'app/models/search.rb', line 109 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
115 116 117 |
# File 'app/models/search.rb', line 115 def self.maximum_unpersisted_queries 5 end |
.options(_visible_only = true) ⇒ Object
86 87 88 |
# File 'app/models/search.rb', line 86 def self.(_visible_only = true) .map { |search_klass| [search_klass.search_name, search_klass.name] } end |
.options_classes ⇒ Object
90 91 92 93 94 95 |
# File 'app/models/search.rb', line 90 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
129 130 131 132 133 |
# File 'app/models/search.rb', line 129 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
73 |
# File 'app/models/search.rb', line 73 scope :recent_searches_for_employee_id, ->(employee_id) { where(employee_id: employee_id).order('created_at DESC') } |
.search_name ⇒ Object
105 106 107 |
# File 'app/models/search.rb', line 105 def self.search_name name.titleize end |
.search_type ⇒ Object
97 98 99 |
# File 'app/models/search.rb', line 97 def self.search_type name.tableize.singularize end |
.search_type_humanized ⇒ Object
101 102 103 |
# File 'app/models/search.rb', line 101 def self.search_type_humanized search_type.humanize.titleize end |
.select_sort_columns ⇒ Object
Returns available columns for sorting
160 161 162 |
# File 'app/models/search.rb', line 160 def self.select_sort_columns view_resource_klass.select_sort_columns end |
.unpersisted_queries_quote_reached?(search_type, employee_id) ⇒ Boolean
119 120 121 |
# File 'app/models/search.rb', line 119 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
147 148 149 |
# File 'app/models/search.rb', line 147 def self.view_resource_class "View#{name.match(/(.+)Search/)[1]}" end |
.view_resource_klass ⇒ Object
151 152 153 |
# File 'app/models/search.rb', line 151 def self.view_resource_klass view_resource_class.constantize end |
.view_resource_table ⇒ Object
155 156 157 |
# File 'app/models/search.rb', line 155 def self.view_resource_table view_resource_class.tableize end |
.visible? ⇒ Boolean
135 136 137 |
# File 'app/models/search.rb', line 135 def self.visible? true end |
.with_search_results ⇒ ActiveRecord::Relation<Search>
A relation of Searches that are with search results. Active Record Scope
72 |
# File 'app/models/search.rb', line 72 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:
274 275 276 |
# File 'app/models/search.rb', line 274 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)'
307 308 309 310 311 |
# File 'app/models/search.rb', line 307 def append_custom_column(*) column_name = .first.to_s composite_column(*) append_ouput_column(column_name) end |
#append_ouput_column(column_name) ⇒ Object
320 321 322 323 |
# File 'app/models/search.rb', line 320 def append_ouput_column(column_name) selected_columns.delete(column_name) selected_columns << column_name end |
#append_to_sql_select_columns(*args) ⇒ Object (protected)
886 887 888 |
# File 'app/models/search.rb', line 886 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.
594 595 596 597 598 599 600 601 602 603 604 605 606 |
# File 'app/models/search.rb', line 594 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
863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 |
# File 'app/models/search.rb', line 863 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
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'app/models/search.rb', line 421 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
486 487 488 489 490 491 492 493 494 495 |
# File 'app/models/search.rb', line 486 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
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 |
# File 'app/models/search.rb', line 497 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
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 |
# File 'app/models/search.rb', line 458 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
610 611 612 613 614 615 616 617 618 619 620 621 622 |
# File 'app/models/search.rb', line 610 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
827 828 829 830 831 832 833 |
# File 'app/models/search.rb', line 827 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
258 259 260 |
# File 'app/models/search.rb', line 258 def available_output_columns (composite_columns || {}).keys | self.class.available_output_columns end |
#cleanup_search_results ⇒ Object (protected)
894 895 896 |
# File 'app/models/search.rb', line 894 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
266 267 268 269 |
# File 'app/models/search.rb', line 266 def composite_column(*) @composite_columns ||= {} @composite_columns[.shift] = ..dup end |
#discard_excess ⇒ Object (protected)
904 905 906 907 |
# File 'app/models/search.rb', line 904 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
340 341 342 |
# File 'app/models/search.rb', line 340 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
336 337 338 |
# File 'app/models/search.rb', line 336 def effective_short_name name || "#{self.class.name.humanize.titleize} #{created_at.to_fs(:crm_default)}" end |
#employee ⇒ Employee
53 |
# File 'app/models/search.rb', line 53 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.
707 708 709 710 711 |
# File 'app/models/search.rb', line 707 def return if employee_id.blank? CrmNavbarRefreshWorker.schedule(user_id: employee_id, badge: :pinned) end |
#fast_count ⇒ Object
365 366 367 368 369 |
# File 'app/models/search.rb', line 365 def fast_count results = apply_criteria results.select(selected_columns.first) results.count end |
#get_pinned_results ⇒ Object
355 356 357 |
# File 'app/models/search.rb', line 355 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
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 |
# File 'app/models/search.rb', line 534 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
344 345 346 347 348 |
# File 'app/models/search.rb', line 344 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)
882 883 884 |
# File 'app/models/search.rb', line 882 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.
351 352 353 |
# File 'app/models/search.rb', line 351 def [10, 25, 50, 100, 200, 500, 1000, 5000] end |
#mass_export(params = {}, cur_user = nil, path = nil) ⇒ Object
Mass Export
718 719 720 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 |
# File 'app/models/search.rb', line 718 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).
839 840 841 |
# File 'app/models/search.rb', line 839 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
281 282 283 284 285 |
# File 'app/models/search.rb', line 281 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.
45 46 47 48 49 |
# File 'app/models/search.rb', line 45 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
371 372 373 374 375 376 377 378 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 |
# File 'app/models/search.rb', line 371 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
416 417 418 419 |
# File 'app/models/search.rb', line 416 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
361 362 363 |
# File 'app/models/search.rb', line 361 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.
314 315 316 317 318 |
# File 'app/models/search.rb', line 314 def prepend_custom_column(*) column_name = .first.to_s composite_column(*) prepend_output_column(column_name) end |
#prepend_output_column(column_name) ⇒ Object
325 326 327 328 |
# File 'app/models/search.rb', line 325 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).
454 455 456 |
# File 'app/models/search.rb', line 454 def ransack_probe_params query_params.except(*DISTANCE_SEARCH_KEYS, *self.class.custom_criteria_keys) end |
#record_list(resource_ids) ⇒ Object
624 625 626 627 628 629 630 631 632 633 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 |
# File 'app/models/search.rb', line 624 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.where(search_id: id).delete_all # 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.connection.execute(sql) else return if resource_ids.blank? search_results = [] resource_ids.each do |raw_resource_id| resource_type, resource_id = raw_resource_id.split('|') search_results << SearchResult.new( resource_type: resource_type, resource_id: resource_id, search_id: id ) end require 'activerecord-import/base' require 'activerecord-import/active_record/adapters/postgresql_adapter' SearchResult.import search_results, validate: false, on_duplicate_key_ignore: true 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.
690 691 692 693 694 695 696 697 698 699 700 701 702 |
# File 'app/models/search.rb', line 690 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).pluck(:id) stale_ids = pinned_ids - matching_ids return 0 if stale_ids.empty? search_results.where(resource_type: main_type, resource_id: stale_ids).delete_all if pinned stale_ids.size end |
#remove_other_pins ⇒ Object (protected)
898 899 900 901 902 |
# File 'app/models/search.rb', line 898 def remove_other_pins return unless employee && pinned_changed? && pinned employee.searches.where(pinned: true).update_all(pinned: false) end |
#search_name ⇒ Object
713 714 715 |
# File 'app/models/search.rb', line 713 def search_name name || self.class.search_name end |
#search_results ⇒ ActiveRecord::Relation<SearchResult>
54 |
# File 'app/models/search.rb', line 54 has_many :search_results, inverse_of: :search, dependent: :delete_all |
#select_sort_columns ⇒ Object
Instance level list of possible sort columns
288 289 290 291 292 |
# File 'app/models/search.rb', line 288 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)
890 891 892 |
# File 'app/models/search.rb', line 890 def select_statement @sql_select_columns.join(',') end |
#set_defaults ⇒ Object
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 |
# File 'app/models/search.rb', line 809 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
555 556 557 558 559 560 561 562 563 564 565 |
# File 'app/models/search.rb', line 555 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_columns_for_select ⇒ Object
Retrieves a list of sort columns suitable for a select
581 582 583 584 585 586 587 588 589 590 |
# File 'app/models/search.rb', line 581 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 }
571 572 573 574 575 576 577 578 |
# File 'app/models/search.rb', line 571 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)
845 846 847 848 849 850 851 852 853 854 855 856 857 |
# File 'app/models/search.rb', line 845 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
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 |
# File 'app/models/search.rb', line 662 def unrecord_list(resource_ids) return if resource_ids.blank? if resource_ids == '*' SearchResult.where(search_id: id).delete_all 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.where(search_id: id, resource_type: resource_type, resource_id: resource_ids).delete_all 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.
298 299 300 |
# File 'app/models/search.rb', line 298 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
294 |
# File 'app/models/search.rb', line 294 delegate :view_resource_klass, to: :class |